@rstest/browser 0.11.3 → 0.11.4

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);
@@ -3644,9 +3642,19 @@ const createBrowserRuntime = async ({ context, projectEntries: initialProjectEnt
3644
3642
  target: 'web',
3645
3643
  sourceMap: {
3646
3644
  js: 'source-map'
3645
+ },
3646
+ distPath: {
3647
+ root: join(tempDir, 'server', toSafeVarName(project.environmentName))
3647
3648
  }
3648
3649
  },
3649
3650
  tools: {
3651
+ swc: (swcConfig)=>{
3652
+ swcConfig.env ??= {};
3653
+ swcConfig.env.exclude = Array.from(new Set([
3654
+ ...swcConfig.env.exclude ?? [],
3655
+ 'transform-parameters'
3656
+ ]));
3657
+ },
3650
3658
  rspack: (rspackConfig)=>{
3651
3659
  rspackConfig.mode = 'development';
3652
3660
  applyWebMockRspackConfig(rspackConfig, {
@@ -3685,21 +3693,46 @@ const createBrowserRuntime = async ({ context, projectEntries: initialProjectEnt
3685
3693
  name: 'rstest:browser-watch',
3686
3694
  setup (api) {
3687
3695
  api.onBeforeDevCompile(()=>{
3688
- if (!watchContext.hooksEnabled) return;
3696
+ watchState.compileStartTimes.set(project.name, Date.now());
3697
+ if (!watchState.hooksEnabled) return;
3689
3698
  logger.log(color.cyan('\nFile changed, re-running tests...\n'));
3690
3699
  });
3691
3700
  api.onAfterDevCompile(async ({ stats })=>{
3701
+ const compileStart = watchState.compileStartTimes.get(project.name);
3702
+ if (void 0 !== compileStart) {
3703
+ watchState.compileStartTimes.delete(project.name);
3704
+ if (watchState.hooksEnabled) watchState.pendingBuildTimeMs = Math.max(watchState.pendingBuildTimeMs, Date.now() - compileStart);
3705
+ }
3692
3706
  if (stats) {
3693
- const allProjectEntries = await collectProjectEntries(context);
3694
- const entryTestFiles = new Set(collectWatchTestFiles(allProjectEntries).map((file)=>file.testPath));
3707
+ const [projectEntry] = await collectProjectEntries(context, [
3708
+ project
3709
+ ]);
3710
+ const entryTestFiles = new Set(collectWatchTestFiles(projectEntry ? [
3711
+ projectEntry
3712
+ ] : []).map((file)=>file.testPath));
3713
+ const setupFiles = new Set((projectEntry?.setupFiles ?? []).map((file)=>normalize(file)));
3714
+ let state = watchState.invalidation.get(project.name);
3715
+ if (!state) {
3716
+ state = {};
3717
+ watchState.invalidation.set(project.name, state);
3718
+ }
3695
3719
  const statsJson = stats.toJson({
3696
3720
  all: true
3697
3721
  });
3698
- const affected = getAffectedTestFiles(statsJson.chunks, entryTestFiles);
3699
- watchContext.affectedTestFiles = affected;
3700
- if (affected.length > 0) logger.debug(`[Watch] Affected test files: ${affected.join(', ')}`);
3722
+ const affected = getAffectedTestFiles({
3723
+ chunks: statsJson.chunks,
3724
+ entryTestFiles,
3725
+ setupFiles,
3726
+ state
3727
+ });
3728
+ if (affected.length > 0) {
3729
+ const pending = watchState.pendingAffectedTestFiles.get(project.name) ?? new Set();
3730
+ for (const file of affected)pending.add(file);
3731
+ watchState.pendingAffectedTestFiles.set(project.name, pending);
3732
+ logger.debug(`[Watch] Affected test files: ${affected.join(', ')}`);
3733
+ }
3701
3734
  }
3702
- if (!watchContext.hooksEnabled) return;
3735
+ if (!watchState.hooksEnabled) return;
3703
3736
  await onTriggerRerun();
3704
3737
  });
3705
3738
  }
@@ -3732,6 +3765,7 @@ const createBrowserRuntime = async ({ context, projectEntries: initialProjectEnt
3732
3765
  });
3733
3766
  if (isDebug()) await rsbuildInstance.inspectConfig({
3734
3767
  writeToDisk: true,
3768
+ outputPath: external_pathe_resolve(context.rootPath, context.normalizedConfig.output.distPath.root, '.rsbuild'),
3735
3769
  extraConfigs: {
3736
3770
  rstest: {
3737
3771
  ...context.normalizedConfig,
@@ -3844,7 +3878,8 @@ const createBrowserRuntime = async ({ context, projectEntries: initialProjectEnt
3844
3878
  setContainerOptions,
3845
3879
  dispatchHandlers,
3846
3880
  wss,
3847
- projectEntries
3881
+ projectEntries,
3882
+ watchState
3848
3883
  };
3849
3884
  } catch (error) {
3850
3885
  wss.close();
@@ -4034,12 +4069,12 @@ const runBrowserController = async (context, options)=>{
4034
4069
  if (reportEmptyTestSet()) return allowEmptyRun ? createEmptyRunResult() : void 0;
4035
4070
  }
4036
4071
  if (!filesOnly) await notifyTestRunStart();
4037
- const enableCliShortcuts = isWatchMode && isBrowserWatchCliShortcutsEnabled();
4072
+ const enableCliShortcuts = isWatchMode && isTTY('stdin');
4038
4073
  const browserTempOutputRoot = context.normalizedConfig.output.distPath.root;
4039
4074
  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
4075
  let runtime = isWatchMode ? watchContext.runtime : null;
4042
4076
  let triggerRerun;
4077
+ let awaitHeadlessRerunIdle;
4043
4078
  if (!runtime) {
4044
4079
  try {
4045
4080
  runtime = await createBrowserRuntime({
@@ -4068,12 +4103,63 @@ const runBrowserController = async (context, options)=>{
4068
4103
  }
4069
4104
  if (isWatchMode) {
4070
4105
  watchContext.runtime = runtime;
4071
- registerWatchCleanup();
4072
- if (enableCliShortcuts && !watchContext.closeCliShortcuts) watchContext.closeCliShortcuts = await setupBrowserWatchCliShortcuts({
4073
- close: cleanupWatchRuntime
4074
- });
4106
+ registerWatchCleanup(context.embedded);
4075
4107
  }
4076
4108
  }
4109
+ const watchState = runtime.watchState;
4110
+ if (isWatchMode) watchState.lastTestFiles = collectWatchTestFiles(projectEntries);
4111
+ const seedPendingRerun = (testPaths)=>{
4112
+ const wanted = testPaths ? new Set(testPaths.map((testPath)=>normalize(testPath))) : null;
4113
+ let seeded = 0;
4114
+ for (const file of watchState.lastTestFiles){
4115
+ if (wanted && !wanted.has(file.testPath)) continue;
4116
+ const pending = watchState.pendingAffectedTestFiles.get(file.projectName) ?? new Set();
4117
+ pending.add(file.testPath);
4118
+ watchState.pendingAffectedTestFiles.set(file.projectName, pending);
4119
+ seeded += 1;
4120
+ }
4121
+ return seeded;
4122
+ };
4123
+ const watchHandles = isWatchMode ? {
4124
+ rerun: async (testPaths)=>{
4125
+ const seeded = seedPendingRerun(testPaths);
4126
+ if (testPaths && 0 === seeded) return;
4127
+ await triggerRerun?.();
4128
+ await awaitHeadlessRerunIdle?.();
4129
+ },
4130
+ close: cleanupWatchRuntime
4131
+ } : void 0;
4132
+ const finalizeWatchRerun = async ({ rerunTestPaths, testTime, unhandledErrors })=>{
4133
+ const rerunPathSet = new Set(rerunTestPaths);
4134
+ let sessionCoverage;
4135
+ const coverageMap = buildBrowserCoverageMap(context.reporterResults.results, coverageProvider, {
4136
+ keepResultCoverage: true
4137
+ });
4138
+ if (coverageMap && coverageMap.files().length > 0) sessionCoverage = coverageMap.toJSON();
4139
+ const outcome = {
4140
+ results: context.reporterResults.results.filter((result)=>rerunPathSet.has(result.testPath)),
4141
+ testResults: context.reporterResults.testResults.filter((result)=>rerunPathSet.has(result.testPath)),
4142
+ errors: unhandledErrors ?? [],
4143
+ testPaths: rerunTestPaths,
4144
+ duration: {
4145
+ buildTime: drainPendingBuildTime(watchState),
4146
+ testTime
4147
+ },
4148
+ coverage: sessionCoverage ? {
4149
+ map: sessionCoverage
4150
+ } : void 0,
4151
+ resolveSourcemap: resolveBrowserSourcemap
4152
+ };
4153
+ await finalizeRunCycle(context, {
4154
+ outcomes: [
4155
+ outcome
4156
+ ],
4157
+ mode: 'on-demand',
4158
+ isWatchMode: true,
4159
+ coverageProvider,
4160
+ reportOnFailure: coverageConfig?.reportOnFailure ?? false
4161
+ });
4162
+ };
4077
4163
  projectEntries = runtime.projectEntries;
4078
4164
  totalTests = projectEntries.reduce((total, item)=>total + item.testFiles.length, 0);
4079
4165
  const buildTime = Date.now() - buildStart;
@@ -4120,7 +4206,7 @@ const runBrowserController = async (context, options)=>{
4120
4206
  rootPath: normalize(context.rootPath),
4121
4207
  projects: projectRuntimeConfigs,
4122
4208
  snapshot: {
4123
- updateSnapshot: context.snapshotManager.options.updateSnapshot
4209
+ updateSnapshot: options?.updateSnapshot ?? context.snapshotManager.options.updateSnapshot
4124
4210
  },
4125
4211
  runnerUrl: `http://localhost:${runtime.containerServer.port}`,
4126
4212
  projectRunnerUrls,
@@ -4461,6 +4547,9 @@ const runBrowserController = async (context, options)=>{
4461
4547
  });
4462
4548
  const inlineOptions = {
4463
4549
  ...hostOptions,
4550
+ snapshot: {
4551
+ updateSnapshot: context.snapshotManager.options.updateSnapshot
4552
+ },
4464
4553
  testFile: file.testPath,
4465
4554
  runId: `${run.token}:${session.id}`
4466
4555
  };
@@ -4573,20 +4662,16 @@ const runBrowserController = async (context, options)=>{
4573
4662
  } finally{
4574
4663
  const testTime = Math.max(0, Date.now() - rerunStartTime);
4575
4664
  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),
4665
+ await finalizeWatchRerun({
4666
+ rerunTestPaths: files.map((file)=>file.testPath),
4667
+ testTime,
4583
4668
  unhandledErrors: rerunError ? [
4584
4669
  rerunError
4585
4670
  ] : rerunFatalError ? [
4586
4671
  rerunFatalError
4587
4672
  ] : void 0
4588
4673
  });
4589
- logBrowserWatchReadyMessage(enableCliShortcuts);
4674
+ logWatchReadyMessage(context, enableCliShortcuts);
4590
4675
  }
4591
4676
  },
4592
4677
  onError: async (error)=>{
@@ -4600,6 +4685,7 @@ const runBrowserController = async (context, options)=>{
4600
4685
  logger.debug(`[Headless] Interrupting active run token ${run.token} before scheduling latest rerun`);
4601
4686
  }
4602
4687
  });
4688
+ awaitHeadlessRerunIdle = ()=>latestRerunScheduler.whenIdle();
4603
4689
  if (0 === allTestFiles.length) {
4604
4690
  const duration = {
4605
4691
  totalTime: buildTime,
@@ -4616,7 +4702,8 @@ const runBrowserController = async (context, options)=>{
4616
4702
  close: isWatchMode ? void 0 : async ()=>{
4617
4703
  sessionRegistry.clear();
4618
4704
  await destroyBrowserRuntime(runtime);
4619
- }
4705
+ },
4706
+ watch: watchHandles
4620
4707
  };
4621
4708
  if (isWatchMode) await notifyTestRunEnd({
4622
4709
  duration
@@ -4626,25 +4713,24 @@ const runBrowserController = async (context, options)=>{
4626
4713
  const newProjectEntries = await collectProjectEntries(context);
4627
4714
  const rerunPlan = planWatchRerun({
4628
4715
  projectEntries: newProjectEntries,
4629
- previousTestFiles: watchContext.lastTestFiles,
4630
- affectedTestFiles: watchContext.affectedTestFiles
4716
+ previousTestFiles: watchState.lastTestFiles,
4717
+ affectedTestFiles: drainPendingAffectedTestFiles(watchState)
4631
4718
  });
4632
- watchContext.affectedTestFiles = [];
4633
4719
  if (rerunPlan.filesChanged) {
4634
- watchContext.lastTestFiles = rerunPlan.currentTestFiles;
4720
+ watchState.lastTestFiles = rerunPlan.currentTestFiles;
4635
4721
  if (0 === rerunPlan.currentTestFiles.length) {
4636
4722
  logger.log(color.cyan('No browser test files remain after update.\n'));
4637
- logBrowserWatchReadyMessage(enableCliShortcuts);
4723
+ logWatchReadyMessage(context, enableCliShortcuts);
4638
4724
  return;
4639
4725
  }
4640
4726
  logger.log(color.cyan(`Test file set changed, re-running ${rerunPlan.currentTestFiles.length} file(s)...\n`));
4641
- latestRerunScheduler.enqueueLatest(rerunPlan.currentTestFiles);
4727
+ await latestRerunScheduler.enqueueLatest(rerunPlan.currentTestFiles);
4642
4728
  return;
4643
4729
  }
4644
- logBrowserWatchReadyMessage(enableCliShortcuts);
4730
+ logWatchReadyMessage(context, enableCliShortcuts);
4645
4731
  };
4646
- watchContext.hooksEnabled = true;
4647
- logBrowserWatchReadyMessage(enableCliShortcuts);
4732
+ watchState.hooksEnabled = true;
4733
+ logWatchReadyMessage(context, enableCliShortcuts);
4648
4734
  }
4649
4735
  return result;
4650
4736
  }
@@ -4655,31 +4741,30 @@ const runBrowserController = async (context, options)=>{
4655
4741
  const newProjectEntries = await collectProjectEntries(context);
4656
4742
  const rerunPlan = planWatchRerun({
4657
4743
  projectEntries: newProjectEntries,
4658
- previousTestFiles: watchContext.lastTestFiles,
4659
- affectedTestFiles: watchContext.affectedTestFiles
4744
+ previousTestFiles: watchState.lastTestFiles,
4745
+ affectedTestFiles: drainPendingAffectedTestFiles(watchState)
4660
4746
  });
4661
- watchContext.affectedTestFiles = [];
4662
4747
  if (rerunPlan.filesChanged) {
4663
- const deletedTestPaths = collectDeletedTestPaths(watchContext.lastTestFiles, rerunPlan.currentTestFiles);
4748
+ const deletedTestPaths = collectDeletedTestPaths(watchState.lastTestFiles, rerunPlan.currentTestFiles);
4664
4749
  if (deletedTestPaths.length > 0) context.updateReporterResultState([], [], deletedTestPaths);
4665
- watchContext.lastTestFiles = rerunPlan.currentTestFiles;
4750
+ watchState.lastTestFiles = rerunPlan.currentTestFiles;
4666
4751
  if (0 === rerunPlan.currentTestFiles.length) {
4667
4752
  await latestRerunScheduler.enqueueLatest([]);
4668
4753
  logger.log(color.cyan('No browser test files remain after update.\n'));
4669
- logBrowserWatchReadyMessage(enableCliShortcuts);
4754
+ logWatchReadyMessage(context, enableCliShortcuts);
4670
4755
  return;
4671
4756
  }
4672
4757
  logger.log(color.cyan(`Test file set changed, re-running ${rerunPlan.currentTestFiles.length} file(s)...\n`));
4673
- latestRerunScheduler.enqueueLatest(rerunPlan.currentTestFiles);
4758
+ await latestRerunScheduler.enqueueLatest(rerunPlan.currentTestFiles);
4674
4759
  return;
4675
4760
  }
4676
4761
  if (0 === rerunPlan.affectedTestFiles.length) {
4677
4762
  logger.log(color.cyan('No affected browser test files detected, skipping re-run.\n'));
4678
- logBrowserWatchReadyMessage(enableCliShortcuts);
4763
+ logWatchReadyMessage(context, enableCliShortcuts);
4679
4764
  return;
4680
4765
  }
4681
4766
  logger.log(color.cyan(`Re-running ${rerunPlan.affectedTestFiles.length} affected test file(s)...\n`));
4682
- latestRerunScheduler.enqueueLatest(rerunPlan.affectedTestFiles);
4767
+ await latestRerunScheduler.enqueueLatest(rerunPlan.affectedTestFiles);
4683
4768
  };
4684
4769
  const closeHeadlessRuntime = isWatchMode ? void 0 : async ()=>{
4685
4770
  sessionRegistry.clear();
@@ -4701,7 +4786,8 @@ const runBrowserController = async (context, options)=>{
4701
4786
  hasFailure: isFailure,
4702
4787
  getSourcemap: getBrowserSourcemap,
4703
4788
  resolveSourcemap: resolveBrowserSourcemap,
4704
- close: closeHeadlessRuntime
4789
+ close: closeHeadlessRuntime,
4790
+ watch: watchHandles
4705
4791
  };
4706
4792
  if (isWatchMode) try {
4707
4793
  await notifyTestRunEnd({
@@ -4711,8 +4797,8 @@ const runBrowserController = async (context, options)=>{
4711
4797
  await closeHeadlessRuntime?.();
4712
4798
  }
4713
4799
  if (isWatchMode && triggerRerun) {
4714
- watchContext.hooksEnabled = true;
4715
- logBrowserWatchReadyMessage(enableCliShortcuts);
4800
+ watchState.hooksEnabled = true;
4801
+ logWatchReadyMessage(context, enableCliShortcuts);
4716
4802
  }
4717
4803
  return result;
4718
4804
  }
@@ -4919,22 +5005,31 @@ const runBrowserController = async (context, options)=>{
4919
5005
  testTime = Date.now() - testStart;
4920
5006
  }
4921
5007
  if (isWatchMode) triggerRerun = async ()=>{
4922
- const newProjectEntries = await collectProjectEntries(context);
5008
+ const refreshedHostOptions = {
5009
+ ...hostOptions,
5010
+ snapshot: {
5011
+ updateSnapshot: context.snapshotManager.options.updateSnapshot
5012
+ }
5013
+ };
5014
+ runtime.setContainerOptions(refreshedHostOptions);
5015
+ const [, newProjectEntries] = await Promise.all([
5016
+ rpcManager.updateHostConfig(refreshedHostOptions),
5017
+ collectProjectEntries(context)
5018
+ ]);
4923
5019
  const rerunPlan = planWatchRerun({
4924
5020
  projectEntries: newProjectEntries,
4925
- previousTestFiles: watchContext.lastTestFiles,
4926
- affectedTestFiles: watchContext.affectedTestFiles
5021
+ previousTestFiles: watchState.lastTestFiles,
5022
+ affectedTestFiles: drainPendingAffectedTestFiles(watchState)
4927
5023
  });
4928
- watchContext.affectedTestFiles = [];
4929
5024
  if (rerunPlan.filesChanged) {
4930
- const deletedTestPaths = collectDeletedTestPaths(watchContext.lastTestFiles, rerunPlan.currentTestFiles);
5025
+ const deletedTestPaths = collectDeletedTestPaths(watchState.lastTestFiles, rerunPlan.currentTestFiles);
4931
5026
  if (deletedTestPaths.length > 0) context.updateReporterResultState([], [], deletedTestPaths);
4932
- watchContext.lastTestFiles = rerunPlan.currentTestFiles;
5027
+ watchState.lastTestFiles = rerunPlan.currentTestFiles;
4933
5028
  currentTestFiles = rerunPlan.currentTestFiles;
4934
5029
  await rpcManager.notifyTestFileUpdate(currentTestFiles);
4935
5030
  if (0 === currentTestFiles.length) {
4936
5031
  logger.log(color.cyan('No browser test files remain after update.\n'));
4937
- logBrowserWatchReadyMessage(enableCliShortcuts);
5032
+ logWatchReadyMessage(context, enableCliShortcuts);
4938
5033
  return;
4939
5034
  }
4940
5035
  await waitForRunnerFramesReady(currentTestFiles.map((file)=>file.testPath));
@@ -4954,25 +5049,21 @@ const runBrowserController = async (context, options)=>{
4954
5049
  } finally{
4955
5050
  const testTime = Math.max(0, Date.now() - rerunStartTime);
4956
5051
  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,
5052
+ await finalizeWatchRerun({
5053
+ rerunTestPaths: rerunPlan.normalizedAffectedTestFiles,
5054
+ testTime,
4964
5055
  unhandledErrors: rerunError ? [
4965
5056
  rerunError
4966
5057
  ] : rerunFatalError ? [
4967
5058
  rerunFatalError
4968
5059
  ] : void 0
4969
5060
  });
4970
- logBrowserWatchReadyMessage(enableCliShortcuts);
5061
+ logWatchReadyMessage(context, enableCliShortcuts);
4971
5062
  }
4972
- } else if (rerunPlan.filesChanged) logBrowserWatchReadyMessage(enableCliShortcuts);
5063
+ } else if (rerunPlan.filesChanged) logWatchReadyMessage(context, enableCliShortcuts);
4973
5064
  else {
4974
5065
  logger.log(color.cyan('Tests will be re-executed automatically\n'));
4975
- logBrowserWatchReadyMessage(enableCliShortcuts);
5066
+ logWatchReadyMessage(context, enableCliShortcuts);
4976
5067
  }
4977
5068
  };
4978
5069
  const closeContainerRuntime = isWatchMode ? void 0 : async ()=>{
@@ -5000,7 +5091,8 @@ const runBrowserController = async (context, options)=>{
5000
5091
  hasFailure: isFailure,
5001
5092
  getSourcemap: getBrowserSourcemap,
5002
5093
  resolveSourcemap: resolveBrowserSourcemap,
5003
- close: closeContainerRuntime
5094
+ close: closeContainerRuntime,
5095
+ watch: watchHandles
5004
5096
  };
5005
5097
  if (isWatchMode) try {
5006
5098
  await notifyTestRunEnd({
@@ -5010,8 +5102,8 @@ const runBrowserController = async (context, options)=>{
5010
5102
  await closeContainerRuntime?.();
5011
5103
  }
5012
5104
  if (isWatchMode && triggerRerun) {
5013
- watchContext.hooksEnabled = true;
5014
- logBrowserWatchReadyMessage(enableCliShortcuts);
5105
+ watchState.hooksEnabled = true;
5106
+ logWatchReadyMessage(context, enableCliShortcuts);
5015
5107
  }
5016
5108
  return result;
5017
5109
  };
@@ -5204,7 +5296,7 @@ const emptyOutcome = ()=>({
5204
5296
  }
5205
5297
  });
5206
5298
  async function createBrowserExecutor(context, options) {
5207
- const { projects, coverageProvider, freezeShardedEntries, filesOnly, allowEmptyRun, appliedModifyRstestConfigEnvironments } = options;
5299
+ const { projects, coverageProvider, shardedEntries, freezeShardedEntries, filesOnly, allowEmptyRun, appliedModifyRstestConfigEnvironments } = options;
5208
5300
  let deferredClose;
5209
5301
  let inFlightCycle;
5210
5302
  const foldOutcome = (result)=>{
@@ -5232,12 +5324,13 @@ async function createBrowserExecutor(context, options) {
5232
5324
  async runCycle (opts) {
5233
5325
  const cycle = runBrowserController(context, {
5234
5326
  projects,
5235
- shardedEntries: opts.shardedEntries,
5327
+ shardedEntries,
5236
5328
  freezeShardedEntries,
5237
5329
  allowEmptyRun,
5238
5330
  appliedModifyRstestConfigEnvironments,
5239
5331
  onTraceEvents: opts.onTraceEvents,
5240
- env: opts.env
5332
+ env: opts.env,
5333
+ updateSnapshot: opts.updateSnapshot
5241
5334
  });
5242
5335
  inFlightCycle = cycle;
5243
5336
  try {
@@ -5248,10 +5341,10 @@ async function createBrowserExecutor(context, options) {
5248
5341
  inFlightCycle = void 0;
5249
5342
  }
5250
5343
  },
5251
- async collect (opts) {
5344
+ async collect () {
5252
5345
  const pending = listBrowserTests(context, {
5253
5346
  projects,
5254
- shardedEntries: opts.shardedEntries,
5347
+ shardedEntries,
5255
5348
  freezeShardedEntries,
5256
5349
  filesOnly,
5257
5350
  appliedModifyRstestConfigEnvironments