diff --git a/src/tui/list-modal.test.ts b/src/tui/list-modal.test.ts index ffe6af16..d965b4a5 100644 --- a/src/tui/list-modal.test.ts +++ b/src/tui/list-modal.test.ts @@ -1,20 +1,27 @@ -import { describe, expect, test } from "bun:test"; +import { afterEach, describe, expect, test } from "bun:test"; -import { createHarness } from "./harness.js"; +import { createHarness, type Harness } from "./harness.js"; import { runListModal } from "./list-modal.js"; +let harness: Harness | undefined; + +afterEach(() => { + harness?.destroy(); + harness = undefined; +}); + async function mountModal(): Promise<{ choice: Promise; - harness: Awaited>; + harness: Harness; }> { - const harness = await createHarness({ width: 80, height: 24 }); + harness = await createHarness({ width: 80, height: 24 }); const choice = runListModal({ title: "resume session", options: [ { id: "s-1", label: "First session" }, { id: "s-2", label: "Second session" }, ], - createRenderer: async () => harness.renderer, + createRenderer: async () => harness!.renderer, }); await harness.renderOnce(); return { choice, harness }; diff --git a/src/tui/mouse-reporting-disabled.test.ts b/src/tui/mouse-reporting-disabled.test.ts index ae1071c4..a687339b 100644 --- a/src/tui/mouse-reporting-disabled.test.ts +++ b/src/tui/mouse-reporting-disabled.test.ts @@ -7,7 +7,7 @@ * `createCliRenderer` branch runs, and assert on the options it was * actually called with. */ -import { afterAll, describe, expect, mock, test } from "bun:test"; +import { afterAll, afterEach, describe, expect, mock, test } from "bun:test"; import type { Harness } from "./harness.js"; interface CapturedRendererOptions { @@ -49,6 +49,11 @@ afterAll(() => { mock.module("@opentui/core", () => realCore); }); +afterEach(() => { + let harness: Harness | undefined; + while ((harness = mountedHarnesses.pop()) !== undefined) harness.destroy(); +}); + const { runListModal } = await import("./list-modal.js"); const { runProviderSetup } = await import("./provider-setup.js"); @@ -71,7 +76,6 @@ describe("default renderer mount disables DEC mouse reporting (CL-5540)", () => expect(capturedOptions).toHaveLength(1); expect(capturedOptions[0]?.useMouse).toBe(false); expect(capturedOptions[0]?.enableMouseMovement).toBe(false); - mountedHarnesses.pop()?.destroy(); }); test("runProviderSetup", async () => { diff --git a/src/tui/provider-connect.test.ts b/src/tui/provider-connect.test.ts index 2dc282f5..71030416 100644 --- a/src/tui/provider-connect.test.ts +++ b/src/tui/provider-connect.test.ts @@ -1,9 +1,9 @@ -import { describe, test, expect } from "bun:test"; +import { afterEach, describe, test, expect } from "bun:test"; import { mkdtemp, rm } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; -import { createHarness } from "./harness.js"; +import { createHarness, type Harness } from "./harness.js"; import { connectProviderInline } from "./provider-connect.js"; import { loadSettings } from "../config/settings.js"; @@ -12,17 +12,24 @@ import { loadSettings } from "../config/settings.js"; // pins that an empty key on a key-required preset is rejected here too, // rather than silently downgraded to a keyless credential. describe("connectProviderInline", () => { + let harness: Harness | undefined; + + afterEach(() => { + harness?.destroy(); + harness = undefined; + }); + test("rejects an empty key on a key-required preset without persisting", async () => { const dir = await mkdtemp(join(tmpdir(), "provider-connect-")); const settingsPath = join(dir, "settings.json"); try { - const harness = await createHarness({ width: 80, height: 30 }); + harness = await createHarness({ width: 80, height: 30 }); const resultPromise = connectProviderInline({ providerId: "openai", settingsPath, localSettingsPath: join(dir, "local.json"), existing: null, - createRenderer: async () => harness.renderer, + createRenderer: async () => harness!.renderer, }); await harness.renderOnce(); diff --git a/src/tui/provider-setup.test.ts b/src/tui/provider-setup.test.ts index a3d40fb3..a63ad0b9 100644 --- a/src/tui/provider-setup.test.ts +++ b/src/tui/provider-setup.test.ts @@ -1,6 +1,6 @@ -import { describe, expect, test } from "bun:test"; +import { afterEach, describe, expect, test } from "bun:test"; -import { createHarness, type Harness } from "./harness.js"; +import { createHarness as createRawHarness, type Harness } from "./harness.js"; import { addProviderSelectorChoices, connectedAccountCount, @@ -41,6 +41,20 @@ const EMPTY: ProviderFormValues = { oauthProfile: "", }; +// This file mounts a fresh renderer per test; track every one so a single +// afterEach can free them regardless of which assertion in a test fails. +const activeHarnesses: Harness[] = []; + +async function createHarness(opts: { width: number; height: number }): Promise { + const harness = await createRawHarness(opts); + activeHarnesses.push(harness); + return harness; +} + +afterEach(() => { + while (activeHarnesses.length > 0) activeHarnesses.pop()!.destroy(); +}); + describe("provider setup pure helpers", () => { test("only the API key may be left blank", () => { expect(stepReady("name", "")).toBe(false); @@ -1047,23 +1061,19 @@ describe("runProviderSetup paste", () => { const { done, harness } = await mountSetup(async (values) => { seen = values; }); - try { - await pickRow(harness, PROVIDER_IDS, "openai"); - await flush(harness); - harness.pressKey("Enter"); - await harness.renderOnce(); - await harness.mockInput.pasteBracketedText(key); - await harness.renderOnce(); - const frame = harness.captureCharFrame(); - harness.pressKey("Enter"); - await harness.renderOnce(); - harness.pressKey("Enter"); - await harness.renderOnce(); - await done; - return { values: seen, frame }; - } finally { - harness.destroy(); - } + await pickRow(harness, PROVIDER_IDS, "openai"); + await flush(harness); + harness.pressKey("Enter"); + await harness.renderOnce(); + await harness.mockInput.pasteBracketedText(key); + await harness.renderOnce(); + const frame = harness.captureCharFrame(); + harness.pressKey("Enter"); + await harness.renderOnce(); + harness.pressKey("Enter"); + await harness.renderOnce(); + await done; + return { values: seen, frame }; } test("a pasted key lands in the field, never echoed in the clear", async () => {