@rstest/browser 0.11.5 → 0.11.7

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.
Files changed (86) hide show
  1. package/dist/browser-container/container-static/css/index.43daaf6c93.css +1 -0
  2. package/dist/browser-container/container-static/js/260.3739fe815c.js +194 -0
  3. package/dist/browser-container/container-static/js/index.c0f87bfb3e.js +1 -0
  4. package/dist/browser-container/index.html +1 -1
  5. package/dist/browserExecutor.d.ts +7 -5
  6. package/dist/browserRsbuild.d.ts +139 -0
  7. package/dist/browserV8Coverage.d.ts +15 -0
  8. package/dist/client/240~1.js +150 -0
  9. package/dist/{augmentExpect.d.ts → client/client/augmentExpect.d.ts} +1 -1
  10. package/dist/client/{browserRpc.d.ts → client/browserRpc.d.ts} +1 -1
  11. package/dist/client/{dispatchTransport.d.ts → client/dispatchTransport.d.ts} +8 -4
  12. package/dist/client/client/index.d.ts +2 -0
  13. package/dist/client/client/runIdentity.d.ts +12 -0
  14. package/dist/client/client/runner.d.ts +4 -0
  15. package/dist/client/client/snapshot.d.ts +27 -0
  16. package/dist/client/client/sourceMapSupport.d.ts +36 -0
  17. package/dist/{browser.js → client/index.js} +3 -102
  18. package/dist/client/protocol.d.ts +225 -0
  19. package/dist/client/rpcProtocol.d.ts +138 -0
  20. package/dist/client/runner.js +675 -0
  21. package/dist/client/sourceMap/sourceMapLoader.d.ts +25 -0
  22. package/dist/containerRpc.d.ts +60 -0
  23. package/dist/dispatchCapabilities.d.ts +1 -2
  24. package/dist/headedRunRegistry.d.ts +51 -0
  25. package/dist/headedScheduler.d.ts +58 -0
  26. package/dist/headlessScheduler.d.ts +38 -0
  27. package/dist/headlessTransport.d.ts +4 -2
  28. package/dist/hostController.d.ts +12 -47
  29. package/dist/hostPayloads.d.ts +24 -0
  30. package/dist/index.js +2019 -1516
  31. package/dist/protocol.d.ts +48 -8
  32. package/dist/providers/index.d.ts +21 -0
  33. package/dist/providers/playwright/v8Coverage.d.ts +2 -0
  34. package/dist/rpcProtocol.d.ts +1 -2
  35. package/dist/schedulerSeam.d.ts +42 -0
  36. package/dist/sourceMap/sourceMapLoader.d.ts +12 -1
  37. package/dist/watchRerunPlanner.d.ts +20 -4
  38. package/dist/watchRuntime.d.ts +21 -0
  39. package/dist/watchSignals.d.ts +29 -0
  40. package/package.json +12 -12
  41. package/dist/33.js +0 -7
  42. package/dist/browser-container/container-static/css/index.0521ff95b1.css +0 -1
  43. package/dist/browser-container/container-static/js/506.b2f920fa67.js +0 -194
  44. package/dist/browser-container/container-static/js/index.d27fd63f5e.js +0 -1
  45. package/dist/browser.d.ts +0 -2
  46. package/dist/headlessLatestRerunScheduler.d.ts +0 -18
  47. package/src/augmentExpect.ts +0 -62
  48. package/src/browser.ts +0 -3
  49. package/src/browserExecutor.ts +0 -140
  50. package/src/browserRpcRegistry.ts +0 -57
  51. package/src/client/api.ts +0 -213
  52. package/src/client/browserRpc.ts +0 -86
  53. package/src/client/dispatchTransport.ts +0 -240
  54. package/src/client/entry.ts +0 -807
  55. package/src/client/locator.ts +0 -448
  56. package/src/client/snapshot.ts +0 -113
  57. package/src/client/sourceMapSupport.ts +0 -159
  58. package/src/concurrency.ts +0 -65
  59. package/src/configValidation.ts +0 -263
  60. package/src/dispatchCapabilities.ts +0 -182
  61. package/src/dispatchRouter.ts +0 -82
  62. package/src/env.d.ts +0 -54
  63. package/src/headedSerialTaskQueue.ts +0 -23
  64. package/src/headlessLatestRerunScheduler.ts +0 -76
  65. package/src/headlessTransport.ts +0 -28
  66. package/src/hostController.ts +0 -4633
  67. package/src/index.ts +0 -51
  68. package/src/manifest.d.ts +0 -41
  69. package/src/protocol.ts +0 -225
  70. package/src/providers/index.ts +0 -126
  71. package/src/providers/playwright/compileLocator.ts +0 -130
  72. package/src/providers/playwright/dispatchBrowserRpc.ts +0 -372
  73. package/src/providers/playwright/expectUtils.ts +0 -57
  74. package/src/providers/playwright/implementation.ts +0 -35
  75. package/src/providers/playwright/index.ts +0 -1
  76. package/src/providers/playwright/runtime.ts +0 -159
  77. package/src/providers/playwright/textMatcher.ts +0 -10
  78. package/src/rpcProtocol.ts +0 -218
  79. package/src/runSession.ts +0 -110
  80. package/src/sessionRegistry.ts +0 -89
  81. package/src/sourceMap/sourceMapLoader.ts +0 -96
  82. package/src/viewportPresets.ts +0 -69
  83. package/src/watchRerunPlanner.ts +0 -77
  84. /package/dist/{rslib-runtime.js → 612~0.js} +0 -0
  85. /package/dist/client/{api.d.ts → client/api.d.ts} +0 -0
  86. /package/dist/client/{locator.d.ts → client/locator.d.ts} +0 -0
@@ -1,807 +0,0 @@
1
- import {
2
- type ManifestProjectConfig,
3
- type ManifestTestContext,
4
- projectSetupLoaders,
5
- // Multi-project APIs
6
- projects,
7
- projectTestContexts,
8
- } from '@rstest/browser-manifest';
9
- import type {
10
- CoverageMapData,
11
- CurrentTaskInfo,
12
- RunnerHooks,
13
- RuntimeConfig,
14
- WorkerState,
15
- } from '@rstest/core/internal/browser-runtime';
16
- import {
17
- createBrowserTaskContext,
18
- createRstestRuntime,
19
- formatConsoleArgs,
20
- globalApis,
21
- RSTEST_API_GLOBAL_KEY,
22
- RSTEST_ENV_SYMBOL_KEY,
23
- setRealTimers,
24
- unwrapRegex,
25
- } from '@rstest/core/internal/browser-runtime';
26
- import { normalize } from 'pathe';
27
- import type {
28
- BrowserClientMessage,
29
- BrowserProjectRuntime,
30
- RunnerLifecycleMethod,
31
- } from '../protocol';
32
- import { DISPATCH_MESSAGE_TYPE, RSTEST_CONFIG_MESSAGE_TYPE } from '../protocol';
33
- import {
34
- createRunnerLifecycleRequest,
35
- sendRunnerLifecycle,
36
- } from './dispatchTransport';
37
- import { BrowserSnapshotEnvironment } from './snapshot';
38
- import {
39
- findNewScriptUrl,
40
- getScriptUrls,
41
- preloadRunnerSourceMap,
42
- preloadTestFileSourceMap,
43
- } from './sourceMapSupport';
44
-
45
- declare global {
46
- // eslint-disable-next-line no-var
47
- var __coverage__: Record<string, unknown> | undefined;
48
- }
49
-
50
- /**
51
- * Debug logger for browser client.
52
- * Only logs when debug mode is enabled (DEBUG=rstest on server side).
53
- */
54
- const debugLog = (...args: unknown[]): void => {
55
- if (window.__RSTEST_BROWSER_OPTIONS__?.debug) {
56
- console.log(...args);
57
- }
58
- };
59
-
60
- type RuntimeEnvStore = Record<string, string | undefined>;
61
- const RSTEST_ENV_SYMBOL = Symbol.for(RSTEST_ENV_SYMBOL_KEY);
62
-
63
- /**
64
- * Publish the runtime API on the globals test modules read: the
65
- * `@rstest/core` external and the `import.meta.rstest` define (node parity:
66
- * `global['@rstest/core']` in runInPool), plus the `globals: true` API names.
67
- */
68
- const installRuntimeGlobals = (
69
- runtime: Awaited<ReturnType<typeof createRstestRuntime>>,
70
- runtimeConfig: RuntimeConfig,
71
- ): void => {
72
- (globalThis as Record<string, unknown>)[RSTEST_API_GLOBAL_KEY] = runtime.api;
73
- if (runtimeConfig.globals) {
74
- for (const apiKey of globalApis) {
75
- (globalThis as any)[apiKey] = (runtime.api as any)[apiKey];
76
- }
77
- }
78
- };
79
-
80
- type GlobalWithRuntimeEnv = typeof globalThis &
81
- Record<symbol, unknown> & {
82
- global?: typeof globalThis;
83
- };
84
-
85
- const restoreRuntimeConfig = (
86
- config: BrowserProjectRuntime['runtimeConfig'],
87
- ): RuntimeConfig => {
88
- const { testNamePattern } = config;
89
- // The browser wire (BrowserRuntimeConfig) omits node-only fields
90
- // (testEnvironment / coverage / logHeapUsage / detectAsyncLeaks) that the
91
- // browser runtime never reads. The shared WorkerState / runner types require
92
- // the full RuntimeConfig shape and runner heap sampling is guarded against
93
- // the absent logHeapUsage, so widening back here is sound.
94
- return {
95
- ...config,
96
- testNamePattern:
97
- typeof testNamePattern === 'string'
98
- ? unwrapRegex(testNamePattern)
99
- : testNamePattern,
100
- } as RuntimeConfig;
101
- };
102
-
103
- const ensureRuntimeEnv = (env: RuntimeConfig['env'] | undefined): void => {
104
- const globalRef = globalThis as GlobalWithRuntimeEnv;
105
- if (!globalRef.global) {
106
- globalRef.global = globalRef;
107
- }
108
-
109
- const existingEnv = globalRef[RSTEST_ENV_SYMBOL];
110
- let runtimeEnv: RuntimeEnvStore;
111
- if (existingEnv && typeof existingEnv === 'object') {
112
- runtimeEnv = existingEnv as RuntimeEnvStore;
113
- } else {
114
- runtimeEnv = {};
115
- globalRef[RSTEST_ENV_SYMBOL] = runtimeEnv;
116
- }
117
-
118
- if (env) {
119
- for (const [key, value] of Object.entries(env)) {
120
- const normalizedValue =
121
- typeof value === 'string'
122
- ? value
123
- : value == null
124
- ? undefined
125
- : String(value);
126
-
127
- if (normalizedValue === undefined) {
128
- delete runtimeEnv[key];
129
- } else {
130
- runtimeEnv[key] = normalizedValue;
131
- }
132
- }
133
- }
134
- };
135
-
136
- const getFileTaskId = (testPath: string): string => {
137
- return `file:${testPath}`;
138
- };
139
-
140
- /**
141
- * Intercept console methods and forward to host via send().
142
- * Returns a restore function to revert console to original.
143
- */
144
- const interceptConsole = (
145
- projectName: string,
146
- getCurrentTask: () => CurrentTaskInfo | undefined,
147
- printConsoleTrace: boolean,
148
- ): (() => void) => {
149
- const originalConsole = {
150
- log: console.log.bind(console),
151
- warn: console.warn.bind(console),
152
- error: console.error.bind(console),
153
- info: console.info.bind(console),
154
- debug: console.debug.bind(console),
155
- };
156
-
157
- const getConsoleTrace = (): string | undefined => {
158
- if (!printConsoleTrace) return undefined;
159
- const stack = new Error('STACK_TRACE').stack;
160
- // Skip: Error, getConsoleTrace, createConsoleInterceptor wrapper, console.log call
161
- return stack?.split('\n').slice(4).join('\n');
162
- };
163
-
164
- const createConsoleInterceptor = (
165
- level: 'log' | 'warn' | 'error' | 'info' | 'debug',
166
- ) => {
167
- return (...args: unknown[]) => {
168
- // Call original for browser DevTools
169
- originalConsole[level](...args);
170
-
171
- const content = formatConsoleArgs(args);
172
- const currentTask = getCurrentTask();
173
-
174
- // Send to host
175
- send({
176
- type: 'log',
177
- payload: {
178
- level,
179
- content,
180
- projectName,
181
- taskId: currentTask?.taskId,
182
- taskName: currentTask?.taskName,
183
- taskParentNames: currentTask?.taskParentNames,
184
- taskType: currentTask?.taskType,
185
- testPath: currentTask?.testPath ?? '',
186
- type: level === 'error' || level === 'warn' ? 'stderr' : 'stdout',
187
- trace: getConsoleTrace(),
188
- },
189
- });
190
- };
191
- };
192
-
193
- console.log = createConsoleInterceptor('log');
194
- console.warn = createConsoleInterceptor('warn');
195
- console.error = createConsoleInterceptor('error');
196
- console.info = createConsoleInterceptor('info');
197
- console.debug = createConsoleInterceptor('debug');
198
-
199
- return () => {
200
- console.log = originalConsole.log;
201
- console.warn = originalConsole.warn;
202
- console.error = originalConsole.error;
203
- console.info = originalConsole.info;
204
- console.debug = originalConsole.debug;
205
- };
206
- };
207
-
208
- const send = (message: BrowserClientMessage): void => {
209
- // If in iframe, send to parent window (container) which will forward to host via RPC
210
- if (window.parent !== window) {
211
- window.parent.postMessage(
212
- { type: DISPATCH_MESSAGE_TYPE, payload: message },
213
- '*',
214
- );
215
- return;
216
- }
217
- // Fallback: direct call if running outside iframe (not typical)
218
- // Note: This binding may not exist if not using Playwright
219
- window[DISPATCH_MESSAGE_TYPE]?.(message);
220
- };
221
-
222
- const dispatchRunnerLifecycle = (
223
- method: RunnerLifecycleMethod,
224
- payload: unknown,
225
- ): void => {
226
- sendRunnerLifecycle(
227
- createRunnerLifecycleRequest(method, payload),
228
- (error: unknown) => {
229
- debugLog('[Runner] Failed to dispatch lifecycle method:', method, error);
230
- },
231
- );
232
- };
233
-
234
- /**
235
- * Timeout for waiting for browser config from container (30 seconds).
236
- *
237
- * Coincidentally equal to the RPC default (client/dispatchTransport.ts) and the
238
- * host's RUNNER_FRAMES_READY_TIMEOUT_MS (hostController.ts), but semantically
239
- * distinct and in a different runtime, so deliberately not shared. Implicit
240
- * invariant: this must not exceed the host's frames-ready timeout, or the host
241
- * declares the runner un-ready before it can even receive its config.
242
- */
243
- const CONFIG_WAIT_TIMEOUT_MS = 30_000;
244
-
245
- /**
246
- * Wait for configuration from container if running in iframe.
247
- * This is a prerequisite for test execution - without config, tests cannot run.
248
- */
249
- const waitForConfig = (): Promise<void> => {
250
- // If not in iframe or already has config, resolve immediately
251
- if (window.parent === window || window.__RSTEST_BROWSER_OPTIONS__) {
252
- return Promise.resolve();
253
- }
254
-
255
- return new Promise((resolve, reject) => {
256
- const handleMessage = (event: MessageEvent) => {
257
- if (event.data?.type === RSTEST_CONFIG_MESSAGE_TYPE) {
258
- window.__RSTEST_BROWSER_OPTIONS__ = event.data.payload;
259
- debugLog(
260
- '[Runner] Received config from container:',
261
- event.data.payload,
262
- );
263
- window.removeEventListener('message', handleMessage);
264
- resolve();
265
- }
266
- };
267
-
268
- window.addEventListener('message', handleMessage);
269
-
270
- setTimeout(() => {
271
- window.removeEventListener('message', handleMessage);
272
- reject(
273
- new Error(
274
- `[Rstest] Failed to receive browser config within ${CONFIG_WAIT_TIMEOUT_MS / 1000}s. ` +
275
- 'This may indicate a connection issue between the runner iframe and container.',
276
- ),
277
- );
278
- }, CONFIG_WAIT_TIMEOUT_MS);
279
- });
280
- };
281
-
282
- /**
283
- * Convert absolute path to context key (relative path)
284
- * e.g., '/project/src/foo.test.ts' -> './src/foo.test.ts'
285
- * 'D:/project/src/foo.test.ts' -> './src/foo.test.ts'
286
- *
287
- * Uses pathe's normalize to handle cross-platform path separators.
288
- */
289
- const toContextKey = (absolutePath: string, projectRoot: string): string => {
290
- // Normalize both paths to use forward slashes for cross-platform compatibility
291
- const normalizedAbsolute = normalize(absolutePath);
292
- const normalizedRoot = normalize(projectRoot);
293
-
294
- // Only strip the root at a path boundary: a bare `startsWith` would mangle a
295
- // sibling like `/repo/pkg-extra/a.test.ts` under root `/repo/pkg`. Must stay
296
- // in sync with the host `toContextKey` (hostController.ts) so the non-watch
297
- // import-map keys resolve.
298
- const withinRoot =
299
- normalizedAbsolute === normalizedRoot ||
300
- normalizedAbsolute.startsWith(`${normalizedRoot}/`);
301
- if (!withinRoot) {
302
- // Test file outside the project root: keep the absolute path as the key so
303
- // `toAbsolutePath` round-trips it instead of re-rooting under projectRoot.
304
- return normalizedAbsolute;
305
- }
306
- const relative = normalizedAbsolute.slice(normalizedRoot.length);
307
- return relative.startsWith('/') ? `.${relative}` : `./${relative}`;
308
- };
309
-
310
- /**
311
- * Convert context key to absolute path
312
- * e.g., './src/foo.test.ts' -> '/project/src/foo.test.ts'
313
- */
314
- const toAbsolutePath = (key: string, projectRoot: string): string => {
315
- // An absolute key (test file outside the project root, see `toContextKey`)
316
- // round-trips as-is; only `./`-prefixed relative keys are re-rooted.
317
- if (!key.startsWith('.')) {
318
- return key;
319
- }
320
- // key format: ./src/foo.test.ts
321
- // Ensure no double slashes by removing trailing slash from projectRoot
322
- const normalizedRoot = normalize(projectRoot).replace(/\/$/, '');
323
- return normalizedRoot + key.slice(1);
324
- };
325
-
326
- /**
327
- * Find the project that contains the given test file.
328
- * Matches by checking if the testFile path starts with the project root.
329
- *
330
- * Uses pathe's normalize to handle cross-platform path separators.
331
- */
332
- const findProjectForTestFile = (
333
- testFile: string,
334
- allProjects: ManifestProjectConfig[],
335
- ): ManifestProjectConfig | undefined => {
336
- // Normalize the test file path for cross-platform compatibility
337
- const normalizedTestFile = normalize(testFile);
338
-
339
- // Sort projects by root path length (longest first) for most specific match
340
- const sorted = [...allProjects].sort(
341
- (a, b) => b.projectRoot.length - a.projectRoot.length,
342
- );
343
-
344
- for (const proj of sorted) {
345
- // projectRoot should already be normalized, but normalize again for safety
346
- const normalizedRoot = normalize(proj.projectRoot);
347
- if (normalizedTestFile.startsWith(normalizedRoot)) {
348
- return proj;
349
- }
350
- }
351
-
352
- // Fallback to first project
353
- return allProjects[0];
354
- };
355
-
356
- const run = async () => {
357
- // Wait for configuration if in iframe
358
- await waitForConfig();
359
- let options = window.__RSTEST_BROWSER_OPTIONS__;
360
-
361
- // Support reading testFile and testNamePattern from URL parameters
362
- const urlParams = new URLSearchParams(window.location.search);
363
- const urlTestFile = urlParams.get('testFile');
364
- const urlRunId = urlParams.get('runId');
365
- const urlTestNamePattern = urlParams.get('testNamePattern');
366
-
367
- if (urlTestFile && options) {
368
- // Override testFile from URL parameter
369
- options = {
370
- ...options,
371
- testFile: urlTestFile,
372
- };
373
- }
374
-
375
- if (urlRunId && options) {
376
- options = {
377
- ...options,
378
- runId: urlRunId,
379
- };
380
- }
381
-
382
- // Override testNamePattern from URL parameter if provided
383
- if (urlTestNamePattern && options) {
384
- options = {
385
- ...options,
386
- projects: options.projects.map((project) => ({
387
- ...project,
388
- runtimeConfig: {
389
- ...project.runtimeConfig,
390
- testNamePattern: urlTestNamePattern,
391
- },
392
- })),
393
- };
394
- }
395
-
396
- if (!options) {
397
- send({
398
- type: 'fatal',
399
- payload: {
400
- message: 'Browser test runtime is not configured.',
401
- },
402
- });
403
- window.__RSTEST_DONE__ = true;
404
- return;
405
- }
406
-
407
- send({ type: 'ready' });
408
-
409
- setRealTimers();
410
-
411
- // Preload runner.js sourcemap for inline snapshot support.
412
- // The snapshot code runs in runner.js, so we need its sourcemap
413
- // to map stack traces back to original source files.
414
- await preloadRunnerSourceMap();
415
-
416
- // Find the project for this test file
417
- const targetTestFile = options.testFile;
418
- const currentProject = targetTestFile
419
- ? findProjectForTestFile(
420
- targetTestFile,
421
- projects as ManifestProjectConfig[],
422
- )
423
- : (projects as ManifestProjectConfig[])[0];
424
-
425
- if (!currentProject) {
426
- send({
427
- type: 'fatal',
428
- payload: {
429
- message: 'No project found for test file',
430
- },
431
- });
432
- window.__RSTEST_DONE__ = true;
433
- return;
434
- }
435
-
436
- // Find the runtime config for this project
437
- const projectRuntime = options.projects.find(
438
- (p) => p.name === currentProject.name,
439
- );
440
- if (!projectRuntime) {
441
- send({
442
- type: 'fatal',
443
- payload: {
444
- message: `Project ${currentProject.name} not found in runtime options`,
445
- },
446
- });
447
- window.__RSTEST_DONE__ = true;
448
- return;
449
- }
450
-
451
- const runtimeConfig = restoreRuntimeConfig(projectRuntime.runtimeConfig);
452
- ensureRuntimeEnv(runtimeConfig.env);
453
-
454
- // Get this project's setup loaders and test context
455
- const currentSetupLoaders =
456
- (projectSetupLoaders as Record<string, Array<() => Promise<unknown>>>)[
457
- currentProject.name
458
- ] || [];
459
- const currentTestContext = (
460
- projectTestContexts as Record<string, ManifestTestContext>
461
- )[currentProject.name];
462
-
463
- if (!currentTestContext) {
464
- send({
465
- type: 'fatal',
466
- payload: {
467
- message: `Test context not found for project ${currentProject.name}`,
468
- },
469
- });
470
- window.__RSTEST_DONE__ = true;
471
- return;
472
- }
473
-
474
- const loadSetupFiles = async (): Promise<void> => {
475
- for (const loadSetup of currentSetupLoaders) {
476
- await loadSetup();
477
- }
478
- };
479
-
480
- // 1. Determine which test files to run
481
- let testKeysToRun: string[];
482
-
483
- if (targetTestFile) {
484
- // Single file mode: convert absolute path to context key
485
- const key = toContextKey(targetTestFile, currentProject.projectRoot);
486
- testKeysToRun = [key];
487
- } else {
488
- // Full run mode: get all test keys from context
489
- testKeysToRun = currentTestContext.getTestKeys();
490
- }
491
-
492
- // Check execution mode
493
- const executionMode = options.mode || 'run';
494
-
495
- // Collect mode: only gather test metadata without running
496
- if (executionMode === 'collect') {
497
- for (const key of testKeysToRun) {
498
- const testPath = toAbsolutePath(key, currentProject.projectRoot);
499
-
500
- const workerState: WorkerState = {
501
- project: projectRuntime.name,
502
- projectRoot: projectRuntime.projectRoot,
503
- rootPath: options.rootPath,
504
- runtimeConfig,
505
- taskId: 0,
506
- // The kept-module-cache flush keyed on `buildId` is node worker-pool
507
- // only (#1373); browser runners never reuse a node worker, so a constant
508
- // inert id is correct here.
509
- buildId: 0,
510
- outputModule: false,
511
- environment: 'browser',
512
- testPath,
513
- distPath: testPath,
514
- snapshotOptions: {
515
- updateSnapshot: options.snapshot.updateSnapshot,
516
- snapshotEnvironment: new BrowserSnapshotEnvironment(),
517
- snapshotFormat: runtimeConfig.snapshotFormat,
518
- },
519
- };
520
-
521
- const runtime = await createRstestRuntime(workerState, {
522
- taskContext: createBrowserTaskContext(),
523
- });
524
-
525
- installRuntimeGlobals(runtime, runtimeConfig);
526
-
527
- try {
528
- // Load setup files for this project after runtime is ready.
529
- await loadSetupFiles();
530
-
531
- // Load the test file dynamically (registers tests without running)
532
- await currentTestContext.loadTest(key);
533
-
534
- // Collect tests metadata
535
- const tests = await runtime.runner.collectTests();
536
-
537
- send({
538
- type: 'collect-result',
539
- payload: {
540
- testPath,
541
- project: projectRuntime.name,
542
- tests,
543
- },
544
- });
545
- } catch (_error) {
546
- const error =
547
- _error instanceof Error ? _error : new Error(String(_error));
548
- send({
549
- type: 'fatal',
550
- payload: {
551
- message: error.message,
552
- stack: error.stack,
553
- },
554
- });
555
- window.__RSTEST_DONE__ = true;
556
- return;
557
- }
558
- }
559
-
560
- send({ type: 'collect-complete' });
561
- window.__RSTEST_DONE__ = true;
562
- return;
563
- }
564
-
565
- // Capture unhandled errors/rejections that escape a test file's execution.
566
- // Parity with the node worker, which attaches process-level
567
- // uncaughtException/unhandledRejection to the running file's result and fails
568
- // the file. `activeUnhandledErrors` points at the currently running file's
569
- // collector (undefined between files, so stray late events are ignored).
570
- let activeUnhandledErrors: Error[] | undefined;
571
- const onWindowError = (event: ErrorEvent): void => {
572
- activeUnhandledErrors?.push(
573
- event.error instanceof Error
574
- ? event.error
575
- : new Error(event.message || String(event.error)),
576
- );
577
- };
578
- const onUnhandledRejection = (event: PromiseRejectionEvent): void => {
579
- const { reason } = event;
580
- activeUnhandledErrors?.push(
581
- reason instanceof Error ? reason : new Error(String(reason)),
582
- );
583
- };
584
- window.addEventListener('error', onWindowError);
585
- window.addEventListener('unhandledrejection', onUnhandledRejection);
586
-
587
- // 2. Run tests for each file
588
- for (const key of testKeysToRun) {
589
- const testPath = toAbsolutePath(key, currentProject.projectRoot);
590
- const taskStack: CurrentTaskInfo[] = [
591
- {
592
- taskId: getFileTaskId(testPath),
593
- taskType: 'file',
594
- testPath,
595
- },
596
- ];
597
-
598
- // Per-file TaskContext; taskStack supplies the concurrent attribution
599
- // that the single-slot fallback can't.
600
- const taskContext = createBrowserTaskContext();
601
-
602
- const shouldInterceptConsole =
603
- !runtimeConfig.disableConsoleIntercept ||
604
- runtimeConfig.silent === true ||
605
- runtimeConfig.silent === 'passed-only';
606
-
607
- // Intercept console methods to forward logs to host
608
- const restoreConsole = shouldInterceptConsole
609
- ? interceptConsole(
610
- projectRuntime.name,
611
- () => taskContext.getCurrent() ?? taskStack[taskStack.length - 1],
612
- runtimeConfig.disableConsoleIntercept
613
- ? false
614
- : (runtimeConfig.printConsoleTrace ?? false),
615
- )
616
- : () => {};
617
-
618
- const workerState: WorkerState = {
619
- project: projectRuntime.name,
620
- projectRoot: projectRuntime.projectRoot,
621
- rootPath: options.rootPath,
622
- runtimeConfig,
623
- taskId: 0,
624
- // See the `buildId` note above: inert in browser mode.
625
- buildId: 0,
626
- outputModule: false,
627
- environment: 'browser',
628
- currentTask: taskStack[0],
629
- testPath,
630
- distPath: testPath,
631
- snapshotOptions: {
632
- updateSnapshot: options.snapshot.updateSnapshot,
633
- snapshotEnvironment: new BrowserSnapshotEnvironment(),
634
- snapshotFormat: runtimeConfig.snapshotFormat,
635
- },
636
- };
637
-
638
- const syncCurrentTask = (): void => {
639
- workerState.currentTask = taskStack[taskStack.length - 1];
640
- };
641
-
642
- const removeTaskFromStack = (taskId: string): void => {
643
- const taskIndex = taskStack.findLastIndex(
644
- (task) => task.taskId === taskId,
645
- );
646
- if (taskIndex < 0) {
647
- return;
648
- }
649
- taskStack.splice(taskIndex, 1);
650
- syncCurrentTask();
651
- };
652
-
653
- const runtime = await createRstestRuntime(workerState, { taskContext });
654
-
655
- installRuntimeGlobals(runtime, runtimeConfig);
656
-
657
- let failedTestsCount = 0;
658
-
659
- const runnerHooks: RunnerHooks = {
660
- onTestFileReady: async (test) => {
661
- dispatchRunnerLifecycle('file-ready', test);
662
- },
663
- onTestSuiteStart: async (test) => {
664
- taskStack.push({
665
- taskId: test.testId,
666
- taskName: test.name,
667
- taskParentNames: test.parentNames,
668
- taskType: 'suite',
669
- testPath: test.testPath,
670
- });
671
- syncCurrentTask();
672
- dispatchRunnerLifecycle('suite-start', test);
673
- },
674
- onTestSuiteResult: async (result) => {
675
- removeTaskFromStack(result.testId);
676
- dispatchRunnerLifecycle('suite-result', result);
677
- },
678
- onTestCaseStart: async (test) => {
679
- taskStack.push({
680
- taskId: test.testId,
681
- taskName: test.name,
682
- taskParentNames: test.parentNames,
683
- taskType: 'case',
684
- testPath: test.testPath,
685
- });
686
- syncCurrentTask();
687
- dispatchRunnerLifecycle('case-start', test);
688
- },
689
- onTestCaseResult: async (result) => {
690
- removeTaskFromStack(result.testId);
691
- if (result.status === 'fail') {
692
- failedTestsCount++;
693
- }
694
- send({
695
- type: 'case-result',
696
- payload: result,
697
- });
698
- },
699
- getCountOfFailedTests: async () => failedTestsCount,
700
- };
701
-
702
- send({
703
- type: 'file-start',
704
- payload: {
705
- testPath,
706
- projectName: projectRuntime.name,
707
- },
708
- });
709
-
710
- const unhandledErrors: Error[] = [];
711
- activeUnhandledErrors = unhandledErrors;
712
-
713
- try {
714
- // Load setup files for this project after runtime is ready.
715
- await loadSetupFiles();
716
-
717
- // Record script URLs before loading the test file
718
- const beforeScripts = getScriptUrls();
719
-
720
- // Load the test file dynamically using this project's context
721
- await currentTestContext.loadTest(key);
722
-
723
- // Find the newly loaded chunk and preload its source map (for inline snapshots)
724
- const afterScripts = getScriptUrls();
725
- const chunkUrl = findNewScriptUrl(beforeScripts, afterScripts);
726
- if (chunkUrl) {
727
- await preloadTestFileSourceMap(chunkUrl);
728
- }
729
-
730
- const result = await runtime.runner.runTests(
731
- testPath,
732
- runnerHooks,
733
- runtime.api,
734
- );
735
-
736
- // The browser dispatches `unhandledrejection` in a task queued at the
737
- // current task's microtask checkpoint, so a rejection leaked by a
738
- // synchronous test is not observable yet when `runTests()` resolves.
739
- // Yield two macrotasks: the first reaches the checkpoint that queues
740
- // the event task, the second runs after that task regardless of how
741
- // the browser orders the timer and event task sources.
742
- for (let i = 0; i < 2; i++) {
743
- await new Promise<void>((resolve) => {
744
- setTimeout(resolve, 0);
745
- });
746
- }
747
-
748
- // An unhandled error/rejection that escaped the run fails the file even
749
- // when every test passed.
750
- if (unhandledErrors.length > 0) {
751
- result.status = 'fail';
752
- result.errors = [
753
- ...(result.errors ?? []),
754
- ...unhandledErrors.map((error) => ({
755
- name: error.name,
756
- message: error.message,
757
- stack: error.stack,
758
- })),
759
- ];
760
- }
761
-
762
- // Collect coverage data from global __coverage__ object
763
- if (globalThis.__coverage__) {
764
- result.coverage = globalThis.__coverage__ as CoverageMapData;
765
- }
766
-
767
- send({
768
- type: 'file-complete',
769
- payload: result,
770
- });
771
- } catch (_error) {
772
- const error =
773
- _error instanceof Error ? _error : new Error(String(_error));
774
- send({
775
- type: 'fatal',
776
- payload: {
777
- message: error.message,
778
- stack: error.stack,
779
- },
780
- });
781
- window.__RSTEST_DONE__ = true;
782
- return;
783
- } finally {
784
- // Restore original console methods
785
- restoreConsole();
786
- activeUnhandledErrors = undefined;
787
- }
788
- }
789
-
790
- window.removeEventListener('error', onWindowError);
791
- window.removeEventListener('unhandledrejection', onUnhandledRejection);
792
-
793
- send({ type: 'complete' });
794
- window.__RSTEST_DONE__ = true;
795
- };
796
-
797
- void run().catch((error) => {
798
- const err = error instanceof Error ? error : new Error(String(error));
799
- send({
800
- type: 'fatal',
801
- payload: {
802
- message: err.message,
803
- stack: err.stack,
804
- },
805
- });
806
- window.__RSTEST_DONE__ = true;
807
- });