@rstest/browser 0.11.3 → 0.11.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { DEFAULT_TEST_TIMEOUT, PhaseTracker, RSTEST_ENV_SYMBOL_KEY, applyWebMockRspackConfig, browserIgnoredRuntimeConfigKeys, buildBrowserCoverageMap, color, createCoverageProvider, createRunnerEventSink, createSilentConsoleController, getNoTestFilesMessage, getNumCpus as browser_getNumCpus, getPrettyConsoleName, getSetupFiles, getTestEntries, hasUserRstestConfigPlugins, importMetaRstestDefine, initModifyRstestConfigHooks, isDebug, isTTY, loadCoverageProvider, logger, pluginMockRuntime, prepareWatchRerunState, projectRuntimeConfig, resolveProjectBuildCache, resolveShardedEntries, resolveSnapshotPathDefault, resolveWorkerCount, rsbuild, serializableConfig } from "@rstest/core/internal/browser";
1
+ import { DEFAULT_TEST_TIMEOUT, FATAL_SIGNALS, PhaseTracker, RSTEST_ENV_SYMBOL_KEY, applyWatchInvalidation, applyWebMockRspackConfig, browserIgnoredRuntimeConfigKeys, buildBrowserCoverageMap, color, createCoverageProvider, createRunnerEventSink, createSilentConsoleController, finalizeRunCycle, getNoTestFilesMessage, getNumCpus as browser_getNumCpus, getPrettyConsoleName, getSetupFiles, getTestEntries, hasUserRstestConfigPlugins, importMetaRstestDefine, initModifyRstestConfigHooks, isDebug, isTTY, loadCoverageProvider, logWatchReadyMessage, logger, pluginMockRuntime, prepareWatchRerunState, projectRuntimeConfig, resolveProjectBuildCache, resolveShardedEntries, resolveSnapshotPathDefault, resolveWorkerCount, rsbuild, serializableConfig } from "@rstest/core/internal/browser";
2
2
  import { existsSync } from "node:fs";
3
3
  import promises from "node:fs/promises";
4
4
  import { fileURLToPath } from "node:url";
@@ -2054,6 +2054,11 @@ const ignoredKeyWarnings = {
2054
2054
  message: ()=>"Ignoring isolate: false in browser mode: each test file still runs in a fresh context.",
2055
2055
  browserOnly: true
2056
2056
  },
2057
+ federation: {
2058
+ isNonDefault: (config)=>true === config.federation,
2059
+ message: ()=>'Ignoring federation in browser mode: it only applies to the Node runner.',
2060
+ browserOnly: true
2061
+ },
2057
2062
  detectAsyncLeaks: {
2058
2063
  isNonDefault: (config)=>true === config.detectAsyncLeaks,
2059
2064
  message: ()=>"Ignoring detectAsyncLeaks in browser mode: it relies on node async_hooks."
@@ -2835,44 +2840,6 @@ const loadSourceMapWithCache = async ({ jsUrl, cache, force = false, origin, fet
2835
2840
  return null;
2836
2841
  }
2837
2842
  };
2838
- const isBrowserWatchCliShortcutsEnabled = ()=>isTTY('stdin');
2839
- const getBrowserWatchCliShortcutsHintMessage = ()=>` ${color.dim('press')} ${color.bold('q')} ${color.dim('to quit')}\n`;
2840
- const logBrowserWatchReadyMessage = (enableCliShortcuts)=>{
2841
- logger.log(color.green(' Waiting for file changes...'));
2842
- if (enableCliShortcuts) logger.log(getBrowserWatchCliShortcutsHintMessage());
2843
- };
2844
- async function setupBrowserWatchCliShortcuts({ close }) {
2845
- const { emitKeypressEvents } = await import("node:readline");
2846
- emitKeypressEvents(process.stdin);
2847
- process.stdin.setRawMode(true);
2848
- process.stdin.resume();
2849
- process.stdin.setEncoding('utf8');
2850
- let isClosing = false;
2851
- const handleKeypress = (str, key)=>{
2852
- if (key.ctrl && 'c' === key.name) return void process.kill(process.pid, 'SIGINT');
2853
- if (key.ctrl && 'z' === key.name) {
2854
- if ('win32' !== process.platform) process.kill(process.pid, 'SIGTSTP');
2855
- return;
2856
- }
2857
- if ('q' !== str || isClosing) return;
2858
- isClosing = true;
2859
- (async ()=>{
2860
- try {
2861
- await close();
2862
- } finally{
2863
- process.exit(0);
2864
- }
2865
- })();
2866
- };
2867
- process.stdin.on('keypress', handleKeypress);
2868
- return ()=>{
2869
- try {
2870
- process.stdin.setRawMode(false);
2871
- process.stdin.pause();
2872
- } catch {}
2873
- process.stdin.off('keypress', handleKeypress);
2874
- };
2875
- }
2876
2843
  const serializeTestFiles = (files)=>JSON.stringify(files.map((f)=>`${f.projectName}:${f.testPath}`).sort());
2877
2844
  const normalizeTestFiles = (files)=>files.map((file)=>({
2878
2845
  ...file,
@@ -3004,6 +2971,9 @@ class ContainerRpcManager {
3004
2971
  async notifyTestFileUpdate(files) {
3005
2972
  await this.rpc?.onTestFileUpdate(files);
3006
2973
  }
2974
+ async updateHostConfig(config) {
2975
+ await this.rpc?.onHostConfigUpdate(config);
2976
+ }
3007
2977
  async reloadTestFile(testFile, testNamePattern) {
3008
2978
  logger.debug(`[Browser UI] reloadTestFile called, rpc: ${this.rpc ? 'exists' : 'null'}, ws: ${this.ws ? 'exists' : 'null'}`);
3009
2979
  if (!this.rpc) throw new Error('Browser UI RPC not available for reloadTestFile');
@@ -3011,15 +2981,29 @@ class ContainerRpcManager {
3011
2981
  return this.rpc.reloadTestFile(testFile, testNamePattern);
3012
2982
  }
3013
2983
  }
2984
+ const createBrowserWatchState = ()=>({
2985
+ lastTestFiles: [],
2986
+ hooksEnabled: false,
2987
+ invalidation: new Map(),
2988
+ pendingAffectedTestFiles: new Map(),
2989
+ compileStartTimes: new Map(),
2990
+ pendingBuildTimeMs: 0
2991
+ });
2992
+ const drainPendingBuildTime = (watchState)=>{
2993
+ const buildTime = watchState.pendingBuildTimeMs;
2994
+ watchState.pendingBuildTimeMs = 0;
2995
+ return buildTime;
2996
+ };
2997
+ const drainPendingAffectedTestFiles = (watchState)=>{
2998
+ const affected = new Set();
2999
+ for (const files of watchState.pendingAffectedTestFiles.values())for (const file of files)affected.add(file);
3000
+ watchState.pendingAffectedTestFiles.clear();
3001
+ return Array.from(affected);
3002
+ };
3014
3003
  const watchContext = {
3015
3004
  runtime: null,
3016
- lastTestFiles: [],
3017
- hooksEnabled: false,
3018
3005
  cleanupRegistered: false,
3019
- cleanupPromise: null,
3020
- closeCliShortcuts: null,
3021
- chunkHashes: new Map(),
3022
- affectedTestFiles: []
3006
+ cleanupPromise: null
3023
3007
  };
3024
3008
  const resolveViewport = (viewport)=>{
3025
3009
  if (!viewport) return null;
@@ -3202,25 +3186,42 @@ const getChunkKey = (chunk)=>{
3202
3186
  if (chunk.files && chunk.files.length > 0) return chunk.files[0];
3203
3187
  return null;
3204
3188
  };
3205
- const getAffectedTestFiles = (chunks, entryTestFiles)=>{
3206
- if (!chunks) return [];
3207
- const affectedFiles = new Set();
3208
- const currentHashes = new Map();
3209
- for (const chunk of chunks){
3189
+ const getAffectedTestFiles = ({ chunks, entryTestFiles, setupFiles, state })=>{
3190
+ const entryHashes = new Map();
3191
+ const setupHashes = new Map();
3192
+ const recordChunk = (snapshot, entryPath, chunkKey, hash)=>{
3193
+ const record = snapshot.get(entryPath) ?? {};
3194
+ record[chunkKey] = hash;
3195
+ snapshot.set(entryPath, record);
3196
+ };
3197
+ for (const chunk of chunks || []){
3210
3198
  if (!chunk.hash) continue;
3211
- const testFile = findTestFileInModules(chunk.modules, entryTestFiles);
3212
- if (!testFile) continue;
3213
3199
  const chunkKey = getChunkKey(chunk);
3214
3200
  if (!chunkKey) continue;
3215
- const prevHash = watchContext.chunkHashes.get(chunkKey);
3216
- currentHashes.set(chunkKey, chunk.hash);
3217
- if (void 0 !== prevHash && prevHash !== chunk.hash) {
3218
- affectedFiles.add(testFile);
3219
- logger.debug(`[Watch] Chunk hash changed for ${chunkKey}: ${prevHash} -> ${chunk.hash} (test: ${testFile})`);
3201
+ const testFile = findTestFileInModules(chunk.modules, entryTestFiles);
3202
+ if (testFile) {
3203
+ recordChunk(entryHashes, testFile, chunkKey, chunk.hash);
3204
+ continue;
3220
3205
  }
3206
+ const setupFile = findTestFileInModules(chunk.modules, setupFiles);
3207
+ if (setupFile) recordChunk(setupHashes, setupFile, chunkKey, chunk.hash);
3221
3208
  }
3222
- watchContext.chunkHashes = currentHashes;
3223
- return Array.from(affectedFiles);
3209
+ const seedFirstSeen = (baseline, current)=>{
3210
+ if (!baseline) return;
3211
+ for (const [entryPath, record] of current)if (!baseline.has(entryPath)) baseline.set(entryPath, record);
3212
+ };
3213
+ seedFirstSeen(state.entryHashes, entryHashes);
3214
+ seedFirstSeen(state.setupHashes, setupHashes);
3215
+ const outcome = applyWatchInvalidation(state, {
3216
+ entryHashes,
3217
+ setupHashes
3218
+ });
3219
+ if (outcome.rerunAll) {
3220
+ logger.debug('[Watch] Setup file changed, re-running all test files of the project');
3221
+ return Array.from(entryTestFiles);
3222
+ }
3223
+ for (const affected of outcome.affectedPaths)logger.debug(`[Watch] Chunk hash changed for test: ${affected}`);
3224
+ return outcome.affectedPaths;
3224
3225
  };
3225
3226
  const getBrowserProjects = (context)=>context.projects.filter((project)=>project.normalizedConfig.browser.enabled);
3226
3227
  const getBrowserRsbuildEnvironmentConfig = (project)=>({
@@ -3426,27 +3427,22 @@ const destroyBrowserRuntime = async (runtime)=>{
3426
3427
  const cleanupWatchRuntime = ()=>{
3427
3428
  if (watchContext.cleanupPromise) return watchContext.cleanupPromise;
3428
3429
  watchContext.cleanupPromise = (async ()=>{
3429
- watchContext.closeCliShortcuts?.();
3430
- watchContext.closeCliShortcuts = null;
3431
3430
  if (!watchContext.runtime) return;
3432
3431
  await destroyBrowserRuntime(watchContext.runtime);
3433
3432
  watchContext.runtime = null;
3434
3433
  })();
3435
3434
  return watchContext.cleanupPromise;
3436
3435
  };
3437
- const registerWatchCleanup = ()=>{
3436
+ const registerWatchCleanup = (embedded)=>{
3438
3437
  if (watchContext.cleanupRegistered) return;
3439
- for (const signal of [
3440
- 'SIGINT',
3441
- 'SIGTERM',
3442
- 'SIGTSTP'
3443
- ])process.once(signal, ()=>{
3438
+ watchContext.cleanupRegistered = true;
3439
+ if (embedded) return;
3440
+ for (const signal of FATAL_SIGNALS)process.once(signal, ()=>{
3444
3441
  cleanupWatchRuntime();
3445
3442
  });
3446
3443
  process.once('exit', ()=>{
3447
3444
  cleanupWatchRuntime();
3448
3445
  });
3449
- watchContext.cleanupRegistered = true;
3450
3446
  };
3451
3447
  const createBrowserRuntime = async ({ context, projectEntries: initialProjectEntries, browserProjects, shardedEntries, freezeShardedEntries, tempDir, isWatchMode, onTriggerRerun, containerDistPath, containerDevServer, forceHeadless, skipProviderLaunch, appliedModifyRstestConfigEnvironments })=>{
3452
3448
  const containerHtmlTemplate = containerDistPath ? await promises.readFile(join(containerDistPath, 'index.html'), 'utf-8') : null;
@@ -3459,6 +3455,7 @@ const createBrowserRuntime = async ({ context, projectEntries: initialProjectEnt
3459
3455
  };
3460
3456
  let browserLaunchOptions = ensureConsistentBrowserLaunchOptions(browserProjects);
3461
3457
  let projectEntries = initialProjectEntries;
3458
+ const watchState = createBrowserWatchState();
3462
3459
  const manifestModules = [];
3463
3460
  const createRuntimeWithoutProvider = ()=>{
3464
3461
  const firstProject = browserProjects[0];
@@ -3481,7 +3478,8 @@ const createBrowserRuntime = async ({ context, projectEntries: initialProjectEnt
3481
3478
  setContainerOptions,
3482
3479
  dispatchHandlers,
3483
3480
  wss: void 0,
3484
- projectEntries
3481
+ projectEntries,
3482
+ watchState
3485
3483
  };
3486
3484
  };
3487
3485
  const getProjectEntry = (project)=>projectEntries.find((item)=>item.project.environmentName === project.environmentName);
@@ -3551,6 +3549,30 @@ const createBrowserRuntime = async ({ context, projectEntries: initialProjectEnt
3551
3549
  return false;
3552
3550
  }
3553
3551
  };
3552
+ const serveContainerRoute = async (req, res, next)=>{
3553
+ if (!req.url) return void next();
3554
+ const url = new URL(req.url, 'http://localhost');
3555
+ if ('/' === url.pathname) {
3556
+ if (await respondWithDevServerHtml(url, res)) return;
3557
+ const html = injectedContainerHtml || containerHtmlTemplate?.replace(OPTIONS_PLACEHOLDER, 'null');
3558
+ if (html) {
3559
+ res.setHeader('Content-Type', 'text/html');
3560
+ res.end(html);
3561
+ return;
3562
+ }
3563
+ res.statusCode = 502;
3564
+ res.end('Container UI is not available.');
3565
+ return;
3566
+ }
3567
+ if (url.pathname.startsWith('/container-static/')) {
3568
+ if (await proxyDevServerAsset(req, res)) return;
3569
+ if (serveContainer) return void serveContainer(req, res, next);
3570
+ res.statusCode = 502;
3571
+ res.end('Container assets are not available.');
3572
+ return;
3573
+ }
3574
+ next();
3575
+ };
3554
3576
  const buildProjectServer = async (project, isContainerServer)=>{
3555
3577
  const manifestPath = join(tempDir, toSafeVarName(project.environmentName), VIRTUAL_MANIFEST_FILENAME);
3556
3578
  const entry = getProjectEntry(project);
@@ -3587,7 +3609,10 @@ const createBrowserRuntime = async ({ context, projectEntries: initialProjectEnt
3587
3609
  server: {
3588
3610
  printUrls: false,
3589
3611
  port: project.normalizedConfig.browser.port ?? (isContainerServer ? 4000 : 0),
3590
- strictPort: project.normalizedConfig.browser.strictPort
3612
+ strictPort: project.normalizedConfig.browser.strictPort,
3613
+ setup: isContainerServer ? ({ server })=>{
3614
+ server.middlewares.use(serveContainerRoute);
3615
+ } : void 0
3591
3616
  },
3592
3617
  dev: createBrowserRsbuildDevConfig(enableHmr),
3593
3618
  environments: {
@@ -3644,9 +3669,19 @@ const createBrowserRuntime = async ({ context, projectEntries: initialProjectEnt
3644
3669
  target: 'web',
3645
3670
  sourceMap: {
3646
3671
  js: 'source-map'
3672
+ },
3673
+ distPath: {
3674
+ root: join(tempDir, 'server', toSafeVarName(project.environmentName))
3647
3675
  }
3648
3676
  },
3649
3677
  tools: {
3678
+ swc: (swcConfig)=>{
3679
+ swcConfig.env ??= {};
3680
+ swcConfig.env.exclude = Array.from(new Set([
3681
+ ...swcConfig.env.exclude ?? [],
3682
+ 'transform-parameters'
3683
+ ]));
3684
+ },
3650
3685
  rspack: (rspackConfig)=>{
3651
3686
  rspackConfig.mode = 'development';
3652
3687
  applyWebMockRspackConfig(rspackConfig, {
@@ -3685,21 +3720,46 @@ const createBrowserRuntime = async ({ context, projectEntries: initialProjectEnt
3685
3720
  name: 'rstest:browser-watch',
3686
3721
  setup (api) {
3687
3722
  api.onBeforeDevCompile(()=>{
3688
- if (!watchContext.hooksEnabled) return;
3723
+ watchState.compileStartTimes.set(project.name, Date.now());
3724
+ if (!watchState.hooksEnabled) return;
3689
3725
  logger.log(color.cyan('\nFile changed, re-running tests...\n'));
3690
3726
  });
3691
3727
  api.onAfterDevCompile(async ({ stats })=>{
3728
+ const compileStart = watchState.compileStartTimes.get(project.name);
3729
+ if (void 0 !== compileStart) {
3730
+ watchState.compileStartTimes.delete(project.name);
3731
+ if (watchState.hooksEnabled) watchState.pendingBuildTimeMs = Math.max(watchState.pendingBuildTimeMs, Date.now() - compileStart);
3732
+ }
3692
3733
  if (stats) {
3693
- const allProjectEntries = await collectProjectEntries(context);
3694
- const entryTestFiles = new Set(collectWatchTestFiles(allProjectEntries).map((file)=>file.testPath));
3734
+ const [projectEntry] = await collectProjectEntries(context, [
3735
+ project
3736
+ ]);
3737
+ const entryTestFiles = new Set(collectWatchTestFiles(projectEntry ? [
3738
+ projectEntry
3739
+ ] : []).map((file)=>file.testPath));
3740
+ const setupFiles = new Set((projectEntry?.setupFiles ?? []).map((file)=>normalize(file)));
3741
+ let state = watchState.invalidation.get(project.name);
3742
+ if (!state) {
3743
+ state = {};
3744
+ watchState.invalidation.set(project.name, state);
3745
+ }
3695
3746
  const statsJson = stats.toJson({
3696
3747
  all: true
3697
3748
  });
3698
- const affected = getAffectedTestFiles(statsJson.chunks, entryTestFiles);
3699
- watchContext.affectedTestFiles = affected;
3700
- if (affected.length > 0) logger.debug(`[Watch] Affected test files: ${affected.join(', ')}`);
3749
+ const affected = getAffectedTestFiles({
3750
+ chunks: statsJson.chunks,
3751
+ entryTestFiles,
3752
+ setupFiles,
3753
+ state
3754
+ });
3755
+ if (affected.length > 0) {
3756
+ const pending = watchState.pendingAffectedTestFiles.get(project.name) ?? new Set();
3757
+ for (const file of affected)pending.add(file);
3758
+ watchState.pendingAffectedTestFiles.set(project.name, pending);
3759
+ logger.debug(`[Watch] Affected test files: ${affected.join(', ')}`);
3760
+ }
3701
3761
  }
3702
- if (!watchContext.hooksEnabled) return;
3762
+ if (!watchState.hooksEnabled) return;
3703
3763
  await onTriggerRerun();
3704
3764
  });
3705
3765
  }
@@ -3732,6 +3792,7 @@ const createBrowserRuntime = async ({ context, projectEntries: initialProjectEnt
3732
3792
  });
3733
3793
  if (isDebug()) await rsbuildInstance.inspectConfig({
3734
3794
  writeToDisk: true,
3795
+ outputPath: external_pathe_resolve(context.rootPath, context.normalizedConfig.output.distPath.root, '.rsbuild'),
3735
3796
  extraConfigs: {
3736
3797
  rstest: {
3737
3798
  ...context.normalizedConfig,
@@ -3766,27 +3827,6 @@ const createBrowserRuntime = async ({ context, projectEntries: initialProjectEnt
3766
3827
  }
3767
3828
  return;
3768
3829
  }
3769
- if (isContainerServer) {
3770
- if ('/' === url.pathname) {
3771
- if (await respondWithDevServerHtml(url, res)) return;
3772
- const html = injectedContainerHtml || containerHtmlTemplate?.replace(OPTIONS_PLACEHOLDER, 'null');
3773
- if (html) {
3774
- res.setHeader('Content-Type', 'text/html');
3775
- res.end(html);
3776
- return;
3777
- }
3778
- res.statusCode = 502;
3779
- res.end('Container UI is not available.');
3780
- return;
3781
- }
3782
- if (url.pathname.startsWith('/container-static/')) {
3783
- if (await proxyDevServerAsset(req, res)) return;
3784
- if (serveContainer) return void serveContainer(req, res, next);
3785
- res.statusCode = 502;
3786
- res.end('Container assets are not available.');
3787
- return;
3788
- }
3789
- }
3790
3830
  if ('/runner.html' === url.pathname) {
3791
3831
  res.setHeader('Content-Type', 'text/html');
3792
3832
  res.end(htmlTemplate);
@@ -3844,7 +3884,8 @@ const createBrowserRuntime = async ({ context, projectEntries: initialProjectEnt
3844
3884
  setContainerOptions,
3845
3885
  dispatchHandlers,
3846
3886
  wss,
3847
- projectEntries
3887
+ projectEntries,
3888
+ watchState
3848
3889
  };
3849
3890
  } catch (error) {
3850
3891
  wss.close();
@@ -3871,10 +3912,11 @@ async function resolveProjectEntries(context, shardedEntries, browserProjects) {
3871
3912
  return collectProjectEntries(context, browserProjects);
3872
3913
  }
3873
3914
  const runBrowserController = async (context, options)=>{
3874
- const { allowEmptyWatchRun = false, allowEmptyRun = false, filesOnly = false, onTraceEvents, env } = options ?? {};
3915
+ const { allowEmptyRun = false, filesOnly = false, onTraceEvents, env } = options ?? {};
3875
3916
  const buildStart = Date.now();
3876
3917
  const isWatchMode = 'watch' === context.command;
3877
3918
  const phaseTrackers = onTraceEvents ? new Map() : void 0;
3919
+ const trackerKey = (project, testPath)=>`${project}\u0000${testPath}`;
3878
3920
  const browserProjects = options?.projects ?? getBrowserProjects(context);
3879
3921
  const useHeadlessDirect = browserProjects.every((project)=>project.normalizedConfig.browser.headless);
3880
3922
  const browserSourceMapCache = new Map();
@@ -3954,27 +3996,15 @@ const runBrowserController = async (context, options)=>{
3954
3996
  };
3955
3997
  const coverageConfig = browserProjects.find((project)=>project.normalizedConfig.coverage?.enabled)?.normalizedConfig.coverage;
3956
3998
  const coverageProvider = coverageConfig?.enabled ? await createCoverageProvider(coverageConfig, context.rootPath) : null;
3957
- const notifyTestRunEnd = async ({ duration, unhandledErrors, filterRerunTestPaths })=>{
3999
+ const notifyTestRunEnd = async ({ duration, coverage })=>{
3958
4000
  if (!isWatchMode) return;
3959
- let mergedCoverage;
3960
- if (coverageProvider) {
3961
- const coverageMap = coverageProvider.createCoverageMap();
3962
- let hasCoverage = false;
3963
- for (const result of context.reporterResults.results)if (result.coverage) {
3964
- coverageMap.merge(result.coverage);
3965
- hasCoverage = true;
3966
- }
3967
- if (hasCoverage) mergedCoverage = coverageMap.toJSON();
3968
- }
3969
4001
  for (const reporter of context.reporters)await reporter.onTestRunEnd?.({
3970
4002
  results: context.reporterResults.results,
3971
- coverage: mergedCoverage,
4003
+ coverage,
3972
4004
  testResults: context.reporterResults.testResults,
3973
4005
  duration,
3974
4006
  snapshotSummary: context.snapshotManager.summary,
3975
- getSourcemap: getBrowserSourcemap,
3976
- unhandledErrors,
3977
- filterRerunTestPaths
4007
+ getSourcemap: getBrowserSourcemap
3978
4008
  });
3979
4009
  };
3980
4010
  const containerDevServerEnv = process.env.RSTEST_CONTAINER_DEV_SERVER;
@@ -3997,7 +4027,6 @@ const runBrowserController = async (context, options)=>{
3997
4027
  }
3998
4028
  let projectEntries = await resolveProjectEntries(context, options?.shardedEntries, browserProjects);
3999
4029
  let totalTests = projectEntries.reduce((total, item)=>total + item.testFiles.length, 0);
4000
- const shouldKeepWatchingWithEmptySet = isWatchMode && allowEmptyWatchRun;
4001
4030
  const shouldInitializeEmptyBrowserHooks = 0 === totalTests && hasUserRstestConfigPlugins(browserProjects);
4002
4031
  const createEmptyRunResult = ()=>{
4003
4032
  const elapsed = Math.max(0, Date.now() - buildStart);
@@ -4017,7 +4046,7 @@ const runBrowserController = async (context, options)=>{
4017
4046
  const reportEmptyTestSet = ()=>{
4018
4047
  const code = context.normalizedConfig.passWithNoTests ? 0 : 1;
4019
4048
  if (isWatchMode || !allowEmptyRun) {
4020
- const message = shouldKeepWatchingWithEmptySet ? 'No test files found.' : getNoTestFilesMessage({
4049
+ const message = getNoTestFilesMessage({
4021
4050
  context,
4022
4051
  code,
4023
4052
  defaultMessage: `No test files found, exiting with code ${code}.`
@@ -4027,19 +4056,19 @@ const runBrowserController = async (context, options)=>{
4027
4056
  if (context.relatedFilters?.length) logger.log(color.gray('related: '), context.relatedFilters.join(color.gray(', ')));
4028
4057
  else if (context.fileFilters?.length) logger.log(color.gray('filter: '), context.fileFilters.join(color.gray(', ')));
4029
4058
  }
4030
- if (isWatchMode && 0 !== code && !shouldKeepWatchingWithEmptySet && !allowEmptyRun) ensureProcessExitCode(code);
4031
- return !shouldKeepWatchingWithEmptySet;
4059
+ if (isWatchMode && 0 !== code && !allowEmptyRun) ensureProcessExitCode(code);
4032
4060
  };
4033
4061
  if (0 === totalTests && !shouldInitializeEmptyBrowserHooks) {
4034
- if (reportEmptyTestSet()) return allowEmptyRun ? createEmptyRunResult() : void 0;
4062
+ reportEmptyTestSet();
4063
+ return allowEmptyRun ? createEmptyRunResult() : void 0;
4035
4064
  }
4036
4065
  if (!filesOnly) await notifyTestRunStart();
4037
- const enableCliShortcuts = isWatchMode && isBrowserWatchCliShortcutsEnabled();
4066
+ const enableCliShortcuts = isWatchMode && isTTY('stdin');
4038
4067
  const browserTempOutputRoot = context.normalizedConfig.output.distPath.root;
4039
4068
  const tempDir = isWatchMode && watchContext.runtime ? watchContext.runtime.tempDir : isWatchMode ? join(context.rootPath, browserTempOutputRoot, 'browser', 'watch') : join(context.rootPath, browserTempOutputRoot, 'browser', Date.now().toString());
4040
- if (isWatchMode) watchContext.lastTestFiles = collectWatchTestFiles(projectEntries);
4041
4069
  let runtime = isWatchMode ? watchContext.runtime : null;
4042
4070
  let triggerRerun;
4071
+ let awaitHeadlessRerunIdle;
4043
4072
  if (!runtime) {
4044
4073
  try {
4045
4074
  runtime = await createBrowserRuntime({
@@ -4068,12 +4097,62 @@ const runBrowserController = async (context, options)=>{
4068
4097
  }
4069
4098
  if (isWatchMode) {
4070
4099
  watchContext.runtime = runtime;
4071
- registerWatchCleanup();
4072
- if (enableCliShortcuts && !watchContext.closeCliShortcuts) watchContext.closeCliShortcuts = await setupBrowserWatchCliShortcuts({
4073
- close: cleanupWatchRuntime
4074
- });
4100
+ registerWatchCleanup(context.embedded);
4075
4101
  }
4076
4102
  }
4103
+ const watchState = runtime.watchState;
4104
+ if (isWatchMode) watchState.lastTestFiles = collectWatchTestFiles(projectEntries);
4105
+ const seedPendingRerun = (testPaths)=>{
4106
+ const wanted = testPaths ? new Set(testPaths.map((testPath)=>normalize(testPath))) : null;
4107
+ let seeded = 0;
4108
+ for (const file of watchState.lastTestFiles){
4109
+ if (wanted && !wanted.has(file.testPath)) continue;
4110
+ const pending = watchState.pendingAffectedTestFiles.get(file.projectName) ?? new Set();
4111
+ pending.add(file.testPath);
4112
+ watchState.pendingAffectedTestFiles.set(file.projectName, pending);
4113
+ seeded += 1;
4114
+ }
4115
+ return seeded;
4116
+ };
4117
+ const watchHandles = isWatchMode ? {
4118
+ rerun: async (testPaths)=>{
4119
+ const seeded = seedPendingRerun(testPaths);
4120
+ if (testPaths && 0 === seeded) return;
4121
+ await triggerRerun?.();
4122
+ await awaitHeadlessRerunIdle?.();
4123
+ },
4124
+ close: cleanupWatchRuntime
4125
+ } : void 0;
4126
+ const finalizeWatchRerun = async ({ rerunTestPaths, testTime, unhandledErrors })=>{
4127
+ const rerunPathSet = new Set(rerunTestPaths);
4128
+ const rerunResults = context.reporterResults.results.filter((result)=>rerunPathSet.has(result.testPath));
4129
+ let rerunCoverage;
4130
+ const coverageMap = buildBrowserCoverageMap(rerunResults, coverageProvider);
4131
+ if (coverageMap && coverageMap.files().length > 0) rerunCoverage = coverageMap.toJSON();
4132
+ const outcome = {
4133
+ results: rerunResults,
4134
+ testResults: context.reporterResults.testResults.filter((result)=>rerunPathSet.has(result.testPath)),
4135
+ errors: unhandledErrors ?? [],
4136
+ testPaths: rerunTestPaths,
4137
+ duration: {
4138
+ buildTime: drainPendingBuildTime(watchState),
4139
+ testTime
4140
+ },
4141
+ coverage: rerunCoverage ? {
4142
+ map: rerunCoverage
4143
+ } : void 0,
4144
+ resolveSourcemap: resolveBrowserSourcemap
4145
+ };
4146
+ await finalizeRunCycle(context, {
4147
+ outcomes: [
4148
+ outcome
4149
+ ],
4150
+ mode: 'on-demand',
4151
+ isWatchMode: true,
4152
+ coverageProvider,
4153
+ reportOnFailure: coverageConfig?.reportOnFailure ?? false
4154
+ });
4155
+ };
4077
4156
  projectEntries = runtime.projectEntries;
4078
4157
  totalTests = projectEntries.reduce((total, item)=>total + item.testFiles.length, 0);
4079
4158
  const buildTime = Date.now() - buildStart;
@@ -4090,7 +4169,8 @@ const runBrowserController = async (context, options)=>{
4090
4169
  resolveSourcemap: resolveBrowserSourcemap,
4091
4170
  close: ()=>destroyBrowserRuntime(runtime)
4092
4171
  };
4093
- if (0 === totalTests && reportEmptyTestSet()) {
4172
+ if (0 === totalTests) {
4173
+ reportEmptyTestSet();
4094
4174
  await destroyBrowserRuntime(runtime);
4095
4175
  return allowEmptyRun ? createEmptyRunResult() : void 0;
4096
4176
  }
@@ -4120,7 +4200,7 @@ const runBrowserController = async (context, options)=>{
4120
4200
  rootPath: normalize(context.rootPath),
4121
4201
  projects: projectRuntimeConfigs,
4122
4202
  snapshot: {
4123
- updateSnapshot: context.snapshotManager.options.updateSnapshot
4203
+ updateSnapshot: options?.updateSnapshot ?? context.snapshotManager.options.updateSnapshot
4124
4204
  },
4125
4205
  runnerUrl: `http://localhost:${runtime.containerServer.port}`,
4126
4206
  projectRunnerUrls,
@@ -4174,19 +4254,17 @@ const runBrowserController = async (context, options)=>{
4174
4254
  project.name,
4175
4255
  createRunnerEventSink(context, project.normalizedConfig)
4176
4256
  ]));
4177
- const firstBrowserSink = runnerSinks.get(browserProjects[0].name);
4178
- const projectNameByTestPath = new Map();
4179
- const sinkForProjectName = (projectName)=>runnerSinks.get(projectName) ?? firstBrowserSink;
4180
- const sinkForTestPath = (testPath)=>{
4181
- const projectName = projectNameByTestPath.get(testPath);
4182
- return projectName ? sinkForProjectName(projectName) : firstBrowserSink;
4257
+ const sinkForProjectName = (projectName)=>{
4258
+ const sink = runnerSinks.get(projectName);
4259
+ if (!sink) throw new Error(`No runner event sink for project "${projectName}"`);
4260
+ return sink;
4183
4261
  };
4184
4262
  const silentConsoleController = createSilentConsoleController({
4185
4263
  runtimeConfig: {
4186
4264
  silent: context.normalizedConfig.silent,
4187
4265
  disableConsoleIntercept: context.normalizedConfig.disableConsoleIntercept
4188
4266
  },
4189
- emitInterceptedLog: (log)=>sinkForTestPath(log.testPath).onConsoleLog(log),
4267
+ emitInterceptedLog: (log)=>sinkForProjectName(log.project).onConsoleLog(log),
4190
4268
  writeOriginalLog: ()=>{}
4191
4269
  });
4192
4270
  const snapshotRpcMethods = {
@@ -4214,7 +4292,6 @@ const runBrowserController = async (context, options)=>{
4214
4292
  }
4215
4293
  };
4216
4294
  const handleTestFileStart = async (payload)=>{
4217
- projectNameByTestPath.set(payload.testPath, payload.projectName);
4218
4295
  if (phaseTrackers) {
4219
4296
  const tracker = new PhaseTracker({
4220
4297
  trace: {
@@ -4224,25 +4301,26 @@ const runBrowserController = async (context, options)=>{
4224
4301
  pid: nextBrowserFilePid++
4225
4302
  });
4226
4303
  tracker.transition('prepare');
4227
- phaseTrackers.set(payload.testPath, tracker);
4304
+ phaseTrackers.set(trackerKey(payload.projectName, payload.testPath), tracker);
4228
4305
  }
4229
4306
  await sinkForProjectName(payload.projectName).onTestFileStart({
4230
4307
  testId: getFileTaskId(payload.testPath),
4231
4308
  testPath: payload.testPath,
4309
+ project: payload.projectName,
4232
4310
  tests: []
4233
4311
  });
4234
4312
  };
4235
4313
  const handleTestFileReady = async (payload)=>{
4236
- phaseTrackers?.get(payload.testPath)?.transition('tests');
4237
- await sinkForTestPath(payload.testPath).onTestFileReady(payload);
4314
+ phaseTrackers?.get(trackerKey(payload.project, payload.testPath))?.transition('tests');
4315
+ await sinkForProjectName(payload.project).onTestFileReady(payload);
4238
4316
  };
4239
4317
  const handleTestSuiteStart = async (payload)=>{
4240
- phaseTrackers?.get(payload.testPath)?.recordSuiteStart(payload);
4241
- await sinkForTestPath(payload.testPath).onTestSuiteStart(payload);
4318
+ phaseTrackers?.get(trackerKey(payload.project, payload.testPath))?.recordSuiteStart(payload);
4319
+ await sinkForProjectName(payload.project).onTestSuiteStart(payload);
4242
4320
  };
4243
4321
  const handleTestSuiteResult = async (payload)=>{
4244
- phaseTrackers?.get(payload.testPath)?.recordSuiteResult(payload);
4245
- await sinkForTestPath(payload.testPath).onTestSuiteResult(payload);
4322
+ phaseTrackers?.get(trackerKey(payload.project, payload.testPath))?.recordSuiteResult(payload);
4323
+ await sinkForProjectName(payload.project).onTestSuiteResult(payload);
4246
4324
  if ('passed-only' === context.normalizedConfig.silent) silentConsoleController.flushBufferedLogsForTask({
4247
4325
  taskId: payload.testId,
4248
4326
  status: payload.status,
@@ -4252,13 +4330,13 @@ const runBrowserController = async (context, options)=>{
4252
4330
  });
4253
4331
  };
4254
4332
  const handleTestCaseStart = async (payload)=>{
4255
- phaseTrackers?.get(payload.testPath)?.recordCaseStart(payload);
4256
- sinkForTestPath(payload.testPath).onTestCaseStart(payload);
4333
+ phaseTrackers?.get(trackerKey(payload.project, payload.testPath))?.recordCaseStart(payload);
4334
+ sinkForProjectName(payload.project).onTestCaseStart(payload);
4257
4335
  };
4258
4336
  const handleTestCaseResult = async (payload)=>{
4259
4337
  caseResults.push(payload);
4260
- phaseTrackers?.get(payload.testPath)?.recordCaseResult(payload);
4261
- await sinkForTestPath(payload.testPath).onTestCaseResult(payload);
4338
+ phaseTrackers?.get(trackerKey(payload.project, payload.testPath))?.recordCaseResult(payload);
4339
+ await sinkForProjectName(payload.project).onTestCaseResult(payload);
4262
4340
  if ('passed-only' === context.normalizedConfig.silent) silentConsoleController.flushBufferedLogsForTask({
4263
4341
  taskId: payload.testId,
4264
4342
  status: payload.status,
@@ -4273,12 +4351,13 @@ const runBrowserController = async (context, options)=>{
4273
4351
  payload
4274
4352
  ], payload.results);
4275
4353
  if (phaseTrackers) {
4276
- const tracker = phaseTrackers.get(payload.testPath);
4354
+ const key = trackerKey(payload.project, payload.testPath);
4355
+ const tracker = phaseTrackers.get(key);
4277
4356
  if (tracker) {
4278
4357
  tracker.end();
4279
4358
  const events = tracker.getTraceEvents();
4280
4359
  if (events) onTraceEvents?.(events);
4281
- phaseTrackers.delete(payload.testPath);
4360
+ phaseTrackers.delete(key);
4282
4361
  }
4283
4362
  }
4284
4363
  if ('passed-only' === context.normalizedConfig.silent) silentConsoleController.flushBufferedLogsForTask({
@@ -4288,7 +4367,7 @@ const runBrowserController = async (context, options)=>{
4288
4367
  taskType: 'file',
4289
4368
  testPath: payload.testPath
4290
4369
  });
4291
- await sinkForTestPath(payload.testPath).onTestFileResult(payload);
4370
+ await sinkForProjectName(payload.project).onTestFileResult(payload);
4292
4371
  if (isWatchMode && 'fail' === payload.status) ensureProcessExitCode(1);
4293
4372
  };
4294
4373
  const handleLog = async (payload)=>{
@@ -4300,6 +4379,7 @@ const runBrowserController = async (context, options)=>{
4300
4379
  taskParentNames: payload.taskParentNames,
4301
4380
  taskType: payload.taskType,
4302
4381
  testPath: payload.testPath,
4382
+ project: payload.projectName,
4303
4383
  type: payload.type,
4304
4384
  trace: payload.trace
4305
4385
  };
@@ -4461,6 +4541,9 @@ const runBrowserController = async (context, options)=>{
4461
4541
  });
4462
4542
  const inlineOptions = {
4463
4543
  ...hostOptions,
4544
+ snapshot: {
4545
+ updateSnapshot: context.snapshotManager.options.updateSnapshot
4546
+ },
4464
4547
  testFile: file.testPath,
4465
4548
  runId: `${run.token}:${session.id}`
4466
4549
  };
@@ -4573,20 +4656,16 @@ const runBrowserController = async (context, options)=>{
4573
4656
  } finally{
4574
4657
  const testTime = Math.max(0, Date.now() - rerunStartTime);
4575
4658
  const rerunFatalError = fatalError && fatalError !== fatalErrorBeforeRun ? fatalError : void 0;
4576
- await notifyTestRunEnd({
4577
- duration: {
4578
- totalTime: testTime,
4579
- buildTime: 0,
4580
- testTime
4581
- },
4582
- filterRerunTestPaths: files.map((file)=>file.testPath),
4659
+ await finalizeWatchRerun({
4660
+ rerunTestPaths: files.map((file)=>file.testPath),
4661
+ testTime,
4583
4662
  unhandledErrors: rerunError ? [
4584
4663
  rerunError
4585
4664
  ] : rerunFatalError ? [
4586
4665
  rerunFatalError
4587
4666
  ] : void 0
4588
4667
  });
4589
- logBrowserWatchReadyMessage(enableCliShortcuts);
4668
+ logWatchReadyMessage(context, enableCliShortcuts);
4590
4669
  }
4591
4670
  },
4592
4671
  onError: async (error)=>{
@@ -4600,54 +4679,7 @@ const runBrowserController = async (context, options)=>{
4600
4679
  logger.debug(`[Headless] Interrupting active run token ${run.token} before scheduling latest rerun`);
4601
4680
  }
4602
4681
  });
4603
- if (0 === allTestFiles.length) {
4604
- const duration = {
4605
- totalTime: buildTime,
4606
- buildTime,
4607
- testTime: 0
4608
- };
4609
- const result = {
4610
- results: reporterResults,
4611
- testResults: caseResults,
4612
- duration,
4613
- hasFailure: false,
4614
- getSourcemap: getBrowserSourcemap,
4615
- resolveSourcemap: resolveBrowserSourcemap,
4616
- close: isWatchMode ? void 0 : async ()=>{
4617
- sessionRegistry.clear();
4618
- await destroyBrowserRuntime(runtime);
4619
- }
4620
- };
4621
- if (isWatchMode) await notifyTestRunEnd({
4622
- duration
4623
- });
4624
- if (isWatchMode) {
4625
- triggerRerun = async ()=>{
4626
- const newProjectEntries = await collectProjectEntries(context);
4627
- const rerunPlan = planWatchRerun({
4628
- projectEntries: newProjectEntries,
4629
- previousTestFiles: watchContext.lastTestFiles,
4630
- affectedTestFiles: watchContext.affectedTestFiles
4631
- });
4632
- watchContext.affectedTestFiles = [];
4633
- if (rerunPlan.filesChanged) {
4634
- watchContext.lastTestFiles = rerunPlan.currentTestFiles;
4635
- if (0 === rerunPlan.currentTestFiles.length) {
4636
- logger.log(color.cyan('No browser test files remain after update.\n'));
4637
- logBrowserWatchReadyMessage(enableCliShortcuts);
4638
- return;
4639
- }
4640
- logger.log(color.cyan(`Test file set changed, re-running ${rerunPlan.currentTestFiles.length} file(s)...\n`));
4641
- latestRerunScheduler.enqueueLatest(rerunPlan.currentTestFiles);
4642
- return;
4643
- }
4644
- logBrowserWatchReadyMessage(enableCliShortcuts);
4645
- };
4646
- watchContext.hooksEnabled = true;
4647
- logBrowserWatchReadyMessage(enableCliShortcuts);
4648
- }
4649
- return result;
4650
- }
4682
+ awaitHeadlessRerunIdle = ()=>latestRerunScheduler.whenIdle();
4651
4683
  const testStart = Date.now();
4652
4684
  await runFilesWithPool(allTestFiles);
4653
4685
  const testTime = Date.now() - testStart;
@@ -4655,31 +4687,30 @@ const runBrowserController = async (context, options)=>{
4655
4687
  const newProjectEntries = await collectProjectEntries(context);
4656
4688
  const rerunPlan = planWatchRerun({
4657
4689
  projectEntries: newProjectEntries,
4658
- previousTestFiles: watchContext.lastTestFiles,
4659
- affectedTestFiles: watchContext.affectedTestFiles
4690
+ previousTestFiles: watchState.lastTestFiles,
4691
+ affectedTestFiles: drainPendingAffectedTestFiles(watchState)
4660
4692
  });
4661
- watchContext.affectedTestFiles = [];
4662
4693
  if (rerunPlan.filesChanged) {
4663
- const deletedTestPaths = collectDeletedTestPaths(watchContext.lastTestFiles, rerunPlan.currentTestFiles);
4694
+ const deletedTestPaths = collectDeletedTestPaths(watchState.lastTestFiles, rerunPlan.currentTestFiles);
4664
4695
  if (deletedTestPaths.length > 0) context.updateReporterResultState([], [], deletedTestPaths);
4665
- watchContext.lastTestFiles = rerunPlan.currentTestFiles;
4696
+ watchState.lastTestFiles = rerunPlan.currentTestFiles;
4666
4697
  if (0 === rerunPlan.currentTestFiles.length) {
4667
4698
  await latestRerunScheduler.enqueueLatest([]);
4668
4699
  logger.log(color.cyan('No browser test files remain after update.\n'));
4669
- logBrowserWatchReadyMessage(enableCliShortcuts);
4700
+ logWatchReadyMessage(context, enableCliShortcuts);
4670
4701
  return;
4671
4702
  }
4672
4703
  logger.log(color.cyan(`Test file set changed, re-running ${rerunPlan.currentTestFiles.length} file(s)...\n`));
4673
- latestRerunScheduler.enqueueLatest(rerunPlan.currentTestFiles);
4704
+ await latestRerunScheduler.enqueueLatest(rerunPlan.currentTestFiles);
4674
4705
  return;
4675
4706
  }
4676
4707
  if (0 === rerunPlan.affectedTestFiles.length) {
4677
4708
  logger.log(color.cyan('No affected browser test files detected, skipping re-run.\n'));
4678
- logBrowserWatchReadyMessage(enableCliShortcuts);
4709
+ logWatchReadyMessage(context, enableCliShortcuts);
4679
4710
  return;
4680
4711
  }
4681
4712
  logger.log(color.cyan(`Re-running ${rerunPlan.affectedTestFiles.length} affected test file(s)...\n`));
4682
- latestRerunScheduler.enqueueLatest(rerunPlan.affectedTestFiles);
4713
+ await latestRerunScheduler.enqueueLatest(rerunPlan.affectedTestFiles);
4683
4714
  };
4684
4715
  const closeHeadlessRuntime = isWatchMode ? void 0 : async ()=>{
4685
4716
  sessionRegistry.clear();
@@ -4694,6 +4725,7 @@ const runBrowserController = async (context, options)=>{
4694
4725
  context.updateReporterResultState(reporterResults, caseResults);
4695
4726
  const isFailure = reporterResults.some((result)=>'fail' === result.status);
4696
4727
  if (isWatchMode && isFailure) ensureProcessExitCode(1);
4728
+ const cycleCoverageMap = isWatchMode && coverageProvider ? buildBrowserCoverageMap(reporterResults, coverageProvider) : void 0;
4697
4729
  const result = {
4698
4730
  results: reporterResults,
4699
4731
  testResults: caseResults,
@@ -4701,18 +4733,21 @@ const runBrowserController = async (context, options)=>{
4701
4733
  hasFailure: isFailure,
4702
4734
  getSourcemap: getBrowserSourcemap,
4703
4735
  resolveSourcemap: resolveBrowserSourcemap,
4704
- close: closeHeadlessRuntime
4736
+ close: closeHeadlessRuntime,
4737
+ coverage: cycleCoverageMap,
4738
+ watch: watchHandles
4705
4739
  };
4706
4740
  if (isWatchMode) try {
4707
4741
  await notifyTestRunEnd({
4708
- duration
4742
+ duration,
4743
+ coverage: cycleCoverageMap?.files().length ? cycleCoverageMap.toJSON() : void 0
4709
4744
  });
4710
4745
  } finally{
4711
4746
  await closeHeadlessRuntime?.();
4712
4747
  }
4713
4748
  if (isWatchMode && triggerRerun) {
4714
- watchContext.hooksEnabled = true;
4715
- logBrowserWatchReadyMessage(enableCliShortcuts);
4749
+ watchState.hooksEnabled = true;
4750
+ logWatchReadyMessage(context, enableCliShortcuts);
4716
4751
  }
4717
4752
  return result;
4718
4753
  }
@@ -4919,22 +4954,31 @@ const runBrowserController = async (context, options)=>{
4919
4954
  testTime = Date.now() - testStart;
4920
4955
  }
4921
4956
  if (isWatchMode) triggerRerun = async ()=>{
4922
- const newProjectEntries = await collectProjectEntries(context);
4957
+ const refreshedHostOptions = {
4958
+ ...hostOptions,
4959
+ snapshot: {
4960
+ updateSnapshot: context.snapshotManager.options.updateSnapshot
4961
+ }
4962
+ };
4963
+ runtime.setContainerOptions(refreshedHostOptions);
4964
+ const [, newProjectEntries] = await Promise.all([
4965
+ rpcManager.updateHostConfig(refreshedHostOptions),
4966
+ collectProjectEntries(context)
4967
+ ]);
4923
4968
  const rerunPlan = planWatchRerun({
4924
4969
  projectEntries: newProjectEntries,
4925
- previousTestFiles: watchContext.lastTestFiles,
4926
- affectedTestFiles: watchContext.affectedTestFiles
4970
+ previousTestFiles: watchState.lastTestFiles,
4971
+ affectedTestFiles: drainPendingAffectedTestFiles(watchState)
4927
4972
  });
4928
- watchContext.affectedTestFiles = [];
4929
4973
  if (rerunPlan.filesChanged) {
4930
- const deletedTestPaths = collectDeletedTestPaths(watchContext.lastTestFiles, rerunPlan.currentTestFiles);
4974
+ const deletedTestPaths = collectDeletedTestPaths(watchState.lastTestFiles, rerunPlan.currentTestFiles);
4931
4975
  if (deletedTestPaths.length > 0) context.updateReporterResultState([], [], deletedTestPaths);
4932
- watchContext.lastTestFiles = rerunPlan.currentTestFiles;
4976
+ watchState.lastTestFiles = rerunPlan.currentTestFiles;
4933
4977
  currentTestFiles = rerunPlan.currentTestFiles;
4934
4978
  await rpcManager.notifyTestFileUpdate(currentTestFiles);
4935
4979
  if (0 === currentTestFiles.length) {
4936
4980
  logger.log(color.cyan('No browser test files remain after update.\n'));
4937
- logBrowserWatchReadyMessage(enableCliShortcuts);
4981
+ logWatchReadyMessage(context, enableCliShortcuts);
4938
4982
  return;
4939
4983
  }
4940
4984
  await waitForRunnerFramesReady(currentTestFiles.map((file)=>file.testPath));
@@ -4954,25 +4998,21 @@ const runBrowserController = async (context, options)=>{
4954
4998
  } finally{
4955
4999
  const testTime = Math.max(0, Date.now() - rerunStartTime);
4956
5000
  const rerunFatalError = fatalError && fatalError !== fatalErrorBeforeRun ? fatalError : void 0;
4957
- await notifyTestRunEnd({
4958
- duration: {
4959
- totalTime: testTime,
4960
- buildTime: 0,
4961
- testTime
4962
- },
4963
- filterRerunTestPaths: rerunPlan.normalizedAffectedTestFiles,
5001
+ await finalizeWatchRerun({
5002
+ rerunTestPaths: rerunPlan.normalizedAffectedTestFiles,
5003
+ testTime,
4964
5004
  unhandledErrors: rerunError ? [
4965
5005
  rerunError
4966
5006
  ] : rerunFatalError ? [
4967
5007
  rerunFatalError
4968
5008
  ] : void 0
4969
5009
  });
4970
- logBrowserWatchReadyMessage(enableCliShortcuts);
5010
+ logWatchReadyMessage(context, enableCliShortcuts);
4971
5011
  }
4972
- } else if (rerunPlan.filesChanged) logBrowserWatchReadyMessage(enableCliShortcuts);
5012
+ } else if (rerunPlan.filesChanged) logWatchReadyMessage(context, enableCliShortcuts);
4973
5013
  else {
4974
5014
  logger.log(color.cyan('Tests will be re-executed automatically\n'));
4975
- logBrowserWatchReadyMessage(enableCliShortcuts);
5015
+ logWatchReadyMessage(context, enableCliShortcuts);
4976
5016
  }
4977
5017
  };
4978
5018
  const closeContainerRuntime = isWatchMode ? void 0 : async ()=>{
@@ -4993,6 +5033,7 @@ const runBrowserController = async (context, options)=>{
4993
5033
  context.updateReporterResultState(reporterResults, caseResults);
4994
5034
  const isFailure = reporterResults.some((result)=>'fail' === result.status);
4995
5035
  if (isWatchMode && isFailure) ensureProcessExitCode(1);
5036
+ const cycleCoverageMap = isWatchMode && coverageProvider ? buildBrowserCoverageMap(reporterResults, coverageProvider) : void 0;
4996
5037
  const result = {
4997
5038
  results: reporterResults,
4998
5039
  testResults: caseResults,
@@ -5000,18 +5041,21 @@ const runBrowserController = async (context, options)=>{
5000
5041
  hasFailure: isFailure,
5001
5042
  getSourcemap: getBrowserSourcemap,
5002
5043
  resolveSourcemap: resolveBrowserSourcemap,
5003
- close: closeContainerRuntime
5044
+ close: closeContainerRuntime,
5045
+ coverage: cycleCoverageMap,
5046
+ watch: watchHandles
5004
5047
  };
5005
5048
  if (isWatchMode) try {
5006
5049
  await notifyTestRunEnd({
5007
- duration
5050
+ duration,
5051
+ coverage: cycleCoverageMap?.files().length ? cycleCoverageMap.toJSON() : void 0
5008
5052
  });
5009
5053
  } finally{
5010
5054
  await closeContainerRuntime?.();
5011
5055
  }
5012
5056
  if (isWatchMode && triggerRerun) {
5013
- watchContext.hooksEnabled = true;
5014
- logBrowserWatchReadyMessage(enableCliShortcuts);
5057
+ watchState.hooksEnabled = true;
5058
+ logWatchReadyMessage(context, enableCliShortcuts);
5015
5059
  }
5016
5060
  return result;
5017
5061
  };
@@ -5204,7 +5248,7 @@ const emptyOutcome = ()=>({
5204
5248
  }
5205
5249
  });
5206
5250
  async function createBrowserExecutor(context, options) {
5207
- const { projects, coverageProvider, freezeShardedEntries, filesOnly, allowEmptyRun, appliedModifyRstestConfigEnvironments } = options;
5251
+ const { projects, coverageProvider, shardedEntries, freezeShardedEntries, filesOnly, allowEmptyRun, appliedModifyRstestConfigEnvironments } = options;
5208
5252
  let deferredClose;
5209
5253
  let inFlightCycle;
5210
5254
  const foldOutcome = (result)=>{
@@ -5232,12 +5276,13 @@ async function createBrowserExecutor(context, options) {
5232
5276
  async runCycle (opts) {
5233
5277
  const cycle = runBrowserController(context, {
5234
5278
  projects,
5235
- shardedEntries: opts.shardedEntries,
5279
+ shardedEntries,
5236
5280
  freezeShardedEntries,
5237
5281
  allowEmptyRun,
5238
5282
  appliedModifyRstestConfigEnvironments,
5239
5283
  onTraceEvents: opts.onTraceEvents,
5240
- env: opts.env
5284
+ env: opts.env,
5285
+ updateSnapshot: opts.updateSnapshot
5241
5286
  });
5242
5287
  inFlightCycle = cycle;
5243
5288
  try {
@@ -5248,10 +5293,10 @@ async function createBrowserExecutor(context, options) {
5248
5293
  inFlightCycle = void 0;
5249
5294
  }
5250
5295
  },
5251
- async collect (opts) {
5296
+ async collect () {
5252
5297
  const pending = listBrowserTests(context, {
5253
5298
  projects,
5254
- shardedEntries: opts.shardedEntries,
5299
+ shardedEntries,
5255
5300
  freezeShardedEntries,
5256
5301
  filesOnly,
5257
5302
  appliedModifyRstestConfigEnvironments