@rstest/browser 0.11.6 → 0.11.7
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/index.43daaf6c93.css +1 -0
- package/dist/browser-container/container-static/js/260.3739fe815c.js +194 -0
- package/dist/browser-container/container-static/js/index.c0f87bfb3e.js +1 -0
- package/dist/browser-container/index.html +1 -1
- package/dist/browserRsbuild.d.ts +24 -4
- package/dist/browserV8Coverage.d.ts +15 -0
- package/dist/client/240~1.js +150 -0
- package/dist/{augmentExpect.d.ts → client/client/augmentExpect.d.ts} +1 -1
- package/dist/client/{browserRpc.d.ts → client/browserRpc.d.ts} +1 -1
- package/dist/client/{dispatchTransport.d.ts → client/dispatchTransport.d.ts} +8 -4
- package/dist/client/client/index.d.ts +2 -0
- package/dist/client/client/runIdentity.d.ts +12 -0
- package/dist/client/client/runner.d.ts +4 -0
- package/dist/client/client/snapshot.d.ts +27 -0
- package/dist/client/client/sourceMapSupport.d.ts +36 -0
- package/dist/{browser.js → client/index.js} +3 -102
- package/dist/client/protocol.d.ts +225 -0
- package/dist/client/rpcProtocol.d.ts +138 -0
- package/dist/client/runner.js +675 -0
- package/dist/client/sourceMap/sourceMapLoader.d.ts +25 -0
- package/dist/containerRpc.d.ts +27 -19
- package/dist/headedRunRegistry.d.ts +51 -0
- package/dist/headedScheduler.d.ts +10 -10
- package/dist/headlessScheduler.d.ts +5 -4
- package/dist/headlessTransport.d.ts +4 -2
- package/dist/hostPayloads.d.ts +1 -7
- package/dist/index.js +840 -330
- package/dist/protocol.d.ts +48 -8
- package/dist/providers/index.d.ts +21 -0
- package/dist/providers/playwright/v8Coverage.d.ts +2 -0
- package/dist/rpcProtocol.d.ts +1 -2
- package/dist/schedulerSeam.d.ts +5 -0
- package/dist/sourceMap/sourceMapLoader.d.ts +12 -1
- package/dist/watchRerunPlanner.d.ts +20 -9
- package/dist/watchSignals.d.ts +8 -1
- package/package.json +10 -10
- package/dist/33.js +0 -7
- package/dist/browser-container/container-static/css/index.0521ff95b1.css +0 -1
- package/dist/browser-container/container-static/js/683.4f821ce8f1.js +0 -194
- package/dist/browser-container/container-static/js/index.568aa7cb35.js +0 -1
- package/dist/browser.d.ts +0 -2
- package/src/augmentExpect.ts +0 -62
- package/src/browser.ts +0 -3
- package/src/browserExecutor.ts +0 -222
- package/src/browserRpcRegistry.ts +0 -57
- package/src/browserRsbuild.ts +0 -1927
- package/src/client/api.ts +0 -213
- package/src/client/browserRpc.ts +0 -86
- package/src/client/dispatchTransport.ts +0 -240
- package/src/client/entry.ts +0 -807
- package/src/client/locator.ts +0 -448
- package/src/client/snapshot.ts +0 -113
- package/src/client/sourceMapSupport.ts +0 -159
- package/src/concurrency.ts +0 -65
- package/src/configValidation.ts +0 -263
- package/src/containerRpc.ts +0 -206
- package/src/dispatchCapabilities.ts +0 -177
- package/src/dispatchRouter.ts +0 -82
- package/src/env.d.ts +0 -54
- package/src/headedScheduler.ts +0 -664
- package/src/headedSerialTaskQueue.ts +0 -23
- package/src/headlessScheduler.ts +0 -566
- package/src/headlessTransport.ts +0 -28
- package/src/hostController.ts +0 -1365
- package/src/hostPayloads.ts +0 -62
- package/src/index.ts +0 -51
- package/src/manifest.d.ts +0 -41
- package/src/protocol.ts +0 -225
- package/src/providers/index.ts +0 -126
- package/src/providers/playwright/compileLocator.ts +0 -130
- package/src/providers/playwright/dispatchBrowserRpc.ts +0 -372
- package/src/providers/playwright/expectUtils.ts +0 -57
- package/src/providers/playwright/implementation.ts +0 -35
- package/src/providers/playwright/index.ts +0 -1
- package/src/providers/playwright/runtime.ts +0 -159
- package/src/providers/playwright/textMatcher.ts +0 -10
- package/src/rpcProtocol.ts +0 -218
- package/src/runSession.ts +0 -110
- package/src/schedulerSeam.ts +0 -49
- package/src/sessionRegistry.ts +0 -89
- package/src/sourceMap/sourceMapLoader.ts +0 -96
- package/src/viewportPresets.ts +0 -69
- package/src/watchRerunPlanner.ts +0 -88
- package/src/watchRuntime.ts +0 -83
- package/src/watchSignals.ts +0 -93
- /package/dist/{rslib-runtime.js → 612~0.js} +0 -0
- /package/dist/client/{api.d.ts → client/api.d.ts} +0 -0
- /package/dist/client/{locator.d.ts → client/locator.d.ts} +0 -0
package/src/headlessScheduler.ts
DELETED
|
@@ -1,566 +0,0 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
RstestContext,
|
|
3
|
-
TestFileResult,
|
|
4
|
-
} from '@rstest/core/internal/browser';
|
|
5
|
-
import { color, logger } from '@rstest/core/internal/browser';
|
|
6
|
-
import { normalize } from 'pathe';
|
|
7
|
-
import {
|
|
8
|
-
type BrowserRuntime,
|
|
9
|
-
drainPendingAffectedTestFiles,
|
|
10
|
-
mapViewportByProject,
|
|
11
|
-
serializeForInlineScript,
|
|
12
|
-
} from './browserRsbuild';
|
|
13
|
-
import { getHeadlessConcurrency } from './concurrency';
|
|
14
|
-
import type { HostDispatchRouterOptions } from './dispatchCapabilities';
|
|
15
|
-
import type { HostDispatchRouter } from './dispatchRouter';
|
|
16
|
-
import { attachHeadlessRunnerTransport } from './headlessTransport';
|
|
17
|
-
import {
|
|
18
|
-
createDeferredPromise,
|
|
19
|
-
getFileTaskId,
|
|
20
|
-
type FatalPayload,
|
|
21
|
-
toError,
|
|
22
|
-
} from './hostPayloads';
|
|
23
|
-
import type {
|
|
24
|
-
BrowserClientMessage,
|
|
25
|
-
BrowserHostConfig,
|
|
26
|
-
BrowserProjectRuntime,
|
|
27
|
-
TestFileInfo,
|
|
28
|
-
} from './protocol';
|
|
29
|
-
import { DISPATCH_NAMESPACE_RUNNER } from './protocol';
|
|
30
|
-
import type {
|
|
31
|
-
BrowserProviderBrowser,
|
|
32
|
-
BrowserProviderContext,
|
|
33
|
-
BrowserProviderPage,
|
|
34
|
-
} from './providers';
|
|
35
|
-
import {
|
|
36
|
-
createRunSession,
|
|
37
|
-
type RunSession,
|
|
38
|
-
RunSessionLifecycle,
|
|
39
|
-
} from './runSession';
|
|
40
|
-
import { RunnerSessionRegistry } from './sessionRegistry';
|
|
41
|
-
import { collectDeletedTestPaths, planWatchRerun } from './watchRerunPlanner';
|
|
42
|
-
import type {
|
|
43
|
-
BrowserWatchSession,
|
|
44
|
-
DispatchPageResolver,
|
|
45
|
-
SchedulerRunResult,
|
|
46
|
-
} from './schedulerSeam';
|
|
47
|
-
import type { WatchSignals } from './watchSignals';
|
|
48
|
-
|
|
49
|
-
type HeadlessSchedulerContext = Pick<
|
|
50
|
-
RstestContext,
|
|
51
|
-
'command' | 'snapshotManager' | 'stateManager' | 'updateReporterResultState'
|
|
52
|
-
> & {
|
|
53
|
-
normalizedConfig: Pick<RstestContext['normalizedConfig'], 'bail' | 'pool'>;
|
|
54
|
-
};
|
|
55
|
-
|
|
56
|
-
type HeadlessSchedulerDeps = {
|
|
57
|
-
context: HeadlessSchedulerContext;
|
|
58
|
-
browser: BrowserProviderBrowser;
|
|
59
|
-
browserLaunchOptions: BrowserRuntime['browserLaunchOptions'];
|
|
60
|
-
projectServers: BrowserRuntime['projectServers'];
|
|
61
|
-
allTestFiles: TestFileInfo[];
|
|
62
|
-
projectRuntimeConfigs: BrowserProjectRuntime[];
|
|
63
|
-
hostOptions: BrowserHostConfig;
|
|
64
|
-
watchState: BrowserRuntime['watchState'];
|
|
65
|
-
isWatchMode: boolean;
|
|
66
|
-
createDispatchRouter: (
|
|
67
|
-
options?: HostDispatchRouterOptions,
|
|
68
|
-
) => HostDispatchRouter;
|
|
69
|
-
handlers: {
|
|
70
|
-
handleFatal: (payload: FatalPayload) => Promise<void>;
|
|
71
|
-
handleTestFileComplete: (payload: TestFileResult) => Promise<void>;
|
|
72
|
-
};
|
|
73
|
-
watchSignals: Pick<
|
|
74
|
-
WatchSignals,
|
|
75
|
-
'setDispatchRerun' | 'setInterrupt' | 'signalInvalidation'
|
|
76
|
-
>;
|
|
77
|
-
setDispatchPageResolver: (resolver: DispatchPageResolver) => void;
|
|
78
|
-
createWatchSession: (
|
|
79
|
-
execute: (testPaths: string[]) => Promise<void>,
|
|
80
|
-
) => BrowserWatchSession;
|
|
81
|
-
collectProjectEntries: () => Promise<
|
|
82
|
-
Parameters<typeof planWatchRerun>[0]['projectEntries']
|
|
83
|
-
>;
|
|
84
|
-
logWatchReady: () => void;
|
|
85
|
-
destroyRuntime: () => Promise<void>;
|
|
86
|
-
};
|
|
87
|
-
|
|
88
|
-
export const createHeadlessScheduler = async ({
|
|
89
|
-
context,
|
|
90
|
-
browser,
|
|
91
|
-
browserLaunchOptions,
|
|
92
|
-
projectServers,
|
|
93
|
-
allTestFiles,
|
|
94
|
-
projectRuntimeConfigs,
|
|
95
|
-
hostOptions,
|
|
96
|
-
watchState,
|
|
97
|
-
isWatchMode,
|
|
98
|
-
createDispatchRouter,
|
|
99
|
-
handlers: { handleFatal, handleTestFileComplete },
|
|
100
|
-
watchSignals,
|
|
101
|
-
setDispatchPageResolver,
|
|
102
|
-
createWatchSession,
|
|
103
|
-
collectProjectEntries,
|
|
104
|
-
logWatchReady,
|
|
105
|
-
destroyRuntime,
|
|
106
|
-
}: HeadlessSchedulerDeps): Promise<SchedulerRunResult> => {
|
|
107
|
-
// Session-based scheduling path: lifecycle + session index + dispatch routing.
|
|
108
|
-
type ActiveHeadlessRun = RunSession & {
|
|
109
|
-
contexts: Set<BrowserProviderContext>;
|
|
110
|
-
};
|
|
111
|
-
|
|
112
|
-
const viewportByProject = mapViewportByProject(projectRuntimeConfigs);
|
|
113
|
-
const runLifecycle = new RunSessionLifecycle<ActiveHeadlessRun>();
|
|
114
|
-
const sessionRegistry = new RunnerSessionRegistry();
|
|
115
|
-
setDispatchPageResolver((target) => ({
|
|
116
|
-
runnerPage: target?.sessionId
|
|
117
|
-
? sessionRegistry.getById(target.sessionId)?.page
|
|
118
|
-
: undefined,
|
|
119
|
-
}));
|
|
120
|
-
let dispatchRequestCounter = 0;
|
|
121
|
-
|
|
122
|
-
const nextDispatchRequestId = (namespace: string): string => {
|
|
123
|
-
return `${namespace}-${++dispatchRequestCounter}`;
|
|
124
|
-
};
|
|
125
|
-
|
|
126
|
-
const closeContextSafely = async (
|
|
127
|
-
browserContext: BrowserProviderContext,
|
|
128
|
-
): Promise<void> => {
|
|
129
|
-
try {
|
|
130
|
-
await browserContext.close();
|
|
131
|
-
} catch {
|
|
132
|
-
// ignore
|
|
133
|
-
}
|
|
134
|
-
};
|
|
135
|
-
|
|
136
|
-
const cancelRun = async (
|
|
137
|
-
run: ActiveHeadlessRun,
|
|
138
|
-
waitForDone = true,
|
|
139
|
-
): Promise<void> => {
|
|
140
|
-
await runLifecycle.cancel(run, {
|
|
141
|
-
waitForDone,
|
|
142
|
-
onCancel: async (session) => {
|
|
143
|
-
await Promise.all(
|
|
144
|
-
Array.from(session.contexts).map((browserContext) =>
|
|
145
|
-
closeContextSafely(browserContext),
|
|
146
|
-
),
|
|
147
|
-
);
|
|
148
|
-
},
|
|
149
|
-
});
|
|
150
|
-
};
|
|
151
|
-
|
|
152
|
-
const dispatchRouter = createDispatchRouter({
|
|
153
|
-
isRunTokenStale: (runToken) => runLifecycle.isTokenStale(runToken),
|
|
154
|
-
onStale: (request) => {
|
|
155
|
-
if (request.namespace === DISPATCH_NAMESPACE_RUNNER) {
|
|
156
|
-
logger.debug(
|
|
157
|
-
`[Headless] Dropped stale message "${request.method}" for ${request.target?.testFile ?? 'unknown'}`,
|
|
158
|
-
);
|
|
159
|
-
}
|
|
160
|
-
},
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
const dispatchRunnerMessage = async (
|
|
164
|
-
run: ActiveHeadlessRun,
|
|
165
|
-
file: TestFileInfo,
|
|
166
|
-
sessionId: string,
|
|
167
|
-
message: BrowserClientMessage,
|
|
168
|
-
): Promise<void> => {
|
|
169
|
-
const response = await dispatchRouter.dispatch({
|
|
170
|
-
requestId: nextDispatchRequestId(DISPATCH_NAMESPACE_RUNNER),
|
|
171
|
-
runToken: run.token,
|
|
172
|
-
namespace: DISPATCH_NAMESPACE_RUNNER,
|
|
173
|
-
method: message.type,
|
|
174
|
-
args: 'payload' in message ? message.payload : undefined,
|
|
175
|
-
target: {
|
|
176
|
-
sessionId,
|
|
177
|
-
testFile: file.testPath,
|
|
178
|
-
projectName: file.projectName,
|
|
179
|
-
},
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
if (response.stale) {
|
|
183
|
-
return;
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
if (response.error) {
|
|
187
|
-
throw new Error(response.error);
|
|
188
|
-
}
|
|
189
|
-
};
|
|
190
|
-
|
|
191
|
-
const runSingleFile = async (
|
|
192
|
-
run: ActiveHeadlessRun,
|
|
193
|
-
file: TestFileInfo,
|
|
194
|
-
): Promise<void> => {
|
|
195
|
-
if (run.cancelled || runLifecycle.isTokenStale(run.token)) {
|
|
196
|
-
return;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
const viewport = viewportByProject.get(file.projectName);
|
|
200
|
-
const browserContext = await browser.newContext({
|
|
201
|
-
providerOptions: browserLaunchOptions.providerOptions,
|
|
202
|
-
viewport: viewport ?? null,
|
|
203
|
-
});
|
|
204
|
-
run.contexts.add(browserContext);
|
|
205
|
-
|
|
206
|
-
let page: BrowserProviderPage | null = null;
|
|
207
|
-
let sessionId: string | null = null;
|
|
208
|
-
let settled = false;
|
|
209
|
-
let resolveDone: (() => void) | null = null;
|
|
210
|
-
|
|
211
|
-
const markDone = (): void => {
|
|
212
|
-
if (!settled) {
|
|
213
|
-
settled = true;
|
|
214
|
-
resolveDone?.();
|
|
215
|
-
}
|
|
216
|
-
};
|
|
217
|
-
|
|
218
|
-
const donePromise = new Promise<void>((resolve) => {
|
|
219
|
-
resolveDone = resolve;
|
|
220
|
-
});
|
|
221
|
-
|
|
222
|
-
// Event-driven death detection (vitest-style): a renderer crash or an
|
|
223
|
-
// unexpected page close produces no further messages, so fail the file at
|
|
224
|
-
// once. Per-test/hook timeouts are enforced inside the runner, so the host
|
|
225
|
-
// deliberately keeps no execution-duration watchdog. Our own teardown
|
|
226
|
-
// close is ignored because `settled`/`run.cancelled` are set by then.
|
|
227
|
-
const crashDeferred = createDeferredPromise<string>();
|
|
228
|
-
const onPageDead = (reason: string): void => {
|
|
229
|
-
if (settled || run.cancelled || !runLifecycle.isTokenActive(run.token)) {
|
|
230
|
-
return;
|
|
231
|
-
}
|
|
232
|
-
settled = true;
|
|
233
|
-
crashDeferred.resolve(reason);
|
|
234
|
-
};
|
|
235
|
-
|
|
236
|
-
try {
|
|
237
|
-
page = await browserContext.newPage();
|
|
238
|
-
page.on('crash', () =>
|
|
239
|
-
onPageDead(`Browser page crashed while running ${file.testPath}.`),
|
|
240
|
-
);
|
|
241
|
-
page.on('close', () =>
|
|
242
|
-
onPageDead(
|
|
243
|
-
`Browser page closed unexpectedly while running ${file.testPath}.`,
|
|
244
|
-
),
|
|
245
|
-
);
|
|
246
|
-
|
|
247
|
-
const session = sessionRegistry.register({
|
|
248
|
-
testFile: file.testPath,
|
|
249
|
-
projectName: file.projectName,
|
|
250
|
-
runToken: run.token,
|
|
251
|
-
mode: 'headless-page',
|
|
252
|
-
context: browserContext,
|
|
253
|
-
page,
|
|
254
|
-
});
|
|
255
|
-
sessionId = session.id;
|
|
256
|
-
|
|
257
|
-
await attachHeadlessRunnerTransport(page, {
|
|
258
|
-
onDispatchMessage: async (message) => {
|
|
259
|
-
try {
|
|
260
|
-
await dispatchRunnerMessage(run, file, session.id, message);
|
|
261
|
-
if (
|
|
262
|
-
message.type === 'file-complete' ||
|
|
263
|
-
message.type === 'complete'
|
|
264
|
-
) {
|
|
265
|
-
markDone();
|
|
266
|
-
} else if (message.type === 'fatal') {
|
|
267
|
-
markDone();
|
|
268
|
-
await cancelRun(run, false);
|
|
269
|
-
}
|
|
270
|
-
} catch (error) {
|
|
271
|
-
const formatted = toError(error);
|
|
272
|
-
await handleFatal({
|
|
273
|
-
message: formatted.message,
|
|
274
|
-
stack: formatted.stack,
|
|
275
|
-
});
|
|
276
|
-
markDone();
|
|
277
|
-
await cancelRun(run, false);
|
|
278
|
-
}
|
|
279
|
-
},
|
|
280
|
-
onDispatchRpc: async (request) => {
|
|
281
|
-
return dispatchRouter.dispatch({
|
|
282
|
-
...request,
|
|
283
|
-
runToken: run.token,
|
|
284
|
-
target: {
|
|
285
|
-
sessionId: session.id,
|
|
286
|
-
testFile: file.testPath,
|
|
287
|
-
projectName: file.projectName,
|
|
288
|
-
...request.target,
|
|
289
|
-
},
|
|
290
|
-
});
|
|
291
|
-
},
|
|
292
|
-
});
|
|
293
|
-
|
|
294
|
-
const inlineOptions: BrowserHostConfig = {
|
|
295
|
-
...hostOptions,
|
|
296
|
-
// Read live per page load, not from the construction-time
|
|
297
|
-
// `hostOptions` value: the 'u' shortcut flips
|
|
298
|
-
// `snapshotManager.options` between reruns.
|
|
299
|
-
snapshot: {
|
|
300
|
-
updateSnapshot: context.snapshotManager.options.updateSnapshot,
|
|
301
|
-
},
|
|
302
|
-
testFile: file.testPath,
|
|
303
|
-
runId: `${run.token}:${session.id}`,
|
|
304
|
-
};
|
|
305
|
-
const serializedOptions = serializeForInlineScript(inlineOptions);
|
|
306
|
-
await page.addInitScript(
|
|
307
|
-
`window.__RSTEST_BROWSER_OPTIONS__ = ${serializedOptions};`,
|
|
308
|
-
);
|
|
309
|
-
|
|
310
|
-
const projectServer = projectServers.get(file.projectName);
|
|
311
|
-
if (!projectServer) {
|
|
312
|
-
throw new Error(
|
|
313
|
-
`No browser dev server for project "${file.projectName}" (test file: ${file.testPath}).`,
|
|
314
|
-
);
|
|
315
|
-
}
|
|
316
|
-
await page.goto(`http://localhost:${projectServer.port}/runner.html`, {
|
|
317
|
-
waitUntil: 'load',
|
|
318
|
-
});
|
|
319
|
-
|
|
320
|
-
const state = await Promise.race([
|
|
321
|
-
donePromise.then(() => ({ type: 'done' as const })),
|
|
322
|
-
crashDeferred.promise.then((reason) => ({
|
|
323
|
-
type: 'crash' as const,
|
|
324
|
-
reason,
|
|
325
|
-
})),
|
|
326
|
-
run.cancelSignal.then(() => ({ type: 'cancelled' as const })),
|
|
327
|
-
]);
|
|
328
|
-
|
|
329
|
-
if (state.type === 'cancelled') {
|
|
330
|
-
return;
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
if (
|
|
334
|
-
state.type === 'crash' &&
|
|
335
|
-
runLifecycle.isTokenActive(run.token) &&
|
|
336
|
-
!run.cancelled
|
|
337
|
-
) {
|
|
338
|
-
await handleFatal({ message: state.reason });
|
|
339
|
-
await cancelRun(run, false);
|
|
340
|
-
}
|
|
341
|
-
} catch (error) {
|
|
342
|
-
if (runLifecycle.isTokenActive(run.token) && !run.cancelled) {
|
|
343
|
-
const formatted = toError(error);
|
|
344
|
-
await handleFatal({
|
|
345
|
-
message: formatted.message,
|
|
346
|
-
stack: formatted.stack,
|
|
347
|
-
});
|
|
348
|
-
await cancelRun(run, false);
|
|
349
|
-
}
|
|
350
|
-
} finally {
|
|
351
|
-
// A superseded run can hold a renderer that will never answer again:
|
|
352
|
-
// its test file may have been deleted mid-flight, leaving the page
|
|
353
|
-
// waiting on a chunk the bundler will never produce, and closing such a
|
|
354
|
-
// page blocks for as long as the renderer stays wedged. The cycle waits
|
|
355
|
-
// on this teardown, so for an abandoned run it is detached — its
|
|
356
|
-
// results are already discarded, and the replacement cycle must not be
|
|
357
|
-
// held up by a page nobody is reading. A run that ends normally closes
|
|
358
|
-
// in band, which is what keeps the open-context count at the
|
|
359
|
-
// concurrency limit.
|
|
360
|
-
const abandoned = run.cancelled || runLifecycle.isTokenStale(run.token);
|
|
361
|
-
const teardown = async (): Promise<void> => {
|
|
362
|
-
if (page) {
|
|
363
|
-
try {
|
|
364
|
-
await page.close();
|
|
365
|
-
} catch {
|
|
366
|
-
// ignore
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
await closeContextSafely(browserContext);
|
|
370
|
-
};
|
|
371
|
-
if (sessionId) {
|
|
372
|
-
sessionRegistry.deleteById(sessionId);
|
|
373
|
-
}
|
|
374
|
-
run.contexts.delete(browserContext);
|
|
375
|
-
if (abandoned) {
|
|
376
|
-
void teardown();
|
|
377
|
-
} else {
|
|
378
|
-
await teardown();
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
};
|
|
382
|
-
|
|
383
|
-
// Bailed files never run, so they carry no case results — mirror the node
|
|
384
|
-
// pool's skip result (`runInPool.ts`) so the summary reports them as skipped
|
|
385
|
-
// rather than dropping them silently.
|
|
386
|
-
const makeSkippedFileResult = (file: TestFileInfo): TestFileResult => ({
|
|
387
|
-
testId: getFileTaskId(file.testPath),
|
|
388
|
-
status: 'skip',
|
|
389
|
-
name: '',
|
|
390
|
-
testPath: file.testPath,
|
|
391
|
-
project: file.projectName,
|
|
392
|
-
results: [],
|
|
393
|
-
});
|
|
394
|
-
|
|
395
|
-
const runFilesWithPool = async (files: TestFileInfo[]): Promise<void> => {
|
|
396
|
-
if (files.length === 0) {
|
|
397
|
-
return;
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
const previous = runLifecycle.activeSession;
|
|
401
|
-
if (previous) {
|
|
402
|
-
await cancelRun(previous);
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
const run = runLifecycle.createSession((token) => ({
|
|
406
|
-
...createRunSession(token),
|
|
407
|
-
contexts: new Set<BrowserProviderContext>(),
|
|
408
|
-
}));
|
|
409
|
-
|
|
410
|
-
const queue = [...files];
|
|
411
|
-
const concurrency = getHeadlessConcurrency(context, queue.length);
|
|
412
|
-
const bail = context.normalizedConfig.bail;
|
|
413
|
-
|
|
414
|
-
const worker = async (): Promise<void> => {
|
|
415
|
-
while (
|
|
416
|
-
queue.length > 0 &&
|
|
417
|
-
!run.cancelled &&
|
|
418
|
-
runLifecycle.isTokenActive(run.token)
|
|
419
|
-
) {
|
|
420
|
-
// Cross-file bail gate (parity with the node pool's pickup-time skip
|
|
421
|
-
// at `runInPool.ts`): once the cycle-wide failed count reaches `bail`,
|
|
422
|
-
// drain the remaining files as skipped instead of running them. The
|
|
423
|
-
// count is cycle-scoped because core clears `stateManager` ahead of
|
|
424
|
-
// every cycle, a watch session's first one included — so a mixed
|
|
425
|
-
// launch cannot drain this queue on the node initial cycle's
|
|
426
|
-
// failures.
|
|
427
|
-
if (bail && context.stateManager.getCountOfFailedTests() >= bail) {
|
|
428
|
-
let skipped = queue.shift();
|
|
429
|
-
while (skipped) {
|
|
430
|
-
await handleTestFileComplete(makeSkippedFileResult(skipped));
|
|
431
|
-
skipped = queue.shift();
|
|
432
|
-
}
|
|
433
|
-
return;
|
|
434
|
-
}
|
|
435
|
-
const next = queue.shift();
|
|
436
|
-
if (!next) {
|
|
437
|
-
return;
|
|
438
|
-
}
|
|
439
|
-
await runSingleFile(run, next);
|
|
440
|
-
}
|
|
441
|
-
};
|
|
442
|
-
|
|
443
|
-
run.done = Promise.all(
|
|
444
|
-
Array.from(
|
|
445
|
-
{ length: Math.min(queue.length, Math.max(concurrency, 1)) },
|
|
446
|
-
() => worker(),
|
|
447
|
-
),
|
|
448
|
-
).then(() => {});
|
|
449
|
-
|
|
450
|
-
await run.done;
|
|
451
|
-
runLifecycle.clearIfActive(run);
|
|
452
|
-
};
|
|
453
|
-
|
|
454
|
-
const testStart = Date.now();
|
|
455
|
-
await runFilesWithPool(allTestFiles);
|
|
456
|
-
const testTime = Date.now() - testStart;
|
|
457
|
-
|
|
458
|
-
let watchSession: BrowserWatchSession | undefined;
|
|
459
|
-
if (isWatchMode) {
|
|
460
|
-
// A queued scope can go stale before its cycle is dequeued — a later
|
|
461
|
-
// trigger may have rebuilt the file set without one of these files — so a
|
|
462
|
-
// path that no longer resolves is skipped rather than failing the cycle
|
|
463
|
-
// beside its still-valid siblings.
|
|
464
|
-
const runScope = async (testPaths: string[]): Promise<void> => {
|
|
465
|
-
const pathSet = new Set(testPaths.map((testPath) => normalize(testPath)));
|
|
466
|
-
await runFilesWithPool(
|
|
467
|
-
watchState.lastTestFiles.filter((file) => pathSet.has(file.testPath)),
|
|
468
|
-
);
|
|
469
|
-
};
|
|
470
|
-
|
|
471
|
-
// Cutting the in-flight run short lets its cycle finalize with what it had
|
|
472
|
-
// and the queued replacement start immediately; invalidating the token
|
|
473
|
-
// first makes every late dispatch from it a no-op. Deliberately not
|
|
474
|
-
// awaiting `run.done` — the cancelled run's own cycle is what awaits it.
|
|
475
|
-
//
|
|
476
|
-
// Unlike every other cancel this one does not tear the run's browser
|
|
477
|
-
// contexts down, because a rebuild trigger reaches it from inside the
|
|
478
|
-
// bundler's dev-compile hook: a page still fetching from the dev server
|
|
479
|
-
// that same hook is holding up cannot be closed, and the run it belongs
|
|
480
|
-
// to then never ends. Signalling the cancel is enough — the run's own
|
|
481
|
-
// teardown closes page and context as soon as it unwinds, and every page
|
|
482
|
-
// operation it can be sitting in is bounded by the driver's own timeout.
|
|
483
|
-
watchSignals.setInterrupt(async () => {
|
|
484
|
-
const active = runLifecycle.activeSession;
|
|
485
|
-
if (!active || active.cancelled) {
|
|
486
|
-
return;
|
|
487
|
-
}
|
|
488
|
-
runLifecycle.invalidateActiveToken();
|
|
489
|
-
await runLifecycle.cancel(active, { waitForDone: false });
|
|
490
|
-
});
|
|
491
|
-
|
|
492
|
-
watchSignals.setDispatchRerun(async () => {
|
|
493
|
-
const newProjectEntries = await collectProjectEntries();
|
|
494
|
-
const rerunPlan = planWatchRerun({
|
|
495
|
-
projectEntries: newProjectEntries,
|
|
496
|
-
previousTestFiles: watchState.lastTestFiles,
|
|
497
|
-
affectedTestFiles: drainPendingAffectedTestFiles(watchState),
|
|
498
|
-
});
|
|
499
|
-
|
|
500
|
-
if (rerunPlan.filesChanged) {
|
|
501
|
-
const deletedTestPaths = collectDeletedTestPaths(
|
|
502
|
-
watchState.lastTestFiles,
|
|
503
|
-
rerunPlan.currentTestFiles,
|
|
504
|
-
);
|
|
505
|
-
if (deletedTestPaths.length > 0) {
|
|
506
|
-
context.updateReporterResultState([], [], deletedTestPaths);
|
|
507
|
-
}
|
|
508
|
-
watchState.lastTestFiles = rerunPlan.currentTestFiles;
|
|
509
|
-
if (rerunPlan.currentTestFiles.length === 0) {
|
|
510
|
-
logger.log(
|
|
511
|
-
color.cyan('No browser test files remain after update.\n'),
|
|
512
|
-
);
|
|
513
|
-
// Still one cycle: core's finalize reports the emptied run.
|
|
514
|
-
await watchSignals.signalInvalidation([]);
|
|
515
|
-
return;
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
logger.log(
|
|
519
|
-
color.cyan(
|
|
520
|
-
`Test file set changed, re-running ${rerunPlan.currentTestFiles.length} file(s)...\n`,
|
|
521
|
-
),
|
|
522
|
-
);
|
|
523
|
-
await watchSignals.signalInvalidation([
|
|
524
|
-
...new Set(rerunPlan.currentTestFiles.map((file) => file.testPath)),
|
|
525
|
-
]);
|
|
526
|
-
return;
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
if (rerunPlan.affectedTestFiles.length === 0) {
|
|
530
|
-
logger.log(
|
|
531
|
-
color.cyan(
|
|
532
|
-
'No affected browser test files detected, skipping re-run.\n',
|
|
533
|
-
),
|
|
534
|
-
);
|
|
535
|
-
logWatchReady();
|
|
536
|
-
return;
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
logger.log(
|
|
540
|
-
color.cyan(
|
|
541
|
-
`Re-running ${rerunPlan.affectedTestFiles.length} affected test file(s)...\n`,
|
|
542
|
-
),
|
|
543
|
-
);
|
|
544
|
-
await watchSignals.signalInvalidation([
|
|
545
|
-
...new Set(rerunPlan.affectedTestFiles.map((file) => file.testPath)),
|
|
546
|
-
]);
|
|
547
|
-
});
|
|
548
|
-
|
|
549
|
-
watchSession = createWatchSession(runScope);
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
const closeHeadlessRuntime = !isWatchMode
|
|
553
|
-
? async () => {
|
|
554
|
-
sessionRegistry.clear();
|
|
555
|
-
await destroyRuntime();
|
|
556
|
-
}
|
|
557
|
-
: undefined;
|
|
558
|
-
|
|
559
|
-
return {
|
|
560
|
-
testTime,
|
|
561
|
-
watchSession,
|
|
562
|
-
// `closeHeadlessRuntime` is already `undefined` in watch mode: the watch
|
|
563
|
-
// runtime outlives the cycle and is torn down through `executor.close()`.
|
|
564
|
-
close: closeHeadlessRuntime,
|
|
565
|
-
};
|
|
566
|
-
};
|
package/src/headlessTransport.ts
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
BrowserClientMessage,
|
|
3
|
-
BrowserDispatchRequest,
|
|
4
|
-
BrowserDispatchResponse,
|
|
5
|
-
} from './protocol';
|
|
6
|
-
import { DISPATCH_MESSAGE_TYPE, DISPATCH_RPC_BRIDGE_NAME } from './protocol';
|
|
7
|
-
import type { BrowserProviderPage } from './providers';
|
|
8
|
-
|
|
9
|
-
type HeadlessRunnerTransportHandlers = {
|
|
10
|
-
onDispatchMessage: (message: BrowserClientMessage) => Promise<void>;
|
|
11
|
-
onDispatchRpc: (
|
|
12
|
-
request: BrowserDispatchRequest,
|
|
13
|
-
) => Promise<BrowserDispatchResponse>;
|
|
14
|
-
};
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Headless transport adapter.
|
|
18
|
-
* This only binds page bridge functions and delegates all scheduling decisions upstream.
|
|
19
|
-
*/
|
|
20
|
-
export const attachHeadlessRunnerTransport = async (
|
|
21
|
-
page: BrowserProviderPage,
|
|
22
|
-
handlers: HeadlessRunnerTransportHandlers,
|
|
23
|
-
): Promise<void> => {
|
|
24
|
-
// Fire-and-forget runner lifecycle messages (ready/log/result/fatal).
|
|
25
|
-
await page.exposeFunction(DISPATCH_MESSAGE_TYPE, handlers.onDispatchMessage);
|
|
26
|
-
// Request/response RPC bridge shared by snapshot and future namespaces.
|
|
27
|
-
await page.exposeFunction(DISPATCH_RPC_BRIDGE_NAME, handlers.onDispatchRpc);
|
|
28
|
-
};
|