@rstest/browser 0.7.9
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/css/container.dc438e35.css +1 -0
- package/dist/browser-container/container-static/js/916.5aee8d2f.js +23549 -0
- package/dist/browser-container/container-static/js/916.5aee8d2f.js.LICENSE.txt +1 -0
- package/dist/browser-container/container-static/js/container.5ddd46c3.js +2281 -0
- package/dist/browser-container/container-static/js/lib-react.a9c9a89b.js +8464 -0
- package/dist/browser-container/container-static/js/lib-react.a9c9a89b.js.LICENSE.txt +1 -0
- package/dist/browser-container/container.html +14 -0
- package/dist/client/entry.d.ts +7 -0
- package/dist/client/fakeTimersStub.d.ts +25 -0
- package/dist/client/public.d.ts +9 -0
- package/dist/client/snapshot.d.ts +35 -0
- package/dist/client/sourceMapSupport.d.ts +45 -0
- package/dist/hostController.d.ts +17 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +2576 -0
- package/dist/protocol.d.ts +134 -0
- package/dist/rslib-runtime.js +18 -0
- package/package.json +80 -0
- package/src/client/entry.ts +628 -0
- package/src/client/fakeTimersStub.ts +91 -0
- package/src/client/public.ts +12 -0
- package/src/client/snapshot.ts +177 -0
- package/src/client/sourceMapSupport.ts +178 -0
- package/src/env.d.ts +43 -0
- package/src/hostController.ts +1761 -0
- package/src/index.ts +18 -0
- package/src/manifest.d.ts +41 -0
- package/src/protocol.ts +132 -0
|
@@ -0,0 +1,628 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type ManifestProjectConfig,
|
|
3
|
+
type ManifestTestContext,
|
|
4
|
+
projectSetupLoaders,
|
|
5
|
+
// Multi-project APIs
|
|
6
|
+
projects,
|
|
7
|
+
projectTestContexts,
|
|
8
|
+
} from '@rstest/browser-manifest';
|
|
9
|
+
import type {
|
|
10
|
+
RunnerHooks,
|
|
11
|
+
RuntimeConfig,
|
|
12
|
+
WorkerState,
|
|
13
|
+
} from '@rstest/core/browser-runtime';
|
|
14
|
+
import {
|
|
15
|
+
createRstestRuntime,
|
|
16
|
+
globalApis,
|
|
17
|
+
setRealTimers,
|
|
18
|
+
} from '@rstest/core/browser-runtime';
|
|
19
|
+
import { normalize } from 'pathe';
|
|
20
|
+
import type {
|
|
21
|
+
BrowserClientMessage,
|
|
22
|
+
BrowserHostConfig,
|
|
23
|
+
BrowserProjectRuntime,
|
|
24
|
+
} from '../protocol';
|
|
25
|
+
import { BrowserSnapshotEnvironment } from './snapshot';
|
|
26
|
+
import {
|
|
27
|
+
findNewScriptUrl,
|
|
28
|
+
getScriptUrls,
|
|
29
|
+
preloadRunnerSourceMap,
|
|
30
|
+
preloadTestFileSourceMap,
|
|
31
|
+
} from './sourceMapSupport';
|
|
32
|
+
|
|
33
|
+
declare global {
|
|
34
|
+
interface Window {
|
|
35
|
+
__RSTEST_BROWSER_OPTIONS__?: BrowserHostConfig;
|
|
36
|
+
__rstest_dispatch__?: (message: BrowserClientMessage) => void;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Debug logger for browser client.
|
|
42
|
+
* Only logs when debug mode is enabled (DEBUG=rstest on server side).
|
|
43
|
+
*/
|
|
44
|
+
const debugLog = (...args: unknown[]): void => {
|
|
45
|
+
if (window.__RSTEST_BROWSER_OPTIONS__?.debug) {
|
|
46
|
+
console.log(...args);
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
type GlobalWithProcess = typeof globalThis & {
|
|
51
|
+
global?: typeof globalThis;
|
|
52
|
+
process?: NodeJS.Process;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const REGEXP_FLAG_PREFIX = 'RSTEST_REGEXP:';
|
|
56
|
+
|
|
57
|
+
const unwrapRegex = (value: string): string | RegExp => {
|
|
58
|
+
if (value.startsWith(REGEXP_FLAG_PREFIX)) {
|
|
59
|
+
const raw = value.slice(REGEXP_FLAG_PREFIX.length);
|
|
60
|
+
const match = raw.match(/^\/(.+)\/([gimuy]*)$/);
|
|
61
|
+
if (match) {
|
|
62
|
+
const [, pattern, flags] = match;
|
|
63
|
+
return new RegExp(pattern!, flags);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return value;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const restoreRuntimeConfig = (
|
|
70
|
+
config: BrowserProjectRuntime['runtimeConfig'],
|
|
71
|
+
): RuntimeConfig => {
|
|
72
|
+
const { testNamePattern } = config as RuntimeConfig;
|
|
73
|
+
return {
|
|
74
|
+
...config,
|
|
75
|
+
testNamePattern:
|
|
76
|
+
typeof testNamePattern === 'string'
|
|
77
|
+
? unwrapRegex(testNamePattern)
|
|
78
|
+
: testNamePattern,
|
|
79
|
+
};
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const ensureProcessEnv = (env: RuntimeConfig['env'] | undefined): void => {
|
|
83
|
+
const globalRef = globalThis as GlobalWithProcess;
|
|
84
|
+
if (!globalRef.global) {
|
|
85
|
+
globalRef.global = globalRef;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (!globalRef.process) {
|
|
89
|
+
const processShim: Partial<NodeJS.Process> & {
|
|
90
|
+
env: Record<string, string | undefined>;
|
|
91
|
+
} = {
|
|
92
|
+
env: {},
|
|
93
|
+
argv: [],
|
|
94
|
+
version: 'browser',
|
|
95
|
+
cwd: () => '/',
|
|
96
|
+
platform: 'linux',
|
|
97
|
+
nextTick: (cb: (...args: unknown[]) => void, ...args: unknown[]) =>
|
|
98
|
+
queueMicrotask(() => cb(...args)),
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
globalRef.process = processShim as unknown as NodeJS.Process;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
globalRef.process.env ??= {};
|
|
105
|
+
|
|
106
|
+
if (env) {
|
|
107
|
+
for (const [key, value] of Object.entries(env)) {
|
|
108
|
+
if (value === undefined) {
|
|
109
|
+
delete globalRef.process.env[key];
|
|
110
|
+
} else {
|
|
111
|
+
globalRef.process.env[key] = value;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Format an argument for console output.
|
|
119
|
+
*/
|
|
120
|
+
const formatArg = (arg: unknown): string => {
|
|
121
|
+
if (arg === null) return 'null';
|
|
122
|
+
if (arg === undefined) return 'undefined';
|
|
123
|
+
if (typeof arg === 'string') return arg;
|
|
124
|
+
if (typeof arg === 'number' || typeof arg === 'boolean') return String(arg);
|
|
125
|
+
if (arg instanceof Error) {
|
|
126
|
+
return arg.stack || `${arg.name}: ${arg.message}`;
|
|
127
|
+
}
|
|
128
|
+
try {
|
|
129
|
+
return JSON.stringify(arg, null, 2);
|
|
130
|
+
} catch {
|
|
131
|
+
return String(arg);
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Intercept console methods and forward to host via send().
|
|
137
|
+
* Returns a restore function to revert console to original.
|
|
138
|
+
*/
|
|
139
|
+
const interceptConsole = (
|
|
140
|
+
testPath: string,
|
|
141
|
+
printConsoleTrace: boolean,
|
|
142
|
+
disableConsoleIntercept: boolean,
|
|
143
|
+
): (() => void) => {
|
|
144
|
+
if (disableConsoleIntercept) {
|
|
145
|
+
return () => {};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const originalConsole = {
|
|
149
|
+
log: console.log.bind(console),
|
|
150
|
+
warn: console.warn.bind(console),
|
|
151
|
+
error: console.error.bind(console),
|
|
152
|
+
info: console.info.bind(console),
|
|
153
|
+
debug: console.debug.bind(console),
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
const getConsoleTrace = (): string | undefined => {
|
|
157
|
+
if (!printConsoleTrace) return undefined;
|
|
158
|
+
const stack = new Error('STACK_TRACE').stack;
|
|
159
|
+
// Skip: Error, getConsoleTrace, createConsoleInterceptor wrapper, console.log call
|
|
160
|
+
return stack?.split('\n').slice(4).join('\n');
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
const createConsoleInterceptor = (
|
|
164
|
+
level: 'log' | 'warn' | 'error' | 'info' | 'debug',
|
|
165
|
+
) => {
|
|
166
|
+
return (...args: unknown[]) => {
|
|
167
|
+
// Call original for browser DevTools
|
|
168
|
+
originalConsole[level](...args);
|
|
169
|
+
|
|
170
|
+
// Format message
|
|
171
|
+
const content = args.map(formatArg).join(' ');
|
|
172
|
+
|
|
173
|
+
// Send to host
|
|
174
|
+
send({
|
|
175
|
+
type: 'log',
|
|
176
|
+
payload: {
|
|
177
|
+
level,
|
|
178
|
+
content,
|
|
179
|
+
testPath,
|
|
180
|
+
type: level === 'error' || level === 'warn' ? 'stderr' : 'stdout',
|
|
181
|
+
trace: getConsoleTrace(),
|
|
182
|
+
},
|
|
183
|
+
});
|
|
184
|
+
};
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
console.log = createConsoleInterceptor('log');
|
|
188
|
+
console.warn = createConsoleInterceptor('warn');
|
|
189
|
+
console.error = createConsoleInterceptor('error');
|
|
190
|
+
console.info = createConsoleInterceptor('info');
|
|
191
|
+
console.debug = createConsoleInterceptor('debug');
|
|
192
|
+
|
|
193
|
+
return () => {
|
|
194
|
+
console.log = originalConsole.log;
|
|
195
|
+
console.warn = originalConsole.warn;
|
|
196
|
+
console.error = originalConsole.error;
|
|
197
|
+
console.info = originalConsole.info;
|
|
198
|
+
console.debug = originalConsole.debug;
|
|
199
|
+
};
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
const send = (message: BrowserClientMessage): void => {
|
|
203
|
+
// If in iframe, send to parent window (container) which will forward to host via RPC
|
|
204
|
+
if (window.parent !== window) {
|
|
205
|
+
window.parent.postMessage(
|
|
206
|
+
{ type: '__rstest_dispatch__', payload: message },
|
|
207
|
+
'*',
|
|
208
|
+
);
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
// Fallback: direct call if running outside iframe (not typical)
|
|
212
|
+
// Note: This binding may not exist if not using Playwright
|
|
213
|
+
window.__rstest_dispatch__?.(message);
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
/** Timeout for waiting for browser config from container (30 seconds) */
|
|
217
|
+
const CONFIG_WAIT_TIMEOUT_MS = 30_000;
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Wait for configuration from container if running in iframe.
|
|
221
|
+
* This is a prerequisite for test execution - without config, tests cannot run.
|
|
222
|
+
*/
|
|
223
|
+
const waitForConfig = (): Promise<void> => {
|
|
224
|
+
// If not in iframe or already has config, resolve immediately
|
|
225
|
+
if (window.parent === window || window.__RSTEST_BROWSER_OPTIONS__) {
|
|
226
|
+
return Promise.resolve();
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return new Promise((resolve, reject) => {
|
|
230
|
+
const handleMessage = (event: MessageEvent) => {
|
|
231
|
+
if (event.data?.type === 'RSTEST_CONFIG') {
|
|
232
|
+
window.__RSTEST_BROWSER_OPTIONS__ = event.data.payload;
|
|
233
|
+
debugLog(
|
|
234
|
+
'[Runner] Received config from container:',
|
|
235
|
+
event.data.payload,
|
|
236
|
+
);
|
|
237
|
+
window.removeEventListener('message', handleMessage);
|
|
238
|
+
resolve();
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
window.addEventListener('message', handleMessage);
|
|
243
|
+
|
|
244
|
+
setTimeout(() => {
|
|
245
|
+
window.removeEventListener('message', handleMessage);
|
|
246
|
+
reject(
|
|
247
|
+
new Error(
|
|
248
|
+
`[Rstest] Failed to receive browser config within ${CONFIG_WAIT_TIMEOUT_MS / 1000}s. ` +
|
|
249
|
+
'This may indicate a connection issue between the runner iframe and container.',
|
|
250
|
+
),
|
|
251
|
+
);
|
|
252
|
+
}, CONFIG_WAIT_TIMEOUT_MS);
|
|
253
|
+
});
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Convert absolute path to context key (relative path)
|
|
258
|
+
* e.g., '/project/src/foo.test.ts' -> './src/foo.test.ts'
|
|
259
|
+
* 'D:/project/src/foo.test.ts' -> './src/foo.test.ts'
|
|
260
|
+
*
|
|
261
|
+
* Uses pathe's normalize to handle cross-platform path separators.
|
|
262
|
+
*/
|
|
263
|
+
const toContextKey = (absolutePath: string, projectRoot: string): string => {
|
|
264
|
+
// Normalize both paths to use forward slashes for cross-platform compatibility
|
|
265
|
+
const normalizedAbsolute = normalize(absolutePath);
|
|
266
|
+
const normalizedRoot = normalize(projectRoot);
|
|
267
|
+
|
|
268
|
+
let relative = normalizedAbsolute;
|
|
269
|
+
if (normalizedAbsolute.startsWith(normalizedRoot)) {
|
|
270
|
+
relative = normalizedAbsolute.slice(normalizedRoot.length);
|
|
271
|
+
}
|
|
272
|
+
return relative.startsWith('/') ? `.${relative}` : `./${relative}`;
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Convert context key to absolute path
|
|
277
|
+
* e.g., './src/foo.test.ts' -> '/project/src/foo.test.ts'
|
|
278
|
+
*/
|
|
279
|
+
const toAbsolutePath = (key: string, projectRoot: string): string => {
|
|
280
|
+
// key format: ./src/foo.test.ts
|
|
281
|
+
// Ensure no double slashes by removing trailing slash from projectRoot
|
|
282
|
+
const normalizedRoot = normalize(projectRoot).replace(/\/$/, '');
|
|
283
|
+
return normalizedRoot + key.slice(1);
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Find the project that contains the given test file.
|
|
288
|
+
* Matches by checking if the testFile path starts with the project root.
|
|
289
|
+
*
|
|
290
|
+
* Uses pathe's normalize to handle cross-platform path separators.
|
|
291
|
+
*/
|
|
292
|
+
const findProjectForTestFile = (
|
|
293
|
+
testFile: string,
|
|
294
|
+
allProjects: ManifestProjectConfig[],
|
|
295
|
+
): ManifestProjectConfig | undefined => {
|
|
296
|
+
// Normalize the test file path for cross-platform compatibility
|
|
297
|
+
const normalizedTestFile = normalize(testFile);
|
|
298
|
+
|
|
299
|
+
// Sort projects by root path length (longest first) for most specific match
|
|
300
|
+
const sorted = [...allProjects].sort(
|
|
301
|
+
(a, b) => b.projectRoot.length - a.projectRoot.length,
|
|
302
|
+
);
|
|
303
|
+
|
|
304
|
+
for (const proj of sorted) {
|
|
305
|
+
// projectRoot should already be normalized, but normalize again for safety
|
|
306
|
+
const normalizedRoot = normalize(proj.projectRoot);
|
|
307
|
+
if (normalizedTestFile.startsWith(normalizedRoot)) {
|
|
308
|
+
return proj;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Fallback to first project
|
|
313
|
+
return allProjects[0];
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
const run = async () => {
|
|
317
|
+
// Wait for configuration if in iframe
|
|
318
|
+
await waitForConfig();
|
|
319
|
+
let options = window.__RSTEST_BROWSER_OPTIONS__;
|
|
320
|
+
|
|
321
|
+
// Support reading testFile and testNamePattern from URL parameters
|
|
322
|
+
const urlParams = new URLSearchParams(window.location.search);
|
|
323
|
+
const urlTestFile = urlParams.get('testFile');
|
|
324
|
+
const urlTestNamePattern = urlParams.get('testNamePattern');
|
|
325
|
+
|
|
326
|
+
if (urlTestFile && options) {
|
|
327
|
+
// Override testFile from URL parameter
|
|
328
|
+
options = {
|
|
329
|
+
...options,
|
|
330
|
+
testFile: urlTestFile,
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Override testNamePattern from URL parameter if provided
|
|
335
|
+
if (urlTestNamePattern && options) {
|
|
336
|
+
options = {
|
|
337
|
+
...options,
|
|
338
|
+
projects: options.projects.map((project) => ({
|
|
339
|
+
...project,
|
|
340
|
+
runtimeConfig: {
|
|
341
|
+
...project.runtimeConfig,
|
|
342
|
+
testNamePattern: urlTestNamePattern,
|
|
343
|
+
},
|
|
344
|
+
})),
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
if (!options) {
|
|
349
|
+
send({
|
|
350
|
+
type: 'fatal',
|
|
351
|
+
payload: {
|
|
352
|
+
message: 'Browser test runtime is not configured.',
|
|
353
|
+
},
|
|
354
|
+
});
|
|
355
|
+
window.__RSTEST_DONE__ = true;
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
send({ type: 'ready' });
|
|
360
|
+
|
|
361
|
+
setRealTimers();
|
|
362
|
+
|
|
363
|
+
// Preload runner.js sourcemap for inline snapshot support.
|
|
364
|
+
// The snapshot code runs in runner.js, so we need its sourcemap
|
|
365
|
+
// to map stack traces back to original source files.
|
|
366
|
+
await preloadRunnerSourceMap();
|
|
367
|
+
|
|
368
|
+
// Find the project for this test file
|
|
369
|
+
const targetTestFile = options.testFile;
|
|
370
|
+
const currentProject = targetTestFile
|
|
371
|
+
? findProjectForTestFile(
|
|
372
|
+
targetTestFile,
|
|
373
|
+
projects as ManifestProjectConfig[],
|
|
374
|
+
)
|
|
375
|
+
: (projects as ManifestProjectConfig[])[0];
|
|
376
|
+
|
|
377
|
+
if (!currentProject) {
|
|
378
|
+
send({
|
|
379
|
+
type: 'fatal',
|
|
380
|
+
payload: {
|
|
381
|
+
message: 'No project found for test file',
|
|
382
|
+
},
|
|
383
|
+
});
|
|
384
|
+
window.__RSTEST_DONE__ = true;
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// Find the runtime config for this project
|
|
389
|
+
const projectRuntime = options.projects.find(
|
|
390
|
+
(p) => p.name === currentProject.name,
|
|
391
|
+
);
|
|
392
|
+
if (!projectRuntime) {
|
|
393
|
+
send({
|
|
394
|
+
type: 'fatal',
|
|
395
|
+
payload: {
|
|
396
|
+
message: `Project ${currentProject.name} not found in runtime options`,
|
|
397
|
+
},
|
|
398
|
+
});
|
|
399
|
+
window.__RSTEST_DONE__ = true;
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
const runtimeConfig = restoreRuntimeConfig(projectRuntime.runtimeConfig);
|
|
404
|
+
ensureProcessEnv(runtimeConfig.env);
|
|
405
|
+
|
|
406
|
+
// Get this project's setup loaders and test context
|
|
407
|
+
const currentSetupLoaders =
|
|
408
|
+
(projectSetupLoaders as Record<string, Array<() => Promise<unknown>>>)[
|
|
409
|
+
currentProject.name
|
|
410
|
+
] || [];
|
|
411
|
+
const currentTestContext = (
|
|
412
|
+
projectTestContexts as Record<string, ManifestTestContext>
|
|
413
|
+
)[currentProject.name];
|
|
414
|
+
|
|
415
|
+
if (!currentTestContext) {
|
|
416
|
+
send({
|
|
417
|
+
type: 'fatal',
|
|
418
|
+
payload: {
|
|
419
|
+
message: `Test context not found for project ${currentProject.name}`,
|
|
420
|
+
},
|
|
421
|
+
});
|
|
422
|
+
window.__RSTEST_DONE__ = true;
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// 1. Load setup files for this project
|
|
427
|
+
for (const loadSetup of currentSetupLoaders) {
|
|
428
|
+
await loadSetup();
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// 2. Determine which test files to run
|
|
432
|
+
let testKeysToRun: string[];
|
|
433
|
+
|
|
434
|
+
if (targetTestFile) {
|
|
435
|
+
// Single file mode: convert absolute path to context key
|
|
436
|
+
const key = toContextKey(targetTestFile, currentProject.projectRoot);
|
|
437
|
+
testKeysToRun = [key];
|
|
438
|
+
} else {
|
|
439
|
+
// Full run mode: get all test keys from context
|
|
440
|
+
testKeysToRun = currentTestContext.getTestKeys();
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// Check execution mode
|
|
444
|
+
const executionMode = options.mode || 'run';
|
|
445
|
+
|
|
446
|
+
// Collect mode: only gather test metadata without running
|
|
447
|
+
if (executionMode === 'collect') {
|
|
448
|
+
for (const key of testKeysToRun) {
|
|
449
|
+
const testPath = toAbsolutePath(key, currentProject.projectRoot);
|
|
450
|
+
|
|
451
|
+
const workerState: WorkerState = {
|
|
452
|
+
project: projectRuntime.name,
|
|
453
|
+
projectRoot: projectRuntime.projectRoot,
|
|
454
|
+
rootPath: options.rootPath,
|
|
455
|
+
runtimeConfig,
|
|
456
|
+
taskId: 0,
|
|
457
|
+
outputModule: false,
|
|
458
|
+
environment: 'browser',
|
|
459
|
+
testPath,
|
|
460
|
+
distPath: testPath,
|
|
461
|
+
snapshotOptions: {
|
|
462
|
+
updateSnapshot: options.snapshot.updateSnapshot,
|
|
463
|
+
snapshotEnvironment: new BrowserSnapshotEnvironment(),
|
|
464
|
+
snapshotFormat: runtimeConfig.snapshotFormat,
|
|
465
|
+
},
|
|
466
|
+
};
|
|
467
|
+
|
|
468
|
+
const runtime = await createRstestRuntime(workerState);
|
|
469
|
+
|
|
470
|
+
// Register global APIs if globals config is enabled
|
|
471
|
+
if (runtimeConfig.globals) {
|
|
472
|
+
for (const apiKey of globalApis) {
|
|
473
|
+
(globalThis as any)[apiKey] = (runtime.api as any)[apiKey];
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
try {
|
|
478
|
+
// Load the test file dynamically (registers tests without running)
|
|
479
|
+
await currentTestContext.loadTest(key);
|
|
480
|
+
|
|
481
|
+
// Collect tests metadata
|
|
482
|
+
const tests = await runtime.runner.collectTests();
|
|
483
|
+
|
|
484
|
+
send({
|
|
485
|
+
type: 'collect-result',
|
|
486
|
+
payload: {
|
|
487
|
+
testPath,
|
|
488
|
+
project: projectRuntime.name,
|
|
489
|
+
tests,
|
|
490
|
+
},
|
|
491
|
+
});
|
|
492
|
+
} catch (_error) {
|
|
493
|
+
const error =
|
|
494
|
+
_error instanceof Error ? _error : new Error(String(_error));
|
|
495
|
+
send({
|
|
496
|
+
type: 'fatal',
|
|
497
|
+
payload: {
|
|
498
|
+
message: error.message,
|
|
499
|
+
stack: error.stack,
|
|
500
|
+
},
|
|
501
|
+
});
|
|
502
|
+
window.__RSTEST_DONE__ = true;
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
send({ type: 'collect-complete' });
|
|
508
|
+
window.__RSTEST_DONE__ = true;
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// 3. Run tests for each file
|
|
513
|
+
for (const key of testKeysToRun) {
|
|
514
|
+
const testPath = toAbsolutePath(key, currentProject.projectRoot);
|
|
515
|
+
|
|
516
|
+
// Intercept console methods to forward logs to host
|
|
517
|
+
const restoreConsole = interceptConsole(
|
|
518
|
+
testPath,
|
|
519
|
+
runtimeConfig.printConsoleTrace ?? false,
|
|
520
|
+
runtimeConfig.disableConsoleIntercept ?? false,
|
|
521
|
+
);
|
|
522
|
+
|
|
523
|
+
const workerState: WorkerState = {
|
|
524
|
+
project: projectRuntime.name,
|
|
525
|
+
projectRoot: projectRuntime.projectRoot,
|
|
526
|
+
rootPath: options.rootPath,
|
|
527
|
+
runtimeConfig,
|
|
528
|
+
taskId: 0,
|
|
529
|
+
outputModule: false,
|
|
530
|
+
environment: 'browser',
|
|
531
|
+
testPath,
|
|
532
|
+
distPath: testPath,
|
|
533
|
+
snapshotOptions: {
|
|
534
|
+
updateSnapshot: options.snapshot.updateSnapshot,
|
|
535
|
+
snapshotEnvironment: new BrowserSnapshotEnvironment(),
|
|
536
|
+
snapshotFormat: runtimeConfig.snapshotFormat,
|
|
537
|
+
},
|
|
538
|
+
};
|
|
539
|
+
|
|
540
|
+
const runtime = await createRstestRuntime(workerState);
|
|
541
|
+
|
|
542
|
+
// Register global APIs if globals config is enabled
|
|
543
|
+
if (runtimeConfig.globals) {
|
|
544
|
+
for (const apiKey of globalApis) {
|
|
545
|
+
(globalThis as any)[apiKey] = (runtime.api as any)[apiKey];
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
let failedTestsCount = 0;
|
|
550
|
+
|
|
551
|
+
const runnerHooks: RunnerHooks = {
|
|
552
|
+
onTestCaseResult: async (result) => {
|
|
553
|
+
if (result.status === 'fail') {
|
|
554
|
+
failedTestsCount++;
|
|
555
|
+
}
|
|
556
|
+
send({
|
|
557
|
+
type: 'case-result',
|
|
558
|
+
payload: result,
|
|
559
|
+
});
|
|
560
|
+
},
|
|
561
|
+
getCountOfFailedTests: async () => failedTestsCount,
|
|
562
|
+
};
|
|
563
|
+
|
|
564
|
+
send({
|
|
565
|
+
type: 'file-start',
|
|
566
|
+
payload: {
|
|
567
|
+
testPath,
|
|
568
|
+
projectName: projectRuntime.name,
|
|
569
|
+
},
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
try {
|
|
573
|
+
// Record script URLs before loading the test file
|
|
574
|
+
const beforeScripts = getScriptUrls();
|
|
575
|
+
|
|
576
|
+
// Load the test file dynamically using this project's context
|
|
577
|
+
await currentTestContext.loadTest(key);
|
|
578
|
+
|
|
579
|
+
// Find the newly loaded chunk and preload its source map (for inline snapshots)
|
|
580
|
+
const afterScripts = getScriptUrls();
|
|
581
|
+
const chunkUrl = findNewScriptUrl(beforeScripts, afterScripts);
|
|
582
|
+
if (chunkUrl) {
|
|
583
|
+
await preloadTestFileSourceMap(chunkUrl);
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
const result = await runtime.runner.runTests(
|
|
587
|
+
testPath,
|
|
588
|
+
runnerHooks,
|
|
589
|
+
runtime.api,
|
|
590
|
+
);
|
|
591
|
+
|
|
592
|
+
send({
|
|
593
|
+
type: 'file-complete',
|
|
594
|
+
payload: result,
|
|
595
|
+
});
|
|
596
|
+
} catch (_error) {
|
|
597
|
+
const error =
|
|
598
|
+
_error instanceof Error ? _error : new Error(String(_error));
|
|
599
|
+
send({
|
|
600
|
+
type: 'fatal',
|
|
601
|
+
payload: {
|
|
602
|
+
message: error.message,
|
|
603
|
+
stack: error.stack,
|
|
604
|
+
},
|
|
605
|
+
});
|
|
606
|
+
window.__RSTEST_DONE__ = true;
|
|
607
|
+
return;
|
|
608
|
+
} finally {
|
|
609
|
+
// Restore original console methods
|
|
610
|
+
restoreConsole();
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
send({ type: 'complete' });
|
|
615
|
+
window.__RSTEST_DONE__ = true;
|
|
616
|
+
};
|
|
617
|
+
|
|
618
|
+
void run().catch((error) => {
|
|
619
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
620
|
+
send({
|
|
621
|
+
type: 'fatal',
|
|
622
|
+
payload: {
|
|
623
|
+
message: err.message,
|
|
624
|
+
stack: err.stack,
|
|
625
|
+
},
|
|
626
|
+
});
|
|
627
|
+
window.__RSTEST_DONE__ = true;
|
|
628
|
+
});
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
export type FakeTimerInstallOpts = Record<string, unknown>;
|
|
2
|
+
|
|
3
|
+
export type FakeTimerWithContext = {
|
|
4
|
+
timers: Record<string, unknown>;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export type InstalledClock = {
|
|
8
|
+
now: number;
|
|
9
|
+
reset: () => void;
|
|
10
|
+
uninstall: () => void;
|
|
11
|
+
runAll: () => void;
|
|
12
|
+
runAllAsync: () => Promise<void>;
|
|
13
|
+
runToLast: () => void;
|
|
14
|
+
runToLastAsync: () => Promise<void>;
|
|
15
|
+
tick: (ms: number) => void;
|
|
16
|
+
tickAsync: (ms: number) => Promise<void>;
|
|
17
|
+
next: () => void;
|
|
18
|
+
nextAsync: () => Promise<void>;
|
|
19
|
+
runToFrame: () => void;
|
|
20
|
+
runMicrotasks: () => void;
|
|
21
|
+
setSystemTime: (now?: number | Date) => void;
|
|
22
|
+
countTimers: () => number;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const createClock = (): InstalledClock => {
|
|
26
|
+
const clock: InstalledClock = {
|
|
27
|
+
now: Date.now(),
|
|
28
|
+
reset: () => {
|
|
29
|
+
clock.now = Date.now();
|
|
30
|
+
},
|
|
31
|
+
uninstall: () => {
|
|
32
|
+
/* noop */
|
|
33
|
+
},
|
|
34
|
+
runAll: () => {
|
|
35
|
+
/* noop */
|
|
36
|
+
},
|
|
37
|
+
runAllAsync: async () => {
|
|
38
|
+
/* noop */
|
|
39
|
+
},
|
|
40
|
+
runToLast: () => {
|
|
41
|
+
/* noop */
|
|
42
|
+
},
|
|
43
|
+
runToLastAsync: async () => {
|
|
44
|
+
/* noop */
|
|
45
|
+
},
|
|
46
|
+
tick: (ms: number) => {
|
|
47
|
+
clock.now += ms;
|
|
48
|
+
},
|
|
49
|
+
tickAsync: async (ms: number) => {
|
|
50
|
+
clock.now += ms;
|
|
51
|
+
},
|
|
52
|
+
next: () => {
|
|
53
|
+
/* noop */
|
|
54
|
+
},
|
|
55
|
+
nextAsync: async () => {
|
|
56
|
+
/* noop */
|
|
57
|
+
},
|
|
58
|
+
runToFrame: () => {
|
|
59
|
+
/* noop */
|
|
60
|
+
},
|
|
61
|
+
runMicrotasks: () => {
|
|
62
|
+
/* noop */
|
|
63
|
+
},
|
|
64
|
+
setSystemTime: (value?: number | Date) => {
|
|
65
|
+
if (typeof value === 'number') {
|
|
66
|
+
clock.now = value;
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
if (value instanceof Date) {
|
|
70
|
+
clock.now = value.valueOf();
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
clock.now = Date.now();
|
|
74
|
+
},
|
|
75
|
+
countTimers: () => 0,
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
return clock;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
export const withGlobal = (_global: typeof globalThis) => {
|
|
82
|
+
const clock = createClock();
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
timers: {},
|
|
86
|
+
install: (_config: FakeTimerInstallOpts = {}): InstalledClock => {
|
|
87
|
+
clock.now = Date.now();
|
|
88
|
+
return clock;
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
};
|