File size: 3,328 Bytes
1da8cb0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { ProgressDisplay } from "./progress.js";

// Capture stdout writes instead of printing to terminal
let output: string[];
beforeEach(() => {
  output = [];
  vi.spyOn(process.stdout, "write").mockImplementation((data: string | Uint8Array) => {
    output.push(String(data));
    return true;
  });
  vi.useFakeTimers();
});

afterEach(() => {
  vi.restoreAllMocks();
  vi.useRealTimers();
});

describe("ProgressDisplay", () => {
  it("renders pending stages with blank time", () => {
    const p = new ProgressDisplay();
    p.addStage("Research");
    p.addStage("TTS");
    // Trigger render by starting stage 0
    p.start(0);
    const joined = output.join("");
    // Pending stage (TTS) should not show a time value
    expect(joined).toContain("TTS");
  });

  it("renders a spinner for running stages", () => {
    const p = new ProgressDisplay();
    p.addStage("Research");
    p.start(0);
    // The running stage should include a time like "0.0s"
    const joined = output.join("");
    expect(joined).toMatch(/\d+\.\d+s/);
  });

  it("starts a tick interval on start() and clears on complete()", () => {
    const p = new ProgressDisplay();
    const idx = p.addStage("Research");
    p.start(idx);

    // Advance fake timers — spinner should update
    const beforeLen = output.length;
    vi.advanceTimersByTime(160); // 2 ticks at 80ms
    expect(output.length).toBeGreaterThan(beforeLen);

    // Complete the stage — interval should stop
    p.complete(idx, "done");
    const afterComplete = output.length;
    vi.advanceTimersByTime(160);
    // No more renders after completion
    expect(output.length).toBe(afterComplete);
  });

  it("shows checkmark and duration for completed stages", () => {
    const p = new ProgressDisplay();
    const idx = p.addStage("Research");
    p.start(idx);
    p.complete(idx, "8 facts");
    const joined = output.join("");
    expect(joined).toContain("\u2714"); // checkmark
    expect(joined).toContain("8 facts");
  });

  it("shows X mark for failed stages", () => {
    const p = new ProgressDisplay();
    const idx = p.addStage("TTS");
    p.start(idx);
    p.fail(idx, "API error");
    const joined = output.join("");
    expect(joined).toContain("\u2718"); // X mark
    expect(joined).toContain("API error");
  });

  it("summary clears the interval and prints totals", () => {
    const logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
    const p = new ProgressDisplay();
    const idx = p.addStage("Research");
    p.start(idx);
    p.complete(idx);
    p.summary();
    expect(logSpy).toHaveBeenCalledWith(expect.stringContaining("Completed 1/1 stages"));
  });

  it("keeps ticking if one stage completes but another is still running", () => {
    const p = new ProgressDisplay();
    const a = p.addStage("Research");
    const b = p.addStage("TTS");
    p.start(a);
    p.start(b);
    p.complete(a, "done");

    // Stage b still running — ticks should continue
    const beforeLen = output.length;
    vi.advanceTimersByTime(160);
    expect(output.length).toBeGreaterThan(beforeLen);

    p.complete(b, "done");
    const afterComplete = output.length;
    vi.advanceTimersByTime(160);
    expect(output.length).toBe(afterComplete);
  });
});