@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
@@ -45,6 +45,11 @@ export type BrowserProjectRuntime = {
45
45
  */ export type BrowserLogPayload = {
46
46
  level: 'log' | 'warn' | 'error' | 'info' | 'debug';
47
47
  content: string;
48
+ /**
49
+ * Owning project, resolved by the client from its manifest. The host must
50
+ * not re-derive it from `testPath` — concurrent projects can run the same
51
+ * file, so a path-keyed lookup can attribute the log to the wrong project.
52
+ */ projectName: string;
48
53
  taskId?: string;
49
54
  taskName?: string;
50
55
  taskParentNames?: string[];
@@ -0,0 +1,37 @@
1
+ import type { ExecutorCycleOutcome } from '@rstest/core/internal/browser';
2
+ import type { BrowserDispatchRequest } from './protocol.js';
3
+ import type { BrowserProviderPage } from './providers/index.js';
4
+ /**
5
+ * The contract between the controller and whichever run branch executes a run.
6
+ * It lives outside both so the schedulers never reach back into the controller
7
+ * for a type: the controller owns the run's shape, the schedulers own the run.
8
+ */ /**
9
+ * The watch-session control surface a watch-mode controller run hands back with
10
+ * its initial cycle. Every rerun trigger the host owns (dev rebuild, HMR, the
11
+ * in-page rerun button) resolves its own scope and then signals core's
12
+ * invalidation subscriber; core resets the cycle state and calls back into
13
+ * {@link BrowserWatchSession.runCycle} to execute it. A trigger that resolves to
14
+ * no work never signals, so a scope matching none of this host's files produces
15
+ * no cycle and no cycle output.
16
+ */ export type BrowserWatchSession = {
17
+ /** Execute the scope the last trigger resolved, as one cycle outcome. */ runCycle: (testPaths: string[]) => Promise<ExecutorCycleOutcome>;
18
+ /** Explicit path-scoped rerun request (a CLI shortcut's browser fanout). */ requestRerun: (testPaths?: string[]) => Promise<void>;
19
+ };
20
+ /**
21
+ * What a run branch produces. Results and errors accumulate in the sinks the
22
+ * controller owns, so a scheduler reports only what it alone knows: how long
23
+ * the tests took, the session it left behind, and how to tear down a runtime
24
+ * that does not outlive the run.
25
+ */ export type SchedulerRunResult = {
26
+ testTime: number;
27
+ watchSession?: BrowserWatchSession;
28
+ close?: () => Promise<void>;
29
+ };
30
+ /**
31
+ * How the shared dispatch layer reaches the pages of whichever run branch is
32
+ * live. Only the running branch knows its pages, so it installs the resolver
33
+ * and the dispatch layer stays branch-agnostic.
34
+ */ export type DispatchPageResolver = (target?: BrowserDispatchRequest['target']) => {
35
+ runnerPage?: BrowserProviderPage;
36
+ containerPage?: BrowserProviderPage;
37
+ };
@@ -1,4 +1,9 @@
1
1
  import type { TestFileInfo } from './protocol.js';
2
+ /**
3
+ * Paths the previous cycle ran that the current file set no longer contains.
4
+ * Core prunes its own state from this, so a file deleted mid-session stops
5
+ * being reported instead of lingering as a passing result.
6
+ */ export declare const collectDeletedTestPaths: (previous: TestFileInfo[], current: TestFileInfo[]) => string[];
2
7
  type WatchPlannerProjectEntry = {
3
8
  project: {
4
9
  name: string;
@@ -0,0 +1,21 @@
1
+ import { type BrowserRuntime } from './browserRsbuild.js';
2
+ // Only process-wide concerns stay module-level: the runtime handle reused
3
+ // across controller re-entry (config-change restarts), and the signal/exit
4
+ // cleanup that must run once per process. Diff/rerun state lives on
5
+ // `BrowserRuntime.watchState`.
6
+ export type WatchContext = {
7
+ runtime: BrowserRuntime | null;
8
+ cleanupRegistered: boolean;
9
+ cleanupPromise: Promise<void> | null;
10
+ };
11
+ export declare const watchContext: WatchContext;
12
+ /**
13
+ * Tear down the persistent watch runtime (dev servers, provider, browser,
14
+ * WebSocket server). Idempotent, and the single teardown the browser executor's
15
+ * `close` and the process-exit nets both go through.
16
+ */ export declare const runWatchRuntimeTeardown: <T>(state: {
17
+ runtime: T | null;
18
+ cleanupPromise: Promise<void> | null;
19
+ }, destroy: (runtime: T) => Promise<void>) => Promise<void>;
20
+ export declare const cleanupWatchRuntime: () => Promise<void>;
21
+ export declare const registerWatchCleanup: (embedded: boolean) => void;
@@ -0,0 +1,22 @@
1
+ import { type ExecutorInvalidationCallback } from '@rstest/core/internal/browser';
2
+ /**
3
+ * The handover between this host's rerun triggers and core's watch-cycle
4
+ * driver. Triggers reach it from three unrelated places — the bundler's
5
+ * dev-compile hook, a CLI shortcut's fanout, the in-page rerun button — and
6
+ * only the ordering inside `signalInvalidation` keeps them from racing each
7
+ * other. Each run branch installs the pieces it owns; nothing here knows which
8
+ * branch is running.
9
+ */ export type WatchSignals = ReturnType<typeof createWatchSignals>;
10
+ export declare const createWatchSignals: (onInvalidate: ExecutorInvalidationCallback | undefined) => {
11
+ setDispatchRerun(fn: () => Promise<void>): void;
12
+ runDispatchRerun(): Promise<void>;
13
+ setInterrupt(fn: () => Promise<void>): void;
14
+ signalInvalidation(fileFilters: string[], /**
15
+ * Run state this trigger binds to its own paths, taken in the same turn as
16
+ * the handover — after any interrupt, so no queued cycle can be dequeued in
17
+ * between and read it. The headed rerun's per-file test-name pattern is the
18
+ * one such state; core's cycle options cannot carry it, so the only thing
19
+ * that makes it the property of one cycle is claiming it here.
20
+ */ claimScope?: () => void): Promise<void>;
21
+ awaitSignalledCycle(): Promise<void>;
22
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rstest/browser",
3
- "version": "0.11.4",
3
+ "version": "0.11.6",
4
4
  "description": "Browser mode support for Rstest testing framework.",
5
5
  "keywords": [
6
6
  "rstest",
@@ -55,14 +55,14 @@
55
55
  "@vitest/snapshot": "^4.1.10",
56
56
  "birpc": "^4.0.0",
57
57
  "picomatch": "^4.0.5",
58
- "playwright": "^1.61.1",
58
+ "playwright": "^1.62.1",
59
59
  "@rstest/browser-ui": "0.0.0",
60
- "@rstest/core": "0.11.4",
61
- "@rstest/tsconfig": "0.0.1"
60
+ "@rstest/tsconfig": "0.0.1",
61
+ "@rstest/core": "0.11.6"
62
62
  },
63
63
  "peerDependencies": {
64
64
  "playwright": "^1.49.1",
65
- "@rstest/core": "^0.11.4"
65
+ "@rstest/core": "0.11.6"
66
66
  },
67
67
  "peerDependenciesMeta": {
68
68
  "playwright": {
@@ -2,14 +2,19 @@ import {
2
2
  type BrowserTestExecutor,
3
3
  type BrowserTestRunResult,
4
4
  buildBrowserCoverageMap,
5
+ color,
5
6
  type CreateBrowserExecutorOptions,
6
7
  type ExecutorCycleOutcome,
8
+ type ExecutorInvalidationCallback,
7
9
  type ExecutorRunCycleOptions,
8
10
  type ListCommandResult,
11
+ logger,
9
12
  type RstestContext,
10
13
  type TestFileResult,
11
14
  } from '@rstest/core/internal/browser';
12
15
  import { listBrowserTests, runBrowserController } from './hostController';
16
+ import type { BrowserWatchSession } from './schedulerSeam';
17
+ import { cleanupWatchRuntime } from './watchRuntime';
13
18
 
14
19
  const emptyOutcome = (): ExecutorCycleOutcome => ({
15
20
  results: [],
@@ -22,12 +27,14 @@ const emptyOutcome = (): ExecutorCycleOutcome => ({
22
27
  /**
23
28
  * The browser side of the {@link TestExecutor} seam. It delegates into the
24
29
  * existing `hostController` in place (no file split this phase) and adapts the
25
- * host's `BrowserTestRunResult` into the shared `ExecutorCycleOutcome` — the
26
- * former `toBrowserOutcome` core adapter is folded in here and deleted.
30
+ * host's `BrowserTestRunResult` into the shared `ExecutorCycleOutcome`.
27
31
  *
28
- * Only used in non-watch runs (the shared executor loop); browser watch stays
29
- * host-driven and self-finalizing until Phase 6, so `runCycle` maps directly
30
- * onto one `runBrowserController` invocation and its coverage/close semantics.
32
+ * Watch and non-watch differ only in what one `runCycle` means. Non-watch: one
33
+ * `runBrowserController` invocation that returns a deferred `close`. Watch: the
34
+ * first cycle boots the persistent runtime and hands back a live
35
+ * {@link BrowserWatchSession}; every cycle after it is a rerun the host's own
36
+ * triggers signalled through `onInvalidate` and core scheduled. Either way core
37
+ * finalizes, so this adapter never touches reporters or the exit code.
31
38
  */
32
39
  export async function createBrowserExecutor(
33
40
  context: RstestContext,
@@ -42,12 +49,17 @@ export async function createBrowserExecutor(
42
49
  allowEmptyRun,
43
50
  appliedModifyRstestConfigEnvironments,
44
51
  } = options;
52
+ const isWatchMode = context.command === 'watch';
45
53
  let deferredClose: (() => Promise<void>) | undefined;
46
54
  // The host has no mid-launch abort, so `close()` must wait for an in-flight
47
55
  // cycle to settle before it can tear down — otherwise a close racing the
48
56
  // cycle (e.g. the signal-driven cleanup path) sees no `deferredClose` yet
49
57
  // and leaks the launching browser + servers.
50
58
  let inFlightCycle: Promise<unknown> | undefined;
59
+ // Registered before the first cycle: booting the runtime installs the watch
60
+ // triggers, and the first rebuild can signal as soon as it does.
61
+ let invalidationCallback: ExecutorInvalidationCallback | undefined;
62
+ let watchSession: BrowserWatchSession | undefined;
51
63
 
52
64
  // Merge the host's per-file `result.coverage` into one map (shared core
53
65
  // helper, stripping it from each result to avoid reporter/state cache
@@ -83,12 +95,47 @@ export async function createBrowserExecutor(
83
95
  async init(): Promise<void> {
84
96
  // Server/provider launch stays inside `runBrowserController` (delegate in
85
97
  // place). Kept as an explicit hook so the plan → init → runCycle barrier
86
- // is honored structurally and Phase 5 can attach browser-side hook
87
- // application here.
98
+ // holds structurally for both executors, and so browser-side work that
99
+ // must precede the first cycle has somewhere to go.
88
100
  },
89
101
  async runCycle(
90
102
  opts: ExecutorRunCycleOptions,
91
103
  ): Promise<ExecutorCycleOutcome> {
104
+ if (watchSession) {
105
+ // A watch rerun: the host's trigger already resolved the scope and
106
+ // handed it over as the invalidation hint, which core passes back here.
107
+ //
108
+ // `opts.updateSnapshot` is dropped, and that is a gap rather than a
109
+ // choice. Core resolves it per trigger so a cycle no `u` selected files
110
+ // for cannot rewrite their snapshots; the host instead re-reads the live
111
+ // `snapshotManager` flag for every page it loads, so a browser cycle
112
+ // queued inside the `u` hold window still runs under `'all'`. Closing it
113
+ // means plumbing this option through `watchSession.runCycle` to the
114
+ // per-page config, which is also what would make the seam doc on
115
+ // `ExecutorRunCycleOptions.updateSnapshot` true on this side.
116
+ //
117
+ // Whichever shape that takes has to settle a fold hazard first. Trigger
118
+ // kind is erased on this side of the seam: a `u` press reaches the host
119
+ // as `requestRerun`, routes through `dispatchRerun`/`signalInvalidation`
120
+ // like any rebuild, and arrives back at the driver as
121
+ // `trigger: 'invalidation'` with the same options — so `canFold` already
122
+ // unions the two, and only the drop above keeps that from mattering.
123
+ // Exactly one of two things must therefore be true once the option is
124
+ // honored: the originating trigger crosses the seam and becomes part of
125
+ // the fold identity, or `updateSnapshot` rides the per-cycle options the
126
+ // fold predicate already compares (`'all'` then differs from the session
127
+ // value, so the two cycles cannot fold). Sending the flag out of band —
128
+ // straight to the host, driver-side options unchanged — is the one shape
129
+ // that folds a rebuild's files into the `u` scope.
130
+ const cycle = watchSession.runCycle(opts.fileFilters ?? []);
131
+ inFlightCycle = cycle;
132
+ try {
133
+ return await cycle;
134
+ } finally {
135
+ inFlightCycle = undefined;
136
+ }
137
+ }
138
+
92
139
  const cycle = runBrowserController(context, {
93
140
  projects,
94
141
  shardedEntries,
@@ -98,6 +145,9 @@ export async function createBrowserExecutor(
98
145
  onTraceEvents: opts.onTraceEvents,
99
146
  env: opts.env,
100
147
  updateSnapshot: opts.updateSnapshot,
148
+ onInvalidate: isWatchMode
149
+ ? (hint) => invalidationCallback?.(hint)
150
+ : undefined,
101
151
  });
102
152
  inFlightCycle = cycle;
103
153
  try {
@@ -105,18 +155,43 @@ export async function createBrowserExecutor(
105
155
  // Non-watch runs return a deferred `close`; collapse teardown into the
106
156
  // shared `executors.close()` exit path.
107
157
  deferredClose = result?.close;
158
+ watchSession = result?.watchSession;
108
159
  return foldOutcome(result);
109
160
  } finally {
110
161
  inFlightCycle = undefined;
111
162
  }
112
163
  },
113
- async collect(): Promise<{ list: ListCommandResult[] }> {
164
+ onInvalidate(cb: ExecutorInvalidationCallback): void {
165
+ invalidationCallback = cb;
166
+ },
167
+ hasWatchSession(): boolean {
168
+ return watchSession !== undefined;
169
+ },
170
+ async requestRerun(testPaths?: string[]): Promise<void> {
171
+ if (!watchSession) {
172
+ // Core gates rerun keys until every executor is past its first cycle, so
173
+ // reaching here means no session will ever open: the launch found no test
174
+ // files or failed outright, and reported that itself. The keys stay
175
+ // installed either way — a mixed run's node side keeps watching, and even
176
+ // a browser-only run outlives a launch that opened nothing — so resolving
177
+ // in silence would let the shortcut claim a rerun that never happened.
178
+ logger.log(
179
+ color.yellow(
180
+ '\nBrowser Mode has no live watch session, so this rerun skipped it.',
181
+ ),
182
+ );
183
+ return;
184
+ }
185
+ await watchSession.requestRerun(testPaths);
186
+ },
187
+ async collect(opts): Promise<{ list: ListCommandResult[] }> {
114
188
  const pending = listBrowserTests(context, {
115
189
  projects,
116
190
  shardedEntries,
117
191
  freezeShardedEntries,
118
192
  filesOnly,
119
193
  appliedModifyRstestConfigEnvironments,
194
+ env: opts.env,
120
195
  });
121
196
  inFlightCycle = pending;
122
197
  try {
@@ -132,6 +207,13 @@ export async function createBrowserExecutor(
132
207
  // A rejected cycle cleans up host-side; settling is all that's needed.
133
208
  await inFlightCycle.catch(() => undefined);
134
209
  }
210
+ if (isWatchMode) {
211
+ // The watch runtime spans every cycle, so it is not a per-cycle
212
+ // deferred close — the executor is its owner.
213
+ watchSession = undefined;
214
+ await cleanupWatchRuntime();
215
+ return;
216
+ }
135
217
  const close = deferredClose;
136
218
  deferredClose = undefined;
137
219
  await close?.();