@rstest/browser 0.11.6 → 0.11.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/browser-container/container-static/css/index.43daaf6c93.css +1 -0
- package/dist/browser-container/container-static/js/260.3739fe815c.js +194 -0
- package/dist/browser-container/container-static/js/index.c0f87bfb3e.js +1 -0
- package/dist/browser-container/index.html +1 -1
- package/dist/browserRsbuild.d.ts +24 -4
- package/dist/browserV8Coverage.d.ts +15 -0
- package/dist/client/240~1.js +150 -0
- package/dist/{augmentExpect.d.ts → client/client/augmentExpect.d.ts} +1 -1
- package/dist/client/{browserRpc.d.ts → client/browserRpc.d.ts} +1 -1
- package/dist/client/{dispatchTransport.d.ts → client/dispatchTransport.d.ts} +8 -4
- package/dist/client/client/index.d.ts +2 -0
- package/dist/client/client/runIdentity.d.ts +12 -0
- package/dist/client/client/runner.d.ts +4 -0
- package/dist/client/client/snapshot.d.ts +27 -0
- package/dist/client/client/sourceMapSupport.d.ts +36 -0
- package/dist/{browser.js → client/index.js} +3 -102
- package/dist/client/protocol.d.ts +225 -0
- package/dist/client/rpcProtocol.d.ts +138 -0
- package/dist/client/runner.js +675 -0
- package/dist/client/sourceMap/sourceMapLoader.d.ts +25 -0
- package/dist/containerRpc.d.ts +27 -19
- package/dist/headedRunRegistry.d.ts +51 -0
- package/dist/headedScheduler.d.ts +10 -10
- package/dist/headlessScheduler.d.ts +5 -4
- package/dist/headlessTransport.d.ts +4 -2
- package/dist/hostPayloads.d.ts +1 -7
- package/dist/index.js +840 -330
- package/dist/protocol.d.ts +48 -8
- package/dist/providers/index.d.ts +21 -0
- package/dist/providers/playwright/v8Coverage.d.ts +2 -0
- package/dist/rpcProtocol.d.ts +1 -2
- package/dist/schedulerSeam.d.ts +5 -0
- package/dist/sourceMap/sourceMapLoader.d.ts +12 -1
- package/dist/watchRerunPlanner.d.ts +20 -9
- package/dist/watchSignals.d.ts +8 -1
- package/package.json +11 -11
- package/dist/33.js +0 -7
- package/dist/browser-container/container-static/css/index.0521ff95b1.css +0 -1
- package/dist/browser-container/container-static/js/683.4f821ce8f1.js +0 -194
- package/dist/browser-container/container-static/js/index.568aa7cb35.js +0 -1
- package/dist/browser.d.ts +0 -2
- package/src/augmentExpect.ts +0 -62
- package/src/browser.ts +0 -3
- package/src/browserExecutor.ts +0 -222
- package/src/browserRpcRegistry.ts +0 -57
- package/src/browserRsbuild.ts +0 -1927
- package/src/client/api.ts +0 -213
- package/src/client/browserRpc.ts +0 -86
- package/src/client/dispatchTransport.ts +0 -240
- package/src/client/entry.ts +0 -807
- package/src/client/locator.ts +0 -448
- package/src/client/snapshot.ts +0 -113
- package/src/client/sourceMapSupport.ts +0 -159
- package/src/concurrency.ts +0 -65
- package/src/configValidation.ts +0 -263
- package/src/containerRpc.ts +0 -206
- package/src/dispatchCapabilities.ts +0 -177
- package/src/dispatchRouter.ts +0 -82
- package/src/env.d.ts +0 -54
- package/src/headedScheduler.ts +0 -664
- package/src/headedSerialTaskQueue.ts +0 -23
- package/src/headlessScheduler.ts +0 -566
- package/src/headlessTransport.ts +0 -28
- package/src/hostController.ts +0 -1365
- package/src/hostPayloads.ts +0 -62
- package/src/index.ts +0 -51
- package/src/manifest.d.ts +0 -41
- package/src/protocol.ts +0 -225
- package/src/providers/index.ts +0 -126
- package/src/providers/playwright/compileLocator.ts +0 -130
- package/src/providers/playwright/dispatchBrowserRpc.ts +0 -372
- package/src/providers/playwright/expectUtils.ts +0 -57
- package/src/providers/playwright/implementation.ts +0 -35
- package/src/providers/playwright/index.ts +0 -1
- package/src/providers/playwright/runtime.ts +0 -159
- package/src/providers/playwright/textMatcher.ts +0 -10
- package/src/rpcProtocol.ts +0 -218
- package/src/runSession.ts +0 -110
- package/src/schedulerSeam.ts +0 -49
- package/src/sessionRegistry.ts +0 -89
- package/src/sourceMap/sourceMapLoader.ts +0 -96
- package/src/viewportPresets.ts +0 -69
- package/src/watchRerunPlanner.ts +0 -88
- package/src/watchRuntime.ts +0 -83
- package/src/watchSignals.ts +0 -93
- /package/dist/{rslib-runtime.js → 612~0.js} +0 -0
- /package/dist/client/{api.d.ts → client/api.d.ts} +0 -0
- /package/dist/client/{locator.d.ts → client/locator.d.ts} +0 -0
package/src/hostController.ts
DELETED
|
@@ -1,1365 +0,0 @@
|
|
|
1
|
-
import fs from 'node:fs/promises';
|
|
2
|
-
import {
|
|
3
|
-
type BrowserTestRunOptions,
|
|
4
|
-
type BrowserTestRunResult,
|
|
5
|
-
buildBrowserCoverageMap,
|
|
6
|
-
type ExecutorCycleOutcome,
|
|
7
|
-
type ExecutorInvalidationCallback,
|
|
8
|
-
type ListBrowserTestsOptions,
|
|
9
|
-
color,
|
|
10
|
-
createCoverageProvider,
|
|
11
|
-
createRunnerEventSink,
|
|
12
|
-
createSilentConsoleController,
|
|
13
|
-
DEFAULT_TEST_TIMEOUT,
|
|
14
|
-
type FormattedError,
|
|
15
|
-
getPrettyConsoleName,
|
|
16
|
-
hasUserRstestConfigPlugins,
|
|
17
|
-
isDebug,
|
|
18
|
-
isTTY,
|
|
19
|
-
type ListCommandResult,
|
|
20
|
-
logger,
|
|
21
|
-
logWatchReadyMessage,
|
|
22
|
-
projectRuntimeConfig,
|
|
23
|
-
PhaseTracker,
|
|
24
|
-
type ProjectContext,
|
|
25
|
-
type RunnerEventSink,
|
|
26
|
-
type RstestContext,
|
|
27
|
-
resolveSnapshotPathDefault,
|
|
28
|
-
serializableConfig,
|
|
29
|
-
type Test,
|
|
30
|
-
type TestFileResult,
|
|
31
|
-
type TestResult,
|
|
32
|
-
type UserConsoleLog,
|
|
33
|
-
} from '@rstest/core/internal/browser';
|
|
34
|
-
import { dirname, join, normalize } from 'pathe';
|
|
35
|
-
import {
|
|
36
|
-
createHostDispatchRouter,
|
|
37
|
-
type HostDispatchRouterOptions,
|
|
38
|
-
} from './dispatchCapabilities';
|
|
39
|
-
import {
|
|
40
|
-
collectProjectEntries,
|
|
41
|
-
createBrowserRuntime,
|
|
42
|
-
destroyBrowserRuntime,
|
|
43
|
-
drainPendingBuildTime,
|
|
44
|
-
getBrowserProjects,
|
|
45
|
-
type BrowserProjectServer,
|
|
46
|
-
type BrowserProviderProject,
|
|
47
|
-
resolveContainerDist,
|
|
48
|
-
resolveProjectEntries,
|
|
49
|
-
serializeForInlineScript,
|
|
50
|
-
type BrowserRuntime,
|
|
51
|
-
} from './browserRsbuild';
|
|
52
|
-
import { createHeadedScheduler } from './headedScheduler';
|
|
53
|
-
import { createHeadlessScheduler } from './headlessScheduler';
|
|
54
|
-
import type {
|
|
55
|
-
BrowserDispatchRequest,
|
|
56
|
-
BrowserHostConfig,
|
|
57
|
-
BrowserProjectRuntime,
|
|
58
|
-
BrowserRpcRequest,
|
|
59
|
-
SnapshotRpcRequest,
|
|
60
|
-
TestFileInfo,
|
|
61
|
-
} from './protocol';
|
|
62
|
-
import {
|
|
63
|
-
DISPATCH_MESSAGE_TYPE,
|
|
64
|
-
DISPATCH_NAMESPACE_BROWSER,
|
|
65
|
-
validateBrowserRpcRequest,
|
|
66
|
-
} from './protocol';
|
|
67
|
-
import {
|
|
68
|
-
type BrowserProvider,
|
|
69
|
-
type BrowserProviderImplementation,
|
|
70
|
-
getBrowserProviderImplementation,
|
|
71
|
-
} from './providers';
|
|
72
|
-
import {
|
|
73
|
-
type FatalPayload,
|
|
74
|
-
getFileTaskId,
|
|
75
|
-
type LogPayload,
|
|
76
|
-
type TestCaseStartPayload,
|
|
77
|
-
type TestFileReadyPayload,
|
|
78
|
-
type TestFileStartPayload,
|
|
79
|
-
type TestSuiteResultPayload,
|
|
80
|
-
type TestSuiteStartPayload,
|
|
81
|
-
toError,
|
|
82
|
-
} from './hostPayloads';
|
|
83
|
-
import {
|
|
84
|
-
loadSourceMapWithCache,
|
|
85
|
-
normalizeJavaScriptUrl,
|
|
86
|
-
type SourceMapPayload,
|
|
87
|
-
} from './sourceMap/sourceMapLoader';
|
|
88
|
-
import { collectWatchTestFiles } from './watchRerunPlanner';
|
|
89
|
-
import type {
|
|
90
|
-
BrowserWatchSession,
|
|
91
|
-
DispatchPageResolver,
|
|
92
|
-
} from './schedulerSeam';
|
|
93
|
-
import { registerWatchCleanup, watchContext } from './watchRuntime';
|
|
94
|
-
import { createWatchSignals } from './watchSignals';
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* Monotonic counter for synthetic per-file Perfetto `pid` values in `--trace`
|
|
98
|
-
* mode. Browser host runs every test file inside the same Node process, so
|
|
99
|
-
* without an override every file would emit events under the same `pid` and
|
|
100
|
-
* share a single track labelled `worker <hostPid>`. Giving each file its own
|
|
101
|
-
* synthetic `pid` makes the track title surface the file path instead,
|
|
102
|
-
* matching node mode's default `isolate: true` behavior. The 1_000_000_000
|
|
103
|
-
* base keeps each synthetic `pid` well clear of real OS `pid` values in
|
|
104
|
-
* mixed-mode traces.
|
|
105
|
-
*/
|
|
106
|
-
let nextBrowserFilePid = 1_000_000_000;
|
|
107
|
-
|
|
108
|
-
// ============================================================================
|
|
109
|
-
// Utility Functions
|
|
110
|
-
// ============================================================================
|
|
111
|
-
|
|
112
|
-
/**
|
|
113
|
-
* The exit code a launch that found no test files at all must leave behind.
|
|
114
|
-
*
|
|
115
|
-
* Core's `reportNoTestFiles` owns the message and the no-test reporter
|
|
116
|
-
* lifecycle for such a launch, but in watch mode its report deliberately leaves
|
|
117
|
-
* the exit code alone — a rerun matching nothing is not a failure. Such a launch
|
|
118
|
-
* opened no session, so no later cycle can raise the code either. That makes
|
|
119
|
-
* this the host's only write to `process.exitCode`: a boot failure looks like a
|
|
120
|
-
* second launch-path exception but is not one, because it rides the outcome out
|
|
121
|
-
* of `failWithError` and core raises the code from there. One-shot runs keep
|
|
122
|
-
* going through the cycle, and a caller that passed `allowEmptyRun` — today only
|
|
123
|
-
* the config-hook discovery boot — reads the outcome instead of the process.
|
|
124
|
-
*/
|
|
125
|
-
const resolveEmptyLaunchExitCode = (
|
|
126
|
-
current: number | string | undefined,
|
|
127
|
-
{
|
|
128
|
-
allowEmptyRun,
|
|
129
|
-
isWatchMode,
|
|
130
|
-
passWithNoTests,
|
|
131
|
-
}: {
|
|
132
|
-
allowEmptyRun: boolean;
|
|
133
|
-
isWatchMode: boolean;
|
|
134
|
-
passWithNoTests: boolean;
|
|
135
|
-
},
|
|
136
|
-
): number | string | undefined => {
|
|
137
|
-
if (allowEmptyRun || !isWatchMode || passWithNoTests) {
|
|
138
|
-
return current;
|
|
139
|
-
}
|
|
140
|
-
// Never downgrade: a code already raised by an earlier failure stands.
|
|
141
|
-
return current === undefined || current === 0 ? 1 : current;
|
|
142
|
-
};
|
|
143
|
-
|
|
144
|
-
const getMaxTestTimeoutForRpc = (projects: ProjectContext[]): number =>
|
|
145
|
-
Math.max(
|
|
146
|
-
...projects.map(
|
|
147
|
-
(p) => p.normalizedConfig.testTimeout ?? DEFAULT_TEST_TIMEOUT,
|
|
148
|
-
),
|
|
149
|
-
);
|
|
150
|
-
|
|
151
|
-
const resolveProviderForTestPath = ({
|
|
152
|
-
testPath,
|
|
153
|
-
browserProjects,
|
|
154
|
-
}: {
|
|
155
|
-
testPath: string;
|
|
156
|
-
browserProjects: BrowserProviderProject[];
|
|
157
|
-
}): BrowserProvider => {
|
|
158
|
-
const normalizedTestPath = normalize(testPath);
|
|
159
|
-
const sortedProjects = [...browserProjects].sort(
|
|
160
|
-
(a, b) => b.rootPath.length - a.rootPath.length,
|
|
161
|
-
);
|
|
162
|
-
|
|
163
|
-
for (const project of sortedProjects) {
|
|
164
|
-
if (normalizedTestPath.startsWith(project.rootPath)) {
|
|
165
|
-
return project.provider;
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
throw new Error(
|
|
170
|
-
`Cannot resolve browser provider for test path: ${JSON.stringify(testPath)}. ` +
|
|
171
|
-
`Known project roots: ${JSON.stringify(sortedProjects.map((p) => p.rootPath))}`,
|
|
172
|
-
);
|
|
173
|
-
};
|
|
174
|
-
|
|
175
|
-
// ============================================================================
|
|
176
|
-
|
|
177
|
-
export type BrowserControllerOptions = BrowserTestRunOptions & {
|
|
178
|
-
/**
|
|
179
|
-
* Watch only: core's watch-cycle driver (see `TestExecutor.onInvalidate`).
|
|
180
|
-
* Its promise settles when the cycle it queued has finalized, which only an
|
|
181
|
-
* explicit request may wait for (see `signalInvalidation`).
|
|
182
|
-
*/
|
|
183
|
-
onInvalidate?: ExecutorInvalidationCallback;
|
|
184
|
-
};
|
|
185
|
-
|
|
186
|
-
export type BrowserControllerResult = BrowserTestRunResult & {
|
|
187
|
-
watchSession?: BrowserWatchSession;
|
|
188
|
-
};
|
|
189
|
-
|
|
190
|
-
export const runBrowserController = async (
|
|
191
|
-
context: RstestContext,
|
|
192
|
-
options?: BrowserControllerOptions,
|
|
193
|
-
): Promise<BrowserControllerResult | void> => {
|
|
194
|
-
const {
|
|
195
|
-
allowEmptyRun = false,
|
|
196
|
-
filesOnly = false,
|
|
197
|
-
onTraceEvents,
|
|
198
|
-
env,
|
|
199
|
-
onInvalidate,
|
|
200
|
-
} = options ?? {};
|
|
201
|
-
const buildStart = Date.now();
|
|
202
|
-
// Watch mode changes what this controller *owns*, never who finalizes: core's
|
|
203
|
-
// `finalizeRunCycle` reduces every cycle on both commands. What watch adds is
|
|
204
|
-
// a persistent runtime (reused across controller re-entry), the rerun
|
|
205
|
-
// triggers, and HMR — so the initial run returns a live watch session instead
|
|
206
|
-
// of a deferred `close`.
|
|
207
|
-
const isWatchMode = context.command === 'watch';
|
|
208
|
-
|
|
209
|
-
// Per-file PhaseTrackers, populated only when `--trace` is on (caller
|
|
210
|
-
// passes `onTraceEvents`). The browser host shares one Node process across
|
|
211
|
-
// every test file, so each tracker is assigned a synthetic per-file pid
|
|
212
|
-
// (`nextBrowserFilePid`) that lets Perfetto render each file as its own
|
|
213
|
-
// process track with the file path as the title. Keyed by project + path so
|
|
214
|
-
// concurrent projects running the same file keep separate trackers.
|
|
215
|
-
const phaseTrackers = onTraceEvents
|
|
216
|
-
? new Map<string, PhaseTracker>()
|
|
217
|
-
: undefined;
|
|
218
|
-
const trackerKey = (project: string, testPath: string) =>
|
|
219
|
-
`${project}\u0000${testPath}`;
|
|
220
|
-
// Explicit projects input (plan output) replaces re-deriving `browser.enabled`
|
|
221
|
-
// projects from `context`, whose `projects` array is mutated during planning.
|
|
222
|
-
// Falls back to re-derivation only when the caller passes no list at all —
|
|
223
|
-
// an explicit empty subset must stay empty, not widen to every project.
|
|
224
|
-
const browserProjects = options?.projects ?? getBrowserProjects(context);
|
|
225
|
-
const useHeadlessDirect = browserProjects.every(
|
|
226
|
-
(project) => project.normalizedConfig.browser.headless,
|
|
227
|
-
);
|
|
228
|
-
|
|
229
|
-
const browserSourceMapCache = new Map<string, SourceMapPayload | null>();
|
|
230
|
-
|
|
231
|
-
const isHttpLikeFile = (file: string): boolean => /^https?:\/\//.test(file);
|
|
232
|
-
|
|
233
|
-
const resolveBrowserSourcemap = async (sourcePath: string) => {
|
|
234
|
-
if (!isHttpLikeFile(sourcePath)) {
|
|
235
|
-
return {
|
|
236
|
-
handled: false,
|
|
237
|
-
sourcemap: null,
|
|
238
|
-
};
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
const normalizedUrl = normalizeJavaScriptUrl(sourcePath);
|
|
242
|
-
if (!normalizedUrl) {
|
|
243
|
-
return {
|
|
244
|
-
handled: true,
|
|
245
|
-
sourcemap: null,
|
|
246
|
-
};
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
if (browserSourceMapCache.has(normalizedUrl)) {
|
|
250
|
-
return {
|
|
251
|
-
handled: true,
|
|
252
|
-
sourcemap: browserSourceMapCache.get(normalizedUrl) ?? null,
|
|
253
|
-
};
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
return {
|
|
257
|
-
handled: true,
|
|
258
|
-
sourcemap: await loadSourceMapWithCache({
|
|
259
|
-
jsUrl: normalizedUrl,
|
|
260
|
-
cache: browserSourceMapCache,
|
|
261
|
-
}),
|
|
262
|
-
};
|
|
263
|
-
};
|
|
264
|
-
|
|
265
|
-
const getBrowserSourcemap = async (
|
|
266
|
-
sourcePath: string,
|
|
267
|
-
): Promise<SourceMapPayload | null> => {
|
|
268
|
-
const result = await resolveBrowserSourcemap(sourcePath);
|
|
269
|
-
return result.handled ? result.sourcemap : null;
|
|
270
|
-
};
|
|
271
|
-
|
|
272
|
-
/**
|
|
273
|
-
* Build an error BrowserTestRunResult and call onTestRunEnd if needed.
|
|
274
|
-
* Used for early-exit error paths to ensure errors reach the summary report.
|
|
275
|
-
*/
|
|
276
|
-
const buildErrorResult = (
|
|
277
|
-
error: Error,
|
|
278
|
-
close?: () => Promise<void>,
|
|
279
|
-
): BrowserTestRunResult => {
|
|
280
|
-
const elapsed = Math.max(0, Date.now() - buildStart);
|
|
281
|
-
return {
|
|
282
|
-
results: [],
|
|
283
|
-
testResults: [],
|
|
284
|
-
duration: { totalTime: elapsed, buildTime: elapsed, testTime: 0 },
|
|
285
|
-
hasFailure: true,
|
|
286
|
-
unhandledErrors: [error],
|
|
287
|
-
getSourcemap: getBrowserSourcemap,
|
|
288
|
-
resolveSourcemap: resolveBrowserSourcemap,
|
|
289
|
-
close,
|
|
290
|
-
};
|
|
291
|
-
};
|
|
292
|
-
|
|
293
|
-
const failWithError = async (
|
|
294
|
-
error: unknown,
|
|
295
|
-
cleanup?: () => Promise<void>,
|
|
296
|
-
): Promise<BrowserTestRunResult> => {
|
|
297
|
-
// The error rides the returned result into the cycle outcome, and core's
|
|
298
|
-
// `finalizeRunCycle` raises the exit code from it — on both commands.
|
|
299
|
-
const normalizedError = toError(error);
|
|
300
|
-
|
|
301
|
-
if (cleanup && !isWatchMode) {
|
|
302
|
-
return buildErrorResult(normalizedError, cleanup);
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
try {
|
|
306
|
-
return buildErrorResult(normalizedError);
|
|
307
|
-
} finally {
|
|
308
|
-
await cleanup?.();
|
|
309
|
-
}
|
|
310
|
-
};
|
|
311
|
-
|
|
312
|
-
const coverageConfig = browserProjects.find(
|
|
313
|
-
(project) => project.normalizedConfig.coverage?.enabled,
|
|
314
|
-
)?.normalizedConfig.coverage;
|
|
315
|
-
const coverageProvider = coverageConfig?.enabled
|
|
316
|
-
? await createCoverageProvider(coverageConfig, context.rootPath)
|
|
317
|
-
: null;
|
|
318
|
-
|
|
319
|
-
const containerDevServerEnv = process.env.RSTEST_CONTAINER_DEV_SERVER;
|
|
320
|
-
let containerDevServer: string | undefined;
|
|
321
|
-
let containerDistPath: string | undefined;
|
|
322
|
-
|
|
323
|
-
if (!useHeadlessDirect) {
|
|
324
|
-
if (containerDevServerEnv) {
|
|
325
|
-
try {
|
|
326
|
-
containerDevServer = new URL(containerDevServerEnv).toString();
|
|
327
|
-
logger.debug(
|
|
328
|
-
`[Browser UI] Using dev server for container: ${containerDevServer}`,
|
|
329
|
-
);
|
|
330
|
-
} catch (error) {
|
|
331
|
-
const originalError = toError(error);
|
|
332
|
-
originalError.message = `Invalid RSTEST_CONTAINER_DEV_SERVER value: ${originalError.message}`;
|
|
333
|
-
return failWithError(originalError);
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
if (!containerDevServer) {
|
|
338
|
-
try {
|
|
339
|
-
containerDistPath = resolveContainerDist();
|
|
340
|
-
} catch (error) {
|
|
341
|
-
return failWithError(error);
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
let projectEntries = await resolveProjectEntries(
|
|
347
|
-
context,
|
|
348
|
-
options?.shardedEntries,
|
|
349
|
-
browserProjects,
|
|
350
|
-
);
|
|
351
|
-
let totalTests = projectEntries.reduce(
|
|
352
|
-
(total, item) => total + item.testFiles.length,
|
|
353
|
-
0,
|
|
354
|
-
);
|
|
355
|
-
const shouldInitializeEmptyBrowserHooks =
|
|
356
|
-
totalTests === 0 && hasUserRstestConfigPlugins(browserProjects);
|
|
357
|
-
|
|
358
|
-
const createEmptyRunResult = (): BrowserTestRunResult => {
|
|
359
|
-
const elapsed = Math.max(0, Date.now() - buildStart);
|
|
360
|
-
return {
|
|
361
|
-
results: [],
|
|
362
|
-
testResults: [],
|
|
363
|
-
duration: {
|
|
364
|
-
totalTime: elapsed,
|
|
365
|
-
buildTime: elapsed,
|
|
366
|
-
testTime: 0,
|
|
367
|
-
},
|
|
368
|
-
hasFailure: false,
|
|
369
|
-
getSourcemap: getBrowserSourcemap,
|
|
370
|
-
resolveSourcemap: resolveBrowserSourcemap,
|
|
371
|
-
};
|
|
372
|
-
};
|
|
373
|
-
|
|
374
|
-
const writeEmptyLaunchExitCode = (): void => {
|
|
375
|
-
process.exitCode = resolveEmptyLaunchExitCode(process.exitCode, {
|
|
376
|
-
allowEmptyRun,
|
|
377
|
-
isWatchMode,
|
|
378
|
-
passWithNoTests: context.normalizedConfig.passWithNoTests,
|
|
379
|
-
});
|
|
380
|
-
};
|
|
381
|
-
|
|
382
|
-
if (totalTests === 0 && !shouldInitializeEmptyBrowserHooks) {
|
|
383
|
-
writeEmptyLaunchExitCode();
|
|
384
|
-
return allowEmptyRun ? createEmptyRunResult() : undefined;
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
const enableCliShortcuts = isWatchMode && isTTY('stdin');
|
|
388
|
-
const browserTempOutputRoot = context.normalizedConfig.output.distPath.root;
|
|
389
|
-
const tempDir =
|
|
390
|
-
isWatchMode && watchContext.runtime
|
|
391
|
-
? watchContext.runtime.tempDir
|
|
392
|
-
: isWatchMode
|
|
393
|
-
? join(context.rootPath, browserTempOutputRoot, 'browser', 'watch')
|
|
394
|
-
: join(
|
|
395
|
-
context.rootPath,
|
|
396
|
-
browserTempOutputRoot,
|
|
397
|
-
'browser',
|
|
398
|
-
Date.now().toString(),
|
|
399
|
-
);
|
|
400
|
-
|
|
401
|
-
let runtime = isWatchMode ? watchContext.runtime : null;
|
|
402
|
-
|
|
403
|
-
const watchSignals = createWatchSignals(onInvalidate);
|
|
404
|
-
|
|
405
|
-
if (!runtime) {
|
|
406
|
-
try {
|
|
407
|
-
runtime = await createBrowserRuntime({
|
|
408
|
-
context,
|
|
409
|
-
projectEntries,
|
|
410
|
-
browserProjects,
|
|
411
|
-
shardedEntries: options?.shardedEntries,
|
|
412
|
-
freezeShardedEntries: options?.freezeShardedEntries,
|
|
413
|
-
tempDir,
|
|
414
|
-
isWatchMode,
|
|
415
|
-
onTriggerRerun: isWatchMode
|
|
416
|
-
? async () => {
|
|
417
|
-
await watchSignals.runDispatchRerun();
|
|
418
|
-
}
|
|
419
|
-
: undefined,
|
|
420
|
-
containerDistPath,
|
|
421
|
-
containerDevServer,
|
|
422
|
-
skipProviderLaunch: filesOnly,
|
|
423
|
-
appliedModifyRstestConfigEnvironments:
|
|
424
|
-
options?.appliedModifyRstestConfigEnvironments,
|
|
425
|
-
});
|
|
426
|
-
} catch (error) {
|
|
427
|
-
return failWithError(error, async () => {
|
|
428
|
-
await fs.rm(tempDir, { recursive: true, force: true }).catch(() => {});
|
|
429
|
-
});
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
// `filesOnly` is the config-hook discovery boot, which destroys its runtime
|
|
433
|
-
// through the returned `close`. Caching it here would leave the real watch
|
|
434
|
-
// session re-entering on a destroyed runtime.
|
|
435
|
-
if (isWatchMode && !filesOnly) {
|
|
436
|
-
watchContext.runtime = runtime;
|
|
437
|
-
registerWatchCleanup(context.embedded);
|
|
438
|
-
}
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
const watchState = runtime.watchState;
|
|
442
|
-
|
|
443
|
-
// Track initial test files for watch mode (from this controller's freshly
|
|
444
|
-
// collected entries, before adopting the runtime's entry snapshot below).
|
|
445
|
-
if (isWatchMode) {
|
|
446
|
-
watchState.lastTestFiles = collectWatchTestFiles(projectEntries);
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
// Mark files as pending-affected so the next trigger reruns them through the
|
|
450
|
-
// normal plan/cycle pipeline (used by explicit rerun requests; omitted paths
|
|
451
|
-
// = all current files). Returns the number of seeded files so a path-scoped
|
|
452
|
-
// request matching no browser test file can skip the rerun entirely (mixed
|
|
453
|
-
// watch 'u' with node-only snapshot updates must produce no browser cycle).
|
|
454
|
-
const seedPendingRerun = (testPaths?: string[]): number => {
|
|
455
|
-
const wanted = testPaths
|
|
456
|
-
? new Set(testPaths.map((testPath) => normalize(testPath)))
|
|
457
|
-
: null;
|
|
458
|
-
let seeded = 0;
|
|
459
|
-
for (const file of watchState.lastTestFiles) {
|
|
460
|
-
if (wanted && !wanted.has(file.testPath)) {
|
|
461
|
-
continue;
|
|
462
|
-
}
|
|
463
|
-
const pending =
|
|
464
|
-
watchState.pendingAffectedTestFiles.get(file.projectName) ??
|
|
465
|
-
new Set<string>();
|
|
466
|
-
pending.add(file.testPath);
|
|
467
|
-
watchState.pendingAffectedTestFiles.set(file.projectName, pending);
|
|
468
|
-
seeded += 1;
|
|
469
|
-
}
|
|
470
|
-
return seeded;
|
|
471
|
-
};
|
|
472
|
-
|
|
473
|
-
/**
|
|
474
|
-
* Fold one watch rerun into the `ExecutorCycleOutcome` core's
|
|
475
|
-
* `finalizeRunCycle` reduces. `context.reporterResults` is the cross-cycle
|
|
476
|
-
* accumulator, so filtering it by the rerun's paths is what makes the outcome
|
|
477
|
-
* cycle-scoped. `buildTime` is the drained duration of the change-triggered
|
|
478
|
-
* compile(s) (zero for a shortcut-driven rerun, which compiles nothing).
|
|
479
|
-
*/
|
|
480
|
-
const buildRerunOutcome = ({
|
|
481
|
-
rerunTestPaths,
|
|
482
|
-
testTime,
|
|
483
|
-
unhandledErrors,
|
|
484
|
-
}: {
|
|
485
|
-
rerunTestPaths: string[];
|
|
486
|
-
testTime: number;
|
|
487
|
-
unhandledErrors?: Error[];
|
|
488
|
-
}): ExecutorCycleOutcome => {
|
|
489
|
-
const rerunPathSet = new Set(rerunTestPaths);
|
|
490
|
-
const rerunResults = context.reporterResults.results.filter((result) =>
|
|
491
|
-
rerunPathSet.has(result.testPath),
|
|
492
|
-
);
|
|
493
|
-
// Watch coverage is per-cycle on both transports: only the files this
|
|
494
|
-
// rerun executed are reported.
|
|
495
|
-
const coverageMap = buildBrowserCoverageMap(rerunResults, coverageProvider);
|
|
496
|
-
|
|
497
|
-
return {
|
|
498
|
-
results: rerunResults,
|
|
499
|
-
testResults: context.reporterResults.testResults.filter((result) =>
|
|
500
|
-
rerunPathSet.has(result.testPath),
|
|
501
|
-
),
|
|
502
|
-
errors: unhandledErrors ?? [],
|
|
503
|
-
testPaths: rerunTestPaths,
|
|
504
|
-
duration: {
|
|
505
|
-
buildTime: drainPendingBuildTime(watchState),
|
|
506
|
-
testTime,
|
|
507
|
-
},
|
|
508
|
-
coverage: coverageMap?.files().length
|
|
509
|
-
? { map: coverageMap.toJSON() }
|
|
510
|
-
: undefined,
|
|
511
|
-
resolveSourcemap: resolveBrowserSourcemap,
|
|
512
|
-
};
|
|
513
|
-
};
|
|
514
|
-
|
|
515
|
-
/**
|
|
516
|
-
* The watch session both transports hand back. Only `execute` differs — the
|
|
517
|
-
* cycle's timing, its fatal-error capture window, and the error-to-outcome
|
|
518
|
-
* precedence are one contract with core, so they live in one place.
|
|
519
|
-
*
|
|
520
|
-
* `execute`'s synchronous prefix runs before `runCycle` ever suspends, which
|
|
521
|
-
* is what lets the headed transport claim its cycle scope inside it.
|
|
522
|
-
*/
|
|
523
|
-
const createWatchSession = (
|
|
524
|
-
execute: (testPaths: string[]) => Promise<void>,
|
|
525
|
-
): BrowserWatchSession => ({
|
|
526
|
-
runCycle: async (testPaths) => {
|
|
527
|
-
const rerunStartTime = Date.now();
|
|
528
|
-
const fatalErrorBeforeRun = fatalErrorRef.current;
|
|
529
|
-
let rerunError: Error | undefined;
|
|
530
|
-
|
|
531
|
-
try {
|
|
532
|
-
await execute(testPaths);
|
|
533
|
-
} catch (error) {
|
|
534
|
-
// Surfaced through the outcome rather than thrown: core finalizes this
|
|
535
|
-
// cycle either way, and its results belong in the report even when the
|
|
536
|
-
// run that produced them ended badly.
|
|
537
|
-
rerunError = toError(error);
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
const rerunFatalError =
|
|
541
|
-
fatalErrorRef.current && fatalErrorRef.current !== fatalErrorBeforeRun
|
|
542
|
-
? fatalErrorRef.current
|
|
543
|
-
: undefined;
|
|
544
|
-
return buildRerunOutcome({
|
|
545
|
-
rerunTestPaths: testPaths,
|
|
546
|
-
testTime: Math.max(0, Date.now() - rerunStartTime),
|
|
547
|
-
unhandledErrors: rerunError
|
|
548
|
-
? [rerunError]
|
|
549
|
-
: rerunFatalError
|
|
550
|
-
? [rerunFatalError]
|
|
551
|
-
: undefined,
|
|
552
|
-
});
|
|
553
|
-
},
|
|
554
|
-
requestRerun: async (testPaths) => {
|
|
555
|
-
const seeded = seedPendingRerun(testPaths);
|
|
556
|
-
if (testPaths && seeded === 0) {
|
|
557
|
-
return;
|
|
558
|
-
}
|
|
559
|
-
await watchSignals.runDispatchRerun();
|
|
560
|
-
await watchSignals.awaitSignalledCycle();
|
|
561
|
-
},
|
|
562
|
-
});
|
|
563
|
-
|
|
564
|
-
projectEntries = runtime.projectEntries;
|
|
565
|
-
totalTests = projectEntries.reduce(
|
|
566
|
-
(total, item) => total + item.testFiles.length,
|
|
567
|
-
0,
|
|
568
|
-
);
|
|
569
|
-
|
|
570
|
-
const buildTime = Date.now() - buildStart;
|
|
571
|
-
|
|
572
|
-
if (filesOnly) {
|
|
573
|
-
return {
|
|
574
|
-
results: [],
|
|
575
|
-
testResults: [],
|
|
576
|
-
duration: {
|
|
577
|
-
totalTime: buildTime,
|
|
578
|
-
buildTime,
|
|
579
|
-
testTime: 0,
|
|
580
|
-
},
|
|
581
|
-
hasFailure: false,
|
|
582
|
-
getSourcemap: getBrowserSourcemap,
|
|
583
|
-
resolveSourcemap: resolveBrowserSourcemap,
|
|
584
|
-
close: () => destroyBrowserRuntime(runtime),
|
|
585
|
-
};
|
|
586
|
-
}
|
|
587
|
-
|
|
588
|
-
if (totalTests === 0) {
|
|
589
|
-
writeEmptyLaunchExitCode();
|
|
590
|
-
await destroyBrowserRuntime(runtime);
|
|
591
|
-
return allowEmptyRun ? createEmptyRunResult() : undefined;
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
const { browser, browserLaunchOptions, wsPort } = runtime;
|
|
595
|
-
|
|
596
|
-
// Collect all test files from project entries with project info
|
|
597
|
-
// Normalize paths to posix format for cross-platform compatibility
|
|
598
|
-
const allTestFiles: TestFileInfo[] = projectEntries.flatMap((entry) =>
|
|
599
|
-
entry.testFiles.map((testPath) => ({
|
|
600
|
-
testPath: normalize(testPath),
|
|
601
|
-
projectName: entry.project.name,
|
|
602
|
-
})),
|
|
603
|
-
);
|
|
604
|
-
|
|
605
|
-
// Only include browser mode projects in runtime configs
|
|
606
|
-
// Normalize projectRoot to posix format for cross-platform compatibility
|
|
607
|
-
const projectRuntimeConfigs: BrowserProjectRuntime[] = browserProjects.map(
|
|
608
|
-
(project: ProjectContext) => ({
|
|
609
|
-
name: project.name,
|
|
610
|
-
environmentName: project.environmentName,
|
|
611
|
-
projectRoot: normalize(project.rootPath),
|
|
612
|
-
runtimeConfig: serializableConfig(
|
|
613
|
-
// `env` is the post-globalSetup change-set from the core pre-cycle
|
|
614
|
-
// stage; the projection layers it between the static base and the
|
|
615
|
-
// user `test.env` config.
|
|
616
|
-
projectRuntimeConfig(project, { envMode: 'static', envOverlay: env }),
|
|
617
|
-
),
|
|
618
|
-
viewport: project.normalizedConfig.browser.viewport,
|
|
619
|
-
}),
|
|
620
|
-
);
|
|
621
|
-
|
|
622
|
-
const maxTestTimeoutForRpc = getMaxTestTimeoutForRpc(browserProjects);
|
|
623
|
-
|
|
624
|
-
const projectRunnerUrls = Object.fromEntries(
|
|
625
|
-
[...runtime.projectServers].map(([name, server]) => [
|
|
626
|
-
name,
|
|
627
|
-
`http://localhost:${server.port}`,
|
|
628
|
-
]),
|
|
629
|
-
);
|
|
630
|
-
|
|
631
|
-
const hostOptions: BrowserHostConfig = {
|
|
632
|
-
rootPath: normalize(context.rootPath),
|
|
633
|
-
projects: projectRuntimeConfigs,
|
|
634
|
-
snapshot: {
|
|
635
|
-
updateSnapshot:
|
|
636
|
-
options?.updateSnapshot ??
|
|
637
|
-
context.snapshotManager.options.updateSnapshot,
|
|
638
|
-
},
|
|
639
|
-
// Container origin (fallback). Per-project runner origins below.
|
|
640
|
-
runnerUrl: `http://localhost:${runtime.containerServer.port}`,
|
|
641
|
-
projectRunnerUrls,
|
|
642
|
-
wsPort,
|
|
643
|
-
debug: isDebug(),
|
|
644
|
-
rpcTimeout: maxTestTimeoutForRpc,
|
|
645
|
-
};
|
|
646
|
-
|
|
647
|
-
const browserProviderProjects: BrowserProviderProject[] = browserProjects.map(
|
|
648
|
-
(project) => ({
|
|
649
|
-
rootPath: normalize(project.rootPath),
|
|
650
|
-
provider: project.normalizedConfig.browser.provider,
|
|
651
|
-
}),
|
|
652
|
-
);
|
|
653
|
-
const implementationByProvider = new Map<
|
|
654
|
-
BrowserProvider,
|
|
655
|
-
BrowserProviderImplementation
|
|
656
|
-
>();
|
|
657
|
-
for (const browserProject of browserProviderProjects) {
|
|
658
|
-
if (!implementationByProvider.has(browserProject.provider)) {
|
|
659
|
-
implementationByProvider.set(
|
|
660
|
-
browserProject.provider,
|
|
661
|
-
getBrowserProviderImplementation(browserProject.provider),
|
|
662
|
-
);
|
|
663
|
-
}
|
|
664
|
-
}
|
|
665
|
-
|
|
666
|
-
let resolveDispatchPages: DispatchPageResolver = () => ({});
|
|
667
|
-
const setDispatchPageResolver = (resolver: DispatchPageResolver): void => {
|
|
668
|
-
resolveDispatchPages = resolver;
|
|
669
|
-
};
|
|
670
|
-
|
|
671
|
-
const dispatchBrowserRpcRequest = async ({
|
|
672
|
-
request,
|
|
673
|
-
target,
|
|
674
|
-
}: {
|
|
675
|
-
request: BrowserRpcRequest;
|
|
676
|
-
target?: BrowserDispatchRequest['target'];
|
|
677
|
-
}): Promise<unknown> => {
|
|
678
|
-
const timeoutFallbackMs = maxTestTimeoutForRpc;
|
|
679
|
-
const provider = resolveProviderForTestPath({
|
|
680
|
-
testPath: request.testPath,
|
|
681
|
-
browserProjects: browserProviderProjects,
|
|
682
|
-
});
|
|
683
|
-
const implementation = implementationByProvider.get(provider);
|
|
684
|
-
if (!implementation) {
|
|
685
|
-
throw new Error(`Browser provider implementation not found: ${provider}`);
|
|
686
|
-
}
|
|
687
|
-
|
|
688
|
-
const { runnerPage, containerPage } = resolveDispatchPages(target);
|
|
689
|
-
|
|
690
|
-
if (target?.sessionId && !runnerPage) {
|
|
691
|
-
throw new Error(
|
|
692
|
-
`Runner page session not found for browser dispatch: ${target.sessionId}`,
|
|
693
|
-
);
|
|
694
|
-
}
|
|
695
|
-
|
|
696
|
-
if (!runnerPage && !containerPage) {
|
|
697
|
-
throw new Error('Browser container page is not initialized');
|
|
698
|
-
}
|
|
699
|
-
|
|
700
|
-
try {
|
|
701
|
-
return await implementation.dispatchRpc({
|
|
702
|
-
containerPage: runnerPage ? undefined : containerPage,
|
|
703
|
-
runnerPage,
|
|
704
|
-
request,
|
|
705
|
-
timeoutFallbackMs,
|
|
706
|
-
});
|
|
707
|
-
} catch (error) {
|
|
708
|
-
// birpc serializes thrown Errors as `{}` over JSON; throw a string instead.
|
|
709
|
-
if (error instanceof Error) {
|
|
710
|
-
throw error.message;
|
|
711
|
-
}
|
|
712
|
-
throw String(error);
|
|
713
|
-
}
|
|
714
|
-
};
|
|
715
|
-
|
|
716
|
-
runtime.dispatchHandlers.set(
|
|
717
|
-
DISPATCH_NAMESPACE_BROWSER,
|
|
718
|
-
async (dispatchRequest) => {
|
|
719
|
-
const request = validateBrowserRpcRequest(dispatchRequest.args);
|
|
720
|
-
return dispatchBrowserRpcRequest({
|
|
721
|
-
request,
|
|
722
|
-
target: dispatchRequest.target,
|
|
723
|
-
});
|
|
724
|
-
},
|
|
725
|
-
);
|
|
726
|
-
|
|
727
|
-
runtime.setContainerOptions(hostOptions);
|
|
728
|
-
|
|
729
|
-
// Track test results from browser runners
|
|
730
|
-
const reporterResults: TestFileResult[] = [];
|
|
731
|
-
const caseResults: TestResult[] = [];
|
|
732
|
-
const fatalErrorRef = { current: null as Error | null };
|
|
733
|
-
|
|
734
|
-
// Runner lifecycle events flow through the shared RunnerEventSink (the same
|
|
735
|
-
// pump the node pool uses), so browser mode feeds stateManager and fans out to
|
|
736
|
-
// reporters via one implementation. One sink is bound per browser project up
|
|
737
|
-
// front from the executor's own project plan (`browserProjects`) — never from
|
|
738
|
-
// `context.projects`, which planning mutates to also contain node projects. The
|
|
739
|
-
// previous lazy resolver fell back to `context.projects[0]`, so a browser event
|
|
740
|
-
// could be attributed to a *node* project's config; binding per browser project
|
|
741
|
-
// here removes that fallback and keeps per-project `onConsoleLog` filtering and
|
|
742
|
-
// `resolveSnapshotPath` correct across browser projects that share a relative
|
|
743
|
-
// test path.
|
|
744
|
-
const runnerSinks = new Map<string, RunnerEventSink>(
|
|
745
|
-
browserProjects.map((project) => [
|
|
746
|
-
project.name,
|
|
747
|
-
createRunnerEventSink(context, project.normalizedConfig),
|
|
748
|
-
]),
|
|
749
|
-
);
|
|
750
|
-
// Every per-file wire payload carries its owning project name, so routing
|
|
751
|
-
// never derives a project from a test path — concurrent projects can run the
|
|
752
|
-
// same file, and a path-keyed lookup would attribute events to the wrong one.
|
|
753
|
-
// The client resolves its project from the host's own manifest, so a miss is
|
|
754
|
-
// a protocol bug; fail loudly rather than route through another project's
|
|
755
|
-
// config.
|
|
756
|
-
const sinkForProjectName = (projectName: string): RunnerEventSink => {
|
|
757
|
-
const sink = runnerSinks.get(projectName);
|
|
758
|
-
if (!sink) {
|
|
759
|
-
throw new Error(`No runner event sink for project "${projectName}"`);
|
|
760
|
-
}
|
|
761
|
-
return sink;
|
|
762
|
-
};
|
|
763
|
-
|
|
764
|
-
// Silent-console buffering runs through the shared controller — the same
|
|
765
|
-
// engine the node worker uses — so `silent: 'passed-only'` buffers logs and
|
|
766
|
-
// replays only the failing tasks'. Intercepted replays route through the
|
|
767
|
-
// owning project's sink, so they honor per-project `onConsoleLog` and
|
|
768
|
-
// `disableConsoleIntercept` (the browser host previously flushed straight to
|
|
769
|
-
// reporters, bypassing both). `writeOriginalLog` is a host-side no-op: page
|
|
770
|
-
// logs have no host "original stream" — the page console and headed terminal
|
|
771
|
-
// forwarding already show them, so re-emitting here would double-print.
|
|
772
|
-
const silentConsoleController = createSilentConsoleController({
|
|
773
|
-
runtimeConfig: {
|
|
774
|
-
silent: context.normalizedConfig.silent,
|
|
775
|
-
disableConsoleIntercept: context.normalizedConfig.disableConsoleIntercept,
|
|
776
|
-
},
|
|
777
|
-
emitInterceptedLog: (log) =>
|
|
778
|
-
sinkForProjectName(log.project).onConsoleLog(log),
|
|
779
|
-
writeOriginalLog: () => {},
|
|
780
|
-
});
|
|
781
|
-
|
|
782
|
-
const snapshotRpcMethods = {
|
|
783
|
-
async resolveSnapshotPath(testPath: string): Promise<string> {
|
|
784
|
-
return resolveSnapshotPathDefault(
|
|
785
|
-
testPath,
|
|
786
|
-
context.normalizedConfig.resolveSnapshotPath,
|
|
787
|
-
);
|
|
788
|
-
},
|
|
789
|
-
async readSnapshotFile(filepath: string): Promise<string | null> {
|
|
790
|
-
try {
|
|
791
|
-
return await fs.readFile(filepath, 'utf-8');
|
|
792
|
-
} catch {
|
|
793
|
-
return null;
|
|
794
|
-
}
|
|
795
|
-
},
|
|
796
|
-
async saveSnapshotFile(filepath: string, content: string): Promise<void> {
|
|
797
|
-
const dir = dirname(filepath);
|
|
798
|
-
await fs.mkdir(dir, { recursive: true });
|
|
799
|
-
await fs.writeFile(filepath, content, 'utf-8');
|
|
800
|
-
},
|
|
801
|
-
async removeSnapshotFile(filepath: string): Promise<void> {
|
|
802
|
-
try {
|
|
803
|
-
await fs.unlink(filepath);
|
|
804
|
-
} catch {
|
|
805
|
-
// ignore if file doesn't exist
|
|
806
|
-
}
|
|
807
|
-
},
|
|
808
|
-
};
|
|
809
|
-
|
|
810
|
-
const handleTestFileStart = async (
|
|
811
|
-
payload: TestFileStartPayload,
|
|
812
|
-
): Promise<void> => {
|
|
813
|
-
if (phaseTrackers) {
|
|
814
|
-
const tracker = new PhaseTracker({
|
|
815
|
-
trace: {
|
|
816
|
-
testPath: payload.testPath,
|
|
817
|
-
project: payload.projectName,
|
|
818
|
-
},
|
|
819
|
-
pid: nextBrowserFilePid++,
|
|
820
|
-
});
|
|
821
|
-
tracker.transition('prepare');
|
|
822
|
-
phaseTrackers.set(
|
|
823
|
-
trackerKey(payload.projectName, payload.testPath),
|
|
824
|
-
tracker,
|
|
825
|
-
);
|
|
826
|
-
}
|
|
827
|
-
// The client sends `{ testPath, projectName }`; the sink adapter builds the
|
|
828
|
-
// `TestFileInfo` the reporters and stateManager expect.
|
|
829
|
-
await sinkForProjectName(payload.projectName).onTestFileStart({
|
|
830
|
-
testId: getFileTaskId(payload.testPath),
|
|
831
|
-
testPath: payload.testPath,
|
|
832
|
-
project: payload.projectName,
|
|
833
|
-
tests: [],
|
|
834
|
-
});
|
|
835
|
-
};
|
|
836
|
-
|
|
837
|
-
const handleTestFileReady = async (
|
|
838
|
-
payload: TestFileReadyPayload,
|
|
839
|
-
): Promise<void> => {
|
|
840
|
-
phaseTrackers
|
|
841
|
-
?.get(trackerKey(payload.project, payload.testPath))
|
|
842
|
-
?.transition('tests');
|
|
843
|
-
await sinkForProjectName(payload.project).onTestFileReady(payload);
|
|
844
|
-
};
|
|
845
|
-
|
|
846
|
-
const handleTestSuiteStart = async (
|
|
847
|
-
payload: TestSuiteStartPayload,
|
|
848
|
-
): Promise<void> => {
|
|
849
|
-
phaseTrackers
|
|
850
|
-
?.get(trackerKey(payload.project, payload.testPath))
|
|
851
|
-
?.recordSuiteStart(payload);
|
|
852
|
-
await sinkForProjectName(payload.project).onTestSuiteStart(payload);
|
|
853
|
-
};
|
|
854
|
-
|
|
855
|
-
const handleTestSuiteResult = async (
|
|
856
|
-
payload: TestSuiteResultPayload,
|
|
857
|
-
): Promise<void> => {
|
|
858
|
-
phaseTrackers
|
|
859
|
-
?.get(trackerKey(payload.project, payload.testPath))
|
|
860
|
-
?.recordSuiteResult(payload);
|
|
861
|
-
await sinkForProjectName(payload.project).onTestSuiteResult(payload);
|
|
862
|
-
|
|
863
|
-
if (context.normalizedConfig.silent === 'passed-only') {
|
|
864
|
-
silentConsoleController.flushBufferedLogsForTask({
|
|
865
|
-
taskId: payload.testId,
|
|
866
|
-
status: payload.status,
|
|
867
|
-
taskParentNames: payload.parentNames,
|
|
868
|
-
taskType: 'suite',
|
|
869
|
-
testPath: payload.testPath,
|
|
870
|
-
});
|
|
871
|
-
}
|
|
872
|
-
};
|
|
873
|
-
|
|
874
|
-
const handleTestCaseStart = async (
|
|
875
|
-
payload: TestCaseStartPayload,
|
|
876
|
-
): Promise<void> => {
|
|
877
|
-
phaseTrackers
|
|
878
|
-
?.get(trackerKey(payload.project, payload.testPath))
|
|
879
|
-
?.recordCaseStart(payload);
|
|
880
|
-
// Fire-and-forget on both transports (the sink does not await case-start).
|
|
881
|
-
sinkForProjectName(payload.project).onTestCaseStart(payload);
|
|
882
|
-
};
|
|
883
|
-
|
|
884
|
-
const handleTestCaseResult = async (payload: TestResult): Promise<void> => {
|
|
885
|
-
caseResults.push(payload);
|
|
886
|
-
phaseTrackers
|
|
887
|
-
?.get(trackerKey(payload.project, payload.testPath))
|
|
888
|
-
?.recordCaseResult(payload);
|
|
889
|
-
await sinkForProjectName(payload.project).onTestCaseResult(payload);
|
|
890
|
-
|
|
891
|
-
if (context.normalizedConfig.silent === 'passed-only') {
|
|
892
|
-
silentConsoleController.flushBufferedLogsForTask({
|
|
893
|
-
taskId: payload.testId,
|
|
894
|
-
status: payload.status,
|
|
895
|
-
taskParentNames: payload.parentNames,
|
|
896
|
-
taskType: 'case',
|
|
897
|
-
testPath: payload.testPath,
|
|
898
|
-
});
|
|
899
|
-
}
|
|
900
|
-
};
|
|
901
|
-
|
|
902
|
-
const handleTestFileComplete = async (
|
|
903
|
-
payload: TestFileResult,
|
|
904
|
-
): Promise<void> => {
|
|
905
|
-
reporterResults.push(payload);
|
|
906
|
-
context.updateReporterResultState([payload], payload.results);
|
|
907
|
-
|
|
908
|
-
if (phaseTrackers) {
|
|
909
|
-
const key = trackerKey(payload.project, payload.testPath);
|
|
910
|
-
const tracker = phaseTrackers.get(key);
|
|
911
|
-
if (tracker) {
|
|
912
|
-
tracker.end();
|
|
913
|
-
const events = tracker.getTraceEvents();
|
|
914
|
-
if (events) onTraceEvents?.(events);
|
|
915
|
-
phaseTrackers.delete(key);
|
|
916
|
-
}
|
|
917
|
-
}
|
|
918
|
-
|
|
919
|
-
if (context.normalizedConfig.silent === 'passed-only') {
|
|
920
|
-
silentConsoleController.flushBufferedLogsForTask({
|
|
921
|
-
taskId: payload.testId,
|
|
922
|
-
status: payload.status,
|
|
923
|
-
taskParentNames: payload.parentNames,
|
|
924
|
-
taskType: 'file',
|
|
925
|
-
testPath: payload.testPath,
|
|
926
|
-
});
|
|
927
|
-
}
|
|
928
|
-
|
|
929
|
-
// Feeds stateManager, fans out onTestFileResult to reporters, and ingests
|
|
930
|
-
// payload.snapshotResult (the snapshotManager.add moved into the sink).
|
|
931
|
-
await sinkForProjectName(payload.project).onTestFileResult(payload);
|
|
932
|
-
};
|
|
933
|
-
|
|
934
|
-
const handleLog = async (payload: LogPayload): Promise<void> => {
|
|
935
|
-
const log: UserConsoleLog = {
|
|
936
|
-
content: payload.content,
|
|
937
|
-
// Same colored level label as the node worker's CustomConsole.
|
|
938
|
-
name: getPrettyConsoleName(payload.level),
|
|
939
|
-
taskId: payload.taskId,
|
|
940
|
-
taskName: payload.taskName,
|
|
941
|
-
taskParentNames: payload.taskParentNames,
|
|
942
|
-
taskType: payload.taskType,
|
|
943
|
-
testPath: payload.testPath,
|
|
944
|
-
project: payload.projectName,
|
|
945
|
-
type: payload.type,
|
|
946
|
-
trace: payload.trace,
|
|
947
|
-
};
|
|
948
|
-
silentConsoleController.onConsoleLog(log);
|
|
949
|
-
};
|
|
950
|
-
|
|
951
|
-
// Every failing file and every fatal error reaches core through the cycle
|
|
952
|
-
// outcome on both commands, so the exit code keeps a single writer:
|
|
953
|
-
// `finalizeRunCycle`.
|
|
954
|
-
const handleFatal = async (payload: FatalPayload): Promise<void> => {
|
|
955
|
-
const error = new Error(payload.message);
|
|
956
|
-
error.stack = payload.stack;
|
|
957
|
-
fatalErrorRef.current = error;
|
|
958
|
-
};
|
|
959
|
-
|
|
960
|
-
const runSnapshotRpc = async (
|
|
961
|
-
request: SnapshotRpcRequest,
|
|
962
|
-
): Promise<unknown> => {
|
|
963
|
-
switch (request.method) {
|
|
964
|
-
case 'resolveSnapshotPath':
|
|
965
|
-
return snapshotRpcMethods.resolveSnapshotPath(request.args.testPath);
|
|
966
|
-
case 'readSnapshotFile':
|
|
967
|
-
return snapshotRpcMethods.readSnapshotFile(request.args.filepath);
|
|
968
|
-
case 'saveSnapshotFile':
|
|
969
|
-
return snapshotRpcMethods.saveSnapshotFile(
|
|
970
|
-
request.args.filepath,
|
|
971
|
-
request.args.content,
|
|
972
|
-
);
|
|
973
|
-
case 'removeSnapshotFile':
|
|
974
|
-
return snapshotRpcMethods.removeSnapshotFile(request.args.filepath);
|
|
975
|
-
default: {
|
|
976
|
-
// Exhaustiveness guard: a new SnapshotRpcRequest method without a case
|
|
977
|
-
// here fails to compile rather than silently returning undefined.
|
|
978
|
-
const _exhaustive: never = request;
|
|
979
|
-
return _exhaustive;
|
|
980
|
-
}
|
|
981
|
-
}
|
|
982
|
-
};
|
|
983
|
-
|
|
984
|
-
const createDispatchRouter = (options?: HostDispatchRouterOptions) => {
|
|
985
|
-
return createHostDispatchRouter({
|
|
986
|
-
routerOptions: options,
|
|
987
|
-
runnerCallbacks: {
|
|
988
|
-
onTestFileStart: handleTestFileStart,
|
|
989
|
-
onTestFileReady: handleTestFileReady,
|
|
990
|
-
onTestSuiteStart: handleTestSuiteStart,
|
|
991
|
-
onTestSuiteResult: handleTestSuiteResult,
|
|
992
|
-
onTestCaseStart: handleTestCaseStart,
|
|
993
|
-
onTestCaseResult: handleTestCaseResult,
|
|
994
|
-
onTestFileComplete: handleTestFileComplete,
|
|
995
|
-
onLog: handleLog,
|
|
996
|
-
onFatal: handleFatal,
|
|
997
|
-
},
|
|
998
|
-
runSnapshotRpc,
|
|
999
|
-
extensionHandlers: runtime.dispatchHandlers,
|
|
1000
|
-
onDuplicateNamespace: (namespace) => {
|
|
1001
|
-
logger.debug(
|
|
1002
|
-
`[Dispatch] Skip registering dispatch namespace "${namespace}" because it is already reserved`,
|
|
1003
|
-
);
|
|
1004
|
-
},
|
|
1005
|
-
});
|
|
1006
|
-
};
|
|
1007
|
-
|
|
1008
|
-
const schedulerDeps = {
|
|
1009
|
-
context,
|
|
1010
|
-
allTestFiles,
|
|
1011
|
-
hostOptions,
|
|
1012
|
-
isWatchMode,
|
|
1013
|
-
createDispatchRouter,
|
|
1014
|
-
fatalErrorRef,
|
|
1015
|
-
watchSignals,
|
|
1016
|
-
setDispatchPageResolver,
|
|
1017
|
-
createWatchSession,
|
|
1018
|
-
collectProjectEntries: () => collectProjectEntries(context),
|
|
1019
|
-
logWatchReady: () => logWatchReadyMessage(context, enableCliShortcuts),
|
|
1020
|
-
destroyRuntime: () => destroyBrowserRuntime(runtime),
|
|
1021
|
-
};
|
|
1022
|
-
|
|
1023
|
-
const { testTime, watchSession, close } = useHeadlessDirect
|
|
1024
|
-
? await createHeadlessScheduler({
|
|
1025
|
-
...schedulerDeps,
|
|
1026
|
-
browser,
|
|
1027
|
-
browserLaunchOptions,
|
|
1028
|
-
projectServers: runtime.projectServers,
|
|
1029
|
-
projectRuntimeConfigs,
|
|
1030
|
-
watchState,
|
|
1031
|
-
handlers: { handleFatal, handleTestFileComplete },
|
|
1032
|
-
})
|
|
1033
|
-
: await createHeadedScheduler({
|
|
1034
|
-
...schedulerDeps,
|
|
1035
|
-
runtime,
|
|
1036
|
-
handlers: {
|
|
1037
|
-
handleTestFileStart,
|
|
1038
|
-
handleTestCaseResult,
|
|
1039
|
-
handleTestFileComplete,
|
|
1040
|
-
handleLog,
|
|
1041
|
-
handleFatal,
|
|
1042
|
-
},
|
|
1043
|
-
});
|
|
1044
|
-
|
|
1045
|
-
// A fatal error the run reported outranks its results: it rides the returned
|
|
1046
|
-
// outcome into core's finalize, which raises the exit code from it.
|
|
1047
|
-
if (fatalErrorRef.current) {
|
|
1048
|
-
return failWithError(fatalErrorRef.current, close);
|
|
1049
|
-
}
|
|
1050
|
-
|
|
1051
|
-
context.updateReporterResultState(reporterResults, caseResults);
|
|
1052
|
-
|
|
1053
|
-
// Enable the compile hooks only after the initial cycle, so the first build
|
|
1054
|
-
// never triggers a duplicate run.
|
|
1055
|
-
watchState.hooksEnabled = isWatchMode;
|
|
1056
|
-
|
|
1057
|
-
return {
|
|
1058
|
-
results: reporterResults,
|
|
1059
|
-
testResults: caseResults,
|
|
1060
|
-
duration: {
|
|
1061
|
-
totalTime: buildTime + testTime,
|
|
1062
|
-
buildTime,
|
|
1063
|
-
testTime,
|
|
1064
|
-
},
|
|
1065
|
-
hasFailure: reporterResults.some(
|
|
1066
|
-
(result: TestFileResult) => result.status === 'fail',
|
|
1067
|
-
),
|
|
1068
|
-
getSourcemap: getBrowserSourcemap,
|
|
1069
|
-
resolveSourcemap: resolveBrowserSourcemap,
|
|
1070
|
-
// `close` is already `undefined` in watch mode: the watch runtime outlives
|
|
1071
|
-
// the cycle and is torn down through `executor.close()`.
|
|
1072
|
-
close,
|
|
1073
|
-
watchSession,
|
|
1074
|
-
};
|
|
1075
|
-
};
|
|
1076
|
-
// ============================================================================
|
|
1077
|
-
// List Browser Tests
|
|
1078
|
-
// ============================================================================
|
|
1079
|
-
|
|
1080
|
-
/**
|
|
1081
|
-
* Result from collecting browser tests.
|
|
1082
|
-
* This is the return type for listBrowserTests, designed for future extraction
|
|
1083
|
-
* to a separate browser package.
|
|
1084
|
-
*/
|
|
1085
|
-
export type ListBrowserTestsResult = {
|
|
1086
|
-
list: ListCommandResult[];
|
|
1087
|
-
close: () => Promise<void>;
|
|
1088
|
-
};
|
|
1089
|
-
|
|
1090
|
-
/**
|
|
1091
|
-
* Collect test metadata from browser mode projects without running them.
|
|
1092
|
-
* This function creates a headless browser runtime, loads test files,
|
|
1093
|
-
* and collects their test structure (describe/test declarations).
|
|
1094
|
-
*/
|
|
1095
|
-
export const listBrowserTests = async (
|
|
1096
|
-
context: RstestContext,
|
|
1097
|
-
options?: ListBrowserTestsOptions,
|
|
1098
|
-
): Promise<ListBrowserTestsResult> => {
|
|
1099
|
-
const browserProjects = options?.projects ?? getBrowserProjects(context);
|
|
1100
|
-
const projectEntries = await resolveProjectEntries(
|
|
1101
|
-
context,
|
|
1102
|
-
options?.shardedEntries,
|
|
1103
|
-
browserProjects,
|
|
1104
|
-
);
|
|
1105
|
-
const totalTests = projectEntries.reduce(
|
|
1106
|
-
(total, item) => total + item.testFiles.length,
|
|
1107
|
-
0,
|
|
1108
|
-
);
|
|
1109
|
-
|
|
1110
|
-
if (totalTests === 0 && !hasUserRstestConfigPlugins(browserProjects)) {
|
|
1111
|
-
return {
|
|
1112
|
-
list: [],
|
|
1113
|
-
close: async () => {},
|
|
1114
|
-
};
|
|
1115
|
-
}
|
|
1116
|
-
|
|
1117
|
-
const tempDir = join(
|
|
1118
|
-
context.rootPath,
|
|
1119
|
-
context.normalizedConfig.output.distPath.root,
|
|
1120
|
-
'browser',
|
|
1121
|
-
`list-${Date.now()}`,
|
|
1122
|
-
);
|
|
1123
|
-
|
|
1124
|
-
// Create a simplified browser runtime for collect mode
|
|
1125
|
-
let runtime: BrowserRuntime;
|
|
1126
|
-
try {
|
|
1127
|
-
runtime = await createBrowserRuntime({
|
|
1128
|
-
context,
|
|
1129
|
-
projectEntries,
|
|
1130
|
-
browserProjects,
|
|
1131
|
-
shardedEntries: options?.shardedEntries,
|
|
1132
|
-
freezeShardedEntries: options?.freezeShardedEntries,
|
|
1133
|
-
tempDir,
|
|
1134
|
-
isWatchMode: false,
|
|
1135
|
-
containerDistPath: undefined,
|
|
1136
|
-
containerDevServer: undefined,
|
|
1137
|
-
forceHeadless: true, // Always use headless for list command
|
|
1138
|
-
skipProviderLaunch: options?.filesOnly,
|
|
1139
|
-
appliedModifyRstestConfigEnvironments:
|
|
1140
|
-
options?.appliedModifyRstestConfigEnvironments,
|
|
1141
|
-
});
|
|
1142
|
-
} catch (error) {
|
|
1143
|
-
const providers = [
|
|
1144
|
-
...new Set(
|
|
1145
|
-
browserProjects.map((p) => p.normalizedConfig.browser.provider),
|
|
1146
|
-
),
|
|
1147
|
-
];
|
|
1148
|
-
logger.error(
|
|
1149
|
-
color.red(
|
|
1150
|
-
`Failed to initialize browser provider runtime (${providers.join(', ')}).`,
|
|
1151
|
-
),
|
|
1152
|
-
error,
|
|
1153
|
-
);
|
|
1154
|
-
throw error;
|
|
1155
|
-
}
|
|
1156
|
-
|
|
1157
|
-
if (options?.filesOnly) {
|
|
1158
|
-
const list = runtime.projectEntries.flatMap((entry) =>
|
|
1159
|
-
entry.testFiles.map((testPath) => ({
|
|
1160
|
-
testPath,
|
|
1161
|
-
project: entry.project.name,
|
|
1162
|
-
tests: [],
|
|
1163
|
-
})),
|
|
1164
|
-
);
|
|
1165
|
-
await destroyBrowserRuntime(runtime);
|
|
1166
|
-
return {
|
|
1167
|
-
list,
|
|
1168
|
-
close: async () => {},
|
|
1169
|
-
};
|
|
1170
|
-
}
|
|
1171
|
-
|
|
1172
|
-
if (!runtime.projectEntries.some((entry) => entry.testFiles.length > 0)) {
|
|
1173
|
-
await destroyBrowserRuntime(runtime);
|
|
1174
|
-
return {
|
|
1175
|
-
list: [],
|
|
1176
|
-
close: async () => {},
|
|
1177
|
-
};
|
|
1178
|
-
}
|
|
1179
|
-
|
|
1180
|
-
const { browser, browserLaunchOptions } = runtime;
|
|
1181
|
-
|
|
1182
|
-
// Get browser projects for runtime config
|
|
1183
|
-
// Normalize projectRoot to posix format for cross-platform compatibility
|
|
1184
|
-
const projectRuntimeConfigs: BrowserProjectRuntime[] = browserProjects.map(
|
|
1185
|
-
(project: ProjectContext) => ({
|
|
1186
|
-
name: project.name,
|
|
1187
|
-
environmentName: project.environmentName,
|
|
1188
|
-
projectRoot: normalize(project.rootPath),
|
|
1189
|
-
runtimeConfig: serializableConfig(
|
|
1190
|
-
projectRuntimeConfig(project, {
|
|
1191
|
-
envMode: 'static',
|
|
1192
|
-
envOverlay: options?.env,
|
|
1193
|
-
}),
|
|
1194
|
-
),
|
|
1195
|
-
viewport: project.normalizedConfig.browser.viewport,
|
|
1196
|
-
}),
|
|
1197
|
-
);
|
|
1198
|
-
|
|
1199
|
-
const maxTestTimeoutForRpc = getMaxTestTimeoutForRpc(browserProjects);
|
|
1200
|
-
|
|
1201
|
-
const hostOptions: BrowserHostConfig = {
|
|
1202
|
-
rootPath: normalize(context.rootPath),
|
|
1203
|
-
projects: projectRuntimeConfigs,
|
|
1204
|
-
snapshot: {
|
|
1205
|
-
updateSnapshot: context.snapshotManager.options.updateSnapshot,
|
|
1206
|
-
},
|
|
1207
|
-
mode: 'collect', // Use collect mode
|
|
1208
|
-
debug: isDebug(),
|
|
1209
|
-
rpcTimeout: maxTestTimeoutForRpc,
|
|
1210
|
-
};
|
|
1211
|
-
|
|
1212
|
-
runtime.setContainerOptions(hostOptions);
|
|
1213
|
-
|
|
1214
|
-
// Collect results across every project's isolated dev server. Each server
|
|
1215
|
-
// serves only its own project's manifest, so collection navigates one page
|
|
1216
|
-
// per project; each page returns its own results, aggregated afterwards.
|
|
1217
|
-
const browserContext = await browser.newContext({
|
|
1218
|
-
providerOptions: browserLaunchOptions.providerOptions,
|
|
1219
|
-
viewport: null,
|
|
1220
|
-
});
|
|
1221
|
-
|
|
1222
|
-
const serializedOptions = serializeForInlineScript(hostOptions);
|
|
1223
|
-
|
|
1224
|
-
// Per-page collect watchdog: a test file whose module evaluation stalls must
|
|
1225
|
-
// not hang `rstest list` forever.
|
|
1226
|
-
const collectTimeoutMs = 30_000;
|
|
1227
|
-
|
|
1228
|
-
const collectFromServer = async (
|
|
1229
|
-
server: BrowserProjectServer,
|
|
1230
|
-
): Promise<{ results: ListCommandResult[]; error: Error | null }> => {
|
|
1231
|
-
const results: ListCommandResult[] = [];
|
|
1232
|
-
let error: Error | null = null;
|
|
1233
|
-
let collectCompleted = false;
|
|
1234
|
-
let resolveCollect: (() => void) | undefined;
|
|
1235
|
-
const collectPromise = new Promise<void>((resolve) => {
|
|
1236
|
-
resolveCollect = resolve;
|
|
1237
|
-
});
|
|
1238
|
-
|
|
1239
|
-
const page = await browserContext.newPage();
|
|
1240
|
-
|
|
1241
|
-
// Expose dispatch function for browser client to send messages
|
|
1242
|
-
await page.exposeFunction(
|
|
1243
|
-
DISPATCH_MESSAGE_TYPE,
|
|
1244
|
-
(message: { type: string; payload?: unknown }) => {
|
|
1245
|
-
switch (message.type) {
|
|
1246
|
-
case 'collect-result': {
|
|
1247
|
-
const payload = message.payload as {
|
|
1248
|
-
testPath: string;
|
|
1249
|
-
project: string;
|
|
1250
|
-
tests: Test[];
|
|
1251
|
-
};
|
|
1252
|
-
results.push({
|
|
1253
|
-
testPath: payload.testPath,
|
|
1254
|
-
project: payload.project,
|
|
1255
|
-
tests: payload.tests,
|
|
1256
|
-
});
|
|
1257
|
-
break;
|
|
1258
|
-
}
|
|
1259
|
-
case 'collect-complete':
|
|
1260
|
-
collectCompleted = true;
|
|
1261
|
-
resolveCollect?.();
|
|
1262
|
-
break;
|
|
1263
|
-
case 'fatal': {
|
|
1264
|
-
const payload = message.payload as {
|
|
1265
|
-
message: string;
|
|
1266
|
-
stack?: string;
|
|
1267
|
-
};
|
|
1268
|
-
error = new Error(payload.message);
|
|
1269
|
-
error.stack = payload.stack;
|
|
1270
|
-
resolveCollect?.();
|
|
1271
|
-
break;
|
|
1272
|
-
}
|
|
1273
|
-
case 'ready':
|
|
1274
|
-
case 'log':
|
|
1275
|
-
// Ignore these messages during collection
|
|
1276
|
-
break;
|
|
1277
|
-
default:
|
|
1278
|
-
// Log unexpected messages for debugging
|
|
1279
|
-
logger.debug(`[List] Unexpected message: ${message.type}`);
|
|
1280
|
-
}
|
|
1281
|
-
},
|
|
1282
|
-
);
|
|
1283
|
-
|
|
1284
|
-
// Inject host options before navigation so the runner can access them
|
|
1285
|
-
await page.addInitScript(
|
|
1286
|
-
`window.__RSTEST_BROWSER_OPTIONS__ = ${serializedOptions};`,
|
|
1287
|
-
);
|
|
1288
|
-
|
|
1289
|
-
// Navigate to this project's runner page
|
|
1290
|
-
await page.goto(`http://localhost:${server.port}/runner.html`, {
|
|
1291
|
-
waitUntil: 'load',
|
|
1292
|
-
});
|
|
1293
|
-
|
|
1294
|
-
// Wait for collection to complete with the shared collect timeout.
|
|
1295
|
-
let timeoutId: ReturnType<typeof setTimeout> | undefined;
|
|
1296
|
-
const timeoutPromise = new Promise<void>((resolve) => {
|
|
1297
|
-
timeoutId = setTimeout(() => {
|
|
1298
|
-
if (!collectCompleted) {
|
|
1299
|
-
logger.warn(
|
|
1300
|
-
color.yellow(
|
|
1301
|
-
`[List] Browser test collection timed out after ${collectTimeoutMs}ms`,
|
|
1302
|
-
),
|
|
1303
|
-
);
|
|
1304
|
-
}
|
|
1305
|
-
resolve();
|
|
1306
|
-
}, collectTimeoutMs);
|
|
1307
|
-
});
|
|
1308
|
-
|
|
1309
|
-
await Promise.race([collectPromise, timeoutPromise]);
|
|
1310
|
-
|
|
1311
|
-
// Clear timeout to prevent Node.js from waiting for it
|
|
1312
|
-
if (timeoutId) {
|
|
1313
|
-
clearTimeout(timeoutId);
|
|
1314
|
-
}
|
|
1315
|
-
|
|
1316
|
-
await page.close().catch(() => {});
|
|
1317
|
-
return { results, error };
|
|
1318
|
-
};
|
|
1319
|
-
|
|
1320
|
-
// Collect every project concurrently — each navigates its own page against
|
|
1321
|
-
// its own dev server and returns its own results.
|
|
1322
|
-
const collected = await Promise.all(
|
|
1323
|
-
[...runtime.projectServers.values()].map((server) =>
|
|
1324
|
-
collectFromServer(server),
|
|
1325
|
-
),
|
|
1326
|
-
);
|
|
1327
|
-
const collectResults = collected.flatMap((entry) => entry.results);
|
|
1328
|
-
const fatalError = collected.find((entry) => entry.error)?.error ?? null;
|
|
1329
|
-
|
|
1330
|
-
// Cleanup
|
|
1331
|
-
const cleanup = async () => {
|
|
1332
|
-
try {
|
|
1333
|
-
await browserContext.close();
|
|
1334
|
-
} catch {
|
|
1335
|
-
// ignore
|
|
1336
|
-
}
|
|
1337
|
-
await destroyBrowserRuntime(runtime);
|
|
1338
|
-
};
|
|
1339
|
-
|
|
1340
|
-
if (fatalError) {
|
|
1341
|
-
await cleanup();
|
|
1342
|
-
// Return error in the result format instead of throwing
|
|
1343
|
-
const errorResult: ListCommandResult = {
|
|
1344
|
-
testPath: '',
|
|
1345
|
-
project: '',
|
|
1346
|
-
tests: [],
|
|
1347
|
-
errors: [
|
|
1348
|
-
{
|
|
1349
|
-
name: 'BrowserCollectError',
|
|
1350
|
-
message: fatalError.message,
|
|
1351
|
-
stack: fatalError.stack,
|
|
1352
|
-
} as FormattedError,
|
|
1353
|
-
],
|
|
1354
|
-
};
|
|
1355
|
-
return {
|
|
1356
|
-
list: [errorResult],
|
|
1357
|
-
close: async () => {},
|
|
1358
|
-
};
|
|
1359
|
-
}
|
|
1360
|
-
|
|
1361
|
-
return {
|
|
1362
|
-
list: collectResults,
|
|
1363
|
-
close: cleanup,
|
|
1364
|
-
};
|
|
1365
|
-
};
|