@rstest/browser 0.10.6 → 0.11.1

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
@@ -9,11 +9,11 @@ import sirv from "sirv";
9
9
  import { WebSocketServer } from "ws";
10
10
  import convert_source_map from "convert-source-map";
11
11
  import { __webpack_require__ } from "./rslib-runtime.js";
12
- import { DISPATCH_RPC_BRIDGE_NAME, DISPATCH_NAMESPACE_SNAPSHOT, DISPATCH_MESSAGE_TYPE, DISPATCH_NAMESPACE_BROWSER } from "./626.js";
12
+ import { DISPATCH_RPC_BRIDGE_NAME, DISPATCH_NAMESPACE_SNAPSHOT, DISPATCH_MESSAGE_TYPE, DISPATCH_NAMESPACE_BROWSER } from "./33.js";
13
13
  __webpack_require__.add({
14
- "../../node_modules/.pnpm/picomatch@4.0.4/node_modules/picomatch/index.js" (module, __unused_rspack_exports, __webpack_require__) {
15
- const pico = __webpack_require__("../../node_modules/.pnpm/picomatch@4.0.4/node_modules/picomatch/lib/picomatch.js");
16
- const utils = __webpack_require__("../../node_modules/.pnpm/picomatch@4.0.4/node_modules/picomatch/lib/utils.js");
14
+ "../../node_modules/.pnpm/picomatch@4.0.5/node_modules/picomatch/index.js" (module, __unused_rspack_exports, __webpack_require__) {
15
+ const pico = __webpack_require__("../../node_modules/.pnpm/picomatch@4.0.5/node_modules/picomatch/lib/picomatch.js");
16
+ const utils = __webpack_require__("../../node_modules/.pnpm/picomatch@4.0.5/node_modules/picomatch/lib/utils.js");
17
17
  function picomatch(glob, options, returnState = false) {
18
18
  if (options && (null === options.windows || void 0 === options.windows)) options = {
19
19
  ...options,
@@ -24,7 +24,7 @@ __webpack_require__.add({
24
24
  Object.assign(picomatch, pico);
25
25
  module.exports = picomatch;
26
26
  },
27
- "../../node_modules/.pnpm/picomatch@4.0.4/node_modules/picomatch/lib/constants.js" (module) {
27
+ "../../node_modules/.pnpm/picomatch@4.0.5/node_modules/picomatch/lib/constants.js" (module) {
28
28
  const WIN_SLASH = '\\\\/';
29
29
  const WIN_NO_SLASH = `[^${WIN_SLASH}]`;
30
30
  const DEFAULT_MAX_EXTGLOB_RECURSION = 0;
@@ -187,9 +187,9 @@ __webpack_require__.add({
187
187
  }
188
188
  };
189
189
  },
190
- "../../node_modules/.pnpm/picomatch@4.0.4/node_modules/picomatch/lib/parse.js" (module, __unused_rspack_exports, __webpack_require__) {
191
- const constants = __webpack_require__("../../node_modules/.pnpm/picomatch@4.0.4/node_modules/picomatch/lib/constants.js");
192
- const utils = __webpack_require__("../../node_modules/.pnpm/picomatch@4.0.4/node_modules/picomatch/lib/utils.js");
190
+ "../../node_modules/.pnpm/picomatch@4.0.5/node_modules/picomatch/lib/parse.js" (module, __unused_rspack_exports, __webpack_require__) {
191
+ const constants = __webpack_require__("../../node_modules/.pnpm/picomatch@4.0.5/node_modules/picomatch/lib/constants.js");
192
+ const utils = __webpack_require__("../../node_modules/.pnpm/picomatch@4.0.5/node_modules/picomatch/lib/utils.js");
193
193
  const { MAX_LENGTH, POSIX_REGEX_SOURCE, REGEX_NON_SPECIAL_CHARS, REGEX_SPECIAL_CHARS_BACKREF, REPLACEMENTS } = constants;
194
194
  const expandRange = (args, options)=>{
195
195
  if ('function' == typeof options.expandRange) return options.expandRange(...args, options);
@@ -333,7 +333,11 @@ __webpack_require__.add({
333
333
  }
334
334
  }
335
335
  };
336
- const getStarExtglobSequenceOutput = (pattern)=>{
336
+ const buildCharClassStar = (chars)=>{
337
+ const source = 1 === chars.length ? utils.escapeRegex(chars[0]) : `[${chars.map((ch)=>utils.escapeRegex(ch)).join('')}]`;
338
+ return `${source}*`;
339
+ };
340
+ const getStarExtglobSequenceChars = (pattern)=>{
337
341
  let index = 0;
338
342
  const chars = [];
339
343
  while(index < pattern.length){
@@ -347,8 +351,7 @@ __webpack_require__.add({
347
351
  index += match.end + 1;
348
352
  }
349
353
  if (chars.length < 1) return;
350
- const source = 1 === chars.length ? utils.escapeRegex(chars[0]) : `[${chars.map((ch)=>utils.escapeRegex(ch)).join('')}]`;
351
- return `${source}*`;
354
+ return chars;
352
355
  };
353
356
  const repeatedExtglobRecursion = (pattern)=>{
354
357
  let depth = 0;
@@ -372,16 +375,34 @@ __webpack_require__.add({
372
375
  risky: true
373
376
  };
374
377
  }
378
+ const safeChars = [];
379
+ let sawStarSequence = false;
380
+ let combinable = true;
375
381
  for (const branch of branches){
376
- const safeOutput = getStarExtglobSequenceOutput(branch);
377
- if (safeOutput) return {
378
- risky: true,
379
- safeOutput
380
- };
382
+ const chars = getStarExtglobSequenceChars(branch);
383
+ if (chars) {
384
+ sawStarSequence = true;
385
+ safeChars.push(...chars);
386
+ continue;
387
+ }
388
+ const literal = normalizeSimpleBranch(branch);
389
+ if (literal && 1 === literal.length) {
390
+ safeChars.push(literal);
391
+ continue;
392
+ }
393
+ combinable = false;
381
394
  if (repeatedExtglobRecursion(branch) > max) return {
382
395
  risky: true
383
396
  };
384
397
  }
398
+ if (sawStarSequence) return combinable ? {
399
+ risky: true,
400
+ safeOutput: buildCharClassStar([
401
+ ...new Set(safeChars)
402
+ ])
403
+ } : {
404
+ risky: true
405
+ };
385
406
  return {
386
407
  risky: false
387
408
  };
@@ -1185,11 +1206,11 @@ __webpack_require__.add({
1185
1206
  };
1186
1207
  module.exports = parse;
1187
1208
  },
1188
- "../../node_modules/.pnpm/picomatch@4.0.4/node_modules/picomatch/lib/picomatch.js" (module, __unused_rspack_exports, __webpack_require__) {
1189
- const scan = __webpack_require__("../../node_modules/.pnpm/picomatch@4.0.4/node_modules/picomatch/lib/scan.js");
1190
- const parse = __webpack_require__("../../node_modules/.pnpm/picomatch@4.0.4/node_modules/picomatch/lib/parse.js");
1191
- const utils = __webpack_require__("../../node_modules/.pnpm/picomatch@4.0.4/node_modules/picomatch/lib/utils.js");
1192
- const constants = __webpack_require__("../../node_modules/.pnpm/picomatch@4.0.4/node_modules/picomatch/lib/constants.js");
1209
+ "../../node_modules/.pnpm/picomatch@4.0.5/node_modules/picomatch/lib/picomatch.js" (module, __unused_rspack_exports, __webpack_require__) {
1210
+ const scan = __webpack_require__("../../node_modules/.pnpm/picomatch@4.0.5/node_modules/picomatch/lib/scan.js");
1211
+ const parse = __webpack_require__("../../node_modules/.pnpm/picomatch@4.0.5/node_modules/picomatch/lib/parse.js");
1212
+ const utils = __webpack_require__("../../node_modules/.pnpm/picomatch@4.0.5/node_modules/picomatch/lib/utils.js");
1213
+ const constants = __webpack_require__("../../node_modules/.pnpm/picomatch@4.0.5/node_modules/picomatch/lib/constants.js");
1193
1214
  const isObject = (val)=>val && 'object' == typeof val && !Array.isArray(val);
1194
1215
  const picomatch = (glob, options, returnState = false)=>{
1195
1216
  if (Array.isArray(glob)) {
@@ -1272,9 +1293,11 @@ __webpack_require__.add({
1272
1293
  output
1273
1294
  };
1274
1295
  };
1275
- picomatch.matchBase = (input, glob, options)=>{
1296
+ picomatch.matchBase = (input, glob, options, posix = options && options.windows)=>{
1276
1297
  const regex = glob instanceof RegExp ? glob : picomatch.makeRe(glob, options);
1277
- return regex.test(utils.basename(input));
1298
+ return regex.test(utils.basename(input, {
1299
+ windows: posix
1300
+ }));
1278
1301
  };
1279
1302
  picomatch.isMatch = (str, patterns, options)=>picomatch(patterns, options)(str);
1280
1303
  picomatch.parse = (pattern, options)=>{
@@ -1318,9 +1341,9 @@ __webpack_require__.add({
1318
1341
  picomatch.constants = constants;
1319
1342
  module.exports = picomatch;
1320
1343
  },
1321
- "../../node_modules/.pnpm/picomatch@4.0.4/node_modules/picomatch/lib/scan.js" (module, __unused_rspack_exports, __webpack_require__) {
1322
- const utils = __webpack_require__("../../node_modules/.pnpm/picomatch@4.0.4/node_modules/picomatch/lib/utils.js");
1323
- const { CHAR_ASTERISK, CHAR_AT, CHAR_BACKWARD_SLASH, CHAR_COMMA, CHAR_DOT, CHAR_EXCLAMATION_MARK, CHAR_FORWARD_SLASH, CHAR_LEFT_CURLY_BRACE, CHAR_LEFT_PARENTHESES, CHAR_LEFT_SQUARE_BRACKET, CHAR_PLUS, CHAR_QUESTION_MARK, CHAR_RIGHT_CURLY_BRACE, CHAR_RIGHT_PARENTHESES, CHAR_RIGHT_SQUARE_BRACKET } = __webpack_require__("../../node_modules/.pnpm/picomatch@4.0.4/node_modules/picomatch/lib/constants.js");
1344
+ "../../node_modules/.pnpm/picomatch@4.0.5/node_modules/picomatch/lib/scan.js" (module, __unused_rspack_exports, __webpack_require__) {
1345
+ const utils = __webpack_require__("../../node_modules/.pnpm/picomatch@4.0.5/node_modules/picomatch/lib/utils.js");
1346
+ const { CHAR_ASTERISK, CHAR_AT, CHAR_BACKWARD_SLASH, CHAR_COMMA, CHAR_DOT, CHAR_EXCLAMATION_MARK, CHAR_FORWARD_SLASH, CHAR_LEFT_CURLY_BRACE, CHAR_LEFT_PARENTHESES, CHAR_LEFT_SQUARE_BRACKET, CHAR_PLUS, CHAR_QUESTION_MARK, CHAR_RIGHT_CURLY_BRACE, CHAR_RIGHT_PARENTHESES, CHAR_RIGHT_SQUARE_BRACKET } = __webpack_require__("../../node_modules/.pnpm/picomatch@4.0.5/node_modules/picomatch/lib/constants.js");
1324
1347
  const isPathSeparator = (code)=>code === CHAR_FORWARD_SLASH || code === CHAR_BACKWARD_SLASH;
1325
1348
  const depth = (token)=>{
1326
1349
  if (true !== token.isPrefix) token.depth = token.isGlobstar ? 1 / 0 : 1;
@@ -1586,8 +1609,8 @@ __webpack_require__.add({
1586
1609
  };
1587
1610
  module.exports = scan;
1588
1611
  },
1589
- "../../node_modules/.pnpm/picomatch@4.0.4/node_modules/picomatch/lib/utils.js" (__unused_rspack_module, exports, __webpack_require__) {
1590
- const { REGEX_BACKSLASH, REGEX_REMOVE_BACKSLASH, REGEX_SPECIAL_CHARS, REGEX_SPECIAL_CHARS_GLOBAL } = __webpack_require__("../../node_modules/.pnpm/picomatch@4.0.4/node_modules/picomatch/lib/constants.js");
1612
+ "../../node_modules/.pnpm/picomatch@4.0.5/node_modules/picomatch/lib/utils.js" (__unused_rspack_module, exports, __webpack_require__) {
1613
+ const { REGEX_BACKSLASH, REGEX_REMOVE_BACKSLASH, REGEX_SPECIAL_CHARS, REGEX_SPECIAL_CHARS_GLOBAL } = __webpack_require__("../../node_modules/.pnpm/picomatch@4.0.5/node_modules/picomatch/lib/constants.js");
1591
1614
  exports.isObject = (val)=>null !== val && 'object' == typeof val && !Array.isArray(val);
1592
1615
  exports.hasRegexChars = (str)=>REGEX_SPECIAL_CHARS.test(str);
1593
1616
  exports.isRegexChar = (str)=>1 === str.length && exports.hasRegexChars(str);
@@ -2522,6 +2545,7 @@ const wrapPage = (page)=>{
2522
2545
  if ('popup' === event) return void originalOn(event, (popup)=>{
2523
2546
  listener(wrapPage(popup));
2524
2547
  });
2548
+ if ('crash' === event || 'close' === event) return void originalOn(event, listener);
2525
2549
  originalOn(event, listener);
2526
2550
  };
2527
2551
  return withAsyncDispose(page);
@@ -2810,12 +2834,11 @@ const planWatchRerun = ({ projectEntries, previousTestFiles, affectedTestFiles }
2810
2834
  affectedTestFiles: matchedAffectedFiles
2811
2835
  };
2812
2836
  };
2813
- const picomatch = __webpack_require__("../../node_modules/.pnpm/picomatch@4.0.4/node_modules/picomatch/index.js");
2837
+ const picomatch = __webpack_require__("../../node_modules/.pnpm/picomatch@4.0.5/node_modules/picomatch/index.js");
2814
2838
  var picomatch_default = /*#__PURE__*/ __webpack_require__.n(picomatch);
2815
2839
  const { createRsbuild: createRsbuild, rspack: rspack } = rsbuild;
2816
2840
  const hostController_dirname = dirname(fileURLToPath(import.meta.url));
2817
2841
  const OPTIONS_PLACEHOLDER = '__RSTEST_OPTIONS_PLACEHOLDER__';
2818
- const PER_FILE_TIMEOUT_BUFFER_MS = 30000;
2819
2842
  let nextBrowserFilePid = 1000000000;
2820
2843
  const serializeForInlineScript = (value)=>JSON.stringify(value).replace(/</g, '\\u003c').replace(/\u2028/g, '\\u2028').replace(/\u2029/g, '\\u2029');
2821
2844
  const getBrowserProviderOptions = (project)=>{
@@ -2991,9 +3014,10 @@ const createBrowserLazyCompilationConfig = (setupFiles)=>{
2991
3014
  }
2992
3015
  };
2993
3016
  };
2994
- const createBrowserRsbuildDevConfig = (_isWatchMode)=>({
3017
+ const shouldEnableBrowserHmr = (isWatchMode, isHeadless)=>isWatchMode && !isHeadless;
3018
+ const createBrowserRsbuildDevConfig = (enableHmr)=>({
2995
3019
  writeToDisk: isDebug(),
2996
- hmr: true,
3020
+ hmr: enableHmr,
2997
3021
  client: {
2998
3022
  logLevel: 'error'
2999
3023
  }
@@ -3183,7 +3207,7 @@ const ensureConsistentBrowserLaunchOptions = (projects)=>{
3183
3207
  const firstOptions = getBrowserLaunchOptions(firstProject);
3184
3208
  for (const project of projects.slice(1)){
3185
3209
  const options = getBrowserLaunchOptions(project);
3186
- if (options.provider !== firstOptions.provider || options.browser !== firstOptions.browser || options.headless !== firstOptions.headless || options.port !== firstOptions.port || options.strictPort !== firstOptions.strictPort || !isDeepStrictEqual(options.providerOptions, firstOptions.providerOptions)) throw new Error(`Browser launch config mismatch between projects "${firstProject.name}" and "${project.name}". All browser-enabled projects in one run must share provider/browser/headless/port/strictPort/providerOptions.`);
3210
+ if (options.provider !== firstOptions.provider || options.browser !== firstOptions.browser || options.headless !== firstOptions.headless || !isDeepStrictEqual(options.providerOptions, firstOptions.providerOptions)) throw new Error(`Browser launch config mismatch between projects "${firstProject.name}" and "${project.name}". All browser-enabled projects in one run must share provider/browser/headless/providerOptions.`);
3187
3211
  }
3188
3212
  return firstOptions;
3189
3213
  };
@@ -3230,7 +3254,14 @@ const resolveContainerDist = ()=>{
3230
3254
  throw new Error(`Browser container build not found at ${distPath}. Please run "pnpm --filter @rstest/browser build".`);
3231
3255
  };
3232
3256
  const toSafeVarName = (name)=>name.replace(/[^a-zA-Z0-9_]/g, '_');
3233
- const generateManifestModule = ({ manifestPath, entries })=>{
3257
+ const toContextKey = (filePath, projectRootPosix)=>{
3258
+ const posixPath = normalize(filePath);
3259
+ const withinRoot = posixPath === projectRootPosix || posixPath.startsWith(`${projectRootPosix}/`);
3260
+ if (!withinRoot) return posixPath;
3261
+ const rel = posixPath.slice(projectRootPosix.length);
3262
+ return rel.startsWith('/') ? `.${rel}` : `./${rel}`;
3263
+ };
3264
+ const generateManifestModule = ({ manifestPath, entries, isWatchMode })=>{
3234
3265
  const manifestDirPosix = normalize(dirname(manifestPath));
3235
3266
  const toRelativeImport = (filePath)=>{
3236
3267
  const posixPath = normalize(filePath);
@@ -3263,18 +3294,30 @@ const generateManifestModule = ({ manifestPath, entries })=>{
3263
3294
  lines.push('};');
3264
3295
  lines.push('');
3265
3296
  lines.push('// Test context for each project');
3266
- for (const { project } of entries){
3297
+ for (const { project, testFiles } of entries){
3267
3298
  const varName = `context_${toSafeVarName(project.environmentName)}`;
3268
3299
  const projectRootPosix = normalize(project.rootPath);
3269
- const includeRegExp = globPatternsToRegExp(project.normalizedConfig.include);
3270
- const excludePatterns = project.normalizedConfig.exclude.patterns;
3271
- const excludeRegExp = createBrowserContextExcludeRegExp(excludePatterns, projectRootPosix);
3272
- lines.push(`const ${varName} = import.meta.webpackContext(${JSON.stringify(projectRootPosix)}, {`);
3273
- lines.push(' recursive: true,');
3274
- lines.push(` regExp: ${includeRegExp.toString()},`);
3275
- if (excludeRegExp) lines.push(` exclude: ${excludeRegExp.toString()},`);
3276
- lines.push(" mode: 'lazy',");
3277
- lines.push('});');
3300
+ if (isWatchMode) {
3301
+ const includeRegExp = globPatternsToRegExp(project.normalizedConfig.include);
3302
+ 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('});');
3309
+ } else {
3310
+ lines.push(`const ${varName}_modules = {`);
3311
+ for (const filePath of testFiles){
3312
+ const key = toContextKey(filePath, projectRootPosix);
3313
+ const importPath = toRelativeImport(filePath);
3314
+ lines.push(` ${JSON.stringify(key)}: () => import(${JSON.stringify(importPath)}),`);
3315
+ }
3316
+ lines.push('};');
3317
+ lines.push(`const ${varName} = Object.assign((key) => ${varName}_modules[key](), {`);
3318
+ lines.push(` keys: () => Object.keys(${varName}_modules),`);
3319
+ lines.push('});');
3320
+ }
3278
3321
  lines.push('');
3279
3322
  }
3280
3323
  lines.push('export const projectTestContexts = {');
@@ -3308,13 +3351,21 @@ const htmlTemplate = `<!DOCTYPE html>
3308
3351
  </html>
3309
3352
  `;
3310
3353
  const VIRTUAL_MANIFEST_FILENAME = 'virtual-manifest.ts';
3354
+ const closeAllProjectServers = (servers)=>Promise.allSettled([
3355
+ ...servers
3356
+ ].map((server)=>server.devServer.close()));
3357
+ const copyProxyResponseHeaders = (response, res)=>{
3358
+ res.statusCode = response.status;
3359
+ response.headers.forEach((value, key)=>{
3360
+ if ('content-length' === key.toLowerCase()) return;
3361
+ res.setHeader(key, value);
3362
+ });
3363
+ };
3311
3364
  const destroyBrowserRuntime = async (runtime)=>{
3312
3365
  try {
3313
3366
  await runtime.browser?.close?.();
3314
3367
  } catch {}
3315
- try {
3316
- await runtime.devServer?.close?.();
3317
- } catch {}
3368
+ await closeAllProjectServers(runtime.projectServers.values());
3318
3369
  try {
3319
3370
  runtime.wss?.close();
3320
3371
  } catch {}
@@ -3348,10 +3399,7 @@ const registerWatchCleanup = ()=>{
3348
3399
  });
3349
3400
  watchContext.cleanupRegistered = true;
3350
3401
  };
3351
- const createBrowserRuntime = async ({ context, manifestPath, manifestSource, tempDir, isWatchMode, onTriggerRerun, containerDistPath, containerDevServer, forceHeadless })=>{
3352
- const virtualManifestPlugin = new rspack.experiments.VirtualModulesPlugin({
3353
- [manifestPath]: manifestSource
3354
- });
3402
+ const createBrowserRuntime = async ({ context, projectEntries, tempDir, isWatchMode, onTriggerRerun, containerDistPath, containerDevServer, forceHeadless })=>{
3355
3403
  const containerHtmlTemplate = containerDistPath ? await promises.readFile(join(containerDistPath, 'index.html'), 'utf-8') : null;
3356
3404
  let injectedContainerHtml = null;
3357
3405
  let serializedOptions = 'null';
@@ -3361,156 +3409,14 @@ const createBrowserRuntime = async ({ context, manifestPath, manifestSource, tem
3361
3409
  if (containerHtmlTemplate) injectedContainerHtml = containerHtmlTemplate.replace(OPTIONS_PLACEHOLDER, serializedOptions);
3362
3410
  };
3363
3411
  const browserProjects = getBrowserProjects(context);
3364
- const projectByEnvironmentName = new Map(browserProjects.map((project)=>[
3365
- project.environmentName,
3366
- project
3367
- ]));
3368
- const userPlugins = browserProjects.flatMap((project)=>project.normalizedConfig.plugins || []);
3369
3412
  const browserLaunchOptions = ensureConsistentBrowserLaunchOptions(browserProjects);
3370
3413
  const browserRuntimePath = fileURLToPath(import.meta.resolve('@rstest/core/internal/browser-runtime'));
3371
- const rstestInternalAliases = {
3372
- '@rstest/browser-manifest': manifestPath,
3414
+ const staticRstestAliases = {
3373
3415
  '@rstest/core': resolveBrowserFile('client/public.ts'),
3374
3416
  '@rstest/browser': resolveBrowserFile('browser.ts'),
3375
- '@rstest/core/internal/browser-runtime': browserRuntimePath,
3376
- '@sinonjs/fake-timers': resolveBrowserFile('client/fakeTimersStub.ts')
3417
+ '@rstest/core/internal/browser-runtime': browserRuntimePath
3377
3418
  };
3378
- const rsbuildInstance = await createRsbuild({
3379
- callerName: 'rstest-browser',
3380
- rsbuildConfig: {
3381
- root: context.rootPath,
3382
- mode: 'development',
3383
- plugins: userPlugins,
3384
- server: {
3385
- printUrls: false,
3386
- port: browserLaunchOptions.port ?? 4000,
3387
- strictPort: browserLaunchOptions.strictPort
3388
- },
3389
- dev: createBrowserRsbuildDevConfig(isWatchMode),
3390
- environments: {
3391
- ...Object.fromEntries(browserProjects.map((project)=>[
3392
- project.environmentName,
3393
- {}
3394
- ]))
3395
- }
3396
- }
3397
- });
3398
- rsbuildInstance.addPlugins([
3399
- {
3400
- name: 'rstest:browser-user-config',
3401
- setup (api) {
3402
- api.expose?.('rstest:browser', {
3403
- registerDispatchHandler: (namespace, handler)=>{
3404
- dispatchHandlers.set(namespace, handler);
3405
- }
3406
- });
3407
- api.modifyEnvironmentConfig({
3408
- handler: (config, { mergeEnvironmentConfig, name })=>{
3409
- const project = projectByEnvironmentName.get(name);
3410
- if (!project) return config;
3411
- const userRsbuildConfig = project.normalizedConfig;
3412
- const buildCache = resolveProjectBuildCache({
3413
- context,
3414
- project
3415
- });
3416
- const setupFiles = Object.values(getSetupFiles(project.normalizedConfig.setupFiles, project.rootPath));
3417
- const rstestEnvDefine = `globalThis[Symbol.for(${JSON.stringify(RSTEST_ENV_SYMBOL_KEY)})]`;
3418
- const merged = mergeEnvironmentConfig(config, {
3419
- ...userRsbuildConfig,
3420
- performance: buildCache ? {
3421
- ...userRsbuildConfig.performance,
3422
- buildCache
3423
- } : userRsbuildConfig.performance
3424
- }, {
3425
- resolve: {
3426
- alias: rstestInternalAliases
3427
- },
3428
- source: {
3429
- define: {
3430
- 'process.env': rstestEnvDefine,
3431
- 'import.meta.env': rstestEnvDefine
3432
- }
3433
- },
3434
- output: {
3435
- target: 'web',
3436
- sourceMap: {
3437
- js: 'source-map'
3438
- }
3439
- },
3440
- tools: {
3441
- rspack: (rspackConfig)=>{
3442
- rspackConfig.mode = 'development';
3443
- rspackConfig.lazyCompilation = createBrowserLazyCompilationConfig(setupFiles);
3444
- rspackConfig.plugins = rspackConfig.plugins || [];
3445
- rspackConfig.plugins.push(virtualManifestPlugin);
3446
- applyDefaultWatchOptions(rspackConfig, isWatchMode);
3447
- const browserRuntimeDir = dirname(browserRuntimePath);
3448
- rspackConfig.module = rspackConfig.module || {};
3449
- rspackConfig.module.rules = rspackConfig.module.rules || [];
3450
- rspackConfig.module.rules.unshift({
3451
- test: /\.js$/,
3452
- include: browserRuntimeDir,
3453
- extractSourceMap: true
3454
- });
3455
- if (isDebug()) logger.log(`[rstest:browser] extractSourceMap rule added for: ${browserRuntimeDir}`);
3456
- }
3457
- }
3458
- });
3459
- merged.source = merged.source || {};
3460
- merged.source.entry = {
3461
- runner: resolveBrowserFile('client/entry.ts')
3462
- };
3463
- return merged;
3464
- },
3465
- order: 'post'
3466
- });
3467
- }
3468
- }
3469
- ]);
3470
- if (isWatchMode && onTriggerRerun) rsbuildInstance.addPlugins([
3471
- {
3472
- name: 'rstest:browser-watch',
3473
- setup (api) {
3474
- api.onBeforeDevCompile(()=>{
3475
- if (!watchContext.hooksEnabled) return;
3476
- logger.log(color.cyan('\nFile changed, re-running tests...\n'));
3477
- });
3478
- api.onAfterDevCompile(async ({ stats })=>{
3479
- if (stats) {
3480
- const projectEntries = await collectProjectEntries(context);
3481
- const entryTestFiles = new Set(collectWatchTestFiles(projectEntries).map((file)=>file.testPath));
3482
- const statsJson = stats.toJson({
3483
- all: true
3484
- });
3485
- const affected = getAffectedTestFiles(statsJson.chunks, entryTestFiles);
3486
- watchContext.affectedTestFiles = affected;
3487
- if (affected.length > 0) logger.debug(`[Watch] Affected test files: ${affected.join(', ')}`);
3488
- }
3489
- if (!watchContext.hooksEnabled) return;
3490
- await onTriggerRerun();
3491
- });
3492
- }
3493
- }
3494
- ]);
3495
- const coverage = browserProjects.find((project)=>project.normalizedConfig.coverage?.enabled)?.normalizedConfig.coverage;
3496
- if (coverage?.enabled && 'list' !== context.command) {
3497
- const { pluginCoverage } = await loadCoverageProvider(coverage, context.rootPath);
3498
- rsbuildInstance.addPlugins([
3499
- pluginCoverage(coverage)
3500
- ]);
3501
- }
3502
- const devServer = await rsbuildInstance.createDevServer({
3503
- getPortSilently: true
3504
- });
3505
- if (isDebug()) await rsbuildInstance.inspectConfig({
3506
- writeToDisk: true,
3507
- extraConfigs: {
3508
- rstest: {
3509
- ...context.normalizedConfig,
3510
- projects: browserProjects.map((p)=>p.normalizedConfig)
3511
- }
3512
- }
3513
- });
3419
+ const rstestEnvDefine = `globalThis[Symbol.for(${JSON.stringify(RSTEST_ENV_SYMBOL_KEY)})]`;
3514
3420
  const serveContainer = containerDistPath ? sirv(containerDistPath, {
3515
3421
  dev: false,
3516
3422
  single: 'index.html'
@@ -3524,11 +3430,7 @@ const createBrowserRuntime = async ({ context, manifestPath, manifestSource, tem
3524
3430
  if (!response.ok) return false;
3525
3431
  let html = await response.text();
3526
3432
  html = html.replace(OPTIONS_PLACEHOLDER, serializedOptions);
3527
- res.statusCode = response.status;
3528
- response.headers.forEach((value, key)=>{
3529
- if ('content-length' === key.toLowerCase()) return;
3530
- res.setHeader(key, value);
3531
- });
3433
+ copyProxyResponseHeaders(response, res);
3532
3434
  res.setHeader('Content-Type', 'text/html');
3533
3435
  res.end(html);
3534
3436
  return true;
@@ -3544,11 +3446,7 @@ const createBrowserRuntime = async ({ context, manifestPath, manifestSource, tem
3544
3446
  const response = await fetch(target);
3545
3447
  if (!response.ok) return false;
3546
3448
  const buffer = Buffer.from(await response.arrayBuffer());
3547
- res.statusCode = response.status;
3548
- response.headers.forEach((value, key)=>{
3549
- if ('content-length' === key.toLowerCase()) return;
3550
- res.setHeader(key, value);
3551
- });
3449
+ copyProxyResponseHeaders(response, res);
3552
3450
  res.end(buffer);
3553
3451
  return true;
3554
3452
  } catch (error) {
@@ -3556,59 +3454,241 @@ const createBrowserRuntime = async ({ context, manifestPath, manifestSource, tem
3556
3454
  return false;
3557
3455
  }
3558
3456
  };
3559
- devServer.middlewares.use(async (req, res, next)=>{
3560
- if (!req.url) return void next();
3561
- const url = new URL(req.url, 'http://localhost');
3562
- if ('/__open-in-editor' === url.pathname) {
3563
- const file = url.searchParams.get('file');
3564
- if (!file) {
3565
- res.statusCode = 400;
3566
- res.end('Missing file');
3457
+ const entryByEnvironmentName = new Map(projectEntries.map((entry)=>[
3458
+ entry.project.environmentName,
3459
+ entry
3460
+ ]));
3461
+ const buildProjectServer = async (project, isContainerServer)=>{
3462
+ const manifestPath = join(tempDir, toSafeVarName(project.environmentName), VIRTUAL_MANIFEST_FILENAME);
3463
+ const entry = entryByEnvironmentName.get(project.environmentName);
3464
+ const manifestSource = generateManifestModule({
3465
+ 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
3477
+ });
3478
+ const rstestInternalAliases = {
3479
+ '@rstest/browser-manifest': manifestPath,
3480
+ ...staticRstestAliases
3481
+ };
3482
+ const isHeadless = forceHeadless || project.normalizedConfig.browser.headless;
3483
+ const enableHmr = shouldEnableBrowserHmr(isWatchMode, isHeadless);
3484
+ const rsbuildInstance = await createRsbuild({
3485
+ callerName: 'rstest-browser',
3486
+ rsbuildConfig: {
3487
+ root: context.rootPath,
3488
+ mode: 'development',
3489
+ plugins: project.normalizedConfig.plugins || [],
3490
+ server: {
3491
+ printUrls: false,
3492
+ port: project.normalizedConfig.browser.port ?? (isContainerServer ? 4000 : 0),
3493
+ strictPort: project.normalizedConfig.browser.strictPort
3494
+ },
3495
+ dev: createBrowserRsbuildDevConfig(enableHmr),
3496
+ environments: {
3497
+ [project.environmentName]: {}
3498
+ }
3499
+ }
3500
+ });
3501
+ rsbuildInstance.addPlugins([
3502
+ {
3503
+ name: 'rstest:browser-user-config',
3504
+ setup (api) {
3505
+ api.expose?.('rstest:browser', {
3506
+ registerDispatchHandler: (namespace, handler)=>{
3507
+ dispatchHandlers.set(namespace, handler);
3508
+ }
3509
+ });
3510
+ api.modifyEnvironmentConfig({
3511
+ handler: (config, { mergeEnvironmentConfig, name })=>{
3512
+ if (name !== project.environmentName) return config;
3513
+ const userRsbuildConfig = project.normalizedConfig;
3514
+ const buildCache = resolveProjectBuildCache({
3515
+ context,
3516
+ project
3517
+ });
3518
+ const setupFiles = Object.values(getSetupFiles(project.normalizedConfig.setupFiles, project.rootPath));
3519
+ const merged = mergeEnvironmentConfig(config, {
3520
+ ...userRsbuildConfig,
3521
+ performance: buildCache ? {
3522
+ ...userRsbuildConfig.performance,
3523
+ buildCache
3524
+ } : userRsbuildConfig.performance
3525
+ }, {
3526
+ resolve: {
3527
+ alias: rstestInternalAliases
3528
+ },
3529
+ source: {
3530
+ define: {
3531
+ 'process.env': rstestEnvDefine,
3532
+ 'import.meta.env': rstestEnvDefine
3533
+ }
3534
+ },
3535
+ output: {
3536
+ target: 'web',
3537
+ sourceMap: {
3538
+ js: 'source-map'
3539
+ }
3540
+ },
3541
+ tools: {
3542
+ rspack: (rspackConfig)=>{
3543
+ rspackConfig.mode = 'development';
3544
+ rspackConfig.lazyCompilation = enableHmr ? createBrowserLazyCompilationConfig(setupFiles) : false;
3545
+ rspackConfig.plugins = rspackConfig.plugins || [];
3546
+ rspackConfig.plugins.push(virtualManifestPlugin);
3547
+ applyDefaultWatchOptions(rspackConfig, isWatchMode);
3548
+ const browserRuntimeDir = dirname(browserRuntimePath);
3549
+ rspackConfig.module = rspackConfig.module || {};
3550
+ rspackConfig.module.rules = rspackConfig.module.rules || [];
3551
+ rspackConfig.module.rules.unshift({
3552
+ test: /\.js$/,
3553
+ include: browserRuntimeDir,
3554
+ extractSourceMap: true
3555
+ });
3556
+ if (isDebug()) logger.log(`[rstest:browser] extractSourceMap rule added for: ${browserRuntimeDir}`);
3557
+ }
3558
+ }
3559
+ });
3560
+ merged.source = merged.source || {};
3561
+ merged.source.entry = {
3562
+ runner: resolveBrowserFile('client/entry.ts')
3563
+ };
3564
+ return merged;
3565
+ },
3566
+ order: 'post'
3567
+ });
3568
+ }
3569
+ }
3570
+ ]);
3571
+ if (isWatchMode && onTriggerRerun) rsbuildInstance.addPlugins([
3572
+ {
3573
+ name: 'rstest:browser-watch',
3574
+ setup (api) {
3575
+ api.onBeforeDevCompile(()=>{
3576
+ if (!watchContext.hooksEnabled) return;
3577
+ logger.log(color.cyan('\nFile changed, re-running tests...\n'));
3578
+ });
3579
+ api.onAfterDevCompile(async ({ stats })=>{
3580
+ if (stats) {
3581
+ const allProjectEntries = await collectProjectEntries(context);
3582
+ const entryTestFiles = new Set(collectWatchTestFiles(allProjectEntries).map((file)=>file.testPath));
3583
+ const statsJson = stats.toJson({
3584
+ all: true
3585
+ });
3586
+ const affected = getAffectedTestFiles(statsJson.chunks, entryTestFiles);
3587
+ watchContext.affectedTestFiles = affected;
3588
+ if (affected.length > 0) logger.debug(`[Watch] Affected test files: ${affected.join(', ')}`);
3589
+ }
3590
+ if (!watchContext.hooksEnabled) return;
3591
+ await onTriggerRerun();
3592
+ });
3593
+ }
3594
+ }
3595
+ ]);
3596
+ const coverage = project.normalizedConfig.coverage;
3597
+ if (coverage?.enabled && 'list' !== context.command) {
3598
+ const { pluginCoverage } = await loadCoverageProvider(coverage, context.rootPath);
3599
+ rsbuildInstance.addPlugins([
3600
+ pluginCoverage(coverage)
3601
+ ]);
3602
+ }
3603
+ const devServer = await rsbuildInstance.createDevServer({
3604
+ getPortSilently: true
3605
+ });
3606
+ if (isDebug()) await rsbuildInstance.inspectConfig({
3607
+ writeToDisk: true,
3608
+ extraConfigs: {
3609
+ rstest: {
3610
+ ...context.normalizedConfig,
3611
+ projects: [
3612
+ project.normalizedConfig
3613
+ ]
3614
+ }
3615
+ }
3616
+ });
3617
+ devServer.middlewares.use(async (req, res, next)=>{
3618
+ if (!req.url) return void next();
3619
+ const url = new URL(req.url, 'http://localhost');
3620
+ if ('/__open-in-editor' === url.pathname) {
3621
+ const file = url.searchParams.get('file');
3622
+ if (!file) {
3623
+ res.statusCode = 400;
3624
+ res.end('Missing file');
3625
+ return;
3626
+ }
3627
+ try {
3628
+ await open_editor([
3629
+ {
3630
+ file
3631
+ }
3632
+ ]);
3633
+ res.statusCode = 204;
3634
+ res.end();
3635
+ } catch (error) {
3636
+ logger.debug(`[Browser UI] Failed to open editor: ${String(error)}`);
3637
+ res.statusCode = 500;
3638
+ res.end('Failed to open editor');
3639
+ }
3567
3640
  return;
3568
3641
  }
3569
- try {
3570
- await open_editor([
3571
- {
3572
- file
3642
+ if (isContainerServer) {
3643
+ if ('/' === url.pathname) {
3644
+ if (await respondWithDevServerHtml(url, res)) return;
3645
+ const html = injectedContainerHtml || containerHtmlTemplate?.replace(OPTIONS_PLACEHOLDER, 'null');
3646
+ if (html) {
3647
+ res.setHeader('Content-Type', 'text/html');
3648
+ res.end(html);
3649
+ return;
3573
3650
  }
3574
- ]);
3575
- res.statusCode = 204;
3576
- res.end();
3577
- } catch (error) {
3578
- logger.debug(`[Browser UI] Failed to open editor: ${String(error)}`);
3579
- res.statusCode = 500;
3580
- res.end('Failed to open editor');
3651
+ res.statusCode = 502;
3652
+ res.end('Container UI is not available.');
3653
+ return;
3654
+ }
3655
+ if (url.pathname.startsWith('/container-static/')) {
3656
+ if (await proxyDevServerAsset(req, res)) return;
3657
+ if (serveContainer) return void serveContainer(req, res, next);
3658
+ res.statusCode = 502;
3659
+ res.end('Container assets are not available.');
3660
+ return;
3661
+ }
3581
3662
  }
3582
- return;
3583
- }
3584
- if ('/' === url.pathname) {
3585
- if (await respondWithDevServerHtml(url, res)) return;
3586
- const html = injectedContainerHtml || containerHtmlTemplate?.replace(OPTIONS_PLACEHOLDER, 'null');
3587
- if (html) {
3663
+ if ('/runner.html' === url.pathname) {
3588
3664
  res.setHeader('Content-Type', 'text/html');
3589
- res.end(html);
3665
+ res.end(htmlTemplate);
3590
3666
  return;
3591
3667
  }
3592
- res.statusCode = 502;
3593
- res.end('Container UI is not available.');
3594
- return;
3595
- }
3596
- if (url.pathname.startsWith('/container-static/')) {
3597
- if (await proxyDevServerAsset(req, res)) return;
3598
- if (serveContainer) return void serveContainer(req, res, next);
3599
- res.statusCode = 502;
3600
- res.end('Container assets are not available.');
3601
- return;
3602
- }
3603
- if ('/runner.html' === url.pathname) {
3604
- res.setHeader('Content-Type', 'text/html');
3605
- res.end(htmlTemplate);
3606
- return;
3668
+ next();
3669
+ });
3670
+ const { port: listenPort } = await devServer.listen();
3671
+ const port = resolveListenPort(listenPort, devServer.httpServer);
3672
+ return {
3673
+ projectName: project.name,
3674
+ environmentName: project.environmentName,
3675
+ rsbuildInstance,
3676
+ devServer,
3677
+ port,
3678
+ manifestPath
3679
+ };
3680
+ };
3681
+ const projectServers = new Map();
3682
+ try {
3683
+ for (const [index, project] of browserProjects.entries()){
3684
+ const server = await buildProjectServer(project, 0 === index);
3685
+ projectServers.set(server.projectName, server);
3607
3686
  }
3608
- next();
3609
- });
3610
- const { port: listenPort } = await devServer.listen();
3611
- const port = resolveListenPort(listenPort, devServer.httpServer);
3687
+ } catch (error) {
3688
+ await closeAllProjectServers(projectServers.values());
3689
+ throw error;
3690
+ }
3691
+ const containerServer = projectServers.get(browserProjects[0].name);
3612
3692
  const wss = new WebSocketServer({
3613
3693
  port: 0
3614
3694
  });
@@ -3627,22 +3707,19 @@ const createBrowserRuntime = async ({ context, manifestPath, manifestSource, tem
3627
3707
  providerOptions: browserLaunchOptions.providerOptions
3628
3708
  });
3629
3709
  return {
3630
- rsbuildInstance,
3631
- devServer,
3710
+ projectServers,
3711
+ containerServer,
3632
3712
  browser: runtime.browser,
3633
3713
  browserLaunchOptions,
3634
- port,
3635
3714
  wsPort,
3636
- manifestPath,
3637
3715
  tempDir,
3638
- manifestPlugin: virtualManifestPlugin,
3639
3716
  setContainerOptions,
3640
3717
  dispatchHandlers,
3641
3718
  wss
3642
3719
  };
3643
3720
  } catch (error) {
3644
3721
  wss.close();
3645
- await devServer.close();
3722
+ await closeAllProjectServers(projectServers.values());
3646
3723
  throw error;
3647
3724
  }
3648
3725
  };
@@ -3813,11 +3890,6 @@ const runBrowserController = async (context, options)=>{
3813
3890
  const enableCliShortcuts = isWatchMode && isBrowserWatchCliShortcutsEnabled();
3814
3891
  const browserTempOutputRoot = context.normalizedConfig.output.distPath.root;
3815
3892
  const tempDir = isWatchMode && watchContext.runtime ? watchContext.runtime.tempDir : isWatchMode ? join(context.rootPath, browserTempOutputRoot, 'browser', 'watch') : join(context.rootPath, browserTempOutputRoot, 'browser', Date.now().toString());
3816
- const manifestPath = join(tempDir, VIRTUAL_MANIFEST_FILENAME);
3817
- const manifestSource = generateManifestModule({
3818
- manifestPath,
3819
- entries: projectEntries
3820
- });
3821
3893
  if (isWatchMode) watchContext.lastTestFiles = collectWatchTestFiles(projectEntries);
3822
3894
  let runtime = isWatchMode ? watchContext.runtime : null;
3823
3895
  let triggerRerun;
@@ -3825,8 +3897,7 @@ const runBrowserController = async (context, options)=>{
3825
3897
  try {
3826
3898
  runtime = await createBrowserRuntime({
3827
3899
  context,
3828
- manifestPath,
3829
- manifestSource,
3900
+ projectEntries,
3830
3901
  tempDir,
3831
3902
  isWatchMode,
3832
3903
  onTriggerRerun: isWatchMode ? async ()=>{
@@ -3851,7 +3922,7 @@ const runBrowserController = async (context, options)=>{
3851
3922
  });
3852
3923
  }
3853
3924
  }
3854
- const { browser, browserLaunchOptions, port, wsPort, wss } = runtime;
3925
+ const { browser, browserLaunchOptions, wsPort, wss } = runtime;
3855
3926
  const buildTime = Date.now() - buildStart;
3856
3927
  const allTestFiles = projectEntries.flatMap((entry)=>entry.testFiles.map((testPath)=>({
3857
3928
  testPath: normalize(testPath),
@@ -3865,13 +3936,20 @@ const runBrowserController = async (context, options)=>{
3865
3936
  viewport: project.normalizedConfig.browser.viewport
3866
3937
  }));
3867
3938
  const maxTestTimeoutForRpc = Math.max(...browserProjects.map((p)=>p.normalizedConfig.testTimeout ?? DEFAULT_TEST_TIMEOUT));
3939
+ const projectRunnerUrls = Object.fromEntries([
3940
+ ...runtime.projectServers
3941
+ ].map(([name, server])=>[
3942
+ name,
3943
+ `http://localhost:${server.port}`
3944
+ ]));
3868
3945
  const hostOptions = {
3869
3946
  rootPath: normalize(context.rootPath),
3870
3947
  projects: projectRuntimeConfigs,
3871
3948
  snapshot: {
3872
3949
  updateSnapshot: context.snapshotManager.options.updateSnapshot
3873
3950
  },
3874
- runnerUrl: `http://localhost:${port}`,
3951
+ runnerUrl: `http://localhost:${runtime.containerServer.port}`,
3952
+ projectRunnerUrls,
3875
3953
  wsPort,
3876
3954
  debug: isDebug(),
3877
3955
  rpcTimeout: maxTestTimeoutForRpc
@@ -4190,11 +4268,16 @@ const runBrowserController = async (context, options)=>{
4190
4268
  const donePromise = new Promise((resolve)=>{
4191
4269
  resolveDone = resolve;
4192
4270
  });
4193
- const projectRuntime = projectRuntimeConfigs.find((project)=>project.name === file.projectName);
4194
- const perFileTimeoutMs = (projectRuntime?.runtimeConfig.testTimeout ?? maxTestTimeoutForRpc) + PER_FILE_TIMEOUT_BUFFER_MS;
4195
- let timeoutId;
4271
+ const crashDeferred = createDeferredPromise();
4272
+ const onPageDead = (reason)=>{
4273
+ if (settled || run.cancelled || !runLifecycle.isTokenActive(run.token)) return;
4274
+ settled = true;
4275
+ crashDeferred.resolve(reason);
4276
+ };
4196
4277
  try {
4197
4278
  page = await browserContext.newPage();
4279
+ page.on('crash', ()=>onPageDead(`Browser page crashed while running ${file.testPath}.`));
4280
+ page.on('close', ()=>onPageDead(`Browser page closed unexpectedly while running ${file.testPath}.`));
4198
4281
  const session = sessionRegistry.register({
4199
4282
  testFile: file.testPath,
4200
4283
  projectName: file.projectName,
@@ -4241,21 +4324,27 @@ const runBrowserController = async (context, options)=>{
4241
4324
  };
4242
4325
  const serializedOptions = serializeForInlineScript(inlineOptions);
4243
4326
  await page.addInitScript(`window.__RSTEST_BROWSER_OPTIONS__ = ${serializedOptions};`);
4244
- await page.goto(`http://localhost:${port}/runner.html`, {
4327
+ const projectServer = runtime.projectServers.get(file.projectName);
4328
+ if (!projectServer) throw new Error(`No browser dev server for project "${file.projectName}" (test file: ${file.testPath}).`);
4329
+ await page.goto(`http://localhost:${projectServer.port}/runner.html`, {
4245
4330
  waitUntil: 'load'
4246
4331
  });
4247
- const timeoutPromise = new Promise((resolve)=>{
4248
- timeoutId = setTimeout(()=>resolve('timeout'), perFileTimeoutMs);
4249
- });
4250
4332
  const state = await Promise.race([
4251
- donePromise.then(()=>'done'),
4252
- timeoutPromise,
4253
- run.cancelSignal.then(()=>'cancelled')
4333
+ donePromise.then(()=>({
4334
+ type: 'done'
4335
+ })),
4336
+ crashDeferred.promise.then((reason)=>({
4337
+ type: 'crash',
4338
+ reason
4339
+ })),
4340
+ run.cancelSignal.then(()=>({
4341
+ type: 'cancelled'
4342
+ }))
4254
4343
  ]);
4255
- if ('cancelled' === state) return;
4256
- if ('timeout' === state && runLifecycle.isTokenActive(run.token) && !run.cancelled) {
4344
+ if ('cancelled' === state.type) return;
4345
+ if ('crash' === state.type && runLifecycle.isTokenActive(run.token) && !run.cancelled) {
4257
4346
  await handleFatal({
4258
- message: `Test execution timeout after ${perFileTimeoutMs / 1000}s for ${file.testPath}.`
4347
+ message: state.reason
4259
4348
  });
4260
4349
  await cancelRun(run, false);
4261
4350
  }
@@ -4269,7 +4358,6 @@ const runBrowserController = async (context, options)=>{
4269
4358
  await cancelRun(run, false);
4270
4359
  }
4271
4360
  } finally{
4272
- if (timeoutId) clearTimeout(timeoutId);
4273
4361
  if (page) try {
4274
4362
  await page.close();
4275
4363
  } catch {}
@@ -4512,10 +4600,6 @@ const runBrowserController = async (context, options)=>{
4512
4600
  if (!fileInfo) throw new Error(`Unknown browser test file: ${JSON.stringify(testFile)}`);
4513
4601
  return fileInfo;
4514
4602
  };
4515
- const getHeadedPerFileTimeoutMs = (file)=>{
4516
- const projectRuntime = projectRuntimeConfigs.find((project)=>project.name === file.projectName);
4517
- return (projectRuntime?.runtimeConfig.testTimeout ?? maxTestTimeoutForRpc) + PER_FILE_TIMEOUT_BUFFER_MS;
4518
- };
4519
4603
  let containerContext;
4520
4604
  let containerPage;
4521
4605
  let isNewPage = false;
@@ -4585,26 +4669,14 @@ const runBrowserController = async (context, options)=>{
4585
4669
  pendingHeadedReloads.delete(testPath);
4586
4670
  pending.deferred.resolve();
4587
4671
  };
4588
- const reloadTestFileWithTimeout = async (file, testNamePattern)=>{
4589
- const timeoutMs = getHeadedPerFileTimeoutMs(file);
4590
- let timeoutId;
4672
+ const reloadTestFileAndWait = async (file, testNamePattern)=>{
4591
4673
  let reloadAck;
4592
4674
  try {
4593
4675
  reloadAck = await rpcManager.reloadTestFile(file.testPath, testNamePattern);
4594
- const completionPromise = registerPendingHeadedReload(file.testPath, reloadAck.runId);
4595
- await Promise.race([
4596
- completionPromise,
4597
- new Promise((_, reject)=>{
4598
- timeoutId = setTimeout(()=>{
4599
- reject(new Error(`Headed test execution timeout after ${timeoutMs / 1000}s for ${file.testPath}.`));
4600
- }, timeoutMs);
4601
- })
4602
- ]);
4676
+ await registerPendingHeadedReload(file.testPath, reloadAck.runId);
4603
4677
  } catch (error) {
4604
4678
  if (reloadAck?.runId) rejectPendingHeadedReload(file.testPath, toError(error), reloadAck.runId);
4605
4679
  throw error;
4606
- } finally{
4607
- if (timeoutId) clearTimeout(timeoutId);
4608
4680
  }
4609
4681
  };
4610
4682
  const createRpcMethods = ()=>({
@@ -4661,14 +4733,15 @@ const runBrowserController = async (context, options)=>{
4661
4733
  }
4662
4734
  if (isNewPage) {
4663
4735
  const pagePath = '/';
4664
- await containerPage.goto(`http://localhost:${port}${pagePath}`, {
4736
+ const containerPort = runtime.containerServer.port;
4737
+ await containerPage.goto(`http://localhost:${containerPort}${pagePath}`, {
4665
4738
  waitUntil: 'load'
4666
4739
  });
4667
- logger.log(color.cyan(`\nBrowser mode opened at http://localhost:${port}${pagePath}\n`));
4740
+ logger.log(color.cyan(`\nBrowser mode opened at http://localhost:${containerPort}${pagePath}\n`));
4668
4741
  }
4669
4742
  enqueueHeadedReload = async (file, testNamePattern)=>headedReloadQueue.enqueue(async ()=>{
4670
4743
  if (fatalError) return;
4671
- await reloadTestFileWithTimeout(file, testNamePattern);
4744
+ await reloadTestFileAndWait(file, testNamePattern);
4672
4745
  });
4673
4746
  let testTime = 0;
4674
4747
  if (currentTestFiles.length > 0) {
@@ -4789,18 +4862,12 @@ const listBrowserTests = async (context, options)=>{
4789
4862
  close: async ()=>{}
4790
4863
  };
4791
4864
  const tempDir = join(context.rootPath, context.normalizedConfig.output.distPath.root, 'browser', `list-${Date.now()}`);
4792
- const manifestPath = join(tempDir, VIRTUAL_MANIFEST_FILENAME);
4793
- const manifestSource = generateManifestModule({
4794
- manifestPath,
4795
- entries: projectEntries
4796
- });
4797
4865
  const browserProjects = getBrowserProjects(context);
4798
4866
  let runtime;
4799
4867
  try {
4800
4868
  runtime = await createBrowserRuntime({
4801
4869
  context,
4802
- manifestPath,
4803
- manifestSource,
4870
+ projectEntries,
4804
4871
  tempDir,
4805
4872
  isWatchMode: false,
4806
4873
  containerDistPath: void 0,
@@ -4814,7 +4881,7 @@ const listBrowserTests = async (context, options)=>{
4814
4881
  logger.error(color.red(`Failed to initialize browser provider runtime (${providers.join(', ')}).`), error);
4815
4882
  throw error;
4816
4883
  }
4817
- const { browser, browserLaunchOptions, port } = runtime;
4884
+ const { browser, browserLaunchOptions } = runtime;
4818
4885
  const projectRuntimeConfigs = browserProjects.map((project)=>({
4819
4886
  name: project.name,
4820
4887
  environmentName: project.environmentName,
@@ -4834,70 +4901,81 @@ const listBrowserTests = async (context, options)=>{
4834
4901
  rpcTimeout: maxTestTimeoutForRpc
4835
4902
  };
4836
4903
  runtime.setContainerOptions(hostOptions);
4837
- const collectResults = [];
4838
- let fatalError = null;
4839
- let collectCompleted = false;
4840
- let resolveCollect;
4841
- const collectPromise = new Promise((resolve)=>{
4842
- resolveCollect = resolve;
4843
- });
4844
4904
  const browserContext = await browser.newContext({
4845
4905
  providerOptions: browserLaunchOptions.providerOptions,
4846
4906
  viewport: null
4847
4907
  });
4848
- const page = await browserContext.newPage();
4849
- await page.exposeFunction(DISPATCH_MESSAGE_TYPE, (message)=>{
4850
- switch(message.type){
4851
- case 'collect-result':
4852
- {
4853
- const payload = message.payload;
4854
- collectResults.push({
4855
- testPath: payload.testPath,
4856
- project: payload.project,
4857
- tests: payload.tests
4858
- });
4859
- break;
4860
- }
4861
- case 'collect-complete':
4862
- collectCompleted = true;
4863
- resolveCollect?.();
4864
- break;
4865
- case 'fatal':
4866
- {
4867
- const payload = message.payload;
4868
- fatalError = new Error(payload.message);
4869
- fatalError.stack = payload.stack;
4908
+ const serializedOptions = serializeForInlineScript(hostOptions);
4909
+ const collectFromServer = async (server)=>{
4910
+ const results = [];
4911
+ let error = null;
4912
+ let collectCompleted = false;
4913
+ let resolveCollect;
4914
+ const collectPromise = new Promise((resolve)=>{
4915
+ resolveCollect = resolve;
4916
+ });
4917
+ const page = await browserContext.newPage();
4918
+ await page.exposeFunction(DISPATCH_MESSAGE_TYPE, (message)=>{
4919
+ switch(message.type){
4920
+ case 'collect-result':
4921
+ {
4922
+ const payload = message.payload;
4923
+ results.push({
4924
+ testPath: payload.testPath,
4925
+ project: payload.project,
4926
+ tests: payload.tests
4927
+ });
4928
+ break;
4929
+ }
4930
+ case 'collect-complete':
4931
+ collectCompleted = true;
4870
4932
  resolveCollect?.();
4871
4933
  break;
4872
- }
4873
- case 'ready':
4874
- case 'log':
4875
- break;
4876
- default:
4877
- logger.debug(`[List] Unexpected message: ${message.type}`);
4878
- }
4879
- });
4880
- const serializedOptions = serializeForInlineScript(hostOptions);
4881
- await page.addInitScript(`window.__RSTEST_BROWSER_OPTIONS__ = ${serializedOptions};`);
4882
- await page.goto(`http://localhost:${port}/runner.html`, {
4883
- waitUntil: 'load'
4884
- });
4885
- const timeoutMs = 30000;
4886
- let timeoutId;
4887
- const timeoutPromise = new Promise((resolve)=>{
4888
- timeoutId = setTimeout(()=>{
4889
- if (!collectCompleted) logger.warn(color.yellow(`[List] Browser test collection timed out after ${timeoutMs}ms`));
4890
- resolve();
4891
- }, timeoutMs);
4892
- });
4893
- await Promise.race([
4894
- collectPromise,
4895
- timeoutPromise
4896
- ]);
4897
- if (timeoutId) clearTimeout(timeoutId);
4934
+ case 'fatal':
4935
+ {
4936
+ const payload = message.payload;
4937
+ error = new Error(payload.message);
4938
+ error.stack = payload.stack;
4939
+ resolveCollect?.();
4940
+ break;
4941
+ }
4942
+ case 'ready':
4943
+ case 'log':
4944
+ break;
4945
+ default:
4946
+ logger.debug(`[List] Unexpected message: ${message.type}`);
4947
+ }
4948
+ });
4949
+ await page.addInitScript(`window.__RSTEST_BROWSER_OPTIONS__ = ${serializedOptions};`);
4950
+ await page.goto(`http://localhost:${server.port}/runner.html`, {
4951
+ waitUntil: 'load'
4952
+ });
4953
+ const timeoutMs = 30000;
4954
+ let timeoutId;
4955
+ const timeoutPromise = new Promise((resolve)=>{
4956
+ timeoutId = setTimeout(()=>{
4957
+ if (!collectCompleted) logger.warn(color.yellow(`[List] Browser test collection timed out after ${timeoutMs}ms`));
4958
+ resolve();
4959
+ }, timeoutMs);
4960
+ });
4961
+ await Promise.race([
4962
+ collectPromise,
4963
+ timeoutPromise
4964
+ ]);
4965
+ if (timeoutId) clearTimeout(timeoutId);
4966
+ await page.close().catch(()=>{});
4967
+ return {
4968
+ results,
4969
+ error
4970
+ };
4971
+ };
4972
+ const collected = await Promise.all([
4973
+ ...runtime.projectServers.values()
4974
+ ].map((server)=>collectFromServer(server)));
4975
+ const collectResults = collected.flatMap((entry)=>entry.results);
4976
+ const fatalError = collected.find((entry)=>entry.error)?.error ?? null;
4898
4977
  const cleanup = async ()=>{
4899
4978
  try {
4900
- await page.close();
4901
4979
  await browserContext.close();
4902
4980
  } catch {}
4903
4981
  await destroyBrowserRuntime(runtime);