@rstest/browser 0.11.4 → 0.11.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/browser-container/container-static/js/506.b2f920fa67.js +194 -0
- package/dist/browser-container/container-static/js/index.d27fd63f5e.js +1 -0
- package/dist/browser-container/container-static/js/lib-react.e24a1d366b.js +2 -0
- package/dist/browser-container/index.html +1 -1
- package/dist/index.js +76 -124
- package/dist/protocol.d.ts +5 -0
- package/package.json +4 -4
- package/src/client/entry.ts +3 -0
- package/src/hostController.ts +155 -195
- package/src/protocol.ts +6 -0
- package/dist/browser-container/container-static/js/243.a8eed2b9e7.js +0 -27406
- package/dist/browser-container/container-static/js/243.a8eed2b9e7.js.LICENSE.txt +0 -1
- package/dist/browser-container/container-static/js/index.84aaafaf21.js +0 -3058
- package/dist/browser-container/container-static/js/lib-react.62b27a21db.js +0 -8454
- package/dist/browser-container/container-static/js/lib-react.62b27a21db.js.LICENSE.txt +0 -1
package/src/hostController.ts
CHANGED
|
@@ -1789,6 +1789,55 @@ const createBrowserRuntime = async ({
|
|
|
1789
1789
|
}
|
|
1790
1790
|
};
|
|
1791
1791
|
|
|
1792
|
+
const serveContainerRoute = async (
|
|
1793
|
+
req: IncomingMessage,
|
|
1794
|
+
res: ServerResponse,
|
|
1795
|
+
next: () => void,
|
|
1796
|
+
): Promise<void> => {
|
|
1797
|
+
if (!req.url) {
|
|
1798
|
+
next();
|
|
1799
|
+
return;
|
|
1800
|
+
}
|
|
1801
|
+
|
|
1802
|
+
const url = new URL(req.url, 'http://localhost');
|
|
1803
|
+
if (url.pathname === '/') {
|
|
1804
|
+
if (await respondWithDevServerHtml(url, res)) {
|
|
1805
|
+
return;
|
|
1806
|
+
}
|
|
1807
|
+
|
|
1808
|
+
const html =
|
|
1809
|
+
injectedContainerHtml ||
|
|
1810
|
+
containerHtmlTemplate?.replace(OPTIONS_PLACEHOLDER, 'null');
|
|
1811
|
+
|
|
1812
|
+
if (html) {
|
|
1813
|
+
res.setHeader('Content-Type', 'text/html');
|
|
1814
|
+
res.end(html);
|
|
1815
|
+
return;
|
|
1816
|
+
}
|
|
1817
|
+
|
|
1818
|
+
res.statusCode = 502;
|
|
1819
|
+
res.end('Container UI is not available.');
|
|
1820
|
+
return;
|
|
1821
|
+
}
|
|
1822
|
+
|
|
1823
|
+
if (url.pathname.startsWith('/container-static/')) {
|
|
1824
|
+
if (await proxyDevServerAsset(req, res)) {
|
|
1825
|
+
return;
|
|
1826
|
+
}
|
|
1827
|
+
|
|
1828
|
+
if (serveContainer) {
|
|
1829
|
+
serveContainer(req, res, next);
|
|
1830
|
+
return;
|
|
1831
|
+
}
|
|
1832
|
+
|
|
1833
|
+
res.statusCode = 502;
|
|
1834
|
+
res.end('Container assets are not available.');
|
|
1835
|
+
return;
|
|
1836
|
+
}
|
|
1837
|
+
|
|
1838
|
+
next();
|
|
1839
|
+
};
|
|
1840
|
+
|
|
1792
1841
|
// ---- Build one isolated rsbuild instance + dev server per project ----
|
|
1793
1842
|
const buildProjectServer = async (
|
|
1794
1843
|
project: ProjectContext,
|
|
@@ -1846,6 +1895,13 @@ const createBrowserRuntime = async ({
|
|
|
1846
1895
|
project.normalizedConfig.browser.port ??
|
|
1847
1896
|
(isContainerServer ? 4000 : 0),
|
|
1848
1897
|
strictPort: project.normalizedConfig.browser.strictPort,
|
|
1898
|
+
// User plugins may emit index.html; register before Rsbuild's HTML
|
|
1899
|
+
// completion middleware so `/` remains owned by the Browser UI.
|
|
1900
|
+
setup: isContainerServer
|
|
1901
|
+
? ({ server }) => {
|
|
1902
|
+
server.middlewares.use(serveContainerRoute);
|
|
1903
|
+
}
|
|
1904
|
+
: undefined,
|
|
1849
1905
|
},
|
|
1850
1906
|
dev: createBrowserRsbuildDevConfig(enableHmr),
|
|
1851
1907
|
environments: {
|
|
@@ -2196,43 +2252,6 @@ const createBrowserRuntime = async ({
|
|
|
2196
2252
|
}
|
|
2197
2253
|
return;
|
|
2198
2254
|
}
|
|
2199
|
-
// Container UI HTML + static assets are served by the container origin
|
|
2200
|
-
// only. Per-project runner servers expose just /runner.html + assets.
|
|
2201
|
-
if (isContainerServer) {
|
|
2202
|
-
if (url.pathname === '/') {
|
|
2203
|
-
if (await respondWithDevServerHtml(url, res)) {
|
|
2204
|
-
return;
|
|
2205
|
-
}
|
|
2206
|
-
|
|
2207
|
-
const html =
|
|
2208
|
-
injectedContainerHtml ||
|
|
2209
|
-
containerHtmlTemplate?.replace(OPTIONS_PLACEHOLDER, 'null');
|
|
2210
|
-
|
|
2211
|
-
if (html) {
|
|
2212
|
-
res.setHeader('Content-Type', 'text/html');
|
|
2213
|
-
res.end(html);
|
|
2214
|
-
return;
|
|
2215
|
-
}
|
|
2216
|
-
|
|
2217
|
-
res.statusCode = 502;
|
|
2218
|
-
res.end('Container UI is not available.');
|
|
2219
|
-
return;
|
|
2220
|
-
}
|
|
2221
|
-
if (url.pathname.startsWith('/container-static/')) {
|
|
2222
|
-
if (await proxyDevServerAsset(req, res)) {
|
|
2223
|
-
return;
|
|
2224
|
-
}
|
|
2225
|
-
|
|
2226
|
-
if (serveContainer) {
|
|
2227
|
-
serveContainer(req, res, next);
|
|
2228
|
-
return;
|
|
2229
|
-
}
|
|
2230
|
-
|
|
2231
|
-
res.statusCode = 502;
|
|
2232
|
-
res.end('Container assets are not available.');
|
|
2233
|
-
return;
|
|
2234
|
-
}
|
|
2235
|
-
}
|
|
2236
2255
|
if (url.pathname === '/runner.html') {
|
|
2237
2256
|
res.setHeader('Content-Type', 'text/html');
|
|
2238
2257
|
res.end(htmlTemplate);
|
|
@@ -2353,7 +2372,6 @@ export const runBrowserController = async (
|
|
|
2353
2372
|
options?: BrowserTestRunOptions,
|
|
2354
2373
|
): Promise<BrowserTestRunResult | void> => {
|
|
2355
2374
|
const {
|
|
2356
|
-
allowEmptyWatchRun = false,
|
|
2357
2375
|
allowEmptyRun = false,
|
|
2358
2376
|
filesOnly = false,
|
|
2359
2377
|
onTraceEvents,
|
|
@@ -2371,10 +2389,13 @@ export const runBrowserController = async (
|
|
|
2371
2389
|
// passes `onTraceEvents`). The browser host shares one Node process across
|
|
2372
2390
|
// every test file, so each tracker is assigned a synthetic per-file pid
|
|
2373
2391
|
// (`nextBrowserFilePid`) that lets Perfetto render each file as its own
|
|
2374
|
-
// process track with the file path as the title.
|
|
2392
|
+
// process track with the file path as the title. Keyed by project + path so
|
|
2393
|
+
// concurrent projects running the same file keep separate trackers.
|
|
2375
2394
|
const phaseTrackers = onTraceEvents
|
|
2376
2395
|
? new Map<string, PhaseTracker>()
|
|
2377
2396
|
: undefined;
|
|
2397
|
+
const trackerKey = (project: string, testPath: string) =>
|
|
2398
|
+
`${project}\u0000${testPath}`;
|
|
2378
2399
|
// Explicit projects input (plan output) replaces re-deriving `browser.enabled`
|
|
2379
2400
|
// projects from `context`, whose `projects` array is mutated during planning.
|
|
2380
2401
|
// Falls back to re-derivation only when the caller passes no list at all —
|
|
@@ -2520,47 +2541,27 @@ export const runBrowserController = async (
|
|
|
2520
2541
|
|
|
2521
2542
|
const notifyTestRunEnd = async ({
|
|
2522
2543
|
duration,
|
|
2523
|
-
|
|
2524
|
-
filterRerunTestPaths,
|
|
2544
|
+
coverage,
|
|
2525
2545
|
}: {
|
|
2526
2546
|
duration: {
|
|
2527
2547
|
totalTime: number;
|
|
2528
2548
|
buildTime: number;
|
|
2529
2549
|
testTime: number;
|
|
2530
2550
|
};
|
|
2531
|
-
|
|
2532
|
-
filterRerunTestPaths?: string[];
|
|
2551
|
+
coverage?: CoverageMapData;
|
|
2533
2552
|
}): Promise<void> => {
|
|
2534
2553
|
if (!isWatchMode) {
|
|
2535
2554
|
return;
|
|
2536
2555
|
}
|
|
2537
2556
|
|
|
2538
|
-
// Merge per-file coverage into a single CoverageMapData for reporters
|
|
2539
|
-
let mergedCoverage: CoverageMapData | undefined;
|
|
2540
|
-
if (coverageProvider) {
|
|
2541
|
-
const coverageMap = coverageProvider.createCoverageMap();
|
|
2542
|
-
let hasCoverage = false;
|
|
2543
|
-
for (const result of context.reporterResults.results) {
|
|
2544
|
-
if (result.coverage) {
|
|
2545
|
-
coverageMap.merge(result.coverage);
|
|
2546
|
-
hasCoverage = true;
|
|
2547
|
-
}
|
|
2548
|
-
}
|
|
2549
|
-
if (hasCoverage) {
|
|
2550
|
-
mergedCoverage = coverageMap.toJSON();
|
|
2551
|
-
}
|
|
2552
|
-
}
|
|
2553
|
-
|
|
2554
2557
|
for (const reporter of context.reporters) {
|
|
2555
2558
|
await reporter.onTestRunEnd?.({
|
|
2556
2559
|
results: context.reporterResults.results,
|
|
2557
|
-
coverage
|
|
2560
|
+
coverage,
|
|
2558
2561
|
testResults: context.reporterResults.testResults,
|
|
2559
2562
|
duration,
|
|
2560
2563
|
snapshotSummary: context.snapshotManager.summary,
|
|
2561
2564
|
getSourcemap: getBrowserSourcemap,
|
|
2562
|
-
unhandledErrors,
|
|
2563
|
-
filterRerunTestPaths,
|
|
2564
2565
|
});
|
|
2565
2566
|
}
|
|
2566
2567
|
};
|
|
@@ -2601,7 +2602,6 @@ export const runBrowserController = async (
|
|
|
2601
2602
|
(total, item) => total + item.testFiles.length,
|
|
2602
2603
|
0,
|
|
2603
2604
|
);
|
|
2604
|
-
const shouldKeepWatchingWithEmptySet = isWatchMode && allowEmptyWatchRun;
|
|
2605
2605
|
const shouldInitializeEmptyBrowserHooks =
|
|
2606
2606
|
totalTests === 0 && hasUserRstestConfigPlugins(browserProjects);
|
|
2607
2607
|
|
|
@@ -2621,16 +2621,14 @@ export const runBrowserController = async (
|
|
|
2621
2621
|
};
|
|
2622
2622
|
};
|
|
2623
2623
|
|
|
2624
|
-
const reportEmptyTestSet = ():
|
|
2624
|
+
const reportEmptyTestSet = (): void => {
|
|
2625
2625
|
const code = context.normalizedConfig.passWithNoTests ? 0 : 1;
|
|
2626
2626
|
if (isWatchMode || !allowEmptyRun) {
|
|
2627
|
-
const message =
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
defaultMessage: `No test files found, exiting with code ${code}.`,
|
|
2633
|
-
});
|
|
2627
|
+
const message = getNoTestFilesMessage({
|
|
2628
|
+
context,
|
|
2629
|
+
code,
|
|
2630
|
+
defaultMessage: `No test files found, exiting with code ${code}.`,
|
|
2631
|
+
});
|
|
2634
2632
|
if (code === 0) {
|
|
2635
2633
|
logger.log(color.yellow(message));
|
|
2636
2634
|
} else {
|
|
@@ -2653,22 +2651,14 @@ export const runBrowserController = async (
|
|
|
2653
2651
|
// In non-watch runs the host returns a void outcome and core's
|
|
2654
2652
|
// `reportNoTestFiles` owns the exit code and the no-test reporter lifecycle;
|
|
2655
2653
|
// the host must not set the code itself. Watch keeps its own exit code.
|
|
2656
|
-
if (
|
|
2657
|
-
isWatchMode &&
|
|
2658
|
-
code !== 0 &&
|
|
2659
|
-
!shouldKeepWatchingWithEmptySet &&
|
|
2660
|
-
!allowEmptyRun
|
|
2661
|
-
) {
|
|
2654
|
+
if (isWatchMode && code !== 0 && !allowEmptyRun) {
|
|
2662
2655
|
ensureProcessExitCode(code);
|
|
2663
2656
|
}
|
|
2664
|
-
|
|
2665
|
-
return !shouldKeepWatchingWithEmptySet;
|
|
2666
2657
|
};
|
|
2667
2658
|
|
|
2668
2659
|
if (totalTests === 0 && !shouldInitializeEmptyBrowserHooks) {
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
}
|
|
2660
|
+
reportEmptyTestSet();
|
|
2661
|
+
return allowEmptyRun ? createEmptyRunResult() : undefined;
|
|
2672
2662
|
}
|
|
2673
2663
|
|
|
2674
2664
|
if (!filesOnly) {
|
|
@@ -2795,23 +2785,19 @@ export const runBrowserController = async (
|
|
|
2795
2785
|
unhandledErrors?: Error[];
|
|
2796
2786
|
}): Promise<void> => {
|
|
2797
2787
|
const rerunPathSet = new Set(rerunTestPaths);
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
// must not strip `result.coverage`, or later reruns would lose it.
|
|
2801
|
-
let sessionCoverage: CoverageMapData | undefined;
|
|
2802
|
-
const coverageMap = buildBrowserCoverageMap(
|
|
2803
|
-
context.reporterResults.results,
|
|
2804
|
-
coverageProvider,
|
|
2805
|
-
{ keepResultCoverage: true },
|
|
2788
|
+
const rerunResults = context.reporterResults.results.filter((result) =>
|
|
2789
|
+
rerunPathSet.has(result.testPath),
|
|
2806
2790
|
);
|
|
2791
|
+
// Watch coverage is per-cycle on both transports: only the files this
|
|
2792
|
+
// rerun executed are reported.
|
|
2793
|
+
let rerunCoverage: CoverageMapData | undefined;
|
|
2794
|
+
const coverageMap = buildBrowserCoverageMap(rerunResults, coverageProvider);
|
|
2807
2795
|
if (coverageMap && coverageMap.files().length > 0) {
|
|
2808
|
-
|
|
2796
|
+
rerunCoverage = coverageMap.toJSON();
|
|
2809
2797
|
}
|
|
2810
2798
|
|
|
2811
2799
|
const outcome: ExecutorCycleOutcome = {
|
|
2812
|
-
results:
|
|
2813
|
-
rerunPathSet.has(result.testPath),
|
|
2814
|
-
),
|
|
2800
|
+
results: rerunResults,
|
|
2815
2801
|
testResults: context.reporterResults.testResults.filter((result) =>
|
|
2816
2802
|
rerunPathSet.has(result.testPath),
|
|
2817
2803
|
),
|
|
@@ -2821,7 +2807,7 @@ export const runBrowserController = async (
|
|
|
2821
2807
|
buildTime: drainPendingBuildTime(watchState),
|
|
2822
2808
|
testTime,
|
|
2823
2809
|
},
|
|
2824
|
-
coverage:
|
|
2810
|
+
coverage: rerunCoverage ? { map: rerunCoverage } : undefined,
|
|
2825
2811
|
resolveSourcemap: resolveBrowserSourcemap,
|
|
2826
2812
|
};
|
|
2827
2813
|
|
|
@@ -2858,7 +2844,8 @@ export const runBrowserController = async (
|
|
|
2858
2844
|
};
|
|
2859
2845
|
}
|
|
2860
2846
|
|
|
2861
|
-
if (totalTests === 0
|
|
2847
|
+
if (totalTests === 0) {
|
|
2848
|
+
reportEmptyTestSet();
|
|
2862
2849
|
await destroyBrowserRuntime(runtime);
|
|
2863
2850
|
return allowEmptyRun ? createEmptyRunResult() : undefined;
|
|
2864
2851
|
}
|
|
@@ -3022,22 +3009,18 @@ export const runBrowserController = async (
|
|
|
3022
3009
|
createRunnerEventSink(context, project.normalizedConfig),
|
|
3023
3010
|
]),
|
|
3024
3011
|
);
|
|
3025
|
-
|
|
3026
|
-
|
|
3027
|
-
//
|
|
3028
|
-
//
|
|
3029
|
-
//
|
|
3030
|
-
//
|
|
3031
|
-
|
|
3032
|
-
|
|
3033
|
-
|
|
3034
|
-
|
|
3035
|
-
|
|
3036
|
-
|
|
3037
|
-
|
|
3038
|
-
const sinkForTestPath = (testPath: string): RunnerEventSink => {
|
|
3039
|
-
const projectName = projectNameByTestPath.get(testPath);
|
|
3040
|
-
return projectName ? sinkForProjectName(projectName) : firstBrowserSink;
|
|
3012
|
+
// Every per-file wire payload carries its owning project name, so routing
|
|
3013
|
+
// never derives a project from a test path — concurrent projects can run the
|
|
3014
|
+
// same file, and a path-keyed lookup would attribute events to the wrong one.
|
|
3015
|
+
// The client resolves its project from the host's own manifest, so a miss is
|
|
3016
|
+
// a protocol bug; fail loudly rather than route through another project's
|
|
3017
|
+
// config.
|
|
3018
|
+
const sinkForProjectName = (projectName: string): RunnerEventSink => {
|
|
3019
|
+
const sink = runnerSinks.get(projectName);
|
|
3020
|
+
if (!sink) {
|
|
3021
|
+
throw new Error(`No runner event sink for project "${projectName}"`);
|
|
3022
|
+
}
|
|
3023
|
+
return sink;
|
|
3041
3024
|
};
|
|
3042
3025
|
|
|
3043
3026
|
// Silent-console buffering runs through the shared controller — the same
|
|
@@ -3054,7 +3037,7 @@ export const runBrowserController = async (
|
|
|
3054
3037
|
disableConsoleIntercept: context.normalizedConfig.disableConsoleIntercept,
|
|
3055
3038
|
},
|
|
3056
3039
|
emitInterceptedLog: (log) =>
|
|
3057
|
-
|
|
3040
|
+
sinkForProjectName(log.project).onConsoleLog(log),
|
|
3058
3041
|
writeOriginalLog: () => {},
|
|
3059
3042
|
});
|
|
3060
3043
|
|
|
@@ -3089,7 +3072,6 @@ export const runBrowserController = async (
|
|
|
3089
3072
|
const handleTestFileStart = async (
|
|
3090
3073
|
payload: TestFileStartPayload,
|
|
3091
3074
|
): Promise<void> => {
|
|
3092
|
-
projectNameByTestPath.set(payload.testPath, payload.projectName);
|
|
3093
3075
|
if (phaseTrackers) {
|
|
3094
3076
|
const tracker = new PhaseTracker({
|
|
3095
3077
|
trace: {
|
|
@@ -3099,13 +3081,17 @@ export const runBrowserController = async (
|
|
|
3099
3081
|
pid: nextBrowserFilePid++,
|
|
3100
3082
|
});
|
|
3101
3083
|
tracker.transition('prepare');
|
|
3102
|
-
phaseTrackers.set(
|
|
3084
|
+
phaseTrackers.set(
|
|
3085
|
+
trackerKey(payload.projectName, payload.testPath),
|
|
3086
|
+
tracker,
|
|
3087
|
+
);
|
|
3103
3088
|
}
|
|
3104
3089
|
// The client sends `{ testPath, projectName }`; the sink adapter builds the
|
|
3105
3090
|
// `TestFileInfo` the reporters and stateManager expect.
|
|
3106
3091
|
await sinkForProjectName(payload.projectName).onTestFileStart({
|
|
3107
3092
|
testId: getFileTaskId(payload.testPath),
|
|
3108
3093
|
testPath: payload.testPath,
|
|
3094
|
+
project: payload.projectName,
|
|
3109
3095
|
tests: [],
|
|
3110
3096
|
});
|
|
3111
3097
|
};
|
|
@@ -3113,22 +3099,28 @@ export const runBrowserController = async (
|
|
|
3113
3099
|
const handleTestFileReady = async (
|
|
3114
3100
|
payload: TestFileReadyPayload,
|
|
3115
3101
|
): Promise<void> => {
|
|
3116
|
-
phaseTrackers
|
|
3117
|
-
|
|
3102
|
+
phaseTrackers
|
|
3103
|
+
?.get(trackerKey(payload.project, payload.testPath))
|
|
3104
|
+
?.transition('tests');
|
|
3105
|
+
await sinkForProjectName(payload.project).onTestFileReady(payload);
|
|
3118
3106
|
};
|
|
3119
3107
|
|
|
3120
3108
|
const handleTestSuiteStart = async (
|
|
3121
3109
|
payload: TestSuiteStartPayload,
|
|
3122
3110
|
): Promise<void> => {
|
|
3123
|
-
phaseTrackers
|
|
3124
|
-
|
|
3111
|
+
phaseTrackers
|
|
3112
|
+
?.get(trackerKey(payload.project, payload.testPath))
|
|
3113
|
+
?.recordSuiteStart(payload);
|
|
3114
|
+
await sinkForProjectName(payload.project).onTestSuiteStart(payload);
|
|
3125
3115
|
};
|
|
3126
3116
|
|
|
3127
3117
|
const handleTestSuiteResult = async (
|
|
3128
3118
|
payload: TestSuiteResultPayload,
|
|
3129
3119
|
): Promise<void> => {
|
|
3130
|
-
phaseTrackers
|
|
3131
|
-
|
|
3120
|
+
phaseTrackers
|
|
3121
|
+
?.get(trackerKey(payload.project, payload.testPath))
|
|
3122
|
+
?.recordSuiteResult(payload);
|
|
3123
|
+
await sinkForProjectName(payload.project).onTestSuiteResult(payload);
|
|
3132
3124
|
|
|
3133
3125
|
if (context.normalizedConfig.silent === 'passed-only') {
|
|
3134
3126
|
silentConsoleController.flushBufferedLogsForTask({
|
|
@@ -3144,15 +3136,19 @@ export const runBrowserController = async (
|
|
|
3144
3136
|
const handleTestCaseStart = async (
|
|
3145
3137
|
payload: TestCaseStartPayload,
|
|
3146
3138
|
): Promise<void> => {
|
|
3147
|
-
phaseTrackers
|
|
3139
|
+
phaseTrackers
|
|
3140
|
+
?.get(trackerKey(payload.project, payload.testPath))
|
|
3141
|
+
?.recordCaseStart(payload);
|
|
3148
3142
|
// Fire-and-forget on both transports (the sink does not await case-start).
|
|
3149
|
-
|
|
3143
|
+
sinkForProjectName(payload.project).onTestCaseStart(payload);
|
|
3150
3144
|
};
|
|
3151
3145
|
|
|
3152
3146
|
const handleTestCaseResult = async (payload: TestResult): Promise<void> => {
|
|
3153
3147
|
caseResults.push(payload);
|
|
3154
|
-
phaseTrackers
|
|
3155
|
-
|
|
3148
|
+
phaseTrackers
|
|
3149
|
+
?.get(trackerKey(payload.project, payload.testPath))
|
|
3150
|
+
?.recordCaseResult(payload);
|
|
3151
|
+
await sinkForProjectName(payload.project).onTestCaseResult(payload);
|
|
3156
3152
|
|
|
3157
3153
|
if (context.normalizedConfig.silent === 'passed-only') {
|
|
3158
3154
|
silentConsoleController.flushBufferedLogsForTask({
|
|
@@ -3172,12 +3168,13 @@ export const runBrowserController = async (
|
|
|
3172
3168
|
context.updateReporterResultState([payload], payload.results);
|
|
3173
3169
|
|
|
3174
3170
|
if (phaseTrackers) {
|
|
3175
|
-
const
|
|
3171
|
+
const key = trackerKey(payload.project, payload.testPath);
|
|
3172
|
+
const tracker = phaseTrackers.get(key);
|
|
3176
3173
|
if (tracker) {
|
|
3177
3174
|
tracker.end();
|
|
3178
3175
|
const events = tracker.getTraceEvents();
|
|
3179
3176
|
if (events) onTraceEvents?.(events);
|
|
3180
|
-
phaseTrackers.delete(
|
|
3177
|
+
phaseTrackers.delete(key);
|
|
3181
3178
|
}
|
|
3182
3179
|
}
|
|
3183
3180
|
|
|
@@ -3193,7 +3190,7 @@ export const runBrowserController = async (
|
|
|
3193
3190
|
|
|
3194
3191
|
// Feeds stateManager, fans out onTestFileResult to reporters, and ingests
|
|
3195
3192
|
// payload.snapshotResult (the snapshotManager.add moved into the sink).
|
|
3196
|
-
await
|
|
3193
|
+
await sinkForProjectName(payload.project).onTestFileResult(payload);
|
|
3197
3194
|
// In non-watch runs core owns the exit code via `finalizeRunCycle` (the
|
|
3198
3195
|
// failing file rides the returned outcome); watch reruns set it here.
|
|
3199
3196
|
if (isWatchMode && payload.status === 'fail') {
|
|
@@ -3211,6 +3208,7 @@ export const runBrowserController = async (
|
|
|
3211
3208
|
taskParentNames: payload.taskParentNames,
|
|
3212
3209
|
taskType: payload.taskType,
|
|
3213
3210
|
testPath: payload.testPath,
|
|
3211
|
+
project: payload.projectName,
|
|
3214
3212
|
type: payload.type,
|
|
3215
3213
|
trace: payload.trace,
|
|
3216
3214
|
};
|
|
@@ -3671,71 +3669,6 @@ export const runBrowserController = async (
|
|
|
3671
3669
|
|
|
3672
3670
|
awaitHeadlessRerunIdle = () => latestRerunScheduler.whenIdle();
|
|
3673
3671
|
|
|
3674
|
-
if (allTestFiles.length === 0) {
|
|
3675
|
-
const duration = {
|
|
3676
|
-
totalTime: buildTime,
|
|
3677
|
-
buildTime,
|
|
3678
|
-
testTime: 0,
|
|
3679
|
-
};
|
|
3680
|
-
const result = {
|
|
3681
|
-
results: reporterResults,
|
|
3682
|
-
testResults: caseResults,
|
|
3683
|
-
duration,
|
|
3684
|
-
hasFailure: false,
|
|
3685
|
-
getSourcemap: getBrowserSourcemap,
|
|
3686
|
-
resolveSourcemap: resolveBrowserSourcemap,
|
|
3687
|
-
close: !isWatchMode
|
|
3688
|
-
? async () => {
|
|
3689
|
-
sessionRegistry.clear();
|
|
3690
|
-
await destroyBrowserRuntime(runtime);
|
|
3691
|
-
}
|
|
3692
|
-
: undefined,
|
|
3693
|
-
watch: watchHandles,
|
|
3694
|
-
};
|
|
3695
|
-
|
|
3696
|
-
if (isWatchMode) {
|
|
3697
|
-
await notifyTestRunEnd({ duration });
|
|
3698
|
-
}
|
|
3699
|
-
|
|
3700
|
-
if (isWatchMode) {
|
|
3701
|
-
triggerRerun = async () => {
|
|
3702
|
-
const newProjectEntries = await collectProjectEntries(context);
|
|
3703
|
-
const rerunPlan = planWatchRerun({
|
|
3704
|
-
projectEntries: newProjectEntries,
|
|
3705
|
-
previousTestFiles: watchState.lastTestFiles,
|
|
3706
|
-
affectedTestFiles: drainPendingAffectedTestFiles(watchState),
|
|
3707
|
-
});
|
|
3708
|
-
|
|
3709
|
-
if (rerunPlan.filesChanged) {
|
|
3710
|
-
watchState.lastTestFiles = rerunPlan.currentTestFiles;
|
|
3711
|
-
if (rerunPlan.currentTestFiles.length === 0) {
|
|
3712
|
-
logger.log(
|
|
3713
|
-
color.cyan('No browser test files remain after update.\n'),
|
|
3714
|
-
);
|
|
3715
|
-
logWatchReadyMessage(context, enableCliShortcuts);
|
|
3716
|
-
return;
|
|
3717
|
-
}
|
|
3718
|
-
|
|
3719
|
-
logger.log(
|
|
3720
|
-
color.cyan(
|
|
3721
|
-
`Test file set changed, re-running ${rerunPlan.currentTestFiles.length} file(s)...\n`,
|
|
3722
|
-
),
|
|
3723
|
-
);
|
|
3724
|
-
await latestRerunScheduler.enqueueLatest(
|
|
3725
|
-
rerunPlan.currentTestFiles,
|
|
3726
|
-
);
|
|
3727
|
-
return;
|
|
3728
|
-
}
|
|
3729
|
-
|
|
3730
|
-
logWatchReadyMessage(context, enableCliShortcuts);
|
|
3731
|
-
};
|
|
3732
|
-
watchState.hooksEnabled = true;
|
|
3733
|
-
logWatchReadyMessage(context, enableCliShortcuts);
|
|
3734
|
-
}
|
|
3735
|
-
|
|
3736
|
-
return result;
|
|
3737
|
-
}
|
|
3738
|
-
|
|
3739
3672
|
const testStart = Date.now();
|
|
3740
3673
|
await runFilesWithPool(allTestFiles);
|
|
3741
3674
|
const testTime = Date.now() - testStart;
|
|
@@ -3823,6 +3756,15 @@ export const runBrowserController = async (
|
|
|
3823
3756
|
ensureProcessExitCode(1);
|
|
3824
3757
|
}
|
|
3825
3758
|
|
|
3759
|
+
// Fold and strip the initial cycle's coverage (the same per-cycle fold
|
|
3760
|
+
// reruns get in `finalizeWatchRerun`); the map rides on the result so the
|
|
3761
|
+
// browser-only watch path can report it without re-merging. Non-watch runs
|
|
3762
|
+
// keep `result.coverage` intact for the executor's outcome fold.
|
|
3763
|
+
const cycleCoverageMap =
|
|
3764
|
+
isWatchMode && coverageProvider
|
|
3765
|
+
? buildBrowserCoverageMap(reporterResults, coverageProvider)
|
|
3766
|
+
: undefined;
|
|
3767
|
+
|
|
3826
3768
|
const result = {
|
|
3827
3769
|
results: reporterResults,
|
|
3828
3770
|
testResults: caseResults,
|
|
@@ -3833,12 +3775,18 @@ export const runBrowserController = async (
|
|
|
3833
3775
|
// `closeHeadlessRuntime` is already `undefined` in watch mode, so the
|
|
3834
3776
|
// non-watch caller (core) receives the deferred close and watch does not.
|
|
3835
3777
|
close: closeHeadlessRuntime,
|
|
3778
|
+
coverage: cycleCoverageMap,
|
|
3836
3779
|
watch: watchHandles,
|
|
3837
3780
|
};
|
|
3838
3781
|
|
|
3839
3782
|
if (isWatchMode) {
|
|
3840
3783
|
try {
|
|
3841
|
-
await notifyTestRunEnd({
|
|
3784
|
+
await notifyTestRunEnd({
|
|
3785
|
+
duration,
|
|
3786
|
+
coverage: cycleCoverageMap?.files().length
|
|
3787
|
+
? cycleCoverageMap.toJSON()
|
|
3788
|
+
: undefined,
|
|
3789
|
+
});
|
|
3842
3790
|
} finally {
|
|
3843
3791
|
await closeHeadlessRuntime?.();
|
|
3844
3792
|
}
|
|
@@ -4354,6 +4302,12 @@ export const runBrowserController = async (
|
|
|
4354
4302
|
ensureProcessExitCode(1);
|
|
4355
4303
|
}
|
|
4356
4304
|
|
|
4305
|
+
// Same per-cycle fold-and-strip as the headless path above.
|
|
4306
|
+
const cycleCoverageMap =
|
|
4307
|
+
isWatchMode && coverageProvider
|
|
4308
|
+
? buildBrowserCoverageMap(reporterResults, coverageProvider)
|
|
4309
|
+
: undefined;
|
|
4310
|
+
|
|
4357
4311
|
const result = {
|
|
4358
4312
|
results: reporterResults,
|
|
4359
4313
|
testResults: caseResults,
|
|
@@ -4364,12 +4318,18 @@ export const runBrowserController = async (
|
|
|
4364
4318
|
// `closeContainerRuntime` is already `undefined` in watch mode, so the
|
|
4365
4319
|
// non-watch caller (core) receives the deferred close and watch does not.
|
|
4366
4320
|
close: closeContainerRuntime,
|
|
4321
|
+
coverage: cycleCoverageMap,
|
|
4367
4322
|
watch: watchHandles,
|
|
4368
4323
|
};
|
|
4369
4324
|
|
|
4370
4325
|
if (isWatchMode) {
|
|
4371
4326
|
try {
|
|
4372
|
-
await notifyTestRunEnd({
|
|
4327
|
+
await notifyTestRunEnd({
|
|
4328
|
+
duration,
|
|
4329
|
+
coverage: cycleCoverageMap?.files().length
|
|
4330
|
+
? cycleCoverageMap.toJSON()
|
|
4331
|
+
: undefined,
|
|
4332
|
+
});
|
|
4373
4333
|
} finally {
|
|
4374
4334
|
await closeContainerRuntime?.();
|
|
4375
4335
|
}
|
package/src/protocol.ts
CHANGED
|
@@ -69,6 +69,12 @@ export type BrowserExecutionMode = 'run' | 'collect';
|
|
|
69
69
|
export type BrowserLogPayload = {
|
|
70
70
|
level: 'log' | 'warn' | 'error' | 'info' | 'debug';
|
|
71
71
|
content: string;
|
|
72
|
+
/**
|
|
73
|
+
* Owning project, resolved by the client from its manifest. The host must
|
|
74
|
+
* not re-derive it from `testPath` — concurrent projects can run the same
|
|
75
|
+
* file, so a path-keyed lookup can attribute the log to the wrong project.
|
|
76
|
+
*/
|
|
77
|
+
projectName: string;
|
|
72
78
|
taskId?: string;
|
|
73
79
|
taskName?: string;
|
|
74
80
|
taskParentNames?: string[];
|