@rstest/browser 0.11.0 → 0.11.2

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, 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
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),
@@ -3399,7 +3431,7 @@ const registerWatchCleanup = ()=>{
3399
3431
  });
3400
3432
  watchContext.cleanupRegistered = true;
3401
3433
  };
3402
- const createBrowserRuntime = async ({ context, projectEntries, tempDir, isWatchMode, onTriggerRerun, containerDistPath, containerDevServer, forceHeadless })=>{
3434
+ const createBrowserRuntime = async ({ context, projectEntries: initialProjectEntries, browserProjects, shardedEntries, freezeShardedEntries, tempDir, isWatchMode, onTriggerRerun, containerDistPath, containerDevServer, forceHeadless, skipProviderLaunch, appliedModifyRstestConfigEnvironments })=>{
3403
3435
  const containerHtmlTemplate = containerDistPath ? await promises.readFile(join(containerDistPath, 'index.html'), 'utf-8') : null;
3404
3436
  let injectedContainerHtml = null;
3405
3437
  let serializedOptions = 'null';
@@ -3408,8 +3440,57 @@ const createBrowserRuntime = async ({ context, projectEntries, tempDir, isWatchM
3408
3440
  serializedOptions = serializeForInlineScript(options);
3409
3441
  if (containerHtmlTemplate) injectedContainerHtml = containerHtmlTemplate.replace(OPTIONS_PLACEHOLDER, serializedOptions);
3410
3442
  };
3411
- const browserProjects = getBrowserProjects(context);
3412
- const browserLaunchOptions = ensureConsistentBrowserLaunchOptions(browserProjects);
3443
+ let browserLaunchOptions = ensureConsistentBrowserLaunchOptions(browserProjects);
3444
+ let projectEntries = initialProjectEntries;
3445
+ const manifestModules = [];
3446
+ const createRuntimeWithoutProvider = ()=>{
3447
+ const firstProject = browserProjects[0];
3448
+ return {
3449
+ projectServers: new Map(),
3450
+ containerServer: {
3451
+ projectName: firstProject.name,
3452
+ environmentName: firstProject.environmentName,
3453
+ rsbuildInstance: void 0,
3454
+ devServer: {
3455
+ close: async ()=>void 0
3456
+ },
3457
+ port: 0,
3458
+ manifestPath: ''
3459
+ },
3460
+ browser: void 0,
3461
+ browserLaunchOptions,
3462
+ wsPort: 0,
3463
+ tempDir,
3464
+ setContainerOptions,
3465
+ dispatchHandlers,
3466
+ wss: void 0,
3467
+ projectEntries
3468
+ };
3469
+ };
3470
+ const getProjectEntry = (project)=>projectEntries.find((item)=>item.project.environmentName === project.environmentName);
3471
+ const refreshManifestModule = (manifestModule)=>{
3472
+ const entry = getProjectEntry(manifestModule.project);
3473
+ manifestModule.modules[manifestModule.manifestPath] = generateManifestModule({
3474
+ manifestPath: manifestModule.manifestPath,
3475
+ entries: [
3476
+ {
3477
+ project: manifestModule.project,
3478
+ testFiles: entry?.testFiles ?? [],
3479
+ setupFiles: entry?.setupFiles ?? []
3480
+ }
3481
+ ],
3482
+ isWatchMode
3483
+ });
3484
+ };
3485
+ const refreshProjectEntries = async ()=>{
3486
+ validateBrowserConfig(context);
3487
+ browserLaunchOptions = ensureConsistentBrowserLaunchOptions(browserProjects);
3488
+ const updatedShardedEntries = freezeShardedEntries ? shardedEntries : context.normalizedConfig.shard ? await resolveShardedEntries(context, {
3489
+ silent: true
3490
+ }) : shardedEntries;
3491
+ projectEntries = await resolveProjectEntries(context, updatedShardedEntries, browserProjects);
3492
+ for (const manifestModule of manifestModules)refreshManifestModule(manifestModule);
3493
+ };
3413
3494
  const browserRuntimePath = fileURLToPath(import.meta.resolve('@rstest/core/internal/browser-runtime'));
3414
3495
  const staticRstestAliases = {
3415
3496
  '@rstest/core': resolveBrowserFile('client/public.ts'),
@@ -3454,26 +3535,27 @@ const createBrowserRuntime = async ({ context, projectEntries, tempDir, isWatchM
3454
3535
  return false;
3455
3536
  }
3456
3537
  };
3457
- const entryByEnvironmentName = new Map(projectEntries.map((entry)=>[
3458
- entry.project.environmentName,
3459
- entry
3460
- ]));
3461
3538
  const buildProjectServer = async (project, isContainerServer)=>{
3462
3539
  const manifestPath = join(tempDir, toSafeVarName(project.environmentName), VIRTUAL_MANIFEST_FILENAME);
3463
- const entry = entryByEnvironmentName.get(project.environmentName);
3464
- const manifestSource = generateManifestModule({
3540
+ const entry = getProjectEntry(project);
3541
+ const virtualManifestModules = {
3542
+ [manifestPath]: generateManifestModule({
3543
+ manifestPath,
3544
+ entries: [
3545
+ {
3546
+ project,
3547
+ testFiles: entry?.testFiles ?? [],
3548
+ setupFiles: entry?.setupFiles ?? []
3549
+ }
3550
+ ],
3551
+ isWatchMode
3552
+ })
3553
+ };
3554
+ const virtualManifestPlugin = new rspack.experiments.VirtualModulesPlugin(virtualManifestModules);
3555
+ manifestModules.push({
3465
3556
  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
3557
+ project,
3558
+ modules: virtualManifestModules
3477
3559
  });
3478
3560
  const rstestInternalAliases = {
3479
3561
  '@rstest/browser-manifest': manifestPath,
@@ -3482,11 +3564,10 @@ const createBrowserRuntime = async ({ context, projectEntries, tempDir, isWatchM
3482
3564
  const isHeadless = forceHeadless || project.normalizedConfig.browser.headless;
3483
3565
  const enableHmr = shouldEnableBrowserHmr(isWatchMode, isHeadless);
3484
3566
  const rsbuildInstance = await createRsbuild({
3485
- callerName: 'rstest-browser',
3567
+ callerName: 'rstest',
3486
3568
  rsbuildConfig: {
3487
3569
  root: context.rootPath,
3488
3570
  mode: 'development',
3489
- plugins: project.normalizedConfig.plugins || [],
3490
3571
  server: {
3491
3572
  printUrls: false,
3492
3573
  port: project.normalizedConfig.browser.port ?? (isContainerServer ? 4000 : 0),
@@ -3494,10 +3575,19 @@ const createBrowserRuntime = async ({ context, projectEntries, tempDir, isWatchM
3494
3575
  },
3495
3576
  dev: createBrowserRsbuildDevConfig(enableHmr),
3496
3577
  environments: {
3497
- [project.environmentName]: {}
3578
+ [project.environmentName]: getBrowserRsbuildEnvironmentConfig(project)
3498
3579
  }
3499
3580
  }
3500
3581
  });
3582
+ initModifyRstestConfigHooks(context, rsbuildInstance, [
3583
+ project
3584
+ ], [
3585
+ project
3586
+ ], {
3587
+ getEnvironmentConfig: getBrowserRsbuildEnvironmentConfig,
3588
+ onModifyRstestConfigApplied: refreshProjectEntries,
3589
+ appliedEnvironmentNames: appliedModifyRstestConfigEnvironments
3590
+ });
3501
3591
  rsbuildInstance.addPlugins([
3502
3592
  {
3503
3593
  name: 'rstest:browser-user-config',
@@ -3593,6 +3683,21 @@ const createBrowserRuntime = async ({ context, projectEntries, tempDir, isWatchM
3593
3683
  }
3594
3684
  }
3595
3685
  ]);
3686
+ if (skipProviderLaunch) {
3687
+ await rsbuildInstance.initConfigs({
3688
+ action: 'dev'
3689
+ });
3690
+ return {
3691
+ projectName: project.name,
3692
+ environmentName: project.environmentName,
3693
+ rsbuildInstance,
3694
+ devServer: {
3695
+ close: async ()=>void 0
3696
+ },
3697
+ port: 0,
3698
+ manifestPath
3699
+ };
3700
+ }
3596
3701
  const coverage = project.normalizedConfig.coverage;
3597
3702
  if (coverage?.enabled && 'list' !== context.command) {
3598
3703
  const { pluginCoverage } = await loadCoverageProvider(coverage, context.rootPath);
@@ -3688,6 +3793,7 @@ const createBrowserRuntime = async ({ context, projectEntries, tempDir, isWatchM
3688
3793
  await closeAllProjectServers(projectServers.values());
3689
3794
  throw error;
3690
3795
  }
3796
+ if (skipProviderLaunch) return createRuntimeWithoutProvider();
3691
3797
  const containerServer = projectServers.get(browserProjects[0].name);
3692
3798
  const wss = new WebSocketServer({
3693
3799
  port: 0
@@ -3715,7 +3821,8 @@ const createBrowserRuntime = async ({ context, projectEntries, tempDir, isWatchM
3715
3821
  tempDir,
3716
3822
  setContainerOptions,
3717
3823
  dispatchHandlers,
3718
- wss
3824
+ wss,
3825
+ projectEntries
3719
3826
  };
3720
3827
  } catch (error) {
3721
3828
  wss.close();
@@ -3723,9 +3830,8 @@ const createBrowserRuntime = async ({ context, projectEntries, tempDir, isWatchM
3723
3830
  throw error;
3724
3831
  }
3725
3832
  };
3726
- async function resolveProjectEntries(context, shardedEntries) {
3833
+ async function resolveProjectEntries(context, shardedEntries, browserProjects) {
3727
3834
  if (shardedEntries) {
3728
- const browserProjects = getBrowserProjects(context);
3729
3835
  const projectEntries = [];
3730
3836
  for (const project of browserProjects){
3731
3837
  const entryInfo = shardedEntries.get(project.environmentName);
@@ -3740,14 +3846,14 @@ async function resolveProjectEntries(context, shardedEntries) {
3740
3846
  }
3741
3847
  return projectEntries;
3742
3848
  }
3743
- return collectProjectEntries(context);
3849
+ return collectProjectEntries(context, browserProjects);
3744
3850
  }
3745
3851
  const runBrowserController = async (context, options)=>{
3746
- const { skipOnTestRunEnd = false, allowEmptyWatchRun = false, onTraceEvents } = options ?? {};
3852
+ const { allowEmptyWatchRun = false, allowEmptyRun = false, filesOnly = false, onTraceEvents, env } = options ?? {};
3747
3853
  const buildStart = Date.now();
3748
3854
  const isWatchMode = 'watch' === context.command;
3749
3855
  const phaseTrackers = onTraceEvents ? new Map() : void 0;
3750
- const browserProjects = getBrowserProjects(context);
3856
+ const browserProjects = options?.projects ?? getBrowserProjects(context);
3751
3857
  const useHeadlessDirect = browserProjects.every((project)=>project.normalizedConfig.browser.headless);
3752
3858
  const browserSourceMapCache = new Map();
3753
3859
  const isHttpLikeFile = (file)=>/^https?:\/\//.test(file);
@@ -3795,7 +3901,7 @@ const runBrowserController = async (context, options)=>{
3795
3901
  resolveSourcemap: resolveBrowserSourcemap,
3796
3902
  close
3797
3903
  };
3798
- if (!skipOnTestRunEnd) for (const reporter of context.reporters)await reporter.onTestRunEnd?.({
3904
+ if (isWatchMode) for (const reporter of context.reporters)await reporter.onTestRunEnd?.({
3799
3905
  results: [],
3800
3906
  testResults: [],
3801
3907
  duration: errorResult.duration,
@@ -3807,9 +3913,9 @@ const runBrowserController = async (context, options)=>{
3807
3913
  };
3808
3914
  const toError = (error)=>error instanceof Error ? error : new Error(String(error));
3809
3915
  const failWithError = async (error, cleanup)=>{
3810
- ensureProcessExitCode(1);
3916
+ if (isWatchMode) ensureProcessExitCode(1);
3811
3917
  const normalizedError = toError(error);
3812
- if (cleanup && skipOnTestRunEnd) return buildErrorResult(normalizedError, cleanup);
3918
+ if (cleanup && !isWatchMode) return buildErrorResult(normalizedError, cleanup);
3813
3919
  try {
3814
3920
  return await buildErrorResult(normalizedError);
3815
3921
  } finally{
@@ -3821,13 +3927,13 @@ const runBrowserController = async (context, options)=>{
3821
3927
  return previous.map((file)=>file.testPath).filter((testPath)=>!currentPathSet.has(testPath));
3822
3928
  };
3823
3929
  const notifyTestRunStart = async ()=>{
3824
- if (skipOnTestRunEnd) return;
3930
+ if (!isWatchMode) return;
3825
3931
  for (const reporter of context.reporters)await reporter.onTestRunStart?.();
3826
3932
  };
3827
3933
  const coverageConfig = browserProjects.find((project)=>project.normalizedConfig.coverage?.enabled)?.normalizedConfig.coverage;
3828
3934
  const coverageProvider = coverageConfig?.enabled ? await createCoverageProvider(coverageConfig, context.rootPath) : null;
3829
3935
  const notifyTestRunEnd = async ({ duration, unhandledErrors, filterRerunTestPaths })=>{
3830
- if (skipOnTestRunEnd) return;
3936
+ if (!isWatchMode) return;
3831
3937
  let mergedCoverage;
3832
3938
  if (coverageProvider) {
3833
3939
  const coverageMap = coverageProvider.createCoverageMap();
@@ -3867,12 +3973,28 @@ const runBrowserController = async (context, options)=>{
3867
3973
  return failWithError(error);
3868
3974
  }
3869
3975
  }
3870
- const projectEntries = await resolveProjectEntries(context, options?.shardedEntries);
3871
- const totalTests = projectEntries.reduce((total, item)=>total + item.testFiles.length, 0);
3976
+ let projectEntries = await resolveProjectEntries(context, options?.shardedEntries, browserProjects);
3977
+ let totalTests = projectEntries.reduce((total, item)=>total + item.testFiles.length, 0);
3872
3978
  const shouldKeepWatchingWithEmptySet = isWatchMode && allowEmptyWatchRun;
3873
- if (0 === totalTests) {
3979
+ const shouldInitializeEmptyBrowserHooks = 0 === totalTests && hasUserRstestConfigPlugins(browserProjects);
3980
+ const createEmptyRunResult = ()=>{
3981
+ const elapsed = Math.max(0, Date.now() - buildStart);
3982
+ return {
3983
+ results: [],
3984
+ testResults: [],
3985
+ duration: {
3986
+ totalTime: elapsed,
3987
+ buildTime: elapsed,
3988
+ testTime: 0
3989
+ },
3990
+ hasFailure: false,
3991
+ getSourcemap: getBrowserSourcemap,
3992
+ resolveSourcemap: resolveBrowserSourcemap
3993
+ };
3994
+ };
3995
+ const reportEmptyTestSet = ()=>{
3874
3996
  const code = context.normalizedConfig.passWithNoTests ? 0 : 1;
3875
- if (!skipOnTestRunEnd) {
3997
+ if (isWatchMode || !allowEmptyRun) {
3876
3998
  const message = shouldKeepWatchingWithEmptySet ? 'No test files found.' : getNoTestFilesMessage({
3877
3999
  context,
3878
4000
  code,
@@ -3883,10 +4005,13 @@ const runBrowserController = async (context, options)=>{
3883
4005
  if (context.relatedFilters?.length) logger.log(color.gray('related: '), context.relatedFilters.join(color.gray(', ')));
3884
4006
  else if (context.fileFilters?.length) logger.log(color.gray('filter: '), context.fileFilters.join(color.gray(', ')));
3885
4007
  }
3886
- if (0 !== code && !shouldKeepWatchingWithEmptySet) ensureProcessExitCode(code);
3887
- if (!shouldKeepWatchingWithEmptySet) return;
4008
+ if (isWatchMode && 0 !== code && !shouldKeepWatchingWithEmptySet && !allowEmptyRun) ensureProcessExitCode(code);
4009
+ return !shouldKeepWatchingWithEmptySet;
4010
+ };
4011
+ if (0 === totalTests && !shouldInitializeEmptyBrowserHooks) {
4012
+ if (reportEmptyTestSet()) return allowEmptyRun ? createEmptyRunResult() : void 0;
3888
4013
  }
3889
- await notifyTestRunStart();
4014
+ if (!filesOnly) await notifyTestRunStart();
3890
4015
  const enableCliShortcuts = isWatchMode && isBrowserWatchCliShortcutsEnabled();
3891
4016
  const browserTempOutputRoot = context.normalizedConfig.output.distPath.root;
3892
4017
  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 +4023,18 @@ const runBrowserController = async (context, options)=>{
3898
4023
  runtime = await createBrowserRuntime({
3899
4024
  context,
3900
4025
  projectEntries,
4026
+ browserProjects,
4027
+ shardedEntries: options?.shardedEntries,
4028
+ freezeShardedEntries: options?.freezeShardedEntries,
3901
4029
  tempDir,
3902
4030
  isWatchMode,
3903
4031
  onTriggerRerun: isWatchMode ? async ()=>{
3904
4032
  await triggerRerun?.();
3905
4033
  } : void 0,
3906
4034
  containerDistPath,
3907
- containerDevServer
4035
+ containerDevServer,
4036
+ skipProviderLaunch: filesOnly,
4037
+ appliedModifyRstestConfigEnvironments: options?.appliedModifyRstestConfigEnvironments
3908
4038
  });
3909
4039
  } catch (error) {
3910
4040
  return failWithError(error, async ()=>{
@@ -3922,8 +4052,27 @@ const runBrowserController = async (context, options)=>{
3922
4052
  });
3923
4053
  }
3924
4054
  }
3925
- const { browser, browserLaunchOptions, wsPort, wss } = runtime;
4055
+ projectEntries = runtime.projectEntries;
4056
+ totalTests = projectEntries.reduce((total, item)=>total + item.testFiles.length, 0);
3926
4057
  const buildTime = Date.now() - buildStart;
4058
+ if (filesOnly) return {
4059
+ results: [],
4060
+ testResults: [],
4061
+ duration: {
4062
+ totalTime: buildTime,
4063
+ buildTime,
4064
+ testTime: 0
4065
+ },
4066
+ hasFailure: false,
4067
+ getSourcemap: getBrowserSourcemap,
4068
+ resolveSourcemap: resolveBrowserSourcemap,
4069
+ close: ()=>destroyBrowserRuntime(runtime)
4070
+ };
4071
+ if (0 === totalTests && reportEmptyTestSet()) {
4072
+ await destroyBrowserRuntime(runtime);
4073
+ return allowEmptyRun ? createEmptyRunResult() : void 0;
4074
+ }
4075
+ const { browser, browserLaunchOptions, wsPort, wss } = runtime;
3927
4076
  const allTestFiles = projectEntries.flatMap((entry)=>entry.testFiles.map((testPath)=>({
3928
4077
  testPath: normalize(testPath),
3929
4078
  projectName: entry.project.name
@@ -3932,10 +4081,13 @@ const runBrowserController = async (context, options)=>{
3932
4081
  name: project.name,
3933
4082
  environmentName: project.environmentName,
3934
4083
  projectRoot: normalize(project.rootPath),
3935
- runtimeConfig: serializableConfig(getRuntimeConfigFromProject(project)),
4084
+ runtimeConfig: serializableConfig(projectRuntimeConfig(project, {
4085
+ envMode: 'static',
4086
+ envOverlay: env
4087
+ })),
3936
4088
  viewport: project.normalizedConfig.browser.viewport
3937
4089
  }));
3938
- const maxTestTimeoutForRpc = Math.max(...browserProjects.map((p)=>p.normalizedConfig.testTimeout ?? DEFAULT_TEST_TIMEOUT));
4090
+ const maxTestTimeoutForRpc = getMaxTestTimeoutForRpc(browserProjects);
3939
4091
  const projectRunnerUrls = Object.fromEntries([
3940
4092
  ...runtime.projectServers
3941
4093
  ].map(([name, server])=>[
@@ -3996,11 +4148,28 @@ const runBrowserController = async (context, options)=>{
3996
4148
  const reporterResults = [];
3997
4149
  const caseResults = [];
3998
4150
  let fatalError = null;
4151
+ const runnerSinks = new Map(browserProjects.map((project)=>[
4152
+ project.name,
4153
+ createRunnerEventSink(context, project.normalizedConfig)
4154
+ ]));
4155
+ const firstBrowserSink = runnerSinks.get(browserProjects[0].name);
4156
+ const projectNameByTestPath = new Map();
4157
+ const sinkForProjectName = (projectName)=>runnerSinks.get(projectName) ?? firstBrowserSink;
4158
+ const sinkForTestPath = (testPath)=>{
4159
+ const projectName = projectNameByTestPath.get(testPath);
4160
+ return projectName ? sinkForProjectName(projectName) : firstBrowserSink;
4161
+ };
4162
+ const silentConsoleController = createSilentConsoleController({
4163
+ runtimeConfig: {
4164
+ silent: context.normalizedConfig.silent,
4165
+ disableConsoleIntercept: context.normalizedConfig.disableConsoleIntercept
4166
+ },
4167
+ emitInterceptedLog: (log)=>sinkForTestPath(log.testPath).onConsoleLog(log),
4168
+ writeOriginalLog: ()=>{}
4169
+ });
3999
4170
  const snapshotRpcMethods = {
4000
4171
  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);
4172
+ return resolveSnapshotPathDefault(testPath, context.normalizedConfig.resolveSnapshotPath);
4004
4173
  },
4005
4174
  async readSnapshotFile (filepath) {
4006
4175
  try {
@@ -4023,6 +4192,7 @@ const runBrowserController = async (context, options)=>{
4023
4192
  }
4024
4193
  };
4025
4194
  const handleTestFileStart = async (payload)=>{
4195
+ projectNameByTestPath.set(payload.testPath, payload.projectName);
4026
4196
  if (phaseTrackers) {
4027
4197
  const tracker = new PhaseTracker({
4028
4198
  trace: {
@@ -4034,24 +4204,24 @@ const runBrowserController = async (context, options)=>{
4034
4204
  tracker.transition('prepare');
4035
4205
  phaseTrackers.set(payload.testPath, tracker);
4036
4206
  }
4037
- await Promise.all(context.reporters.map((reporter)=>reporter.onTestFileStart?.({
4038
- testId: getFileTaskId(payload.testPath),
4039
- testPath: payload.testPath,
4040
- tests: []
4041
- })));
4207
+ await sinkForProjectName(payload.projectName).onTestFileStart({
4208
+ testId: getFileTaskId(payload.testPath),
4209
+ testPath: payload.testPath,
4210
+ tests: []
4211
+ });
4042
4212
  };
4043
4213
  const handleTestFileReady = async (payload)=>{
4044
4214
  phaseTrackers?.get(payload.testPath)?.transition('tests');
4045
- await Promise.all(context.reporters.map((reporter)=>reporter.onTestFileReady?.(payload)));
4215
+ await sinkForTestPath(payload.testPath).onTestFileReady(payload);
4046
4216
  };
4047
4217
  const handleTestSuiteStart = async (payload)=>{
4048
4218
  phaseTrackers?.get(payload.testPath)?.recordSuiteStart(payload);
4049
- await Promise.all(context.reporters.map((reporter)=>reporter.onTestSuiteStart?.(payload)));
4219
+ await sinkForTestPath(payload.testPath).onTestSuiteStart(payload);
4050
4220
  };
4051
4221
  const handleTestSuiteResult = async (payload)=>{
4052
4222
  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({
4223
+ await sinkForTestPath(payload.testPath).onTestSuiteResult(payload);
4224
+ if ('passed-only' === context.normalizedConfig.silent) silentConsoleController.flushBufferedLogsForTask({
4055
4225
  taskId: payload.testId,
4056
4226
  status: payload.status,
4057
4227
  taskParentNames: payload.parentNames,
@@ -4061,13 +4231,13 @@ const runBrowserController = async (context, options)=>{
4061
4231
  };
4062
4232
  const handleTestCaseStart = async (payload)=>{
4063
4233
  phaseTrackers?.get(payload.testPath)?.recordCaseStart(payload);
4064
- await Promise.all(context.reporters.map((reporter)=>reporter.onTestCaseStart?.(payload)));
4234
+ sinkForTestPath(payload.testPath).onTestCaseStart(payload);
4065
4235
  };
4066
4236
  const handleTestCaseResult = async (payload)=>{
4067
4237
  caseResults.push(payload);
4068
4238
  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({
4239
+ await sinkForTestPath(payload.testPath).onTestCaseResult(payload);
4240
+ if ('passed-only' === context.normalizedConfig.silent) silentConsoleController.flushBufferedLogsForTask({
4071
4241
  taskId: payload.testId,
4072
4242
  status: payload.status,
4073
4243
  taskParentNames: payload.parentNames,
@@ -4080,7 +4250,6 @@ const runBrowserController = async (context, options)=>{
4080
4250
  context.updateReporterResultState([
4081
4251
  payload
4082
4252
  ], payload.results);
4083
- if (payload.snapshotResult) context.snapshotManager.add(payload.snapshotResult);
4084
4253
  if (phaseTrackers) {
4085
4254
  const tracker = phaseTrackers.get(payload.testPath);
4086
4255
  if (tracker) {
@@ -4090,15 +4259,15 @@ const runBrowserController = async (context, options)=>{
4090
4259
  phaseTrackers.delete(payload.testPath);
4091
4260
  }
4092
4261
  }
4093
- if ('passed-only' === context.normalizedConfig.silent) await flushBufferedLogsForTask({
4262
+ if ('passed-only' === context.normalizedConfig.silent) silentConsoleController.flushBufferedLogsForTask({
4094
4263
  taskId: payload.testId,
4095
4264
  status: payload.status,
4096
4265
  taskParentNames: payload.parentNames,
4097
4266
  taskType: 'file',
4098
4267
  testPath: payload.testPath
4099
4268
  });
4100
- await Promise.all(context.reporters.map((reporter)=>reporter.onTestFileResult?.(payload)));
4101
- if ('fail' === payload.status) ensureProcessExitCode(1);
4269
+ await sinkForTestPath(payload.testPath).onTestFileResult(payload);
4270
+ if (isWatchMode && 'fail' === payload.status) ensureProcessExitCode(1);
4102
4271
  };
4103
4272
  const handleLog = async (payload)=>{
4104
4273
  const log = {
@@ -4112,62 +4281,13 @@ const runBrowserController = async (context, options)=>{
4112
4281
  type: payload.type,
4113
4282
  trace: payload.trace
4114
4283
  };
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);
4284
+ silentConsoleController.onConsoleLog(log);
4119
4285
  };
4120
4286
  const handleFatal = async (payload)=>{
4121
4287
  const error = new Error(payload.message);
4122
4288
  error.stack = payload.stack;
4123
4289
  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
- }
4290
+ if (isWatchMode) ensureProcessExitCode(1);
4171
4291
  };
4172
4292
  const runSnapshotRpc = async (request)=>{
4173
4293
  switch(request.method){
@@ -4366,6 +4486,14 @@ const runBrowserController = async (context, options)=>{
4366
4486
  await closeContextSafely(browserContext);
4367
4487
  }
4368
4488
  };
4489
+ const makeSkippedFileResult = (file)=>({
4490
+ testId: getFileTaskId(file.testPath),
4491
+ status: 'skip',
4492
+ name: '',
4493
+ testPath: file.testPath,
4494
+ project: file.projectName,
4495
+ results: []
4496
+ });
4369
4497
  const runFilesWithPool = async (files)=>{
4370
4498
  if (0 === files.length) return;
4371
4499
  const previous = runLifecycle.activeSession;
@@ -4378,8 +4506,17 @@ const runBrowserController = async (context, options)=>{
4378
4506
  ...files
4379
4507
  ];
4380
4508
  const concurrency = getHeadlessConcurrency(context, queue.length);
4509
+ const bail = context.normalizedConfig.bail;
4381
4510
  const worker = async ()=>{
4382
4511
  while(queue.length > 0 && !run.cancelled && runLifecycle.isTokenActive(run.token)){
4512
+ if (bail && context.stateManager.getCountOfFailedTests() >= bail) {
4513
+ let skipped = queue.shift();
4514
+ while(skipped){
4515
+ await handleTestFileComplete(makeSkippedFileResult(skipped));
4516
+ skipped = queue.shift();
4517
+ }
4518
+ return;
4519
+ }
4383
4520
  const next = queue.shift();
4384
4521
  if (!next) return;
4385
4522
  await runSingleFile(run, next);
@@ -4401,6 +4538,7 @@ const runBrowserController = async (context, options)=>{
4401
4538
  await cancelRun(run, false);
4402
4539
  },
4403
4540
  runFiles: async (files)=>{
4541
+ prepareWatchRerunState(context);
4404
4542
  await notifyTestRunStart();
4405
4543
  const rerunStartTime = Date.now();
4406
4544
  const fatalErrorBeforeRun = fatalError;
@@ -4453,12 +4591,12 @@ const runBrowserController = async (context, options)=>{
4453
4591
  hasFailure: false,
4454
4592
  getSourcemap: getBrowserSourcemap,
4455
4593
  resolveSourcemap: resolveBrowserSourcemap,
4456
- close: skipOnTestRunEnd ? async ()=>{
4594
+ close: isWatchMode ? void 0 : async ()=>{
4457
4595
  sessionRegistry.clear();
4458
4596
  await destroyBrowserRuntime(runtime);
4459
- } : void 0
4597
+ }
4460
4598
  };
4461
- if (!skipOnTestRunEnd) await notifyTestRunEnd({
4599
+ if (isWatchMode) await notifyTestRunEnd({
4462
4600
  duration
4463
4601
  });
4464
4602
  if (isWatchMode) {
@@ -4533,7 +4671,7 @@ const runBrowserController = async (context, options)=>{
4533
4671
  };
4534
4672
  context.updateReporterResultState(reporterResults, caseResults);
4535
4673
  const isFailure = reporterResults.some((result)=>'fail' === result.status);
4536
- if (isFailure) ensureProcessExitCode(1);
4674
+ if (isWatchMode && isFailure) ensureProcessExitCode(1);
4537
4675
  const result = {
4538
4676
  results: reporterResults,
4539
4677
  testResults: caseResults,
@@ -4541,9 +4679,9 @@ const runBrowserController = async (context, options)=>{
4541
4679
  hasFailure: isFailure,
4542
4680
  getSourcemap: getBrowserSourcemap,
4543
4681
  resolveSourcemap: resolveBrowserSourcemap,
4544
- close: skipOnTestRunEnd ? closeHeadlessRuntime : void 0
4682
+ close: closeHeadlessRuntime
4545
4683
  };
4546
- if (!skipOnTestRunEnd) try {
4684
+ if (isWatchMode) try {
4547
4685
  await notifyTestRunEnd({
4548
4686
  duration
4549
4687
  });
@@ -4754,7 +4892,7 @@ const runBrowserController = async (context, options)=>{
4754
4892
  }
4755
4893
  } catch (error) {
4756
4894
  fatalError = fatalError ?? toError(error);
4757
- ensureProcessExitCode(1);
4895
+ if (isWatchMode) ensureProcessExitCode(1);
4758
4896
  }
4759
4897
  testTime = Date.now() - testStart;
4760
4898
  }
@@ -4781,6 +4919,7 @@ const runBrowserController = async (context, options)=>{
4781
4919
  }
4782
4920
  if (rerunPlan.normalizedAffectedTestFiles.length > 0) {
4783
4921
  logger.log(color.cyan(`Re-running ${rerunPlan.normalizedAffectedTestFiles.length} affected test file(s)...\n`));
4922
+ prepareWatchRerunState(context);
4784
4923
  await notifyTestRunStart();
4785
4924
  const rerunStartTime = Date.now();
4786
4925
  const fatalErrorBeforeRun = fatalError;
@@ -4831,7 +4970,7 @@ const runBrowserController = async (context, options)=>{
4831
4970
  };
4832
4971
  context.updateReporterResultState(reporterResults, caseResults);
4833
4972
  const isFailure = reporterResults.some((result)=>'fail' === result.status);
4834
- if (isFailure) ensureProcessExitCode(1);
4973
+ if (isWatchMode && isFailure) ensureProcessExitCode(1);
4835
4974
  const result = {
4836
4975
  results: reporterResults,
4837
4976
  testResults: caseResults,
@@ -4839,9 +4978,9 @@ const runBrowserController = async (context, options)=>{
4839
4978
  hasFailure: isFailure,
4840
4979
  getSourcemap: getBrowserSourcemap,
4841
4980
  resolveSourcemap: resolveBrowserSourcemap,
4842
- close: skipOnTestRunEnd ? closeContainerRuntime : void 0
4981
+ close: closeContainerRuntime
4843
4982
  };
4844
- if (!skipOnTestRunEnd) try {
4983
+ if (isWatchMode) try {
4845
4984
  await notifyTestRunEnd({
4846
4985
  duration
4847
4986
  });
@@ -4855,24 +4994,29 @@ const runBrowserController = async (context, options)=>{
4855
4994
  return result;
4856
4995
  };
4857
4996
  const listBrowserTests = async (context, options)=>{
4858
- const projectEntries = await resolveProjectEntries(context, options?.shardedEntries);
4997
+ const browserProjects = options?.projects ?? getBrowserProjects(context);
4998
+ const projectEntries = await resolveProjectEntries(context, options?.shardedEntries, browserProjects);
4859
4999
  const totalTests = projectEntries.reduce((total, item)=>total + item.testFiles.length, 0);
4860
- if (0 === totalTests) return {
5000
+ if (0 === totalTests && !hasUserRstestConfigPlugins(browserProjects)) return {
4861
5001
  list: [],
4862
5002
  close: async ()=>{}
4863
5003
  };
4864
5004
  const tempDir = join(context.rootPath, context.normalizedConfig.output.distPath.root, 'browser', `list-${Date.now()}`);
4865
- const browserProjects = getBrowserProjects(context);
4866
5005
  let runtime;
4867
5006
  try {
4868
5007
  runtime = await createBrowserRuntime({
4869
5008
  context,
4870
5009
  projectEntries,
5010
+ browserProjects,
5011
+ shardedEntries: options?.shardedEntries,
5012
+ freezeShardedEntries: options?.freezeShardedEntries,
4871
5013
  tempDir,
4872
5014
  isWatchMode: false,
4873
5015
  containerDistPath: void 0,
4874
5016
  containerDevServer: void 0,
4875
- forceHeadless: true
5017
+ forceHeadless: true,
5018
+ skipProviderLaunch: options?.filesOnly,
5019
+ appliedModifyRstestConfigEnvironments: options?.appliedModifyRstestConfigEnvironments
4876
5020
  });
4877
5021
  } catch (error) {
4878
5022
  const providers = [
@@ -4881,15 +5025,36 @@ const listBrowserTests = async (context, options)=>{
4881
5025
  logger.error(color.red(`Failed to initialize browser provider runtime (${providers.join(', ')}).`), error);
4882
5026
  throw error;
4883
5027
  }
5028
+ if (options?.filesOnly) {
5029
+ const list = runtime.projectEntries.flatMap((entry)=>entry.testFiles.map((testPath)=>({
5030
+ testPath,
5031
+ project: entry.project.name,
5032
+ tests: []
5033
+ })));
5034
+ await destroyBrowserRuntime(runtime);
5035
+ return {
5036
+ list,
5037
+ close: async ()=>{}
5038
+ };
5039
+ }
5040
+ if (!runtime.projectEntries.some((entry)=>entry.testFiles.length > 0)) {
5041
+ await destroyBrowserRuntime(runtime);
5042
+ return {
5043
+ list: [],
5044
+ close: async ()=>{}
5045
+ };
5046
+ }
4884
5047
  const { browser, browserLaunchOptions } = runtime;
4885
5048
  const projectRuntimeConfigs = browserProjects.map((project)=>({
4886
5049
  name: project.name,
4887
5050
  environmentName: project.environmentName,
4888
5051
  projectRoot: normalize(project.rootPath),
4889
- runtimeConfig: serializableConfig(getRuntimeConfigFromProject(project)),
5052
+ runtimeConfig: serializableConfig(projectRuntimeConfig(project, {
5053
+ envMode: 'static'
5054
+ })),
4890
5055
  viewport: project.normalizedConfig.browser.viewport
4891
5056
  }));
4892
- const maxTestTimeoutForRpc = Math.max(...browserProjects.map((p)=>p.normalizedConfig.testTimeout ?? DEFAULT_TEST_TIMEOUT));
5057
+ const maxTestTimeoutForRpc = getMaxTestTimeoutForRpc(browserProjects);
4893
5058
  const hostOptions = {
4894
5059
  rootPath: normalize(context.rootPath),
4895
5060
  projects: projectRuntimeConfigs,
@@ -4906,6 +5071,7 @@ const listBrowserTests = async (context, options)=>{
4906
5071
  viewport: null
4907
5072
  });
4908
5073
  const serializedOptions = serializeForInlineScript(hostOptions);
5074
+ const collectTimeoutMs = 30000;
4909
5075
  const collectFromServer = async (server)=>{
4910
5076
  const results = [];
4911
5077
  let error = null;
@@ -4950,13 +5116,12 @@ const listBrowserTests = async (context, options)=>{
4950
5116
  await page.goto(`http://localhost:${server.port}/runner.html`, {
4951
5117
  waitUntil: 'load'
4952
5118
  });
4953
- const timeoutMs = 30000;
4954
5119
  let timeoutId;
4955
5120
  const timeoutPromise = new Promise((resolve)=>{
4956
5121
  timeoutId = setTimeout(()=>{
4957
- if (!collectCompleted) logger.warn(color.yellow(`[List] Browser test collection timed out after ${timeoutMs}ms`));
5122
+ if (!collectCompleted) logger.warn(color.yellow(`[List] Browser test collection timed out after ${collectTimeoutMs}ms`));
4958
5123
  resolve();
4959
- }, timeoutMs);
5124
+ }, collectTimeoutMs);
4960
5125
  });
4961
5126
  await Promise.race([
4962
5127
  collectPromise,
@@ -5006,10 +5171,92 @@ const listBrowserTests = async (context, options)=>{
5006
5171
  close: cleanup
5007
5172
  };
5008
5173
  };
5174
+ const emptyOutcome = ()=>({
5175
+ results: [],
5176
+ testResults: [],
5177
+ errors: [],
5178
+ testPaths: [],
5179
+ duration: {
5180
+ buildTime: 0,
5181
+ testTime: 0
5182
+ }
5183
+ });
5184
+ async function createBrowserExecutor(context, options) {
5185
+ const { projects, coverageProvider, freezeShardedEntries, filesOnly, allowEmptyRun, appliedModifyRstestConfigEnvironments } = options;
5186
+ let deferredClose;
5187
+ let inFlightCycle;
5188
+ const foldOutcome = (result)=>{
5189
+ if (!result) return emptyOutcome();
5190
+ const map = buildBrowserCoverageMap(result.results, coverageProvider);
5191
+ return {
5192
+ results: result.results,
5193
+ testResults: result.testResults,
5194
+ errors: result.unhandledErrors ?? [],
5195
+ testPaths: result.results.map((r)=>r.testPath),
5196
+ duration: {
5197
+ buildTime: result.duration.buildTime,
5198
+ testTime: result.duration.testTime
5199
+ },
5200
+ coverage: {
5201
+ map: map?.toJSON()
5202
+ },
5203
+ resolveSourcemap: result.resolveSourcemap
5204
+ };
5205
+ };
5206
+ return {
5207
+ name: 'browser',
5208
+ projects,
5209
+ async init () {},
5210
+ async runCycle (opts) {
5211
+ const cycle = runBrowserController(context, {
5212
+ projects,
5213
+ shardedEntries: opts.shardedEntries,
5214
+ freezeShardedEntries,
5215
+ allowEmptyRun,
5216
+ appliedModifyRstestConfigEnvironments,
5217
+ onTraceEvents: opts.onTraceEvents,
5218
+ env: opts.env
5219
+ });
5220
+ inFlightCycle = cycle;
5221
+ try {
5222
+ const result = await cycle;
5223
+ deferredClose = result?.close;
5224
+ return foldOutcome(result);
5225
+ } finally{
5226
+ inFlightCycle = void 0;
5227
+ }
5228
+ },
5229
+ async collect (opts) {
5230
+ const pending = listBrowserTests(context, {
5231
+ projects,
5232
+ shardedEntries: opts.shardedEntries,
5233
+ freezeShardedEntries,
5234
+ filesOnly,
5235
+ appliedModifyRstestConfigEnvironments
5236
+ });
5237
+ inFlightCycle = pending;
5238
+ try {
5239
+ const { list, close } = await pending;
5240
+ deferredClose = close;
5241
+ return {
5242
+ list
5243
+ };
5244
+ } finally{
5245
+ inFlightCycle = void 0;
5246
+ }
5247
+ },
5248
+ async close () {
5249
+ if (inFlightCycle) await inFlightCycle.catch(()=>void 0);
5250
+ const close = deferredClose;
5251
+ deferredClose = void 0;
5252
+ await close?.();
5253
+ }
5254
+ };
5255
+ }
5009
5256
  async function runBrowserTests(context, options) {
5010
5257
  return runBrowserController(context, options);
5011
5258
  }
5012
5259
  async function src_listBrowserTests(context, options) {
5013
5260
  return listBrowserTests(context, options);
5014
5261
  }
5015
- export { runBrowserTests, src_listBrowserTests as listBrowserTests, validateBrowserConfig };
5262
+ export { createBrowserExecutor, runBrowserTests, src_listBrowserTests as listBrowserTests, validateBrowserConfig };