@rstest/browser 0.11.4 → 0.11.6

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 (40) hide show
  1. package/dist/browser-container/container-static/js/683.4f821ce8f1.js +194 -0
  2. package/dist/browser-container/container-static/js/index.568aa7cb35.js +1 -0
  3. package/dist/browser-container/container-static/js/lib-react.e24a1d366b.js +2 -0
  4. package/dist/browser-container/index.html +1 -1
  5. package/dist/browserExecutor.d.ts +7 -5
  6. package/dist/browserRsbuild.d.ts +119 -0
  7. package/dist/containerRpc.d.ts +52 -0
  8. package/dist/dispatchCapabilities.d.ts +1 -2
  9. package/dist/headedScheduler.d.ts +58 -0
  10. package/dist/headlessScheduler.d.ts +37 -0
  11. package/dist/hostController.d.ts +12 -47
  12. package/dist/hostPayloads.d.ts +30 -0
  13. package/dist/index.js +1738 -1793
  14. package/dist/protocol.d.ts +5 -0
  15. package/dist/schedulerSeam.d.ts +37 -0
  16. package/dist/watchRerunPlanner.d.ts +5 -0
  17. package/dist/watchRuntime.d.ts +21 -0
  18. package/dist/watchSignals.d.ts +22 -0
  19. package/package.json +5 -5
  20. package/src/browserExecutor.ts +90 -8
  21. package/src/browserRsbuild.ts +1927 -0
  22. package/src/client/entry.ts +3 -0
  23. package/src/containerRpc.ts +206 -0
  24. package/src/dispatchCapabilities.ts +1 -6
  25. package/src/headedScheduler.ts +664 -0
  26. package/src/headlessScheduler.ts +566 -0
  27. package/src/hostController.ts +855 -4163
  28. package/src/hostPayloads.ts +62 -0
  29. package/src/protocol.ts +6 -0
  30. package/src/schedulerSeam.ts +49 -0
  31. package/src/watchRerunPlanner.ts +18 -7
  32. package/src/watchRuntime.ts +83 -0
  33. package/src/watchSignals.ts +93 -0
  34. package/dist/browser-container/container-static/js/243.a8eed2b9e7.js +0 -27406
  35. package/dist/browser-container/container-static/js/243.a8eed2b9e7.js.LICENSE.txt +0 -1
  36. package/dist/browser-container/container-static/js/index.84aaafaf21.js +0 -3058
  37. package/dist/browser-container/container-static/js/lib-react.62b27a21db.js +0 -8454
  38. package/dist/browser-container/container-static/js/lib-react.62b27a21db.js.LICENSE.txt +0 -1
  39. package/dist/headlessLatestRerunScheduler.d.ts +0 -18
  40. package/src/headlessLatestRerunScheduler.ts +0 -76
@@ -0,0 +1,62 @@
1
+ import type { Reporter, TestFileResult } from '@rstest/core/internal/browser';
2
+ import type { BrowserLogPayload } from './protocol';
3
+
4
+ /** Payload for test file start event */
5
+ export type TestFileStartPayload = {
6
+ testPath: string;
7
+ projectName: string;
8
+ };
9
+
10
+ /** Payload for log event — single-sourced from the wire protocol. */
11
+ export type LogPayload = BrowserLogPayload;
12
+
13
+ /** Payload for fatal error event */
14
+ export type FatalPayload = {
15
+ message: string;
16
+ stack?: string;
17
+ };
18
+
19
+ export type ReporterHookArg<THook extends keyof Reporter> =
20
+ NonNullable<Reporter[THook]> extends (...args: infer TArgs) => unknown
21
+ ? TArgs[0]
22
+ : never;
23
+
24
+ export type TestFileReadyPayload = ReporterHookArg<'onTestFileReady'>;
25
+ export type TestSuiteStartPayload = ReporterHookArg<'onTestSuiteStart'>;
26
+ export type TestSuiteResultPayload = ReporterHookArg<'onTestSuiteResult'>;
27
+ export type TestCaseStartPayload = ReporterHookArg<'onTestCaseStart'>;
28
+ export type ReloadTestFileAck = {
29
+ runId: string;
30
+ };
31
+ export type HeadedTestFileCompletePayload = TestFileResult & {
32
+ runId?: string;
33
+ };
34
+
35
+ export type DeferredPromise<T> = {
36
+ promise: Promise<T>;
37
+ resolve: (value: T | PromiseLike<T>) => void;
38
+ reject: (reason?: unknown) => void;
39
+ };
40
+
41
+ export const getFileTaskId = (testPath: string): string => {
42
+ return `file:${testPath}`;
43
+ };
44
+
45
+ export const toError = (error: unknown): Error => {
46
+ return error instanceof Error ? error : new Error(String(error));
47
+ };
48
+
49
+ export const createDeferredPromise = <T>(): DeferredPromise<T> => {
50
+ let resolve!: DeferredPromise<T>['resolve'];
51
+ let reject!: DeferredPromise<T>['reject'];
52
+ const promise = new Promise<T>((res, rej) => {
53
+ resolve = res;
54
+ reject = rej;
55
+ });
56
+
57
+ return {
58
+ promise,
59
+ resolve,
60
+ reject,
61
+ };
62
+ };
package/src/protocol.ts CHANGED
@@ -69,6 +69,12 @@ export type BrowserExecutionMode = 'run' | 'collect';
69
69
  export type BrowserLogPayload = {
70
70
  level: 'log' | 'warn' | 'error' | 'info' | 'debug';
71
71
  content: string;
72
+ /**
73
+ * Owning project, resolved by the client from its manifest. The host must
74
+ * not re-derive it from `testPath` — concurrent projects can run the same
75
+ * file, so a path-keyed lookup can attribute the log to the wrong project.
76
+ */
77
+ projectName: string;
72
78
  taskId?: string;
73
79
  taskName?: string;
74
80
  taskParentNames?: string[];
@@ -0,0 +1,49 @@
1
+ import type { ExecutorCycleOutcome } from '@rstest/core/internal/browser';
2
+ import type { BrowserDispatchRequest } from './protocol';
3
+ import type { BrowserProviderPage } from './providers';
4
+
5
+ /**
6
+ * The contract between the controller and whichever run branch executes a run.
7
+ * It lives outside both so the schedulers never reach back into the controller
8
+ * for a type: the controller owns the run's shape, the schedulers own the run.
9
+ */
10
+
11
+ /**
12
+ * The watch-session control surface a watch-mode controller run hands back with
13
+ * its initial cycle. Every rerun trigger the host owns (dev rebuild, HMR, the
14
+ * in-page rerun button) resolves its own scope and then signals core's
15
+ * invalidation subscriber; core resets the cycle state and calls back into
16
+ * {@link BrowserWatchSession.runCycle} to execute it. A trigger that resolves to
17
+ * no work never signals, so a scope matching none of this host's files produces
18
+ * no cycle and no cycle output.
19
+ */
20
+ export type BrowserWatchSession = {
21
+ /** Execute the scope the last trigger resolved, as one cycle outcome. */
22
+ runCycle: (testPaths: string[]) => Promise<ExecutorCycleOutcome>;
23
+ /** Explicit path-scoped rerun request (a CLI shortcut's browser fanout). */
24
+ requestRerun: (testPaths?: string[]) => Promise<void>;
25
+ };
26
+
27
+ /**
28
+ * What a run branch produces. Results and errors accumulate in the sinks the
29
+ * controller owns, so a scheduler reports only what it alone knows: how long
30
+ * the tests took, the session it left behind, and how to tear down a runtime
31
+ * that does not outlive the run.
32
+ */
33
+ export type SchedulerRunResult = {
34
+ testTime: number;
35
+ watchSession?: BrowserWatchSession;
36
+ close?: () => Promise<void>;
37
+ };
38
+
39
+ /**
40
+ * How the shared dispatch layer reaches the pages of whichever run branch is
41
+ * live. Only the running branch knows its pages, so it installs the resolver
42
+ * and the dispatch layer stays branch-agnostic.
43
+ */
44
+ export type DispatchPageResolver = (
45
+ target?: BrowserDispatchRequest['target'],
46
+ ) => {
47
+ runnerPage?: BrowserProviderPage;
48
+ containerPage?: BrowserProviderPage;
49
+ };
@@ -1,6 +1,21 @@
1
1
  import { normalize } from 'pathe';
2
2
  import type { TestFileInfo } from './protocol';
3
3
 
4
+ /**
5
+ * Paths the previous cycle ran that the current file set no longer contains.
6
+ * Core prunes its own state from this, so a file deleted mid-session stops
7
+ * being reported instead of lingering as a passing result.
8
+ */
9
+ export const collectDeletedTestPaths = (
10
+ previous: TestFileInfo[],
11
+ current: TestFileInfo[],
12
+ ): string[] => {
13
+ const currentPathSet = new Set(current.map((file) => file.testPath));
14
+ return previous
15
+ .map((file) => file.testPath)
16
+ .filter((testPath) => !currentPathSet.has(testPath));
17
+ };
18
+
4
19
  type WatchPlannerProjectEntry = {
5
20
  project: {
6
21
  name: string;
@@ -59,15 +74,11 @@ export const planWatchRerun = ({
59
74
  const normalizedAffectedTestFiles = affectedTestFiles.map((testFile) =>
60
75
  normalize(testFile),
61
76
  );
62
-
63
- const currentFileMap = new Map(
64
- currentTestFiles.map((file) => [file.testPath, file] as const),
77
+ const affectedPathSet = new Set(normalizedAffectedTestFiles);
78
+ const matchedAffectedFiles = currentTestFiles.filter((file) =>
79
+ affectedPathSet.has(file.testPath),
65
80
  );
66
81
 
67
- const matchedAffectedFiles = normalizedAffectedTestFiles
68
- .map((testFile) => currentFileMap.get(testFile))
69
- .filter((file): file is TestFileInfo => Boolean(file));
70
-
71
82
  return {
72
83
  currentTestFiles,
73
84
  filesChanged,
@@ -0,0 +1,83 @@
1
+ import { FATAL_SIGNALS } from '@rstest/core/internal/browser';
2
+ import { destroyBrowserRuntime, type BrowserRuntime } from './browserRsbuild';
3
+
4
+ // Only process-wide concerns stay module-level: the runtime handle reused
5
+ // across controller re-entry (config-change restarts), and the signal/exit
6
+ // cleanup that must run once per process. Diff/rerun state lives on
7
+ // `BrowserRuntime.watchState`.
8
+ export type WatchContext = {
9
+ runtime: BrowserRuntime | null;
10
+ cleanupRegistered: boolean;
11
+ cleanupPromise: Promise<void> | null;
12
+ };
13
+
14
+ export const watchContext: WatchContext = {
15
+ runtime: null,
16
+ cleanupRegistered: false,
17
+ cleanupPromise: null,
18
+ };
19
+
20
+ /**
21
+ * Tear down the persistent watch runtime (dev servers, provider, browser,
22
+ * WebSocket server). Idempotent, and the single teardown the browser executor's
23
+ * `close` and the process-exit nets both go through.
24
+ */
25
+ export const runWatchRuntimeTeardown = <T>(
26
+ state: { runtime: T | null; cleanupPromise: Promise<void> | null },
27
+ destroy: (runtime: T) => Promise<void>,
28
+ ): Promise<void> => {
29
+ if (state.cleanupPromise) {
30
+ return state.cleanupPromise;
31
+ }
32
+
33
+ state.cleanupPromise = (async () => {
34
+ if (!state.runtime) {
35
+ return;
36
+ }
37
+
38
+ await destroy(state.runtime);
39
+ state.runtime = null;
40
+ })();
41
+
42
+ // The memo is released once this teardown settles, because the state outlives
43
+ // the session: a config-file change restarts the run against a fresh runtime,
44
+ // and a memo left resolved from the previous session would make every later
45
+ // teardown a no-op — leaving the session after that to reuse a runtime built
46
+ // from the pre-restart config. Idempotency only has to hold within a runtime.
47
+ return state.cleanupPromise.finally(() => {
48
+ state.cleanupPromise = null;
49
+ });
50
+ };
51
+
52
+ export const cleanupWatchRuntime = (): Promise<void> =>
53
+ // `cleanupRegistered` is deliberately not re-armed alongside the memo: the
54
+ // signal nets it installs read `watchContext.runtime` live, so they stay
55
+ // correct across a restart, and re-registering would stack a fresh set of
56
+ // listeners on every one.
57
+ runWatchRuntimeTeardown(watchContext, destroyBrowserRuntime);
58
+
59
+ export const registerWatchCleanup = (embedded: boolean): void => {
60
+ if (watchContext.cleanupRegistered) {
61
+ return;
62
+ }
63
+ watchContext.cleanupRegistered = true;
64
+
65
+ // Embedded (programmatic) hosts own the process lifecycle; they tear the
66
+ // session down through the browser executor's `close` instead of signals.
67
+ if (embedded) {
68
+ return;
69
+ }
70
+
71
+ // Cleanup-only nets: core's watch loop owns the signal → exit-code path and
72
+ // awaits the same idempotent `cleanupWatchRuntime` promise through the
73
+ // browser executor's `close`.
74
+ for (const signal of FATAL_SIGNALS) {
75
+ process.once(signal, () => {
76
+ void cleanupWatchRuntime();
77
+ });
78
+ }
79
+
80
+ process.once('exit', () => {
81
+ void cleanupWatchRuntime();
82
+ });
83
+ };
@@ -0,0 +1,93 @@
1
+ import {
2
+ color,
3
+ type ExecutorInvalidationCallback,
4
+ logger,
5
+ } from '@rstest/core/internal/browser';
6
+
7
+ /**
8
+ * The handover between this host's rerun triggers and core's watch-cycle
9
+ * driver. Triggers reach it from three unrelated places — the bundler's
10
+ * dev-compile hook, a CLI shortcut's fanout, the in-page rerun button — and
11
+ * only the ordering inside `signalInvalidation` keeps them from racing each
12
+ * other. Each run branch installs the pieces it owns; nothing here knows which
13
+ * branch is running.
14
+ */
15
+ export type WatchSignals = ReturnType<typeof createWatchSignals>;
16
+
17
+ export const createWatchSignals = (
18
+ onInvalidate: ExecutorInvalidationCallback | undefined,
19
+ ) => {
20
+ // The transport-owned rerun trigger, installed by whichever run branch
21
+ // (headless/headed) is driving this run: resolve this rebuild's scope, then
22
+ // hand it to core's invalidation subscriber. Populated after the initial
23
+ // cycle.
24
+ let dispatchRerun: (() => Promise<void>) | undefined;
25
+
26
+ /**
27
+ * Latest-wins interrupt, installed by the run branch whose in-flight run can
28
+ * be cut short (headless). Core serializes cycles, so a trigger arriving
29
+ * mid-cycle would otherwise wait out a run the user has already superseded.
30
+ */
31
+ let interruptInFlightRun: (() => Promise<void>) | undefined;
32
+
33
+ /**
34
+ * The cycle core is running for the scope last signalled. Only an explicit
35
+ * request awaits it, right after it dispatched: a CLI shortcut's
36
+ * `updateSnapshot` stays flipped only until `requestRerun` resolves, and the
37
+ * in-page rerun button answers its RPC when the rerun is done. A rejection is
38
+ * reported here rather than left to an awaiting caller, because compile-driven
39
+ * triggers have no caller — and a failed cycle must not end the session.
40
+ */
41
+ let signalledCycle: Promise<void> | undefined;
42
+
43
+ return {
44
+ setDispatchRerun(fn: () => Promise<void>): void {
45
+ dispatchRerun = fn;
46
+ },
47
+ async runDispatchRerun(): Promise<void> {
48
+ await dispatchRerun?.();
49
+ },
50
+ setInterrupt(fn: () => Promise<void>): void {
51
+ interruptInFlightRun = fn;
52
+ },
53
+ /**
54
+ * Hand the scope this trigger resolved to core, which resets the cycle state,
55
+ * calls back into the session's `runCycle`, and finalizes.
56
+ *
57
+ * The cycle is deliberately *not* awaited here. Rebuild triggers reach this
58
+ * from inside the bundler's dev-compile hook, and the bundler keeps no
59
+ * watcher attached while that hook is pending: anything created or deleted in
60
+ * that window is never seen, so it never rebuilds and never reruns. Holding
61
+ * the hook for a whole cycle widens that blind window to the cycle's full
62
+ * duration, which loses test files added or removed mid-run for good. Core
63
+ * serializes the cycles itself, so nothing here has to.
64
+ *
65
+ * The in-flight run is cut short here rather than at the trigger, because only
66
+ * this point knows a replacement cycle is actually coming: a trigger that
67
+ * resolves to no affected files must leave the running cycle alone, or it
68
+ * finalizes on results it never produced.
69
+ */
70
+ async signalInvalidation(
71
+ fileFilters: string[],
72
+ /**
73
+ * Run state this trigger binds to its own paths, taken in the same turn as
74
+ * the handover — after any interrupt, so no queued cycle can be dequeued in
75
+ * between and read it. The headed rerun's per-file test-name pattern is the
76
+ * one such state; core's cycle options cannot carry it, so the only thing
77
+ * that makes it the property of one cycle is claiming it here.
78
+ */
79
+ claimScope?: () => void,
80
+ ): Promise<void> {
81
+ await interruptInFlightRun?.();
82
+ claimScope?.();
83
+ signalledCycle = Promise.resolve(
84
+ onInvalidate?.({ isFirstBuild: false, fileFilters }),
85
+ ).catch((error) => {
86
+ logger.error(color.red('Browser Mode watch cycle failed:'), error);
87
+ });
88
+ },
89
+ async awaitSignalledCycle(): Promise<void> {
90
+ await signalledCycle;
91
+ },
92
+ };
93
+ };