From ccd7666ea3de5f85c60bd07079f6885c5a74281e Mon Sep 17 00:00:00 2001 From: Thomas Kosiewski Date: Tue, 16 Jun 2026 10:15:47 +0000 Subject: [PATCH] tests: isolate renderer test globals --- .../Tools/ProposePlanToolCall.test.tsx | 166 +++++++++++------- .../hooks/useModelsFromSettings.test.ts | 130 +++++++------- src/browser/hooks/useRouting.test.ts | 14 +- src/browser/utils/events.test.ts | 16 +- 4 files changed, 195 insertions(+), 131 deletions(-) diff --git a/src/browser/features/Tools/ProposePlanToolCall.test.tsx b/src/browser/features/Tools/ProposePlanToolCall.test.tsx index a9c341ba01..b2b1dda673 100644 --- a/src/browser/features/Tools/ProposePlanToolCall.test.tsx +++ b/src/browser/features/Tools/ProposePlanToolCall.test.tsx @@ -1,8 +1,15 @@ import type { ComponentProps } from "react"; -import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test"; +import { afterAll, afterEach, beforeEach, describe, expect, mock, test } from "bun:test"; import { installDom } from "../../../../tests/ui/dom"; import { cleanup, fireEvent, render, waitFor } from "@testing-library/react"; +import * as APIModule from "@/browser/contexts/API"; +import * as WorkspaceContextModule from "@/browser/contexts/WorkspaceContext"; +import * as UseOpenInEditorModule from "@/browser/hooks/useOpenInEditor"; +import * as UseReviewsModule from "@/browser/hooks/useReviews"; +import * as UseStartHereModule from "@/browser/hooks/useStartHere"; +import * as DiffRendererModule from "@/browser/features/Shared/DiffRenderer"; +import * as ReviewTypesModule from "@/common/types/review"; import type { SendMessageOptions } from "@/common/orpc/types"; import type { AgentDefinitionDescriptor } from "@/common/types/agentDefinition"; import { AgentProvider } from "@/browser/contexts/AgentContext"; @@ -87,70 +94,93 @@ const useStartHereMock = mock( } ); -void mock.module("@/browser/hooks/useStartHere", () => ({ - useStartHere: useStartHereMock, -})); - -void mock.module("@/browser/contexts/API", () => ({ - useAPI: () => ({ api: mockApi, status: "connected" as const, error: null }), -})); - -void mock.module("@/browser/hooks/useOpenInEditor", () => ({ - useOpenInEditor: () => () => Promise.resolve({ success: true } as const), -})); - -void mock.module("@/browser/contexts/WorkspaceContext", () => ({ - useWorkspaceContext: () => ({ - workspaceMetadata: new Map(), - }), -})); - -void mock.module("@/browser/hooks/useReviews", () => ({ - useReviews: () => ({ - reviews: [], - pendingCount: 0, - attachedCount: 0, - checkedCount: 0, - attachedReviews: [], - addReview: (data: unknown) => ({ - id: "test-review", - data, - status: "attached" as const, - createdAt: Date.now(), +const actualUseStartHereModule = { ...UseStartHereModule }; +const actualAPIModule = { ...APIModule }; +const actualUseOpenInEditorModule = { ...UseOpenInEditorModule }; +const actualWorkspaceContextModule = { ...WorkspaceContextModule }; +const actualUseReviewsModule = { ...UseReviewsModule }; +const actualDiffRendererModule = { ...DiffRendererModule }; +const actualReviewTypesModule = { ...ReviewTypesModule }; + +async function installProposePlanModuleMocks() { + await mock.module("@/browser/hooks/useStartHere", () => ({ + ...actualUseStartHereModule, + useStartHere: useStartHereMock, + })); + await mock.module("@/browser/contexts/API", () => ({ + ...actualAPIModule, + useAPI: () => ({ api: mockApi, status: "connected" as const, error: null }), + })); + await mock.module("@/browser/hooks/useOpenInEditor", () => ({ + ...actualUseOpenInEditorModule, + useOpenInEditor: () => () => Promise.resolve({ success: true } as const), + })); + await mock.module("@/browser/contexts/WorkspaceContext", () => ({ + ...actualWorkspaceContextModule, + useWorkspaceContext: () => ({ + workspaceMetadata: new Map(), }), - attachReview: () => undefined, - detachReview: () => undefined, - attachAllPending: () => undefined, - detachAllAttached: () => undefined, - checkReview: () => undefined, - uncheckReview: () => undefined, - removeReview: () => undefined, - updateReviewNote: () => undefined, - clearChecked: () => undefined, - clearAll: () => undefined, - getReview: () => undefined, - }), -})); - -void mock.module("@/browser/features/Shared/DiffRenderer", () => ({ - SelectableDiffRenderer: (props: { filePath?: string }) => { - selectableDiffRendererCalls.push({ filePath: props.filePath }); - return
; - }, -})); - -void mock.module("@/common/types/review", () => ({ - isPlanFilePath: (filePath: string) => /[/\\]plans[/\\]/.test(filePath), - normalizePlanFilePath: (filePath: string) => { - const normalizedPath = filePath.replace(/\\/g, "/"); - const tildeMuxMatch = /^~\/\.mux\/plans\/(.+)$/.exec(normalizedPath); - if (tildeMuxMatch?.[1]) { - return `.mux/plans/${tildeMuxMatch[1]}`; - } + })); + await mock.module("@/browser/hooks/useReviews", () => ({ + ...actualUseReviewsModule, + useReviews: () => ({ + reviews: [], + pendingCount: 0, + attachedCount: 0, + checkedCount: 0, + attachedReviews: [], + addReview: (data: unknown) => ({ + id: "test-review", + data, + status: "attached" as const, + createdAt: Date.now(), + }), + attachReview: () => undefined, + detachReview: () => undefined, + attachAllPending: () => undefined, + detachAllAttached: () => undefined, + checkReview: () => undefined, + uncheckReview: () => undefined, + removeReview: () => undefined, + updateReviewNote: () => undefined, + clearChecked: () => undefined, + clearAll: () => undefined, + getReview: () => undefined, + }), + })); + await mock.module("@/browser/features/Shared/DiffRenderer", () => ({ + ...actualDiffRendererModule, + SelectableDiffRenderer: (props: { filePath?: string }) => { + selectableDiffRendererCalls.push({ filePath: props.filePath }); + return
; + }, + })); + await mock.module("@/common/types/review", () => ({ + ...actualReviewTypesModule, + isPlanFilePath: (filePath: string) => /[/\\]plans[/\\]/.test(filePath), + normalizePlanFilePath: (filePath: string) => { + const normalizedPath = filePath.replace(/\\/g, "/"); + const tildeMuxMatch = /^~\/\.mux\/plans\/(.+)$/.exec(normalizedPath); + if (tildeMuxMatch?.[1]) { + return `.mux/plans/${tildeMuxMatch[1]}`; + } + + return normalizedPath; + }, + })); +} - return normalizedPath; - }, -})); +async function restoreProposePlanModuleMocks() { + // Bun's mock.module() has no disposer, and mock.restore() does not undo module mocks. + // Restore real exports so this test's renderer stubs do not leak into review suites. + await mock.module("@/browser/hooks/useStartHere", () => actualUseStartHereModule); + await mock.module("@/browser/contexts/API", () => actualAPIModule); + await mock.module("@/browser/hooks/useOpenInEditor", () => actualUseOpenInEditorModule); + await mock.module("@/browser/contexts/WorkspaceContext", () => actualWorkspaceContextModule); + await mock.module("@/browser/hooks/useReviews", () => actualUseReviewsModule); + await mock.module("@/browser/features/Shared/DiffRenderer", () => actualDiffRendererModule); + await mock.module("@/common/types/review", () => actualReviewTypesModule); +} const WORKSPACE_ID = "ws-123"; const PLAN_PATH = "~/.mux/plans/demo/ws-123.md"; @@ -278,15 +308,21 @@ function expectSingleQuoteRoot(view: { container: HTMLElement }, text: string) { describe("ProposePlanToolCall", () => { let cleanupDom: (() => void) | null = null; - beforeEach(() => { + afterAll(async () => { + await restoreProposePlanModuleMocks(); + }); + + beforeEach(async () => { startHereCalls = []; selectableDiffRendererCalls = []; mockApi = null; cleanupDom = installDom(); + await installProposePlanModuleMocks(); }); - afterEach(() => { + afterEach(async () => { cleanup(); + await restoreProposePlanModuleMocks(); mock.restore(); cleanupDom?.(); cleanupDom = null; diff --git a/src/browser/hooks/useModelsFromSettings.test.ts b/src/browser/hooks/useModelsFromSettings.test.ts index 76d16cc241..b3b88a25eb 100644 --- a/src/browser/hooks/useModelsFromSettings.test.ts +++ b/src/browser/hooks/useModelsFromSettings.test.ts @@ -1,6 +1,10 @@ -import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test"; +import { afterAll, afterEach, beforeEach, describe, expect, mock, test } from "bun:test"; import { act, cleanup, renderHook, waitFor } from "@testing-library/react"; -import { GlobalWindow } from "happy-dom"; +import * as APIModule from "@/browser/contexts/API"; +import * as PolicyContextModule from "@/browser/contexts/PolicyContext"; +import * as ProvidersConfigModule from "@/browser/hooks/useProvidersConfig"; +import * as RoutingModule from "@/browser/hooks/useRouting"; +import { installDom } from "../../../tests/ui/dom"; import { filterHiddenModels, getDefaultModel, @@ -78,24 +82,66 @@ const useRoutingMock = mock(() => ({ }, })); -void mock.module("@/browser/hooks/useProvidersConfig", () => ({ - useProvidersConfig: useProvidersConfigMock, -})); +const actualProvidersConfigModule = { ...ProvidersConfigModule }; +const actualRoutingModule = { ...RoutingModule }; +const actualAPIModule = { ...APIModule }; +const actualPolicyContextModule = { ...PolicyContextModule }; + +async function installUseModelsModuleMocks() { + await mock.module("@/browser/hooks/useProvidersConfig", () => ({ + ...actualProvidersConfigModule, + useProvidersConfig: useProvidersConfigMock, + })); + await mock.module("@/browser/hooks/useRouting", () => ({ + ...actualRoutingModule, + useRouting: useRoutingMock, + })); + await mock.module("@/browser/contexts/API", () => ({ + ...actualAPIModule, + useAPI: () => ({ api: apiMock }), + })); + await mock.module("@/browser/contexts/PolicyContext", () => ({ + ...actualPolicyContextModule, + usePolicy: () => ({ + status: { state: "disabled" as const }, + policy: null, + }), + })); +} -void mock.module("@/browser/hooks/useRouting", () => ({ - useRouting: useRoutingMock, -})); +async function restoreUseModelsModuleMocks() { + // Bun's mock.module() has no disposer, and mock.restore() does not undo module mocks. + // Restore the real modules so this file cannot poison later tests in the same process. + await mock.module("@/browser/hooks/useProvidersConfig", () => actualProvidersConfigModule); + await mock.module("@/browser/hooks/useRouting", () => actualRoutingModule); + await mock.module("@/browser/contexts/API", () => actualAPIModule); + await mock.module("@/browser/contexts/PolicyContext", () => actualPolicyContextModule); +} -void mock.module("@/browser/contexts/API", () => ({ - useAPI: () => ({ api: apiMock }), -})); +let cleanupDom: (() => void) | null = null; -void mock.module("@/browser/contexts/PolicyContext", () => ({ - usePolicy: () => ({ - status: { state: "disabled" as const }, - policy: null, - }), -})); +async function setupUseModelsHookTest() { + cleanupDom = installDom(); + window.localStorage.clear(); + providersConfig = null; + routePriority = ["direct"]; + routeOverrides = {}; + apiMock = null; + await installUseModelsModuleMocks(); +} + +async function cleanupUseModelsHookTest() { + cleanup(); + await restoreUseModelsModuleMocks(); + mock.restore(); + cleanupDom?.(); + cleanupDom = null; + apiMock = null; +} + +afterAll(async () => { + await restoreUseModelsModuleMocks(); +}); describe("getSuggestedModels", () => { test("returns custom models first, then built-ins (deduped)", () => { @@ -169,22 +215,8 @@ describe("filterHiddenModels", () => { }); describe("useModelsFromSettings selected model preservation", () => { - beforeEach(() => { - globalThis.window = new GlobalWindow() as unknown as Window & typeof globalThis; - globalThis.document = globalThis.window.document; - globalThis.window.localStorage.clear(); - providersConfig = null; - routePriority = ["direct"]; - routeOverrides = {}; - apiMock = null; - }); - - afterEach(() => { - cleanup(); - globalThis.window = undefined as unknown as Window & typeof globalThis; - globalThis.document = undefined as unknown as Document; - apiMock = null; - }); + beforeEach(setupUseModelsHookTest); + afterEach(cleanupUseModelsHookTest); test("getDefaultModel preserves explicit gateway-scoped defaults", () => { const gatewayModel = "openrouter:openai/gpt-5"; @@ -312,20 +344,8 @@ describe("useModelsFromSettings selected model preservation", () => { }); describe("useModelsFromSettings OpenAI Codex OAuth gating", () => { - beforeEach(() => { - globalThis.window = new GlobalWindow() as unknown as Window & typeof globalThis; - globalThis.document = globalThis.window.document; - globalThis.window.localStorage.clear(); - providersConfig = null; - routePriority = ["direct"]; - routeOverrides = {}; - }); - - afterEach(() => { - cleanup(); - globalThis.window = undefined as unknown as Window & typeof globalThis; - globalThis.document = undefined as unknown as Document; - }); + beforeEach(setupUseModelsHookTest); + afterEach(cleanupUseModelsHookTest); test("codex oauth only: shows OAuth-routable OpenAI models and hides API-key-only ones", () => { providersConfig = { @@ -442,20 +462,8 @@ describe("useModelsFromSettings OpenAI Codex OAuth gating", () => { }); describe("useModelsFromSettings provider availability gating", () => { - beforeEach(() => { - globalThis.window = new GlobalWindow() as unknown as Window & typeof globalThis; - globalThis.document = globalThis.window.document; - globalThis.window.localStorage.clear(); - providersConfig = null; - routePriority = ["direct"]; - routeOverrides = {}; - }); - - afterEach(() => { - cleanup(); - globalThis.window = undefined as unknown as Window & typeof globalThis; - globalThis.document = undefined as unknown as Document; - }); + beforeEach(setupUseModelsHookTest); + afterEach(cleanupUseModelsHookTest); test("hides models from unconfigured providers", () => { providersConfig = { diff --git a/src/browser/hooks/useRouting.test.ts b/src/browser/hooks/useRouting.test.ts index 029531124e..0e7b6362f9 100644 --- a/src/browser/hooks/useRouting.test.ts +++ b/src/browser/hooks/useRouting.test.ts @@ -49,9 +49,15 @@ const wrapper: React.FC<{ children: React.ReactNode }> = (props) => ); describe("useRouting", () => { + let previousWindow: typeof globalThis.window; + let previousDocument: typeof globalThis.document; + let testWindow: GlobalWindow | null = null; + beforeEach(() => { - globalThis.window = new GlobalWindow({ url: "https://mux.example.com/" }) as unknown as Window & - typeof globalThis; + previousWindow = globalThis.window; + previousDocument = globalThis.document; + testWindow = new GlobalWindow({ url: "https://mux.example.com/" }); + globalThis.window = testWindow as unknown as Window & typeof globalThis; globalThis.document = globalThis.window.document; providersConfig = null; routePriority = ["direct"]; @@ -62,6 +68,10 @@ describe("useRouting", () => { afterEach(() => { cleanup(); getProvidersConfigStore().setClient(null); + testWindow?.close(); + testWindow = null; + globalThis.window = previousWindow; + globalThis.document = previousDocument; }); test("resolveRoute and availableRoutes honor gateway model accessibility", async () => { diff --git a/src/browser/utils/events.test.ts b/src/browser/utils/events.test.ts index 1e86ba3aa8..93f7075a4f 100644 --- a/src/browser/utils/events.test.ts +++ b/src/browser/utils/events.test.ts @@ -1,9 +1,19 @@ -import "../../../tests/ui/dom"; - -import { describe, expect, test } from "bun:test"; +import { afterEach, beforeEach, describe, expect, test } from "bun:test"; +import { installDom } from "../../../tests/ui/dom"; import { isEventFromDialogPortal } from "./events"; describe("isEventFromDialogPortal", () => { + let cleanupDom: (() => void) | null = null; + + beforeEach(() => { + cleanupDom = installDom(); + }); + + afterEach(() => { + cleanupDom?.(); + cleanupDom = null; + }); + test("detects targets inside role=dialog ancestors", () => { const dialog = document.createElement("div"); dialog.setAttribute("role", "dialog");