import { visibleWidth } from "@earendil-works/pi-tui"; import { describe, expect, it, vi } from "vitest"; import { createAfkWidget } from "../src/widget.ts"; describe("AFK widget", () => { it("preserves blink cadence across invalidation", () => { vi.useFakeTimers(); const widget = createAfkWidget( { requestRender() {} } as never, { fg: (_color: string, text: string) => text } as never, () => false, ); try { for (const _tick of [1, 2]) { vi.advanceTimersByTime(700); expect(widget.render(80).join("\n")).toContain("o.o"); } widget.invalidate(); vi.advanceTimersByTime(700); expect(widget.render(80).join("\n")).toContain("-.-"); vi.advanceTimersByTime(700); expect(widget.render(80).join("\n")).toContain("o.o"); } finally { widget.dispose(); vi.useRealTimers(); } }); it("resumes animation when new work wakes the cat", () => { vi.useFakeTimers(); let napping = true; const requestRender = vi.fn(); const widget = createAfkWidget( { requestRender } as never, { fg: (_color: string, text: string) => text } as never, () => napping, ); try { expect(widget.render(80).join("\n")).toContain("zzz"); vi.advanceTimersByTime(5_600); napping = false; widget.invalidate(); expect(widget.render(80).join("\n")).toContain("o.o"); vi.advanceTimersByTime(2_100); expect(widget.render(80).join("\n")).toContain("-.-"); } finally { widget.dispose(); vi.useRealTimers(); } }); it("redraws only blink transitions and has no timer while napping", () => { vi.useFakeTimers(); let napping = false; const requestRender = vi.fn(); const widget = createAfkWidget( { requestRender } as never, { fg: (_color: string, text: string) => text } as never, () => napping, ); try { vi.advanceTimersByTime(1_400); expect(requestRender).not.toHaveBeenCalled(); vi.advanceTimersByTime(1_400); expect(requestRender).toHaveBeenCalledTimes(2); napping = true; widget.invalidate(); expect(vi.getTimerCount()).toBe(0); const asleepRenders = requestRender.mock.calls.length; vi.advanceTimersByTime(60_000); expect(requestRender).toHaveBeenCalledTimes(asleepRenders); napping = false; widget.invalidate(); expect(vi.getTimerCount()).toBe(1); widget.invalidate(); expect(vi.getTimerCount()).toBe(1); } finally { widget.dispose(); vi.useRealTimers(); } }); it("blinks, naps, fits widths, bounds frames, and disposes idempotently", () => { vi.useFakeTimers(); try { const tui = { requestRender: vi.fn() }; let napping = false; const widget = createAfkWidget( tui as never, { fg: (_color: string, text: string) => `\u001b[36m${text}\u001b[0m`, } as never, () => napping, ); for (const width of [0, 1, 7, 12, 80]) expect( widget.render(width).every((line) => visibleWidth(line) <= width), ).toBe(true); expect(widget.render(80).join("\\n")).toContain("o.o"); vi.advanceTimersByTime(2_100); expect(widget.render(80).join("\\n")).toContain("-.-"); vi.advanceTimersByTime(700 * 1_001); expect(widget.render(80).join("\\n")).toContain("o.o"); napping = true; expect(widget.render(80).join("\\n")).toContain("zzz"); const renders = tui.requestRender.mock.calls.length; widget.dispose(); widget.dispose(); vi.advanceTimersByTime(1_400); expect(tui.requestRender).toHaveBeenCalledTimes(renders); expect(vi.getTimerCount()).toBe(0); } finally { vi.useRealTimers(); } }); });