@rstest/browser 0.9.9 → 0.10.0

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.
Files changed (37) hide show
  1. package/dist/augmentExpect.d.ts +1 -1
  2. package/dist/browser-container/container-static/js/{464.89762ff658.js → 484.af891b432a.js} +3183 -3152
  3. package/dist/browser-container/container-static/js/484.af891b432a.js.LICENSE.txt +1 -0
  4. package/dist/browser-container/container-static/js/{index.c13dfe1ecf.js → index.463ae8de87.js} +12 -12
  5. package/dist/browser-container/container-static/js/{lib-react.9e703e1b29.js → lib-react.5ae9b90ee5.js} +24 -24
  6. package/dist/browser-container/container-static/js/lib-react.5ae9b90ee5.js.LICENSE.txt +1 -0
  7. package/dist/browser-container/index.html +1 -1
  8. package/dist/browser.d.ts +2 -2
  9. package/dist/client/api.d.ts +2 -2
  10. package/dist/client/browserRpc.d.ts +2 -2
  11. package/dist/client/dispatchTransport.d.ts +1 -1
  12. package/dist/client/locator.d.ts +2 -2
  13. package/dist/concurrency.d.ts +1 -1
  14. package/dist/dispatchCapabilities.d.ts +2 -2
  15. package/dist/dispatchRouter.d.ts +1 -1
  16. package/dist/headlessTransport.d.ts +2 -2
  17. package/dist/hostController.d.ts +2 -2
  18. package/dist/index.d.ts +3 -3
  19. package/dist/index.js +205 -24
  20. package/dist/protocol.d.ts +6 -2
  21. package/dist/providers/index.d.ts +1 -1
  22. package/dist/providers/playwright/compileLocator.d.ts +1 -1
  23. package/dist/providers/playwright/dispatchBrowserRpc.d.ts +1 -1
  24. package/dist/providers/playwright/expectUtils.d.ts +1 -1
  25. package/dist/providers/playwright/implementation.d.ts +1 -1
  26. package/dist/providers/playwright/index.d.ts +1 -1
  27. package/dist/providers/playwright/runtime.d.ts +1 -1
  28. package/dist/providers/playwright/textMatcher.d.ts +2 -2
  29. package/dist/sessionRegistry.d.ts +1 -1
  30. package/dist/viewportPresets.d.ts +1 -1
  31. package/dist/watchRerunPlanner.d.ts +1 -1
  32. package/package.json +9 -9
  33. package/src/client/entry.ts +75 -14
  34. package/src/hostController.ts +405 -71
  35. package/src/protocol.ts +4 -0
  36. package/dist/browser-container/container-static/js/464.89762ff658.js.LICENSE.txt +0 -1
  37. package/dist/browser-container/container-static/js/lib-react.9e703e1b29.js.LICENSE.txt +0 -1
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@ import { existsSync } from "node:fs";
2
2
  import promises from "node:fs/promises";
3
3
  import { fileURLToPath } from "node:url";
4
4
  import { isDeepStrictEqual } from "node:util";
5
- import { color, createCoverageProvider, getSetupFiles, getTestEntries, isDebug, loadCoverageProvider, logger, rsbuild, serializableConfig } from "@rstest/core/browser";
5
+ import { PhaseTracker, color, createCoverageProvider, getNoTestFilesMessage, getSetupFiles, getTestEntries, isDebug, loadCoverageProvider, logger, resolveProjectBuildCache, rsbuild, serializableConfig } from "@rstest/core/browser";
6
6
  import open_editor from "open-editor";
7
7
  import { basename, dirname, join, normalize, relative, resolve as external_pathe_resolve } from "pathe";
8
8
  import sirv from "sirv";
@@ -2800,11 +2800,14 @@ var picomatch_default = /*#__PURE__*/ __webpack_require__.n(picomatch);
2800
2800
  const { createRsbuild: createRsbuild, rspack: rspack } = rsbuild;
2801
2801
  const hostController_dirname = dirname(fileURLToPath(import.meta.url));
2802
2802
  const OPTIONS_PLACEHOLDER = '__RSTEST_OPTIONS_PLACEHOLDER__';
2803
+ let nextBrowserFilePid = 1000000000;
2803
2804
  const serializeForInlineScript = (value)=>JSON.stringify(value).replace(/</g, '\\u003c').replace(/\u2028/g, '\\u2028').replace(/\u2029/g, '\\u2029');
2804
2805
  const getBrowserProviderOptions = (project)=>{
2805
2806
  const browserConfig = project.normalizedConfig.browser;
2806
2807
  return browserConfig.providerOptions ?? {};
2807
2808
  };
2809
+ const getFileTaskId = (testPath)=>`file:${testPath}`;
2810
+ const getBufferedLogTaskId = (log)=>log.taskId ?? getFileTaskId(log.testPath);
2808
2811
  const createDeferredPromise = ()=>{
2809
2812
  let resolve;
2810
2813
  let reject;
@@ -2949,7 +2952,7 @@ const applyDefaultWatchOptions = (rspackConfig, isWatchMode)=>{
2949
2952
  }
2950
2953
  rspackConfig.watchOptions.ignored = castArray(rspackConfig.watchOptions.ignored || []);
2951
2954
  if (0 === rspackConfig.watchOptions.ignored.length) rspackConfig.watchOptions.ignored.push('**/.git', '**/node_modules');
2952
- rspackConfig.output?.path && rspackConfig.watchOptions.ignored.push(rspackConfig.output.path);
2955
+ if (rspackConfig.output?.path) rspackConfig.watchOptions.ignored.push(rspackConfig.output.path);
2953
2956
  };
2954
2957
  const resolveListenPort = (listenPort, httpServer)=>{
2955
2958
  if (listenPort) return listenPort;
@@ -3059,9 +3062,12 @@ const getAffectedTestFiles = (chunks, entryTestFiles)=>{
3059
3062
  return Array.from(affectedFiles);
3060
3063
  };
3061
3064
  const getRuntimeConfigFromProject = (project)=>{
3062
- const { testNamePattern, testTimeout, passWithNoTests, retry, globals, clearMocks, resetMocks, restoreMocks, unstubEnvs, unstubGlobals, maxConcurrency, printConsoleTrace, disableConsoleIntercept, testEnvironment, hookTimeout, isolate, coverage, snapshotFormat, env, bail, logHeapUsage, chaiConfig, includeTaskLocation } = project.normalizedConfig;
3065
+ const { testNamePattern, testTimeout, passWithNoTests, retry, globals, clearMocks, resetMocks, restoreMocks, unstubEnvs, unstubGlobals, maxConcurrency, printConsoleTrace, disableConsoleIntercept, testEnvironment, hookTimeout, isolate, coverage, snapshotFormat, env, bail, logHeapUsage, chaiConfig, includeTaskLocation, silent } = project.normalizedConfig;
3063
3066
  return {
3064
- env,
3067
+ env: {
3068
+ NODE_ENV: process.env.NODE_ENV,
3069
+ ...env
3070
+ },
3065
3071
  testNamePattern,
3066
3072
  testTimeout,
3067
3073
  hookTimeout,
@@ -3083,7 +3089,8 @@ const getRuntimeConfigFromProject = (project)=>{
3083
3089
  bail,
3084
3090
  logHeapUsage,
3085
3091
  chaiConfig,
3086
- includeTaskLocation
3092
+ includeTaskLocation,
3093
+ silent
3087
3094
  };
3088
3095
  };
3089
3096
  const getBrowserProjects = (context)=>context.projects.filter((project)=>project.normalizedConfig.browser.enabled);
@@ -3123,7 +3130,8 @@ const collectProjectEntries = async (context)=>{
3123
3130
  includeSource,
3124
3131
  rootPath: context.rootPath,
3125
3132
  projectRoot: project.rootPath,
3126
- fileFilters: context.fileFilters || []
3133
+ fileFilters: context.fileFilters || [],
3134
+ fileFilterMode: context.fileFilterMode
3127
3135
  });
3128
3136
  const setup = getSetupFiles(setupFiles, project.rootPath);
3129
3137
  return {
@@ -3326,8 +3334,18 @@ const createBrowserRuntime = async ({ context, manifestPath, manifestSource, tem
3326
3334
  const project = projectByEnvironmentName.get(name);
3327
3335
  if (!project) return config;
3328
3336
  const userRsbuildConfig = project.normalizedConfig;
3337
+ const buildCache = resolveProjectBuildCache({
3338
+ context,
3339
+ project
3340
+ });
3329
3341
  const setupFiles = Object.values(getSetupFiles(project.normalizedConfig.setupFiles, project.rootPath));
3330
- const merged = mergeEnvironmentConfig(config, userRsbuildConfig, {
3342
+ const merged = mergeEnvironmentConfig(config, {
3343
+ ...userRsbuildConfig,
3344
+ performance: buildCache ? {
3345
+ ...userRsbuildConfig.performance,
3346
+ buildCache
3347
+ } : userRsbuildConfig.performance
3348
+ }, {
3331
3349
  resolve: {
3332
3350
  alias: rstestInternalAliases
3333
3351
  },
@@ -3572,8 +3590,10 @@ async function resolveProjectEntries(context, shardedEntries) {
3572
3590
  return collectProjectEntries(context);
3573
3591
  }
3574
3592
  const runBrowserController = async (context, options)=>{
3575
- const { skipOnTestRunEnd = false } = options ?? {};
3593
+ const { skipOnTestRunEnd = false, allowEmptyWatchRun = false, onTraceEvents } = options ?? {};
3576
3594
  const buildStart = Date.now();
3595
+ const isWatchMode = 'watch' === context.command;
3596
+ const phaseTrackers = onTraceEvents ? new Map() : void 0;
3577
3597
  const browserProjects = getBrowserProjects(context);
3578
3598
  const useHeadlessDirect = browserProjects.every((project)=>project.normalizedConfig.browser.headless);
3579
3599
  const browserSourceMapCache = new Map();
@@ -3696,18 +3716,24 @@ const runBrowserController = async (context, options)=>{
3696
3716
  }
3697
3717
  const projectEntries = await resolveProjectEntries(context, options?.shardedEntries);
3698
3718
  const totalTests = projectEntries.reduce((total, item)=>total + item.testFiles.length, 0);
3719
+ const shouldKeepWatchingWithEmptySet = isWatchMode && allowEmptyWatchRun;
3699
3720
  if (0 === totalTests) {
3700
3721
  const code = context.normalizedConfig.passWithNoTests ? 0 : 1;
3701
3722
  if (!skipOnTestRunEnd) {
3702
- const message = `No test files found, exiting with code ${code}.`;
3723
+ const message = shouldKeepWatchingWithEmptySet ? 'No test files found.' : getNoTestFilesMessage({
3724
+ context,
3725
+ code,
3726
+ defaultMessage: `No test files found, exiting with code ${code}.`
3727
+ });
3703
3728
  if (0 === code) logger.log(color.yellow(message));
3704
3729
  else logger.error(color.red(message));
3730
+ if (context.relatedFilters?.length) logger.log(color.gray('related: '), context.relatedFilters.join(color.gray(', ')));
3731
+ else if (context.fileFilters?.length) logger.log(color.gray('filter: '), context.fileFilters.join(color.gray(', ')));
3705
3732
  }
3706
- if (0 !== code) ensureProcessExitCode(code);
3707
- return;
3733
+ if (0 !== code && !shouldKeepWatchingWithEmptySet) ensureProcessExitCode(code);
3734
+ if (!shouldKeepWatchingWithEmptySet) return;
3708
3735
  }
3709
3736
  await notifyTestRunStart();
3710
- const isWatchMode = 'watch' === context.command;
3711
3737
  const enableCliShortcuts = isWatchMode && isBrowserWatchCliShortcutsEnabled();
3712
3738
  const browserTempOutputRoot = context.normalizedConfig.output.distPath.root;
3713
3739
  const tempDir = isWatchMode && watchContext.runtime ? watchContext.runtime.tempDir : isWatchMode ? join(context.rootPath, browserTempOutputRoot, 'browser', 'watch') : join(context.rootPath, browserTempOutputRoot, 'browser', Date.now().toString());
@@ -3843,26 +3869,57 @@ const runBrowserController = async (context, options)=>{
3843
3869
  }
3844
3870
  };
3845
3871
  const handleTestFileStart = async (payload)=>{
3872
+ if (phaseTrackers) {
3873
+ const tracker = new PhaseTracker({
3874
+ trace: {
3875
+ testPath: payload.testPath,
3876
+ project: payload.projectName
3877
+ },
3878
+ pid: nextBrowserFilePid++
3879
+ });
3880
+ tracker.transition('prepare');
3881
+ phaseTrackers.set(payload.testPath, tracker);
3882
+ }
3846
3883
  await Promise.all(context.reporters.map((reporter)=>reporter.onTestFileStart?.({
3884
+ testId: getFileTaskId(payload.testPath),
3847
3885
  testPath: payload.testPath,
3848
3886
  tests: []
3849
3887
  })));
3850
3888
  };
3851
3889
  const handleTestFileReady = async (payload)=>{
3890
+ phaseTrackers?.get(payload.testPath)?.transition('tests');
3852
3891
  await Promise.all(context.reporters.map((reporter)=>reporter.onTestFileReady?.(payload)));
3853
3892
  };
3854
3893
  const handleTestSuiteStart = async (payload)=>{
3894
+ phaseTrackers?.get(payload.testPath)?.recordSuiteStart(payload);
3855
3895
  await Promise.all(context.reporters.map((reporter)=>reporter.onTestSuiteStart?.(payload)));
3856
3896
  };
3857
3897
  const handleTestSuiteResult = async (payload)=>{
3898
+ phaseTrackers?.get(payload.testPath)?.recordSuiteResult(payload);
3858
3899
  await Promise.all(context.reporters.map((reporter)=>reporter.onTestSuiteResult?.(payload)));
3900
+ if ('passed-only' === context.normalizedConfig.silent) await flushBufferedLogsForTask({
3901
+ taskId: payload.testId,
3902
+ status: payload.status,
3903
+ taskParentNames: payload.parentNames,
3904
+ taskType: 'suite',
3905
+ testPath: payload.testPath
3906
+ });
3859
3907
  };
3860
3908
  const handleTestCaseStart = async (payload)=>{
3909
+ phaseTrackers?.get(payload.testPath)?.recordCaseStart(payload);
3861
3910
  await Promise.all(context.reporters.map((reporter)=>reporter.onTestCaseStart?.(payload)));
3862
3911
  };
3863
3912
  const handleTestCaseResult = async (payload)=>{
3864
3913
  caseResults.push(payload);
3914
+ phaseTrackers?.get(payload.testPath)?.recordCaseResult(payload);
3865
3915
  await Promise.all(context.reporters.map((reporter)=>reporter.onTestCaseResult?.(payload)));
3916
+ if ('passed-only' === context.normalizedConfig.silent) await flushBufferedLogsForTask({
3917
+ taskId: payload.testId,
3918
+ status: payload.status,
3919
+ taskParentNames: payload.parentNames,
3920
+ taskType: 'case',
3921
+ testPath: payload.testPath
3922
+ });
3866
3923
  };
3867
3924
  const handleTestFileComplete = async (payload)=>{
3868
3925
  reporterResults.push(payload);
@@ -3870,6 +3927,22 @@ const runBrowserController = async (context, options)=>{
3870
3927
  payload
3871
3928
  ], payload.results);
3872
3929
  if (payload.snapshotResult) context.snapshotManager.add(payload.snapshotResult);
3930
+ if (phaseTrackers) {
3931
+ const tracker = phaseTrackers.get(payload.testPath);
3932
+ if (tracker) {
3933
+ tracker.end();
3934
+ const events = tracker.getTraceEvents();
3935
+ if (events) onTraceEvents?.(events);
3936
+ phaseTrackers.delete(payload.testPath);
3937
+ }
3938
+ }
3939
+ if ('passed-only' === context.normalizedConfig.silent) await flushBufferedLogsForTask({
3940
+ taskId: payload.testId,
3941
+ status: payload.status,
3942
+ taskParentNames: payload.parentNames,
3943
+ taskType: 'file',
3944
+ testPath: payload.testPath
3945
+ });
3873
3946
  await Promise.all(context.reporters.map((reporter)=>reporter.onTestFileResult?.(payload)));
3874
3947
  if ('fail' === payload.status) ensureProcessExitCode(1);
3875
3948
  };
@@ -3877,12 +3950,18 @@ const runBrowserController = async (context, options)=>{
3877
3950
  const log = {
3878
3951
  content: payload.content,
3879
3952
  name: payload.level,
3953
+ taskId: payload.taskId,
3954
+ taskName: payload.taskName,
3955
+ taskParentNames: payload.taskParentNames,
3956
+ taskType: payload.taskType,
3880
3957
  testPath: payload.testPath,
3881
3958
  type: payload.type,
3882
3959
  trace: payload.trace
3883
3960
  };
3884
- const shouldLog = context.normalizedConfig.onConsoleLog?.(log.content) ?? true;
3885
- if (shouldLog) await Promise.all(context.reporters.map((reporter)=>reporter.onUserConsoleLog?.(log)));
3961
+ if (true === context.normalizedConfig.silent) return;
3962
+ if ('passed-only' === context.normalizedConfig.silent) return void bufferConsoleLog(log);
3963
+ if (context.normalizedConfig.disableConsoleIntercept) return;
3964
+ await emitUserConsoleLog(log);
3886
3965
  };
3887
3966
  const handleFatal = async (payload)=>{
3888
3967
  const error = new Error(payload.message);
@@ -3890,6 +3969,52 @@ const runBrowserController = async (context, options)=>{
3890
3969
  fatalError = error;
3891
3970
  ensureProcessExitCode(1);
3892
3971
  };
3972
+ const bufferedConsoleLogs = new Map();
3973
+ const suiteIdsByChain = new Map();
3974
+ const getSuiteChainKey = (names)=>names.join('\u0000');
3975
+ const pushTaskId = (taskIds, taskId)=>{
3976
+ if (!taskIds.includes(taskId)) taskIds.push(taskId);
3977
+ };
3978
+ const shouldEmitUserConsoleLog = (log)=>context.normalizedConfig.onConsoleLog?.(log.content) !== false;
3979
+ const emitUserConsoleLog = async (log)=>{
3980
+ if (!shouldEmitUserConsoleLog(log)) return;
3981
+ await Promise.all(context.reporters.map((reporter)=>reporter.onUserConsoleLog?.(log)));
3982
+ };
3983
+ const bufferConsoleLog = (log)=>{
3984
+ const taskId = getBufferedLogTaskId(log);
3985
+ const logs = bufferedConsoleLogs.get(taskId) || [];
3986
+ logs.push(log);
3987
+ bufferedConsoleLogs.set(taskId, logs);
3988
+ if ('suite' === log.taskType && log.taskId) suiteIdsByChain.set(getSuiteChainKey([
3989
+ ...log.taskParentNames || [],
3990
+ log.taskName || ''
3991
+ ]), log.taskId);
3992
+ };
3993
+ const flushBufferedLogsForTask = async ({ taskId, status, taskParentNames, taskType, testPath })=>{
3994
+ if ('fail' !== status) return void bufferedConsoleLogs.delete(taskId);
3995
+ const taskIdsToFlush = [];
3996
+ if ('case' === taskType) {
3997
+ pushTaskId(taskIdsToFlush, getFileTaskId(testPath));
3998
+ const suiteNames = taskParentNames || [];
3999
+ for(let i = 0; i < suiteNames.length; i++){
4000
+ const suiteId = suiteIdsByChain.get(getSuiteChainKey(suiteNames.slice(0, i + 1)));
4001
+ if (suiteId) pushTaskId(taskIdsToFlush, suiteId);
4002
+ }
4003
+ pushTaskId(taskIdsToFlush, taskId);
4004
+ }
4005
+ if ('suite' === taskType) {
4006
+ pushTaskId(taskIdsToFlush, getFileTaskId(testPath));
4007
+ pushTaskId(taskIdsToFlush, taskId);
4008
+ }
4009
+ if ('file' === taskType) pushTaskId(taskIdsToFlush, taskId);
4010
+ for (const bufferedTaskId of taskIdsToFlush){
4011
+ const logs = bufferedConsoleLogs.get(bufferedTaskId);
4012
+ if (logs) {
4013
+ bufferedConsoleLogs.delete(bufferedTaskId);
4014
+ for (const log of logs)await Promise.all(context.reporters.map((reporter)=>reporter.onUserConsoleLog?.(log)));
4015
+ }
4016
+ }
4017
+ };
3893
4018
  const runSnapshotRpc = async (request)=>{
3894
4019
  switch(request.method){
3895
4020
  case 'resolveSnapshotPath':
@@ -4148,6 +4273,54 @@ const runBrowserController = async (context, options)=>{
4148
4273
  logger.debug(`[Headless] Interrupting active run token ${run.token} before scheduling latest rerun`);
4149
4274
  }
4150
4275
  });
4276
+ if (0 === allTestFiles.length) {
4277
+ const duration = {
4278
+ totalTime: buildTime,
4279
+ buildTime,
4280
+ testTime: 0
4281
+ };
4282
+ const result = {
4283
+ results: reporterResults,
4284
+ testResults: caseResults,
4285
+ duration,
4286
+ hasFailure: false,
4287
+ getSourcemap: getBrowserSourcemap,
4288
+ resolveSourcemap: resolveBrowserSourcemap,
4289
+ close: skipOnTestRunEnd ? async ()=>{
4290
+ sessionRegistry.clear();
4291
+ await destroyBrowserRuntime(runtime);
4292
+ } : void 0
4293
+ };
4294
+ if (!skipOnTestRunEnd) await notifyTestRunEnd({
4295
+ duration
4296
+ });
4297
+ if (isWatchMode) {
4298
+ triggerRerun = async ()=>{
4299
+ const newProjectEntries = await collectProjectEntries(context);
4300
+ const rerunPlan = planWatchRerun({
4301
+ projectEntries: newProjectEntries,
4302
+ previousTestFiles: watchContext.lastTestFiles,
4303
+ affectedTestFiles: watchContext.affectedTestFiles
4304
+ });
4305
+ watchContext.affectedTestFiles = [];
4306
+ if (rerunPlan.filesChanged) {
4307
+ watchContext.lastTestFiles = rerunPlan.currentTestFiles;
4308
+ if (0 === rerunPlan.currentTestFiles.length) {
4309
+ logger.log(color.cyan('No browser test files remain after update.\n'));
4310
+ logBrowserWatchReadyMessage(enableCliShortcuts);
4311
+ return;
4312
+ }
4313
+ logger.log(color.cyan(`Test file set changed, re-running ${rerunPlan.currentTestFiles.length} file(s)...\n`));
4314
+ latestRerunScheduler.enqueueLatest(rerunPlan.currentTestFiles);
4315
+ return;
4316
+ }
4317
+ logBrowserWatchReadyMessage(enableCliShortcuts);
4318
+ };
4319
+ watchContext.hooksEnabled = true;
4320
+ logBrowserWatchReadyMessage(enableCliShortcuts);
4321
+ }
4322
+ return result;
4323
+ }
4151
4324
  const testStart = Date.now();
4152
4325
  await runFilesWithPool(allTestFiles);
4153
4326
  const testTime = Date.now() - testStart;
@@ -4418,18 +4591,21 @@ const runBrowserController = async (context, options)=>{
4418
4591
  if (fatalError) return;
4419
4592
  await reloadTestFileWithTimeout(file, testNamePattern);
4420
4593
  });
4421
- const testStart = Date.now();
4422
- try {
4423
- await waitForRunnerFramesReady(currentTestFiles.map((file)=>file.testPath));
4424
- for (const file of currentTestFiles){
4425
- await enqueueHeadedReload(file);
4426
- if (fatalError) break;
4594
+ let testTime = 0;
4595
+ if (currentTestFiles.length > 0) {
4596
+ const testStart = Date.now();
4597
+ try {
4598
+ await waitForRunnerFramesReady(currentTestFiles.map((file)=>file.testPath));
4599
+ for (const file of currentTestFiles){
4600
+ await enqueueHeadedReload(file);
4601
+ if (fatalError) break;
4602
+ }
4603
+ } catch (error) {
4604
+ fatalError = fatalError ?? toError(error);
4605
+ ensureProcessExitCode(1);
4427
4606
  }
4428
- } catch (error) {
4429
- fatalError = fatalError ?? toError(error);
4430
- ensureProcessExitCode(1);
4607
+ testTime = Date.now() - testStart;
4431
4608
  }
4432
- const testTime = Date.now() - testStart;
4433
4609
  if (isWatchMode) triggerRerun = async ()=>{
4434
4610
  const newProjectEntries = await collectProjectEntries(context);
4435
4611
  const rerunPlan = planWatchRerun({
@@ -4444,6 +4620,11 @@ const runBrowserController = async (context, options)=>{
4444
4620
  watchContext.lastTestFiles = rerunPlan.currentTestFiles;
4445
4621
  currentTestFiles = rerunPlan.currentTestFiles;
4446
4622
  await rpcManager.notifyTestFileUpdate(currentTestFiles);
4623
+ if (0 === currentTestFiles.length) {
4624
+ logger.log(color.cyan('No browser test files remain after update.\n'));
4625
+ logBrowserWatchReadyMessage(enableCliShortcuts);
4626
+ return;
4627
+ }
4447
4628
  await waitForRunnerFramesReady(currentTestFiles.map((file)=>file.testPath));
4448
4629
  }
4449
4630
  if (rerunPlan.normalizedAffectedTestFiles.length > 0) {
@@ -1,8 +1,8 @@
1
1
  import type { DevicePreset } from '@rstest/core/browser';
2
2
  import type { RuntimeConfig, TestFileResult, TestInfo, TestResult } from '@rstest/core/browser-runtime';
3
3
  import type { SnapshotUpdateState } from '@vitest/snapshot';
4
- export type { BrowserLocatorIR, BrowserRpcRequest, SnapshotRpcRequest, } from './rpcProtocol';
5
- export { validateBrowserRpcRequest } from './rpcProtocol';
4
+ export type { BrowserLocatorIR, BrowserRpcRequest, SnapshotRpcRequest, } from './rpcProtocol.js';
5
+ export { validateBrowserRpcRequest } from './rpcProtocol.js';
6
6
  export declare const DISPATCH_MESSAGE_TYPE = "__rstest_dispatch__";
7
7
  export declare const DISPATCH_RESPONSE_TYPE = "__rstest_dispatch_response__";
8
8
  export declare const DISPATCH_RPC_BRIDGE_NAME = "__rstest_dispatch_rpc__";
@@ -91,6 +91,10 @@ export type BrowserClientMessage = {
91
91
  payload: {
92
92
  level: 'log' | 'warn' | 'error' | 'info' | 'debug';
93
93
  content: string;
94
+ taskId?: string;
95
+ taskName?: string;
96
+ taskParentNames?: string[];
97
+ taskType?: 'file' | 'suite' | 'case';
94
98
  testPath: string;
95
99
  type: 'stdout' | 'stderr';
96
100
  trace?: string;
@@ -1,4 +1,4 @@
1
- import type { BrowserRpcRequest } from '../rpcProtocol';
1
+ import type { BrowserRpcRequest } from '../rpcProtocol.js';
2
2
  /**
3
3
  * Browser provider contract hub.
4
4
  *
@@ -1,3 +1,3 @@
1
1
  import type { FrameLocator, Locator, Page } from 'playwright';
2
- import type { BrowserLocatorIR } from '../../protocol';
2
+ import type { BrowserLocatorIR } from '../../protocol.js';
3
3
  export declare const compilePlaywrightLocator: (frame: FrameLocator | Page, locatorIR: BrowserLocatorIR) => Locator;
@@ -4,7 +4,7 @@
4
4
  * Copyright (c) Microsoft Corporation, Apache-2.0.
5
5
  */
6
6
  import type { Page } from 'playwright';
7
- import type { BrowserRpcRequest } from '../../rpcProtocol';
7
+ import type { BrowserRpcRequest } from '../../rpcProtocol.js';
8
8
  export declare function dispatchPlaywrightBrowserRpc({ containerPage, runnerPage, request, timeoutFallbackMs, }: {
9
9
  containerPage?: Page;
10
10
  runnerPage?: Page;
@@ -3,7 +3,7 @@
3
3
  * https://github.com/microsoft/playwright/blob/main/packages/playwright/src/matchers/matchers.ts
4
4
  * Copyright (c) Microsoft Corporation, Apache-2.0.
5
5
  */
6
- import type { BrowserLocatorText } from '../../rpcProtocol';
6
+ import type { BrowserLocatorText } from '../../rpcProtocol.js';
7
7
  type ExpectedTextValue = {
8
8
  string?: string;
9
9
  regexSource?: string;
@@ -1,2 +1,2 @@
1
- import type { BrowserProviderImplementation } from '../index';
1
+ import type { BrowserProviderImplementation } from '../index.js';
2
2
  export declare const playwrightProviderImplementation: BrowserProviderImplementation;
@@ -1 +1 @@
1
- export { playwrightProviderImplementation } from './implementation';
1
+ export { playwrightProviderImplementation } from './implementation.js';
@@ -1,4 +1,4 @@
1
- import type { BrowserProviderRuntime } from '../index';
1
+ import type { BrowserProviderRuntime } from '../index.js';
2
2
  export declare function launchPlaywrightBrowser({ browserName, headless, providerOptions, }: {
3
3
  browserName: 'chromium' | 'firefox' | 'webkit';
4
4
  headless: boolean | undefined;
@@ -1,8 +1,8 @@
1
1
  export declare const reviveBrowserLocatorText: (text: {
2
- type: "string";
2
+ type: 'string';
3
3
  value: string;
4
4
  } | {
5
- type: "regexp";
5
+ type: 'regexp';
6
6
  source: string;
7
7
  flags?: string;
8
8
  }) => string | RegExp;
@@ -1,4 +1,4 @@
1
- import type { BrowserProviderContext, BrowserProviderPage } from './providers';
1
+ import type { BrowserProviderContext, BrowserProviderPage } from './providers/index.js';
2
2
  type RunnerSessionRecord = {
3
3
  id: string;
4
4
  testFile: string;
@@ -5,7 +5,7 @@
5
5
  * `@rstest/core` (`packages/core/src/types/config.ts`) so `defineConfig`
6
6
  * autocomplete and runtime validation stay consistent.
7
7
  */
8
- export declare const BROWSER_VIEWPORT_PRESET_IDS: readonly ["iPhoneSE", "iPhoneXR", "iPhone12Pro", "iPhone14ProMax", "Pixel7", "SamsungGalaxyS8Plus", "SamsungGalaxyS20Ultra", "iPadMini", "iPadAir", "iPadPro", "SurfacePro7", "SurfaceDuo", "GalaxyZFold5", "AsusZenbookFold", "SamsungGalaxyA51A71", "NestHub", "NestHubMax"];
8
+ export declare const BROWSER_VIEWPORT_PRESET_IDS: readonly ['iPhoneSE', 'iPhoneXR', 'iPhone12Pro', 'iPhone14ProMax', 'Pixel7', 'SamsungGalaxyS8Plus', 'SamsungGalaxyS20Ultra', 'iPadMini', 'iPadAir', 'iPadPro', 'SurfacePro7', 'SurfaceDuo', 'GalaxyZFold5', 'AsusZenbookFold', 'SamsungGalaxyA51A71', 'NestHub', 'NestHubMax'];
9
9
  type BrowserViewportPresetId = (typeof BROWSER_VIEWPORT_PRESET_IDS)[number];
10
10
  type BrowserViewportSize = {
11
11
  width: number;
@@ -1,4 +1,4 @@
1
- import type { TestFileInfo } from './protocol';
1
+ import type { TestFileInfo } from './protocol.js';
2
2
  type WatchPlannerProjectEntry = {
3
3
  project: {
4
4
  name: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rstest/browser",
3
- "version": "0.9.9",
3
+ "version": "0.10.0",
4
4
  "description": "Browser mode support for Rstest testing framework.",
5
5
  "keywords": [
6
6
  "rstest",
@@ -32,7 +32,7 @@
32
32
  "default": "./package.json"
33
33
  }
34
34
  },
35
- "main": "./dist/index.js",
35
+ "main": "./dist/browser.js",
36
36
  "files": [
37
37
  "dist",
38
38
  "src",
@@ -45,25 +45,25 @@
45
45
  "open-editor": "^6.0.0",
46
46
  "pathe": "^2.0.3",
47
47
  "sirv": "^3.0.2",
48
- "ws": "^8.20.0"
48
+ "ws": "^8.20.1"
49
49
  },
50
50
  "devDependencies": {
51
- "@rslib/core": "0.21.0",
51
+ "@rslib/core": "0.21.4",
52
52
  "@types/convert-source-map": "^2.0.3",
53
53
  "@types/picomatch": "^4.0.3",
54
54
  "@types/ws": "^8.18.1",
55
- "@typescript/native-preview": "7.0.0-dev.20260412.1",
55
+ "@typescript/native-preview": "7.0.0-dev.20260512.1",
56
56
  "@vitest/snapshot": "^3.2.4",
57
57
  "birpc": "^4.0.0",
58
58
  "picomatch": "^4.0.4",
59
- "playwright": "^1.59.1",
59
+ "playwright": "^1.60.0",
60
60
  "@rstest/browser-ui": "0.0.0",
61
- "@rstest/core": "0.9.9",
62
- "@rstest/tsconfig": "0.0.1"
61
+ "@rstest/tsconfig": "0.0.1",
62
+ "@rstest/core": "0.10.0"
63
63
  },
64
64
  "peerDependencies": {
65
65
  "playwright": "^1.49.1",
66
- "@rstest/core": "^0.9.9"
66
+ "@rstest/core": "^0.10.0"
67
67
  },
68
68
  "peerDependenciesMeta": {
69
69
  "playwright": {