@rstest/browser 0.11.6 → 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 (88) 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/browserRsbuild.d.ts +24 -4
  6. package/dist/browserV8Coverage.d.ts +15 -0
  7. package/dist/client/240~1.js +150 -0
  8. package/dist/{augmentExpect.d.ts → client/client/augmentExpect.d.ts} +1 -1
  9. package/dist/client/{browserRpc.d.ts → client/browserRpc.d.ts} +1 -1
  10. package/dist/client/{dispatchTransport.d.ts → client/dispatchTransport.d.ts} +8 -4
  11. package/dist/client/client/index.d.ts +2 -0
  12. package/dist/client/client/runIdentity.d.ts +12 -0
  13. package/dist/client/client/runner.d.ts +4 -0
  14. package/dist/client/client/snapshot.d.ts +27 -0
  15. package/dist/client/client/sourceMapSupport.d.ts +36 -0
  16. package/dist/{browser.js → client/index.js} +3 -102
  17. package/dist/client/protocol.d.ts +225 -0
  18. package/dist/client/rpcProtocol.d.ts +138 -0
  19. package/dist/client/runner.js +675 -0
  20. package/dist/client/sourceMap/sourceMapLoader.d.ts +25 -0
  21. package/dist/containerRpc.d.ts +27 -19
  22. package/dist/headedRunRegistry.d.ts +51 -0
  23. package/dist/headedScheduler.d.ts +10 -10
  24. package/dist/headlessScheduler.d.ts +5 -4
  25. package/dist/headlessTransport.d.ts +4 -2
  26. package/dist/hostPayloads.d.ts +1 -7
  27. package/dist/index.js +840 -330
  28. package/dist/protocol.d.ts +48 -8
  29. package/dist/providers/index.d.ts +21 -0
  30. package/dist/providers/playwright/v8Coverage.d.ts +2 -0
  31. package/dist/rpcProtocol.d.ts +1 -2
  32. package/dist/schedulerSeam.d.ts +5 -0
  33. package/dist/sourceMap/sourceMapLoader.d.ts +12 -1
  34. package/dist/watchRerunPlanner.d.ts +20 -9
  35. package/dist/watchSignals.d.ts +8 -1
  36. package/package.json +10 -10
  37. package/dist/33.js +0 -7
  38. package/dist/browser-container/container-static/css/index.0521ff95b1.css +0 -1
  39. package/dist/browser-container/container-static/js/683.4f821ce8f1.js +0 -194
  40. package/dist/browser-container/container-static/js/index.568aa7cb35.js +0 -1
  41. package/dist/browser.d.ts +0 -2
  42. package/src/augmentExpect.ts +0 -62
  43. package/src/browser.ts +0 -3
  44. package/src/browserExecutor.ts +0 -222
  45. package/src/browserRpcRegistry.ts +0 -57
  46. package/src/browserRsbuild.ts +0 -1927
  47. package/src/client/api.ts +0 -213
  48. package/src/client/browserRpc.ts +0 -86
  49. package/src/client/dispatchTransport.ts +0 -240
  50. package/src/client/entry.ts +0 -807
  51. package/src/client/locator.ts +0 -448
  52. package/src/client/snapshot.ts +0 -113
  53. package/src/client/sourceMapSupport.ts +0 -159
  54. package/src/concurrency.ts +0 -65
  55. package/src/configValidation.ts +0 -263
  56. package/src/containerRpc.ts +0 -206
  57. package/src/dispatchCapabilities.ts +0 -177
  58. package/src/dispatchRouter.ts +0 -82
  59. package/src/env.d.ts +0 -54
  60. package/src/headedScheduler.ts +0 -664
  61. package/src/headedSerialTaskQueue.ts +0 -23
  62. package/src/headlessScheduler.ts +0 -566
  63. package/src/headlessTransport.ts +0 -28
  64. package/src/hostController.ts +0 -1365
  65. package/src/hostPayloads.ts +0 -62
  66. package/src/index.ts +0 -51
  67. package/src/manifest.d.ts +0 -41
  68. package/src/protocol.ts +0 -225
  69. package/src/providers/index.ts +0 -126
  70. package/src/providers/playwright/compileLocator.ts +0 -130
  71. package/src/providers/playwright/dispatchBrowserRpc.ts +0 -372
  72. package/src/providers/playwright/expectUtils.ts +0 -57
  73. package/src/providers/playwright/implementation.ts +0 -35
  74. package/src/providers/playwright/index.ts +0 -1
  75. package/src/providers/playwright/runtime.ts +0 -159
  76. package/src/providers/playwright/textMatcher.ts +0 -10
  77. package/src/rpcProtocol.ts +0 -218
  78. package/src/runSession.ts +0 -110
  79. package/src/schedulerSeam.ts +0 -49
  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 -88
  84. package/src/watchRuntime.ts +0 -83
  85. package/src/watchSignals.ts +0 -93
  86. /package/dist/{rslib-runtime.js → 612~0.js} +0 -0
  87. /package/dist/client/{api.d.ts → client/api.d.ts} +0 -0
  88. /package/dist/client/{locator.d.ts → client/locator.d.ts} +0 -0
package/dist/index.js CHANGED
@@ -1,15 +1,16 @@
1
- import { DEFAULT_TEST_TIMEOUT, FATAL_SIGNALS, PhaseTracker, RSTEST_ENV_SYMBOL_KEY, applyWatchInvalidation, applyWebMockRspackConfig, browserIgnoredRuntimeConfigKeys, buildBrowserCoverageMap, color, createCoverageProvider, createRunnerEventSink, createSilentConsoleController, getNumCpus as browser_getNumCpus, getPrettyConsoleName, getSetupFiles, getTestEntries, hasUserRstestConfigPlugins, importMetaRstestDefine, initModifyRstestConfigHooks, isDebug, isTTY, loadCoverageProvider, logWatchReadyMessage, logger, pluginMockRuntime, projectRuntimeConfig, resolveProjectBuildCache, resolveShardedEntries, resolveSnapshotPathDefault, resolveWorkerCount, rsbuild, serializableConfig } from "@rstest/core/internal/browser";
1
+ import { DEFAULT_TEST_TIMEOUT, FATAL_SIGNALS, FIXTURE_CLEANUP_TIMEOUT_MS, PhaseTracker, RSTEST_ENV_SYMBOL_KEY, applyWatchInvalidation, applyWebMockRspackConfig, browserIgnoredRuntimeConfigKeys, buildBrowserCoverageMap, color, createCoverageProvider, createFileCleanupTimeoutResult, createRunnerEventSink, createSilentConsoleController, getNumCpus as browser_getNumCpus, getPrettyConsoleName, getSetupFiles, getTestEntries, hasUserRstestConfigPlugins, initModifyRstestConfigHooks, isDebug, isTTY, loadCoverageProvider, logWatchReadyMessage, logger, pluginMockRuntime, projectRuntimeConfig, resolveProjectBuildCache, resolveShardedEntries, resolveSnapshotPathDefault, resolveWorkerCount, rsbuild, serializableConfig } from "@rstest/core/internal/browser";
2
2
  import promises from "node:fs/promises";
3
- import { dirname, join, normalize, relative, resolve as external_pathe_resolve } from "pathe";
3
+ import { dirname, isAbsolute as external_pathe_isAbsolute, join, normalize, relative, resolve as external_pathe_resolve } from "pathe";
4
4
  import { existsSync } from "node:fs";
5
5
  import { fileURLToPath } from "node:url";
6
6
  import { isDeepStrictEqual } from "node:util";
7
7
  import open_editor from "open-editor";
8
8
  import sirv from "sirv";
9
9
  import { WebSocketServer } from "ws";
10
+ import { AnyMap, TraceMap, encodedMap } from "@jridgewell/trace-mapping";
10
11
  import convert_source_map from "convert-source-map";
11
- import { __webpack_require__ } from "./rslib-runtime.js";
12
- import { DISPATCH_RPC_BRIDGE_NAME, DISPATCH_NAMESPACE_SNAPSHOT, DISPATCH_MESSAGE_TYPE, DISPATCH_NAMESPACE_BROWSER } from "./33.js";
12
+ import { randomUUID } from "node:crypto";
13
+ import { __webpack_require__ } from "./612~0.js";
13
14
  __webpack_require__.add({
14
15
  "../../node_modules/.pnpm/picomatch@4.0.5/node_modules/picomatch/index.js" (module, __unused_rspack_exports, __webpack_require__) {
15
16
  const pico = __webpack_require__("../../node_modules/.pnpm/picomatch@4.0.5/node_modules/picomatch/lib/picomatch.js");
@@ -1702,6 +1703,12 @@ class HostDispatchRouter {
1702
1703
  }
1703
1704
  }
1704
1705
  }
1706
+ const DISPATCH_MESSAGE_TYPE = '__rstest_dispatch__';
1707
+ const DISPATCH_RPC_BRIDGE_NAME = '__rstest_dispatch_rpc__';
1708
+ const DISPATCH_NAMESPACE_BROWSER = 'browser';
1709
+ const DISPATCH_NAMESPACE_FILE_CLEANUP = 'file-cleanup';
1710
+ const DISPATCH_NAMESPACE_SNAPSHOT = 'snapshot';
1711
+ const RSTEST_BROWSER_CACHE_CLEANERS_KEY = '@rstest/browser/cache-cleaners';
1705
1712
  const snapshotRequestBuilders = {
1706
1713
  resolveSnapshotPath: (id, args)=>({
1707
1714
  id,
@@ -1842,11 +1849,6 @@ const ignoredKeyWarnings = {
1842
1849
  message: ()=>"Ignoring isolate: false in browser mode: each test file still runs in a fresh context.",
1843
1850
  browserOnly: true
1844
1851
  },
1845
- federation: {
1846
- isNonDefault: (config)=>true === config.federation,
1847
- message: ()=>'Ignoring federation in browser mode: it only applies to the Node runner.',
1848
- browserOnly: true
1849
- },
1850
1852
  detectAsyncLeaks: {
1851
1853
  isNonDefault: (config)=>true === config.detectAsyncLeaks,
1852
1854
  message: ()=>"Ignoring detectAsyncLeaks in browser mode: it relies on node async_hooks."
@@ -1897,8 +1899,9 @@ const reportUnsupportedBrowserOptions = (context)=>{
1897
1899
  const globalConfig = context.normalizedConfig;
1898
1900
  const { coverage } = globalConfig;
1899
1901
  if ('list' !== context.command && coverage.enabled && 'v8' === coverage.provider) {
1900
- if (isBrowserOnlyRun) throw new Error("Coverage provider 'v8' is not supported in browser mode: browser projects produce no v8 coverage. Use the default istanbul provider (coverage.provider: 'istanbul') for browser coverage.");
1901
- logger.warn(color.yellow("Coverage provider 'v8' produces no coverage for browser project files; use the 'istanbul' provider to collect browser coverage. Node projects will still use v8."));
1902
+ const unsupportedProject = browserProjects.find((project)=>'chromium' !== project.normalizedConfig.browser.browser);
1903
+ if (unsupportedProject) if (isBrowserOnlyRun) throw new Error(`Coverage provider 'v8' in browser mode requires Chromium. Project ${JSON.stringify(unsupportedProject.name)} uses ${JSON.stringify(unsupportedProject.normalizedConfig.browser.browser)}. Use Chromium or coverage.provider: 'istanbul'.`);
1904
+ else logger.warn(color.yellow(`Coverage provider 'v8' requires Chromium for browser project ${JSON.stringify(unsupportedProject.name)}. Its files will not produce coverage; node projects will still use v8.`));
1902
1905
  }
1903
1906
  const warnings = new Set();
1904
1907
  if ('forks' !== globalConfig.pool.type) warnings.add(`Ignoring pool.type '${globalConfig.pool.type}' in browser mode.`);
@@ -2350,6 +2353,17 @@ async function launchPlaywrightBrowser({ browserName, headless, providerOptions
2350
2353
  browser: wrappedBrowser
2351
2354
  };
2352
2355
  }
2356
+ const asCoveragePage = (page)=>page;
2357
+ const createPlaywrightV8CoverageCollector = ()=>({
2358
+ async start (page) {
2359
+ await asCoveragePage(page).coverage.startJSCoverage({
2360
+ resetOnNavigation: false
2361
+ });
2362
+ },
2363
+ async take (page) {
2364
+ return asCoveragePage(page).coverage.stopJSCoverage();
2365
+ }
2366
+ });
2353
2367
  const playwrightProviderImplementation = {
2354
2368
  name: 'playwright',
2355
2369
  async launchRuntime ({ browserName, headless, providerOptions }) {
@@ -2359,6 +2373,9 @@ const playwrightProviderImplementation = {
2359
2373
  providerOptions
2360
2374
  });
2361
2375
  },
2376
+ createV8CoverageCollector ({ browserName }) {
2377
+ return 'chromium' === browserName ? createPlaywrightV8CoverageCollector() : null;
2378
+ },
2362
2379
  async dispatchRpc ({ containerPage, runnerPage, request, timeoutFallbackMs }) {
2363
2380
  return dispatchPlaywrightBrowserRpc({
2364
2381
  containerPage: containerPage,
@@ -2376,15 +2393,18 @@ function getBrowserProviderImplementation(provider) {
2376
2393
  if (!implementation) throw new Error(`Unsupported browser provider: ${String(provider)}`);
2377
2394
  return implementation;
2378
2395
  }
2379
- const collectDeletedTestPaths = (previous, current)=>{
2380
- const currentPathSet = new Set(current.map((file)=>file.testPath));
2381
- return previous.map((file)=>file.testPath).filter((testPath)=>!currentPathSet.has(testPath));
2382
- };
2383
2396
  const serializeTestFiles = (files)=>JSON.stringify(files.map((f)=>`${f.projectName}:${f.testPath}`).sort());
2384
2397
  const normalizeTestFiles = (files)=>files.map((file)=>({
2385
2398
  ...file,
2386
2399
  testPath: normalize(file.testPath)
2387
2400
  }));
2401
+ const collectTestPaths = (files)=>[
2402
+ ...new Set(files.map((file)=>file.testPath))
2403
+ ];
2404
+ const collectDeletedTestPaths = (previous, current)=>{
2405
+ const currentPathSet = new Set(current.map((file)=>file.testPath));
2406
+ return previous.map((file)=>file.testPath).filter((testPath)=>!currentPathSet.has(testPath));
2407
+ };
2388
2408
  const collectWatchTestFiles = (projectEntries)=>projectEntries.flatMap((entry)=>entry.testFiles.map((testPath)=>({
2389
2409
  testPath: normalize(testPath),
2390
2410
  projectName: entry.project.name
@@ -2392,23 +2412,88 @@ const collectWatchTestFiles = (projectEntries)=>projectEntries.flatMap((entry)=>
2392
2412
  const planWatchRerun = ({ projectEntries, previousTestFiles, affectedTestFiles })=>{
2393
2413
  const currentTestFiles = collectWatchTestFiles(projectEntries);
2394
2414
  const normalizedPrevious = normalizeTestFiles(previousTestFiles);
2395
- const filesChanged = serializeTestFiles(currentTestFiles) !== serializeTestFiles(normalizedPrevious);
2396
- const normalizedAffectedTestFiles = affectedTestFiles.map((testFile)=>normalize(testFile));
2397
- const affectedPathSet = new Set(normalizedAffectedTestFiles);
2398
- const matchedAffectedFiles = currentTestFiles.filter((file)=>affectedPathSet.has(file.testPath));
2415
+ const fileSetChanged = serializeTestFiles(currentTestFiles) !== serializeTestFiles(normalizedPrevious);
2416
+ if (fileSetChanged) {
2417
+ const fileSetUpdate = {
2418
+ currentTestFiles,
2419
+ deletedTestPaths: collectDeletedTestPaths(normalizedPrevious, currentTestFiles)
2420
+ };
2421
+ if (0 === currentTestFiles.length) return {
2422
+ fileSetUpdate,
2423
+ decision: {
2424
+ kind: 'empty',
2425
+ message: 'No browser test files remain after update.\n'
2426
+ }
2427
+ };
2428
+ return {
2429
+ fileSetUpdate,
2430
+ decision: {
2431
+ kind: 'rerun',
2432
+ testPaths: collectTestPaths(currentTestFiles),
2433
+ message: `Test file set changed, re-running ${currentTestFiles.length} file(s)...\n`
2434
+ }
2435
+ };
2436
+ }
2437
+ const affectedPathSet = new Set(affectedTestFiles.map((testFile)=>normalize(testFile)));
2438
+ const matchedAffectedFiles = currentTestFiles.filter(({ testPath })=>affectedPathSet.has(testPath));
2439
+ if (0 === matchedAffectedFiles.length) return {
2440
+ decision: {
2441
+ kind: 'idle',
2442
+ message: 'No affected browser test files detected, skipping re-run.\n'
2443
+ }
2444
+ };
2399
2445
  return {
2400
- currentTestFiles,
2401
- filesChanged,
2402
- normalizedAffectedTestFiles,
2403
- affectedTestFiles: matchedAffectedFiles
2446
+ decision: {
2447
+ kind: 'rerun',
2448
+ testPaths: collectTestPaths(matchedAffectedFiles),
2449
+ message: `Re-running ${matchedAffectedFiles.length} affected test file(s)...\n`
2450
+ }
2404
2451
  };
2405
2452
  };
2453
+ const commitWatchFileSetUpdate = (update, watchState, pruneDeletedTestPaths)=>{
2454
+ if (!update) return;
2455
+ if (update.deletedTestPaths.length > 0) pruneDeletedTestPaths(update.deletedTestPaths);
2456
+ watchState.lastTestFiles = update.currentTestFiles;
2457
+ };
2406
2458
  const picomatch = __webpack_require__("../../node_modules/.pnpm/picomatch@4.0.5/node_modules/picomatch/index.js");
2407
2459
  var picomatch_default = /*#__PURE__*/ __webpack_require__.n(picomatch);
2408
2460
  const { createRsbuild: createRsbuild, rspack: rspack } = rsbuild;
2409
2461
  const browserRsbuild_dirname = dirname(fileURLToPath(import.meta.url));
2410
2462
  const OPTIONS_PLACEHOLDER = '__RSTEST_OPTIONS_PLACEHOLDER__';
2411
2463
  const serializeForInlineScript = (value)=>JSON.stringify(value).replace(/</g, '\\u003c').replace(/\u2028/g, '\\u2028').replace(/\u2029/g, '\\u2029');
2464
+ class RstestBrowserRuntimePlugin {
2465
+ apply(compiler) {
2466
+ const { RuntimeModule } = compiler.webpack;
2467
+ class BrowserRuntimeModule extends RuntimeModule {
2468
+ constructor(){
2469
+ super('rstest_browser_runtime');
2470
+ }
2471
+ generate() {
2472
+ return `
2473
+ var __rstest_browser_test_entry_ids__ = Object.create(null);
2474
+
2475
+ __webpack_require__.rstest_register_browser_test_entry = (path, id) => {
2476
+ __rstest_browser_test_entry_ids__[path] = id;
2477
+ };
2478
+
2479
+ function __rstest_clean_browser_test_entry__(testEntryPath) {
2480
+ var testEntryId = __rstest_browser_test_entry_ids__[testEntryPath];
2481
+ if (testEntryId !== undefined) {
2482
+ delete __webpack_module_cache__[testEntryId];
2483
+ }
2484
+ }
2485
+
2486
+ (globalThis[${JSON.stringify(RSTEST_BROWSER_CACHE_CLEANERS_KEY)}] ??= new Set()).add(__rstest_clean_browser_test_entry__);
2487
+ `;
2488
+ }
2489
+ }
2490
+ compiler.hooks.thisCompilation.tap('RstestBrowserRuntimePlugin', (compilation)=>{
2491
+ compilation.hooks.additionalTreeRuntimeRequirements.tap('RstestAddBrowserRuntimePlugin', (chunk)=>{
2492
+ compilation.addRuntimeModule(chunk, new BrowserRuntimeModule());
2493
+ });
2494
+ });
2495
+ }
2496
+ }
2412
2497
  const getBrowserProviderOptions = (project)=>{
2413
2498
  const browserConfig = project.normalizedConfig.browser;
2414
2499
  return browserConfig.providerOptions ?? {};
@@ -2416,6 +2501,7 @@ const getBrowserProviderOptions = (project)=>{
2416
2501
  const createBrowserWatchState = ()=>({
2417
2502
  lastTestFiles: [],
2418
2503
  hooksEnabled: false,
2504
+ headedFileSetVersion: 1,
2419
2505
  invalidation: new Map(),
2420
2506
  pendingAffectedTestFiles: new Map(),
2421
2507
  compileStartTimes: new Map(),
@@ -2688,13 +2774,13 @@ const browserRsbuild_collectProjectEntries = async (context, browserProjects = g
2688
2774
  testFiles: Object.values(tests)
2689
2775
  };
2690
2776
  }));
2691
- const resolveBrowserFile = (relativePath)=>{
2777
+ const resolveBrowserDistFile = (relativePath)=>{
2692
2778
  const candidates = [
2693
- external_pathe_resolve(browserRsbuild_dirname, '../src', relativePath),
2694
- external_pathe_resolve(browserRsbuild_dirname, relativePath)
2779
+ external_pathe_resolve(browserRsbuild_dirname, relativePath),
2780
+ external_pathe_resolve(browserRsbuild_dirname, '../dist', relativePath)
2695
2781
  ];
2696
2782
  for (const candidate of candidates)if (existsSync(candidate)) return candidate;
2697
- throw new Error(`Unable to resolve browser client file: ${relativePath}`);
2783
+ throw new Error(`Unable to resolve browser dist file: ${relativePath}`);
2698
2784
  };
2699
2785
  const resolveContainerDist = ()=>{
2700
2786
  const distPath = external_pathe_resolve(browserRsbuild_dirname, 'browser-container');
@@ -2839,7 +2925,7 @@ const destroyBrowserRuntime = async (runtime)=>{
2839
2925
  force: true
2840
2926
  }).catch(()=>{});
2841
2927
  };
2842
- const createBrowserRuntime = async ({ context, projectEntries: initialProjectEntries, browserProjects, shardedEntries, freezeShardedEntries, tempDir, isWatchMode, onTriggerRerun, containerDistPath, containerDevServer, forceHeadless, skipProviderLaunch, appliedModifyRstestConfigEnvironments })=>{
2928
+ const createBrowserRuntime = async ({ context, projectEntries: initialProjectEntries, browserProjects, shardedEntries, freezeShardedEntries, tempDir, isWatchMode, containerDistPath, containerDevServer, forceHeadless, skipProviderLaunch, appliedModifyRstestConfigEnvironments })=>{
2843
2929
  const containerHtmlTemplate = containerDistPath ? await promises.readFile(join(containerDistPath, 'index.html'), 'utf-8') : null;
2844
2930
  let injectedContainerHtml = null;
2845
2931
  let serializedOptions = 'null';
@@ -2903,7 +2989,7 @@ const createBrowserRuntime = async ({ context, projectEntries: initialProjectEnt
2903
2989
  };
2904
2990
  const browserRuntimePath = fileURLToPath(import.meta.resolve('@rstest/core/internal/browser-runtime'));
2905
2991
  const staticRstestAliases = {
2906
- '@rstest/browser': resolveBrowserFile('browser.ts'),
2992
+ '@rstest/browser': resolveBrowserDistFile('client/index.js'),
2907
2993
  '@rstest/core/internal/browser-runtime': browserRuntimePath
2908
2994
  };
2909
2995
  const rstestEnvDefine = `globalThis[Symbol.for(${JSON.stringify(RSTEST_ENV_SYMBOL_KEY)})]`;
@@ -2991,7 +3077,7 @@ const createBrowserRuntime = async ({ context, projectEntries: initialProjectEnt
2991
3077
  modules: virtualManifestModules
2992
3078
  });
2993
3079
  const rstestInternalAliases = {
2994
- '@rstest/browser-manifest': manifestPath,
3080
+ __rstest_virtual_browser_manifest__: manifestPath,
2995
3081
  ...staticRstestAliases
2996
3082
  };
2997
3083
  const isHeadless = forceHeadless || project.normalizedConfig.browser.headless;
@@ -3029,6 +3115,17 @@ const createBrowserRuntime = async ({ context, projectEntries: initialProjectEnt
3029
3115
  {
3030
3116
  name: 'rstest:browser-user-config',
3031
3117
  setup (api) {
3118
+ if ('list' === context.command) {
3119
+ let testEntryPaths;
3120
+ const getTestEntryPaths = ()=>testEntryPaths ??= new Set(getProjectEntry(project)?.testFiles.map(normalize) ?? []);
3121
+ api.transform({
3122
+ test: (resourcePath)=>getTestEntryPaths().has(normalize(resourcePath))
3123
+ }, ({ code, resourcePath })=>({
3124
+ code: `${code}\nif (__webpack_require__.rstest_register_browser_test_entry && __webpack_module__.id != null) {
3125
+ __webpack_require__.rstest_register_browser_test_entry(${JSON.stringify(normalize(resourcePath))}, __webpack_module__.id);
3126
+ }`
3127
+ }));
3128
+ }
3032
3129
  api.expose?.('rstest:browser', {
3033
3130
  registerDispatchHandler: (namespace, handler)=>{
3034
3131
  dispatchHandlers.set(namespace, handler);
@@ -3056,8 +3153,7 @@ const createBrowserRuntime = async ({ context, projectEntries: initialProjectEnt
3056
3153
  source: {
3057
3154
  define: {
3058
3155
  'process.env': rstestEnvDefine,
3059
- 'import.meta.env': rstestEnvDefine,
3060
- 'import.meta.rstest': importMetaRstestDefine('web')
3156
+ 'import.meta.env': rstestEnvDefine
3061
3157
  }
3062
3158
  },
3063
3159
  output: {
@@ -3085,6 +3181,7 @@ const createBrowserRuntime = async ({ context, projectEntries: initialProjectEnt
3085
3181
  });
3086
3182
  rspackConfig.lazyCompilation = enableHmr ? createBrowserLazyCompilationConfig(setupFiles) : false;
3087
3183
  rspackConfig.plugins = rspackConfig.plugins || [];
3184
+ rspackConfig.plugins.push(new RstestBrowserRuntimePlugin());
3088
3185
  rspackConfig.plugins.push(virtualManifestPlugin);
3089
3186
  applyDefaultWatchOptions(rspackConfig, isWatchMode);
3090
3187
  const browserRuntimeDir = dirname(browserRuntimePath);
@@ -3101,7 +3198,7 @@ const createBrowserRuntime = async ({ context, projectEntries: initialProjectEnt
3101
3198
  });
3102
3199
  merged.source = merged.source || {};
3103
3200
  merged.source.entry = {
3104
- runner: resolveBrowserFile('client/entry.ts')
3201
+ runner: resolveBrowserDistFile('client/runner.js')
3105
3202
  };
3106
3203
  return merged;
3107
3204
  },
@@ -3110,7 +3207,7 @@ const createBrowserRuntime = async ({ context, projectEntries: initialProjectEnt
3110
3207
  }
3111
3208
  }
3112
3209
  ]);
3113
- if (isWatchMode && onTriggerRerun) rsbuildInstance.addPlugins([
3210
+ if (isWatchMode) rsbuildInstance.addPlugins([
3114
3211
  {
3115
3212
  name: 'rstest:browser-watch',
3116
3213
  setup (api) {
@@ -3155,7 +3252,7 @@ const createBrowserRuntime = async ({ context, projectEntries: initialProjectEnt
3155
3252
  }
3156
3253
  }
3157
3254
  if (!watchState.hooksEnabled) return;
3158
- await onTriggerRerun();
3255
+ await watchState.triggerRerun?.();
3159
3256
  });
3160
3257
  }
3161
3258
  }
@@ -3306,8 +3403,182 @@ async function resolveProjectEntries(context, shardedEntries, browserProjects) {
3306
3403
  }
3307
3404
  return browserRsbuild_collectProjectEntries(context, browserProjects);
3308
3405
  }
3309
- const TYPE_REQUEST = "q";
3310
- const TYPE_RESPONSE = "s";
3406
+ const flattenSourceMap = (sourceMap, sourceMapUrl)=>encodedMap(new AnyMap(sourceMap, sourceMapUrl));
3407
+ const normalizeJavaScriptUrl = (value, options)=>{
3408
+ try {
3409
+ const url = options?.origin ? new URL(value, options.origin) : new URL(value);
3410
+ if ('http:' !== url.protocol && 'https:' !== url.protocol) return null;
3411
+ url.hash = '';
3412
+ return url.toString();
3413
+ } catch {
3414
+ return null;
3415
+ }
3416
+ };
3417
+ const resolveInlineSourceMap = (code, sourceMapUrl)=>{
3418
+ try {
3419
+ const converter = convert_source_map.fromSource(code);
3420
+ return converter ? flattenSourceMap(converter.toObject(), sourceMapUrl) : null;
3421
+ } catch {
3422
+ return null;
3423
+ }
3424
+ };
3425
+ const loadSourceMapForSource = async ({ jsUrl, signal, source, fetcher = fetch })=>{
3426
+ const normalizedUrl = normalizeJavaScriptUrl(jsUrl);
3427
+ if (!normalizedUrl) return null;
3428
+ try {
3429
+ const matches = [
3430
+ ...source.matchAll(convert_source_map.mapFileCommentRegex)
3431
+ ];
3432
+ const finalMatch = matches.at(-1);
3433
+ const sourceMapUrl = finalMatch?.[1] ?? finalMatch?.[2]?.trim();
3434
+ if (!sourceMapUrl || sourceMapUrl.startsWith('data:')) return null;
3435
+ const resolvedSourceMapUrl = new URL(sourceMapUrl, normalizedUrl);
3436
+ if ('http:' !== resolvedSourceMapUrl.protocol && 'https:' !== resolvedSourceMapUrl.protocol) return null;
3437
+ const mapResponse = await fetcher(resolvedSourceMapUrl.href, signal ? {
3438
+ signal
3439
+ } : void 0);
3440
+ return mapResponse.ok ? {
3441
+ sourceMap: flattenSourceMap(await mapResponse.json(), resolvedSourceMapUrl.href),
3442
+ sourceMapUrl: resolvedSourceMapUrl.href
3443
+ } : null;
3444
+ } catch {
3445
+ return null;
3446
+ }
3447
+ };
3448
+ const fetchSourceMap = async (jsUrl, fetcher)=>{
3449
+ const jsResponse = await fetcher(jsUrl);
3450
+ if (!jsResponse.ok) return null;
3451
+ const code = await jsResponse.text();
3452
+ const inlineMap = resolveInlineSourceMap(code, jsUrl);
3453
+ if (inlineMap) return inlineMap;
3454
+ const mapUrl = new URL(jsUrl);
3455
+ mapUrl.pathname += '.map';
3456
+ const mapResponse = await fetcher(mapUrl.href);
3457
+ if (!mapResponse.ok) return null;
3458
+ return flattenSourceMap(await mapResponse.json(), mapUrl.href);
3459
+ };
3460
+ const loadSourceMapWithCache = async ({ jsUrl, cache, force = false, origin, fetcher = fetch })=>{
3461
+ const normalizedUrl = normalizeJavaScriptUrl(jsUrl, {
3462
+ origin
3463
+ });
3464
+ if (!normalizedUrl) return null;
3465
+ if (!force && cache.has(normalizedUrl)) return cache.get(normalizedUrl) ?? null;
3466
+ try {
3467
+ const sourceMap = await fetchSourceMap(normalizedUrl, fetcher);
3468
+ cache.set(normalizedUrl, sourceMap);
3469
+ return sourceMap;
3470
+ } catch {
3471
+ cache.set(normalizedUrl, null);
3472
+ return null;
3473
+ }
3474
+ };
3475
+ const resolveCoverageResourceUrl = (value)=>{
3476
+ try {
3477
+ const url = new URL(value);
3478
+ return 'http:' === url.protocol || 'https:' === url.protocol ? url.href : null;
3479
+ } catch {
3480
+ return null;
3481
+ }
3482
+ };
3483
+ const resolveProjectHttpSource = (source, rootPath, projectOrigin)=>{
3484
+ let sourceUrl;
3485
+ try {
3486
+ sourceUrl = new URL(source);
3487
+ } catch {
3488
+ return source;
3489
+ }
3490
+ if ('http:' !== sourceUrl.protocol && 'https:' !== sourceUrl.protocol || sourceUrl.origin !== projectOrigin) return source;
3491
+ let sourcePath = sourceUrl.pathname;
3492
+ try {
3493
+ sourcePath = decodeURIComponent(sourcePath);
3494
+ } catch {}
3495
+ return external_pathe_resolve(rootPath, sourcePath.replace(/^\/+/, ''));
3496
+ };
3497
+ const normalizeSourceMap = (sourceMap, rootPath, sourceMapUrl, scriptOrigin, projectOrigin)=>{
3498
+ const webpackSourceRoot = 'string' == typeof sourceMap.sourceRoot && sourceMap.sourceRoot.startsWith('webpack:') ? sourceMap.sourceRoot : void 0;
3499
+ const resolvedSources = new TraceMap(sourceMap, sourceMapUrl).resolvedSources;
3500
+ const { sourceRoot: _, ...sourceMapWithoutRoot } = sourceMap;
3501
+ return {
3502
+ ...sourceMapWithoutRoot,
3503
+ sources: sourceMap.sources.map((source, index)=>{
3504
+ if ('string' != typeof source) return source;
3505
+ if ('rstest runtime' === source || source.startsWith('webpack/runtime/') || source.startsWith('data:') || source.startsWith('blob:')) return source;
3506
+ if (external_pathe_isAbsolute(source)) return source;
3507
+ let sourceUrl;
3508
+ try {
3509
+ sourceUrl = webpackSourceRoot ? new URL(source, webpackSourceRoot) : new URL(source);
3510
+ } catch {
3511
+ const resolvedSource = resolvedSources[index] ?? source;
3512
+ return resolveProjectHttpSource(resolvedSource, rootPath, projectOrigin);
3513
+ }
3514
+ if ('webpack:' !== sourceUrl.protocol) {
3515
+ const resolvedSource = resolvedSources[index] ?? source;
3516
+ return resolveProjectHttpSource(resolvedSource, rootPath, projectOrigin);
3517
+ }
3518
+ let sourcePath = sourceUrl.pathname;
3519
+ try {
3520
+ sourcePath = decodeURIComponent(sourcePath);
3521
+ } catch {}
3522
+ sourcePath = sourcePath.replace(/^\/+/, '');
3523
+ if ('rstest runtime' === sourcePath || sourcePath.startsWith('data:') || sourcePath.startsWith('blob:')) return sourcePath;
3524
+ if (sourcePath.startsWith('webpack/runtime/')) return source;
3525
+ return scriptOrigin === projectOrigin ? external_pathe_resolve(rootPath, sourcePath) : new URL(sourcePath, `${scriptOrigin}/`).href;
3526
+ })
3527
+ };
3528
+ };
3529
+ const takeBrowserV8Coverage = async ({ collector, page, projectUrl, rootPath, fetchTimeout, sourceMapCache, resourceStore })=>{
3530
+ const rawEntries = await collector.take(page);
3531
+ const projectOrigin = new URL(projectUrl).origin;
3532
+ const entries = [];
3533
+ const assetFiles = {};
3534
+ const sourceMaps = {};
3535
+ await Promise.all(rawEntries.map(async (entry)=>{
3536
+ const url = resolveCoverageResourceUrl(entry.url);
3537
+ const cacheUrl = normalizeJavaScriptUrl(entry.url);
3538
+ if (!url || !cacheUrl || !entry.source) return;
3539
+ const inlineSourceMap = resolveInlineSourceMap(entry.source, url);
3540
+ const loadedSourceMap = inlineSourceMap ? null : await loadSourceMapForSource({
3541
+ jsUrl: url,
3542
+ signal: fetchTimeout > 0 ? AbortSignal.timeout(fetchTimeout) : void 0,
3543
+ source: entry.source
3544
+ });
3545
+ const sourceMap = inlineSourceMap ?? loadedSourceMap?.sourceMap;
3546
+ const normalizedSourceMap = sourceMap ? normalizeSourceMap(sourceMap, rootPath, loadedSourceMap?.sourceMapUrl ?? url, new URL(url).origin, projectOrigin) : void 0;
3547
+ const filePath = url;
3548
+ entries.push({
3549
+ url,
3550
+ scriptId: entry.scriptId,
3551
+ filePath,
3552
+ functions: entry.functions
3553
+ });
3554
+ if (resourceStore?.assetFiles.get(filePath) !== entry.source) {
3555
+ assetFiles[filePath] = entry.source;
3556
+ resourceStore?.assetFiles.set(filePath, entry.source);
3557
+ }
3558
+ if (normalizedSourceMap) {
3559
+ sourceMapCache.set(cacheUrl, normalizedSourceMap);
3560
+ const serializedSourceMap = JSON.stringify(normalizedSourceMap);
3561
+ if (resourceStore?.sourceMaps.get(filePath) !== serializedSourceMap) {
3562
+ sourceMaps[filePath] = serializedSourceMap;
3563
+ resourceStore?.sourceMaps.set(filePath, serializedSourceMap);
3564
+ }
3565
+ } else {
3566
+ sourceMapCache.delete(cacheUrl);
3567
+ resourceStore?.sourceMaps.delete(filePath);
3568
+ }
3569
+ }));
3570
+ if (!entries.length) return null;
3571
+ return {
3572
+ entries,
3573
+ ...Object.keys(assetFiles).length || Object.keys(sourceMaps).length ? {
3574
+ options: {
3575
+ assetFiles,
3576
+ sourceMaps
3577
+ }
3578
+ } : {},
3579
+ root: rootPath
3580
+ };
3581
+ };
3311
3582
  function createPromiseWithResolvers() {
3312
3583
  let resolve;
3313
3584
  let reject;
@@ -3343,7 +3614,7 @@ function createBirpc($functions, options) {
3343
3614
  const req = {
3344
3615
  m: method,
3345
3616
  a: args,
3346
- t: TYPE_REQUEST
3617
+ t: "q"
3347
3618
  };
3348
3619
  if (optional) req.o = true;
3349
3620
  const send = async (_req)=>post(serialize(_req));
@@ -3394,7 +3665,7 @@ function createBirpc($functions, options) {
3394
3665
  $call: (method, ...args)=>_call(method, args, false),
3395
3666
  $callOptional: (method, ...args)=>_call(method, args, false, true),
3396
3667
  $callEvent: (method, ...args)=>_call(method, args, true),
3397
- $callRaw: (options$1)=>_call(options$1.method, options$1.args, options$1.event, options$1.optional),
3668
+ $callRaw: (options)=>_call(options.method, options.args, options.event, options.optional),
3398
3669
  $rejectPendingCalls,
3399
3670
  get $closed () {
3400
3671
  return $closed;
@@ -3407,7 +3678,7 @@ function createBirpc($functions, options) {
3407
3678
  };
3408
3679
  rpc = proxify ? new Proxy({}, {
3409
3680
  get (_, method) {
3410
- if (Object.prototype.hasOwnProperty.call(builtinMethods, method)) return builtinMethods[method];
3681
+ if (Object.hasOwn(builtinMethods, method)) return builtinMethods[method];
3411
3682
  if ("then" === method && !eventNames.includes("then") && !("then" in $functions)) return;
3412
3683
  const sendEvent = (...args)=>_call(method, args, true);
3413
3684
  if (eventNames.includes(method)) {
@@ -3451,7 +3722,7 @@ function createBirpc($functions, options) {
3451
3722
  if (options.onGeneralError?.call(rpc, e) !== true) throw e;
3452
3723
  return;
3453
3724
  }
3454
- if (msg.t === TYPE_REQUEST) {
3725
+ if ("q" === msg.t) {
3455
3726
  const { m: method, a: args, o: optional } = msg;
3456
3727
  let result, error;
3457
3728
  let fn = await (resolver ? resolver.call(rpc, method, $functions[method]) : $functions[method]);
@@ -3468,7 +3739,7 @@ function createBirpc($functions, options) {
3468
3739
  }
3469
3740
  if (!error) try {
3470
3741
  await post(serialize({
3471
- t: TYPE_RESPONSE,
3742
+ t: "s",
3472
3743
  i: msg.i,
3473
3744
  r: result
3474
3745
  }), ...extra);
@@ -3479,7 +3750,7 @@ function createBirpc($functions, options) {
3479
3750
  }
3480
3751
  try {
3481
3752
  await post(serialize({
3482
- t: TYPE_RESPONSE,
3753
+ t: "s",
3483
3754
  i: msg.i,
3484
3755
  e: error
3485
3756
  }), ...extra);
@@ -3507,16 +3778,20 @@ class ContainerRpcManager {
3507
3778
  rpc = null;
3508
3779
  methods;
3509
3780
  onDisconnect;
3781
+ onAttach;
3510
3782
  detachActiveSocketListeners = null;
3511
- constructor(wss, methods, onDisconnect){
3783
+ transportEpoch = 0;
3784
+ constructor(wss, methods, onDisconnect, onAttach){
3512
3785
  this.wss = wss;
3513
3786
  this.methods = methods;
3514
3787
  this.onDisconnect = onDisconnect;
3788
+ this.onAttach = onAttach;
3515
3789
  this.setupConnectionHandler();
3516
3790
  }
3517
- updateMethods(methods, onDisconnect) {
3791
+ updateMethods(methods, onDisconnect, onAttach) {
3518
3792
  this.methods = methods;
3519
3793
  this.onDisconnect = onDisconnect;
3794
+ this.onAttach = onAttach;
3520
3795
  if (this.ws && this.ws.readyState === this.ws.OPEN) this.attachWebSocket(this.ws);
3521
3796
  }
3522
3797
  setupConnectionHandler() {
@@ -3529,6 +3804,8 @@ class ContainerRpcManager {
3529
3804
  attachWebSocket(ws) {
3530
3805
  this.detachActiveSocketListeners?.();
3531
3806
  if (this.rpc && !this.rpc.$closed) this.rpc.$close(new Error('Container RPC transport reattached'));
3807
+ this.transportEpoch += 1;
3808
+ this.onAttach?.(this.transportEpoch);
3532
3809
  this.ws = ws;
3533
3810
  const messageHandlers = new WeakMap();
3534
3811
  this.rpc = createBirpc(this.methods, {
@@ -3578,30 +3855,19 @@ class ContainerRpcManager {
3578
3855
  reattach(ws) {
3579
3856
  this.attachWebSocket(ws);
3580
3857
  }
3581
- async notifyTestFileUpdate(files) {
3582
- await this.rpc?.onTestFileUpdate(files);
3858
+ async notifyTestFileUpdate(files, version) {
3859
+ await this.rpc?.onTestFileUpdate(files, version);
3583
3860
  }
3584
3861
  async updateHostConfig(config) {
3585
3862
  await this.rpc?.onHostConfigUpdate(config);
3586
3863
  }
3587
- async reloadTestFile(testFile, testNamePattern) {
3864
+ async reloadTestFile(testFile, runId, testNamePattern) {
3588
3865
  logger.debug(`[Browser UI] reloadTestFile called, rpc: ${this.rpc ? 'exists' : 'null'}, ws: ${this.ws ? 'exists' : 'null'}`);
3589
3866
  if (!this.rpc) throw new Error('Browser UI RPC not available for reloadTestFile');
3590
- logger.debug(`[Browser UI] Calling reloadTestFile: ${testFile}`);
3591
- return this.rpc.reloadTestFile(testFile, testNamePattern);
3867
+ logger.debug(`[Browser UI] Calling reloadTestFile: ${testFile} (run ${runId})`);
3868
+ await this.rpc.reloadTestFile(testFile, runId, testNamePattern);
3592
3869
  }
3593
3870
  }
3594
- const createHeadedSerialTaskQueue = ()=>{
3595
- let queue = Promise.resolve();
3596
- const enqueue = (task)=>{
3597
- const next = queue.then(task);
3598
- queue = next.catch(()=>{});
3599
- return next;
3600
- };
3601
- return {
3602
- enqueue
3603
- };
3604
- };
3605
3871
  const getFileTaskId = (testPath)=>`file:${testPath}`;
3606
3872
  const toError = (error)=>error instanceof Error ? error : new Error(String(error));
3607
3873
  const createDeferredPromise = ()=>{
@@ -3617,6 +3883,105 @@ const createDeferredPromise = ()=>{
3617
3883
  reject
3618
3884
  };
3619
3885
  };
3886
+ const RUN_BOOT_DEADLINE_MS = 45000;
3887
+ const createHeadedRunRegistry = ()=>{
3888
+ const runsById = new Map();
3889
+ let transportEpoch = 0;
3890
+ const settle = (record, error)=>{
3891
+ if (!runsById.delete(record.runId)) return;
3892
+ clearTimeout(record.bootTimer);
3893
+ clearTimeout(record.cleanupTimer);
3894
+ if (error) record.deferred.reject(error);
3895
+ else record.deferred.resolve();
3896
+ };
3897
+ const sweepOpen = (match, error)=>{
3898
+ for (const record of runsById.values())if ('open' === record.phase && match(record)) settle(record, error);
3899
+ };
3900
+ return {
3901
+ mint (testPath) {
3902
+ sweepOpen((record)=>record.testPath === testPath);
3903
+ const runId = randomUUID();
3904
+ const bootTimer = setTimeout(()=>{
3905
+ const record = runsById.get(runId);
3906
+ if (record) settle(record, new Error(`Headed run for ${testPath} produced no message within ${RUN_BOOT_DEADLINE_MS / 1000}s; its document never booted`));
3907
+ }, RUN_BOOT_DEADLINE_MS);
3908
+ bootTimer.unref?.();
3909
+ const record = {
3910
+ runId,
3911
+ testPath,
3912
+ epoch: transportEpoch,
3913
+ phase: 'open',
3914
+ deferred: createDeferredPromise(),
3915
+ bootTimer
3916
+ };
3917
+ runsById.set(runId, record);
3918
+ return {
3919
+ runId,
3920
+ settled: record.deferred.promise
3921
+ };
3922
+ },
3923
+ admit (runId, terminal) {
3924
+ const record = runsById.get(runId);
3925
+ if (!record) return false;
3926
+ clearTimeout(record.bootTimer);
3927
+ if (!terminal) return true;
3928
+ if ('open' !== record.phase) return false;
3929
+ record.phase = 'claimed';
3930
+ return true;
3931
+ },
3932
+ armCleanupDeadline (runId, ms, onExpire) {
3933
+ const record = runsById.get(runId);
3934
+ if (!record || 'open' !== record.phase) return;
3935
+ clearTimeout(record.cleanupTimer);
3936
+ const cleanupTimer = setTimeout(()=>{
3937
+ const current = runsById.get(runId);
3938
+ if (!current || 'open' !== current.phase) return;
3939
+ current.cleanupTimer = void 0;
3940
+ current.phase = 'claimed';
3941
+ onExpire();
3942
+ }, ms);
3943
+ cleanupTimer.unref?.();
3944
+ record.cleanupTimer = cleanupTimer;
3945
+ },
3946
+ disarmCleanupDeadline (runId) {
3947
+ const record = runsById.get(runId);
3948
+ if (record) {
3949
+ clearTimeout(record.cleanupTimer);
3950
+ record.cleanupTimer = void 0;
3951
+ }
3952
+ },
3953
+ resolve (runId) {
3954
+ const record = runsById.get(runId);
3955
+ if (record) settle(record);
3956
+ },
3957
+ reject (runId, error) {
3958
+ const record = runsById.get(runId);
3959
+ if (record) settle(record, toError(error));
3960
+ },
3961
+ retainPaths (paths) {
3962
+ const retained = new Set(paths);
3963
+ sweepOpen((record)=>!retained.has(record.testPath));
3964
+ },
3965
+ rejectAll (error) {
3966
+ sweepOpen(()=>true, error);
3967
+ },
3968
+ setTransportEpoch (epoch) {
3969
+ transportEpoch = epoch;
3970
+ sweepOpen((record)=>record.epoch < epoch, new Error('Container transport was replaced before the run completed'));
3971
+ }
3972
+ };
3973
+ };
3974
+ const createHeadedSerialTaskQueue = ()=>{
3975
+ let queue = Promise.resolve();
3976
+ const enqueue = (task)=>{
3977
+ const next = queue.then(task);
3978
+ queue = next.catch(()=>{});
3979
+ return next;
3980
+ };
3981
+ return {
3982
+ enqueue
3983
+ };
3984
+ };
3620
3985
  const claimHeadedCycleScope = (testPaths, currentTestFiles, pendingTestNamePatterns)=>{
3621
3986
  const scope = [];
3622
3987
  const filesByPath = new Map(currentTestFiles.map((file)=>[
@@ -3636,131 +4001,161 @@ const claimHeadedCycleScope = (testPaths, currentTestFiles, pendingTestNamePatte
3636
4001
  }
3637
4002
  return scope;
3638
4003
  };
3639
- const createHeadedScheduler = async ({ context, runtime, allTestFiles, hostOptions, isWatchMode, createDispatchRouter, handlers: { handleTestFileStart, handleTestCaseResult, handleTestFileComplete, handleLog, handleFatal }, fatalErrorRef, watchSignals, setDispatchPageResolver, createWatchSession, collectProjectEntries, logWatchReady, destroyRuntime })=>{
4004
+ const createHeadedScheduler = async ({ context, runtime, allTestFiles, hostOptions, v8Coverage, projectRoots, isWatchMode, createDispatchRouter, handlers: { handleTestFileComplete }, fatalErrorRef, watchSignals, setDispatchPageResolver, createWatchSession, collectProjectEntries, logWatchReady, destroyRuntime })=>{
3640
4005
  const { browser, browserLaunchOptions, watchState, wss } = runtime;
4006
+ const containerUrl = `http://localhost:${runtime.containerServer.port}/`;
3641
4007
  let currentTestFiles = allTestFiles;
3642
4008
  const RUNNER_FRAMES_READY_TIMEOUT_MS = 30000;
3643
- let currentRunnerFramesSignature = null;
3644
- const runnerFramesWaiters = new Map();
3645
- const createTestFilesSignature = (testFiles)=>JSON.stringify(testFiles.map((testFile)=>normalize(testFile)));
3646
- const markRunnerFramesReady = (testFiles)=>{
3647
- const signature = createTestFilesSignature(testFiles);
3648
- currentRunnerFramesSignature = signature;
3649
- const waiters = runnerFramesWaiters.get(signature);
3650
- if (!waiters) return;
3651
- runnerFramesWaiters.delete(signature);
3652
- for (const waiter of waiters)waiter();
4009
+ let ackedFrameSetVersion = 0;
4010
+ const frameSetWaiters = new Set();
4011
+ const markFrameSetReady = (version)=>{
4012
+ if (version > watchState.headedFileSetVersion) return;
4013
+ if (version > ackedFrameSetVersion) ackedFrameSetVersion = version;
4014
+ for (const waiter of frameSetWaiters)if (waiter.version <= ackedFrameSetVersion) {
4015
+ clearTimeout(waiter.timeoutId);
4016
+ frameSetWaiters.delete(waiter);
4017
+ waiter.resolve();
4018
+ }
3653
4019
  };
3654
- const waitForRunnerFramesReady = async (testFiles)=>{
3655
- const signature = createTestFilesSignature(testFiles);
3656
- if (currentRunnerFramesSignature === signature) return;
4020
+ const waitForFrameSet = async (version)=>{
4021
+ if (ackedFrameSetVersion >= version) return;
3657
4022
  await new Promise((resolve, reject)=>{
3658
- const waiters = runnerFramesWaiters.get(signature) ?? new Set();
3659
- const cleanup = ()=>{
3660
- const currentWaiters = runnerFramesWaiters.get(signature);
3661
- if (!currentWaiters) return;
3662
- currentWaiters.delete(onReady);
3663
- if (0 === currentWaiters.size) runnerFramesWaiters.delete(signature);
3664
- };
3665
- const onReady = ()=>{
3666
- if (timeoutId) clearTimeout(timeoutId);
3667
- cleanup();
3668
- resolve();
4023
+ const waiter = {
4024
+ version,
4025
+ resolve,
4026
+ timeoutId: setTimeout(()=>{
4027
+ frameSetWaiters.delete(waiter);
4028
+ reject(new Error(`Timed out waiting for headed runner frames to be ready (file-set version ${version}).`));
4029
+ }, RUNNER_FRAMES_READY_TIMEOUT_MS)
3669
4030
  };
3670
- const timeoutId = setTimeout(()=>{
3671
- cleanup();
3672
- reject(new Error(`Timed out waiting for headed runner frames to be ready for ${testFiles.length} file(s).`));
3673
- }, RUNNER_FRAMES_READY_TIMEOUT_MS);
3674
- waiters.add(onReady);
3675
- runnerFramesWaiters.set(signature, waiters);
3676
- if (currentRunnerFramesSignature === signature) onReady();
4031
+ frameSetWaiters.add(waiter);
3677
4032
  });
3678
4033
  };
3679
- const getTestFileInfo = (testFile)=>{
4034
+ const findTestFileInfo = (testFile)=>{
3680
4035
  const normalizedTestFile = normalize(testFile);
3681
- const fileInfo = currentTestFiles.find((file)=>file.testPath === normalizedTestFile);
4036
+ return currentTestFiles.find((file)=>file.testPath === normalizedTestFile);
4037
+ };
4038
+ const getTestFileInfo = (testFile)=>{
4039
+ const fileInfo = findTestFileInfo(testFile);
3682
4040
  if (!fileInfo) throw new Error(`Unknown browser test file: ${JSON.stringify(testFile)}`);
3683
4041
  return fileInfo;
3684
4042
  };
3685
4043
  let containerContext;
3686
4044
  let containerPage;
3687
4045
  let isNewPage = false;
4046
+ const attachContainerPageHandlers = (activeContext, activePage)=>{
4047
+ activePage.on('popup', async (popup)=>{
4048
+ await popup.close().catch(()=>{});
4049
+ });
4050
+ activeContext.on('page', async (page)=>{
4051
+ if (page !== activePage) await page.close().catch(()=>{});
4052
+ });
4053
+ activePage.on('console', (msg)=>{
4054
+ const text = msg.text();
4055
+ if (text.startsWith('[Container]') || text.startsWith('[Runner]')) return void logger.log(color.gray(`[Browser Console] ${text}`));
4056
+ logger.debug(`[Browser Console] ${text}`);
4057
+ });
4058
+ };
4059
+ const createContainerPage = async ()=>{
4060
+ const nextContext = await browser.newContext({
4061
+ providerOptions: browserLaunchOptions.providerOptions,
4062
+ viewport: null
4063
+ });
4064
+ const nextPage = await nextContext.newPage();
4065
+ attachContainerPageHandlers(nextContext, nextPage);
4066
+ return {
4067
+ context: nextContext,
4068
+ page: nextPage
4069
+ };
4070
+ };
3688
4071
  if (isWatchMode && runtime.containerPage && runtime.containerContext) {
3689
4072
  containerContext = runtime.containerContext;
3690
4073
  containerPage = runtime.containerPage;
3691
4074
  logger.log(color.gray('\n[Watch] Reusing existing container page\n'));
3692
4075
  } else {
3693
4076
  isNewPage = true;
3694
- containerContext = await browser.newContext({
3695
- providerOptions: browserLaunchOptions.providerOptions,
3696
- viewport: null
3697
- });
3698
- containerPage = await containerContext.newPage();
3699
- containerPage.on('popup', async (popup)=>{
3700
- await popup.close().catch(()=>{});
3701
- });
3702
- containerContext.on('page', async (page)=>{
3703
- if (page !== containerPage) await page.close().catch(()=>{});
3704
- });
4077
+ const created = await createContainerPage();
4078
+ containerContext = created.context;
4079
+ containerPage = created.page;
3705
4080
  if (isWatchMode) {
3706
4081
  runtime.containerPage = containerPage;
3707
4082
  runtime.containerContext = containerContext;
3708
4083
  }
3709
- containerPage.on('console', (msg)=>{
3710
- const text = msg.text();
3711
- if (text.startsWith('[Container]') || text.startsWith('[Runner]')) logger.log(color.gray(`[Browser Console] ${text}`));
3712
- });
3713
4084
  }
3714
4085
  setDispatchPageResolver(()=>({
3715
4086
  containerPage
3716
4087
  }));
3717
4088
  const dispatchRouter = createDispatchRouter();
3718
4089
  const headedReloadQueue = createHeadedSerialTaskQueue();
3719
- const pendingHeadedReloads = new Map();
3720
- let enqueueHeadedReload = async (_file, _testNamePattern)=>{
3721
- throw new Error('Headed reload queue is not initialized');
4090
+ let rawCoverage = [];
4091
+ let coverageProject;
4092
+ let coverageStarted = false;
4093
+ const startCoverage = async (projectName)=>{
4094
+ if (!v8Coverage) return;
4095
+ await v8Coverage.start(containerPage);
4096
+ coverageStarted = true;
4097
+ coverageProject = projectName;
3722
4098
  };
3723
- const rejectPendingHeadedReload = (testPath, error, runId)=>{
3724
- const pending = pendingHeadedReloads.get(testPath);
3725
- if (!pending) return;
3726
- if (runId && pending.runId !== runId) return;
3727
- pendingHeadedReloads.delete(testPath);
3728
- pending.deferred.reject(error);
4099
+ const collectCoverage = async ()=>{
4100
+ if (!coverageStarted || !v8Coverage) return;
4101
+ coverageStarted = false;
4102
+ const coverage = await v8Coverage.take(containerPage, projectRoots.get(coverageProject ?? '') ?? context.rootPath, coverageProject);
4103
+ if (coverage) rawCoverage.push(coverage);
3729
4104
  };
3730
- const rejectAllPendingHeadedReloads = (error)=>{
3731
- for (const [testPath, pending] of pendingHeadedReloads){
3732
- pendingHeadedReloads.delete(testPath);
3733
- pending.deferred.reject(error);
3734
- }
4105
+ let enqueueHeadedReload = async (_file, _testNamePattern)=>{
4106
+ throw new Error('Headed reload queue is not initialized');
3735
4107
  };
3736
- const registerPendingHeadedReload = (testPath, runId)=>{
3737
- const previousPending = pendingHeadedReloads.get(testPath);
3738
- if (previousPending) {
3739
- previousPending.deferred.reject(new Error(`Reload for "${testPath}" was superseded by a newer request.`));
3740
- pendingHeadedReloads.delete(testPath);
4108
+ const isCurrentTestFile = (testPath)=>void 0 !== findTestFileInfo(testPath);
4109
+ const runs = watchState.headedRuns ??= createHeadedRunRegistry();
4110
+ const recoverHeadedContainer = async ()=>{
4111
+ ackedFrameSetVersion = 0;
4112
+ await containerContext.close().catch(()=>{});
4113
+ const created = await createContainerPage();
4114
+ containerContext = created.context;
4115
+ containerPage = created.page;
4116
+ if (isWatchMode) {
4117
+ runtime.containerPage = containerPage;
4118
+ runtime.containerContext = containerContext;
3741
4119
  }
3742
- const deferred = createDeferredPromise();
3743
- pendingHeadedReloads.set(testPath, {
3744
- runId,
3745
- deferred
4120
+ await containerPage.goto(containerUrl, {
4121
+ waitUntil: 'load'
3746
4122
  });
3747
- return deferred.promise;
3748
- };
3749
- const resolvePendingHeadedReload = (testPath, runId)=>{
3750
- const pending = pendingHeadedReloads.get(testPath);
3751
- if (!pending) return;
3752
- if (runId && pending.runId !== runId) return void logger.debug(`[Browser UI] Ignoring stale file-complete for ${testPath}. current=${pending.runId}, incoming=${runId}`);
3753
- pendingHeadedReloads.delete(testPath);
3754
- pending.deferred.resolve();
4123
+ await waitForFrameSet(watchState.headedFileSetVersion);
3755
4124
  };
4125
+ dispatchRouter.register(DISPATCH_NAMESPACE_FILE_CLEANUP, async (request)=>{
4126
+ const { runId } = request;
4127
+ if (!runId) return;
4128
+ if ('start' === request.method) {
4129
+ const payload = request.args;
4130
+ runs.armCleanupDeadline(runId, FIXTURE_CLEANUP_TIMEOUT_MS, ()=>{
4131
+ (async ()=>{
4132
+ try {
4133
+ await collectCoverage();
4134
+ await handleTestFileComplete(createFileCleanupTimeoutResult({
4135
+ message: `File fixture cleanup did not finish within ${FIXTURE_CLEANUP_TIMEOUT_MS}ms`,
4136
+ projectName: payload.projectName,
4137
+ result: payload.result,
4138
+ testPath: payload.testPath
4139
+ }));
4140
+ await recoverHeadedContainer();
4141
+ runs.resolve(runId);
4142
+ } catch (error) {
4143
+ runs.reject(runId, toError(error));
4144
+ }
4145
+ })();
4146
+ });
4147
+ } else if ('end' === request.method) runs.disarmCleanupDeadline(runId);
4148
+ });
3756
4149
  const reloadTestFileAndWait = async (file, testNamePattern)=>{
3757
- let reloadAck;
4150
+ await startCoverage(file.projectName);
4151
+ const { runId, settled } = runs.mint(file.testPath);
4152
+ rpcManager.reloadTestFile(file.testPath, runId, testNamePattern).catch((error)=>runs.reject(runId, error));
3758
4153
  try {
3759
- reloadAck = await rpcManager.reloadTestFile(file.testPath, testNamePattern);
3760
- await registerPendingHeadedReload(file.testPath, reloadAck.runId);
3761
- } catch (error) {
3762
- if (reloadAck?.runId) rejectPendingHeadedReload(file.testPath, toError(error), reloadAck.runId);
3763
- throw error;
4154
+ await settled;
4155
+ } finally{
4156
+ await collectCoverage().catch((error)=>{
4157
+ logger.debug(`[Headed] Failed to flush coverage: ${error}`);
4158
+ });
3764
4159
  }
3765
4160
  };
3766
4161
  let runUiRequestedRerun = async (file, testNamePattern)=>{
@@ -3775,66 +4170,73 @@ const createHeadedScheduler = async ({ context, runtime, allTestFiles, hostOptio
3775
4170
  await runUiRequestedRerun(getTestFileInfo(testFile), testNamePattern);
3776
4171
  },
3777
4172
  async getTestFiles () {
3778
- return currentTestFiles;
3779
- },
3780
- async onRunnerFramesReady (testFiles) {
3781
- markRunnerFramesReady(testFiles);
3782
- },
3783
- async onTestFileStart (payload) {
3784
- await handleTestFileStart(payload);
4173
+ return {
4174
+ files: currentTestFiles,
4175
+ version: watchState.headedFileSetVersion
4176
+ };
3785
4177
  },
3786
- async onTestCaseResult (payload) {
3787
- await handleTestCaseResult(payload);
4178
+ async onFrameSetReady (version) {
4179
+ markFrameSetReady(version);
3788
4180
  },
3789
- async onTestFileComplete (payload) {
3790
- try {
3791
- await handleTestFileComplete(payload);
3792
- resolvePendingHeadedReload(payload.testPath, payload.runId);
4181
+ async dispatch (request) {
4182
+ const { method, runId } = request;
4183
+ const isTerminal = "runner" === request.namespace && ('file-complete' === method || 'fatal' === method);
4184
+ if (!runId || !runs.admit(runId, isTerminal)) {
4185
+ logger.debug(`[Headed] Dropped stale "${method}" from run ${runId ?? '(none)'}`);
4186
+ return {
4187
+ requestId: request.requestId,
4188
+ stale: true
4189
+ };
4190
+ }
4191
+ let coverageError;
4192
+ if (isTerminal) try {
4193
+ await collectCoverage();
3793
4194
  } catch (error) {
3794
- rejectPendingHeadedReload(payload.testPath, toError(error), payload.runId);
3795
- throw error;
4195
+ coverageError = toError(error);
3796
4196
  }
3797
- },
3798
- async onLog (payload) {
3799
- await handleLog(payload);
3800
- },
3801
- async onFatal (payload) {
3802
- const error = new Error(payload.message);
3803
- error.stack = payload.stack;
3804
- rejectAllPendingHeadedReloads(error);
3805
- await handleFatal(payload);
3806
- },
3807
- async dispatch (request) {
3808
- return dispatchRouter.dispatch(request);
4197
+ const response = await dispatchRouter.dispatch(request);
4198
+ if (!isTerminal) return response;
4199
+ if ('file-complete' === method) {
4200
+ const failure = coverageError ?? (response.error ? new Error(response.error) : void 0);
4201
+ if (failure) runs.reject(runId, failure);
4202
+ else runs.resolve(runId);
4203
+ } else {
4204
+ if (coverageError) logger.error(`[Headed] Failed to take coverage during fatal handling: ${coverageError.message}`);
4205
+ const payload = request.args;
4206
+ const error = new Error(payload?.message ?? 'Fatal browser error');
4207
+ if (payload?.stack) error.stack = payload.stack;
4208
+ runs.reject(runId, error);
4209
+ }
4210
+ return response;
3809
4211
  }
3810
4212
  });
3811
4213
  let rpcManager;
3812
4214
  if (isWatchMode && runtime.rpcManager) {
3813
4215
  rpcManager = runtime.rpcManager;
3814
- rpcManager.updateMethods(createRpcMethods(), rejectAllPendingHeadedReloads);
4216
+ rpcManager.updateMethods(createRpcMethods(), runs.rejectAll, runs.setTransportEpoch);
3815
4217
  const existingWs = rpcManager.currentWebSocket;
3816
4218
  if (existingWs) rpcManager.reattach(existingWs);
3817
4219
  } else {
3818
- rpcManager = new ContainerRpcManager(wss, createRpcMethods(), rejectAllPendingHeadedReloads);
4220
+ rpcManager = new ContainerRpcManager(wss, createRpcMethods(), runs.rejectAll, runs.setTransportEpoch);
3819
4221
  if (isWatchMode) runtime.rpcManager = rpcManager;
3820
4222
  }
3821
4223
  if (isNewPage) {
3822
- const pagePath = '/';
3823
- const containerPort = runtime.containerServer.port;
3824
- await containerPage.goto(`http://localhost:${containerPort}${pagePath}`, {
4224
+ await containerPage.goto(containerUrl, {
3825
4225
  waitUntil: 'load'
3826
4226
  });
3827
- logger.log(color.cyan(`\nBrowser mode opened at http://localhost:${containerPort}${pagePath}\n`));
4227
+ logger.log(color.cyan(`\nBrowser mode opened at ${containerUrl}\n`));
3828
4228
  }
3829
4229
  enqueueHeadedReload = async (file, testNamePattern)=>headedReloadQueue.enqueue(async ()=>{
3830
4230
  if (fatalErrorRef.current) return;
4231
+ if (!isCurrentTestFile(file.testPath)) return void logger.debug(`[Browser UI] Skipping reload for removed test file: ${file.testPath}`);
3831
4232
  await reloadTestFileAndWait(file, testNamePattern);
3832
4233
  });
3833
4234
  let testTime = 0;
3834
4235
  if (currentTestFiles.length > 0) {
3835
4236
  const testStart = Date.now();
3836
4237
  try {
3837
- await waitForRunnerFramesReady(currentTestFiles.map((file)=>file.testPath));
4238
+ if (rpcManager.isConnected) await rpcManager.notifyTestFileUpdate(currentTestFiles, watchState.headedFileSetVersion);
4239
+ await waitForFrameSet(watchState.headedFileSetVersion);
3838
4240
  for (const file of currentTestFiles){
3839
4241
  await enqueueHeadedReload(file);
3840
4242
  if (fatalErrorRef.current) break;
@@ -3848,8 +4250,10 @@ const createHeadedScheduler = async ({ context, runtime, allTestFiles, hostOptio
3848
4250
  if (isWatchMode) {
3849
4251
  const pendingTestNamePatterns = new Map();
3850
4252
  const runScope = async (testPaths)=>{
4253
+ rawCoverage = [];
3851
4254
  const cycleScope = claimHeadedCycleScope(testPaths, currentTestFiles, pendingTestNamePatterns);
3852
4255
  for (const { file, testNamePattern } of cycleScope)await enqueueHeadedReload(file, testNamePattern);
4256
+ return rawCoverage;
3853
4257
  };
3854
4258
  const refreshHostConfig = async ()=>{
3855
4259
  const refreshedHostOptions = {
@@ -3861,46 +4265,44 @@ const createHeadedScheduler = async ({ context, runtime, allTestFiles, hostOptio
3861
4265
  runtime.setContainerOptions(refreshedHostOptions);
3862
4266
  await rpcManager.updateHostConfig(refreshedHostOptions);
3863
4267
  };
3864
- watchSignals.setDispatchRerun(async ()=>{
3865
- const [, newProjectEntries] = await Promise.all([
3866
- refreshHostConfig(),
3867
- collectProjectEntries()
3868
- ]);
3869
- const rerunPlan = planWatchRerun({
3870
- projectEntries: newProjectEntries,
3871
- previousTestFiles: watchState.lastTestFiles,
3872
- affectedTestFiles: drainPendingAffectedTestFiles(watchState)
3873
- });
3874
- if (rerunPlan.filesChanged) {
3875
- const deletedTestPaths = collectDeletedTestPaths(watchState.lastTestFiles, rerunPlan.currentTestFiles);
3876
- if (deletedTestPaths.length > 0) context.updateReporterResultState([], [], deletedTestPaths);
3877
- watchState.lastTestFiles = rerunPlan.currentTestFiles;
3878
- currentTestFiles = rerunPlan.currentTestFiles;
3879
- await rpcManager.notifyTestFileUpdate(currentTestFiles);
3880
- if (0 === currentTestFiles.length) {
3881
- logger.log(color.cyan('No browser test files remain after update.\n'));
3882
- logWatchReady();
3883
- return;
4268
+ const runDispatchRerunOnce = async ()=>{
4269
+ try {
4270
+ const [, newProjectEntries] = await Promise.all([
4271
+ refreshHostConfig(),
4272
+ collectProjectEntries()
4273
+ ]);
4274
+ const rerunPlan = planWatchRerun({
4275
+ projectEntries: newProjectEntries,
4276
+ previousTestFiles: watchState.lastTestFiles,
4277
+ affectedTestFiles: drainPendingAffectedTestFiles(watchState)
4278
+ });
4279
+ commitWatchFileSetUpdate(rerunPlan.fileSetUpdate, watchState, (deletedTestPaths)=>context.updateReporterResultState([], [], deletedTestPaths));
4280
+ if (rerunPlan.fileSetUpdate) {
4281
+ currentTestFiles = rerunPlan.fileSetUpdate.currentTestFiles;
4282
+ const version = watchState.headedFileSetVersion += 1;
4283
+ runs.retainPaths(currentTestFiles.map((file)=>file.testPath));
4284
+ await rpcManager.notifyTestFileUpdate(currentTestFiles, version);
4285
+ if (currentTestFiles.length > 0) await waitForFrameSet(version);
3884
4286
  }
3885
- await waitForRunnerFramesReady(currentTestFiles.map((file)=>file.testPath));
3886
- }
3887
- if (rerunPlan.normalizedAffectedTestFiles.length > 0) {
3888
- logger.log(color.cyan(`Re-running ${rerunPlan.normalizedAffectedTestFiles.length} affected test file(s)...\n`));
3889
- await watchSignals.signalInvalidation(rerunPlan.normalizedAffectedTestFiles);
3890
- return;
4287
+ logger.log(color.cyan(rerunPlan.decision.message));
4288
+ if ('idle' === rerunPlan.decision.kind) return void logWatchReady();
4289
+ await watchSignals.signalInvalidation('empty' === rerunPlan.decision.kind ? [] : rerunPlan.decision.testPaths);
4290
+ } catch (error) {
4291
+ logWatchReady();
4292
+ throw error;
3891
4293
  }
3892
- if (!rerunPlan.filesChanged) logger.log(color.cyan('Tests will be re-executed automatically\n'));
3893
- logWatchReady();
3894
- });
4294
+ };
4295
+ const dispatchRerunQueue = createHeadedSerialTaskQueue();
4296
+ watchSignals.setDispatchRerun(()=>dispatchRerunQueue.enqueue(runDispatchRerunOnce));
3895
4297
  runUiRequestedRerun = async (file, testNamePattern)=>{
3896
4298
  await refreshHostConfig();
3897
- await watchSignals.signalInvalidation([
4299
+ const { cycle } = await watchSignals.signalInvalidation([
3898
4300
  file.testPath
3899
4301
  ], ()=>{
3900
4302
  if (void 0 === testNamePattern) pendingTestNamePatterns.delete(normalize(file.testPath));
3901
4303
  else pendingTestNamePatterns.set(normalize(file.testPath), testNamePattern);
3902
4304
  });
3903
- await watchSignals.awaitSignalledCycle();
4305
+ await cycle;
3904
4306
  };
3905
4307
  watchSession = createWatchSession(runScope);
3906
4308
  }
@@ -3915,6 +4317,7 @@ const createHeadedScheduler = async ({ context, runtime, allTestFiles, hostOptio
3915
4317
  };
3916
4318
  return {
3917
4319
  testTime,
4320
+ rawCoverage,
3918
4321
  watchSession,
3919
4322
  close: closeContainerRuntime
3920
4323
  };
@@ -4053,8 +4456,12 @@ class RunnerSessionRegistry {
4053
4456
  this.sessionIdByTestFile.clear();
4054
4457
  }
4055
4458
  }
4056
- const createHeadlessScheduler = async ({ context, browser, browserLaunchOptions, projectServers, allTestFiles, projectRuntimeConfigs, hostOptions, watchState, isWatchMode, createDispatchRouter, handlers: { handleFatal, handleTestFileComplete }, watchSignals, setDispatchPageResolver, createWatchSession, collectProjectEntries, logWatchReady, destroyRuntime })=>{
4459
+ const createHeadlessScheduler = async ({ context, browser, browserLaunchOptions, projectServers, v8Coverage, allTestFiles, projectRuntimeConfigs, hostOptions, watchState, isWatchMode, createDispatchRouter, handlers: { handleFatal, handleTestFileComplete }, watchSignals, setDispatchPageResolver, createWatchSession, collectProjectEntries, logWatchReady, destroyRuntime })=>{
4057
4460
  const viewportByProject = mapViewportByProject(projectRuntimeConfigs);
4461
+ const projectRootByName = new Map(projectRuntimeConfigs.map((project)=>[
4462
+ project.name,
4463
+ project.projectRoot
4464
+ ]));
4058
4465
  const runLifecycle = new RunSessionLifecycle();
4059
4466
  const sessionRegistry = new RunnerSessionRegistry();
4060
4467
  setDispatchPageResolver((target)=>({
@@ -4075,12 +4482,32 @@ const createHeadlessScheduler = async ({ context, browser, browserLaunchOptions,
4075
4482
  }
4076
4483
  });
4077
4484
  };
4485
+ const collectRunCoverage = async (run)=>{
4486
+ const results = await Promise.allSettled(Array.from(run.coverageCollectors, (collect)=>collect()));
4487
+ for (const result of results)if ('rejected' === result.status) {
4488
+ const error = toError(result.reason);
4489
+ await handleFatal({
4490
+ message: error.message,
4491
+ stack: error.stack
4492
+ });
4493
+ }
4494
+ };
4078
4495
  const dispatchRouter = createDispatchRouter({
4079
4496
  isRunTokenStale: (runToken)=>runLifecycle.isTokenStale(runToken),
4080
4497
  onStale: (request)=>{
4081
4498
  if ("runner" === request.namespace) logger.debug(`[Headless] Dropped stale message "${request.method}" for ${request.target?.testFile ?? 'unknown'}`);
4082
4499
  }
4083
4500
  });
4501
+ const fileCleanupHandlers = new Map();
4502
+ dispatchRouter.register(DISPATCH_NAMESPACE_FILE_CLEANUP, async (request)=>{
4503
+ const sessionId = request.target?.sessionId;
4504
+ if (!sessionId) throw new Error('File cleanup dispatch is missing a browser session.');
4505
+ const handler = fileCleanupHandlers.get(sessionId);
4506
+ if (!handler) return;
4507
+ const payload = request.args;
4508
+ if ('start' === request.method) handler.start(payload.result);
4509
+ else if ('end' === request.method) handler.end();
4510
+ });
4084
4511
  const dispatchRunnerMessage = async (run, file, sessionId, message)=>{
4085
4512
  const response = await dispatchRouter.dispatch({
4086
4513
  requestId: nextDispatchRequestId("runner"),
@@ -4097,7 +4524,7 @@ const createHeadlessScheduler = async ({ context, browser, browserLaunchOptions,
4097
4524
  if (response.stale) return;
4098
4525
  if (response.error) throw new Error(response.error);
4099
4526
  };
4100
- const runSingleFile = async (run, file)=>{
4527
+ const runSingleFile = async (run, file, rawCoverage)=>{
4101
4528
  if (run.cancelled || runLifecycle.isTokenStale(run.token)) return;
4102
4529
  const viewport = viewportByProject.get(file.projectName);
4103
4530
  const browserContext = await browser.newContext({
@@ -4107,8 +4534,12 @@ const createHeadlessScheduler = async ({ context, browser, browserLaunchOptions,
4107
4534
  run.contexts.add(browserContext);
4108
4535
  let page = null;
4109
4536
  let sessionId = null;
4537
+ let coverageCollection;
4110
4538
  let settled = false;
4111
4539
  let resolveDone = null;
4540
+ let cleanupTimer;
4541
+ let fileCleanupFinished = false;
4542
+ let provisionalResult;
4112
4543
  const markDone = ()=>{
4113
4544
  if (!settled) {
4114
4545
  settled = true;
@@ -4124,8 +4555,18 @@ const createHeadlessScheduler = async ({ context, browser, browserLaunchOptions,
4124
4555
  settled = true;
4125
4556
  crashDeferred.resolve(reason);
4126
4557
  };
4558
+ const collectCoverage = async ()=>{
4559
+ if (!page || !v8Coverage) return;
4560
+ if (!coverageCollection) coverageCollection = (async ()=>{
4561
+ const coverage = await v8Coverage.take(page, projectRootByName.get(file.projectName) ?? context.rootPath, file.projectName);
4562
+ if (coverage) rawCoverage.push(coverage);
4563
+ })();
4564
+ await coverageCollection;
4565
+ };
4127
4566
  try {
4128
4567
  page = await browserContext.newPage();
4568
+ await v8Coverage?.start(page);
4569
+ run.coverageCollectors.add(collectCoverage);
4129
4570
  page.on('crash', ()=>onPageDead(`Browser page crashed while running ${file.testPath}.`));
4130
4571
  page.on('close', ()=>onPageDead(`Browser page closed unexpectedly while running ${file.testPath}.`));
4131
4572
  const session = sessionRegistry.register({
@@ -4137,13 +4578,61 @@ const createHeadlessScheduler = async ({ context, browser, browserLaunchOptions,
4137
4578
  page
4138
4579
  });
4139
4580
  sessionId = session.id;
4581
+ const clearFileCleanupTimeout = ()=>{
4582
+ if (cleanupTimer) {
4583
+ clearTimeout(cleanupTimer);
4584
+ cleanupTimer = void 0;
4585
+ }
4586
+ };
4587
+ const finishFileCleanup = ()=>{
4588
+ fileCleanupFinished = true;
4589
+ clearFileCleanupTimeout();
4590
+ };
4591
+ fileCleanupHandlers.set(session.id, {
4592
+ end: finishFileCleanup,
4593
+ start: (result)=>{
4594
+ if (fileCleanupFinished) return;
4595
+ provisionalResult = result;
4596
+ clearFileCleanupTimeout();
4597
+ cleanupTimer = setTimeout(()=>{
4598
+ (async ()=>{
4599
+ if (settled) return;
4600
+ settled = true;
4601
+ const message = `File fixture cleanup did not finish within ${FIXTURE_CLEANUP_TIMEOUT_MS}ms`;
4602
+ try {
4603
+ await collectCoverage();
4604
+ await handleTestFileComplete(createFileCleanupTimeoutResult({
4605
+ message,
4606
+ projectName: file.projectName,
4607
+ result: provisionalResult,
4608
+ testPath: file.testPath
4609
+ }));
4610
+ } catch (error) {
4611
+ const formatted = toError(error);
4612
+ await handleFatal({
4613
+ message: formatted.message,
4614
+ stack: formatted.stack
4615
+ });
4616
+ await collectRunCoverage(run);
4617
+ await cancelRun(run, false);
4618
+ } finally{
4619
+ resolveDone?.();
4620
+ }
4621
+ })();
4622
+ }, FIXTURE_CLEANUP_TIMEOUT_MS);
4623
+ }
4624
+ });
4140
4625
  await attachHeadlessRunnerTransport(page, {
4141
- onDispatchMessage: async (message)=>{
4626
+ onDispatchMessage: async (envelope)=>{
4627
+ const message = envelope.message;
4142
4628
  try {
4629
+ if (settled) return;
4630
+ if ('file-complete' === message.type || 'fatal' === message.type) await collectCoverage();
4143
4631
  await dispatchRunnerMessage(run, file, session.id, message);
4144
- if ('file-complete' === message.type || 'complete' === message.type) markDone();
4632
+ if ('file-complete' === message.type) markDone();
4145
4633
  else if ('fatal' === message.type) {
4146
4634
  markDone();
4635
+ await collectRunCoverage(run);
4147
4636
  await cancelRun(run, false);
4148
4637
  }
4149
4638
  } catch (error) {
@@ -4153,6 +4642,7 @@ const createHeadlessScheduler = async ({ context, browser, browserLaunchOptions,
4153
4642
  stack: formatted.stack
4154
4643
  });
4155
4644
  markDone();
4645
+ await collectRunCoverage(run);
4156
4646
  await cancelRun(run, false);
4157
4647
  }
4158
4648
  },
@@ -4196,6 +4686,7 @@ const createHeadlessScheduler = async ({ context, browser, browserLaunchOptions,
4196
4686
  ]);
4197
4687
  if ('cancelled' === state.type) return;
4198
4688
  if ('crash' === state.type && runLifecycle.isTokenActive(run.token) && !run.cancelled) {
4689
+ await collectRunCoverage(run);
4199
4690
  await handleFatal({
4200
4691
  message: state.reason
4201
4692
  });
@@ -4203,6 +4694,7 @@ const createHeadlessScheduler = async ({ context, browser, browserLaunchOptions,
4203
4694
  }
4204
4695
  } catch (error) {
4205
4696
  if (runLifecycle.isTokenActive(run.token) && !run.cancelled) {
4697
+ await collectRunCoverage(run);
4206
4698
  const formatted = toError(error);
4207
4699
  await handleFatal({
4208
4700
  message: formatted.message,
@@ -4211,6 +4703,9 @@ const createHeadlessScheduler = async ({ context, browser, browserLaunchOptions,
4211
4703
  await cancelRun(run, false);
4212
4704
  }
4213
4705
  } finally{
4706
+ if (cleanupTimer) clearTimeout(cleanupTimer);
4707
+ if (sessionId) fileCleanupHandlers.delete(sessionId);
4708
+ run.coverageCollectors.delete(collectCoverage);
4214
4709
  const abandoned = run.cancelled || runLifecycle.isTokenStale(run.token);
4215
4710
  const teardown = async ()=>{
4216
4711
  if (page) try {
@@ -4233,12 +4728,14 @@ const createHeadlessScheduler = async ({ context, browser, browserLaunchOptions,
4233
4728
  results: []
4234
4729
  });
4235
4730
  const runFilesWithPool = async (files)=>{
4236
- if (0 === files.length) return;
4731
+ const rawCoverage = [];
4732
+ if (0 === files.length) return rawCoverage;
4237
4733
  const previous = runLifecycle.activeSession;
4238
4734
  if (previous) await cancelRun(previous);
4239
4735
  const run = runLifecycle.createSession((token)=>({
4240
4736
  ...createRunSession(token),
4241
- contexts: new Set()
4737
+ contexts: new Set(),
4738
+ coverageCollectors: new Set()
4242
4739
  }));
4243
4740
  const queue = [
4244
4741
  ...files
@@ -4257,7 +4754,7 @@ const createHeadlessScheduler = async ({ context, browser, browserLaunchOptions,
4257
4754
  }
4258
4755
  const next = queue.shift();
4259
4756
  if (!next) return;
4260
- await runSingleFile(run, next);
4757
+ await runSingleFile(run, next, rawCoverage);
4261
4758
  }
4262
4759
  };
4263
4760
  run.done = Promise.all(Array.from({
@@ -4265,15 +4762,16 @@ const createHeadlessScheduler = async ({ context, browser, browserLaunchOptions,
4265
4762
  }, ()=>worker())).then(()=>{});
4266
4763
  await run.done;
4267
4764
  runLifecycle.clearIfActive(run);
4765
+ return rawCoverage;
4268
4766
  };
4269
4767
  const testStart = Date.now();
4270
- await runFilesWithPool(allTestFiles);
4768
+ const rawCoverage = await runFilesWithPool(allTestFiles);
4271
4769
  const testTime = Date.now() - testStart;
4272
4770
  let watchSession;
4273
4771
  if (isWatchMode) {
4274
4772
  const runScope = async (testPaths)=>{
4275
4773
  const pathSet = new Set(testPaths.map((testPath)=>normalize(testPath)));
4276
- await runFilesWithPool(watchState.lastTestFiles.filter((file)=>pathSet.has(file.testPath)));
4774
+ return runFilesWithPool(watchState.lastTestFiles.filter((file)=>pathSet.has(file.testPath)));
4277
4775
  };
4278
4776
  watchSignals.setInterrupt(async ()=>{
4279
4777
  const active = runLifecycle.activeSession;
@@ -4290,30 +4788,10 @@ const createHeadlessScheduler = async ({ context, browser, browserLaunchOptions,
4290
4788
  previousTestFiles: watchState.lastTestFiles,
4291
4789
  affectedTestFiles: drainPendingAffectedTestFiles(watchState)
4292
4790
  });
4293
- if (rerunPlan.filesChanged) {
4294
- const deletedTestPaths = collectDeletedTestPaths(watchState.lastTestFiles, rerunPlan.currentTestFiles);
4295
- if (deletedTestPaths.length > 0) context.updateReporterResultState([], [], deletedTestPaths);
4296
- watchState.lastTestFiles = rerunPlan.currentTestFiles;
4297
- if (0 === rerunPlan.currentTestFiles.length) {
4298
- logger.log(color.cyan('No browser test files remain after update.\n'));
4299
- await watchSignals.signalInvalidation([]);
4300
- return;
4301
- }
4302
- logger.log(color.cyan(`Test file set changed, re-running ${rerunPlan.currentTestFiles.length} file(s)...\n`));
4303
- await watchSignals.signalInvalidation([
4304
- ...new Set(rerunPlan.currentTestFiles.map((file)=>file.testPath))
4305
- ]);
4306
- return;
4307
- }
4308
- if (0 === rerunPlan.affectedTestFiles.length) {
4309
- logger.log(color.cyan('No affected browser test files detected, skipping re-run.\n'));
4310
- logWatchReady();
4311
- return;
4312
- }
4313
- logger.log(color.cyan(`Re-running ${rerunPlan.affectedTestFiles.length} affected test file(s)...\n`));
4314
- await watchSignals.signalInvalidation([
4315
- ...new Set(rerunPlan.affectedTestFiles.map((file)=>file.testPath))
4316
- ]);
4791
+ commitWatchFileSetUpdate(rerunPlan.fileSetUpdate, watchState, (deletedTestPaths)=>context.updateReporterResultState([], [], deletedTestPaths));
4792
+ logger.log(color.cyan(rerunPlan.decision.message));
4793
+ if ('idle' === rerunPlan.decision.kind) return void logWatchReady();
4794
+ await watchSignals.signalInvalidation('empty' === rerunPlan.decision.kind ? [] : rerunPlan.decision.testPaths);
4317
4795
  });
4318
4796
  watchSession = createWatchSession(runScope);
4319
4797
  }
@@ -4323,6 +4801,7 @@ const createHeadlessScheduler = async ({ context, browser, browserLaunchOptions,
4323
4801
  };
4324
4802
  return {
4325
4803
  testTime,
4804
+ rawCoverage,
4326
4805
  watchSession,
4327
4806
  close: closeHeadlessRuntime
4328
4807
  };
@@ -4351,7 +4830,6 @@ const validateBrowserRpcRequest = (payload)=>{
4351
4830
  const request = {
4352
4831
  id: readString(payload, 'id', 'id'),
4353
4832
  testPath: readString(payload, 'testPath', 'testPath'),
4354
- runId: readString(payload, 'runId', 'runId'),
4355
4833
  kind,
4356
4834
  locator: parseBrowserLocatorIR(payload.locator, 'locator'),
4357
4835
  method: readString(payload, 'method', 'method'),
@@ -4369,47 +4847,6 @@ const validateBrowserRpcRequest = (payload)=>{
4369
4847
  }
4370
4848
  return request;
4371
4849
  };
4372
- const normalizeJavaScriptUrl = (value, options)=>{
4373
- try {
4374
- const url = options?.origin ? new URL(value, options.origin) : new URL(value);
4375
- if ('http:' !== url.protocol && 'https:' !== url.protocol) return null;
4376
- url.search = '';
4377
- url.hash = '';
4378
- return url.toString();
4379
- } catch {
4380
- return null;
4381
- }
4382
- };
4383
- const resolveInlineSourceMap = (code)=>{
4384
- const converter = convert_source_map.fromSource(code);
4385
- if (!converter) return null;
4386
- return converter.toObject();
4387
- };
4388
- const fetchSourceMap = async (jsUrl, fetcher)=>{
4389
- const jsResponse = await fetcher(jsUrl);
4390
- if (!jsResponse.ok) return null;
4391
- const code = await jsResponse.text();
4392
- const inlineMap = resolveInlineSourceMap(code);
4393
- if (inlineMap) return inlineMap;
4394
- const mapResponse = await fetcher(`${jsUrl}.map`);
4395
- if (!mapResponse.ok) return null;
4396
- return await mapResponse.json();
4397
- };
4398
- const loadSourceMapWithCache = async ({ jsUrl, cache, force = false, origin, fetcher = fetch })=>{
4399
- const normalizedUrl = normalizeJavaScriptUrl(jsUrl, {
4400
- origin
4401
- });
4402
- if (!normalizedUrl) return null;
4403
- if (!force && cache.has(normalizedUrl)) return cache.get(normalizedUrl) ?? null;
4404
- try {
4405
- const sourceMap = await fetchSourceMap(normalizedUrl, fetcher);
4406
- cache.set(normalizedUrl, sourceMap);
4407
- return sourceMap;
4408
- } catch {
4409
- cache.set(normalizedUrl, null);
4410
- return null;
4411
- }
4412
- };
4413
4850
  const watchContext = {
4414
4851
  runtime: null,
4415
4852
  cleanupRegistered: false,
@@ -4455,12 +4892,16 @@ const createWatchSignals = (onInvalidate)=>{
4455
4892
  async signalInvalidation (fileFilters, claimScope) {
4456
4893
  await interruptInFlightRun?.();
4457
4894
  claimScope?.();
4458
- signalledCycle = Promise.resolve(onInvalidate?.({
4895
+ const cycle = Promise.resolve(onInvalidate?.({
4459
4896
  isFirstBuild: false,
4460
4897
  fileFilters
4461
- })).catch((error)=>{
4898
+ })).then(()=>{}, (error)=>{
4462
4899
  logger.error(color.red('Browser Mode watch cycle failed:'), error);
4463
4900
  });
4901
+ signalledCycle = cycle;
4902
+ return {
4903
+ cycle
4904
+ };
4464
4905
  },
4465
4906
  async awaitSignalledCycle () {
4466
4907
  await signalledCycle;
@@ -4490,6 +4931,21 @@ const runBrowserController = async (context, options)=>{
4490
4931
  const browserProjects = options?.projects ?? getBrowserProjects(context);
4491
4932
  const useHeadlessDirect = browserProjects.every((project)=>project.normalizedConfig.browser.headless);
4492
4933
  const browserSourceMapCache = new Map();
4934
+ const browserCoverageResources = {
4935
+ assetFiles: new Map(),
4936
+ sourceMaps: new Map()
4937
+ };
4938
+ const loadBrowserCoverageResources = async (filenames, resource)=>{
4939
+ const store = browserCoverageResources[resource];
4940
+ const loaded = {};
4941
+ for (const filename of filenames){
4942
+ const value = store.get(filename);
4943
+ if (void 0 !== value) loaded[filename] = value;
4944
+ }
4945
+ return loaded;
4946
+ };
4947
+ const loadBrowserCoverageAssetFiles = (filenames)=>loadBrowserCoverageResources(filenames, 'assetFiles');
4948
+ const loadBrowserCoverageSourceMaps = (filenames)=>loadBrowserCoverageResources(filenames, 'sourceMaps');
4493
4949
  const isHttpLikeFile = (file)=>/^https?:\/\//.test(file);
4494
4950
  const resolveBrowserSourcemap = async (sourcePath)=>{
4495
4951
  if (!isHttpLikeFile(sourcePath)) return {
@@ -4546,7 +5002,8 @@ const runBrowserController = async (context, options)=>{
4546
5002
  }
4547
5003
  };
4548
5004
  const coverageConfig = browserProjects.find((project)=>project.normalizedConfig.coverage?.enabled)?.normalizedConfig.coverage;
4549
- const coverageProvider = coverageConfig?.enabled ? await createCoverageProvider(coverageConfig, context.rootPath) : null;
5005
+ const coverageProvider = !filesOnly && 'list' !== context.command && coverageConfig?.enabled ? await createCoverageProvider(coverageConfig, context.rootPath) : null;
5006
+ const browserCoverageCapabilityError = filesOnly || 'list' === context.command || coverageConfig?.provider !== 'v8' || (browserProjects[0]?.normalizedConfig.browser.browser ?? 'chromium') !== 'chromium' || coverageProvider?.supportsBrowserCoverage === true ? void 0 : new Error("The installed @rstest/coverage-v8 provider does not support Chromium Browser Mode coverage. Upgrade @rstest/coverage-v8 to the version matching @rstest/core, or use coverage.provider: 'istanbul'.");
4550
5007
  const containerDevServerEnv = process.env.RSTEST_CONTAINER_DEV_SERVER;
4551
5008
  let containerDevServer;
4552
5009
  let containerDistPath;
@@ -4609,12 +5066,9 @@ const runBrowserController = async (context, options)=>{
4609
5066
  freezeShardedEntries: options?.freezeShardedEntries,
4610
5067
  tempDir,
4611
5068
  isWatchMode,
4612
- onTriggerRerun: isWatchMode ? async ()=>{
4613
- await watchSignals.runDispatchRerun();
4614
- } : void 0,
4615
5069
  containerDistPath,
4616
5070
  containerDevServer,
4617
- skipProviderLaunch: filesOnly,
5071
+ skipProviderLaunch: filesOnly || Boolean(browserCoverageCapabilityError),
4618
5072
  appliedModifyRstestConfigEnvironments: options?.appliedModifyRstestConfigEnvironments
4619
5073
  });
4620
5074
  } catch (error) {
@@ -4625,13 +5079,18 @@ const runBrowserController = async (context, options)=>{
4625
5079
  }).catch(()=>{});
4626
5080
  });
4627
5081
  }
4628
- if (isWatchMode && !filesOnly) {
5082
+ if (isWatchMode && !filesOnly && !browserCoverageCapabilityError) {
4629
5083
  watchContext.runtime = runtime;
4630
5084
  registerWatchCleanup(context.embedded);
4631
5085
  }
4632
5086
  }
4633
5087
  const watchState = runtime.watchState;
4634
- if (isWatchMode) watchState.lastTestFiles = collectWatchTestFiles(projectEntries);
5088
+ if (isWatchMode) {
5089
+ watchState.lastTestFiles = collectWatchTestFiles(projectEntries);
5090
+ watchState.triggerRerun = async ()=>{
5091
+ await watchSignals.runDispatchRerun();
5092
+ };
5093
+ }
4635
5094
  const seedPendingRerun = (testPaths)=>{
4636
5095
  const wanted = testPaths ? new Set(testPaths.map((testPath)=>normalize(testPath))) : null;
4637
5096
  let seeded = 0;
@@ -4644,7 +5103,7 @@ const runBrowserController = async (context, options)=>{
4644
5103
  }
4645
5104
  return seeded;
4646
5105
  };
4647
- const buildRerunOutcome = ({ rerunTestPaths, testTime, unhandledErrors })=>{
5106
+ const buildRerunOutcome = ({ rerunTestPaths, testTime, rawCoverage, unhandledErrors })=>{
4648
5107
  const rerunPathSet = new Set(rerunTestPaths);
4649
5108
  const rerunResults = context.reporterResults.results.filter((result)=>rerunPathSet.has(result.testPath));
4650
5109
  const coverageMap = buildBrowserCoverageMap(rerunResults, coverageProvider);
@@ -4657,8 +5116,11 @@ const runBrowserController = async (context, options)=>{
4657
5116
  buildTime: drainPendingBuildTime(watchState),
4658
5117
  testTime
4659
5118
  },
4660
- coverage: coverageMap?.files().length ? {
4661
- map: coverageMap.toJSON()
5119
+ coverage: coverageMap?.files().length || rawCoverage.length ? {
5120
+ map: coverageMap?.toJSON(),
5121
+ raw: rawCoverage,
5122
+ loadAssetFiles: loadBrowserCoverageAssetFiles,
5123
+ loadSourceMaps: loadBrowserCoverageSourceMaps
4662
5124
  } : void 0,
4663
5125
  resolveSourcemap: resolveBrowserSourcemap
4664
5126
  };
@@ -4666,17 +5128,19 @@ const runBrowserController = async (context, options)=>{
4666
5128
  const createWatchSession = (execute)=>({
4667
5129
  runCycle: async (testPaths)=>{
4668
5130
  const rerunStartTime = Date.now();
4669
- const fatalErrorBeforeRun = fatalErrorRef.current;
5131
+ fatalErrorRef.current = null;
4670
5132
  let rerunError;
5133
+ let rawCoverage = [];
4671
5134
  try {
4672
- await execute(testPaths);
5135
+ rawCoverage = await execute(testPaths);
4673
5136
  } catch (error) {
4674
5137
  rerunError = toError(error);
4675
5138
  }
4676
- const rerunFatalError = fatalErrorRef.current && fatalErrorRef.current !== fatalErrorBeforeRun ? fatalErrorRef.current : void 0;
5139
+ const rerunFatalError = fatalErrorRef.current ?? void 0;
4677
5140
  return buildRerunOutcome({
4678
5141
  rerunTestPaths: testPaths,
4679
5142
  testTime: Math.max(0, Date.now() - rerunStartTime),
5143
+ rawCoverage,
4680
5144
  unhandledErrors: rerunError ? [
4681
5145
  rerunError
4682
5146
  ] : rerunFatalError ? [
@@ -4712,6 +5176,10 @@ const runBrowserController = async (context, options)=>{
4712
5176
  await destroyBrowserRuntime(runtime);
4713
5177
  return allowEmptyRun ? createEmptyRunResult() : void 0;
4714
5178
  }
5179
+ if (browserCoverageCapabilityError) {
5180
+ await destroyBrowserRuntime(runtime);
5181
+ return failWithError(browserCoverageCapabilityError);
5182
+ }
4715
5183
  const { browser, browserLaunchOptions, wsPort } = runtime;
4716
5184
  const allTestFiles = projectEntries.flatMap((entry)=>entry.testFiles.map((testPath)=>({
4717
5185
  testPath: normalize(testPath),
@@ -4734,17 +5202,18 @@ const runBrowserController = async (context, options)=>{
4734
5202
  name,
4735
5203
  `http://localhost:${server.port}`
4736
5204
  ]));
5205
+ const containerRunnerUrl = `http://localhost:${runtime.containerServer.port}`;
4737
5206
  const hostOptions = {
4738
5207
  rootPath: normalize(context.rootPath),
4739
5208
  projects: projectRuntimeConfigs,
4740
5209
  snapshot: {
4741
5210
  updateSnapshot: options?.updateSnapshot ?? context.snapshotManager.options.updateSnapshot
4742
5211
  },
4743
- runnerUrl: `http://localhost:${runtime.containerServer.port}`,
5212
+ runnerUrl: containerRunnerUrl,
4744
5213
  projectRunnerUrls,
4745
5214
  wsPort,
4746
5215
  debug: isDebug(),
4747
- rpcTimeout: maxTestTimeoutForRpc
5216
+ rpcTimeout: -1
4748
5217
  };
4749
5218
  const browserProviderProjects = browserProjects.map((project)=>({
4750
5219
  rootPath: normalize(project.rootPath),
@@ -4752,6 +5221,22 @@ const runBrowserController = async (context, options)=>{
4752
5221
  }));
4753
5222
  const implementationByProvider = new Map();
4754
5223
  for (const browserProject of browserProviderProjects)if (!implementationByProvider.has(browserProject.provider)) implementationByProvider.set(browserProject.provider, getBrowserProviderImplementation(browserProject.provider));
5224
+ const browserName = browserLaunchOptions.browser ?? 'chromium';
5225
+ const v8CoverageCollector = coverageConfig?.provider === 'v8' ? implementationByProvider.get(browserLaunchOptions.provider)?.createV8CoverageCollector?.({
5226
+ browserName
5227
+ }) ?? null : null;
5228
+ const v8Coverage = v8CoverageCollector ? {
5229
+ start: v8CoverageCollector.start,
5230
+ take: (page, projectRoot, projectName)=>takeBrowserV8Coverage({
5231
+ collector: v8CoverageCollector,
5232
+ fetchTimeout: maxTestTimeoutForRpc,
5233
+ page,
5234
+ projectUrl: projectRunnerUrls[projectName ?? ''] ?? containerRunnerUrl,
5235
+ rootPath: normalize(projectRoot),
5236
+ sourceMapCache: browserSourceMapCache,
5237
+ resourceStore: browserCoverageResources
5238
+ })
5239
+ } : void 0;
4755
5240
  let resolveDispatchPages = ()=>({});
4756
5241
  const setDispatchPageResolver = (resolver)=>{
4757
5242
  resolveDispatchPages = resolver;
@@ -4801,14 +5286,22 @@ const runBrowserController = async (context, options)=>{
4801
5286
  if (!sink) throw new Error(`No runner event sink for project "${projectName}"`);
4802
5287
  return sink;
4803
5288
  };
4804
- const silentConsoleController = createSilentConsoleController({
4805
- runtimeConfig: {
4806
- silent: context.normalizedConfig.silent,
4807
- disableConsoleIntercept: context.normalizedConfig.disableConsoleIntercept
4808
- },
4809
- emitInterceptedLog: (log)=>sinkForProjectName(log.project).onConsoleLog(log),
4810
- writeOriginalLog: ()=>{}
4811
- });
5289
+ const silentConsoleControllers = new Map(browserProjects.map((project)=>[
5290
+ project.name,
5291
+ createSilentConsoleController({
5292
+ runtimeConfig: {
5293
+ silent: project.normalizedConfig.silent,
5294
+ disableConsoleIntercept: project.normalizedConfig.disableConsoleIntercept
5295
+ },
5296
+ emitInterceptedLog: (log)=>sinkForProjectName(log.project).onConsoleLog(log),
5297
+ writeOriginalLog: ()=>{}
5298
+ })
5299
+ ]));
5300
+ const silentConsoleControllerForProjectName = (projectName)=>{
5301
+ const controller = silentConsoleControllers.get(projectName);
5302
+ if (!controller) throw new Error(`No silent console controller for project "${projectName}"`);
5303
+ return controller;
5304
+ };
4812
5305
  const snapshotRpcMethods = {
4813
5306
  async resolveSnapshotPath (testPath) {
4814
5307
  return resolveSnapshotPathDefault(testPath, context.normalizedConfig.resolveSnapshotPath);
@@ -4863,7 +5356,7 @@ const runBrowserController = async (context, options)=>{
4863
5356
  const handleTestSuiteResult = async (payload)=>{
4864
5357
  phaseTrackers?.get(trackerKey(payload.project, payload.testPath))?.recordSuiteResult(payload);
4865
5358
  await sinkForProjectName(payload.project).onTestSuiteResult(payload);
4866
- if ('passed-only' === context.normalizedConfig.silent) silentConsoleController.flushBufferedLogsForTask({
5359
+ silentConsoleControllerForProjectName(payload.project).flushBufferedLogsForTask({
4867
5360
  taskId: payload.testId,
4868
5361
  status: payload.status,
4869
5362
  taskParentNames: payload.parentNames,
@@ -4879,7 +5372,7 @@ const runBrowserController = async (context, options)=>{
4879
5372
  caseResults.push(payload);
4880
5373
  phaseTrackers?.get(trackerKey(payload.project, payload.testPath))?.recordCaseResult(payload);
4881
5374
  await sinkForProjectName(payload.project).onTestCaseResult(payload);
4882
- if ('passed-only' === context.normalizedConfig.silent) silentConsoleController.flushBufferedLogsForTask({
5375
+ silentConsoleControllerForProjectName(payload.project).flushBufferedLogsForTask({
4883
5376
  taskId: payload.testId,
4884
5377
  status: payload.status,
4885
5378
  taskParentNames: payload.parentNames,
@@ -4902,7 +5395,7 @@ const runBrowserController = async (context, options)=>{
4902
5395
  phaseTrackers.delete(key);
4903
5396
  }
4904
5397
  }
4905
- if ('passed-only' === context.normalizedConfig.silent) silentConsoleController.flushBufferedLogsForTask({
5398
+ silentConsoleControllerForProjectName(payload.project).flushBufferedLogsForTask({
4906
5399
  taskId: payload.testId,
4907
5400
  status: payload.status,
4908
5401
  taskParentNames: payload.parentNames,
@@ -4924,7 +5417,7 @@ const runBrowserController = async (context, options)=>{
4924
5417
  type: payload.type,
4925
5418
  trace: payload.trace
4926
5419
  };
4927
- silentConsoleController.onConsoleLog(log);
5420
+ silentConsoleControllerForProjectName(payload.projectName).onConsoleLog(log);
4928
5421
  };
4929
5422
  const handleFatal = async (payload)=>{
4930
5423
  const error = new Error(payload.message);
@@ -4971,6 +5464,10 @@ const runBrowserController = async (context, options)=>{
4971
5464
  context,
4972
5465
  allTestFiles,
4973
5466
  hostOptions,
5467
+ projectRoots: new Map(projectRuntimeConfigs.map((project)=>[
5468
+ project.name,
5469
+ project.projectRoot
5470
+ ])),
4974
5471
  isWatchMode,
4975
5472
  createDispatchRouter,
4976
5473
  fatalErrorRef,
@@ -4981,11 +5478,12 @@ const runBrowserController = async (context, options)=>{
4981
5478
  logWatchReady: ()=>logWatchReadyMessage(context, enableCliShortcuts),
4982
5479
  destroyRuntime: ()=>destroyBrowserRuntime(runtime)
4983
5480
  };
4984
- const { testTime, watchSession, close } = useHeadlessDirect ? await createHeadlessScheduler({
5481
+ const { testTime, rawCoverage, watchSession, close } = useHeadlessDirect ? await createHeadlessScheduler({
4985
5482
  ...schedulerDeps,
4986
5483
  browser,
4987
5484
  browserLaunchOptions,
4988
5485
  projectServers: runtime.projectServers,
5486
+ v8Coverage,
4989
5487
  projectRuntimeConfigs,
4990
5488
  watchState,
4991
5489
  handlers: {
@@ -4995,17 +5493,23 @@ const runBrowserController = async (context, options)=>{
4995
5493
  }) : await createHeadedScheduler({
4996
5494
  ...schedulerDeps,
4997
5495
  runtime,
5496
+ v8Coverage,
4998
5497
  handlers: {
4999
- handleTestFileStart,
5000
- handleTestCaseResult,
5001
- handleTestFileComplete,
5002
- handleLog,
5003
- handleFatal
5498
+ handleTestFileComplete
5004
5499
  }
5005
5500
  });
5006
- if (fatalErrorRef.current) return failWithError(fatalErrorRef.current, close);
5501
+ watchState.hooksEnabled = void 0 !== watchSession;
5502
+ if (fatalErrorRef.current) {
5503
+ const errorResult = await failWithError(fatalErrorRef.current, close);
5504
+ return {
5505
+ ...errorResult,
5506
+ rawCoverage,
5507
+ loadAssetFiles: loadBrowserCoverageAssetFiles,
5508
+ loadSourceMaps: loadBrowserCoverageSourceMaps,
5509
+ watchSession
5510
+ };
5511
+ }
5007
5512
  context.updateReporterResultState(reporterResults, caseResults);
5008
- watchState.hooksEnabled = isWatchMode;
5009
5513
  return {
5010
5514
  results: reporterResults,
5011
5515
  testResults: caseResults,
@@ -5015,6 +5519,9 @@ const runBrowserController = async (context, options)=>{
5015
5519
  testTime
5016
5520
  },
5017
5521
  hasFailure: reporterResults.some((result)=>'fail' === result.status),
5522
+ rawCoverage,
5523
+ loadAssetFiles: loadBrowserCoverageAssetFiles,
5524
+ loadSourceMaps: loadBrowserCoverageSourceMaps,
5018
5525
  getSourcemap: getBrowserSourcemap,
5019
5526
  resolveSourcemap: resolveBrowserSourcemap,
5020
5527
  close,
@@ -5083,7 +5590,6 @@ const listBrowserTests = async (context, options)=>{
5083
5590
  })),
5084
5591
  viewport: project.normalizedConfig.browser.viewport
5085
5592
  }));
5086
- const maxTestTimeoutForRpc = getMaxTestTimeoutForRpc(browserProjects);
5087
5593
  const hostOptions = {
5088
5594
  rootPath: normalize(context.rootPath),
5089
5595
  projects: projectRuntimeConfigs,
@@ -5092,7 +5598,7 @@ const listBrowserTests = async (context, options)=>{
5092
5598
  },
5093
5599
  mode: 'collect',
5094
5600
  debug: isDebug(),
5095
- rpcTimeout: maxTestTimeoutForRpc
5601
+ rpcTimeout: -1
5096
5602
  };
5097
5603
  runtime.setContainerOptions(hostOptions);
5098
5604
  const browserContext = await browser.newContext({
@@ -5110,7 +5616,8 @@ const listBrowserTests = async (context, options)=>{
5110
5616
  resolveCollect = resolve;
5111
5617
  });
5112
5618
  const page = await browserContext.newPage();
5113
- await page.exposeFunction(DISPATCH_MESSAGE_TYPE, (message)=>{
5619
+ await page.exposeFunction(DISPATCH_MESSAGE_TYPE, (envelope)=>{
5620
+ const message = envelope.message;
5114
5621
  switch(message.type){
5115
5622
  case 'collect-result':
5116
5623
  {
@@ -5229,9 +5736,12 @@ async function createBrowserExecutor(context, options) {
5229
5736
  buildTime: result.duration.buildTime,
5230
5737
  testTime: result.duration.testTime
5231
5738
  },
5232
- coverage: {
5233
- map: map?.toJSON()
5234
- },
5739
+ coverage: map?.files().length || result.rawCoverage?.length ? {
5740
+ map: map?.toJSON(),
5741
+ raw: result.rawCoverage,
5742
+ loadAssetFiles: result.loadAssetFiles,
5743
+ loadSourceMaps: result.loadSourceMaps
5744
+ } : void 0,
5235
5745
  resolveSourcemap: result.resolveSourcemap
5236
5746
  };
5237
5747
  };