From ea2b626c981632b493d927c77a4c9e89e63cc7d1 Mon Sep 17 00:00:00 2001 From: Flux Labs Date: Sun, 15 Mar 2026 09:56:50 -0500 Subject: [PATCH] test(pi-tui): add regression tests for loader, cancellable-loader, input First test coverage for pi-tui components (8 tests): - Loader: start() idempotency, dispose() cleanup, stop() safety - CancellableLoader: abort on dispose, callback cleanup, signal state - Input: paste buffer reset on focus loss, focused getter/setter --- .../__tests__/cancellable-loader.test.ts | 45 +++++++++++++++++++ .../src/components/__tests__/input.test.ts | 35 +++++++++++++++ .../src/components/__tests__/loader.test.ts | 45 +++++++++++++++++++ 3 files changed, 125 insertions(+) create mode 100644 packages/pi-tui/src/components/__tests__/cancellable-loader.test.ts create mode 100644 packages/pi-tui/src/components/__tests__/input.test.ts create mode 100644 packages/pi-tui/src/components/__tests__/loader.test.ts diff --git a/packages/pi-tui/src/components/__tests__/cancellable-loader.test.ts b/packages/pi-tui/src/components/__tests__/cancellable-loader.test.ts new file mode 100644 index 000000000..4f7889402 --- /dev/null +++ b/packages/pi-tui/src/components/__tests__/cancellable-loader.test.ts @@ -0,0 +1,45 @@ +// pi-tui CancellableLoader component regression tests +// Copyright (c) 2026 Jeremy McSpadden + +import { describe, it, mock, beforeEach, afterEach } from "node:test"; +import assert from "node:assert/strict"; +import { CancellableLoader } from "../cancellable-loader.js"; + +function makeMockTUI() { + return { requestRender: mock.fn() } as any; +} + +describe("CancellableLoader", () => { + let loader: CancellableLoader; + let tui: ReturnType; + + beforeEach(() => { + tui = makeMockTUI(); + }); + + afterEach(() => { + loader?.dispose(); + }); + + it("dispose() aborts the AbortController signal", () => { + loader = new CancellableLoader(tui, (s) => s, (s) => s, "test"); + assert.equal(loader.aborted, false); + loader.dispose(); + assert.equal(loader.aborted, true); + }); + + it("dispose() clears the onAbort callback", () => { + loader = new CancellableLoader(tui, (s) => s, (s) => s, "test"); + loader.onAbort = () => {}; + loader.dispose(); + assert.equal(loader.onAbort, undefined); + }); + + it("signal is aborted after dispose()", () => { + loader = new CancellableLoader(tui, (s) => s, (s) => s, "test"); + const signal = loader.signal; + assert.equal(signal.aborted, false); + loader.dispose(); + assert.equal(signal.aborted, true); + }); +}); diff --git a/packages/pi-tui/src/components/__tests__/input.test.ts b/packages/pi-tui/src/components/__tests__/input.test.ts new file mode 100644 index 000000000..c47100492 --- /dev/null +++ b/packages/pi-tui/src/components/__tests__/input.test.ts @@ -0,0 +1,35 @@ +// pi-tui Input component regression tests +// Copyright (c) 2026 Jeremy McSpadden + +import { describe, it } from "node:test"; +import assert from "node:assert/strict"; +import { Input } from "../input.js"; + +describe("Input", () => { + it("paste buffer is cleared when focus is lost", () => { + const input = new Input(); + input.focused = true; + + // Simulate starting a paste (bracket paste start marker) + input.handleInput("\x1b[200~partial"); + + // Now lose focus mid-paste + input.focused = false; + + // Regain focus — should not have stale paste state + input.focused = true; + + // Typing normal text should work without paste buffer corruption + input.handleInput("hello"); + assert.equal(input.getValue(), "hello"); + }); + + it("focused getter/setter works correctly", () => { + const input = new Input(); + assert.equal(input.focused, false); + input.focused = true; + assert.equal(input.focused, true); + input.focused = false; + assert.equal(input.focused, false); + }); +}); diff --git a/packages/pi-tui/src/components/__tests__/loader.test.ts b/packages/pi-tui/src/components/__tests__/loader.test.ts new file mode 100644 index 000000000..9c22056fa --- /dev/null +++ b/packages/pi-tui/src/components/__tests__/loader.test.ts @@ -0,0 +1,45 @@ +// pi-tui Loader component regression tests +// Copyright (c) 2026 Jeremy McSpadden + +import { describe, it, mock, beforeEach, afterEach } from "node:test"; +import assert from "node:assert/strict"; +import { Loader } from "../loader.js"; + +function makeMockTUI() { + return { requestRender: mock.fn() } as any; +} + +describe("Loader", () => { + let loader: Loader; + let tui: ReturnType; + + beforeEach(() => { + tui = makeMockTUI(); + }); + + afterEach(() => { + loader?.stop(); + }); + + it("start() is idempotent — calling twice does not leak intervals", () => { + loader = new Loader(tui, (s) => s, (s) => s, "test"); + // Constructor calls start() once, call it again + loader.start(); + // stop() should clear the interval cleanly without orphaned timers + loader.stop(); + }); + + it("dispose() stops the interval and nulls the TUI reference", () => { + loader = new Loader(tui, (s) => s, (s) => s, "test"); + loader.dispose(); + // After dispose, calling stop() again should be safe (no-op) + loader.stop(); + }); + + it("stop() is safe to call multiple times", () => { + loader = new Loader(tui, (s) => s, (s) => s, "test"); + loader.stop(); + loader.stop(); + loader.stop(); + }); +});