@rstest/browser 0.11.8 → 0.11.10

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.
@@ -1,5 +1,5 @@
1
1
  import { projectSetupLoaders, projectTestContexts, projects } from "__rstest_virtual_browser_manifest__";
2
- import { RSTEST_API_GLOBAL_KEY, RSTEST_ENV_SYMBOL_KEY, RSTEST_IMPORT_META_GLOBAL_KEY, SNAPSHOT_HEADER, createBrowserTaskContext, createRstestRuntime, formatConsoleArgs, globalApis, setRealTimers, unwrapRegex } from "@rstest/core/internal/browser-runtime";
2
+ import { FIXTURE_CLEANUP_TIMEOUT_MS, RSTEST_API_GLOBAL_KEY, RSTEST_ENV_SYMBOL_KEY, RSTEST_IMPORT_META_GLOBAL_KEY, SNAPSHOT_HEADER, cleanupWorkerFixtures as browser_runtime_cleanupWorkerFixtures, createBrowserTaskContext, createRstestRuntime, formatConsoleArgs, getRealTimers, globalApis, setRealTimers, unwrapRegex } from "@rstest/core/internal/browser-runtime";
3
3
  import { normalize } from "pathe";
4
4
  import { AnyMap, TraceMap, encodedMap, originalPositionFor } from "@jridgewell/trace-mapping";
5
5
  import convert_source_map from "convert-source-map";
@@ -186,6 +186,42 @@ class BrowserSnapshotEnvironment {
186
186
  const debugLog = (...args)=>{
187
187
  if (window.__RSTEST_BROWSER_OPTIONS__?.debug) console.log(...args);
188
188
  };
189
+ const cloneCoverage = (coverage)=>JSON.parse(JSON.stringify(coverage));
190
+ const subtractCounters = (current, previous)=>Object.fromEntries(Object.entries(current).map(([key, value])=>[
191
+ key,
192
+ value - (previous?.[key] ?? 0)
193
+ ]));
194
+ const getCoverageDelta = (current, previous)=>Object.fromEntries(Object.entries(current).map(([path, file])=>{
195
+ const previousFile = previous?.[path];
196
+ return [
197
+ path,
198
+ {
199
+ ...file,
200
+ s: subtractCounters(file.s, previousFile?.s),
201
+ f: subtractCounters(file.f, previousFile?.f),
202
+ b: Object.fromEntries(Object.entries(file.b).map(([key, values])=>[
203
+ key,
204
+ values.map((value, index)=>value - (previousFile?.b[key]?.[index] ?? 0))
205
+ ]))
206
+ }
207
+ ];
208
+ }));
209
+ const cleanupWorkerFixturesWithTimeout = async ()=>{
210
+ const realTimers = getRealTimers();
211
+ let timer;
212
+ try {
213
+ await Promise.race([
214
+ browser_runtime_cleanupWorkerFixtures(),
215
+ new Promise((_, reject)=>{
216
+ timer = realTimers.setTimeout?.(()=>{
217
+ reject(new Error(`Worker fixture cleanup did not finish within ${FIXTURE_CLEANUP_TIMEOUT_MS}ms`));
218
+ }, FIXTURE_CLEANUP_TIMEOUT_MS);
219
+ })
220
+ ]);
221
+ } finally{
222
+ if (timer) realTimers.clearTimeout?.(timer);
223
+ }
224
+ };
189
225
  const RSTEST_ENV_SYMBOL = Symbol.for(RSTEST_ENV_SYMBOL_KEY);
190
226
  const installRuntimeGlobals = (runtime, runtimeConfig)=>{
191
227
  Object.assign(globalThis, {
@@ -289,8 +325,9 @@ const waitForConfig = ()=>{
289
325
  if (window.parent === window || window.__RSTEST_BROWSER_OPTIONS__) return Promise.resolve();
290
326
  return new Promise((resolve, reject)=>{
291
327
  const handleMessage = (event)=>{
292
- if (event.data?.type === RSTEST_CONFIG_MESSAGE_TYPE && 'string' == typeof event.data.payload?.runId) {
293
- window.__RSTEST_BROWSER_OPTIONS__ = event.data.payload;
328
+ const payload = event.data?.payload;
329
+ if (event.data?.type === RSTEST_CONFIG_MESSAGE_TYPE && 'string' == typeof payload?.runId) {
330
+ window.__RSTEST_BROWSER_OPTIONS__ = payload;
294
331
  debugLog('[Runner] Received config from container:', event.data.payload);
295
332
  window.removeEventListener('message', handleMessage);
296
333
  resolve();
@@ -364,6 +401,9 @@ const run = async ()=>{
364
401
  setRealTimers();
365
402
  await preloadRunnerSourceMap();
366
403
  const targetTestFile = options.testFile;
404
+ const targetTestFiles = options.testFiles?.length ? options.testFiles : targetTestFile ? [
405
+ targetTestFile
406
+ ] : void 0;
367
407
  const currentProject = targetTestFile ? findProjectForTestFile(targetTestFile, projects) : projects["0"];
368
408
  if (!currentProject) {
369
409
  send({
@@ -404,12 +444,7 @@ const run = async ()=>{
404
444
  for (const loadSetup of currentSetupLoaders)await loadSetup();
405
445
  };
406
446
  let testKeysToRun;
407
- if (targetTestFile) {
408
- const key = toContextKey(targetTestFile, currentProject.projectRoot);
409
- testKeysToRun = [
410
- key
411
- ];
412
- } else testKeysToRun = currentTestContext.getTestKeys();
447
+ testKeysToRun = targetTestFiles ? targetTestFiles.map((testFile)=>toContextKey(testFile, currentProject.projectRoot)) : currentTestContext.getTestKeys();
413
448
  const executionMode = options.mode || 'run';
414
449
  if ('collect' === executionMode) {
415
450
  for (const key of testKeysToRun){
@@ -478,183 +513,269 @@ const run = async ()=>{
478
513
  };
479
514
  window.addEventListener('error', onWindowError);
480
515
  window.addEventListener('unhandledrejection', onUnhandledRejection);
481
- for (const key of testKeysToRun){
482
- const testPath = toAbsolutePath(key, currentProject.projectRoot);
483
- const taskStack = [
484
- {
485
- taskId: getFileTaskId(testPath),
486
- taskType: 'file',
487
- testPath
488
- }
489
- ];
490
- const taskContext = createBrowserTaskContext();
491
- const shouldInterceptConsole = !runtimeConfig.disableConsoleIntercept || true === runtimeConfig.silent || 'passed-only' === runtimeConfig.silent;
492
- const restoreConsole = shouldInterceptConsole ? interceptConsole(projectRuntime.name, ()=>taskContext.getCurrent() ?? taskStack[taskStack.length - 1], runtimeConfig.disableConsoleIntercept ? false : runtimeConfig.printConsoleTrace ?? false) : ()=>{};
493
- const workerState = {
494
- project: projectRuntime.name,
495
- projectRoot: projectRuntime.projectRoot,
496
- rootPath: options.rootPath,
497
- runtimeConfig,
498
- taskId: 0,
499
- buildId: 0,
500
- outputModule: false,
501
- environment: 'browser',
502
- currentTask: taskStack[0],
503
- testPath,
504
- distPath: testPath,
505
- snapshotOptions: {
506
- updateSnapshot: options.snapshot.updateSnapshot,
507
- snapshotEnvironment: new BrowserSnapshotEnvironment(),
508
- snapshotFormat: runtimeConfig.snapshotFormat
509
- }
510
- };
511
- const syncCurrentTask = ()=>{
512
- workerState.currentTask = taskStack[taskStack.length - 1];
513
- };
514
- const removeTaskFromStack = (taskId)=>{
515
- const taskIndex = taskStack.findLastIndex((task)=>task.taskId === taskId);
516
- if (taskIndex < 0) return;
517
- taskStack.splice(taskIndex, 1);
518
- syncCurrentTask();
519
- };
520
- const runtime = await createRstestRuntime(workerState, {
521
- taskContext
522
- });
523
- installRuntimeGlobals(runtime, runtimeConfig);
524
- let failedTestsCount = 0;
525
- const dispatchFileCleanup = async (method, result, waitForAcknowledgement = true)=>{
526
- const requestId = createRequestId(`file-cleanup-${method}`);
527
- const request = {
528
- requestId,
529
- namespace: DISPATCH_NAMESPACE_FILE_CLEANUP,
530
- method,
531
- args: {
532
- projectName: projectRuntime.name,
533
- result,
534
- runId: getRunIdentity(),
516
+ const keepWorkerFixtures = false === runtimeConfig.isolate && true !== projectRuntime.hasSetupFiles;
517
+ let restoreWorkerConsole;
518
+ let workerCleanupFailed = false;
519
+ let workerCleanupAttempted = false;
520
+ let setupListeners;
521
+ let previousIstanbulCoverage;
522
+ try {
523
+ for(let fileIndex = 0; fileIndex < testKeysToRun.length; fileIndex++){
524
+ const key = testKeysToRun[fileIndex];
525
+ const testPath = toAbsolutePath(key, currentProject.projectRoot);
526
+ options = {
527
+ ...options,
528
+ testFile: testPath
529
+ };
530
+ window.__RSTEST_BROWSER_OPTIONS__ = options;
531
+ const taskStack = [
532
+ {
533
+ taskId: getFileTaskId(testPath),
534
+ taskType: 'file',
535
535
  testPath
536
536
  }
537
+ ];
538
+ const taskContext = createBrowserTaskContext();
539
+ const shouldInterceptConsole = !runtimeConfig.disableConsoleIntercept || true === runtimeConfig.silent || 'passed-only' === runtimeConfig.silent;
540
+ const restoreConsole = shouldInterceptConsole ? interceptConsole(projectRuntime.name, ()=>taskContext.getCurrent() ?? taskStack[taskStack.length - 1], runtimeConfig.disableConsoleIntercept ? false : runtimeConfig.printConsoleTrace ?? false) : ()=>{};
541
+ if (keepWorkerFixtures) restoreWorkerConsole = restoreConsole;
542
+ const workerState = {
543
+ project: projectRuntime.name,
544
+ projectRoot: projectRuntime.projectRoot,
545
+ rootPath: options.rootPath,
546
+ runtimeConfig,
547
+ taskId: 0,
548
+ buildId: 0,
549
+ outputModule: false,
550
+ environment: 'browser',
551
+ currentTask: taskStack[0],
552
+ testPath,
553
+ distPath: testPath,
554
+ snapshotOptions: {
555
+ updateSnapshot: options.snapshot.updateSnapshot,
556
+ snapshotEnvironment: new BrowserSnapshotEnvironment(),
557
+ snapshotFormat: runtimeConfig.snapshotFormat
558
+ }
559
+ };
560
+ const syncCurrentTask = ()=>{
561
+ workerState.currentTask = taskStack[taskStack.length - 1];
562
+ };
563
+ const removeTaskFromStack = (taskId)=>{
564
+ const taskIndex = taskStack.findLastIndex((task)=>task.taskId === taskId);
565
+ if (taskIndex < 0) return;
566
+ taskStack.splice(taskIndex, 1);
567
+ syncCurrentTask();
568
+ };
569
+ let runtime;
570
+ try {
571
+ runtime = await createRstestRuntime(workerState, {
572
+ taskContext
573
+ });
574
+ installRuntimeGlobals(runtime, runtimeConfig);
575
+ } catch (error) {
576
+ restoreConsole();
577
+ throw error;
578
+ }
579
+ let failedTestsCount = 0;
580
+ const dispatchFileCleanup = async (method, result, waitForAcknowledgement = true)=>{
581
+ const requestId = createRequestId(`file-cleanup-${method}`);
582
+ const request = {
583
+ requestId,
584
+ namespace: DISPATCH_NAMESPACE_FILE_CLEANUP,
585
+ method,
586
+ args: {
587
+ projectName: projectRuntime.name,
588
+ result,
589
+ runId: getRunIdentity(),
590
+ testPath
591
+ }
592
+ };
593
+ if (!waitForAcknowledgement) return void sendDispatchRequest(request);
594
+ await dispatchRpc({
595
+ requestId,
596
+ request,
597
+ timeoutMs: getRpcTimeout('framework'),
598
+ timeoutMessage: `File cleanup ${method} acknowledgement timed out for ${testPath}.`,
599
+ staleMessage: `File cleanup ${method} became stale for ${testPath}.`
600
+ });
537
601
  };
538
- if (!waitForAcknowledgement) return void sendDispatchRequest(request);
539
- await dispatchRpc({
540
- requestId,
541
- request,
542
- timeoutMs: getRpcTimeout('framework'),
543
- timeoutMessage: `File cleanup ${method} acknowledgement timed out for ${testPath}.`,
544
- staleMessage: `File cleanup ${method} became stale for ${testPath}.`
602
+ const cleanupWorkerFixtures = async (result)=>{
603
+ await dispatchFileCleanup('worker-start', result);
604
+ try {
605
+ await cleanupWorkerFixturesWithTimeout();
606
+ } finally{
607
+ await dispatchFileCleanup('worker-end');
608
+ }
609
+ };
610
+ const updateIstanbulCoverage = (result)=>{
611
+ if (!globalThis.__coverage__) return;
612
+ const currentCoverage = globalThis.__coverage__;
613
+ result.coverage = getCoverageDelta(currentCoverage, previousIstanbulCoverage);
614
+ previousIstanbulCoverage = cloneCoverage(currentCoverage);
615
+ };
616
+ const runnerHooks = {
617
+ onFileCleanupStart: async (result)=>{
618
+ if (result && globalThis.__coverage__) result.coverage = globalThis.__coverage__;
619
+ await dispatchFileCleanup('start', result, window.parent !== window);
620
+ },
621
+ onFileCleanupEnd: ()=>dispatchFileCleanup('end', void 0, window.parent !== window),
622
+ onSnapshotSetupStart: async ()=>{
623
+ setRpcPhase('framework');
624
+ },
625
+ onSnapshotSetupEnd: async ()=>{
626
+ setRpcPhase('test');
627
+ },
628
+ onSnapshotFinishStart: async ()=>{
629
+ setRpcPhase('framework');
630
+ },
631
+ onSnapshotFinishEnd: async ()=>{
632
+ setRpcPhase('test');
633
+ },
634
+ onTestFileReady: async (test)=>{
635
+ dispatchRunnerLifecycle('file-ready', test);
636
+ },
637
+ onTestSuiteStart: async (test)=>{
638
+ taskStack.push({
639
+ taskId: test.testId,
640
+ taskName: test.name,
641
+ taskParentNames: test.parentNames,
642
+ taskType: 'suite',
643
+ testPath: test.testPath
644
+ });
645
+ syncCurrentTask();
646
+ dispatchRunnerLifecycle('suite-start', test);
647
+ },
648
+ onTestSuiteResult: async (result)=>{
649
+ removeTaskFromStack(result.testId);
650
+ dispatchRunnerLifecycle('suite-result', result);
651
+ },
652
+ onTestCaseStart: async (test)=>{
653
+ taskStack.push({
654
+ taskId: test.testId,
655
+ taskName: test.name,
656
+ taskParentNames: test.parentNames,
657
+ taskType: 'case',
658
+ testPath: test.testPath
659
+ });
660
+ syncCurrentTask();
661
+ dispatchRunnerLifecycle('case-start', test);
662
+ },
663
+ onTestCaseResult: async (result)=>{
664
+ removeTaskFromStack(result.testId);
665
+ if ('fail' === result.status) failedTestsCount++;
666
+ send({
667
+ type: 'case-result',
668
+ payload: result
669
+ });
670
+ },
671
+ getCountOfFailedTests: async ()=>failedTestsCount
672
+ };
673
+ send({
674
+ type: 'file-start',
675
+ payload: {
676
+ testPath,
677
+ projectName: projectRuntime.name
678
+ }
545
679
  });
546
- };
547
- const runnerHooks = {
548
- onFileCleanupStart: async (result)=>{
549
- if (result && globalThis.__coverage__) result.coverage = globalThis.__coverage__;
550
- await dispatchFileCleanup('start', result, window.parent !== window);
551
- },
552
- onFileCleanupEnd: ()=>dispatchFileCleanup('end', void 0, window.parent !== window),
553
- onSnapshotSetupStart: async ()=>{
554
- setRpcPhase('framework');
555
- },
556
- onSnapshotSetupEnd: async ()=>{
557
- setRpcPhase('test');
558
- },
559
- onSnapshotFinishStart: async ()=>{
680
+ const unhandledErrors = [];
681
+ activeUnhandledErrors = unhandledErrors;
682
+ try {
560
683
  setRpcPhase('framework');
561
- },
562
- onSnapshotFinishEnd: async ()=>{
563
- setRpcPhase('test');
564
- },
565
- onTestFileReady: async (test)=>{
566
- dispatchRunnerLifecycle('file-ready', test);
567
- },
568
- onTestSuiteStart: async (test)=>{
569
- taskStack.push({
570
- taskId: test.testId,
571
- taskName: test.name,
572
- taskParentNames: test.parentNames,
573
- taskType: 'suite',
574
- testPath: test.testPath
575
- });
576
- syncCurrentTask();
577
- dispatchRunnerLifecycle('suite-start', test);
578
- },
579
- onTestSuiteResult: async (result)=>{
580
- removeTaskFromStack(result.testId);
581
- dispatchRunnerLifecycle('suite-result', result);
582
- },
583
- onTestCaseStart: async (test)=>{
584
- taskStack.push({
585
- taskId: test.testId,
586
- taskName: test.name,
587
- taskParentNames: test.parentNames,
588
- taskType: 'case',
589
- testPath: test.testPath
684
+ if (setupListeners) runtime.runner.setRootSuiteListeners(setupListeners);
685
+ await loadSetupFiles();
686
+ setupListeners ??= runtime.runner.getRootSuiteListeners();
687
+ const beforeScripts = getScriptUrls();
688
+ await currentTestContext.loadTest(key);
689
+ const afterScripts = getScriptUrls();
690
+ const chunkUrl = findNewScriptUrl(beforeScripts, afterScripts);
691
+ if (chunkUrl) await preloadTestFileSourceMap(chunkUrl);
692
+ const result = await runtime.runner.runTests(testPath, runnerHooks, runtime.api);
693
+ const cleanupBeforeFileComplete = !keepWorkerFixtures || fileIndex === testKeysToRun.length - 1;
694
+ if (cleanupBeforeFileComplete) {
695
+ workerCleanupAttempted = true;
696
+ try {
697
+ await cleanupWorkerFixtures(result);
698
+ } catch (cleanupError) {
699
+ const formattedCleanupError = cleanupError instanceof Error ? cleanupError : new Error(String(cleanupError));
700
+ result.status = 'fail';
701
+ result.errors = [
702
+ ...result.errors ?? [],
703
+ {
704
+ fullStack: true,
705
+ message: `Worker fixture cleanup failed: ${formattedCleanupError.message}`,
706
+ name: formattedCleanupError.name,
707
+ stack: formattedCleanupError.stack
708
+ }
709
+ ];
710
+ }
711
+ }
712
+ updateIstanbulCoverage(result);
713
+ for(let i = 0; i < 2; i++)await new Promise((resolve)=>{
714
+ setTimeout(resolve, 0);
590
715
  });
591
- syncCurrentTask();
592
- dispatchRunnerLifecycle('case-start', test);
593
- },
594
- onTestCaseResult: async (result)=>{
595
- removeTaskFromStack(result.testId);
596
- if ('fail' === result.status) failedTestsCount++;
716
+ if (unhandledErrors.length > 0) {
717
+ result.status = 'fail';
718
+ result.errors = [
719
+ ...result.errors ?? [],
720
+ ...unhandledErrors.map((error)=>({
721
+ name: error.name,
722
+ message: error.message,
723
+ stack: error.stack
724
+ }))
725
+ ];
726
+ }
597
727
  send({
598
- type: 'case-result',
728
+ type: 'file-complete',
599
729
  payload: result
600
730
  });
601
- },
602
- getCountOfFailedTests: async ()=>failedTestsCount
603
- };
604
- send({
605
- type: 'file-start',
606
- payload: {
607
- testPath,
608
- projectName: projectRuntime.name
609
- }
610
- });
611
- const unhandledErrors = [];
612
- activeUnhandledErrors = unhandledErrors;
613
- try {
614
- setRpcPhase('framework');
615
- await loadSetupFiles();
616
- const beforeScripts = getScriptUrls();
617
- await currentTestContext.loadTest(key);
618
- const afterScripts = getScriptUrls();
619
- const chunkUrl = findNewScriptUrl(beforeScripts, afterScripts);
620
- if (chunkUrl) await preloadTestFileSourceMap(chunkUrl);
621
- const result = await runtime.runner.runTests(testPath, runnerHooks, runtime.api);
622
- for(let i = 0; i < 2; i++)await new Promise((resolve)=>{
623
- setTimeout(resolve, 0);
624
- });
625
- if (unhandledErrors.length > 0) {
626
- result.status = 'fail';
627
- result.errors = [
628
- ...result.errors ?? [],
629
- ...unhandledErrors.map((error)=>({
630
- name: error.name,
631
- message: error.message,
632
- stack: error.stack
633
- }))
634
- ];
731
+ } catch (_error) {
732
+ let error = _error instanceof Error ? _error : new Error(String(_error));
733
+ if (!workerCleanupAttempted) try {
734
+ workerCleanupAttempted = true;
735
+ await cleanupWorkerFixtures();
736
+ } catch (cleanupError) {
737
+ const formattedCleanupError = cleanupError instanceof Error ? cleanupError : new Error(String(cleanupError));
738
+ error = new Error(`${error.message}\nWorker fixture cleanup failed: ${formattedCleanupError.message}`, {
739
+ cause: error
740
+ });
741
+ }
742
+ send({
743
+ type: 'fatal',
744
+ payload: {
745
+ message: error.message,
746
+ stack: error.stack
747
+ }
748
+ });
749
+ window.__RSTEST_DONE__ = true;
750
+ return;
751
+ } finally{
752
+ if (keepWorkerFixtures && fileIndex === testKeysToRun.length - 1) restoreWorkerConsole = restoreConsole;
753
+ else restoreConsole();
754
+ activeUnhandledErrors = void 0;
635
755
  }
636
- send({
637
- type: 'file-complete',
638
- payload: result
639
- });
640
- } catch (_error) {
641
- const error = _error instanceof Error ? _error : new Error(String(_error));
756
+ }
757
+ } finally{
758
+ if (keepWorkerFixtures && !workerCleanupAttempted) try {
759
+ await cleanupWorkerFixturesWithTimeout();
760
+ } catch (error) {
761
+ workerCleanupFailed = true;
762
+ const cleanupError = error instanceof Error ? error : new Error(String(error));
642
763
  send({
643
764
  type: 'fatal',
644
765
  payload: {
645
- message: error.message,
646
- stack: error.stack
766
+ message: cleanupError.message,
767
+ stack: cleanupError.stack
647
768
  }
648
769
  });
649
- window.__RSTEST_DONE__ = true;
650
- return;
651
- } finally{
652
- restoreConsole();
653
- activeUnhandledErrors = void 0;
654
770
  }
771
+ restoreWorkerConsole?.();
655
772
  }
656
773
  window.removeEventListener('error', onWindowError);
657
774
  window.removeEventListener('unhandledrejection', onUnhandledRejection);
775
+ if (workerCleanupFailed) {
776
+ window.__RSTEST_DONE__ = true;
777
+ return;
778
+ }
658
779
  send({
659
780
  type: 'complete'
660
781
  });