From ed21cf836d2278f3c1a579c77e9b120b4f67830f Mon Sep 17 00:00:00 2001 From: MHSanaei Date: Mon, 1 Jun 2026 09:03:47 +0200 Subject: [PATCH] fix(test): drain React scheduler macrotask before jsdom teardown React 19 defers passive-effect flushes onto a setImmediate callback that reads window.event. When one was still queued as vitest tore down the jsdom environment, it fired after window was deleted and surfaced as an unhandled 'window is not defined' error, failing the run with exit 1 despite all tests passing. Drain the macrotask queue in afterEach so any pending callback runs while window still exists. --- frontend/src/generated/zod.ts | 2 +- frontend/src/test/setup.components.ts | 14 +++++++++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/frontend/src/generated/zod.ts b/frontend/src/generated/zod.ts index 9b91016a..29373964 100644 --- a/frontend/src/generated/zod.ts +++ b/frontend/src/generated/zod.ts @@ -292,7 +292,7 @@ export const InboundSchema = z.object({ listen: z.string(), nodeId: z.number().int().nullable().optional(), port: z.number().int().min(1).max(65535), - protocol: z.enum(['vmess', 'vless', 'trojan', 'shadowsocks', 'wireguard', 'hysteria', 'http', 'mixed', 'tunnel']), + protocol: z.enum(['vmess', 'vless', 'trojan', 'shadowsocks', 'wireguard', 'hysteria', 'http', 'mixed', 'tunnel', 'tun']), remark: z.string(), settings: z.unknown(), sniffing: z.unknown(), diff --git a/frontend/src/test/setup.components.ts b/frontend/src/test/setup.components.ts index 45ea0ea1..eb732b51 100644 --- a/frontend/src/test/setup.components.ts +++ b/frontend/src/test/setup.components.ts @@ -58,7 +58,19 @@ if (!i18next.isInitialized) { }); } -afterEach(() => { +afterEach(async () => { cleanup(); document.body.innerHTML = ''; + /* + * React 19 defers passive-effect flushes onto a macrotask (setImmediate), + * whose callback reads `window.event`. If one is still queued when vitest + * tears down the jsdom environment, it fires after `window` is gone and + * throws "window is not defined". Drain a few macrotask ticks here so any + * pending callback runs while `window` still exists. Several ticks are used + * because a microtask resolving mid-drain (rc-trigger/AntD) can queue a new + * one behind the first. + */ + for (let i = 0; i < 3; i += 1) { + await new Promise((resolve) => setImmediate(resolve)); + } });