@rstest/browser 0.11.5 → 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.
- package/dist/browser-container/container-static/js/683.4f821ce8f1.js +194 -0
- package/dist/browser-container/container-static/js/index.568aa7cb35.js +1 -0
- package/dist/browser-container/index.html +1 -1
- package/dist/browserExecutor.d.ts +7 -5
- package/dist/browserRsbuild.d.ts +119 -0
- package/dist/containerRpc.d.ts +52 -0
- package/dist/dispatchCapabilities.d.ts +1 -2
- package/dist/headedScheduler.d.ts +58 -0
- package/dist/headlessScheduler.d.ts +37 -0
- package/dist/hostController.d.ts +12 -47
- package/dist/hostPayloads.d.ts +30 -0
- package/dist/index.js +1714 -1721
- package/dist/schedulerSeam.d.ts +37 -0
- package/dist/watchRerunPlanner.d.ts +5 -0
- package/dist/watchRuntime.d.ts +21 -0
- package/dist/watchSignals.d.ts +22 -0
- package/package.json +5 -5
- package/src/browserExecutor.ts +90 -8
- package/src/browserRsbuild.ts +1927 -0
- package/src/containerRpc.ts +206 -0
- package/src/dispatchCapabilities.ts +1 -6
- package/src/headedScheduler.ts +664 -0
- package/src/headlessScheduler.ts +566 -0
- package/src/hostController.ts +857 -4125
- package/src/hostPayloads.ts +62 -0
- package/src/schedulerSeam.ts +49 -0
- package/src/watchRerunPlanner.ts +18 -7
- package/src/watchRuntime.ts +83 -0
- package/src/watchSignals.ts +93 -0
- package/dist/browser-container/container-static/js/506.b2f920fa67.js +0 -194
- package/dist/browser-container/container-static/js/index.d27fd63f5e.js +0 -1
- package/dist/headlessLatestRerunScheduler.d.ts +0 -18
- package/src/headlessLatestRerunScheduler.ts +0 -76
|
@@ -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.
|
|
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.62.
|
|
58
|
+
"playwright": "^1.62.1",
|
|
59
59
|
"@rstest/browser-ui": "0.0.0",
|
|
60
|
-
"@rstest/
|
|
61
|
-
"@rstest/
|
|
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": "
|
|
65
|
+
"@rstest/core": "0.11.6"
|
|
66
66
|
},
|
|
67
67
|
"peerDependenciesMeta": {
|
|
68
68
|
"playwright": {
|
package/src/browserExecutor.ts
CHANGED
|
@@ -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
|
|
26
|
-
* former `toBrowserOutcome` core adapter is folded in here and deleted.
|
|
30
|
+
* host's `BrowserTestRunResult` into the shared `ExecutorCycleOutcome`.
|
|
27
31
|
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
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
|
-
//
|
|
87
|
-
//
|
|
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
|
-
|
|
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?.();
|