Skip to content

Commit 08f28f4

Browse files
committed
fix: stabilize bun tests for timers and module mocks
Change-Id: Ib691f6831627e0e03ecfb26339a6bd9b4a4c310c Signed-off-by: Thomas Kosiewski <tk@coder.com>
1 parent 57ea1e5 commit 08f28f4

File tree

4 files changed

+37
-33
lines changed

4 files changed

+37
-33
lines changed

src/browser/contexts/API.test.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ void mock.module("@orpc/client/message-port", () => ({
6666
}));
6767

6868
void mock.module("@/browser/components/AuthTokenModal", () => ({
69+
AuthTokenModal: () => null,
6970
getStoredAuthToken: () => null,
7071
// eslint-disable-next-line @typescript-eslint/no-empty-function
7172
clearStoredAuthToken: () => {},

src/browser/hooks/useVoiceInput.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,8 @@ export interface UseVoiceInputResult {
5757
*/
5858
function hasTouchDictation(): boolean {
5959
if (typeof window === "undefined") return false;
60-
const hasTouch = "ontouchstart" in window || navigator.maxTouchPoints > 0;
60+
const hasTouch =
61+
"ontouchstart" in window || (typeof navigator !== "undefined" && navigator.maxTouchPoints > 0);
6162
// Touch-only check: most touch devices have native dictation.
6263
// We don't check screen size because iPads are large but still have dictation.
6364
return hasTouch;
@@ -66,7 +67,9 @@ function hasTouchDictation(): boolean {
6667
const HAS_TOUCH_DICTATION = hasTouchDictation();
6768
const HAS_MEDIA_RECORDER = typeof window !== "undefined" && typeof MediaRecorder !== "undefined";
6869
const HAS_GET_USER_MEDIA =
69-
typeof window !== "undefined" && typeof navigator.mediaDevices?.getUserMedia === "function";
70+
typeof window !== "undefined" &&
71+
typeof navigator !== "undefined" &&
72+
typeof navigator.mediaDevices?.getUserMedia === "function";
7073

7174
// =============================================================================
7275
// Global Key State Tracking
@@ -79,7 +82,7 @@ const HAS_GET_USER_MEDIA =
7982
*/
8083
let isSpaceCurrentlyHeld = false;
8184

82-
if (typeof window !== "undefined") {
85+
if (typeof window !== "undefined" && typeof window.addEventListener === "function") {
8386
window.addEventListener(
8487
"keydown",
8588
(e) => {

src/browser/utils/RefreshController.test.ts

Lines changed: 29 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,31 @@
1-
import { describe, it, expect, beforeEach, afterEach, jest } from "@jest/globals";
2-
import { RefreshController } from "./RefreshController";
1+
import { describe, it, expect, mock } from "bun:test";
32

4-
describe("RefreshController", () => {
5-
beforeEach(() => {
6-
jest.useFakeTimers();
7-
});
3+
import { RefreshController } from "./RefreshController";
84

9-
afterEach(() => {
10-
jest.useRealTimers();
11-
});
5+
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
126

13-
it("debounces multiple schedule() calls", () => {
14-
const onRefresh = jest.fn<() => void>();
15-
const controller = new RefreshController({ onRefresh, debounceMs: 100 });
7+
describe("RefreshController", () => {
8+
it("debounces multiple schedule() calls", async () => {
9+
const onRefresh = mock<() => void>(() => undefined);
10+
const controller = new RefreshController({ onRefresh, debounceMs: 50 });
1611

1712
controller.schedule();
1813
controller.schedule();
1914
controller.schedule();
2015

2116
expect(onRefresh).not.toHaveBeenCalled();
2217

23-
jest.advanceTimersByTime(100);
18+
// Use real timers because bun's jest compatibility doesn't expose timer controls.
19+
await sleep(120);
2420

2521
expect(onRefresh).toHaveBeenCalledTimes(1);
2622

2723
controller.dispose();
2824
});
2925

30-
it("requestImmediate() bypasses debounce", () => {
31-
const onRefresh = jest.fn<() => void>();
32-
const controller = new RefreshController({ onRefresh, debounceMs: 100 });
26+
it("requestImmediate() bypasses debounce", async () => {
27+
const onRefresh = mock<() => void>(() => undefined);
28+
const controller = new RefreshController({ onRefresh, debounceMs: 50 });
3329

3430
controller.schedule();
3531
expect(onRefresh).not.toHaveBeenCalled();
@@ -38,7 +34,7 @@ describe("RefreshController", () => {
3834
expect(onRefresh).toHaveBeenCalledTimes(1);
3935

4036
// Original debounce timer should be cleared
41-
jest.advanceTimersByTime(100);
37+
await sleep(120);
4238
expect(onRefresh).toHaveBeenCalledTimes(1);
4339

4440
controller.dispose();
@@ -47,15 +43,15 @@ describe("RefreshController", () => {
4743
it("guards against concurrent sync refreshes (in-flight queuing)", () => {
4844
// Track if refresh is currently in-flight
4945
let inFlight = false;
50-
const onRefresh = jest.fn(() => {
46+
const onRefresh = mock(() => {
5147
// Simulate sync operation that takes time
5248
expect(inFlight).toBe(false); // Should never be called while already in-flight
5349
inFlight = true;
5450
// Immediately complete (sync)
5551
inFlight = false;
5652
});
5753

58-
const controller = new RefreshController({ onRefresh, debounceMs: 100 });
54+
const controller = new RefreshController({ onRefresh, debounceMs: 50 });
5955

6056
// Multiple immediate requests should only call once (queued ones execute after)
6157
controller.requestImmediate();
@@ -64,14 +60,14 @@ describe("RefreshController", () => {
6460
controller.dispose();
6561
});
6662

67-
it("isRefreshing reflects in-flight state", () => {
63+
it("isRefreshing reflects in-flight state", async () => {
6864
let resolveRefresh: () => void;
6965
const refreshPromise = new Promise<void>((resolve) => {
7066
resolveRefresh = resolve;
7167
});
7268

73-
const onRefresh = jest.fn(() => refreshPromise);
74-
const controller = new RefreshController({ onRefresh, debounceMs: 100 });
69+
const onRefresh = mock(() => refreshPromise);
70+
const controller = new RefreshController({ onRefresh, debounceMs: 50 });
7571

7672
expect(controller.isRefreshing).toBe(false);
7773

@@ -80,31 +76,34 @@ describe("RefreshController", () => {
8076

8177
// Complete the promise
8278
resolveRefresh!();
79+
await Promise.resolve();
80+
81+
expect(controller.isRefreshing).toBe(false);
8382

8483
controller.dispose();
8584
});
8685

87-
it("dispose() cleans up debounce timer", () => {
88-
const onRefresh = jest.fn<() => void>();
89-
const controller = new RefreshController({ onRefresh, debounceMs: 100 });
86+
it("dispose() cleans up debounce timer", async () => {
87+
const onRefresh = mock<() => void>(() => undefined);
88+
const controller = new RefreshController({ onRefresh, debounceMs: 50 });
9089

9190
controller.schedule();
9291
controller.dispose();
9392

94-
jest.advanceTimersByTime(100);
93+
await sleep(120);
9594

9695
expect(onRefresh).not.toHaveBeenCalled();
9796
});
9897

99-
it("does not refresh after dispose", () => {
100-
const onRefresh = jest.fn<() => void>();
101-
const controller = new RefreshController({ onRefresh, debounceMs: 100 });
98+
it("does not refresh after dispose", async () => {
99+
const onRefresh = mock<() => void>(() => undefined);
100+
const controller = new RefreshController({ onRefresh, debounceMs: 50 });
102101

103102
controller.dispose();
104103
controller.schedule();
105104
controller.requestImmediate();
106105

107-
jest.advanceTimersByTime(100);
106+
await sleep(120);
108107

109108
expect(onRefresh).not.toHaveBeenCalled();
110109
});

src/node/services/tools/task.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ describe("task tool", () => {
7575
taskId: "child-task",
7676
reportMarkdown: "Hello from child",
7777
title: "Result",
78+
agentId: "explore",
7879
agentType: "explore",
7980
});
8081
});

0 commit comments

Comments
 (0)