@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.
- 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/container-static/js/lib-react.e24a1d366b.js +2 -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 +1738 -1793
- package/dist/protocol.d.ts +5 -0
- 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/client/entry.ts +3 -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 +855 -4163
- package/src/hostPayloads.ts +62 -0
- package/src/protocol.ts +6 -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/243.a8eed2b9e7.js +0 -27406
- package/dist/browser-container/container-static/js/243.a8eed2b9e7.js.LICENSE.txt +0 -1
- package/dist/browser-container/container-static/js/index.84aaafaf21.js +0 -3058
- package/dist/browser-container/container-static/js/lib-react.62b27a21db.js +0 -8454
- package/dist/browser-container/container-static/js/lib-react.62b27a21db.js.LICENSE.txt +0 -1
- package/dist/headlessLatestRerunScheduler.d.ts +0 -18
- 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
|
+
};
|
package/src/watchRerunPlanner.ts
CHANGED
|
@@ -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
|
|
64
|
-
|
|
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
|
+
};
|