@rstest/browser 0.11.1 → 0.11.3

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,10 +1,10 @@
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
2
  import { existsSync } from "node:fs";
2
3
  import promises from "node:fs/promises";
3
4
  import { fileURLToPath } from "node:url";
4
5
  import { isDeepStrictEqual } from "node:util";
5
- import { DEFAULT_TEST_TIMEOUT, PhaseTracker, RSTEST_ENV_SYMBOL_KEY, color, createCoverageProvider, getNoTestFilesMessage, getNumCpus, getSetupFiles, getTestEntries, isDebug, isTTY, loadCoverageProvider, logger, parseWorkers, resolveProjectBuildCache, rsbuild, serializableConfig } from "@rstest/core/internal/browser";
6
6
  import open_editor from "open-editor";
7
- import { basename, dirname, join, normalize, relative, resolve as external_pathe_resolve } from "pathe";
7
+ import { dirname, join, normalize, relative, resolve as external_pathe_resolve } from "pathe";
8
8
  import sirv from "sirv";
9
9
  import { WebSocketServer } from "ws";
10
10
  import convert_source_map from "convert-source-map";
@@ -1654,113 +1654,6 @@ __webpack_require__.add({
1654
1654
  };
1655
1655
  }
1656
1656
  });
1657
- const BROWSER_VIEWPORT_PRESET_DIMENSIONS = {
1658
- iPhoneSE: {
1659
- width: 375,
1660
- height: 667
1661
- },
1662
- iPhoneXR: {
1663
- width: 414,
1664
- height: 896
1665
- },
1666
- iPhone12Pro: {
1667
- width: 390,
1668
- height: 844
1669
- },
1670
- iPhone14ProMax: {
1671
- width: 430,
1672
- height: 932
1673
- },
1674
- Pixel7: {
1675
- width: 412,
1676
- height: 915
1677
- },
1678
- SamsungGalaxyS8Plus: {
1679
- width: 360,
1680
- height: 740
1681
- },
1682
- SamsungGalaxyS20Ultra: {
1683
- width: 412,
1684
- height: 915
1685
- },
1686
- iPadMini: {
1687
- width: 768,
1688
- height: 1024
1689
- },
1690
- iPadAir: {
1691
- width: 820,
1692
- height: 1180
1693
- },
1694
- iPadPro: {
1695
- width: 1024,
1696
- height: 1366
1697
- },
1698
- SurfacePro7: {
1699
- width: 912,
1700
- height: 1368
1701
- },
1702
- SurfaceDuo: {
1703
- width: 540,
1704
- height: 720
1705
- },
1706
- GalaxyZFold5: {
1707
- width: 344,
1708
- height: 882
1709
- },
1710
- AsusZenbookFold: {
1711
- width: 853,
1712
- height: 1280
1713
- },
1714
- SamsungGalaxyA51A71: {
1715
- width: 412,
1716
- height: 914
1717
- },
1718
- NestHub: {
1719
- width: 1024,
1720
- height: 600
1721
- },
1722
- NestHubMax: {
1723
- width: 1280,
1724
- height: 800
1725
- }
1726
- };
1727
- const resolveBrowserViewportPreset = (presetId)=>{
1728
- const size = BROWSER_VIEWPORT_PRESET_DIMENSIONS[presetId];
1729
- return size ?? null;
1730
- };
1731
- const SUPPORTED_PROVIDERS = [
1732
- 'playwright'
1733
- ];
1734
- const isPlainObject = (value)=>'[object Object]' === Object.prototype.toString.call(value);
1735
- const validateViewport = (viewport)=>{
1736
- if (null == viewport) return;
1737
- if ('string' == typeof viewport) {
1738
- const presetId = viewport.trim();
1739
- if (!presetId) throw new Error('browser.viewport must be a non-empty preset id.');
1740
- if (!resolveBrowserViewportPreset(presetId)) throw new Error(`browser.viewport must be a valid preset id. Received: ${viewport}`);
1741
- return;
1742
- }
1743
- if (isPlainObject(viewport)) {
1744
- const width = viewport.width;
1745
- const height = viewport.height;
1746
- if (!Number.isFinite(width) || width <= 0) throw new Error('browser.viewport.width must be a positive number.');
1747
- if (!Number.isFinite(height) || height <= 0) throw new Error('browser.viewport.height must be a positive number.');
1748
- return;
1749
- }
1750
- throw new Error('browser.viewport must be either a preset id or { width, height }.');
1751
- };
1752
- const validateBrowserConfig = (context)=>{
1753
- for (const project of context.projects){
1754
- const { browser, output } = project.normalizedConfig;
1755
- if (browser.enabled) {
1756
- if (!browser.provider) throw new Error('browser.provider is required when browser.enabled is true.');
1757
- if (!SUPPORTED_PROVIDERS.includes(browser.provider)) throw new Error(`browser.provider must be one of: ${SUPPORTED_PROVIDERS.join(', ')}.`);
1758
- validateViewport(browser.viewport);
1759
- if (!isPlainObject(browser.providerOptions)) throw new Error('browser.providerOptions must be a plain object.');
1760
- if (output?.bundleDependencies === false) throw new Error('output.bundleDependencies false is not supported in browser mode.');
1761
- }
1762
- }
1763
- };
1764
1657
  const TYPE_REQUEST = "q";
1765
1658
  const TYPE_RESPONSE = "s";
1766
1659
  function createPromiseWithResolvers() {
@@ -1957,16 +1850,22 @@ function createBirpc($functions, options) {
1957
1850
  return rpc;
1958
1851
  }
1959
1852
  const DEFAULT_MAX_HEADLESS_WORKERS = 12;
1960
- const resolveDefaultHeadlessWorkers = (command, numCpus = getNumCpus())=>{
1961
- const baseWorkers = Math.max(Math.min(DEFAULT_MAX_HEADLESS_WORKERS, numCpus - 1), 1);
1962
- return 'watch' === command ? Math.max(Math.floor(baseWorkers / 2), 1) : baseWorkers;
1963
- };
1964
- const getHeadlessConcurrency = (context, totalTests)=>{
1965
- if (totalTests <= 0) return 1;
1966
- const maxWorkers = context.normalizedConfig.pool.maxWorkers;
1967
- if (void 0 !== maxWorkers) return Math.min(parseWorkers(maxWorkers), totalTests);
1968
- return Math.min(resolveDefaultHeadlessWorkers(context.command), totalTests);
1853
+ const resolveHeadlessWorkerCount = ({ command, maxWorkers, totalTasks, numCpus = browser_getNumCpus() })=>{
1854
+ const base = Math.max(Math.min(DEFAULT_MAX_HEADLESS_WORKERS, numCpus - 1), 1);
1855
+ return resolveWorkerCount({
1856
+ command,
1857
+ maxWorkers,
1858
+ totalTasks,
1859
+ recommended: base,
1860
+ watchRecommended: Math.max(Math.floor(base / 2), 1),
1861
+ numCpus
1862
+ });
1969
1863
  };
1864
+ const getHeadlessConcurrency = (context, totalTests)=>resolveHeadlessWorkerCount({
1865
+ command: context.command,
1866
+ maxWorkers: context.normalizedConfig.pool.maxWorkers,
1867
+ totalTasks: totalTests
1868
+ });
1970
1869
  const toErrorMessage = (error)=>error instanceof Error ? error.message : String(error);
1971
1870
  class HostDispatchRouter {
1972
1871
  handlers = new Map();
@@ -2071,6 +1970,172 @@ const createHostDispatchRouter = ({ routerOptions, runnerCallbacks, runSnapshotR
2071
1970
  }
2072
1971
  return router;
2073
1972
  };
1973
+ const BROWSER_VIEWPORT_PRESET_DIMENSIONS = {
1974
+ iPhoneSE: {
1975
+ width: 375,
1976
+ height: 667
1977
+ },
1978
+ iPhoneXR: {
1979
+ width: 414,
1980
+ height: 896
1981
+ },
1982
+ iPhone12Pro: {
1983
+ width: 390,
1984
+ height: 844
1985
+ },
1986
+ iPhone14ProMax: {
1987
+ width: 430,
1988
+ height: 932
1989
+ },
1990
+ Pixel7: {
1991
+ width: 412,
1992
+ height: 915
1993
+ },
1994
+ SamsungGalaxyS8Plus: {
1995
+ width: 360,
1996
+ height: 740
1997
+ },
1998
+ SamsungGalaxyS20Ultra: {
1999
+ width: 412,
2000
+ height: 915
2001
+ },
2002
+ iPadMini: {
2003
+ width: 768,
2004
+ height: 1024
2005
+ },
2006
+ iPadAir: {
2007
+ width: 820,
2008
+ height: 1180
2009
+ },
2010
+ iPadPro: {
2011
+ width: 1024,
2012
+ height: 1366
2013
+ },
2014
+ SurfacePro7: {
2015
+ width: 912,
2016
+ height: 1368
2017
+ },
2018
+ SurfaceDuo: {
2019
+ width: 540,
2020
+ height: 720
2021
+ },
2022
+ GalaxyZFold5: {
2023
+ width: 344,
2024
+ height: 882
2025
+ },
2026
+ AsusZenbookFold: {
2027
+ width: 853,
2028
+ height: 1280
2029
+ },
2030
+ SamsungGalaxyA51A71: {
2031
+ width: 412,
2032
+ height: 914
2033
+ },
2034
+ NestHub: {
2035
+ width: 1024,
2036
+ height: 600
2037
+ },
2038
+ NestHubMax: {
2039
+ width: 1280,
2040
+ height: 800
2041
+ }
2042
+ };
2043
+ const resolveBrowserViewportPreset = (presetId)=>{
2044
+ const size = BROWSER_VIEWPORT_PRESET_DIMENSIONS[presetId];
2045
+ return size ?? null;
2046
+ };
2047
+ const ignoredKeyWarnings = {
2048
+ testEnvironment: {
2049
+ isNonDefault: (config)=>'node' !== config.testEnvironment.name,
2050
+ message: (config)=>`Ignoring testEnvironment '${config.testEnvironment.name}' in browser mode: the browser itself is the test environment.`
2051
+ },
2052
+ isolate: {
2053
+ isNonDefault: (config)=>false === config.isolate,
2054
+ message: ()=>"Ignoring isolate: false in browser mode: each test file still runs in a fresh context.",
2055
+ browserOnly: true
2056
+ },
2057
+ detectAsyncLeaks: {
2058
+ isNonDefault: (config)=>true === config.detectAsyncLeaks,
2059
+ message: ()=>"Ignoring detectAsyncLeaks in browser mode: it relies on node async_hooks."
2060
+ },
2061
+ logHeapUsage: {
2062
+ isNonDefault: (config)=>true === config.logHeapUsage,
2063
+ message: ()=>'Ignoring logHeapUsage in browser mode.'
2064
+ }
2065
+ };
2066
+ const speciallyHandledIgnoredKeys = [
2067
+ 'coverage'
2068
+ ];
2069
+ const browserValidatedIgnoredKeys = [
2070
+ ...Object.keys(ignoredKeyWarnings),
2071
+ ...speciallyHandledIgnoredKeys
2072
+ ];
2073
+ const assertIgnoredKeysCovered = ()=>{
2074
+ const covered = new Set(browserValidatedIgnoredKeys);
2075
+ const uncovered = browserIgnoredRuntimeConfigKeys.filter((key)=>!covered.has(key));
2076
+ if (uncovered.length > 0) throw new Error(`Browser config validation is out of sync with executorCapabilities: no check for ignored RuntimeConfig field(s): ${uncovered.join(', ')}. Add a descriptor to \`ignoredKeyWarnings\` or \`speciallyHandledIgnoredKeys\`.`);
2077
+ };
2078
+ assertIgnoredKeysCovered();
2079
+ const SUPPORTED_PROVIDERS = [
2080
+ 'playwright'
2081
+ ];
2082
+ const isPlainObject = (value)=>'[object Object]' === Object.prototype.toString.call(value);
2083
+ const validateViewport = (viewport)=>{
2084
+ if (null == viewport) return;
2085
+ if ('string' == typeof viewport) {
2086
+ const presetId = viewport.trim();
2087
+ if (!presetId) throw new Error('browser.viewport must be a non-empty preset id.');
2088
+ if (!resolveBrowserViewportPreset(presetId)) throw new Error(`browser.viewport must be a valid preset id. Received: ${viewport}`);
2089
+ return;
2090
+ }
2091
+ if (isPlainObject(viewport)) {
2092
+ const width = viewport.width;
2093
+ const height = viewport.height;
2094
+ if (!Number.isFinite(width) || width <= 0) throw new Error('browser.viewport.width must be a positive number.');
2095
+ if (!Number.isFinite(height) || height <= 0) throw new Error('browser.viewport.height must be a positive number.');
2096
+ return;
2097
+ }
2098
+ throw new Error('browser.viewport must be either a preset id or { width, height }.');
2099
+ };
2100
+ const reportUnsupportedBrowserOptions = (context)=>{
2101
+ const browserProjects = context.projects.filter((project)=>project.normalizedConfig.browser.enabled);
2102
+ if (0 === browserProjects.length) return;
2103
+ const isBrowserOnlyRun = browserProjects.length === context.projects.length;
2104
+ const globalConfig = context.normalizedConfig;
2105
+ const { coverage } = globalConfig;
2106
+ if ('list' !== context.command && coverage.enabled && 'v8' === coverage.provider) {
2107
+ if (isBrowserOnlyRun) throw new Error("Coverage provider 'v8' is not supported in browser mode: browser projects produce no v8 coverage. Use the default istanbul provider (coverage.provider: 'istanbul') for browser coverage.");
2108
+ logger.warn(color.yellow("Coverage provider 'v8' produces no coverage for browser project files; use the 'istanbul' provider to collect browser coverage. Node projects will still use v8."));
2109
+ }
2110
+ const warnings = new Set();
2111
+ if ('forks' !== globalConfig.pool.type) warnings.add(`Ignoring pool.type '${globalConfig.pool.type}' in browser mode.`);
2112
+ if (globalConfig.pool.execArgv && globalConfig.pool.execArgv.length > 0) warnings.add('Ignoring pool.execArgv in browser mode.');
2113
+ for (const project of browserProjects){
2114
+ const config = project.normalizedConfig;
2115
+ for (const key of browserIgnoredRuntimeConfigKeys){
2116
+ const descriptor = ignoredKeyWarnings[key];
2117
+ if (descriptor) {
2118
+ if (!descriptor.browserOnly || isBrowserOnlyRun) {
2119
+ if (descriptor.isNonDefault(config)) warnings.add(descriptor.message(config));
2120
+ }
2121
+ }
2122
+ }
2123
+ }
2124
+ for (const message of warnings)logger.warn(color.yellow(message));
2125
+ };
2126
+ const validateBrowserConfig = (context)=>{
2127
+ for (const project of context.projects){
2128
+ const { browser, output } = project.normalizedConfig;
2129
+ if (browser.enabled) {
2130
+ if (!browser.provider) throw new Error('browser.provider is required when browser.enabled is true.');
2131
+ if (!SUPPORTED_PROVIDERS.includes(browser.provider)) throw new Error(`browser.provider must be one of: ${SUPPORTED_PROVIDERS.join(', ')}.`);
2132
+ validateViewport(browser.viewport);
2133
+ if (!isPlainObject(browser.providerOptions)) throw new Error('browser.providerOptions must be a plain object.');
2134
+ if (output?.bundleDependencies === false) throw new Error('output.bundleDependencies false is not supported in browser mode.');
2135
+ }
2136
+ }
2137
+ reportUnsupportedBrowserOptions(context);
2138
+ };
2074
2139
  const createHeadedSerialTaskQueue = ()=>{
2075
2140
  let queue = Promise.resolve();
2076
2141
  const enqueue = (task)=>{
@@ -2846,7 +2911,6 @@ const getBrowserProviderOptions = (project)=>{
2846
2911
  return browserConfig.providerOptions ?? {};
2847
2912
  };
2848
2913
  const getFileTaskId = (testPath)=>`file:${testPath}`;
2849
- const getBufferedLogTaskId = (log)=>log.taskId ?? getFileTaskId(log.testPath);
2850
2914
  const createDeferredPromise = ()=>{
2851
2915
  let resolve;
2852
2916
  let reject;
@@ -3158,41 +3222,12 @@ const getAffectedTestFiles = (chunks, entryTestFiles)=>{
3158
3222
  watchContext.chunkHashes = currentHashes;
3159
3223
  return Array.from(affectedFiles);
3160
3224
  };
3161
- const getRuntimeConfigFromProject = (project)=>{
3162
- const { testNamePattern, testTimeout, passWithNoTests, retry, globals, clearMocks, resetMocks, restoreMocks, unstubEnvs, unstubGlobals, maxConcurrency, printConsoleTrace, disableConsoleIntercept, testEnvironment, hookTimeout, isolate, coverage, snapshotFormat, env, bail, logHeapUsage, detectAsyncLeaks, chaiConfig, includeTaskLocation, silent } = project.normalizedConfig;
3163
- return {
3164
- env: {
3165
- NODE_ENV: process.env.NODE_ENV,
3166
- RSTEST: 'true',
3167
- ...env
3168
- },
3169
- testNamePattern,
3170
- testTimeout,
3171
- hookTimeout,
3172
- passWithNoTests,
3173
- retry,
3174
- globals,
3175
- clearMocks,
3176
- resetMocks,
3177
- restoreMocks,
3178
- unstubEnvs,
3179
- unstubGlobals,
3180
- maxConcurrency,
3181
- printConsoleTrace,
3182
- disableConsoleIntercept,
3183
- testEnvironment,
3184
- isolate,
3185
- coverage,
3186
- snapshotFormat,
3187
- bail,
3188
- logHeapUsage,
3189
- detectAsyncLeaks,
3190
- chaiConfig,
3191
- includeTaskLocation,
3192
- silent
3193
- };
3194
- };
3195
3225
  const getBrowserProjects = (context)=>context.projects.filter((project)=>project.normalizedConfig.browser.enabled);
3226
+ const getBrowserRsbuildEnvironmentConfig = (project)=>({
3227
+ plugins: project.normalizedConfig.plugins,
3228
+ root: project.rootPath
3229
+ });
3230
+ const getMaxTestTimeoutForRpc = (projects)=>Math.max(...projects.map((p)=>p.normalizedConfig.testTimeout ?? DEFAULT_TEST_TIMEOUT));
3196
3231
  const getBrowserLaunchOptions = (project)=>({
3197
3232
  provider: project.normalizedConfig.browser.provider,
3198
3233
  browser: project.normalizedConfig.browser.browser,
@@ -3219,9 +3254,7 @@ const resolveProviderForTestPath = ({ testPath, browserProjects })=>{
3219
3254
  for (const project of sortedProjects)if (normalizedTestPath.startsWith(project.rootPath)) return project.provider;
3220
3255
  throw new Error(`Cannot resolve browser provider for test path: ${JSON.stringify(testPath)}. Known project roots: ${JSON.stringify(sortedProjects.map((p)=>p.rootPath))}`);
3221
3256
  };
3222
- const collectProjectEntries = async (context)=>{
3223
- const browserProjects = getBrowserProjects(context);
3224
- return Promise.all(browserProjects.map(async (project)=>{
3257
+ const collectProjectEntries = async (context, browserProjects = getBrowserProjects(context))=>Promise.all(browserProjects.map(async (project)=>{
3225
3258
  const { normalizedConfig: { include, exclude, includeSource, setupFiles } } = project;
3226
3259
  const tests = await getTestEntries({
3227
3260
  include,
@@ -3239,7 +3272,6 @@ const collectProjectEntries = async (context)=>{
3239
3272
  testFiles: Object.values(tests)
3240
3273
  };
3241
3274
  }));
3242
- };
3243
3275
  const resolveBrowserFile = (relativePath)=>{
3244
3276
  const candidates = [
3245
3277
  external_pathe_resolve(hostController_dirname, '../src', relativePath),
@@ -3300,12 +3332,29 @@ const generateManifestModule = ({ manifestPath, entries, isWatchMode })=>{
3300
3332
  if (isWatchMode) {
3301
3333
  const includeRegExp = globPatternsToRegExp(project.normalizedConfig.include);
3302
3334
  const excludeRegExp = createBrowserContextExcludeRegExp(project.normalizedConfig.exclude.patterns, projectRootPosix);
3303
- lines.push(`const ${varName} = import.meta.webpackContext(${JSON.stringify(projectRootPosix)}, {`);
3304
- lines.push(' recursive: true,');
3305
- lines.push(` regExp: ${includeRegExp.toString()},`);
3306
- if (excludeRegExp) lines.push(` exclude: ${excludeRegExp.toString()},`);
3307
- lines.push(" mode: 'lazy',");
3308
- lines.push('});');
3335
+ const { includeSource } = project.normalizedConfig;
3336
+ const emitContext = (contextVarName, regExp)=>{
3337
+ lines.push(`const ${contextVarName} = import.meta.webpackContext(${JSON.stringify(projectRootPosix)}, {`);
3338
+ lines.push(' recursive: true,');
3339
+ lines.push(` regExp: ${regExp.toString()},`);
3340
+ if (excludeRegExp) lines.push(` exclude: ${excludeRegExp.toString()},`);
3341
+ lines.push(" mode: 'lazy',");
3342
+ lines.push('});');
3343
+ };
3344
+ if (0 === includeSource.length) emitContext(varName, includeRegExp);
3345
+ else {
3346
+ emitContext(`${varName}_include`, includeRegExp);
3347
+ emitContext(`${varName}_source`, globPatternsToRegExp(includeSource));
3348
+ const probedKeys = testFiles.map((filePath)=>toContextKey(filePath, projectRootPosix));
3349
+ lines.push(`const ${varName}_probed = ${JSON.stringify(probedKeys)};`);
3350
+ lines.push(`const ${varName}_includeKeys = new Set(${varName}_include.keys());`);
3351
+ lines.push(`const ${varName} = Object.assign(`);
3352
+ lines.push(` (key) => ${varName}_includeKeys.has(key) ? ${varName}_include(key) : ${varName}_source(key),`);
3353
+ lines.push(' {');
3354
+ lines.push(` keys: () => Array.from(new Set([...${varName}_includeKeys, ...${varName}_probed])),`);
3355
+ lines.push(' },');
3356
+ lines.push(');');
3357
+ }
3309
3358
  } else {
3310
3359
  lines.push(`const ${varName}_modules = {`);
3311
3360
  for (const filePath of testFiles){
@@ -3399,7 +3448,7 @@ const registerWatchCleanup = ()=>{
3399
3448
  });
3400
3449
  watchContext.cleanupRegistered = true;
3401
3450
  };
3402
- const createBrowserRuntime = async ({ context, projectEntries, tempDir, isWatchMode, onTriggerRerun, containerDistPath, containerDevServer, forceHeadless })=>{
3451
+ const createBrowserRuntime = async ({ context, projectEntries: initialProjectEntries, browserProjects, shardedEntries, freezeShardedEntries, tempDir, isWatchMode, onTriggerRerun, containerDistPath, containerDevServer, forceHeadless, skipProviderLaunch, appliedModifyRstestConfigEnvironments })=>{
3403
3452
  const containerHtmlTemplate = containerDistPath ? await promises.readFile(join(containerDistPath, 'index.html'), 'utf-8') : null;
3404
3453
  let injectedContainerHtml = null;
3405
3454
  let serializedOptions = 'null';
@@ -3408,11 +3457,59 @@ const createBrowserRuntime = async ({ context, projectEntries, tempDir, isWatchM
3408
3457
  serializedOptions = serializeForInlineScript(options);
3409
3458
  if (containerHtmlTemplate) injectedContainerHtml = containerHtmlTemplate.replace(OPTIONS_PLACEHOLDER, serializedOptions);
3410
3459
  };
3411
- const browserProjects = getBrowserProjects(context);
3412
- const browserLaunchOptions = ensureConsistentBrowserLaunchOptions(browserProjects);
3460
+ let browserLaunchOptions = ensureConsistentBrowserLaunchOptions(browserProjects);
3461
+ let projectEntries = initialProjectEntries;
3462
+ const manifestModules = [];
3463
+ const createRuntimeWithoutProvider = ()=>{
3464
+ const firstProject = browserProjects[0];
3465
+ return {
3466
+ projectServers: new Map(),
3467
+ containerServer: {
3468
+ projectName: firstProject.name,
3469
+ environmentName: firstProject.environmentName,
3470
+ rsbuildInstance: void 0,
3471
+ devServer: {
3472
+ close: async ()=>void 0
3473
+ },
3474
+ port: 0,
3475
+ manifestPath: ''
3476
+ },
3477
+ browser: void 0,
3478
+ browserLaunchOptions,
3479
+ wsPort: 0,
3480
+ tempDir,
3481
+ setContainerOptions,
3482
+ dispatchHandlers,
3483
+ wss: void 0,
3484
+ projectEntries
3485
+ };
3486
+ };
3487
+ const getProjectEntry = (project)=>projectEntries.find((item)=>item.project.environmentName === project.environmentName);
3488
+ const refreshManifestModule = (manifestModule)=>{
3489
+ const entry = getProjectEntry(manifestModule.project);
3490
+ manifestModule.modules[manifestModule.manifestPath] = generateManifestModule({
3491
+ manifestPath: manifestModule.manifestPath,
3492
+ entries: [
3493
+ {
3494
+ project: manifestModule.project,
3495
+ testFiles: entry?.testFiles ?? [],
3496
+ setupFiles: entry?.setupFiles ?? []
3497
+ }
3498
+ ],
3499
+ isWatchMode
3500
+ });
3501
+ };
3502
+ const refreshProjectEntries = async ()=>{
3503
+ validateBrowserConfig(context);
3504
+ browserLaunchOptions = ensureConsistentBrowserLaunchOptions(browserProjects);
3505
+ const updatedShardedEntries = freezeShardedEntries ? shardedEntries : context.normalizedConfig.shard ? await resolveShardedEntries(context, {
3506
+ silent: true
3507
+ }) : shardedEntries;
3508
+ projectEntries = await resolveProjectEntries(context, updatedShardedEntries, browserProjects);
3509
+ for (const manifestModule of manifestModules)refreshManifestModule(manifestModule);
3510
+ };
3413
3511
  const browserRuntimePath = fileURLToPath(import.meta.resolve('@rstest/core/internal/browser-runtime'));
3414
3512
  const staticRstestAliases = {
3415
- '@rstest/core': resolveBrowserFile('client/public.ts'),
3416
3513
  '@rstest/browser': resolveBrowserFile('browser.ts'),
3417
3514
  '@rstest/core/internal/browser-runtime': browserRuntimePath
3418
3515
  };
@@ -3454,26 +3551,27 @@ const createBrowserRuntime = async ({ context, projectEntries, tempDir, isWatchM
3454
3551
  return false;
3455
3552
  }
3456
3553
  };
3457
- const entryByEnvironmentName = new Map(projectEntries.map((entry)=>[
3458
- entry.project.environmentName,
3459
- entry
3460
- ]));
3461
3554
  const buildProjectServer = async (project, isContainerServer)=>{
3462
3555
  const manifestPath = join(tempDir, toSafeVarName(project.environmentName), VIRTUAL_MANIFEST_FILENAME);
3463
- const entry = entryByEnvironmentName.get(project.environmentName);
3464
- const manifestSource = generateManifestModule({
3556
+ const entry = getProjectEntry(project);
3557
+ const virtualManifestModules = {
3558
+ [manifestPath]: generateManifestModule({
3559
+ manifestPath,
3560
+ entries: [
3561
+ {
3562
+ project,
3563
+ testFiles: entry?.testFiles ?? [],
3564
+ setupFiles: entry?.setupFiles ?? []
3565
+ }
3566
+ ],
3567
+ isWatchMode
3568
+ })
3569
+ };
3570
+ const virtualManifestPlugin = new rspack.experiments.VirtualModulesPlugin(virtualManifestModules);
3571
+ manifestModules.push({
3465
3572
  manifestPath,
3466
- entries: [
3467
- {
3468
- project,
3469
- testFiles: entry?.testFiles ?? [],
3470
- setupFiles: entry?.setupFiles ?? []
3471
- }
3472
- ],
3473
- isWatchMode
3474
- });
3475
- const virtualManifestPlugin = new rspack.experiments.VirtualModulesPlugin({
3476
- [manifestPath]: manifestSource
3573
+ project,
3574
+ modules: virtualManifestModules
3477
3575
  });
3478
3576
  const rstestInternalAliases = {
3479
3577
  '@rstest/browser-manifest': manifestPath,
@@ -3482,11 +3580,10 @@ const createBrowserRuntime = async ({ context, projectEntries, tempDir, isWatchM
3482
3580
  const isHeadless = forceHeadless || project.normalizedConfig.browser.headless;
3483
3581
  const enableHmr = shouldEnableBrowserHmr(isWatchMode, isHeadless);
3484
3582
  const rsbuildInstance = await createRsbuild({
3485
- callerName: 'rstest-browser',
3583
+ callerName: 'rstest',
3486
3584
  rsbuildConfig: {
3487
3585
  root: context.rootPath,
3488
3586
  mode: 'development',
3489
- plugins: project.normalizedConfig.plugins || [],
3490
3587
  server: {
3491
3588
  printUrls: false,
3492
3589
  port: project.normalizedConfig.browser.port ?? (isContainerServer ? 4000 : 0),
@@ -3494,11 +3591,21 @@ const createBrowserRuntime = async ({ context, projectEntries, tempDir, isWatchM
3494
3591
  },
3495
3592
  dev: createBrowserRsbuildDevConfig(enableHmr),
3496
3593
  environments: {
3497
- [project.environmentName]: {}
3594
+ [project.environmentName]: getBrowserRsbuildEnvironmentConfig(project)
3498
3595
  }
3499
3596
  }
3500
3597
  });
3598
+ initModifyRstestConfigHooks(context, rsbuildInstance, [
3599
+ project
3600
+ ], [
3601
+ project
3602
+ ], {
3603
+ getEnvironmentConfig: getBrowserRsbuildEnvironmentConfig,
3604
+ onModifyRstestConfigApplied: refreshProjectEntries,
3605
+ appliedEnvironmentNames: appliedModifyRstestConfigEnvironments
3606
+ });
3501
3607
  rsbuildInstance.addPlugins([
3608
+ pluginMockRuntime,
3502
3609
  {
3503
3610
  name: 'rstest:browser-user-config',
3504
3611
  setup (api) {
@@ -3529,7 +3636,8 @@ const createBrowserRuntime = async ({ context, projectEntries, tempDir, isWatchM
3529
3636
  source: {
3530
3637
  define: {
3531
3638
  'process.env': rstestEnvDefine,
3532
- 'import.meta.env': rstestEnvDefine
3639
+ 'import.meta.env': rstestEnvDefine,
3640
+ 'import.meta.rstest': importMetaRstestDefine('web')
3533
3641
  }
3534
3642
  },
3535
3643
  output: {
@@ -3541,6 +3649,10 @@ const createBrowserRuntime = async ({ context, projectEntries, tempDir, isWatchM
3541
3649
  tools: {
3542
3650
  rspack: (rspackConfig)=>{
3543
3651
  rspackConfig.mode = 'development';
3652
+ applyWebMockRspackConfig(rspackConfig, {
3653
+ rspack: rspack,
3654
+ rootPath: project.rootPath
3655
+ });
3544
3656
  rspackConfig.lazyCompilation = enableHmr ? createBrowserLazyCompilationConfig(setupFiles) : false;
3545
3657
  rspackConfig.plugins = rspackConfig.plugins || [];
3546
3658
  rspackConfig.plugins.push(virtualManifestPlugin);
@@ -3593,6 +3705,21 @@ const createBrowserRuntime = async ({ context, projectEntries, tempDir, isWatchM
3593
3705
  }
3594
3706
  }
3595
3707
  ]);
3708
+ if (skipProviderLaunch) {
3709
+ await rsbuildInstance.initConfigs({
3710
+ action: 'dev'
3711
+ });
3712
+ return {
3713
+ projectName: project.name,
3714
+ environmentName: project.environmentName,
3715
+ rsbuildInstance,
3716
+ devServer: {
3717
+ close: async ()=>void 0
3718
+ },
3719
+ port: 0,
3720
+ manifestPath
3721
+ };
3722
+ }
3596
3723
  const coverage = project.normalizedConfig.coverage;
3597
3724
  if (coverage?.enabled && 'list' !== context.command) {
3598
3725
  const { pluginCoverage } = await loadCoverageProvider(coverage, context.rootPath);
@@ -3688,6 +3815,7 @@ const createBrowserRuntime = async ({ context, projectEntries, tempDir, isWatchM
3688
3815
  await closeAllProjectServers(projectServers.values());
3689
3816
  throw error;
3690
3817
  }
3818
+ if (skipProviderLaunch) return createRuntimeWithoutProvider();
3691
3819
  const containerServer = projectServers.get(browserProjects[0].name);
3692
3820
  const wss = new WebSocketServer({
3693
3821
  port: 0
@@ -3715,7 +3843,8 @@ const createBrowserRuntime = async ({ context, projectEntries, tempDir, isWatchM
3715
3843
  tempDir,
3716
3844
  setContainerOptions,
3717
3845
  dispatchHandlers,
3718
- wss
3846
+ wss,
3847
+ projectEntries
3719
3848
  };
3720
3849
  } catch (error) {
3721
3850
  wss.close();
@@ -3723,9 +3852,8 @@ const createBrowserRuntime = async ({ context, projectEntries, tempDir, isWatchM
3723
3852
  throw error;
3724
3853
  }
3725
3854
  };
3726
- async function resolveProjectEntries(context, shardedEntries) {
3855
+ async function resolveProjectEntries(context, shardedEntries, browserProjects) {
3727
3856
  if (shardedEntries) {
3728
- const browserProjects = getBrowserProjects(context);
3729
3857
  const projectEntries = [];
3730
3858
  for (const project of browserProjects){
3731
3859
  const entryInfo = shardedEntries.get(project.environmentName);
@@ -3740,14 +3868,14 @@ async function resolveProjectEntries(context, shardedEntries) {
3740
3868
  }
3741
3869
  return projectEntries;
3742
3870
  }
3743
- return collectProjectEntries(context);
3871
+ return collectProjectEntries(context, browserProjects);
3744
3872
  }
3745
3873
  const runBrowserController = async (context, options)=>{
3746
- const { skipOnTestRunEnd = false, allowEmptyWatchRun = false, onTraceEvents } = options ?? {};
3874
+ const { allowEmptyWatchRun = false, allowEmptyRun = false, filesOnly = false, onTraceEvents, env } = options ?? {};
3747
3875
  const buildStart = Date.now();
3748
3876
  const isWatchMode = 'watch' === context.command;
3749
3877
  const phaseTrackers = onTraceEvents ? new Map() : void 0;
3750
- const browserProjects = getBrowserProjects(context);
3878
+ const browserProjects = options?.projects ?? getBrowserProjects(context);
3751
3879
  const useHeadlessDirect = browserProjects.every((project)=>project.normalizedConfig.browser.headless);
3752
3880
  const browserSourceMapCache = new Map();
3753
3881
  const isHttpLikeFile = (file)=>/^https?:\/\//.test(file);
@@ -3795,7 +3923,7 @@ const runBrowserController = async (context, options)=>{
3795
3923
  resolveSourcemap: resolveBrowserSourcemap,
3796
3924
  close
3797
3925
  };
3798
- if (!skipOnTestRunEnd) for (const reporter of context.reporters)await reporter.onTestRunEnd?.({
3926
+ if (isWatchMode) for (const reporter of context.reporters)await reporter.onTestRunEnd?.({
3799
3927
  results: [],
3800
3928
  testResults: [],
3801
3929
  duration: errorResult.duration,
@@ -3807,9 +3935,9 @@ const runBrowserController = async (context, options)=>{
3807
3935
  };
3808
3936
  const toError = (error)=>error instanceof Error ? error : new Error(String(error));
3809
3937
  const failWithError = async (error, cleanup)=>{
3810
- ensureProcessExitCode(1);
3938
+ if (isWatchMode) ensureProcessExitCode(1);
3811
3939
  const normalizedError = toError(error);
3812
- if (cleanup && skipOnTestRunEnd) return buildErrorResult(normalizedError, cleanup);
3940
+ if (cleanup && !isWatchMode) return buildErrorResult(normalizedError, cleanup);
3813
3941
  try {
3814
3942
  return await buildErrorResult(normalizedError);
3815
3943
  } finally{
@@ -3821,13 +3949,13 @@ const runBrowserController = async (context, options)=>{
3821
3949
  return previous.map((file)=>file.testPath).filter((testPath)=>!currentPathSet.has(testPath));
3822
3950
  };
3823
3951
  const notifyTestRunStart = async ()=>{
3824
- if (skipOnTestRunEnd) return;
3952
+ if (!isWatchMode) return;
3825
3953
  for (const reporter of context.reporters)await reporter.onTestRunStart?.();
3826
3954
  };
3827
3955
  const coverageConfig = browserProjects.find((project)=>project.normalizedConfig.coverage?.enabled)?.normalizedConfig.coverage;
3828
3956
  const coverageProvider = coverageConfig?.enabled ? await createCoverageProvider(coverageConfig, context.rootPath) : null;
3829
3957
  const notifyTestRunEnd = async ({ duration, unhandledErrors, filterRerunTestPaths })=>{
3830
- if (skipOnTestRunEnd) return;
3958
+ if (!isWatchMode) return;
3831
3959
  let mergedCoverage;
3832
3960
  if (coverageProvider) {
3833
3961
  const coverageMap = coverageProvider.createCoverageMap();
@@ -3867,12 +3995,28 @@ const runBrowserController = async (context, options)=>{
3867
3995
  return failWithError(error);
3868
3996
  }
3869
3997
  }
3870
- const projectEntries = await resolveProjectEntries(context, options?.shardedEntries);
3871
- const totalTests = projectEntries.reduce((total, item)=>total + item.testFiles.length, 0);
3998
+ let projectEntries = await resolveProjectEntries(context, options?.shardedEntries, browserProjects);
3999
+ let totalTests = projectEntries.reduce((total, item)=>total + item.testFiles.length, 0);
3872
4000
  const shouldKeepWatchingWithEmptySet = isWatchMode && allowEmptyWatchRun;
3873
- if (0 === totalTests) {
4001
+ const shouldInitializeEmptyBrowserHooks = 0 === totalTests && hasUserRstestConfigPlugins(browserProjects);
4002
+ const createEmptyRunResult = ()=>{
4003
+ const elapsed = Math.max(0, Date.now() - buildStart);
4004
+ return {
4005
+ results: [],
4006
+ testResults: [],
4007
+ duration: {
4008
+ totalTime: elapsed,
4009
+ buildTime: elapsed,
4010
+ testTime: 0
4011
+ },
4012
+ hasFailure: false,
4013
+ getSourcemap: getBrowserSourcemap,
4014
+ resolveSourcemap: resolveBrowserSourcemap
4015
+ };
4016
+ };
4017
+ const reportEmptyTestSet = ()=>{
3874
4018
  const code = context.normalizedConfig.passWithNoTests ? 0 : 1;
3875
- if (!skipOnTestRunEnd) {
4019
+ if (isWatchMode || !allowEmptyRun) {
3876
4020
  const message = shouldKeepWatchingWithEmptySet ? 'No test files found.' : getNoTestFilesMessage({
3877
4021
  context,
3878
4022
  code,
@@ -3883,10 +4027,13 @@ const runBrowserController = async (context, options)=>{
3883
4027
  if (context.relatedFilters?.length) logger.log(color.gray('related: '), context.relatedFilters.join(color.gray(', ')));
3884
4028
  else if (context.fileFilters?.length) logger.log(color.gray('filter: '), context.fileFilters.join(color.gray(', ')));
3885
4029
  }
3886
- if (0 !== code && !shouldKeepWatchingWithEmptySet) ensureProcessExitCode(code);
3887
- if (!shouldKeepWatchingWithEmptySet) return;
4030
+ if (isWatchMode && 0 !== code && !shouldKeepWatchingWithEmptySet && !allowEmptyRun) ensureProcessExitCode(code);
4031
+ return !shouldKeepWatchingWithEmptySet;
4032
+ };
4033
+ if (0 === totalTests && !shouldInitializeEmptyBrowserHooks) {
4034
+ if (reportEmptyTestSet()) return allowEmptyRun ? createEmptyRunResult() : void 0;
3888
4035
  }
3889
- await notifyTestRunStart();
4036
+ if (!filesOnly) await notifyTestRunStart();
3890
4037
  const enableCliShortcuts = isWatchMode && isBrowserWatchCliShortcutsEnabled();
3891
4038
  const browserTempOutputRoot = context.normalizedConfig.output.distPath.root;
3892
4039
  const tempDir = isWatchMode && watchContext.runtime ? watchContext.runtime.tempDir : isWatchMode ? join(context.rootPath, browserTempOutputRoot, 'browser', 'watch') : join(context.rootPath, browserTempOutputRoot, 'browser', Date.now().toString());
@@ -3898,13 +4045,18 @@ const runBrowserController = async (context, options)=>{
3898
4045
  runtime = await createBrowserRuntime({
3899
4046
  context,
3900
4047
  projectEntries,
4048
+ browserProjects,
4049
+ shardedEntries: options?.shardedEntries,
4050
+ freezeShardedEntries: options?.freezeShardedEntries,
3901
4051
  tempDir,
3902
4052
  isWatchMode,
3903
4053
  onTriggerRerun: isWatchMode ? async ()=>{
3904
4054
  await triggerRerun?.();
3905
4055
  } : void 0,
3906
4056
  containerDistPath,
3907
- containerDevServer
4057
+ containerDevServer,
4058
+ skipProviderLaunch: filesOnly,
4059
+ appliedModifyRstestConfigEnvironments: options?.appliedModifyRstestConfigEnvironments
3908
4060
  });
3909
4061
  } catch (error) {
3910
4062
  return failWithError(error, async ()=>{
@@ -3922,8 +4074,27 @@ const runBrowserController = async (context, options)=>{
3922
4074
  });
3923
4075
  }
3924
4076
  }
3925
- const { browser, browserLaunchOptions, wsPort, wss } = runtime;
4077
+ projectEntries = runtime.projectEntries;
4078
+ totalTests = projectEntries.reduce((total, item)=>total + item.testFiles.length, 0);
3926
4079
  const buildTime = Date.now() - buildStart;
4080
+ if (filesOnly) return {
4081
+ results: [],
4082
+ testResults: [],
4083
+ duration: {
4084
+ totalTime: buildTime,
4085
+ buildTime,
4086
+ testTime: 0
4087
+ },
4088
+ hasFailure: false,
4089
+ getSourcemap: getBrowserSourcemap,
4090
+ resolveSourcemap: resolveBrowserSourcemap,
4091
+ close: ()=>destroyBrowserRuntime(runtime)
4092
+ };
4093
+ if (0 === totalTests && reportEmptyTestSet()) {
4094
+ await destroyBrowserRuntime(runtime);
4095
+ return allowEmptyRun ? createEmptyRunResult() : void 0;
4096
+ }
4097
+ const { browser, browserLaunchOptions, wsPort, wss } = runtime;
3927
4098
  const allTestFiles = projectEntries.flatMap((entry)=>entry.testFiles.map((testPath)=>({
3928
4099
  testPath: normalize(testPath),
3929
4100
  projectName: entry.project.name
@@ -3932,10 +4103,13 @@ const runBrowserController = async (context, options)=>{
3932
4103
  name: project.name,
3933
4104
  environmentName: project.environmentName,
3934
4105
  projectRoot: normalize(project.rootPath),
3935
- runtimeConfig: serializableConfig(getRuntimeConfigFromProject(project)),
4106
+ runtimeConfig: serializableConfig(projectRuntimeConfig(project, {
4107
+ envMode: 'static',
4108
+ envOverlay: env
4109
+ })),
3936
4110
  viewport: project.normalizedConfig.browser.viewport
3937
4111
  }));
3938
- const maxTestTimeoutForRpc = Math.max(...browserProjects.map((p)=>p.normalizedConfig.testTimeout ?? DEFAULT_TEST_TIMEOUT));
4112
+ const maxTestTimeoutForRpc = getMaxTestTimeoutForRpc(browserProjects);
3939
4113
  const projectRunnerUrls = Object.fromEntries([
3940
4114
  ...runtime.projectServers
3941
4115
  ].map(([name, server])=>[
@@ -3996,11 +4170,28 @@ const runBrowserController = async (context, options)=>{
3996
4170
  const reporterResults = [];
3997
4171
  const caseResults = [];
3998
4172
  let fatalError = null;
4173
+ const runnerSinks = new Map(browserProjects.map((project)=>[
4174
+ project.name,
4175
+ createRunnerEventSink(context, project.normalizedConfig)
4176
+ ]));
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;
4183
+ };
4184
+ const silentConsoleController = createSilentConsoleController({
4185
+ runtimeConfig: {
4186
+ silent: context.normalizedConfig.silent,
4187
+ disableConsoleIntercept: context.normalizedConfig.disableConsoleIntercept
4188
+ },
4189
+ emitInterceptedLog: (log)=>sinkForTestPath(log.testPath).onConsoleLog(log),
4190
+ writeOriginalLog: ()=>{}
4191
+ });
3999
4192
  const snapshotRpcMethods = {
4000
4193
  async resolveSnapshotPath (testPath) {
4001
- const snapExtension = '.snap';
4002
- const resolver = context.normalizedConfig.resolveSnapshotPath || (()=>join(dirname(testPath), '__snapshots__', `${basename(testPath)}${snapExtension}`));
4003
- return resolver(testPath, snapExtension);
4194
+ return resolveSnapshotPathDefault(testPath, context.normalizedConfig.resolveSnapshotPath);
4004
4195
  },
4005
4196
  async readSnapshotFile (filepath) {
4006
4197
  try {
@@ -4023,6 +4214,7 @@ const runBrowserController = async (context, options)=>{
4023
4214
  }
4024
4215
  };
4025
4216
  const handleTestFileStart = async (payload)=>{
4217
+ projectNameByTestPath.set(payload.testPath, payload.projectName);
4026
4218
  if (phaseTrackers) {
4027
4219
  const tracker = new PhaseTracker({
4028
4220
  trace: {
@@ -4034,24 +4226,24 @@ const runBrowserController = async (context, options)=>{
4034
4226
  tracker.transition('prepare');
4035
4227
  phaseTrackers.set(payload.testPath, tracker);
4036
4228
  }
4037
- await Promise.all(context.reporters.map((reporter)=>reporter.onTestFileStart?.({
4038
- testId: getFileTaskId(payload.testPath),
4039
- testPath: payload.testPath,
4040
- tests: []
4041
- })));
4229
+ await sinkForProjectName(payload.projectName).onTestFileStart({
4230
+ testId: getFileTaskId(payload.testPath),
4231
+ testPath: payload.testPath,
4232
+ tests: []
4233
+ });
4042
4234
  };
4043
4235
  const handleTestFileReady = async (payload)=>{
4044
4236
  phaseTrackers?.get(payload.testPath)?.transition('tests');
4045
- await Promise.all(context.reporters.map((reporter)=>reporter.onTestFileReady?.(payload)));
4237
+ await sinkForTestPath(payload.testPath).onTestFileReady(payload);
4046
4238
  };
4047
4239
  const handleTestSuiteStart = async (payload)=>{
4048
4240
  phaseTrackers?.get(payload.testPath)?.recordSuiteStart(payload);
4049
- await Promise.all(context.reporters.map((reporter)=>reporter.onTestSuiteStart?.(payload)));
4241
+ await sinkForTestPath(payload.testPath).onTestSuiteStart(payload);
4050
4242
  };
4051
4243
  const handleTestSuiteResult = async (payload)=>{
4052
4244
  phaseTrackers?.get(payload.testPath)?.recordSuiteResult(payload);
4053
- await Promise.all(context.reporters.map((reporter)=>reporter.onTestSuiteResult?.(payload)));
4054
- if ('passed-only' === context.normalizedConfig.silent) await flushBufferedLogsForTask({
4245
+ await sinkForTestPath(payload.testPath).onTestSuiteResult(payload);
4246
+ if ('passed-only' === context.normalizedConfig.silent) silentConsoleController.flushBufferedLogsForTask({
4055
4247
  taskId: payload.testId,
4056
4248
  status: payload.status,
4057
4249
  taskParentNames: payload.parentNames,
@@ -4061,13 +4253,13 @@ const runBrowserController = async (context, options)=>{
4061
4253
  };
4062
4254
  const handleTestCaseStart = async (payload)=>{
4063
4255
  phaseTrackers?.get(payload.testPath)?.recordCaseStart(payload);
4064
- await Promise.all(context.reporters.map((reporter)=>reporter.onTestCaseStart?.(payload)));
4256
+ sinkForTestPath(payload.testPath).onTestCaseStart(payload);
4065
4257
  };
4066
4258
  const handleTestCaseResult = async (payload)=>{
4067
4259
  caseResults.push(payload);
4068
4260
  phaseTrackers?.get(payload.testPath)?.recordCaseResult(payload);
4069
- await Promise.all(context.reporters.map((reporter)=>reporter.onTestCaseResult?.(payload)));
4070
- if ('passed-only' === context.normalizedConfig.silent) await flushBufferedLogsForTask({
4261
+ await sinkForTestPath(payload.testPath).onTestCaseResult(payload);
4262
+ if ('passed-only' === context.normalizedConfig.silent) silentConsoleController.flushBufferedLogsForTask({
4071
4263
  taskId: payload.testId,
4072
4264
  status: payload.status,
4073
4265
  taskParentNames: payload.parentNames,
@@ -4080,7 +4272,6 @@ const runBrowserController = async (context, options)=>{
4080
4272
  context.updateReporterResultState([
4081
4273
  payload
4082
4274
  ], payload.results);
4083
- if (payload.snapshotResult) context.snapshotManager.add(payload.snapshotResult);
4084
4275
  if (phaseTrackers) {
4085
4276
  const tracker = phaseTrackers.get(payload.testPath);
4086
4277
  if (tracker) {
@@ -4090,20 +4281,20 @@ const runBrowserController = async (context, options)=>{
4090
4281
  phaseTrackers.delete(payload.testPath);
4091
4282
  }
4092
4283
  }
4093
- if ('passed-only' === context.normalizedConfig.silent) await flushBufferedLogsForTask({
4284
+ if ('passed-only' === context.normalizedConfig.silent) silentConsoleController.flushBufferedLogsForTask({
4094
4285
  taskId: payload.testId,
4095
4286
  status: payload.status,
4096
4287
  taskParentNames: payload.parentNames,
4097
4288
  taskType: 'file',
4098
4289
  testPath: payload.testPath
4099
4290
  });
4100
- await Promise.all(context.reporters.map((reporter)=>reporter.onTestFileResult?.(payload)));
4101
- if ('fail' === payload.status) ensureProcessExitCode(1);
4291
+ await sinkForTestPath(payload.testPath).onTestFileResult(payload);
4292
+ if (isWatchMode && 'fail' === payload.status) ensureProcessExitCode(1);
4102
4293
  };
4103
4294
  const handleLog = async (payload)=>{
4104
4295
  const log = {
4105
4296
  content: payload.content,
4106
- name: payload.level,
4297
+ name: getPrettyConsoleName(payload.level),
4107
4298
  taskId: payload.taskId,
4108
4299
  taskName: payload.taskName,
4109
4300
  taskParentNames: payload.taskParentNames,
@@ -4112,62 +4303,13 @@ const runBrowserController = async (context, options)=>{
4112
4303
  type: payload.type,
4113
4304
  trace: payload.trace
4114
4305
  };
4115
- if (true === context.normalizedConfig.silent) return;
4116
- if ('passed-only' === context.normalizedConfig.silent) return void bufferConsoleLog(log);
4117
- if (context.normalizedConfig.disableConsoleIntercept) return;
4118
- await emitUserConsoleLog(log);
4306
+ silentConsoleController.onConsoleLog(log);
4119
4307
  };
4120
4308
  const handleFatal = async (payload)=>{
4121
4309
  const error = new Error(payload.message);
4122
4310
  error.stack = payload.stack;
4123
4311
  fatalError = error;
4124
- ensureProcessExitCode(1);
4125
- };
4126
- const bufferedConsoleLogs = new Map();
4127
- const suiteIdsByChain = new Map();
4128
- const getSuiteChainKey = (names)=>names.join('\u0000');
4129
- const pushTaskId = (taskIds, taskId)=>{
4130
- if (!taskIds.includes(taskId)) taskIds.push(taskId);
4131
- };
4132
- const shouldEmitUserConsoleLog = (log)=>context.normalizedConfig.onConsoleLog?.(log.content, log.type) !== false;
4133
- const emitUserConsoleLog = async (log)=>{
4134
- if (!shouldEmitUserConsoleLog(log)) return;
4135
- await Promise.all(context.reporters.map((reporter)=>reporter.onUserConsoleLog?.(log)));
4136
- };
4137
- const bufferConsoleLog = (log)=>{
4138
- const taskId = getBufferedLogTaskId(log);
4139
- const logs = bufferedConsoleLogs.get(taskId) || [];
4140
- logs.push(log);
4141
- bufferedConsoleLogs.set(taskId, logs);
4142
- if ('suite' === log.taskType && log.taskId) suiteIdsByChain.set(getSuiteChainKey([
4143
- ...log.taskParentNames || [],
4144
- log.taskName || ''
4145
- ]), log.taskId);
4146
- };
4147
- const flushBufferedLogsForTask = async ({ taskId, status, taskParentNames, taskType, testPath })=>{
4148
- if ('fail' !== status) return void bufferedConsoleLogs.delete(taskId);
4149
- const taskIdsToFlush = [];
4150
- if ('case' === taskType) {
4151
- pushTaskId(taskIdsToFlush, getFileTaskId(testPath));
4152
- const suiteNames = taskParentNames || [];
4153
- for(let i = 0; i < suiteNames.length; i++){
4154
- const suiteId = suiteIdsByChain.get(getSuiteChainKey(suiteNames.slice(0, i + 1)));
4155
- if (suiteId) pushTaskId(taskIdsToFlush, suiteId);
4156
- }
4157
- pushTaskId(taskIdsToFlush, taskId);
4158
- }
4159
- if ('suite' === taskType) {
4160
- pushTaskId(taskIdsToFlush, getFileTaskId(testPath));
4161
- pushTaskId(taskIdsToFlush, taskId);
4162
- }
4163
- if ('file' === taskType) pushTaskId(taskIdsToFlush, taskId);
4164
- for (const bufferedTaskId of taskIdsToFlush){
4165
- const logs = bufferedConsoleLogs.get(bufferedTaskId);
4166
- if (logs) {
4167
- bufferedConsoleLogs.delete(bufferedTaskId);
4168
- for (const log of logs)await Promise.all(context.reporters.map((reporter)=>reporter.onUserConsoleLog?.(log)));
4169
- }
4170
- }
4312
+ if (isWatchMode) ensureProcessExitCode(1);
4171
4313
  };
4172
4314
  const runSnapshotRpc = async (request)=>{
4173
4315
  switch(request.method){
@@ -4366,6 +4508,14 @@ const runBrowserController = async (context, options)=>{
4366
4508
  await closeContextSafely(browserContext);
4367
4509
  }
4368
4510
  };
4511
+ const makeSkippedFileResult = (file)=>({
4512
+ testId: getFileTaskId(file.testPath),
4513
+ status: 'skip',
4514
+ name: '',
4515
+ testPath: file.testPath,
4516
+ project: file.projectName,
4517
+ results: []
4518
+ });
4369
4519
  const runFilesWithPool = async (files)=>{
4370
4520
  if (0 === files.length) return;
4371
4521
  const previous = runLifecycle.activeSession;
@@ -4378,8 +4528,17 @@ const runBrowserController = async (context, options)=>{
4378
4528
  ...files
4379
4529
  ];
4380
4530
  const concurrency = getHeadlessConcurrency(context, queue.length);
4531
+ const bail = context.normalizedConfig.bail;
4381
4532
  const worker = async ()=>{
4382
4533
  while(queue.length > 0 && !run.cancelled && runLifecycle.isTokenActive(run.token)){
4534
+ if (bail && context.stateManager.getCountOfFailedTests() >= bail) {
4535
+ let skipped = queue.shift();
4536
+ while(skipped){
4537
+ await handleTestFileComplete(makeSkippedFileResult(skipped));
4538
+ skipped = queue.shift();
4539
+ }
4540
+ return;
4541
+ }
4383
4542
  const next = queue.shift();
4384
4543
  if (!next) return;
4385
4544
  await runSingleFile(run, next);
@@ -4401,6 +4560,7 @@ const runBrowserController = async (context, options)=>{
4401
4560
  await cancelRun(run, false);
4402
4561
  },
4403
4562
  runFiles: async (files)=>{
4563
+ prepareWatchRerunState(context);
4404
4564
  await notifyTestRunStart();
4405
4565
  const rerunStartTime = Date.now();
4406
4566
  const fatalErrorBeforeRun = fatalError;
@@ -4453,12 +4613,12 @@ const runBrowserController = async (context, options)=>{
4453
4613
  hasFailure: false,
4454
4614
  getSourcemap: getBrowserSourcemap,
4455
4615
  resolveSourcemap: resolveBrowserSourcemap,
4456
- close: skipOnTestRunEnd ? async ()=>{
4616
+ close: isWatchMode ? void 0 : async ()=>{
4457
4617
  sessionRegistry.clear();
4458
4618
  await destroyBrowserRuntime(runtime);
4459
- } : void 0
4619
+ }
4460
4620
  };
4461
- if (!skipOnTestRunEnd) await notifyTestRunEnd({
4621
+ if (isWatchMode) await notifyTestRunEnd({
4462
4622
  duration
4463
4623
  });
4464
4624
  if (isWatchMode) {
@@ -4533,7 +4693,7 @@ const runBrowserController = async (context, options)=>{
4533
4693
  };
4534
4694
  context.updateReporterResultState(reporterResults, caseResults);
4535
4695
  const isFailure = reporterResults.some((result)=>'fail' === result.status);
4536
- if (isFailure) ensureProcessExitCode(1);
4696
+ if (isWatchMode && isFailure) ensureProcessExitCode(1);
4537
4697
  const result = {
4538
4698
  results: reporterResults,
4539
4699
  testResults: caseResults,
@@ -4541,9 +4701,9 @@ const runBrowserController = async (context, options)=>{
4541
4701
  hasFailure: isFailure,
4542
4702
  getSourcemap: getBrowserSourcemap,
4543
4703
  resolveSourcemap: resolveBrowserSourcemap,
4544
- close: skipOnTestRunEnd ? closeHeadlessRuntime : void 0
4704
+ close: closeHeadlessRuntime
4545
4705
  };
4546
- if (!skipOnTestRunEnd) try {
4706
+ if (isWatchMode) try {
4547
4707
  await notifyTestRunEnd({
4548
4708
  duration
4549
4709
  });
@@ -4754,7 +4914,7 @@ const runBrowserController = async (context, options)=>{
4754
4914
  }
4755
4915
  } catch (error) {
4756
4916
  fatalError = fatalError ?? toError(error);
4757
- ensureProcessExitCode(1);
4917
+ if (isWatchMode) ensureProcessExitCode(1);
4758
4918
  }
4759
4919
  testTime = Date.now() - testStart;
4760
4920
  }
@@ -4781,6 +4941,7 @@ const runBrowserController = async (context, options)=>{
4781
4941
  }
4782
4942
  if (rerunPlan.normalizedAffectedTestFiles.length > 0) {
4783
4943
  logger.log(color.cyan(`Re-running ${rerunPlan.normalizedAffectedTestFiles.length} affected test file(s)...\n`));
4944
+ prepareWatchRerunState(context);
4784
4945
  await notifyTestRunStart();
4785
4946
  const rerunStartTime = Date.now();
4786
4947
  const fatalErrorBeforeRun = fatalError;
@@ -4831,7 +4992,7 @@ const runBrowserController = async (context, options)=>{
4831
4992
  };
4832
4993
  context.updateReporterResultState(reporterResults, caseResults);
4833
4994
  const isFailure = reporterResults.some((result)=>'fail' === result.status);
4834
- if (isFailure) ensureProcessExitCode(1);
4995
+ if (isWatchMode && isFailure) ensureProcessExitCode(1);
4835
4996
  const result = {
4836
4997
  results: reporterResults,
4837
4998
  testResults: caseResults,
@@ -4839,9 +5000,9 @@ const runBrowserController = async (context, options)=>{
4839
5000
  hasFailure: isFailure,
4840
5001
  getSourcemap: getBrowserSourcemap,
4841
5002
  resolveSourcemap: resolveBrowserSourcemap,
4842
- close: skipOnTestRunEnd ? closeContainerRuntime : void 0
5003
+ close: closeContainerRuntime
4843
5004
  };
4844
- if (!skipOnTestRunEnd) try {
5005
+ if (isWatchMode) try {
4845
5006
  await notifyTestRunEnd({
4846
5007
  duration
4847
5008
  });
@@ -4855,24 +5016,29 @@ const runBrowserController = async (context, options)=>{
4855
5016
  return result;
4856
5017
  };
4857
5018
  const listBrowserTests = async (context, options)=>{
4858
- const projectEntries = await resolveProjectEntries(context, options?.shardedEntries);
5019
+ const browserProjects = options?.projects ?? getBrowserProjects(context);
5020
+ const projectEntries = await resolveProjectEntries(context, options?.shardedEntries, browserProjects);
4859
5021
  const totalTests = projectEntries.reduce((total, item)=>total + item.testFiles.length, 0);
4860
- if (0 === totalTests) return {
5022
+ if (0 === totalTests && !hasUserRstestConfigPlugins(browserProjects)) return {
4861
5023
  list: [],
4862
5024
  close: async ()=>{}
4863
5025
  };
4864
5026
  const tempDir = join(context.rootPath, context.normalizedConfig.output.distPath.root, 'browser', `list-${Date.now()}`);
4865
- const browserProjects = getBrowserProjects(context);
4866
5027
  let runtime;
4867
5028
  try {
4868
5029
  runtime = await createBrowserRuntime({
4869
5030
  context,
4870
5031
  projectEntries,
5032
+ browserProjects,
5033
+ shardedEntries: options?.shardedEntries,
5034
+ freezeShardedEntries: options?.freezeShardedEntries,
4871
5035
  tempDir,
4872
5036
  isWatchMode: false,
4873
5037
  containerDistPath: void 0,
4874
5038
  containerDevServer: void 0,
4875
- forceHeadless: true
5039
+ forceHeadless: true,
5040
+ skipProviderLaunch: options?.filesOnly,
5041
+ appliedModifyRstestConfigEnvironments: options?.appliedModifyRstestConfigEnvironments
4876
5042
  });
4877
5043
  } catch (error) {
4878
5044
  const providers = [
@@ -4881,15 +5047,36 @@ const listBrowserTests = async (context, options)=>{
4881
5047
  logger.error(color.red(`Failed to initialize browser provider runtime (${providers.join(', ')}).`), error);
4882
5048
  throw error;
4883
5049
  }
5050
+ if (options?.filesOnly) {
5051
+ const list = runtime.projectEntries.flatMap((entry)=>entry.testFiles.map((testPath)=>({
5052
+ testPath,
5053
+ project: entry.project.name,
5054
+ tests: []
5055
+ })));
5056
+ await destroyBrowserRuntime(runtime);
5057
+ return {
5058
+ list,
5059
+ close: async ()=>{}
5060
+ };
5061
+ }
5062
+ if (!runtime.projectEntries.some((entry)=>entry.testFiles.length > 0)) {
5063
+ await destroyBrowserRuntime(runtime);
5064
+ return {
5065
+ list: [],
5066
+ close: async ()=>{}
5067
+ };
5068
+ }
4884
5069
  const { browser, browserLaunchOptions } = runtime;
4885
5070
  const projectRuntimeConfigs = browserProjects.map((project)=>({
4886
5071
  name: project.name,
4887
5072
  environmentName: project.environmentName,
4888
5073
  projectRoot: normalize(project.rootPath),
4889
- runtimeConfig: serializableConfig(getRuntimeConfigFromProject(project)),
5074
+ runtimeConfig: serializableConfig(projectRuntimeConfig(project, {
5075
+ envMode: 'static'
5076
+ })),
4890
5077
  viewport: project.normalizedConfig.browser.viewport
4891
5078
  }));
4892
- const maxTestTimeoutForRpc = Math.max(...browserProjects.map((p)=>p.normalizedConfig.testTimeout ?? DEFAULT_TEST_TIMEOUT));
5079
+ const maxTestTimeoutForRpc = getMaxTestTimeoutForRpc(browserProjects);
4893
5080
  const hostOptions = {
4894
5081
  rootPath: normalize(context.rootPath),
4895
5082
  projects: projectRuntimeConfigs,
@@ -4906,6 +5093,7 @@ const listBrowserTests = async (context, options)=>{
4906
5093
  viewport: null
4907
5094
  });
4908
5095
  const serializedOptions = serializeForInlineScript(hostOptions);
5096
+ const collectTimeoutMs = 30000;
4909
5097
  const collectFromServer = async (server)=>{
4910
5098
  const results = [];
4911
5099
  let error = null;
@@ -4950,13 +5138,12 @@ const listBrowserTests = async (context, options)=>{
4950
5138
  await page.goto(`http://localhost:${server.port}/runner.html`, {
4951
5139
  waitUntil: 'load'
4952
5140
  });
4953
- const timeoutMs = 30000;
4954
5141
  let timeoutId;
4955
5142
  const timeoutPromise = new Promise((resolve)=>{
4956
5143
  timeoutId = setTimeout(()=>{
4957
- if (!collectCompleted) logger.warn(color.yellow(`[List] Browser test collection timed out after ${timeoutMs}ms`));
5144
+ if (!collectCompleted) logger.warn(color.yellow(`[List] Browser test collection timed out after ${collectTimeoutMs}ms`));
4958
5145
  resolve();
4959
- }, timeoutMs);
5146
+ }, collectTimeoutMs);
4960
5147
  });
4961
5148
  await Promise.race([
4962
5149
  collectPromise,
@@ -5006,10 +5193,92 @@ const listBrowserTests = async (context, options)=>{
5006
5193
  close: cleanup
5007
5194
  };
5008
5195
  };
5196
+ const emptyOutcome = ()=>({
5197
+ results: [],
5198
+ testResults: [],
5199
+ errors: [],
5200
+ testPaths: [],
5201
+ duration: {
5202
+ buildTime: 0,
5203
+ testTime: 0
5204
+ }
5205
+ });
5206
+ async function createBrowserExecutor(context, options) {
5207
+ const { projects, coverageProvider, freezeShardedEntries, filesOnly, allowEmptyRun, appliedModifyRstestConfigEnvironments } = options;
5208
+ let deferredClose;
5209
+ let inFlightCycle;
5210
+ const foldOutcome = (result)=>{
5211
+ if (!result) return emptyOutcome();
5212
+ const map = buildBrowserCoverageMap(result.results, coverageProvider);
5213
+ return {
5214
+ results: result.results,
5215
+ testResults: result.testResults,
5216
+ errors: result.unhandledErrors ?? [],
5217
+ testPaths: result.results.map((r)=>r.testPath),
5218
+ duration: {
5219
+ buildTime: result.duration.buildTime,
5220
+ testTime: result.duration.testTime
5221
+ },
5222
+ coverage: {
5223
+ map: map?.toJSON()
5224
+ },
5225
+ resolveSourcemap: result.resolveSourcemap
5226
+ };
5227
+ };
5228
+ return {
5229
+ name: 'browser',
5230
+ projects,
5231
+ async init () {},
5232
+ async runCycle (opts) {
5233
+ const cycle = runBrowserController(context, {
5234
+ projects,
5235
+ shardedEntries: opts.shardedEntries,
5236
+ freezeShardedEntries,
5237
+ allowEmptyRun,
5238
+ appliedModifyRstestConfigEnvironments,
5239
+ onTraceEvents: opts.onTraceEvents,
5240
+ env: opts.env
5241
+ });
5242
+ inFlightCycle = cycle;
5243
+ try {
5244
+ const result = await cycle;
5245
+ deferredClose = result?.close;
5246
+ return foldOutcome(result);
5247
+ } finally{
5248
+ inFlightCycle = void 0;
5249
+ }
5250
+ },
5251
+ async collect (opts) {
5252
+ const pending = listBrowserTests(context, {
5253
+ projects,
5254
+ shardedEntries: opts.shardedEntries,
5255
+ freezeShardedEntries,
5256
+ filesOnly,
5257
+ appliedModifyRstestConfigEnvironments
5258
+ });
5259
+ inFlightCycle = pending;
5260
+ try {
5261
+ const { list, close } = await pending;
5262
+ deferredClose = close;
5263
+ return {
5264
+ list
5265
+ };
5266
+ } finally{
5267
+ inFlightCycle = void 0;
5268
+ }
5269
+ },
5270
+ async close () {
5271
+ if (inFlightCycle) await inFlightCycle.catch(()=>void 0);
5272
+ const close = deferredClose;
5273
+ deferredClose = void 0;
5274
+ await close?.();
5275
+ }
5276
+ };
5277
+ }
5009
5278
  async function runBrowserTests(context, options) {
5010
5279
  return runBrowserController(context, options);
5011
5280
  }
5012
5281
  async function src_listBrowserTests(context, options) {
5013
5282
  return listBrowserTests(context, options);
5014
5283
  }
5015
- export { runBrowserTests, src_listBrowserTests as listBrowserTests, validateBrowserConfig };
5284
+ export { createBrowserExecutor, runBrowserTests, src_listBrowserTests as listBrowserTests, validateBrowserConfig };