@rstest/browser 0.10.3 → 0.10.5
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/626.js +2 -1
- package/dist/browser-container/container-static/css/index.0521ff95b1.css +1 -0
- package/dist/browser-container/container-static/js/{27.099feaf1da.js → 854.c59f71483d.js} +5728 -5756
- package/dist/browser-container/container-static/js/854.c59f71483d.js.LICENSE.txt +1 -0
- package/dist/browser-container/container-static/js/{index.be8c8d2626.js → index.fedc31dff3.js} +109 -136
- package/dist/browser-container/container-static/js/{lib-react.5ae9b90ee5.js → lib-react.62b27a21db.js} +21 -21
- package/dist/browser-container/container-static/js/lib-react.62b27a21db.js.LICENSE.txt +1 -0
- package/dist/browser-container/index.html +1 -1
- package/dist/browser.js +4 -4
- package/dist/browserRpcRegistry.d.ts +1 -2
- package/dist/client/api.d.ts +1 -1
- package/dist/client/dispatchTransport.d.ts +18 -1
- package/dist/client/locator.d.ts +2 -3
- package/dist/concurrency.d.ts +5 -3
- package/dist/configValidation.d.ts +2 -2
- package/dist/dispatchCapabilities.d.ts +3 -3
- package/dist/dispatchRouter.d.ts +2 -3
- package/dist/headedSerialTaskQueue.d.ts +5 -5
- package/dist/headlessLatestRerunScheduler.d.ts +2 -3
- package/dist/headlessTransport.d.ts +2 -3
- package/dist/hostController.d.ts +12 -9
- package/dist/index.d.ts +6 -6
- package/dist/index.js +169 -216
- package/dist/protocol.d.ts +78 -55
- package/dist/providers/index.d.ts +18 -20
- package/dist/providers/playwright/dispatchBrowserRpc.d.ts +5 -3
- package/dist/providers/playwright/expectUtils.d.ts +2 -3
- package/dist/providers/playwright/implementation.d.ts +1 -1
- package/dist/providers/playwright/runtime.d.ts +1 -1
- package/dist/rpcProtocol.d.ts +36 -29
- package/dist/runSession.d.ts +3 -5
- package/dist/sessionRegistry.d.ts +2 -3
- package/dist/sourceMap/sourceMapLoader.d.ts +2 -2
- package/dist/viewportPresets.d.ts +7 -3
- package/dist/watchCliShortcuts.d.ts +1 -1
- package/dist/watchRerunPlanner.d.ts +2 -2
- package/package.json +7 -9
- package/src/AGENTS.md +7 -1
- package/src/browserRpcRegistry.ts +2 -2
- package/src/client/dispatchTransport.ts +63 -1
- package/src/client/entry.ts +25 -58
- package/src/client/public.ts +3 -3
- package/src/client/snapshot.ts +24 -22
- package/src/client/sourceMapSupport.ts +1 -1
- package/src/concurrency.ts +7 -21
- package/src/configValidation.ts +2 -2
- package/src/dispatchCapabilities.ts +102 -83
- package/src/env.d.ts +6 -2
- package/src/headedSerialTaskQueue.ts +5 -1
- package/src/hostController.ts +85 -43
- package/src/index.ts +22 -11
- package/src/protocol.ts +66 -22
- package/src/providers/index.ts +8 -1
- package/src/rpcProtocol.ts +34 -21
- package/src/viewportPresets.ts +5 -0
- package/src/watchCliShortcuts.ts +2 -4
- package/dist/browser-container/container-static/css/index.5c72297783.css +0 -1
- package/dist/browser-container/container-static/js/27.099feaf1da.js.LICENSE.txt +0 -1
- package/dist/browser-container/container-static/js/lib-react.5ae9b90ee5.js.LICENSE.txt +0 -1
- package/dist/client/entry.d.ts +0 -4
- package/dist/client/fakeTimersStub.d.ts +0 -25
- package/dist/client/public.d.ts +0 -9
- package/dist/client/snapshot.d.ts +0 -29
- package/dist/client/sourceMapSupport.d.ts +0 -42
|
@@ -4,10 +4,15 @@ import type {
|
|
|
4
4
|
} from '../protocol';
|
|
5
5
|
import {
|
|
6
6
|
DISPATCH_MESSAGE_TYPE,
|
|
7
|
+
DISPATCH_NAMESPACE_RUNNER,
|
|
7
8
|
DISPATCH_RESPONSE_TYPE,
|
|
9
|
+
DISPATCH_RPC_BRIDGE_NAME,
|
|
8
10
|
DISPATCH_RPC_REQUEST_TYPE,
|
|
9
11
|
} from '../protocol';
|
|
10
12
|
|
|
13
|
+
// Coincidentally equal to the host-side RUNNER_FRAMES_READY_TIMEOUT_MS and the
|
|
14
|
+
// runner's CONFIG_WAIT_TIMEOUT_MS (entry.ts), but a semantically distinct
|
|
15
|
+
// default in a different runtime, so deliberately not shared with them.
|
|
11
16
|
const DEFAULT_RPC_TIMEOUT_MS = 30_000;
|
|
12
17
|
|
|
13
18
|
export const getRpcTimeout = (): number => {
|
|
@@ -37,6 +42,63 @@ export const createRequestId = (prefix: string): string => {
|
|
|
37
42
|
return `${prefix}-${Date.now().toString(36)}-${requestIdCounter.toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
|
|
38
43
|
};
|
|
39
44
|
|
|
45
|
+
/**
|
|
46
|
+
* Build a runner-lifecycle dispatch request.
|
|
47
|
+
*
|
|
48
|
+
* Lifecycle events (`file-ready`, `suite-start`, `suite-result`, `case-start`)
|
|
49
|
+
* share the dispatch-rpc envelope but are delivered fire-and-forget via
|
|
50
|
+
* {@link sendRunnerLifecycle}, so the request id only needs to be unique — it is
|
|
51
|
+
* produced by the shared {@link createRequestId} factory rather than a bespoke
|
|
52
|
+
* per-runner counter.
|
|
53
|
+
*/
|
|
54
|
+
export const createRunnerLifecycleRequest = (
|
|
55
|
+
method: string,
|
|
56
|
+
args: unknown,
|
|
57
|
+
): BrowserDispatchRequest => ({
|
|
58
|
+
requestId: createRequestId('runner-lifecycle'),
|
|
59
|
+
namespace: DISPATCH_NAMESPACE_RUNNER,
|
|
60
|
+
method,
|
|
61
|
+
args,
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Deliver a runner-lifecycle request fire-and-forget.
|
|
66
|
+
*
|
|
67
|
+
* Unlike {@link dispatchRpc}, this never awaits, unwraps, id-matches, or times
|
|
68
|
+
* out: the host echoes a response but the runner ignores it. Failures surface
|
|
69
|
+
* only through the optional `onError` hook (debug logging at the call site),
|
|
70
|
+
* keeping the hot test loop non-blocking.
|
|
71
|
+
*/
|
|
72
|
+
export const sendRunnerLifecycle = (
|
|
73
|
+
request: BrowserDispatchRequest,
|
|
74
|
+
onError?: (error: unknown) => void,
|
|
75
|
+
): void => {
|
|
76
|
+
if (window.parent === window) {
|
|
77
|
+
const dispatchBridge = window[DISPATCH_RPC_BRIDGE_NAME];
|
|
78
|
+
if (!dispatchBridge) {
|
|
79
|
+
onError?.(
|
|
80
|
+
new Error('Dispatch RPC bridge is not available in top-level runner.'),
|
|
81
|
+
);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
void Promise.resolve(dispatchBridge(request)).catch((error: unknown) => {
|
|
85
|
+
onError?.(error);
|
|
86
|
+
});
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
window.parent.postMessage(
|
|
91
|
+
{
|
|
92
|
+
type: DISPATCH_MESSAGE_TYPE,
|
|
93
|
+
payload: {
|
|
94
|
+
type: DISPATCH_RPC_REQUEST_TYPE,
|
|
95
|
+
payload: request,
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
'*',
|
|
99
|
+
);
|
|
100
|
+
};
|
|
101
|
+
|
|
40
102
|
const isDispatchResponse = (
|
|
41
103
|
value: unknown,
|
|
42
104
|
): value is BrowserDispatchResponse => {
|
|
@@ -116,7 +178,7 @@ export const dispatchRpc = <T>({
|
|
|
116
178
|
staleMessage: string;
|
|
117
179
|
}): Promise<T> => {
|
|
118
180
|
if (window.parent === window) {
|
|
119
|
-
const dispatchBridge = window
|
|
181
|
+
const dispatchBridge = window[DISPATCH_RPC_BRIDGE_NAME];
|
|
120
182
|
if (!dispatchBridge) {
|
|
121
183
|
throw new Error(
|
|
122
184
|
'Dispatch RPC bridge is not available in top-level runner.',
|
package/src/client/entry.ts
CHANGED
|
@@ -12,25 +12,26 @@ import type {
|
|
|
12
12
|
RunnerHooks,
|
|
13
13
|
RuntimeConfig,
|
|
14
14
|
WorkerState,
|
|
15
|
-
} from '@rstest/core/browser-runtime';
|
|
15
|
+
} from '@rstest/core/internal/browser-runtime';
|
|
16
16
|
import {
|
|
17
17
|
createBrowserTaskContext,
|
|
18
18
|
createRstestRuntime,
|
|
19
19
|
globalApis,
|
|
20
|
+
RSTEST_ENV_SYMBOL_KEY,
|
|
20
21
|
setRealTimers,
|
|
21
|
-
|
|
22
|
+
unwrapRegex,
|
|
23
|
+
} from '@rstest/core/internal/browser-runtime';
|
|
22
24
|
import { normalize } from 'pathe';
|
|
23
25
|
import type {
|
|
24
26
|
BrowserClientMessage,
|
|
25
|
-
BrowserDispatchRequest,
|
|
26
27
|
BrowserProjectRuntime,
|
|
28
|
+
RunnerLifecycleMethod,
|
|
27
29
|
} from '../protocol';
|
|
30
|
+
import { DISPATCH_MESSAGE_TYPE, RSTEST_CONFIG_MESSAGE_TYPE } from '../protocol';
|
|
28
31
|
import {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
RSTEST_CONFIG_MESSAGE_TYPE,
|
|
33
|
-
} from '../protocol';
|
|
32
|
+
createRunnerLifecycleRequest,
|
|
33
|
+
sendRunnerLifecycle,
|
|
34
|
+
} from './dispatchTransport';
|
|
34
35
|
import { BrowserSnapshotEnvironment } from './snapshot';
|
|
35
36
|
import {
|
|
36
37
|
findNewScriptUrl,
|
|
@@ -44,14 +45,6 @@ declare global {
|
|
|
44
45
|
var __coverage__: Record<string, unknown> | undefined;
|
|
45
46
|
}
|
|
46
47
|
|
|
47
|
-
type RunnerLifecycleMethod =
|
|
48
|
-
| 'file-ready'
|
|
49
|
-
| 'suite-start'
|
|
50
|
-
| 'suite-result'
|
|
51
|
-
| 'case-start';
|
|
52
|
-
|
|
53
|
-
let runnerDispatchRequestId = 0;
|
|
54
|
-
|
|
55
48
|
/**
|
|
56
49
|
* Debug logger for browser client.
|
|
57
50
|
* Only logs when debug mode is enabled (DEBUG=rstest on server side).
|
|
@@ -63,27 +56,13 @@ const debugLog = (...args: unknown[]): void => {
|
|
|
63
56
|
};
|
|
64
57
|
|
|
65
58
|
type RuntimeEnvStore = Record<string, string | undefined>;
|
|
66
|
-
const RSTEST_ENV_SYMBOL = Symbol.for(
|
|
59
|
+
const RSTEST_ENV_SYMBOL = Symbol.for(RSTEST_ENV_SYMBOL_KEY);
|
|
67
60
|
|
|
68
61
|
type GlobalWithRuntimeEnv = typeof globalThis &
|
|
69
62
|
Record<symbol, unknown> & {
|
|
70
63
|
global?: typeof globalThis;
|
|
71
64
|
};
|
|
72
65
|
|
|
73
|
-
const REGEXP_FLAG_PREFIX = 'RSTEST_REGEXP:';
|
|
74
|
-
|
|
75
|
-
const unwrapRegex = (value: string): string | RegExp => {
|
|
76
|
-
if (value.startsWith(REGEXP_FLAG_PREFIX)) {
|
|
77
|
-
const raw = value.slice(REGEXP_FLAG_PREFIX.length);
|
|
78
|
-
const match = raw.match(/^\/(.+)\/([gimuy]*)$/);
|
|
79
|
-
if (match) {
|
|
80
|
-
const [, pattern, flags] = match;
|
|
81
|
-
return new RegExp(pattern!, flags);
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
return value;
|
|
85
|
-
};
|
|
86
|
-
|
|
87
66
|
const restoreRuntimeConfig = (
|
|
88
67
|
config: BrowserProjectRuntime['runtimeConfig'],
|
|
89
68
|
): RuntimeConfig => {
|
|
@@ -230,42 +209,30 @@ const send = (message: BrowserClientMessage): void => {
|
|
|
230
209
|
}
|
|
231
210
|
// Fallback: direct call if running outside iframe (not typical)
|
|
232
211
|
// Note: This binding may not exist if not using Playwright
|
|
233
|
-
window
|
|
212
|
+
window[DISPATCH_MESSAGE_TYPE]?.(message);
|
|
234
213
|
};
|
|
235
214
|
|
|
236
215
|
const dispatchRunnerLifecycle = (
|
|
237
216
|
method: RunnerLifecycleMethod,
|
|
238
217
|
payload: unknown,
|
|
239
218
|
): void => {
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
method,
|
|
244
|
-
args: payload,
|
|
245
|
-
};
|
|
246
|
-
|
|
247
|
-
if (window.parent === window) {
|
|
248
|
-
const dispatchBridge = window.__rstest_dispatch_rpc__;
|
|
249
|
-
if (!dispatchBridge) {
|
|
250
|
-
debugLog(
|
|
251
|
-
'[Runner] Missing dispatch bridge for lifecycle method:',
|
|
252
|
-
method,
|
|
253
|
-
);
|
|
254
|
-
return;
|
|
255
|
-
}
|
|
256
|
-
void Promise.resolve(dispatchBridge(request)).catch((error: unknown) => {
|
|
219
|
+
sendRunnerLifecycle(
|
|
220
|
+
createRunnerLifecycleRequest(method, payload),
|
|
221
|
+
(error: unknown) => {
|
|
257
222
|
debugLog('[Runner] Failed to dispatch lifecycle method:', method, error);
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
send({
|
|
263
|
-
type: DISPATCH_RPC_REQUEST_TYPE,
|
|
264
|
-
payload: request,
|
|
265
|
-
});
|
|
223
|
+
},
|
|
224
|
+
);
|
|
266
225
|
};
|
|
267
226
|
|
|
268
|
-
/**
|
|
227
|
+
/**
|
|
228
|
+
* Timeout for waiting for browser config from container (30 seconds).
|
|
229
|
+
*
|
|
230
|
+
* Coincidentally equal to the RPC default (client/dispatchTransport.ts) and the
|
|
231
|
+
* host's RUNNER_FRAMES_READY_TIMEOUT_MS (hostController.ts), but semantically
|
|
232
|
+
* distinct and in a different runtime, so deliberately not shared. Implicit
|
|
233
|
+
* invariant: this must not exceed the host's frames-ready timeout, or the host
|
|
234
|
+
* declares the runner un-ready before it can even receive its config.
|
|
235
|
+
*/
|
|
269
236
|
const CONFIG_WAIT_TIMEOUT_MS = 30_000;
|
|
270
237
|
|
|
271
238
|
/**
|
package/src/client/public.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Re-export runtime API from @rstest/core/browser-runtime for browser use.
|
|
2
|
+
* Re-export runtime API from @rstest/core/internal/browser-runtime for browser use.
|
|
3
3
|
* This file is used as an alias target for '@rstest/core' in browser mode.
|
|
4
4
|
*
|
|
5
|
-
* Uses @rstest/core/browser-runtime which only exports the test APIs
|
|
5
|
+
* Uses @rstest/core/internal/browser-runtime which only exports the test APIs
|
|
6
6
|
* (describe, it, expect, etc.) without any Node.js dependencies.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
// Re-export types from @rstest/core (these are compile-time only)
|
|
10
10
|
export type { Assertion, Mock } from '@rstest/core';
|
|
11
11
|
// Re-export all public test APIs
|
|
12
|
-
export * from '@rstest/core/browser-runtime';
|
|
12
|
+
export * from '@rstest/core/internal/browser-runtime';
|
package/src/client/snapshot.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { BrowserDispatchRequest,
|
|
1
|
+
import type { BrowserDispatchRequest, SnapshotRpcCall } from '../protocol';
|
|
2
2
|
import { DISPATCH_NAMESPACE_SNAPSHOT } from '../protocol';
|
|
3
3
|
import {
|
|
4
4
|
createRequestId,
|
|
@@ -11,41 +11,36 @@ const SNAPSHOT_HEADER = '// Rstest Snapshot';
|
|
|
11
11
|
|
|
12
12
|
const createSnapshotDispatchRequest = (
|
|
13
13
|
requestId: string,
|
|
14
|
-
|
|
15
|
-
args: SnapshotRpcRequest['args'],
|
|
14
|
+
call: SnapshotRpcCall,
|
|
16
15
|
): BrowserDispatchRequest => {
|
|
17
16
|
// Snapshot is just one namespace on the shared dispatch RPC channel.
|
|
18
17
|
// Keep this mapping explicit so new runner-side RPC clients can mirror it.
|
|
19
18
|
return {
|
|
20
19
|
requestId,
|
|
21
20
|
namespace: DISPATCH_NAMESPACE_SNAPSHOT,
|
|
22
|
-
method,
|
|
23
|
-
args,
|
|
21
|
+
method: call.method,
|
|
22
|
+
args: call.args,
|
|
24
23
|
};
|
|
25
24
|
};
|
|
26
25
|
|
|
27
26
|
/**
|
|
28
27
|
* Send a snapshot RPC request to the container (parent window).
|
|
29
28
|
* The container will forward it to the host via WebSocket RPC.
|
|
29
|
+
*
|
|
30
|
+
* `call` is a {@link SnapshotRpcCall}, so `method` and `args` are validated as a
|
|
31
|
+
* pair — a mismatched argument shape fails to compile at the call site.
|
|
30
32
|
*/
|
|
31
|
-
const sendRpcRequest = <T>(
|
|
32
|
-
method: SnapshotRpcRequest['method'],
|
|
33
|
-
args: SnapshotRpcRequest['args'],
|
|
34
|
-
): Promise<T> => {
|
|
33
|
+
const sendRpcRequest = <T>(call: SnapshotRpcCall): Promise<T> => {
|
|
35
34
|
const requestId = createRequestId('snapshot-rpc');
|
|
36
35
|
const rpcTimeout = getRpcTimeout();
|
|
37
|
-
const dispatchRequest = createSnapshotDispatchRequest(
|
|
38
|
-
requestId,
|
|
39
|
-
method,
|
|
40
|
-
args,
|
|
41
|
-
);
|
|
36
|
+
const dispatchRequest = createSnapshotDispatchRequest(requestId, call);
|
|
42
37
|
|
|
43
38
|
return dispatchRpc<T>({
|
|
44
39
|
requestId,
|
|
45
40
|
request: dispatchRequest,
|
|
46
41
|
timeoutMs: rpcTimeout,
|
|
47
42
|
staleMessage: 'Stale snapshot RPC request ignored.',
|
|
48
|
-
timeoutMessage: `Snapshot RPC timeout after ${rpcTimeout / 1000}s: ${method}`,
|
|
43
|
+
timeoutMessage: `Snapshot RPC timeout after ${rpcTimeout / 1000}s: ${call.method}`,
|
|
49
44
|
});
|
|
50
45
|
};
|
|
51
46
|
|
|
@@ -67,8 +62,9 @@ export class BrowserSnapshotEnvironment {
|
|
|
67
62
|
}
|
|
68
63
|
|
|
69
64
|
async resolvePath(filepath: string): Promise<string> {
|
|
70
|
-
return sendRpcRequest<string>(
|
|
71
|
-
|
|
65
|
+
return sendRpcRequest<string>({
|
|
66
|
+
method: 'resolveSnapshotPath',
|
|
67
|
+
args: { testPath: filepath },
|
|
72
68
|
});
|
|
73
69
|
}
|
|
74
70
|
|
|
@@ -77,18 +73,24 @@ export class BrowserSnapshotEnvironment {
|
|
|
77
73
|
}
|
|
78
74
|
|
|
79
75
|
async saveSnapshotFile(filepath: string, snapshot: string): Promise<void> {
|
|
80
|
-
await sendRpcRequest<void>(
|
|
81
|
-
|
|
82
|
-
content: snapshot,
|
|
76
|
+
await sendRpcRequest<void>({
|
|
77
|
+
method: 'saveSnapshotFile',
|
|
78
|
+
args: { filepath, content: snapshot },
|
|
83
79
|
});
|
|
84
80
|
}
|
|
85
81
|
|
|
86
82
|
async readSnapshotFile(filepath: string): Promise<string | null> {
|
|
87
|
-
return sendRpcRequest<string | null>(
|
|
83
|
+
return sendRpcRequest<string | null>({
|
|
84
|
+
method: 'readSnapshotFile',
|
|
85
|
+
args: { filepath },
|
|
86
|
+
});
|
|
88
87
|
}
|
|
89
88
|
|
|
90
89
|
async removeSnapshotFile(filepath: string): Promise<void> {
|
|
91
|
-
await sendRpcRequest<void>(
|
|
90
|
+
await sendRpcRequest<void>({
|
|
91
|
+
method: 'removeSnapshotFile',
|
|
92
|
+
args: { filepath },
|
|
93
|
+
});
|
|
92
94
|
}
|
|
93
95
|
|
|
94
96
|
/**
|
|
@@ -102,7 +102,7 @@ export const preloadTestFileSourceMap = async (
|
|
|
102
102
|
* Preload source map for the runner.js file.
|
|
103
103
|
*
|
|
104
104
|
* This is essential for inline snapshot support because the snapshot code
|
|
105
|
-
* runs in runner.js (which contains @rstest/core/browser-runtime).
|
|
105
|
+
* runs in runner.js (which contains @rstest/core/internal/browser-runtime).
|
|
106
106
|
* Without this, stack traces from inline snapshots cannot be mapped back
|
|
107
107
|
* to the original source files.
|
|
108
108
|
*/
|
package/src/concurrency.ts
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
|
-
import
|
|
2
|
-
import type { Rstest } from '@rstest/core/browser';
|
|
1
|
+
import { getNumCpus, parseWorkers } from '@rstest/core/internal/browser';
|
|
2
|
+
import type { Rstest } from '@rstest/core/internal/browser';
|
|
3
|
+
|
|
4
|
+
// Re-export the shared worker primitives so existing consumers (and unit tests
|
|
5
|
+
// importing from `../src/concurrency`) keep a stable import surface.
|
|
6
|
+
export { getNumCpus, parseWorkers };
|
|
3
7
|
|
|
4
8
|
// Shared headless concurrency policy.
|
|
5
9
|
// Keep this in one place so executors reuse the same worker semantics.
|
|
@@ -13,27 +17,9 @@ type HeadlessConcurrencyContext = Pick<Rstest, 'command'> & {
|
|
|
13
17
|
};
|
|
14
18
|
};
|
|
15
19
|
|
|
16
|
-
const getNumCpus = (): number => {
|
|
17
|
-
return os.availableParallelism?.() ?? os.cpus().length;
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
export const parseWorkers = (
|
|
21
|
-
maxWorkers: string | number,
|
|
22
|
-
numCpus = getNumCpus(),
|
|
23
|
-
): number => {
|
|
24
|
-
const parsed = Number.parseInt(maxWorkers.toString(), 10);
|
|
25
|
-
|
|
26
|
-
if (typeof maxWorkers === 'string' && maxWorkers.trim().endsWith('%')) {
|
|
27
|
-
const workers = Math.floor((parsed / 100) * numCpus);
|
|
28
|
-
return Math.max(workers, 1);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
return parsed > 0 ? parsed : 1;
|
|
32
|
-
};
|
|
33
|
-
|
|
34
20
|
export const resolveDefaultHeadlessWorkers = (
|
|
35
21
|
command: HeadlessConcurrencyContext['command'],
|
|
36
|
-
numCpus = getNumCpus(),
|
|
22
|
+
numCpus: number = getNumCpus(),
|
|
37
23
|
): number => {
|
|
38
24
|
const baseWorkers = Math.max(
|
|
39
25
|
Math.min(DEFAULT_MAX_HEADLESS_WORKERS, numCpus - 1),
|
package/src/configValidation.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { RstestContext } from '@rstest/core/internal/browser';
|
|
2
2
|
import { resolveBrowserViewportPreset } from './viewportPresets';
|
|
3
3
|
|
|
4
4
|
const SUPPORTED_PROVIDERS = ['playwright'] as const;
|
|
@@ -42,7 +42,7 @@ const validateViewport = (viewport: unknown): void => {
|
|
|
42
42
|
);
|
|
43
43
|
};
|
|
44
44
|
|
|
45
|
-
export const validateBrowserConfig = (context:
|
|
45
|
+
export const validateBrowserConfig = (context: RstestContext): void => {
|
|
46
46
|
for (const project of context.projects) {
|
|
47
47
|
const { browser, output } = project.normalizedConfig;
|
|
48
48
|
if (!browser.enabled) {
|
|
@@ -1,9 +1,16 @@
|
|
|
1
|
-
import type { Reporter } from '@rstest/core/browser';
|
|
1
|
+
import type { Reporter } from '@rstest/core/internal/browser';
|
|
2
2
|
import { HostDispatchRouter } from './dispatchRouter';
|
|
3
|
+
import {
|
|
4
|
+
DISPATCH_NAMESPACE_RUNNER,
|
|
5
|
+
DISPATCH_NAMESPACE_SNAPSHOT,
|
|
6
|
+
} from './protocol';
|
|
3
7
|
import type {
|
|
4
8
|
BrowserClientMessage,
|
|
5
9
|
BrowserDispatchHandler,
|
|
6
10
|
BrowserDispatchRequest,
|
|
11
|
+
RunnerDispatchMethod,
|
|
12
|
+
SnapshotRpcMethod,
|
|
13
|
+
SnapshotRpcMethodArgs,
|
|
7
14
|
SnapshotRpcRequest,
|
|
8
15
|
} from './protocol';
|
|
9
16
|
|
|
@@ -52,37 +59,50 @@ type CreateHostDispatchRouterOptions = {
|
|
|
52
59
|
onDuplicateNamespace?: (namespace: string) => void;
|
|
53
60
|
};
|
|
54
61
|
|
|
62
|
+
/**
|
|
63
|
+
* Builds a typed {@link SnapshotRpcRequest} from the untrusted wire envelope,
|
|
64
|
+
* one builder per method. Keyed by {@link SnapshotRpcMethod}, so adding or
|
|
65
|
+
* renaming a method in the union forces a matching entry here — a stale name
|
|
66
|
+
* becomes a compile error instead of silently falling through to `null`. The
|
|
67
|
+
* `args` casts are the unavoidable trust boundary for inbound wire data.
|
|
68
|
+
*/
|
|
69
|
+
const snapshotRequestBuilders: {
|
|
70
|
+
[M in SnapshotRpcMethod]: (
|
|
71
|
+
id: string,
|
|
72
|
+
args: BrowserDispatchRequest['args'],
|
|
73
|
+
) => Extract<SnapshotRpcRequest, { method: M }>;
|
|
74
|
+
} = {
|
|
75
|
+
resolveSnapshotPath: (id, args) => ({
|
|
76
|
+
id,
|
|
77
|
+
method: 'resolveSnapshotPath',
|
|
78
|
+
args: args as SnapshotRpcMethodArgs['resolveSnapshotPath'],
|
|
79
|
+
}),
|
|
80
|
+
readSnapshotFile: (id, args) => ({
|
|
81
|
+
id,
|
|
82
|
+
method: 'readSnapshotFile',
|
|
83
|
+
args: args as SnapshotRpcMethodArgs['readSnapshotFile'],
|
|
84
|
+
}),
|
|
85
|
+
saveSnapshotFile: (id, args) => ({
|
|
86
|
+
id,
|
|
87
|
+
method: 'saveSnapshotFile',
|
|
88
|
+
args: args as SnapshotRpcMethodArgs['saveSnapshotFile'],
|
|
89
|
+
}),
|
|
90
|
+
removeSnapshotFile: (id, args) => ({
|
|
91
|
+
id,
|
|
92
|
+
method: 'removeSnapshotFile',
|
|
93
|
+
args: args as SnapshotRpcMethodArgs['removeSnapshotFile'],
|
|
94
|
+
}),
|
|
95
|
+
};
|
|
96
|
+
|
|
55
97
|
const toSnapshotRpcRequest = (
|
|
56
98
|
request: BrowserDispatchRequest,
|
|
57
99
|
): SnapshotRpcRequest | null => {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
};
|
|
65
|
-
case 'readSnapshotFile':
|
|
66
|
-
return {
|
|
67
|
-
id: request.requestId,
|
|
68
|
-
method: 'readSnapshotFile',
|
|
69
|
-
args: request.args as { filepath: string },
|
|
70
|
-
};
|
|
71
|
-
case 'saveSnapshotFile':
|
|
72
|
-
return {
|
|
73
|
-
id: request.requestId,
|
|
74
|
-
method: 'saveSnapshotFile',
|
|
75
|
-
args: request.args as { filepath: string; content: string },
|
|
76
|
-
};
|
|
77
|
-
case 'removeSnapshotFile':
|
|
78
|
-
return {
|
|
79
|
-
id: request.requestId,
|
|
80
|
-
method: 'removeSnapshotFile',
|
|
81
|
-
args: request.args as { filepath: string },
|
|
82
|
-
};
|
|
83
|
-
default:
|
|
84
|
-
return null;
|
|
85
|
-
}
|
|
100
|
+
const builder = snapshotRequestBuilders[
|
|
101
|
+
request.method as SnapshotRpcMethod
|
|
102
|
+
] as
|
|
103
|
+
| ((id: string, args: BrowserDispatchRequest['args']) => SnapshotRpcRequest)
|
|
104
|
+
| undefined;
|
|
105
|
+
return builder ? builder(request.requestId, request.args) : null;
|
|
86
106
|
};
|
|
87
107
|
|
|
88
108
|
export const createHostDispatchRouter = ({
|
|
@@ -94,61 +114,60 @@ export const createHostDispatchRouter = ({
|
|
|
94
114
|
}: CreateHostDispatchRouterOptions): HostDispatchRouter => {
|
|
95
115
|
const router = new HostDispatchRouter(routerOptions);
|
|
96
116
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
break;
|
|
129
|
-
case 'file-complete':
|
|
130
|
-
await runnerCallbacks.onTestFileComplete(
|
|
131
|
-
request.args as RunnerPayload<'file-complete'>,
|
|
132
|
-
);
|
|
133
|
-
break;
|
|
134
|
-
case 'log':
|
|
135
|
-
await runnerCallbacks.onLog(request.args as RunnerPayload<'log'>);
|
|
136
|
-
break;
|
|
137
|
-
case 'fatal':
|
|
138
|
-
await runnerCallbacks.onFatal(request.args as RunnerPayload<'fatal'>);
|
|
139
|
-
break;
|
|
140
|
-
default:
|
|
141
|
-
break;
|
|
142
|
-
}
|
|
143
|
-
});
|
|
117
|
+
// Keyed by RunnerDispatchMethod so adding a runner method to that union forces
|
|
118
|
+
// a handler entry here (a missing key is a compile error) — the previous
|
|
119
|
+
// `switch` with `default: break` silently dropped unhandled methods. The
|
|
120
|
+
// `args` casts are checked against each callback's parameter type, so a wrong
|
|
121
|
+
// payload type also fails to compile.
|
|
122
|
+
const runnerMethodHandlers: {
|
|
123
|
+
[M in RunnerDispatchMethod]: (
|
|
124
|
+
args: BrowserDispatchRequest['args'],
|
|
125
|
+
) => Promise<void>;
|
|
126
|
+
} = {
|
|
127
|
+
'file-start': (args) =>
|
|
128
|
+
runnerCallbacks.onTestFileStart(args as RunnerPayload<'file-start'>),
|
|
129
|
+
'file-ready': (args) =>
|
|
130
|
+
runnerCallbacks.onTestFileReady(args as RunnerDispatchFileReadyPayload),
|
|
131
|
+
'suite-start': (args) =>
|
|
132
|
+
runnerCallbacks.onTestSuiteStart(args as RunnerDispatchSuiteStartPayload),
|
|
133
|
+
'suite-result': (args) =>
|
|
134
|
+
runnerCallbacks.onTestSuiteResult(
|
|
135
|
+
args as RunnerDispatchSuiteResultPayload,
|
|
136
|
+
),
|
|
137
|
+
'case-start': (args) =>
|
|
138
|
+
runnerCallbacks.onTestCaseStart(args as RunnerDispatchCaseStartPayload),
|
|
139
|
+
'case-result': (args) =>
|
|
140
|
+
runnerCallbacks.onTestCaseResult(args as RunnerPayload<'case-result'>),
|
|
141
|
+
'file-complete': (args) =>
|
|
142
|
+
runnerCallbacks.onTestFileComplete(
|
|
143
|
+
args as RunnerPayload<'file-complete'>,
|
|
144
|
+
),
|
|
145
|
+
log: (args) => runnerCallbacks.onLog(args as RunnerPayload<'log'>),
|
|
146
|
+
fatal: (args) => runnerCallbacks.onFatal(args as RunnerPayload<'fatal'>),
|
|
147
|
+
};
|
|
144
148
|
|
|
145
|
-
router.register(
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
149
|
+
router.register(
|
|
150
|
+
DISPATCH_NAMESPACE_RUNNER,
|
|
151
|
+
async (request: BrowserDispatchRequest) => {
|
|
152
|
+
// `request.method` is untrusted wire data, so the lookup may miss. Unknown
|
|
153
|
+
// methods are ignored for forward-compatibility with newer runners,
|
|
154
|
+
// matching the previous `default: break`.
|
|
155
|
+
await runnerMethodHandlers[request.method as RunnerDispatchMethod]?.(
|
|
156
|
+
request.args,
|
|
157
|
+
);
|
|
158
|
+
},
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
router.register(
|
|
162
|
+
DISPATCH_NAMESPACE_SNAPSHOT,
|
|
163
|
+
async (request: BrowserDispatchRequest) => {
|
|
164
|
+
const snapshotRequest = toSnapshotRpcRequest(request);
|
|
165
|
+
if (!snapshotRequest) {
|
|
166
|
+
return undefined;
|
|
167
|
+
}
|
|
168
|
+
return runSnapshotRpc(snapshotRequest);
|
|
169
|
+
},
|
|
170
|
+
);
|
|
152
171
|
|
|
153
172
|
for (const [namespace, handler] of extensionHandlers ?? []) {
|
|
154
173
|
if (router.has(namespace)) {
|
package/src/env.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { DISPATCH_MESSAGE_TYPE, DISPATCH_RPC_BRIDGE_NAME } from './protocol';
|
|
1
2
|
import type {
|
|
2
3
|
BrowserClientMessage,
|
|
3
4
|
BrowserDispatchRequest,
|
|
@@ -38,8 +39,11 @@ declare global {
|
|
|
38
39
|
|
|
39
40
|
interface Window {
|
|
40
41
|
__RSTEST_BROWSER_OPTIONS__?: BrowserHostConfig;
|
|
41
|
-
|
|
42
|
-
|
|
42
|
+
// Keyed by the sentinel constants so the declared global name and the
|
|
43
|
+
// constant are the same source — renaming a constant moves this key with it,
|
|
44
|
+
// and every `window[CONST]` access site stays in lockstep automatically.
|
|
45
|
+
[DISPATCH_MESSAGE_TYPE]?: (message: BrowserClientMessage) => void;
|
|
46
|
+
[DISPATCH_RPC_BRIDGE_NAME]?: (
|
|
43
47
|
request: BrowserDispatchRequest,
|
|
44
48
|
) => Promise<unknown>;
|
|
45
49
|
__rstest_container_dispatch__?: (data: unknown) => void;
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
type HeadedSerialTask = () => Promise<void>;
|
|
2
2
|
|
|
3
|
+
type HeadedSerialTaskQueue = {
|
|
4
|
+
enqueue: (task: HeadedSerialTask) => Promise<void>;
|
|
5
|
+
};
|
|
6
|
+
|
|
3
7
|
/**
|
|
4
8
|
* Serializes headed browser file execution so only one task runs at a time.
|
|
5
9
|
* The queue keeps draining even if an earlier task rejects.
|
|
6
10
|
*/
|
|
7
|
-
export const createHeadedSerialTaskQueue = () => {
|
|
11
|
+
export const createHeadedSerialTaskQueue = (): HeadedSerialTaskQueue => {
|
|
8
12
|
let queue: Promise<void> = Promise.resolve();
|
|
9
13
|
|
|
10
14
|
const enqueue = (task: HeadedSerialTask): Promise<void> => {
|