@rstest/browser 0.11.2 → 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, browserIgnoredRuntimeConfigKeys, buildBrowserCoverageMap, color, createCoverageProvider, createRunnerEventSink, createSilentConsoleController, getNoTestFilesMessage, getNumCpus as browser_getNumCpus, getSetupFiles, getTestEntries, hasUserRstestConfigPlugins, initModifyRstestConfigHooks, isDebug, isTTY, loadCoverageProvider, logger, 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);
3208
+ }
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);
3221
3222
  }
3222
- watchContext.chunkHashes = currentHashes;
3223
- return Array.from(affectedFiles);
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)=>({
@@ -3332,12 +3333,29 @@ const generateManifestModule = ({ manifestPath, entries, isWatchMode })=>{
3332
3333
  if (isWatchMode) {
3333
3334
  const includeRegExp = globPatternsToRegExp(project.normalizedConfig.include);
3334
3335
  const excludeRegExp = createBrowserContextExcludeRegExp(project.normalizedConfig.exclude.patterns, projectRootPosix);
3335
- lines.push(`const ${varName} = import.meta.webpackContext(${JSON.stringify(projectRootPosix)}, {`);
3336
- lines.push(' recursive: true,');
3337
- lines.push(` regExp: ${includeRegExp.toString()},`);
3338
- if (excludeRegExp) lines.push(` exclude: ${excludeRegExp.toString()},`);
3339
- lines.push(" mode: 'lazy',");
3340
- lines.push('});');
3336
+ const { includeSource } = project.normalizedConfig;
3337
+ const emitContext = (contextVarName, regExp)=>{
3338
+ lines.push(`const ${contextVarName} = import.meta.webpackContext(${JSON.stringify(projectRootPosix)}, {`);
3339
+ lines.push(' recursive: true,');
3340
+ lines.push(` regExp: ${regExp.toString()},`);
3341
+ if (excludeRegExp) lines.push(` exclude: ${excludeRegExp.toString()},`);
3342
+ lines.push(" mode: 'lazy',");
3343
+ lines.push('});');
3344
+ };
3345
+ if (0 === includeSource.length) emitContext(varName, includeRegExp);
3346
+ else {
3347
+ emitContext(`${varName}_include`, includeRegExp);
3348
+ emitContext(`${varName}_source`, globPatternsToRegExp(includeSource));
3349
+ const probedKeys = testFiles.map((filePath)=>toContextKey(filePath, projectRootPosix));
3350
+ lines.push(`const ${varName}_probed = ${JSON.stringify(probedKeys)};`);
3351
+ lines.push(`const ${varName}_includeKeys = new Set(${varName}_include.keys());`);
3352
+ lines.push(`const ${varName} = Object.assign(`);
3353
+ lines.push(` (key) => ${varName}_includeKeys.has(key) ? ${varName}_include(key) : ${varName}_source(key),`);
3354
+ lines.push(' {');
3355
+ lines.push(` keys: () => Array.from(new Set([...${varName}_includeKeys, ...${varName}_probed])),`);
3356
+ lines.push(' },');
3357
+ lines.push(');');
3358
+ }
3341
3359
  } else {
3342
3360
  lines.push(`const ${varName}_modules = {`);
3343
3361
  for (const filePath of testFiles){
@@ -3409,27 +3427,22 @@ const destroyBrowserRuntime = async (runtime)=>{
3409
3427
  const cleanupWatchRuntime = ()=>{
3410
3428
  if (watchContext.cleanupPromise) return watchContext.cleanupPromise;
3411
3429
  watchContext.cleanupPromise = (async ()=>{
3412
- watchContext.closeCliShortcuts?.();
3413
- watchContext.closeCliShortcuts = null;
3414
3430
  if (!watchContext.runtime) return;
3415
3431
  await destroyBrowserRuntime(watchContext.runtime);
3416
3432
  watchContext.runtime = null;
3417
3433
  })();
3418
3434
  return watchContext.cleanupPromise;
3419
3435
  };
3420
- const registerWatchCleanup = ()=>{
3436
+ const registerWatchCleanup = (embedded)=>{
3421
3437
  if (watchContext.cleanupRegistered) return;
3422
- for (const signal of [
3423
- 'SIGINT',
3424
- 'SIGTERM',
3425
- 'SIGTSTP'
3426
- ])process.once(signal, ()=>{
3438
+ watchContext.cleanupRegistered = true;
3439
+ if (embedded) return;
3440
+ for (const signal of FATAL_SIGNALS)process.once(signal, ()=>{
3427
3441
  cleanupWatchRuntime();
3428
3442
  });
3429
3443
  process.once('exit', ()=>{
3430
3444
  cleanupWatchRuntime();
3431
3445
  });
3432
- watchContext.cleanupRegistered = true;
3433
3446
  };
3434
3447
  const createBrowserRuntime = async ({ context, projectEntries: initialProjectEntries, browserProjects, shardedEntries, freezeShardedEntries, tempDir, isWatchMode, onTriggerRerun, containerDistPath, containerDevServer, forceHeadless, skipProviderLaunch, appliedModifyRstestConfigEnvironments })=>{
3435
3448
  const containerHtmlTemplate = containerDistPath ? await promises.readFile(join(containerDistPath, 'index.html'), 'utf-8') : null;
@@ -3442,6 +3455,7 @@ const createBrowserRuntime = async ({ context, projectEntries: initialProjectEnt
3442
3455
  };
3443
3456
  let browserLaunchOptions = ensureConsistentBrowserLaunchOptions(browserProjects);
3444
3457
  let projectEntries = initialProjectEntries;
3458
+ const watchState = createBrowserWatchState();
3445
3459
  const manifestModules = [];
3446
3460
  const createRuntimeWithoutProvider = ()=>{
3447
3461
  const firstProject = browserProjects[0];
@@ -3464,7 +3478,8 @@ const createBrowserRuntime = async ({ context, projectEntries: initialProjectEnt
3464
3478
  setContainerOptions,
3465
3479
  dispatchHandlers,
3466
3480
  wss: void 0,
3467
- projectEntries
3481
+ projectEntries,
3482
+ watchState
3468
3483
  };
3469
3484
  };
3470
3485
  const getProjectEntry = (project)=>projectEntries.find((item)=>item.project.environmentName === project.environmentName);
@@ -3493,7 +3508,6 @@ const createBrowserRuntime = async ({ context, projectEntries: initialProjectEnt
3493
3508
  };
3494
3509
  const browserRuntimePath = fileURLToPath(import.meta.resolve('@rstest/core/internal/browser-runtime'));
3495
3510
  const staticRstestAliases = {
3496
- '@rstest/core': resolveBrowserFile('client/public.ts'),
3497
3511
  '@rstest/browser': resolveBrowserFile('browser.ts'),
3498
3512
  '@rstest/core/internal/browser-runtime': browserRuntimePath
3499
3513
  };
@@ -3589,6 +3603,7 @@ const createBrowserRuntime = async ({ context, projectEntries: initialProjectEnt
3589
3603
  appliedEnvironmentNames: appliedModifyRstestConfigEnvironments
3590
3604
  });
3591
3605
  rsbuildInstance.addPlugins([
3606
+ pluginMockRuntime,
3592
3607
  {
3593
3608
  name: 'rstest:browser-user-config',
3594
3609
  setup (api) {
@@ -3619,18 +3634,33 @@ const createBrowserRuntime = async ({ context, projectEntries: initialProjectEnt
3619
3634
  source: {
3620
3635
  define: {
3621
3636
  'process.env': rstestEnvDefine,
3622
- 'import.meta.env': rstestEnvDefine
3637
+ 'import.meta.env': rstestEnvDefine,
3638
+ 'import.meta.rstest': importMetaRstestDefine('web')
3623
3639
  }
3624
3640
  },
3625
3641
  output: {
3626
3642
  target: 'web',
3627
3643
  sourceMap: {
3628
3644
  js: 'source-map'
3645
+ },
3646
+ distPath: {
3647
+ root: join(tempDir, 'server', toSafeVarName(project.environmentName))
3629
3648
  }
3630
3649
  },
3631
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
+ },
3632
3658
  rspack: (rspackConfig)=>{
3633
3659
  rspackConfig.mode = 'development';
3660
+ applyWebMockRspackConfig(rspackConfig, {
3661
+ rspack: rspack,
3662
+ rootPath: project.rootPath
3663
+ });
3634
3664
  rspackConfig.lazyCompilation = enableHmr ? createBrowserLazyCompilationConfig(setupFiles) : false;
3635
3665
  rspackConfig.plugins = rspackConfig.plugins || [];
3636
3666
  rspackConfig.plugins.push(virtualManifestPlugin);
@@ -3663,21 +3693,46 @@ const createBrowserRuntime = async ({ context, projectEntries: initialProjectEnt
3663
3693
  name: 'rstest:browser-watch',
3664
3694
  setup (api) {
3665
3695
  api.onBeforeDevCompile(()=>{
3666
- if (!watchContext.hooksEnabled) return;
3696
+ watchState.compileStartTimes.set(project.name, Date.now());
3697
+ if (!watchState.hooksEnabled) return;
3667
3698
  logger.log(color.cyan('\nFile changed, re-running tests...\n'));
3668
3699
  });
3669
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
+ }
3670
3706
  if (stats) {
3671
- const allProjectEntries = await collectProjectEntries(context);
3672
- 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
+ }
3673
3719
  const statsJson = stats.toJson({
3674
3720
  all: true
3675
3721
  });
3676
- const affected = getAffectedTestFiles(statsJson.chunks, entryTestFiles);
3677
- watchContext.affectedTestFiles = affected;
3678
- 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
+ }
3679
3734
  }
3680
- if (!watchContext.hooksEnabled) return;
3735
+ if (!watchState.hooksEnabled) return;
3681
3736
  await onTriggerRerun();
3682
3737
  });
3683
3738
  }
@@ -3710,6 +3765,7 @@ const createBrowserRuntime = async ({ context, projectEntries: initialProjectEnt
3710
3765
  });
3711
3766
  if (isDebug()) await rsbuildInstance.inspectConfig({
3712
3767
  writeToDisk: true,
3768
+ outputPath: external_pathe_resolve(context.rootPath, context.normalizedConfig.output.distPath.root, '.rsbuild'),
3713
3769
  extraConfigs: {
3714
3770
  rstest: {
3715
3771
  ...context.normalizedConfig,
@@ -3822,7 +3878,8 @@ const createBrowserRuntime = async ({ context, projectEntries: initialProjectEnt
3822
3878
  setContainerOptions,
3823
3879
  dispatchHandlers,
3824
3880
  wss,
3825
- projectEntries
3881
+ projectEntries,
3882
+ watchState
3826
3883
  };
3827
3884
  } catch (error) {
3828
3885
  wss.close();
@@ -4012,12 +4069,12 @@ const runBrowserController = async (context, options)=>{
4012
4069
  if (reportEmptyTestSet()) return allowEmptyRun ? createEmptyRunResult() : void 0;
4013
4070
  }
4014
4071
  if (!filesOnly) await notifyTestRunStart();
4015
- const enableCliShortcuts = isWatchMode && isBrowserWatchCliShortcutsEnabled();
4072
+ const enableCliShortcuts = isWatchMode && isTTY('stdin');
4016
4073
  const browserTempOutputRoot = context.normalizedConfig.output.distPath.root;
4017
4074
  const tempDir = isWatchMode && watchContext.runtime ? watchContext.runtime.tempDir : isWatchMode ? join(context.rootPath, browserTempOutputRoot, 'browser', 'watch') : join(context.rootPath, browserTempOutputRoot, 'browser', Date.now().toString());
4018
- if (isWatchMode) watchContext.lastTestFiles = collectWatchTestFiles(projectEntries);
4019
4075
  let runtime = isWatchMode ? watchContext.runtime : null;
4020
4076
  let triggerRerun;
4077
+ let awaitHeadlessRerunIdle;
4021
4078
  if (!runtime) {
4022
4079
  try {
4023
4080
  runtime = await createBrowserRuntime({
@@ -4046,12 +4103,63 @@ const runBrowserController = async (context, options)=>{
4046
4103
  }
4047
4104
  if (isWatchMode) {
4048
4105
  watchContext.runtime = runtime;
4049
- registerWatchCleanup();
4050
- if (enableCliShortcuts && !watchContext.closeCliShortcuts) watchContext.closeCliShortcuts = await setupBrowserWatchCliShortcuts({
4051
- close: cleanupWatchRuntime
4052
- });
4106
+ registerWatchCleanup(context.embedded);
4053
4107
  }
4054
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
+ };
4055
4163
  projectEntries = runtime.projectEntries;
4056
4164
  totalTests = projectEntries.reduce((total, item)=>total + item.testFiles.length, 0);
4057
4165
  const buildTime = Date.now() - buildStart;
@@ -4098,7 +4206,7 @@ const runBrowserController = async (context, options)=>{
4098
4206
  rootPath: normalize(context.rootPath),
4099
4207
  projects: projectRuntimeConfigs,
4100
4208
  snapshot: {
4101
- updateSnapshot: context.snapshotManager.options.updateSnapshot
4209
+ updateSnapshot: options?.updateSnapshot ?? context.snapshotManager.options.updateSnapshot
4102
4210
  },
4103
4211
  runnerUrl: `http://localhost:${runtime.containerServer.port}`,
4104
4212
  projectRunnerUrls,
@@ -4272,7 +4380,7 @@ const runBrowserController = async (context, options)=>{
4272
4380
  const handleLog = async (payload)=>{
4273
4381
  const log = {
4274
4382
  content: payload.content,
4275
- name: payload.level,
4383
+ name: getPrettyConsoleName(payload.level),
4276
4384
  taskId: payload.taskId,
4277
4385
  taskName: payload.taskName,
4278
4386
  taskParentNames: payload.taskParentNames,
@@ -4439,6 +4547,9 @@ const runBrowserController = async (context, options)=>{
4439
4547
  });
4440
4548
  const inlineOptions = {
4441
4549
  ...hostOptions,
4550
+ snapshot: {
4551
+ updateSnapshot: context.snapshotManager.options.updateSnapshot
4552
+ },
4442
4553
  testFile: file.testPath,
4443
4554
  runId: `${run.token}:${session.id}`
4444
4555
  };
@@ -4551,20 +4662,16 @@ const runBrowserController = async (context, options)=>{
4551
4662
  } finally{
4552
4663
  const testTime = Math.max(0, Date.now() - rerunStartTime);
4553
4664
  const rerunFatalError = fatalError && fatalError !== fatalErrorBeforeRun ? fatalError : void 0;
4554
- await notifyTestRunEnd({
4555
- duration: {
4556
- totalTime: testTime,
4557
- buildTime: 0,
4558
- testTime
4559
- },
4560
- filterRerunTestPaths: files.map((file)=>file.testPath),
4665
+ await finalizeWatchRerun({
4666
+ rerunTestPaths: files.map((file)=>file.testPath),
4667
+ testTime,
4561
4668
  unhandledErrors: rerunError ? [
4562
4669
  rerunError
4563
4670
  ] : rerunFatalError ? [
4564
4671
  rerunFatalError
4565
4672
  ] : void 0
4566
4673
  });
4567
- logBrowserWatchReadyMessage(enableCliShortcuts);
4674
+ logWatchReadyMessage(context, enableCliShortcuts);
4568
4675
  }
4569
4676
  },
4570
4677
  onError: async (error)=>{
@@ -4578,6 +4685,7 @@ const runBrowserController = async (context, options)=>{
4578
4685
  logger.debug(`[Headless] Interrupting active run token ${run.token} before scheduling latest rerun`);
4579
4686
  }
4580
4687
  });
4688
+ awaitHeadlessRerunIdle = ()=>latestRerunScheduler.whenIdle();
4581
4689
  if (0 === allTestFiles.length) {
4582
4690
  const duration = {
4583
4691
  totalTime: buildTime,
@@ -4594,7 +4702,8 @@ const runBrowserController = async (context, options)=>{
4594
4702
  close: isWatchMode ? void 0 : async ()=>{
4595
4703
  sessionRegistry.clear();
4596
4704
  await destroyBrowserRuntime(runtime);
4597
- }
4705
+ },
4706
+ watch: watchHandles
4598
4707
  };
4599
4708
  if (isWatchMode) await notifyTestRunEnd({
4600
4709
  duration
@@ -4604,25 +4713,24 @@ const runBrowserController = async (context, options)=>{
4604
4713
  const newProjectEntries = await collectProjectEntries(context);
4605
4714
  const rerunPlan = planWatchRerun({
4606
4715
  projectEntries: newProjectEntries,
4607
- previousTestFiles: watchContext.lastTestFiles,
4608
- affectedTestFiles: watchContext.affectedTestFiles
4716
+ previousTestFiles: watchState.lastTestFiles,
4717
+ affectedTestFiles: drainPendingAffectedTestFiles(watchState)
4609
4718
  });
4610
- watchContext.affectedTestFiles = [];
4611
4719
  if (rerunPlan.filesChanged) {
4612
- watchContext.lastTestFiles = rerunPlan.currentTestFiles;
4720
+ watchState.lastTestFiles = rerunPlan.currentTestFiles;
4613
4721
  if (0 === rerunPlan.currentTestFiles.length) {
4614
4722
  logger.log(color.cyan('No browser test files remain after update.\n'));
4615
- logBrowserWatchReadyMessage(enableCliShortcuts);
4723
+ logWatchReadyMessage(context, enableCliShortcuts);
4616
4724
  return;
4617
4725
  }
4618
4726
  logger.log(color.cyan(`Test file set changed, re-running ${rerunPlan.currentTestFiles.length} file(s)...\n`));
4619
- latestRerunScheduler.enqueueLatest(rerunPlan.currentTestFiles);
4727
+ await latestRerunScheduler.enqueueLatest(rerunPlan.currentTestFiles);
4620
4728
  return;
4621
4729
  }
4622
- logBrowserWatchReadyMessage(enableCliShortcuts);
4730
+ logWatchReadyMessage(context, enableCliShortcuts);
4623
4731
  };
4624
- watchContext.hooksEnabled = true;
4625
- logBrowserWatchReadyMessage(enableCliShortcuts);
4732
+ watchState.hooksEnabled = true;
4733
+ logWatchReadyMessage(context, enableCliShortcuts);
4626
4734
  }
4627
4735
  return result;
4628
4736
  }
@@ -4633,31 +4741,30 @@ const runBrowserController = async (context, options)=>{
4633
4741
  const newProjectEntries = await collectProjectEntries(context);
4634
4742
  const rerunPlan = planWatchRerun({
4635
4743
  projectEntries: newProjectEntries,
4636
- previousTestFiles: watchContext.lastTestFiles,
4637
- affectedTestFiles: watchContext.affectedTestFiles
4744
+ previousTestFiles: watchState.lastTestFiles,
4745
+ affectedTestFiles: drainPendingAffectedTestFiles(watchState)
4638
4746
  });
4639
- watchContext.affectedTestFiles = [];
4640
4747
  if (rerunPlan.filesChanged) {
4641
- const deletedTestPaths = collectDeletedTestPaths(watchContext.lastTestFiles, rerunPlan.currentTestFiles);
4748
+ const deletedTestPaths = collectDeletedTestPaths(watchState.lastTestFiles, rerunPlan.currentTestFiles);
4642
4749
  if (deletedTestPaths.length > 0) context.updateReporterResultState([], [], deletedTestPaths);
4643
- watchContext.lastTestFiles = rerunPlan.currentTestFiles;
4750
+ watchState.lastTestFiles = rerunPlan.currentTestFiles;
4644
4751
  if (0 === rerunPlan.currentTestFiles.length) {
4645
4752
  await latestRerunScheduler.enqueueLatest([]);
4646
4753
  logger.log(color.cyan('No browser test files remain after update.\n'));
4647
- logBrowserWatchReadyMessage(enableCliShortcuts);
4754
+ logWatchReadyMessage(context, enableCliShortcuts);
4648
4755
  return;
4649
4756
  }
4650
4757
  logger.log(color.cyan(`Test file set changed, re-running ${rerunPlan.currentTestFiles.length} file(s)...\n`));
4651
- latestRerunScheduler.enqueueLatest(rerunPlan.currentTestFiles);
4758
+ await latestRerunScheduler.enqueueLatest(rerunPlan.currentTestFiles);
4652
4759
  return;
4653
4760
  }
4654
4761
  if (0 === rerunPlan.affectedTestFiles.length) {
4655
4762
  logger.log(color.cyan('No affected browser test files detected, skipping re-run.\n'));
4656
- logBrowserWatchReadyMessage(enableCliShortcuts);
4763
+ logWatchReadyMessage(context, enableCliShortcuts);
4657
4764
  return;
4658
4765
  }
4659
4766
  logger.log(color.cyan(`Re-running ${rerunPlan.affectedTestFiles.length} affected test file(s)...\n`));
4660
- latestRerunScheduler.enqueueLatest(rerunPlan.affectedTestFiles);
4767
+ await latestRerunScheduler.enqueueLatest(rerunPlan.affectedTestFiles);
4661
4768
  };
4662
4769
  const closeHeadlessRuntime = isWatchMode ? void 0 : async ()=>{
4663
4770
  sessionRegistry.clear();
@@ -4679,7 +4786,8 @@ const runBrowserController = async (context, options)=>{
4679
4786
  hasFailure: isFailure,
4680
4787
  getSourcemap: getBrowserSourcemap,
4681
4788
  resolveSourcemap: resolveBrowserSourcemap,
4682
- close: closeHeadlessRuntime
4789
+ close: closeHeadlessRuntime,
4790
+ watch: watchHandles
4683
4791
  };
4684
4792
  if (isWatchMode) try {
4685
4793
  await notifyTestRunEnd({
@@ -4689,8 +4797,8 @@ const runBrowserController = async (context, options)=>{
4689
4797
  await closeHeadlessRuntime?.();
4690
4798
  }
4691
4799
  if (isWatchMode && triggerRerun) {
4692
- watchContext.hooksEnabled = true;
4693
- logBrowserWatchReadyMessage(enableCliShortcuts);
4800
+ watchState.hooksEnabled = true;
4801
+ logWatchReadyMessage(context, enableCliShortcuts);
4694
4802
  }
4695
4803
  return result;
4696
4804
  }
@@ -4897,22 +5005,31 @@ const runBrowserController = async (context, options)=>{
4897
5005
  testTime = Date.now() - testStart;
4898
5006
  }
4899
5007
  if (isWatchMode) triggerRerun = async ()=>{
4900
- 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
+ ]);
4901
5019
  const rerunPlan = planWatchRerun({
4902
5020
  projectEntries: newProjectEntries,
4903
- previousTestFiles: watchContext.lastTestFiles,
4904
- affectedTestFiles: watchContext.affectedTestFiles
5021
+ previousTestFiles: watchState.lastTestFiles,
5022
+ affectedTestFiles: drainPendingAffectedTestFiles(watchState)
4905
5023
  });
4906
- watchContext.affectedTestFiles = [];
4907
5024
  if (rerunPlan.filesChanged) {
4908
- const deletedTestPaths = collectDeletedTestPaths(watchContext.lastTestFiles, rerunPlan.currentTestFiles);
5025
+ const deletedTestPaths = collectDeletedTestPaths(watchState.lastTestFiles, rerunPlan.currentTestFiles);
4909
5026
  if (deletedTestPaths.length > 0) context.updateReporterResultState([], [], deletedTestPaths);
4910
- watchContext.lastTestFiles = rerunPlan.currentTestFiles;
5027
+ watchState.lastTestFiles = rerunPlan.currentTestFiles;
4911
5028
  currentTestFiles = rerunPlan.currentTestFiles;
4912
5029
  await rpcManager.notifyTestFileUpdate(currentTestFiles);
4913
5030
  if (0 === currentTestFiles.length) {
4914
5031
  logger.log(color.cyan('No browser test files remain after update.\n'));
4915
- logBrowserWatchReadyMessage(enableCliShortcuts);
5032
+ logWatchReadyMessage(context, enableCliShortcuts);
4916
5033
  return;
4917
5034
  }
4918
5035
  await waitForRunnerFramesReady(currentTestFiles.map((file)=>file.testPath));
@@ -4932,25 +5049,21 @@ const runBrowserController = async (context, options)=>{
4932
5049
  } finally{
4933
5050
  const testTime = Math.max(0, Date.now() - rerunStartTime);
4934
5051
  const rerunFatalError = fatalError && fatalError !== fatalErrorBeforeRun ? fatalError : void 0;
4935
- await notifyTestRunEnd({
4936
- duration: {
4937
- totalTime: testTime,
4938
- buildTime: 0,
4939
- testTime
4940
- },
4941
- filterRerunTestPaths: rerunPlan.normalizedAffectedTestFiles,
5052
+ await finalizeWatchRerun({
5053
+ rerunTestPaths: rerunPlan.normalizedAffectedTestFiles,
5054
+ testTime,
4942
5055
  unhandledErrors: rerunError ? [
4943
5056
  rerunError
4944
5057
  ] : rerunFatalError ? [
4945
5058
  rerunFatalError
4946
5059
  ] : void 0
4947
5060
  });
4948
- logBrowserWatchReadyMessage(enableCliShortcuts);
5061
+ logWatchReadyMessage(context, enableCliShortcuts);
4949
5062
  }
4950
- } else if (rerunPlan.filesChanged) logBrowserWatchReadyMessage(enableCliShortcuts);
5063
+ } else if (rerunPlan.filesChanged) logWatchReadyMessage(context, enableCliShortcuts);
4951
5064
  else {
4952
5065
  logger.log(color.cyan('Tests will be re-executed automatically\n'));
4953
- logBrowserWatchReadyMessage(enableCliShortcuts);
5066
+ logWatchReadyMessage(context, enableCliShortcuts);
4954
5067
  }
4955
5068
  };
4956
5069
  const closeContainerRuntime = isWatchMode ? void 0 : async ()=>{
@@ -4978,7 +5091,8 @@ const runBrowserController = async (context, options)=>{
4978
5091
  hasFailure: isFailure,
4979
5092
  getSourcemap: getBrowserSourcemap,
4980
5093
  resolveSourcemap: resolveBrowserSourcemap,
4981
- close: closeContainerRuntime
5094
+ close: closeContainerRuntime,
5095
+ watch: watchHandles
4982
5096
  };
4983
5097
  if (isWatchMode) try {
4984
5098
  await notifyTestRunEnd({
@@ -4988,8 +5102,8 @@ const runBrowserController = async (context, options)=>{
4988
5102
  await closeContainerRuntime?.();
4989
5103
  }
4990
5104
  if (isWatchMode && triggerRerun) {
4991
- watchContext.hooksEnabled = true;
4992
- logBrowserWatchReadyMessage(enableCliShortcuts);
5105
+ watchState.hooksEnabled = true;
5106
+ logWatchReadyMessage(context, enableCliShortcuts);
4993
5107
  }
4994
5108
  return result;
4995
5109
  };
@@ -5182,7 +5296,7 @@ const emptyOutcome = ()=>({
5182
5296
  }
5183
5297
  });
5184
5298
  async function createBrowserExecutor(context, options) {
5185
- const { projects, coverageProvider, freezeShardedEntries, filesOnly, allowEmptyRun, appliedModifyRstestConfigEnvironments } = options;
5299
+ const { projects, coverageProvider, shardedEntries, freezeShardedEntries, filesOnly, allowEmptyRun, appliedModifyRstestConfigEnvironments } = options;
5186
5300
  let deferredClose;
5187
5301
  let inFlightCycle;
5188
5302
  const foldOutcome = (result)=>{
@@ -5210,12 +5324,13 @@ async function createBrowserExecutor(context, options) {
5210
5324
  async runCycle (opts) {
5211
5325
  const cycle = runBrowserController(context, {
5212
5326
  projects,
5213
- shardedEntries: opts.shardedEntries,
5327
+ shardedEntries,
5214
5328
  freezeShardedEntries,
5215
5329
  allowEmptyRun,
5216
5330
  appliedModifyRstestConfigEnvironments,
5217
5331
  onTraceEvents: opts.onTraceEvents,
5218
- env: opts.env
5332
+ env: opts.env,
5333
+ updateSnapshot: opts.updateSnapshot
5219
5334
  });
5220
5335
  inFlightCycle = cycle;
5221
5336
  try {
@@ -5226,10 +5341,10 @@ async function createBrowserExecutor(context, options) {
5226
5341
  inFlightCycle = void 0;
5227
5342
  }
5228
5343
  },
5229
- async collect (opts) {
5344
+ async collect () {
5230
5345
  const pending = listBrowserTests(context, {
5231
5346
  projects,
5232
- shardedEntries: opts.shardedEntries,
5347
+ shardedEntries,
5233
5348
  freezeShardedEntries,
5234
5349
  filesOnly,
5235
5350
  appliedModifyRstestConfigEnvironments