|
19 | 19 | * request it aborted was accepted. |
20 | 20 | */ |
21 | 21 |
|
22 | | -import { act, type ReactNode, StrictMode, useEffect, useState } from 'react' |
| 22 | +import { type ReactNode, act as reactAct, StrictMode, useEffect, useState } from 'react' |
23 | 23 | import { authClientMock, authClientMockFns } from '@sim/testing/mocks/auth-client.mock' |
24 | 24 | import { libDesktopMock, libDesktopMockFns } from '@sim/testing/mocks/lib-desktop.mock' |
25 | 25 | import { nextNavigationMock, nextNavigationMockFns } from '@sim/testing/mocks/next-navigation.mock' |
@@ -103,6 +103,36 @@ import { useExecutionStore } from '@/stores/execution/store' |
103 | 103 | import { useMothershipEffortStore } from '@/stores/mothership-effort/store' |
104 | 104 | import { useMothershipQueueStore } from '@/stores/mothership-queue/store' |
105 | 105 |
|
| 106 | +/** Captured before any test fakes timers, so the act budget below runs in real time. */ |
| 107 | +const realSetTimeout = globalThis.setTimeout |
| 108 | +const realClearTimeout = globalThis.clearTimeout |
| 109 | +/** Well under the 10s test timeout: the scope must end before the runner abandons the test. */ |
| 110 | +const ACT_BUDGET_MS = 6_000 |
| 111 | + |
| 112 | +/** |
| 113 | + * React's `act`, with async callbacks bounded. A callback that never settles (a |
| 114 | + * regressed send stuck reconnecting) used to run into the test timeout while |
| 115 | + * still inside React's act scope, and the renders of every later test queued |
| 116 | + * behind it ("Hook result is not ready"). Failing the act after a budget ends |
| 117 | + * the scope, so one regression is one red test. Sync callbacks stay synchronous. |
| 118 | + */ |
| 119 | +function act(callback: () => unknown): Promise<void> { |
| 120 | + return reactAct((): undefined | Promise<void> => { |
| 121 | + const result = callback() |
| 122 | + if (!(result instanceof Promise)) return undefined |
| 123 | + let budget: ReturnType<typeof setTimeout> | undefined |
| 124 | + const budgetSpent = new Promise<never>((_, reject) => { |
| 125 | + budget = realSetTimeout( |
| 126 | + () => reject(new Error(`act callback still pending after ${ACT_BUDGET_MS}ms`)), |
| 127 | + ACT_BUDGET_MS |
| 128 | + ) |
| 129 | + }) |
| 130 | + return Promise.race([result.then(() => undefined), budgetSpent]).finally(() => |
| 131 | + realClearTimeout(budget) |
| 132 | + ) |
| 133 | + }) |
| 134 | +} |
| 135 | + |
106 | 136 | authClientMockFns.mockUseSession.mockImplementation(() => ({ |
107 | 137 | data: { user: { id: 'test-viewer' } }, |
108 | 138 | })) |
|
0 commit comments