Skip to content

Commit cea59b2

Browse files
committed
🤖 tests: share browser global test helpers
Extract shared browser test helpers for window and navigator mocking, then use them in GitStatusStore and keybind tests to remove duplicate descriptor cleanup logic. --- _Generated with `mux` • Model: `openai:gpt-5.5` • Thinking: `xhigh` • Cost: `$223.75`_ <!-- mux-attribution: model=openai:gpt-5.5 thinking=xhigh costs=223.75 -->
1 parent f15ddef commit cea59b2

3 files changed

Lines changed: 144 additions & 136 deletions

File tree

‎src/browser/stores/GitStatusStore.test.ts‎

Lines changed: 14 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
import type { RuntimeStatus, RuntimeStatusStore } from "./RuntimeStatusStore";
1212
import type { FrontendWorkspaceMetadata, GitStatus } from "@/common/types/workspace";
1313
import { DEFAULT_RUNTIME_CONFIG } from "@/common/constants/workspace";
14+
import { installTestWindow } from "@/browser/testUtils";
1415

1516
/**
1617
* Unit tests for GitStatusStore.
@@ -183,91 +184,11 @@ function createStore(
183184
return store;
184185
}
185186

186-
type MockableWindow = Window & typeof globalThis & { api?: unknown };
187-
type WindowEventMethod = "addEventListener" | "removeEventListener" | "dispatchEvent";
188-
189-
let mockedWindow: MockableWindow | undefined;
190-
let createdMockWindow = false;
191-
let previousApiDescriptor: PropertyDescriptor | undefined;
192-
let previousEventMethodDescriptors: Partial<
193-
Record<WindowEventMethod, PropertyDescriptor | undefined>
194-
> = {};
195-
196-
function installMockWindowAPI() {
197-
const existingWindow = globalThis.window as MockableWindow | undefined;
198-
const targetWindow = existingWindow ?? (Object.create(null) as MockableWindow);
199-
200-
mockedWindow = targetWindow;
201-
createdMockWindow = existingWindow == null;
202-
previousApiDescriptor = Object.getOwnPropertyDescriptor(targetWindow, "api");
203-
previousEventMethodDescriptors = {};
204-
205-
if (createdMockWindow) {
206-
globalThis.window = targetWindow;
207-
}
208-
209-
Object.defineProperty(targetWindow, "api", {
210-
configurable: true,
211-
value: {
212-
workspace: {
213-
executeBash: mockExecuteBash,
214-
getProjectGitStatuses: mockGetProjectGitStatuses,
215-
},
216-
},
217-
});
218-
219-
ensureWindowEventMethod(targetWindow, "addEventListener", () => undefined);
220-
ensureWindowEventMethod(targetWindow, "removeEventListener", () => undefined);
221-
ensureWindowEventMethod(targetWindow, "dispatchEvent", () => true);
222-
}
223-
224-
function ensureWindowEventMethod(
225-
targetWindow: MockableWindow,
226-
method: WindowEventMethod,
227-
replacement: EventTarget[WindowEventMethod]
228-
) {
229-
if (typeof targetWindow[method] === "function") {
230-
return;
231-
}
232-
233-
previousEventMethodDescriptors[method] = Object.getOwnPropertyDescriptor(targetWindow, method);
234-
Object.defineProperty(targetWindow, method, { configurable: true, value: replacement });
235-
}
236-
237-
function restoreMockWindowAPI() {
238-
const targetWindow = mockedWindow;
239-
if (!targetWindow) {
240-
return;
241-
}
242-
243-
if (previousApiDescriptor) {
244-
Object.defineProperty(targetWindow, "api", previousApiDescriptor);
245-
} else {
246-
delete targetWindow.api;
247-
}
248-
249-
for (const method of Object.keys(previousEventMethodDescriptors) as WindowEventMethod[]) {
250-
const descriptor = previousEventMethodDescriptors[method];
251-
if (descriptor) {
252-
Object.defineProperty(targetWindow, method, descriptor);
253-
} else {
254-
delete (targetWindow as Partial<Record<WindowEventMethod, unknown>>)[method];
255-
}
256-
}
257-
258-
if (createdMockWindow && globalThis.window === targetWindow) {
259-
delete (globalThis as { window?: unknown }).window;
260-
}
261-
262-
mockedWindow = undefined;
263-
createdMockWindow = false;
264-
previousApiDescriptor = undefined;
265-
previousEventMethodDescriptors = {};
266-
}
267-
268187
describe("GitStatusStore", () => {
269188
let store: GitStatusStore;
270189

190+
let restoreTestWindow: (() => void) | undefined;
191+
271192
beforeEach(() => {
272193
mockExecuteBash.mockReset();
273194
mockGetProjectGitStatuses.mockReset();
@@ -282,14 +203,23 @@ describe("GitStatusStore", () => {
282203
} as Result<BashToolResult, string>);
283204
mockGetProjectGitStatuses.mockResolvedValue([]);
284205

285-
installMockWindowAPI();
206+
restoreTestWindow = installTestWindow({
207+
api: {
208+
workspace: {
209+
executeBash: mockExecuteBash,
210+
getProjectGitStatuses: mockGetProjectGitStatuses,
211+
},
212+
},
213+
ensureEventTargetMethods: true,
214+
}).restore;
286215

287216
store = createStore();
288217
});
289218

290219
afterEach(() => {
291220
store.dispose();
292-
restoreMockWindowAPI();
221+
restoreTestWindow?.();
222+
restoreTestWindow = undefined;
293223
});
294224

295225
test("subscribe and unsubscribe", () => {

‎src/browser/testUtils.ts‎

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,111 @@ export function requireTestModule<T>(modulePath: string): T {
1414
return requireForTest(modulePath) as T;
1515
}
1616

17+
type TestWindowEventMethod = "addEventListener" | "removeEventListener" | "dispatchEvent";
18+
19+
export type TestWindowWithApi = Window & typeof globalThis & { api?: unknown };
20+
21+
interface InstallTestWindowOptions {
22+
api?: unknown;
23+
ensureEventTargetMethods?: boolean;
24+
}
25+
26+
interface InstalledTestWindow {
27+
window: TestWindowWithApi;
28+
restore: () => void;
29+
}
30+
31+
const TEST_WINDOW_EVENT_METHODS: Record<TestWindowEventMethod, EventTarget[TestWindowEventMethod]> =
32+
{
33+
addEventListener: () => undefined,
34+
removeEventListener: () => undefined,
35+
dispatchEvent: () => true,
36+
};
37+
38+
export function installTestWindow(options: InstallTestWindowOptions = {}): InstalledTestWindow {
39+
const existingWindow = globalThis.window as TestWindowWithApi | undefined;
40+
const targetWindow = existingWindow ?? (Object.create(null) as TestWindowWithApi);
41+
const createdWindow = existingWindow == null;
42+
const previousApiDescriptor = Object.getOwnPropertyDescriptor(targetWindow, "api");
43+
const previousEventMethodDescriptors = new Map<
44+
TestWindowEventMethod,
45+
PropertyDescriptor | undefined
46+
>();
47+
48+
if (createdWindow) {
49+
globalThis.window = targetWindow;
50+
}
51+
52+
if ("api" in options) {
53+
Object.defineProperty(targetWindow, "api", {
54+
configurable: true,
55+
value: options.api,
56+
});
57+
}
58+
59+
if (options.ensureEventTargetMethods) {
60+
for (const [method, replacement] of Object.entries(TEST_WINDOW_EVENT_METHODS) as Array<
61+
[TestWindowEventMethod, EventTarget[TestWindowEventMethod]]
62+
>) {
63+
if (typeof targetWindow[method] === "function") {
64+
continue;
65+
}
66+
67+
previousEventMethodDescriptors.set(
68+
method,
69+
Object.getOwnPropertyDescriptor(targetWindow, method)
70+
);
71+
Object.defineProperty(targetWindow, method, { configurable: true, value: replacement });
72+
}
73+
}
74+
75+
let restored = false;
76+
return {
77+
window: targetWindow,
78+
restore() {
79+
if (restored) {
80+
return;
81+
}
82+
restored = true;
83+
84+
if (previousApiDescriptor) {
85+
Object.defineProperty(targetWindow, "api", previousApiDescriptor);
86+
} else {
87+
delete targetWindow.api;
88+
}
89+
90+
for (const [method, descriptor] of previousEventMethodDescriptors) {
91+
if (descriptor) {
92+
Object.defineProperty(targetWindow, method, descriptor);
93+
} else {
94+
delete (targetWindow as Partial<Record<TestWindowEventMethod, unknown>>)[method];
95+
}
96+
}
97+
98+
if (createdWindow && globalThis.window === targetWindow) {
99+
delete (globalThis as { window?: unknown }).window;
100+
}
101+
},
102+
};
103+
}
104+
105+
export function installTestNavigator(navigator: Navigator): () => void {
106+
const previousNavigator = globalThis.navigator;
107+
globalThis.navigator = navigator;
108+
109+
return () => {
110+
if (globalThis.navigator !== navigator) {
111+
return;
112+
}
113+
114+
if (previousNavigator) {
115+
globalThis.navigator = previousNavigator;
116+
} else {
117+
delete (globalThis as { navigator?: unknown }).navigator;
118+
}
119+
};
120+
}
121+
17122
/**
18123
* Helper type for recursive partial mocks.
19124
* Allows partial mocking of nested objects and async functions.

‎src/browser/utils/ui/keybinds.test.ts‎

Lines changed: 25 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,77 +1,50 @@
11
import { afterEach, describe, it, expect, test } from "bun:test";
2+
import {
3+
installTestNavigator,
4+
installTestWindow,
5+
type TestWindowWithApi,
6+
} from "@/browser/testUtils";
27
import { isMac, matchesKeybind, KEYBINDS } from "./keybinds";
38
import type { Keybind } from "@/common/types/keybind";
49

5-
type PlatformWindow = Window & typeof globalThis & { api?: unknown };
6-
7-
let mockedWindow: PlatformWindow | undefined;
8-
let createdMockWindow = false;
9-
let previousApiDescriptor: PropertyDescriptor | undefined;
10-
let previousNavigator: Navigator | undefined;
11-
let mockedNavigator: Navigator | undefined;
10+
let testWindow: TestWindowWithApi | undefined;
11+
let restoreTestWindow: (() => void) | undefined;
12+
let restoreTestNavigator: (() => void) | undefined;
1213

1314
function setPlatform(platform: "darwin" | "linux") {
14-
const targetWindow = ensureWindow();
15-
Object.defineProperty(targetWindow, "api", {
15+
Object.defineProperty(ensureWindow(), "api", {
1616
configurable: true,
1717
value: { platform },
1818
});
1919
}
2020

2121
function clearWindowAPI() {
22-
const targetWindow = ensureWindow();
23-
delete targetWindow.api;
22+
delete ensureWindow().api;
2423
}
2524

26-
function ensureWindow(): PlatformWindow {
27-
if (mockedWindow) {
28-
return mockedWindow;
29-
}
30-
31-
const existingWindow = globalThis.window as PlatformWindow | undefined;
32-
mockedWindow = existingWindow ?? (Object.create(null) as PlatformWindow);
33-
createdMockWindow = existingWindow == null;
34-
previousApiDescriptor = Object.getOwnPropertyDescriptor(mockedWindow, "api");
35-
36-
if (createdMockWindow) {
37-
globalThis.window = mockedWindow;
25+
function ensureWindow(): TestWindowWithApi {
26+
if (!testWindow) {
27+
const installedWindow = installTestWindow();
28+
testWindow = installedWindow.window;
29+
restoreTestWindow = installedWindow.restore;
3830
}
3931

40-
return mockedWindow;
32+
return testWindow;
4133
}
4234

4335
function setNavigatorPlatform(platform: string) {
44-
previousNavigator = globalThis.navigator;
45-
mockedNavigator = { platform, userAgent: "Mozilla/5.0" } as unknown as Navigator;
46-
globalThis.navigator = mockedNavigator;
36+
restoreTestNavigator = installTestNavigator({
37+
platform,
38+
userAgent: "Mozilla/5.0",
39+
} as unknown as Navigator);
4740
}
4841

4942
afterEach(() => {
50-
if (mockedWindow) {
51-
if (previousApiDescriptor) {
52-
Object.defineProperty(mockedWindow, "api", previousApiDescriptor);
53-
} else {
54-
delete mockedWindow.api;
55-
}
56-
57-
if (createdMockWindow && globalThis.window === mockedWindow) {
58-
delete (globalThis as { window?: unknown }).window;
59-
}
60-
}
61-
62-
if (mockedNavigator && globalThis.navigator === mockedNavigator) {
63-
if (previousNavigator) {
64-
globalThis.navigator = previousNavigator;
65-
} else {
66-
delete (globalThis as { navigator?: unknown }).navigator;
67-
}
68-
}
69-
70-
mockedWindow = undefined;
71-
createdMockWindow = false;
72-
previousApiDescriptor = undefined;
73-
previousNavigator = undefined;
74-
mockedNavigator = undefined;
43+
restoreTestNavigator?.();
44+
restoreTestWindow?.();
45+
testWindow = undefined;
46+
restoreTestWindow = undefined;
47+
restoreTestNavigator = undefined;
7548
});
7649

7750
// Helper to create a minimal keyboard event

0 commit comments

Comments
 (0)