@rstest/browser 0.11.3 → 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 +290 -245
- package/dist/protocol.d.ts +5 -0
- package/package.json +8 -7
- package/src/browserExecutor.ts +5 -3
- package/src/client/entry.ts +3 -0
- package/src/configValidation.ts +6 -0
- package/src/hostController.ts +556 -286
- package/src/protocol.ts +6 -0
- package/dist/browser-container/container-static/js/949.7d935e2ac9.js +0 -27406
- package/dist/browser-container/container-static/js/949.7d935e2ac9.js.LICENSE.txt +0 -1
- package/dist/browser-container/container-static/js/index.492a6c2f8b.js +0 -3053
- 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/dist/watchCliShortcuts.d.ts +0 -6
- package/src/AGENTS.md +0 -150
- package/src/client/AGENTS.md +0 -89
- package/src/watchCliShortcuts.ts +0 -77
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { DEFAULT_TEST_TIMEOUT, PhaseTracker, RSTEST_ENV_SYMBOL_KEY, applyWebMockRspackConfig, browserIgnoredRuntimeConfigKeys, buildBrowserCoverageMap, color, createCoverageProvider, createRunnerEventSink, createSilentConsoleController, getNoTestFilesMessage, getNumCpus as browser_getNumCpus, getPrettyConsoleName, getSetupFiles, getTestEntries, hasUserRstestConfigPlugins, importMetaRstestDefine, initModifyRstestConfigHooks, isDebug, isTTY, loadCoverageProvider, logger, pluginMockRuntime, prepareWatchRerunState, projectRuntimeConfig, resolveProjectBuildCache, resolveShardedEntries, resolveSnapshotPathDefault, resolveWorkerCount, rsbuild, serializableConfig } from "@rstest/core/internal/browser";
|
|
1
|
+
import { DEFAULT_TEST_TIMEOUT, FATAL_SIGNALS, PhaseTracker, RSTEST_ENV_SYMBOL_KEY, applyWatchInvalidation, applyWebMockRspackConfig, browserIgnoredRuntimeConfigKeys, buildBrowserCoverageMap, color, createCoverageProvider, createRunnerEventSink, createSilentConsoleController, finalizeRunCycle, getNoTestFilesMessage, getNumCpus as browser_getNumCpus, getPrettyConsoleName, getSetupFiles, getTestEntries, hasUserRstestConfigPlugins, importMetaRstestDefine, initModifyRstestConfigHooks, isDebug, isTTY, loadCoverageProvider, logWatchReadyMessage, logger, pluginMockRuntime, prepareWatchRerunState, projectRuntimeConfig, resolveProjectBuildCache, resolveShardedEntries, resolveSnapshotPathDefault, resolveWorkerCount, rsbuild, serializableConfig } from "@rstest/core/internal/browser";
|
|
2
2
|
import { existsSync } from "node:fs";
|
|
3
3
|
import promises from "node:fs/promises";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
@@ -2054,6 +2054,11 @@ const ignoredKeyWarnings = {
|
|
|
2054
2054
|
message: ()=>"Ignoring isolate: false in browser mode: each test file still runs in a fresh context.",
|
|
2055
2055
|
browserOnly: true
|
|
2056
2056
|
},
|
|
2057
|
+
federation: {
|
|
2058
|
+
isNonDefault: (config)=>true === config.federation,
|
|
2059
|
+
message: ()=>'Ignoring federation in browser mode: it only applies to the Node runner.',
|
|
2060
|
+
browserOnly: true
|
|
2061
|
+
},
|
|
2057
2062
|
detectAsyncLeaks: {
|
|
2058
2063
|
isNonDefault: (config)=>true === config.detectAsyncLeaks,
|
|
2059
2064
|
message: ()=>"Ignoring detectAsyncLeaks in browser mode: it relies on node async_hooks."
|
|
@@ -2835,44 +2840,6 @@ const loadSourceMapWithCache = async ({ jsUrl, cache, force = false, origin, fet
|
|
|
2835
2840
|
return null;
|
|
2836
2841
|
}
|
|
2837
2842
|
};
|
|
2838
|
-
const isBrowserWatchCliShortcutsEnabled = ()=>isTTY('stdin');
|
|
2839
|
-
const getBrowserWatchCliShortcutsHintMessage = ()=>` ${color.dim('press')} ${color.bold('q')} ${color.dim('to quit')}\n`;
|
|
2840
|
-
const logBrowserWatchReadyMessage = (enableCliShortcuts)=>{
|
|
2841
|
-
logger.log(color.green(' Waiting for file changes...'));
|
|
2842
|
-
if (enableCliShortcuts) logger.log(getBrowserWatchCliShortcutsHintMessage());
|
|
2843
|
-
};
|
|
2844
|
-
async function setupBrowserWatchCliShortcuts({ close }) {
|
|
2845
|
-
const { emitKeypressEvents } = await import("node:readline");
|
|
2846
|
-
emitKeypressEvents(process.stdin);
|
|
2847
|
-
process.stdin.setRawMode(true);
|
|
2848
|
-
process.stdin.resume();
|
|
2849
|
-
process.stdin.setEncoding('utf8');
|
|
2850
|
-
let isClosing = false;
|
|
2851
|
-
const handleKeypress = (str, key)=>{
|
|
2852
|
-
if (key.ctrl && 'c' === key.name) return void process.kill(process.pid, 'SIGINT');
|
|
2853
|
-
if (key.ctrl && 'z' === key.name) {
|
|
2854
|
-
if ('win32' !== process.platform) process.kill(process.pid, 'SIGTSTP');
|
|
2855
|
-
return;
|
|
2856
|
-
}
|
|
2857
|
-
if ('q' !== str || isClosing) return;
|
|
2858
|
-
isClosing = true;
|
|
2859
|
-
(async ()=>{
|
|
2860
|
-
try {
|
|
2861
|
-
await close();
|
|
2862
|
-
} finally{
|
|
2863
|
-
process.exit(0);
|
|
2864
|
-
}
|
|
2865
|
-
})();
|
|
2866
|
-
};
|
|
2867
|
-
process.stdin.on('keypress', handleKeypress);
|
|
2868
|
-
return ()=>{
|
|
2869
|
-
try {
|
|
2870
|
-
process.stdin.setRawMode(false);
|
|
2871
|
-
process.stdin.pause();
|
|
2872
|
-
} catch {}
|
|
2873
|
-
process.stdin.off('keypress', handleKeypress);
|
|
2874
|
-
};
|
|
2875
|
-
}
|
|
2876
2843
|
const serializeTestFiles = (files)=>JSON.stringify(files.map((f)=>`${f.projectName}:${f.testPath}`).sort());
|
|
2877
2844
|
const normalizeTestFiles = (files)=>files.map((file)=>({
|
|
2878
2845
|
...file,
|
|
@@ -3004,6 +2971,9 @@ class ContainerRpcManager {
|
|
|
3004
2971
|
async notifyTestFileUpdate(files) {
|
|
3005
2972
|
await this.rpc?.onTestFileUpdate(files);
|
|
3006
2973
|
}
|
|
2974
|
+
async updateHostConfig(config) {
|
|
2975
|
+
await this.rpc?.onHostConfigUpdate(config);
|
|
2976
|
+
}
|
|
3007
2977
|
async reloadTestFile(testFile, testNamePattern) {
|
|
3008
2978
|
logger.debug(`[Browser UI] reloadTestFile called, rpc: ${this.rpc ? 'exists' : 'null'}, ws: ${this.ws ? 'exists' : 'null'}`);
|
|
3009
2979
|
if (!this.rpc) throw new Error('Browser UI RPC not available for reloadTestFile');
|
|
@@ -3011,15 +2981,29 @@ class ContainerRpcManager {
|
|
|
3011
2981
|
return this.rpc.reloadTestFile(testFile, testNamePattern);
|
|
3012
2982
|
}
|
|
3013
2983
|
}
|
|
2984
|
+
const createBrowserWatchState = ()=>({
|
|
2985
|
+
lastTestFiles: [],
|
|
2986
|
+
hooksEnabled: false,
|
|
2987
|
+
invalidation: new Map(),
|
|
2988
|
+
pendingAffectedTestFiles: new Map(),
|
|
2989
|
+
compileStartTimes: new Map(),
|
|
2990
|
+
pendingBuildTimeMs: 0
|
|
2991
|
+
});
|
|
2992
|
+
const drainPendingBuildTime = (watchState)=>{
|
|
2993
|
+
const buildTime = watchState.pendingBuildTimeMs;
|
|
2994
|
+
watchState.pendingBuildTimeMs = 0;
|
|
2995
|
+
return buildTime;
|
|
2996
|
+
};
|
|
2997
|
+
const drainPendingAffectedTestFiles = (watchState)=>{
|
|
2998
|
+
const affected = new Set();
|
|
2999
|
+
for (const files of watchState.pendingAffectedTestFiles.values())for (const file of files)affected.add(file);
|
|
3000
|
+
watchState.pendingAffectedTestFiles.clear();
|
|
3001
|
+
return Array.from(affected);
|
|
3002
|
+
};
|
|
3014
3003
|
const watchContext = {
|
|
3015
3004
|
runtime: null,
|
|
3016
|
-
lastTestFiles: [],
|
|
3017
|
-
hooksEnabled: false,
|
|
3018
3005
|
cleanupRegistered: false,
|
|
3019
|
-
cleanupPromise: null
|
|
3020
|
-
closeCliShortcuts: null,
|
|
3021
|
-
chunkHashes: new Map(),
|
|
3022
|
-
affectedTestFiles: []
|
|
3006
|
+
cleanupPromise: null
|
|
3023
3007
|
};
|
|
3024
3008
|
const resolveViewport = (viewport)=>{
|
|
3025
3009
|
if (!viewport) return null;
|
|
@@ -3202,25 +3186,42 @@ const getChunkKey = (chunk)=>{
|
|
|
3202
3186
|
if (chunk.files && chunk.files.length > 0) return chunk.files[0];
|
|
3203
3187
|
return null;
|
|
3204
3188
|
};
|
|
3205
|
-
const getAffectedTestFiles = (chunks, entryTestFiles)=>{
|
|
3206
|
-
|
|
3207
|
-
const
|
|
3208
|
-
const
|
|
3209
|
-
|
|
3189
|
+
const getAffectedTestFiles = ({ chunks, entryTestFiles, setupFiles, state })=>{
|
|
3190
|
+
const entryHashes = new Map();
|
|
3191
|
+
const setupHashes = new Map();
|
|
3192
|
+
const recordChunk = (snapshot, entryPath, chunkKey, hash)=>{
|
|
3193
|
+
const record = snapshot.get(entryPath) ?? {};
|
|
3194
|
+
record[chunkKey] = hash;
|
|
3195
|
+
snapshot.set(entryPath, record);
|
|
3196
|
+
};
|
|
3197
|
+
for (const chunk of chunks || []){
|
|
3210
3198
|
if (!chunk.hash) continue;
|
|
3211
|
-
const testFile = findTestFileInModules(chunk.modules, entryTestFiles);
|
|
3212
|
-
if (!testFile) continue;
|
|
3213
3199
|
const chunkKey = getChunkKey(chunk);
|
|
3214
3200
|
if (!chunkKey) continue;
|
|
3215
|
-
const
|
|
3216
|
-
|
|
3217
|
-
|
|
3218
|
-
|
|
3219
|
-
logger.debug(`[Watch] Chunk hash changed for ${chunkKey}: ${prevHash} -> ${chunk.hash} (test: ${testFile})`);
|
|
3201
|
+
const testFile = findTestFileInModules(chunk.modules, entryTestFiles);
|
|
3202
|
+
if (testFile) {
|
|
3203
|
+
recordChunk(entryHashes, testFile, chunkKey, chunk.hash);
|
|
3204
|
+
continue;
|
|
3220
3205
|
}
|
|
3206
|
+
const setupFile = findTestFileInModules(chunk.modules, setupFiles);
|
|
3207
|
+
if (setupFile) recordChunk(setupHashes, setupFile, chunkKey, chunk.hash);
|
|
3221
3208
|
}
|
|
3222
|
-
|
|
3223
|
-
|
|
3209
|
+
const seedFirstSeen = (baseline, current)=>{
|
|
3210
|
+
if (!baseline) return;
|
|
3211
|
+
for (const [entryPath, record] of current)if (!baseline.has(entryPath)) baseline.set(entryPath, record);
|
|
3212
|
+
};
|
|
3213
|
+
seedFirstSeen(state.entryHashes, entryHashes);
|
|
3214
|
+
seedFirstSeen(state.setupHashes, setupHashes);
|
|
3215
|
+
const outcome = applyWatchInvalidation(state, {
|
|
3216
|
+
entryHashes,
|
|
3217
|
+
setupHashes
|
|
3218
|
+
});
|
|
3219
|
+
if (outcome.rerunAll) {
|
|
3220
|
+
logger.debug('[Watch] Setup file changed, re-running all test files of the project');
|
|
3221
|
+
return Array.from(entryTestFiles);
|
|
3222
|
+
}
|
|
3223
|
+
for (const affected of outcome.affectedPaths)logger.debug(`[Watch] Chunk hash changed for test: ${affected}`);
|
|
3224
|
+
return outcome.affectedPaths;
|
|
3224
3225
|
};
|
|
3225
3226
|
const getBrowserProjects = (context)=>context.projects.filter((project)=>project.normalizedConfig.browser.enabled);
|
|
3226
3227
|
const getBrowserRsbuildEnvironmentConfig = (project)=>({
|
|
@@ -3426,27 +3427,22 @@ const destroyBrowserRuntime = async (runtime)=>{
|
|
|
3426
3427
|
const cleanupWatchRuntime = ()=>{
|
|
3427
3428
|
if (watchContext.cleanupPromise) return watchContext.cleanupPromise;
|
|
3428
3429
|
watchContext.cleanupPromise = (async ()=>{
|
|
3429
|
-
watchContext.closeCliShortcuts?.();
|
|
3430
|
-
watchContext.closeCliShortcuts = null;
|
|
3431
3430
|
if (!watchContext.runtime) return;
|
|
3432
3431
|
await destroyBrowserRuntime(watchContext.runtime);
|
|
3433
3432
|
watchContext.runtime = null;
|
|
3434
3433
|
})();
|
|
3435
3434
|
return watchContext.cleanupPromise;
|
|
3436
3435
|
};
|
|
3437
|
-
const registerWatchCleanup = ()=>{
|
|
3436
|
+
const registerWatchCleanup = (embedded)=>{
|
|
3438
3437
|
if (watchContext.cleanupRegistered) return;
|
|
3439
|
-
|
|
3440
|
-
|
|
3441
|
-
|
|
3442
|
-
'SIGTSTP'
|
|
3443
|
-
])process.once(signal, ()=>{
|
|
3438
|
+
watchContext.cleanupRegistered = true;
|
|
3439
|
+
if (embedded) return;
|
|
3440
|
+
for (const signal of FATAL_SIGNALS)process.once(signal, ()=>{
|
|
3444
3441
|
cleanupWatchRuntime();
|
|
3445
3442
|
});
|
|
3446
3443
|
process.once('exit', ()=>{
|
|
3447
3444
|
cleanupWatchRuntime();
|
|
3448
3445
|
});
|
|
3449
|
-
watchContext.cleanupRegistered = true;
|
|
3450
3446
|
};
|
|
3451
3447
|
const createBrowserRuntime = async ({ context, projectEntries: initialProjectEntries, browserProjects, shardedEntries, freezeShardedEntries, tempDir, isWatchMode, onTriggerRerun, containerDistPath, containerDevServer, forceHeadless, skipProviderLaunch, appliedModifyRstestConfigEnvironments })=>{
|
|
3452
3448
|
const containerHtmlTemplate = containerDistPath ? await promises.readFile(join(containerDistPath, 'index.html'), 'utf-8') : null;
|
|
@@ -3459,6 +3455,7 @@ const createBrowserRuntime = async ({ context, projectEntries: initialProjectEnt
|
|
|
3459
3455
|
};
|
|
3460
3456
|
let browserLaunchOptions = ensureConsistentBrowserLaunchOptions(browserProjects);
|
|
3461
3457
|
let projectEntries = initialProjectEntries;
|
|
3458
|
+
const watchState = createBrowserWatchState();
|
|
3462
3459
|
const manifestModules = [];
|
|
3463
3460
|
const createRuntimeWithoutProvider = ()=>{
|
|
3464
3461
|
const firstProject = browserProjects[0];
|
|
@@ -3481,7 +3478,8 @@ const createBrowserRuntime = async ({ context, projectEntries: initialProjectEnt
|
|
|
3481
3478
|
setContainerOptions,
|
|
3482
3479
|
dispatchHandlers,
|
|
3483
3480
|
wss: void 0,
|
|
3484
|
-
projectEntries
|
|
3481
|
+
projectEntries,
|
|
3482
|
+
watchState
|
|
3485
3483
|
};
|
|
3486
3484
|
};
|
|
3487
3485
|
const getProjectEntry = (project)=>projectEntries.find((item)=>item.project.environmentName === project.environmentName);
|
|
@@ -3551,6 +3549,30 @@ const createBrowserRuntime = async ({ context, projectEntries: initialProjectEnt
|
|
|
3551
3549
|
return false;
|
|
3552
3550
|
}
|
|
3553
3551
|
};
|
|
3552
|
+
const serveContainerRoute = async (req, res, next)=>{
|
|
3553
|
+
if (!req.url) return void next();
|
|
3554
|
+
const url = new URL(req.url, 'http://localhost');
|
|
3555
|
+
if ('/' === url.pathname) {
|
|
3556
|
+
if (await respondWithDevServerHtml(url, res)) return;
|
|
3557
|
+
const html = injectedContainerHtml || containerHtmlTemplate?.replace(OPTIONS_PLACEHOLDER, 'null');
|
|
3558
|
+
if (html) {
|
|
3559
|
+
res.setHeader('Content-Type', 'text/html');
|
|
3560
|
+
res.end(html);
|
|
3561
|
+
return;
|
|
3562
|
+
}
|
|
3563
|
+
res.statusCode = 502;
|
|
3564
|
+
res.end('Container UI is not available.');
|
|
3565
|
+
return;
|
|
3566
|
+
}
|
|
3567
|
+
if (url.pathname.startsWith('/container-static/')) {
|
|
3568
|
+
if (await proxyDevServerAsset(req, res)) return;
|
|
3569
|
+
if (serveContainer) return void serveContainer(req, res, next);
|
|
3570
|
+
res.statusCode = 502;
|
|
3571
|
+
res.end('Container assets are not available.');
|
|
3572
|
+
return;
|
|
3573
|
+
}
|
|
3574
|
+
next();
|
|
3575
|
+
};
|
|
3554
3576
|
const buildProjectServer = async (project, isContainerServer)=>{
|
|
3555
3577
|
const manifestPath = join(tempDir, toSafeVarName(project.environmentName), VIRTUAL_MANIFEST_FILENAME);
|
|
3556
3578
|
const entry = getProjectEntry(project);
|
|
@@ -3587,7 +3609,10 @@ const createBrowserRuntime = async ({ context, projectEntries: initialProjectEnt
|
|
|
3587
3609
|
server: {
|
|
3588
3610
|
printUrls: false,
|
|
3589
3611
|
port: project.normalizedConfig.browser.port ?? (isContainerServer ? 4000 : 0),
|
|
3590
|
-
strictPort: project.normalizedConfig.browser.strictPort
|
|
3612
|
+
strictPort: project.normalizedConfig.browser.strictPort,
|
|
3613
|
+
setup: isContainerServer ? ({ server })=>{
|
|
3614
|
+
server.middlewares.use(serveContainerRoute);
|
|
3615
|
+
} : void 0
|
|
3591
3616
|
},
|
|
3592
3617
|
dev: createBrowserRsbuildDevConfig(enableHmr),
|
|
3593
3618
|
environments: {
|
|
@@ -3644,9 +3669,19 @@ const createBrowserRuntime = async ({ context, projectEntries: initialProjectEnt
|
|
|
3644
3669
|
target: 'web',
|
|
3645
3670
|
sourceMap: {
|
|
3646
3671
|
js: 'source-map'
|
|
3672
|
+
},
|
|
3673
|
+
distPath: {
|
|
3674
|
+
root: join(tempDir, 'server', toSafeVarName(project.environmentName))
|
|
3647
3675
|
}
|
|
3648
3676
|
},
|
|
3649
3677
|
tools: {
|
|
3678
|
+
swc: (swcConfig)=>{
|
|
3679
|
+
swcConfig.env ??= {};
|
|
3680
|
+
swcConfig.env.exclude = Array.from(new Set([
|
|
3681
|
+
...swcConfig.env.exclude ?? [],
|
|
3682
|
+
'transform-parameters'
|
|
3683
|
+
]));
|
|
3684
|
+
},
|
|
3650
3685
|
rspack: (rspackConfig)=>{
|
|
3651
3686
|
rspackConfig.mode = 'development';
|
|
3652
3687
|
applyWebMockRspackConfig(rspackConfig, {
|
|
@@ -3685,21 +3720,46 @@ const createBrowserRuntime = async ({ context, projectEntries: initialProjectEnt
|
|
|
3685
3720
|
name: 'rstest:browser-watch',
|
|
3686
3721
|
setup (api) {
|
|
3687
3722
|
api.onBeforeDevCompile(()=>{
|
|
3688
|
-
|
|
3723
|
+
watchState.compileStartTimes.set(project.name, Date.now());
|
|
3724
|
+
if (!watchState.hooksEnabled) return;
|
|
3689
3725
|
logger.log(color.cyan('\nFile changed, re-running tests...\n'));
|
|
3690
3726
|
});
|
|
3691
3727
|
api.onAfterDevCompile(async ({ stats })=>{
|
|
3728
|
+
const compileStart = watchState.compileStartTimes.get(project.name);
|
|
3729
|
+
if (void 0 !== compileStart) {
|
|
3730
|
+
watchState.compileStartTimes.delete(project.name);
|
|
3731
|
+
if (watchState.hooksEnabled) watchState.pendingBuildTimeMs = Math.max(watchState.pendingBuildTimeMs, Date.now() - compileStart);
|
|
3732
|
+
}
|
|
3692
3733
|
if (stats) {
|
|
3693
|
-
const
|
|
3694
|
-
|
|
3734
|
+
const [projectEntry] = await collectProjectEntries(context, [
|
|
3735
|
+
project
|
|
3736
|
+
]);
|
|
3737
|
+
const entryTestFiles = new Set(collectWatchTestFiles(projectEntry ? [
|
|
3738
|
+
projectEntry
|
|
3739
|
+
] : []).map((file)=>file.testPath));
|
|
3740
|
+
const setupFiles = new Set((projectEntry?.setupFiles ?? []).map((file)=>normalize(file)));
|
|
3741
|
+
let state = watchState.invalidation.get(project.name);
|
|
3742
|
+
if (!state) {
|
|
3743
|
+
state = {};
|
|
3744
|
+
watchState.invalidation.set(project.name, state);
|
|
3745
|
+
}
|
|
3695
3746
|
const statsJson = stats.toJson({
|
|
3696
3747
|
all: true
|
|
3697
3748
|
});
|
|
3698
|
-
const affected = getAffectedTestFiles(
|
|
3699
|
-
|
|
3700
|
-
|
|
3749
|
+
const affected = getAffectedTestFiles({
|
|
3750
|
+
chunks: statsJson.chunks,
|
|
3751
|
+
entryTestFiles,
|
|
3752
|
+
setupFiles,
|
|
3753
|
+
state
|
|
3754
|
+
});
|
|
3755
|
+
if (affected.length > 0) {
|
|
3756
|
+
const pending = watchState.pendingAffectedTestFiles.get(project.name) ?? new Set();
|
|
3757
|
+
for (const file of affected)pending.add(file);
|
|
3758
|
+
watchState.pendingAffectedTestFiles.set(project.name, pending);
|
|
3759
|
+
logger.debug(`[Watch] Affected test files: ${affected.join(', ')}`);
|
|
3760
|
+
}
|
|
3701
3761
|
}
|
|
3702
|
-
if (!
|
|
3762
|
+
if (!watchState.hooksEnabled) return;
|
|
3703
3763
|
await onTriggerRerun();
|
|
3704
3764
|
});
|
|
3705
3765
|
}
|
|
@@ -3732,6 +3792,7 @@ const createBrowserRuntime = async ({ context, projectEntries: initialProjectEnt
|
|
|
3732
3792
|
});
|
|
3733
3793
|
if (isDebug()) await rsbuildInstance.inspectConfig({
|
|
3734
3794
|
writeToDisk: true,
|
|
3795
|
+
outputPath: external_pathe_resolve(context.rootPath, context.normalizedConfig.output.distPath.root, '.rsbuild'),
|
|
3735
3796
|
extraConfigs: {
|
|
3736
3797
|
rstest: {
|
|
3737
3798
|
...context.normalizedConfig,
|
|
@@ -3766,27 +3827,6 @@ const createBrowserRuntime = async ({ context, projectEntries: initialProjectEnt
|
|
|
3766
3827
|
}
|
|
3767
3828
|
return;
|
|
3768
3829
|
}
|
|
3769
|
-
if (isContainerServer) {
|
|
3770
|
-
if ('/' === url.pathname) {
|
|
3771
|
-
if (await respondWithDevServerHtml(url, res)) return;
|
|
3772
|
-
const html = injectedContainerHtml || containerHtmlTemplate?.replace(OPTIONS_PLACEHOLDER, 'null');
|
|
3773
|
-
if (html) {
|
|
3774
|
-
res.setHeader('Content-Type', 'text/html');
|
|
3775
|
-
res.end(html);
|
|
3776
|
-
return;
|
|
3777
|
-
}
|
|
3778
|
-
res.statusCode = 502;
|
|
3779
|
-
res.end('Container UI is not available.');
|
|
3780
|
-
return;
|
|
3781
|
-
}
|
|
3782
|
-
if (url.pathname.startsWith('/container-static/')) {
|
|
3783
|
-
if (await proxyDevServerAsset(req, res)) return;
|
|
3784
|
-
if (serveContainer) return void serveContainer(req, res, next);
|
|
3785
|
-
res.statusCode = 502;
|
|
3786
|
-
res.end('Container assets are not available.');
|
|
3787
|
-
return;
|
|
3788
|
-
}
|
|
3789
|
-
}
|
|
3790
3830
|
if ('/runner.html' === url.pathname) {
|
|
3791
3831
|
res.setHeader('Content-Type', 'text/html');
|
|
3792
3832
|
res.end(htmlTemplate);
|
|
@@ -3844,7 +3884,8 @@ const createBrowserRuntime = async ({ context, projectEntries: initialProjectEnt
|
|
|
3844
3884
|
setContainerOptions,
|
|
3845
3885
|
dispatchHandlers,
|
|
3846
3886
|
wss,
|
|
3847
|
-
projectEntries
|
|
3887
|
+
projectEntries,
|
|
3888
|
+
watchState
|
|
3848
3889
|
};
|
|
3849
3890
|
} catch (error) {
|
|
3850
3891
|
wss.close();
|
|
@@ -3871,10 +3912,11 @@ async function resolveProjectEntries(context, shardedEntries, browserProjects) {
|
|
|
3871
3912
|
return collectProjectEntries(context, browserProjects);
|
|
3872
3913
|
}
|
|
3873
3914
|
const runBrowserController = async (context, options)=>{
|
|
3874
|
-
const {
|
|
3915
|
+
const { allowEmptyRun = false, filesOnly = false, onTraceEvents, env } = options ?? {};
|
|
3875
3916
|
const buildStart = Date.now();
|
|
3876
3917
|
const isWatchMode = 'watch' === context.command;
|
|
3877
3918
|
const phaseTrackers = onTraceEvents ? new Map() : void 0;
|
|
3919
|
+
const trackerKey = (project, testPath)=>`${project}\u0000${testPath}`;
|
|
3878
3920
|
const browserProjects = options?.projects ?? getBrowserProjects(context);
|
|
3879
3921
|
const useHeadlessDirect = browserProjects.every((project)=>project.normalizedConfig.browser.headless);
|
|
3880
3922
|
const browserSourceMapCache = new Map();
|
|
@@ -3954,27 +3996,15 @@ const runBrowserController = async (context, options)=>{
|
|
|
3954
3996
|
};
|
|
3955
3997
|
const coverageConfig = browserProjects.find((project)=>project.normalizedConfig.coverage?.enabled)?.normalizedConfig.coverage;
|
|
3956
3998
|
const coverageProvider = coverageConfig?.enabled ? await createCoverageProvider(coverageConfig, context.rootPath) : null;
|
|
3957
|
-
const notifyTestRunEnd = async ({ duration,
|
|
3999
|
+
const notifyTestRunEnd = async ({ duration, coverage })=>{
|
|
3958
4000
|
if (!isWatchMode) return;
|
|
3959
|
-
let mergedCoverage;
|
|
3960
|
-
if (coverageProvider) {
|
|
3961
|
-
const coverageMap = coverageProvider.createCoverageMap();
|
|
3962
|
-
let hasCoverage = false;
|
|
3963
|
-
for (const result of context.reporterResults.results)if (result.coverage) {
|
|
3964
|
-
coverageMap.merge(result.coverage);
|
|
3965
|
-
hasCoverage = true;
|
|
3966
|
-
}
|
|
3967
|
-
if (hasCoverage) mergedCoverage = coverageMap.toJSON();
|
|
3968
|
-
}
|
|
3969
4001
|
for (const reporter of context.reporters)await reporter.onTestRunEnd?.({
|
|
3970
4002
|
results: context.reporterResults.results,
|
|
3971
|
-
coverage
|
|
4003
|
+
coverage,
|
|
3972
4004
|
testResults: context.reporterResults.testResults,
|
|
3973
4005
|
duration,
|
|
3974
4006
|
snapshotSummary: context.snapshotManager.summary,
|
|
3975
|
-
getSourcemap: getBrowserSourcemap
|
|
3976
|
-
unhandledErrors,
|
|
3977
|
-
filterRerunTestPaths
|
|
4007
|
+
getSourcemap: getBrowserSourcemap
|
|
3978
4008
|
});
|
|
3979
4009
|
};
|
|
3980
4010
|
const containerDevServerEnv = process.env.RSTEST_CONTAINER_DEV_SERVER;
|
|
@@ -3997,7 +4027,6 @@ const runBrowserController = async (context, options)=>{
|
|
|
3997
4027
|
}
|
|
3998
4028
|
let projectEntries = await resolveProjectEntries(context, options?.shardedEntries, browserProjects);
|
|
3999
4029
|
let totalTests = projectEntries.reduce((total, item)=>total + item.testFiles.length, 0);
|
|
4000
|
-
const shouldKeepWatchingWithEmptySet = isWatchMode && allowEmptyWatchRun;
|
|
4001
4030
|
const shouldInitializeEmptyBrowserHooks = 0 === totalTests && hasUserRstestConfigPlugins(browserProjects);
|
|
4002
4031
|
const createEmptyRunResult = ()=>{
|
|
4003
4032
|
const elapsed = Math.max(0, Date.now() - buildStart);
|
|
@@ -4017,7 +4046,7 @@ const runBrowserController = async (context, options)=>{
|
|
|
4017
4046
|
const reportEmptyTestSet = ()=>{
|
|
4018
4047
|
const code = context.normalizedConfig.passWithNoTests ? 0 : 1;
|
|
4019
4048
|
if (isWatchMode || !allowEmptyRun) {
|
|
4020
|
-
const message =
|
|
4049
|
+
const message = getNoTestFilesMessage({
|
|
4021
4050
|
context,
|
|
4022
4051
|
code,
|
|
4023
4052
|
defaultMessage: `No test files found, exiting with code ${code}.`
|
|
@@ -4027,19 +4056,19 @@ const runBrowserController = async (context, options)=>{
|
|
|
4027
4056
|
if (context.relatedFilters?.length) logger.log(color.gray('related: '), context.relatedFilters.join(color.gray(', ')));
|
|
4028
4057
|
else if (context.fileFilters?.length) logger.log(color.gray('filter: '), context.fileFilters.join(color.gray(', ')));
|
|
4029
4058
|
}
|
|
4030
|
-
if (isWatchMode && 0 !== code && !
|
|
4031
|
-
return !shouldKeepWatchingWithEmptySet;
|
|
4059
|
+
if (isWatchMode && 0 !== code && !allowEmptyRun) ensureProcessExitCode(code);
|
|
4032
4060
|
};
|
|
4033
4061
|
if (0 === totalTests && !shouldInitializeEmptyBrowserHooks) {
|
|
4034
|
-
|
|
4062
|
+
reportEmptyTestSet();
|
|
4063
|
+
return allowEmptyRun ? createEmptyRunResult() : void 0;
|
|
4035
4064
|
}
|
|
4036
4065
|
if (!filesOnly) await notifyTestRunStart();
|
|
4037
|
-
const enableCliShortcuts = isWatchMode &&
|
|
4066
|
+
const enableCliShortcuts = isWatchMode && isTTY('stdin');
|
|
4038
4067
|
const browserTempOutputRoot = context.normalizedConfig.output.distPath.root;
|
|
4039
4068
|
const tempDir = isWatchMode && watchContext.runtime ? watchContext.runtime.tempDir : isWatchMode ? join(context.rootPath, browserTempOutputRoot, 'browser', 'watch') : join(context.rootPath, browserTempOutputRoot, 'browser', Date.now().toString());
|
|
4040
|
-
if (isWatchMode) watchContext.lastTestFiles = collectWatchTestFiles(projectEntries);
|
|
4041
4069
|
let runtime = isWatchMode ? watchContext.runtime : null;
|
|
4042
4070
|
let triggerRerun;
|
|
4071
|
+
let awaitHeadlessRerunIdle;
|
|
4043
4072
|
if (!runtime) {
|
|
4044
4073
|
try {
|
|
4045
4074
|
runtime = await createBrowserRuntime({
|
|
@@ -4068,12 +4097,62 @@ const runBrowserController = async (context, options)=>{
|
|
|
4068
4097
|
}
|
|
4069
4098
|
if (isWatchMode) {
|
|
4070
4099
|
watchContext.runtime = runtime;
|
|
4071
|
-
registerWatchCleanup();
|
|
4072
|
-
if (enableCliShortcuts && !watchContext.closeCliShortcuts) watchContext.closeCliShortcuts = await setupBrowserWatchCliShortcuts({
|
|
4073
|
-
close: cleanupWatchRuntime
|
|
4074
|
-
});
|
|
4100
|
+
registerWatchCleanup(context.embedded);
|
|
4075
4101
|
}
|
|
4076
4102
|
}
|
|
4103
|
+
const watchState = runtime.watchState;
|
|
4104
|
+
if (isWatchMode) watchState.lastTestFiles = collectWatchTestFiles(projectEntries);
|
|
4105
|
+
const seedPendingRerun = (testPaths)=>{
|
|
4106
|
+
const wanted = testPaths ? new Set(testPaths.map((testPath)=>normalize(testPath))) : null;
|
|
4107
|
+
let seeded = 0;
|
|
4108
|
+
for (const file of watchState.lastTestFiles){
|
|
4109
|
+
if (wanted && !wanted.has(file.testPath)) continue;
|
|
4110
|
+
const pending = watchState.pendingAffectedTestFiles.get(file.projectName) ?? new Set();
|
|
4111
|
+
pending.add(file.testPath);
|
|
4112
|
+
watchState.pendingAffectedTestFiles.set(file.projectName, pending);
|
|
4113
|
+
seeded += 1;
|
|
4114
|
+
}
|
|
4115
|
+
return seeded;
|
|
4116
|
+
};
|
|
4117
|
+
const watchHandles = isWatchMode ? {
|
|
4118
|
+
rerun: async (testPaths)=>{
|
|
4119
|
+
const seeded = seedPendingRerun(testPaths);
|
|
4120
|
+
if (testPaths && 0 === seeded) return;
|
|
4121
|
+
await triggerRerun?.();
|
|
4122
|
+
await awaitHeadlessRerunIdle?.();
|
|
4123
|
+
},
|
|
4124
|
+
close: cleanupWatchRuntime
|
|
4125
|
+
} : void 0;
|
|
4126
|
+
const finalizeWatchRerun = async ({ rerunTestPaths, testTime, unhandledErrors })=>{
|
|
4127
|
+
const rerunPathSet = new Set(rerunTestPaths);
|
|
4128
|
+
const rerunResults = context.reporterResults.results.filter((result)=>rerunPathSet.has(result.testPath));
|
|
4129
|
+
let rerunCoverage;
|
|
4130
|
+
const coverageMap = buildBrowserCoverageMap(rerunResults, coverageProvider);
|
|
4131
|
+
if (coverageMap && coverageMap.files().length > 0) rerunCoverage = coverageMap.toJSON();
|
|
4132
|
+
const outcome = {
|
|
4133
|
+
results: rerunResults,
|
|
4134
|
+
testResults: context.reporterResults.testResults.filter((result)=>rerunPathSet.has(result.testPath)),
|
|
4135
|
+
errors: unhandledErrors ?? [],
|
|
4136
|
+
testPaths: rerunTestPaths,
|
|
4137
|
+
duration: {
|
|
4138
|
+
buildTime: drainPendingBuildTime(watchState),
|
|
4139
|
+
testTime
|
|
4140
|
+
},
|
|
4141
|
+
coverage: rerunCoverage ? {
|
|
4142
|
+
map: rerunCoverage
|
|
4143
|
+
} : void 0,
|
|
4144
|
+
resolveSourcemap: resolveBrowserSourcemap
|
|
4145
|
+
};
|
|
4146
|
+
await finalizeRunCycle(context, {
|
|
4147
|
+
outcomes: [
|
|
4148
|
+
outcome
|
|
4149
|
+
],
|
|
4150
|
+
mode: 'on-demand',
|
|
4151
|
+
isWatchMode: true,
|
|
4152
|
+
coverageProvider,
|
|
4153
|
+
reportOnFailure: coverageConfig?.reportOnFailure ?? false
|
|
4154
|
+
});
|
|
4155
|
+
};
|
|
4077
4156
|
projectEntries = runtime.projectEntries;
|
|
4078
4157
|
totalTests = projectEntries.reduce((total, item)=>total + item.testFiles.length, 0);
|
|
4079
4158
|
const buildTime = Date.now() - buildStart;
|
|
@@ -4090,7 +4169,8 @@ const runBrowserController = async (context, options)=>{
|
|
|
4090
4169
|
resolveSourcemap: resolveBrowserSourcemap,
|
|
4091
4170
|
close: ()=>destroyBrowserRuntime(runtime)
|
|
4092
4171
|
};
|
|
4093
|
-
if (0 === totalTests
|
|
4172
|
+
if (0 === totalTests) {
|
|
4173
|
+
reportEmptyTestSet();
|
|
4094
4174
|
await destroyBrowserRuntime(runtime);
|
|
4095
4175
|
return allowEmptyRun ? createEmptyRunResult() : void 0;
|
|
4096
4176
|
}
|
|
@@ -4120,7 +4200,7 @@ const runBrowserController = async (context, options)=>{
|
|
|
4120
4200
|
rootPath: normalize(context.rootPath),
|
|
4121
4201
|
projects: projectRuntimeConfigs,
|
|
4122
4202
|
snapshot: {
|
|
4123
|
-
updateSnapshot: context.snapshotManager.options.updateSnapshot
|
|
4203
|
+
updateSnapshot: options?.updateSnapshot ?? context.snapshotManager.options.updateSnapshot
|
|
4124
4204
|
},
|
|
4125
4205
|
runnerUrl: `http://localhost:${runtime.containerServer.port}`,
|
|
4126
4206
|
projectRunnerUrls,
|
|
@@ -4174,19 +4254,17 @@ const runBrowserController = async (context, options)=>{
|
|
|
4174
4254
|
project.name,
|
|
4175
4255
|
createRunnerEventSink(context, project.normalizedConfig)
|
|
4176
4256
|
]));
|
|
4177
|
-
const
|
|
4178
|
-
|
|
4179
|
-
|
|
4180
|
-
|
|
4181
|
-
const projectName = projectNameByTestPath.get(testPath);
|
|
4182
|
-
return projectName ? sinkForProjectName(projectName) : firstBrowserSink;
|
|
4257
|
+
const sinkForProjectName = (projectName)=>{
|
|
4258
|
+
const sink = runnerSinks.get(projectName);
|
|
4259
|
+
if (!sink) throw new Error(`No runner event sink for project "${projectName}"`);
|
|
4260
|
+
return sink;
|
|
4183
4261
|
};
|
|
4184
4262
|
const silentConsoleController = createSilentConsoleController({
|
|
4185
4263
|
runtimeConfig: {
|
|
4186
4264
|
silent: context.normalizedConfig.silent,
|
|
4187
4265
|
disableConsoleIntercept: context.normalizedConfig.disableConsoleIntercept
|
|
4188
4266
|
},
|
|
4189
|
-
emitInterceptedLog: (log)=>
|
|
4267
|
+
emitInterceptedLog: (log)=>sinkForProjectName(log.project).onConsoleLog(log),
|
|
4190
4268
|
writeOriginalLog: ()=>{}
|
|
4191
4269
|
});
|
|
4192
4270
|
const snapshotRpcMethods = {
|
|
@@ -4214,7 +4292,6 @@ const runBrowserController = async (context, options)=>{
|
|
|
4214
4292
|
}
|
|
4215
4293
|
};
|
|
4216
4294
|
const handleTestFileStart = async (payload)=>{
|
|
4217
|
-
projectNameByTestPath.set(payload.testPath, payload.projectName);
|
|
4218
4295
|
if (phaseTrackers) {
|
|
4219
4296
|
const tracker = new PhaseTracker({
|
|
4220
4297
|
trace: {
|
|
@@ -4224,25 +4301,26 @@ const runBrowserController = async (context, options)=>{
|
|
|
4224
4301
|
pid: nextBrowserFilePid++
|
|
4225
4302
|
});
|
|
4226
4303
|
tracker.transition('prepare');
|
|
4227
|
-
phaseTrackers.set(payload.testPath, tracker);
|
|
4304
|
+
phaseTrackers.set(trackerKey(payload.projectName, payload.testPath), tracker);
|
|
4228
4305
|
}
|
|
4229
4306
|
await sinkForProjectName(payload.projectName).onTestFileStart({
|
|
4230
4307
|
testId: getFileTaskId(payload.testPath),
|
|
4231
4308
|
testPath: payload.testPath,
|
|
4309
|
+
project: payload.projectName,
|
|
4232
4310
|
tests: []
|
|
4233
4311
|
});
|
|
4234
4312
|
};
|
|
4235
4313
|
const handleTestFileReady = async (payload)=>{
|
|
4236
|
-
phaseTrackers?.get(payload.testPath)?.transition('tests');
|
|
4237
|
-
await
|
|
4314
|
+
phaseTrackers?.get(trackerKey(payload.project, payload.testPath))?.transition('tests');
|
|
4315
|
+
await sinkForProjectName(payload.project).onTestFileReady(payload);
|
|
4238
4316
|
};
|
|
4239
4317
|
const handleTestSuiteStart = async (payload)=>{
|
|
4240
|
-
phaseTrackers?.get(payload.testPath)?.recordSuiteStart(payload);
|
|
4241
|
-
await
|
|
4318
|
+
phaseTrackers?.get(trackerKey(payload.project, payload.testPath))?.recordSuiteStart(payload);
|
|
4319
|
+
await sinkForProjectName(payload.project).onTestSuiteStart(payload);
|
|
4242
4320
|
};
|
|
4243
4321
|
const handleTestSuiteResult = async (payload)=>{
|
|
4244
|
-
phaseTrackers?.get(payload.testPath)?.recordSuiteResult(payload);
|
|
4245
|
-
await
|
|
4322
|
+
phaseTrackers?.get(trackerKey(payload.project, payload.testPath))?.recordSuiteResult(payload);
|
|
4323
|
+
await sinkForProjectName(payload.project).onTestSuiteResult(payload);
|
|
4246
4324
|
if ('passed-only' === context.normalizedConfig.silent) silentConsoleController.flushBufferedLogsForTask({
|
|
4247
4325
|
taskId: payload.testId,
|
|
4248
4326
|
status: payload.status,
|
|
@@ -4252,13 +4330,13 @@ const runBrowserController = async (context, options)=>{
|
|
|
4252
4330
|
});
|
|
4253
4331
|
};
|
|
4254
4332
|
const handleTestCaseStart = async (payload)=>{
|
|
4255
|
-
phaseTrackers?.get(payload.testPath)?.recordCaseStart(payload);
|
|
4256
|
-
|
|
4333
|
+
phaseTrackers?.get(trackerKey(payload.project, payload.testPath))?.recordCaseStart(payload);
|
|
4334
|
+
sinkForProjectName(payload.project).onTestCaseStart(payload);
|
|
4257
4335
|
};
|
|
4258
4336
|
const handleTestCaseResult = async (payload)=>{
|
|
4259
4337
|
caseResults.push(payload);
|
|
4260
|
-
phaseTrackers?.get(payload.testPath)?.recordCaseResult(payload);
|
|
4261
|
-
await
|
|
4338
|
+
phaseTrackers?.get(trackerKey(payload.project, payload.testPath))?.recordCaseResult(payload);
|
|
4339
|
+
await sinkForProjectName(payload.project).onTestCaseResult(payload);
|
|
4262
4340
|
if ('passed-only' === context.normalizedConfig.silent) silentConsoleController.flushBufferedLogsForTask({
|
|
4263
4341
|
taskId: payload.testId,
|
|
4264
4342
|
status: payload.status,
|
|
@@ -4273,12 +4351,13 @@ const runBrowserController = async (context, options)=>{
|
|
|
4273
4351
|
payload
|
|
4274
4352
|
], payload.results);
|
|
4275
4353
|
if (phaseTrackers) {
|
|
4276
|
-
const
|
|
4354
|
+
const key = trackerKey(payload.project, payload.testPath);
|
|
4355
|
+
const tracker = phaseTrackers.get(key);
|
|
4277
4356
|
if (tracker) {
|
|
4278
4357
|
tracker.end();
|
|
4279
4358
|
const events = tracker.getTraceEvents();
|
|
4280
4359
|
if (events) onTraceEvents?.(events);
|
|
4281
|
-
phaseTrackers.delete(
|
|
4360
|
+
phaseTrackers.delete(key);
|
|
4282
4361
|
}
|
|
4283
4362
|
}
|
|
4284
4363
|
if ('passed-only' === context.normalizedConfig.silent) silentConsoleController.flushBufferedLogsForTask({
|
|
@@ -4288,7 +4367,7 @@ const runBrowserController = async (context, options)=>{
|
|
|
4288
4367
|
taskType: 'file',
|
|
4289
4368
|
testPath: payload.testPath
|
|
4290
4369
|
});
|
|
4291
|
-
await
|
|
4370
|
+
await sinkForProjectName(payload.project).onTestFileResult(payload);
|
|
4292
4371
|
if (isWatchMode && 'fail' === payload.status) ensureProcessExitCode(1);
|
|
4293
4372
|
};
|
|
4294
4373
|
const handleLog = async (payload)=>{
|
|
@@ -4300,6 +4379,7 @@ const runBrowserController = async (context, options)=>{
|
|
|
4300
4379
|
taskParentNames: payload.taskParentNames,
|
|
4301
4380
|
taskType: payload.taskType,
|
|
4302
4381
|
testPath: payload.testPath,
|
|
4382
|
+
project: payload.projectName,
|
|
4303
4383
|
type: payload.type,
|
|
4304
4384
|
trace: payload.trace
|
|
4305
4385
|
};
|
|
@@ -4461,6 +4541,9 @@ const runBrowserController = async (context, options)=>{
|
|
|
4461
4541
|
});
|
|
4462
4542
|
const inlineOptions = {
|
|
4463
4543
|
...hostOptions,
|
|
4544
|
+
snapshot: {
|
|
4545
|
+
updateSnapshot: context.snapshotManager.options.updateSnapshot
|
|
4546
|
+
},
|
|
4464
4547
|
testFile: file.testPath,
|
|
4465
4548
|
runId: `${run.token}:${session.id}`
|
|
4466
4549
|
};
|
|
@@ -4573,20 +4656,16 @@ const runBrowserController = async (context, options)=>{
|
|
|
4573
4656
|
} finally{
|
|
4574
4657
|
const testTime = Math.max(0, Date.now() - rerunStartTime);
|
|
4575
4658
|
const rerunFatalError = fatalError && fatalError !== fatalErrorBeforeRun ? fatalError : void 0;
|
|
4576
|
-
await
|
|
4577
|
-
|
|
4578
|
-
|
|
4579
|
-
buildTime: 0,
|
|
4580
|
-
testTime
|
|
4581
|
-
},
|
|
4582
|
-
filterRerunTestPaths: files.map((file)=>file.testPath),
|
|
4659
|
+
await finalizeWatchRerun({
|
|
4660
|
+
rerunTestPaths: files.map((file)=>file.testPath),
|
|
4661
|
+
testTime,
|
|
4583
4662
|
unhandledErrors: rerunError ? [
|
|
4584
4663
|
rerunError
|
|
4585
4664
|
] : rerunFatalError ? [
|
|
4586
4665
|
rerunFatalError
|
|
4587
4666
|
] : void 0
|
|
4588
4667
|
});
|
|
4589
|
-
|
|
4668
|
+
logWatchReadyMessage(context, enableCliShortcuts);
|
|
4590
4669
|
}
|
|
4591
4670
|
},
|
|
4592
4671
|
onError: async (error)=>{
|
|
@@ -4600,54 +4679,7 @@ const runBrowserController = async (context, options)=>{
|
|
|
4600
4679
|
logger.debug(`[Headless] Interrupting active run token ${run.token} before scheduling latest rerun`);
|
|
4601
4680
|
}
|
|
4602
4681
|
});
|
|
4603
|
-
|
|
4604
|
-
const duration = {
|
|
4605
|
-
totalTime: buildTime,
|
|
4606
|
-
buildTime,
|
|
4607
|
-
testTime: 0
|
|
4608
|
-
};
|
|
4609
|
-
const result = {
|
|
4610
|
-
results: reporterResults,
|
|
4611
|
-
testResults: caseResults,
|
|
4612
|
-
duration,
|
|
4613
|
-
hasFailure: false,
|
|
4614
|
-
getSourcemap: getBrowserSourcemap,
|
|
4615
|
-
resolveSourcemap: resolveBrowserSourcemap,
|
|
4616
|
-
close: isWatchMode ? void 0 : async ()=>{
|
|
4617
|
-
sessionRegistry.clear();
|
|
4618
|
-
await destroyBrowserRuntime(runtime);
|
|
4619
|
-
}
|
|
4620
|
-
};
|
|
4621
|
-
if (isWatchMode) await notifyTestRunEnd({
|
|
4622
|
-
duration
|
|
4623
|
-
});
|
|
4624
|
-
if (isWatchMode) {
|
|
4625
|
-
triggerRerun = async ()=>{
|
|
4626
|
-
const newProjectEntries = await collectProjectEntries(context);
|
|
4627
|
-
const rerunPlan = planWatchRerun({
|
|
4628
|
-
projectEntries: newProjectEntries,
|
|
4629
|
-
previousTestFiles: watchContext.lastTestFiles,
|
|
4630
|
-
affectedTestFiles: watchContext.affectedTestFiles
|
|
4631
|
-
});
|
|
4632
|
-
watchContext.affectedTestFiles = [];
|
|
4633
|
-
if (rerunPlan.filesChanged) {
|
|
4634
|
-
watchContext.lastTestFiles = rerunPlan.currentTestFiles;
|
|
4635
|
-
if (0 === rerunPlan.currentTestFiles.length) {
|
|
4636
|
-
logger.log(color.cyan('No browser test files remain after update.\n'));
|
|
4637
|
-
logBrowserWatchReadyMessage(enableCliShortcuts);
|
|
4638
|
-
return;
|
|
4639
|
-
}
|
|
4640
|
-
logger.log(color.cyan(`Test file set changed, re-running ${rerunPlan.currentTestFiles.length} file(s)...\n`));
|
|
4641
|
-
latestRerunScheduler.enqueueLatest(rerunPlan.currentTestFiles);
|
|
4642
|
-
return;
|
|
4643
|
-
}
|
|
4644
|
-
logBrowserWatchReadyMessage(enableCliShortcuts);
|
|
4645
|
-
};
|
|
4646
|
-
watchContext.hooksEnabled = true;
|
|
4647
|
-
logBrowserWatchReadyMessage(enableCliShortcuts);
|
|
4648
|
-
}
|
|
4649
|
-
return result;
|
|
4650
|
-
}
|
|
4682
|
+
awaitHeadlessRerunIdle = ()=>latestRerunScheduler.whenIdle();
|
|
4651
4683
|
const testStart = Date.now();
|
|
4652
4684
|
await runFilesWithPool(allTestFiles);
|
|
4653
4685
|
const testTime = Date.now() - testStart;
|
|
@@ -4655,31 +4687,30 @@ const runBrowserController = async (context, options)=>{
|
|
|
4655
4687
|
const newProjectEntries = await collectProjectEntries(context);
|
|
4656
4688
|
const rerunPlan = planWatchRerun({
|
|
4657
4689
|
projectEntries: newProjectEntries,
|
|
4658
|
-
previousTestFiles:
|
|
4659
|
-
affectedTestFiles:
|
|
4690
|
+
previousTestFiles: watchState.lastTestFiles,
|
|
4691
|
+
affectedTestFiles: drainPendingAffectedTestFiles(watchState)
|
|
4660
4692
|
});
|
|
4661
|
-
watchContext.affectedTestFiles = [];
|
|
4662
4693
|
if (rerunPlan.filesChanged) {
|
|
4663
|
-
const deletedTestPaths = collectDeletedTestPaths(
|
|
4694
|
+
const deletedTestPaths = collectDeletedTestPaths(watchState.lastTestFiles, rerunPlan.currentTestFiles);
|
|
4664
4695
|
if (deletedTestPaths.length > 0) context.updateReporterResultState([], [], deletedTestPaths);
|
|
4665
|
-
|
|
4696
|
+
watchState.lastTestFiles = rerunPlan.currentTestFiles;
|
|
4666
4697
|
if (0 === rerunPlan.currentTestFiles.length) {
|
|
4667
4698
|
await latestRerunScheduler.enqueueLatest([]);
|
|
4668
4699
|
logger.log(color.cyan('No browser test files remain after update.\n'));
|
|
4669
|
-
|
|
4700
|
+
logWatchReadyMessage(context, enableCliShortcuts);
|
|
4670
4701
|
return;
|
|
4671
4702
|
}
|
|
4672
4703
|
logger.log(color.cyan(`Test file set changed, re-running ${rerunPlan.currentTestFiles.length} file(s)...\n`));
|
|
4673
|
-
latestRerunScheduler.enqueueLatest(rerunPlan.currentTestFiles);
|
|
4704
|
+
await latestRerunScheduler.enqueueLatest(rerunPlan.currentTestFiles);
|
|
4674
4705
|
return;
|
|
4675
4706
|
}
|
|
4676
4707
|
if (0 === rerunPlan.affectedTestFiles.length) {
|
|
4677
4708
|
logger.log(color.cyan('No affected browser test files detected, skipping re-run.\n'));
|
|
4678
|
-
|
|
4709
|
+
logWatchReadyMessage(context, enableCliShortcuts);
|
|
4679
4710
|
return;
|
|
4680
4711
|
}
|
|
4681
4712
|
logger.log(color.cyan(`Re-running ${rerunPlan.affectedTestFiles.length} affected test file(s)...\n`));
|
|
4682
|
-
latestRerunScheduler.enqueueLatest(rerunPlan.affectedTestFiles);
|
|
4713
|
+
await latestRerunScheduler.enqueueLatest(rerunPlan.affectedTestFiles);
|
|
4683
4714
|
};
|
|
4684
4715
|
const closeHeadlessRuntime = isWatchMode ? void 0 : async ()=>{
|
|
4685
4716
|
sessionRegistry.clear();
|
|
@@ -4694,6 +4725,7 @@ const runBrowserController = async (context, options)=>{
|
|
|
4694
4725
|
context.updateReporterResultState(reporterResults, caseResults);
|
|
4695
4726
|
const isFailure = reporterResults.some((result)=>'fail' === result.status);
|
|
4696
4727
|
if (isWatchMode && isFailure) ensureProcessExitCode(1);
|
|
4728
|
+
const cycleCoverageMap = isWatchMode && coverageProvider ? buildBrowserCoverageMap(reporterResults, coverageProvider) : void 0;
|
|
4697
4729
|
const result = {
|
|
4698
4730
|
results: reporterResults,
|
|
4699
4731
|
testResults: caseResults,
|
|
@@ -4701,18 +4733,21 @@ const runBrowserController = async (context, options)=>{
|
|
|
4701
4733
|
hasFailure: isFailure,
|
|
4702
4734
|
getSourcemap: getBrowserSourcemap,
|
|
4703
4735
|
resolveSourcemap: resolveBrowserSourcemap,
|
|
4704
|
-
close: closeHeadlessRuntime
|
|
4736
|
+
close: closeHeadlessRuntime,
|
|
4737
|
+
coverage: cycleCoverageMap,
|
|
4738
|
+
watch: watchHandles
|
|
4705
4739
|
};
|
|
4706
4740
|
if (isWatchMode) try {
|
|
4707
4741
|
await notifyTestRunEnd({
|
|
4708
|
-
duration
|
|
4742
|
+
duration,
|
|
4743
|
+
coverage: cycleCoverageMap?.files().length ? cycleCoverageMap.toJSON() : void 0
|
|
4709
4744
|
});
|
|
4710
4745
|
} finally{
|
|
4711
4746
|
await closeHeadlessRuntime?.();
|
|
4712
4747
|
}
|
|
4713
4748
|
if (isWatchMode && triggerRerun) {
|
|
4714
|
-
|
|
4715
|
-
|
|
4749
|
+
watchState.hooksEnabled = true;
|
|
4750
|
+
logWatchReadyMessage(context, enableCliShortcuts);
|
|
4716
4751
|
}
|
|
4717
4752
|
return result;
|
|
4718
4753
|
}
|
|
@@ -4919,22 +4954,31 @@ const runBrowserController = async (context, options)=>{
|
|
|
4919
4954
|
testTime = Date.now() - testStart;
|
|
4920
4955
|
}
|
|
4921
4956
|
if (isWatchMode) triggerRerun = async ()=>{
|
|
4922
|
-
const
|
|
4957
|
+
const refreshedHostOptions = {
|
|
4958
|
+
...hostOptions,
|
|
4959
|
+
snapshot: {
|
|
4960
|
+
updateSnapshot: context.snapshotManager.options.updateSnapshot
|
|
4961
|
+
}
|
|
4962
|
+
};
|
|
4963
|
+
runtime.setContainerOptions(refreshedHostOptions);
|
|
4964
|
+
const [, newProjectEntries] = await Promise.all([
|
|
4965
|
+
rpcManager.updateHostConfig(refreshedHostOptions),
|
|
4966
|
+
collectProjectEntries(context)
|
|
4967
|
+
]);
|
|
4923
4968
|
const rerunPlan = planWatchRerun({
|
|
4924
4969
|
projectEntries: newProjectEntries,
|
|
4925
|
-
previousTestFiles:
|
|
4926
|
-
affectedTestFiles:
|
|
4970
|
+
previousTestFiles: watchState.lastTestFiles,
|
|
4971
|
+
affectedTestFiles: drainPendingAffectedTestFiles(watchState)
|
|
4927
4972
|
});
|
|
4928
|
-
watchContext.affectedTestFiles = [];
|
|
4929
4973
|
if (rerunPlan.filesChanged) {
|
|
4930
|
-
const deletedTestPaths = collectDeletedTestPaths(
|
|
4974
|
+
const deletedTestPaths = collectDeletedTestPaths(watchState.lastTestFiles, rerunPlan.currentTestFiles);
|
|
4931
4975
|
if (deletedTestPaths.length > 0) context.updateReporterResultState([], [], deletedTestPaths);
|
|
4932
|
-
|
|
4976
|
+
watchState.lastTestFiles = rerunPlan.currentTestFiles;
|
|
4933
4977
|
currentTestFiles = rerunPlan.currentTestFiles;
|
|
4934
4978
|
await rpcManager.notifyTestFileUpdate(currentTestFiles);
|
|
4935
4979
|
if (0 === currentTestFiles.length) {
|
|
4936
4980
|
logger.log(color.cyan('No browser test files remain after update.\n'));
|
|
4937
|
-
|
|
4981
|
+
logWatchReadyMessage(context, enableCliShortcuts);
|
|
4938
4982
|
return;
|
|
4939
4983
|
}
|
|
4940
4984
|
await waitForRunnerFramesReady(currentTestFiles.map((file)=>file.testPath));
|
|
@@ -4954,25 +4998,21 @@ const runBrowserController = async (context, options)=>{
|
|
|
4954
4998
|
} finally{
|
|
4955
4999
|
const testTime = Math.max(0, Date.now() - rerunStartTime);
|
|
4956
5000
|
const rerunFatalError = fatalError && fatalError !== fatalErrorBeforeRun ? fatalError : void 0;
|
|
4957
|
-
await
|
|
4958
|
-
|
|
4959
|
-
|
|
4960
|
-
buildTime: 0,
|
|
4961
|
-
testTime
|
|
4962
|
-
},
|
|
4963
|
-
filterRerunTestPaths: rerunPlan.normalizedAffectedTestFiles,
|
|
5001
|
+
await finalizeWatchRerun({
|
|
5002
|
+
rerunTestPaths: rerunPlan.normalizedAffectedTestFiles,
|
|
5003
|
+
testTime,
|
|
4964
5004
|
unhandledErrors: rerunError ? [
|
|
4965
5005
|
rerunError
|
|
4966
5006
|
] : rerunFatalError ? [
|
|
4967
5007
|
rerunFatalError
|
|
4968
5008
|
] : void 0
|
|
4969
5009
|
});
|
|
4970
|
-
|
|
5010
|
+
logWatchReadyMessage(context, enableCliShortcuts);
|
|
4971
5011
|
}
|
|
4972
|
-
} else if (rerunPlan.filesChanged)
|
|
5012
|
+
} else if (rerunPlan.filesChanged) logWatchReadyMessage(context, enableCliShortcuts);
|
|
4973
5013
|
else {
|
|
4974
5014
|
logger.log(color.cyan('Tests will be re-executed automatically\n'));
|
|
4975
|
-
|
|
5015
|
+
logWatchReadyMessage(context, enableCliShortcuts);
|
|
4976
5016
|
}
|
|
4977
5017
|
};
|
|
4978
5018
|
const closeContainerRuntime = isWatchMode ? void 0 : async ()=>{
|
|
@@ -4993,6 +5033,7 @@ const runBrowserController = async (context, options)=>{
|
|
|
4993
5033
|
context.updateReporterResultState(reporterResults, caseResults);
|
|
4994
5034
|
const isFailure = reporterResults.some((result)=>'fail' === result.status);
|
|
4995
5035
|
if (isWatchMode && isFailure) ensureProcessExitCode(1);
|
|
5036
|
+
const cycleCoverageMap = isWatchMode && coverageProvider ? buildBrowserCoverageMap(reporterResults, coverageProvider) : void 0;
|
|
4996
5037
|
const result = {
|
|
4997
5038
|
results: reporterResults,
|
|
4998
5039
|
testResults: caseResults,
|
|
@@ -5000,18 +5041,21 @@ const runBrowserController = async (context, options)=>{
|
|
|
5000
5041
|
hasFailure: isFailure,
|
|
5001
5042
|
getSourcemap: getBrowserSourcemap,
|
|
5002
5043
|
resolveSourcemap: resolveBrowserSourcemap,
|
|
5003
|
-
close: closeContainerRuntime
|
|
5044
|
+
close: closeContainerRuntime,
|
|
5045
|
+
coverage: cycleCoverageMap,
|
|
5046
|
+
watch: watchHandles
|
|
5004
5047
|
};
|
|
5005
5048
|
if (isWatchMode) try {
|
|
5006
5049
|
await notifyTestRunEnd({
|
|
5007
|
-
duration
|
|
5050
|
+
duration,
|
|
5051
|
+
coverage: cycleCoverageMap?.files().length ? cycleCoverageMap.toJSON() : void 0
|
|
5008
5052
|
});
|
|
5009
5053
|
} finally{
|
|
5010
5054
|
await closeContainerRuntime?.();
|
|
5011
5055
|
}
|
|
5012
5056
|
if (isWatchMode && triggerRerun) {
|
|
5013
|
-
|
|
5014
|
-
|
|
5057
|
+
watchState.hooksEnabled = true;
|
|
5058
|
+
logWatchReadyMessage(context, enableCliShortcuts);
|
|
5015
5059
|
}
|
|
5016
5060
|
return result;
|
|
5017
5061
|
};
|
|
@@ -5204,7 +5248,7 @@ const emptyOutcome = ()=>({
|
|
|
5204
5248
|
}
|
|
5205
5249
|
});
|
|
5206
5250
|
async function createBrowserExecutor(context, options) {
|
|
5207
|
-
const { projects, coverageProvider, freezeShardedEntries, filesOnly, allowEmptyRun, appliedModifyRstestConfigEnvironments } = options;
|
|
5251
|
+
const { projects, coverageProvider, shardedEntries, freezeShardedEntries, filesOnly, allowEmptyRun, appliedModifyRstestConfigEnvironments } = options;
|
|
5208
5252
|
let deferredClose;
|
|
5209
5253
|
let inFlightCycle;
|
|
5210
5254
|
const foldOutcome = (result)=>{
|
|
@@ -5232,12 +5276,13 @@ async function createBrowserExecutor(context, options) {
|
|
|
5232
5276
|
async runCycle (opts) {
|
|
5233
5277
|
const cycle = runBrowserController(context, {
|
|
5234
5278
|
projects,
|
|
5235
|
-
shardedEntries
|
|
5279
|
+
shardedEntries,
|
|
5236
5280
|
freezeShardedEntries,
|
|
5237
5281
|
allowEmptyRun,
|
|
5238
5282
|
appliedModifyRstestConfigEnvironments,
|
|
5239
5283
|
onTraceEvents: opts.onTraceEvents,
|
|
5240
|
-
env: opts.env
|
|
5284
|
+
env: opts.env,
|
|
5285
|
+
updateSnapshot: opts.updateSnapshot
|
|
5241
5286
|
});
|
|
5242
5287
|
inFlightCycle = cycle;
|
|
5243
5288
|
try {
|
|
@@ -5248,10 +5293,10 @@ async function createBrowserExecutor(context, options) {
|
|
|
5248
5293
|
inFlightCycle = void 0;
|
|
5249
5294
|
}
|
|
5250
5295
|
},
|
|
5251
|
-
async collect (
|
|
5296
|
+
async collect () {
|
|
5252
5297
|
const pending = listBrowserTests(context, {
|
|
5253
5298
|
projects,
|
|
5254
|
-
shardedEntries
|
|
5299
|
+
shardedEntries,
|
|
5255
5300
|
freezeShardedEntries,
|
|
5256
5301
|
filesOnly,
|
|
5257
5302
|
appliedModifyRstestConfigEnvironments
|