@rstest/browser 0.11.0 → 0.11.2
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/{328.37af068e21.js → 117.3da95caa39.js} +1006 -1006
- package/dist/browser-container/container-static/js/117.3da95caa39.js.LICENSE.txt +1 -0
- package/dist/browser-container/container-static/js/{index.c8fe4ff310.js → index.6e4da8c9ab.js} +9 -9
- package/dist/browser-container/index.html +1 -1
- package/dist/browserExecutor.d.ts +11 -0
- package/dist/configValidation.d.ts +6 -1
- package/dist/hostController.d.ts +2 -6
- package/dist/index.d.ts +4 -3
- package/dist/index.js +539 -292
- package/dist/protocol.d.ts +2 -2
- package/package.json +5 -5
- package/src/AGENTS.md +17 -1
- package/src/browserExecutor.ts +138 -0
- package/src/client/AGENTS.md +20 -13
- package/src/client/entry.ts +62 -2
- package/src/client/snapshot.ts +1 -2
- package/src/concurrency.ts +42 -25
- package/src/configValidation.ts +182 -1
- package/src/hostController.ts +476 -337
- package/src/index.ts +9 -5
- package/src/protocol.ts +2 -2
- package/dist/browser-container/container-static/js/328.37af068e21.js.LICENSE.txt +0 -1
package/dist/protocol.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { BrowserViewport } from '@rstest/core/internal/browser';
|
|
2
|
-
import type {
|
|
2
|
+
import type { BrowserRuntimeConfig, TestFileResult, TestInfo, TestResult } from '@rstest/core/internal/browser-runtime';
|
|
3
3
|
import type { SnapshotUpdateState } from '@vitest/snapshot';
|
|
4
4
|
export type { BrowserLocatorIR, BrowserRpcRequest, SnapshotRpcCall, SnapshotRpcMethod, SnapshotRpcMethodArgs, SnapshotRpcRequest } from './rpcProtocol.js';
|
|
5
5
|
export { validateBrowserRpcRequest } from './rpcProtocol.js';
|
|
@@ -12,7 +12,7 @@ export declare const DISPATCH_NAMESPACE_RUNNER = 'runner';
|
|
|
12
12
|
export declare const DISPATCH_NAMESPACE_BROWSER = 'browser';
|
|
13
13
|
export declare const DISPATCH_NAMESPACE_SNAPSHOT = 'snapshot';
|
|
14
14
|
export declare const DISPATCH_METHOD_RPC = 'rpc';
|
|
15
|
-
export type SerializedRuntimeConfig =
|
|
15
|
+
export type SerializedRuntimeConfig = BrowserRuntimeConfig;
|
|
16
16
|
// `BrowserViewport` is a core config type (`@rstest/core` owns the canonical
|
|
17
17
|
// definition used by `NormalizedBrowserModeConfig`). Re-export it so the host
|
|
18
18
|
// assigns the SAME type across the seam instead of a hand-copied duplicate.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rstest/browser",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.2",
|
|
4
4
|
"description": "Browser mode support for Rstest testing framework.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"rstest",
|
|
@@ -52,17 +52,17 @@
|
|
|
52
52
|
"@types/convert-source-map": "^2.0.3",
|
|
53
53
|
"@types/picomatch": "^4.0.3",
|
|
54
54
|
"@types/ws": "^8.18.1",
|
|
55
|
-
"@vitest/snapshot": "^3.2.
|
|
55
|
+
"@vitest/snapshot": "^3.2.7",
|
|
56
56
|
"birpc": "^4.0.0",
|
|
57
57
|
"picomatch": "^4.0.5",
|
|
58
58
|
"playwright": "^1.61.1",
|
|
59
|
-
"@rstest/
|
|
59
|
+
"@rstest/core": "0.11.2",
|
|
60
60
|
"@rstest/tsconfig": "0.0.1",
|
|
61
|
-
"@rstest/
|
|
61
|
+
"@rstest/browser-ui": "0.0.0"
|
|
62
62
|
},
|
|
63
63
|
"peerDependencies": {
|
|
64
64
|
"playwright": "^1.49.1",
|
|
65
|
-
"@rstest/core": "^0.11.
|
|
65
|
+
"@rstest/core": "^0.11.2"
|
|
66
66
|
},
|
|
67
67
|
"peerDependenciesMeta": {
|
|
68
68
|
"playwright": {
|
package/src/AGENTS.md
CHANGED
|
@@ -2,15 +2,28 @@
|
|
|
2
2
|
|
|
3
3
|
This document is architecture-only and focuses on browser mode scheduling in `@rstest/browser` host-side modules.
|
|
4
4
|
|
|
5
|
+
## Run lifecycle ownership (browser/node isomorphism)
|
|
6
|
+
|
|
7
|
+
`runBrowserController` splits finalize ownership on `context.command`:
|
|
8
|
+
|
|
9
|
+
- **Non-watch runs**: the host never self-finalizes. It returns a fully-populated `BrowserTestRunResult` with a deferred `close`, and `@rstest/core`'s `finalizeRunCycle` reduces the run's `ExecutorCycleOutcome`s into the verdict — reporter `onTestRunEnd`, coverage merge + report, exit code, and the bail message. `BrowserTestRunOptions.skipOnTestRunEnd` (deprecated and ignored since the finalize unification) has been removed.
|
|
10
|
+
- **Watch runs**: the host owns the per-rerun lifecycle (`onTestRunStart`/`onTestRunEnd` per rerun) and self-finalizes; core skips its finalize entirely for browser-only and zero-node mixed watch runs.
|
|
11
|
+
|
|
12
|
+
Runner lifecycle events flow through per-project `RunnerEventSink`s (`createRunnerEventSink` from core — the same event pump the node pool RPC uses). The host keeps a `Map<projectName, RunnerEventSink>` and resolves sinks via `sinkForProjectName`/`sinkForTestPath` (falling back to the first project for unknown names); it never fans out to reporters or `stateManager` directly.
|
|
13
|
+
|
|
14
|
+
Cross-file `bail` is enforced at file boundaries in the headless scheduler: before picking up the next file, each worker checks the cycle-wide `stateManager.getCountOfFailedTests()` and drains the remaining queue as skipped results once the budget is reached. The headed debugging UI does not apply bail; within a running file, the runner's per-test gate uses the client-local failed count (see `src/client/AGENTS.md`).
|
|
15
|
+
|
|
5
16
|
## Module topology
|
|
6
17
|
|
|
7
18
|
```mermaid
|
|
8
19
|
flowchart LR
|
|
9
20
|
subgraph Host["@rstest/browser host (Node.js)"]
|
|
10
21
|
IDX["index.ts\nrunBrowserTests()"]
|
|
22
|
+
CV["configValidation.ts\nvalidateBrowserConfig()"]
|
|
11
23
|
HC["hostController.ts\nrunBrowserController()"]
|
|
12
24
|
RT["createBrowserRuntime()"]
|
|
13
25
|
DR["dispatchCapabilities.ts + dispatchRouter.ts\nnamespace router"]
|
|
26
|
+
BRR["browserRpcRegistry.ts\nBrowser RPC allowlists"]
|
|
14
27
|
RL["runSession.ts\nRunSessionLifecycle"]
|
|
15
28
|
SR["sessionRegistry.ts\nRunnerSessionRegistry"]
|
|
16
29
|
WP["watchRerunPlanner.ts"]
|
|
@@ -31,9 +44,11 @@ flowchart LR
|
|
|
31
44
|
RPC["dispatch-rpc-request"]
|
|
32
45
|
end
|
|
33
46
|
|
|
47
|
+
IDX -."re-export; invoked by @rstest/core before the run".-> CV
|
|
34
48
|
IDX --> HC
|
|
35
49
|
HC --> RT
|
|
36
50
|
HC --> DR
|
|
51
|
+
DR --> BRR
|
|
37
52
|
HC --> WP
|
|
38
53
|
WP --> LS
|
|
39
54
|
HC --> RL
|
|
@@ -81,6 +96,7 @@ sequenceDiagram
|
|
|
81
96
|
|
|
82
97
|
Runner->>Container: postMessage runner lifecycle
|
|
83
98
|
Container->>Host: forward lifecycle callbacks (onTest*)
|
|
99
|
+
Host->>Host: feed per-project RunnerEventSink (stateManager + reporters)
|
|
84
100
|
|
|
85
101
|
Runner->>Container: postMessage dispatch rpc request
|
|
86
102
|
Container->>Host: rpc.dispatch(request)
|
|
@@ -121,7 +137,7 @@ sequenceDiagram
|
|
|
121
137
|
participant Handler as namespace handler
|
|
122
138
|
|
|
123
139
|
Runner->>Host: __rstest_dispatch__ lifecycle
|
|
124
|
-
Host->>Host:
|
|
140
|
+
Host->>Host: feed per-project RunnerEventSink (stateManager + reporters)
|
|
125
141
|
|
|
126
142
|
Runner->>Host: __rstest_dispatch_rpc__ request
|
|
127
143
|
Host->>Router: routing inbound request
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type BrowserTestExecutor,
|
|
3
|
+
type BrowserTestRunResult,
|
|
4
|
+
buildBrowserCoverageMap,
|
|
5
|
+
type CreateBrowserExecutorOptions,
|
|
6
|
+
type ExecutorCycleOutcome,
|
|
7
|
+
type ExecutorRunCycleOptions,
|
|
8
|
+
type ListCommandResult,
|
|
9
|
+
type RstestContext,
|
|
10
|
+
type TestFileResult,
|
|
11
|
+
} from '@rstest/core/internal/browser';
|
|
12
|
+
import { listBrowserTests, runBrowserController } from './hostController';
|
|
13
|
+
|
|
14
|
+
const emptyOutcome = (): ExecutorCycleOutcome => ({
|
|
15
|
+
results: [],
|
|
16
|
+
testResults: [],
|
|
17
|
+
errors: [],
|
|
18
|
+
testPaths: [],
|
|
19
|
+
duration: { buildTime: 0, testTime: 0 },
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The browser side of the {@link TestExecutor} seam. It delegates into the
|
|
24
|
+
* 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.
|
|
27
|
+
*
|
|
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.
|
|
31
|
+
*/
|
|
32
|
+
export async function createBrowserExecutor(
|
|
33
|
+
context: RstestContext,
|
|
34
|
+
options: CreateBrowserExecutorOptions,
|
|
35
|
+
): Promise<BrowserTestExecutor> {
|
|
36
|
+
const {
|
|
37
|
+
projects,
|
|
38
|
+
coverageProvider,
|
|
39
|
+
freezeShardedEntries,
|
|
40
|
+
filesOnly,
|
|
41
|
+
allowEmptyRun,
|
|
42
|
+
appliedModifyRstestConfigEnvironments,
|
|
43
|
+
} = options;
|
|
44
|
+
let deferredClose: (() => Promise<void>) | undefined;
|
|
45
|
+
// The host has no mid-launch abort, so `close()` must wait for an in-flight
|
|
46
|
+
// cycle to settle before it can tear down — otherwise a close racing the
|
|
47
|
+
// cycle (e.g. the signal-driven cleanup path) sees no `deferredClose` yet
|
|
48
|
+
// and leaks the launching browser + servers.
|
|
49
|
+
let inFlightCycle: Promise<unknown> | undefined;
|
|
50
|
+
|
|
51
|
+
// Merge the host's per-file `result.coverage` into one map (shared core
|
|
52
|
+
// helper, stripping it from each result to avoid reporter/state cache
|
|
53
|
+
// bloat), then hand the shared finalize a coverage `map` (no `raw` — browser
|
|
54
|
+
// coverage is istanbul-only).
|
|
55
|
+
const foldOutcome = (
|
|
56
|
+
result: BrowserTestRunResult | void,
|
|
57
|
+
): ExecutorCycleOutcome => {
|
|
58
|
+
if (!result) {
|
|
59
|
+
return emptyOutcome();
|
|
60
|
+
}
|
|
61
|
+
const map = buildBrowserCoverageMap(
|
|
62
|
+
result.results as TestFileResult[],
|
|
63
|
+
coverageProvider,
|
|
64
|
+
);
|
|
65
|
+
return {
|
|
66
|
+
results: result.results,
|
|
67
|
+
testResults: result.testResults,
|
|
68
|
+
errors: result.unhandledErrors ?? [],
|
|
69
|
+
testPaths: result.results.map((r) => r.testPath),
|
|
70
|
+
duration: {
|
|
71
|
+
buildTime: result.duration.buildTime,
|
|
72
|
+
testTime: result.duration.testTime,
|
|
73
|
+
},
|
|
74
|
+
coverage: { map: map?.toJSON() },
|
|
75
|
+
resolveSourcemap: result.resolveSourcemap,
|
|
76
|
+
};
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
name: 'browser',
|
|
81
|
+
projects,
|
|
82
|
+
async init(): Promise<void> {
|
|
83
|
+
// Server/provider launch stays inside `runBrowserController` (delegate in
|
|
84
|
+
// place). Kept as an explicit hook so the plan → init → runCycle barrier
|
|
85
|
+
// is honored structurally and Phase 5 can attach browser-side hook
|
|
86
|
+
// application here.
|
|
87
|
+
},
|
|
88
|
+
async runCycle(
|
|
89
|
+
opts: ExecutorRunCycleOptions,
|
|
90
|
+
): Promise<ExecutorCycleOutcome> {
|
|
91
|
+
const cycle = runBrowserController(context, {
|
|
92
|
+
projects,
|
|
93
|
+
shardedEntries: opts.shardedEntries,
|
|
94
|
+
freezeShardedEntries,
|
|
95
|
+
allowEmptyRun,
|
|
96
|
+
appliedModifyRstestConfigEnvironments,
|
|
97
|
+
onTraceEvents: opts.onTraceEvents,
|
|
98
|
+
env: opts.env,
|
|
99
|
+
});
|
|
100
|
+
inFlightCycle = cycle;
|
|
101
|
+
try {
|
|
102
|
+
const result = await cycle;
|
|
103
|
+
// Non-watch runs return a deferred `close`; collapse teardown into the
|
|
104
|
+
// shared `executors.close()` exit path.
|
|
105
|
+
deferredClose = result?.close;
|
|
106
|
+
return foldOutcome(result);
|
|
107
|
+
} finally {
|
|
108
|
+
inFlightCycle = undefined;
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
async collect(opts): Promise<{ list: ListCommandResult[] }> {
|
|
112
|
+
const pending = listBrowserTests(context, {
|
|
113
|
+
projects,
|
|
114
|
+
shardedEntries: opts.shardedEntries,
|
|
115
|
+
freezeShardedEntries,
|
|
116
|
+
filesOnly,
|
|
117
|
+
appliedModifyRstestConfigEnvironments,
|
|
118
|
+
});
|
|
119
|
+
inFlightCycle = pending;
|
|
120
|
+
try {
|
|
121
|
+
const { list, close } = await pending;
|
|
122
|
+
deferredClose = close;
|
|
123
|
+
return { list };
|
|
124
|
+
} finally {
|
|
125
|
+
inFlightCycle = undefined;
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
async close(): Promise<void> {
|
|
129
|
+
if (inFlightCycle) {
|
|
130
|
+
// A rejected cycle cleans up host-side; settling is all that's needed.
|
|
131
|
+
await inFlightCycle.catch(() => undefined);
|
|
132
|
+
}
|
|
133
|
+
const close = deferredClose;
|
|
134
|
+
deferredClose = undefined;
|
|
135
|
+
await close?.();
|
|
136
|
+
},
|
|
137
|
+
};
|
|
138
|
+
}
|
package/src/client/AGENTS.md
CHANGED
|
@@ -34,13 +34,13 @@ flowchart LR
|
|
|
34
34
|
subgraph IframePath["Iframe path (headed)"]
|
|
35
35
|
S1["send()"] --> P1["parent.postMessage(__rstest_dispatch__)"]
|
|
36
36
|
R1["dispatchRunnerLifecycle()"] --> P2["postMessage dispatch-rpc-request"]
|
|
37
|
-
SN1["snapshot.
|
|
37
|
+
SN1["snapshot.ts / browserRpc.ts via dispatchTransport.dispatchRpc()"] --> P3["postMessage dispatch-rpc-request + wait __rstest_dispatch_response__"]
|
|
38
38
|
end
|
|
39
39
|
|
|
40
40
|
subgraph TopLevelRunPath["Top-level page path (headless run)"]
|
|
41
41
|
S2["send()"] --> D1["window.__rstest_dispatch__"]
|
|
42
42
|
R2["dispatchRunnerLifecycle()"] --> D2["window.__rstest_dispatch_rpc__"]
|
|
43
|
-
SN2["snapshot.
|
|
43
|
+
SN2["snapshot.ts / browserRpc.ts via dispatchTransport.dispatchRpc()"] --> D3["window.__rstest_dispatch_rpc__"]
|
|
44
44
|
end
|
|
45
45
|
|
|
46
46
|
subgraph TopLevelCollectPath["Top-level page path (list collect)"]
|
|
@@ -48,31 +48,36 @@ flowchart LR
|
|
|
48
48
|
end
|
|
49
49
|
```
|
|
50
50
|
|
|
51
|
-
##
|
|
51
|
+
## Dispatch RPC sequence (snapshot / browser namespaces)
|
|
52
|
+
|
|
53
|
+
The `snapshot` namespace (snapshot file ops) and the `browser` namespace
|
|
54
|
+
(locator/page RPC from `browserRpc.ts`) share one client channel:
|
|
55
|
+
`dispatchTransport.ts` owns request ids, timeouts, and pending-response
|
|
56
|
+
resolution for both the iframe and top-level transports.
|
|
52
57
|
|
|
53
58
|
```mermaid
|
|
54
59
|
sequenceDiagram
|
|
55
|
-
participant
|
|
56
|
-
participant
|
|
60
|
+
participant Caller as snapshot.ts / browserRpc.ts
|
|
61
|
+
participant Transport as dispatchTransport.ts
|
|
57
62
|
participant Container as browser-ui channel
|
|
58
63
|
participant Host as host dispatch router
|
|
59
64
|
|
|
60
|
-
|
|
65
|
+
Caller->>Transport: dispatchRpc(request)
|
|
61
66
|
|
|
62
67
|
alt top-level runner (headless run)
|
|
63
|
-
|
|
64
|
-
Host-->>
|
|
65
|
-
|
|
68
|
+
Transport->>Host: __rstest_dispatch_rpc__(namespace=snapshot|browser)
|
|
69
|
+
Host-->>Transport: BrowserDispatchResponse
|
|
70
|
+
Transport-->>Caller: result/error
|
|
66
71
|
else iframe runner (headed)
|
|
67
|
-
|
|
72
|
+
Transport->>Container: postMessage(dispatch-rpc-request)
|
|
68
73
|
Container->>Host: rpc.dispatch(request)
|
|
69
74
|
Host-->>Container: BrowserDispatchResponse
|
|
70
|
-
Container-->>
|
|
71
|
-
|
|
75
|
+
Container-->>Transport: __rstest_dispatch_response__
|
|
76
|
+
Transport-->>Caller: resolve/reject pending request
|
|
72
77
|
end
|
|
73
78
|
```
|
|
74
79
|
|
|
75
|
-
List collect mode does not use the snapshot RPC
|
|
80
|
+
List collect mode does not use the snapshot or browser RPC namespaces.
|
|
76
81
|
|
|
77
82
|
## Runtime invariants
|
|
78
83
|
|
|
@@ -80,3 +85,5 @@ List collect mode does not use the snapshot RPC namespace.
|
|
|
80
85
|
- Runner lifecycle events (`file-ready`, `suite-start`, `suite-result`, `case-start`) go through the `runner` dispatch namespace.
|
|
81
86
|
- Snapshot file operations go through the `snapshot` dispatch namespace and never access filesystem directly in browser runtime.
|
|
82
87
|
- Console interception is per test file and must restore original console methods in `finally`.
|
|
88
|
+
- An unhandled window error or `unhandledrejection` that escapes a test file fails the file even when every test passed. Before finalizing each file result, the runner yields two macrotasks so a rejection leaked by a synchronous test is still observed (the browser dispatches `unhandledrejection` in a task queued after the current task).
|
|
89
|
+
- The `getCountOfFailedTests` runner hook returns the client-local per-file failed count; cross-file `bail` is enforced host-side at file boundaries (see `../AGENTS.md`), not by this hook.
|
package/src/client/entry.ts
CHANGED
|
@@ -67,14 +67,19 @@ type GlobalWithRuntimeEnv = typeof globalThis &
|
|
|
67
67
|
const restoreRuntimeConfig = (
|
|
68
68
|
config: BrowserProjectRuntime['runtimeConfig'],
|
|
69
69
|
): RuntimeConfig => {
|
|
70
|
-
const { testNamePattern } = config
|
|
70
|
+
const { testNamePattern } = config;
|
|
71
|
+
// The browser wire (BrowserRuntimeConfig) omits node-only fields
|
|
72
|
+
// (testEnvironment / coverage / logHeapUsage / detectAsyncLeaks) that the
|
|
73
|
+
// browser runtime never reads. The shared WorkerState / runner types require
|
|
74
|
+
// the full RuntimeConfig shape and runner heap sampling is guarded against
|
|
75
|
+
// the absent logHeapUsage, so widening back here is sound.
|
|
71
76
|
return {
|
|
72
77
|
...config,
|
|
73
78
|
testNamePattern:
|
|
74
79
|
typeof testNamePattern === 'string'
|
|
75
80
|
? unwrapRegex(testNamePattern)
|
|
76
81
|
: testNamePattern,
|
|
77
|
-
};
|
|
82
|
+
} as RuntimeConfig;
|
|
78
83
|
};
|
|
79
84
|
|
|
80
85
|
const ensureRuntimeEnv = (env: RuntimeConfig['env'] | undefined): void => {
|
|
@@ -542,6 +547,28 @@ const run = async () => {
|
|
|
542
547
|
return;
|
|
543
548
|
}
|
|
544
549
|
|
|
550
|
+
// Capture unhandled errors/rejections that escape a test file's execution.
|
|
551
|
+
// Parity with the node worker, which attaches process-level
|
|
552
|
+
// uncaughtException/unhandledRejection to the running file's result and fails
|
|
553
|
+
// the file. `activeUnhandledErrors` points at the currently running file's
|
|
554
|
+
// collector (undefined between files, so stray late events are ignored).
|
|
555
|
+
let activeUnhandledErrors: Error[] | undefined;
|
|
556
|
+
const onWindowError = (event: ErrorEvent): void => {
|
|
557
|
+
activeUnhandledErrors?.push(
|
|
558
|
+
event.error instanceof Error
|
|
559
|
+
? event.error
|
|
560
|
+
: new Error(event.message || String(event.error)),
|
|
561
|
+
);
|
|
562
|
+
};
|
|
563
|
+
const onUnhandledRejection = (event: PromiseRejectionEvent): void => {
|
|
564
|
+
const { reason } = event;
|
|
565
|
+
activeUnhandledErrors?.push(
|
|
566
|
+
reason instanceof Error ? reason : new Error(String(reason)),
|
|
567
|
+
);
|
|
568
|
+
};
|
|
569
|
+
window.addEventListener('error', onWindowError);
|
|
570
|
+
window.addEventListener('unhandledrejection', onUnhandledRejection);
|
|
571
|
+
|
|
545
572
|
// 2. Run tests for each file
|
|
546
573
|
for (const key of testKeysToRun) {
|
|
547
574
|
const testPath = toAbsolutePath(key, currentProject.projectRoot);
|
|
@@ -669,6 +696,9 @@ const run = async () => {
|
|
|
669
696
|
},
|
|
670
697
|
});
|
|
671
698
|
|
|
699
|
+
const unhandledErrors: Error[] = [];
|
|
700
|
+
activeUnhandledErrors = unhandledErrors;
|
|
701
|
+
|
|
672
702
|
try {
|
|
673
703
|
// Load setup files for this project after runtime is ready.
|
|
674
704
|
await loadSetupFiles();
|
|
@@ -692,6 +722,32 @@ const run = async () => {
|
|
|
692
722
|
runtime.api,
|
|
693
723
|
);
|
|
694
724
|
|
|
725
|
+
// The browser dispatches `unhandledrejection` in a task queued at the
|
|
726
|
+
// current task's microtask checkpoint, so a rejection leaked by a
|
|
727
|
+
// synchronous test is not observable yet when `runTests()` resolves.
|
|
728
|
+
// Yield two macrotasks: the first reaches the checkpoint that queues
|
|
729
|
+
// the event task, the second runs after that task regardless of how
|
|
730
|
+
// the browser orders the timer and event task sources.
|
|
731
|
+
for (let i = 0; i < 2; i++) {
|
|
732
|
+
await new Promise<void>((resolve) => {
|
|
733
|
+
setTimeout(resolve, 0);
|
|
734
|
+
});
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
// An unhandled error/rejection that escaped the run fails the file even
|
|
738
|
+
// when every test passed.
|
|
739
|
+
if (unhandledErrors.length > 0) {
|
|
740
|
+
result.status = 'fail';
|
|
741
|
+
result.errors = [
|
|
742
|
+
...(result.errors ?? []),
|
|
743
|
+
...unhandledErrors.map((error) => ({
|
|
744
|
+
name: error.name,
|
|
745
|
+
message: error.message,
|
|
746
|
+
stack: error.stack,
|
|
747
|
+
})),
|
|
748
|
+
];
|
|
749
|
+
}
|
|
750
|
+
|
|
695
751
|
// Collect coverage data from global __coverage__ object
|
|
696
752
|
if (globalThis.__coverage__) {
|
|
697
753
|
result.coverage = globalThis.__coverage__ as CoverageMapData;
|
|
@@ -716,9 +772,13 @@ const run = async () => {
|
|
|
716
772
|
} finally {
|
|
717
773
|
// Restore original console methods
|
|
718
774
|
restoreConsole();
|
|
775
|
+
activeUnhandledErrors = undefined;
|
|
719
776
|
}
|
|
720
777
|
}
|
|
721
778
|
|
|
779
|
+
window.removeEventListener('error', onWindowError);
|
|
780
|
+
window.removeEventListener('unhandledrejection', onUnhandledRejection);
|
|
781
|
+
|
|
722
782
|
send({ type: 'complete' });
|
|
723
783
|
window.__RSTEST_DONE__ = true;
|
|
724
784
|
};
|
package/src/client/snapshot.ts
CHANGED
|
@@ -5,10 +5,9 @@ import {
|
|
|
5
5
|
dispatchRpc,
|
|
6
6
|
getRpcTimeout,
|
|
7
7
|
} from './dispatchTransport';
|
|
8
|
+
import { SNAPSHOT_HEADER } from '@rstest/core/internal/browser-runtime';
|
|
8
9
|
import { mapStackFrame } from './sourceMapSupport';
|
|
9
10
|
|
|
10
|
-
const SNAPSHOT_HEADER = '// Rstest Snapshot';
|
|
11
|
-
|
|
12
11
|
const createSnapshotDispatchRequest = (
|
|
13
12
|
requestId: string,
|
|
14
13
|
call: SnapshotRpcCall,
|
package/src/concurrency.ts
CHANGED
|
@@ -1,12 +1,17 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
getNumCpus,
|
|
3
|
+
parseWorkers,
|
|
4
|
+
resolveWorkerCount,
|
|
5
|
+
} from '@rstest/core/internal/browser';
|
|
2
6
|
import type { Rstest } from '@rstest/core/internal/browser';
|
|
3
7
|
|
|
4
8
|
// Re-export the shared worker primitives so existing consumers (and unit tests
|
|
5
9
|
// importing from `../src/concurrency`) keep a stable import surface.
|
|
6
10
|
export { getNumCpus, parseWorkers };
|
|
7
11
|
|
|
8
|
-
//
|
|
9
|
-
//
|
|
12
|
+
// The browser headless path caps CPU-derived workers at 12 and halves that
|
|
13
|
+
// capped base in watch; the shared `resolveWorkerCount` helper owns the
|
|
14
|
+
// `maxWorkers` override and workload clamp.
|
|
10
15
|
const DEFAULT_MAX_HEADLESS_WORKERS = 12;
|
|
11
16
|
|
|
12
17
|
type HeadlessConcurrencyContext = Pick<Rstest, 'command'> & {
|
|
@@ -17,32 +22,44 @@ type HeadlessConcurrencyContext = Pick<Rstest, 'command'> & {
|
|
|
17
22
|
};
|
|
18
23
|
};
|
|
19
24
|
|
|
25
|
+
const resolveHeadlessWorkerCount = ({
|
|
26
|
+
command,
|
|
27
|
+
maxWorkers,
|
|
28
|
+
totalTasks,
|
|
29
|
+
numCpus = getNumCpus(),
|
|
30
|
+
}: {
|
|
31
|
+
command: HeadlessConcurrencyContext['command'];
|
|
32
|
+
maxWorkers?: string | number;
|
|
33
|
+
totalTasks: number;
|
|
34
|
+
numCpus?: number;
|
|
35
|
+
}): number => {
|
|
36
|
+
const base = Math.max(Math.min(DEFAULT_MAX_HEADLESS_WORKERS, numCpus - 1), 1);
|
|
37
|
+
return resolveWorkerCount({
|
|
38
|
+
command,
|
|
39
|
+
maxWorkers,
|
|
40
|
+
totalTasks,
|
|
41
|
+
recommended: base,
|
|
42
|
+
watchRecommended: Math.max(Math.floor(base / 2), 1),
|
|
43
|
+
numCpus,
|
|
44
|
+
});
|
|
45
|
+
};
|
|
46
|
+
|
|
20
47
|
export const resolveDefaultHeadlessWorkers = (
|
|
21
48
|
command: HeadlessConcurrencyContext['command'],
|
|
22
49
|
numCpus: number = getNumCpus(),
|
|
23
|
-
): number =>
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
return command === 'watch'
|
|
30
|
-
? Math.max(Math.floor(baseWorkers / 2), 1)
|
|
31
|
-
: baseWorkers;
|
|
32
|
-
};
|
|
50
|
+
): number =>
|
|
51
|
+
resolveHeadlessWorkerCount({
|
|
52
|
+
command,
|
|
53
|
+
totalTasks: Number.POSITIVE_INFINITY,
|
|
54
|
+
numCpus,
|
|
55
|
+
});
|
|
33
56
|
|
|
34
57
|
export const getHeadlessConcurrency = (
|
|
35
58
|
context: HeadlessConcurrencyContext,
|
|
36
59
|
totalTests: number,
|
|
37
|
-
): number =>
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
if (maxWorkers !== undefined) {
|
|
44
|
-
return Math.min(parseWorkers(maxWorkers), totalTests);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
return Math.min(resolveDefaultHeadlessWorkers(context.command), totalTests);
|
|
48
|
-
};
|
|
60
|
+
): number =>
|
|
61
|
+
resolveHeadlessWorkerCount({
|
|
62
|
+
command: context.command,
|
|
63
|
+
maxWorkers: context.normalizedConfig.pool.maxWorkers,
|
|
64
|
+
totalTasks: totalTests,
|
|
65
|
+
});
|