@rstest/browser 0.9.0 → 0.9.2
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/{101.36a8ccdf84.js → 101.82cdbbe145.js} +852 -824
- package/dist/browser-container/container-static/js/101.82cdbbe145.js.LICENSE.txt +1 -0
- package/dist/browser-container/container-static/js/{index.0687a8142a.js → index.602d6770fe.js} +91 -84
- package/dist/browser-container/container-static/js/{lib-react.dcf2a5e57a.js → lib-react.ce60b6aea5.js} +3 -3
- package/dist/browser-container/container-static/js/lib-react.ce60b6aea5.js.LICENSE.txt +1 -0
- package/dist/browser-container/index.html +1 -1
- package/dist/browser.js +1 -1
- package/dist/headedSerialTaskQueue.d.ts +8 -0
- package/dist/hostController.d.ts +16 -0
- package/dist/index.js +219 -47
- package/dist/watchCliShortcuts.d.ts +6 -0
- package/package.json +7 -7
- package/src/headedSerialTaskQueue.ts +19 -0
- package/src/hostController.ts +294 -65
- package/src/watchCliShortcuts.ts +77 -0
- package/dist/browser-container/container-static/js/101.36a8ccdf84.js.LICENSE.txt +0 -1
- package/dist/browser-container/container-static/js/lib-react.dcf2a5e57a.js.LICENSE.txt +0 -1
- /package/dist/{361.js → 323.js} +0 -0
package/src/hostController.ts
CHANGED
|
@@ -3,6 +3,7 @@ import fs from 'node:fs/promises';
|
|
|
3
3
|
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
4
4
|
import type { AddressInfo } from 'node:net';
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import type { Rspack } from '@rstest/core';
|
|
6
7
|
import {
|
|
7
8
|
type BrowserTestRunOptions,
|
|
8
9
|
type BrowserTestRunResult,
|
|
@@ -37,6 +38,7 @@ import {
|
|
|
37
38
|
createHostDispatchRouter,
|
|
38
39
|
type HostDispatchRouterOptions,
|
|
39
40
|
} from './dispatchCapabilities';
|
|
41
|
+
import { createHeadedSerialTaskQueue } from './headedSerialTaskQueue';
|
|
40
42
|
import { createHeadlessLatestRerunScheduler } from './headlessLatestRerunScheduler';
|
|
41
43
|
import { attachHeadlessRunnerTransport } from './headlessTransport';
|
|
42
44
|
import type {
|
|
@@ -76,6 +78,11 @@ import {
|
|
|
76
78
|
type SourceMapPayload,
|
|
77
79
|
} from './sourceMap/sourceMapLoader';
|
|
78
80
|
import { resolveBrowserViewportPreset } from './viewportPresets';
|
|
81
|
+
import {
|
|
82
|
+
isBrowserWatchCliShortcutsEnabled,
|
|
83
|
+
logBrowserWatchReadyMessage,
|
|
84
|
+
setupBrowserWatchCliShortcuts,
|
|
85
|
+
} from './watchCliShortcuts';
|
|
79
86
|
import { collectWatchTestFiles, planWatchRerun } from './watchRerunPlanner';
|
|
80
87
|
|
|
81
88
|
const { createRsbuild, rspack } = rsbuild;
|
|
@@ -155,6 +162,7 @@ type TestCaseStartPayload = ReporterHookArg<'onTestCaseStart'>;
|
|
|
155
162
|
type HostRpcMethods = {
|
|
156
163
|
rerunTest: (testFile: string, testNamePattern?: string) => Promise<void>;
|
|
157
164
|
getTestFiles: () => Promise<TestFileInfo[]>;
|
|
165
|
+
onRunnerFramesReady: (testFiles: string[]) => Promise<void>;
|
|
158
166
|
// Test result callbacks from container
|
|
159
167
|
onTestFileStart: (payload: TestFileStartPayload) => Promise<void>;
|
|
160
168
|
onTestCaseResult: (payload: TestResult) => Promise<void>;
|
|
@@ -314,6 +322,8 @@ type WatchContext = {
|
|
|
314
322
|
lastTestFiles: TestFileInfo[];
|
|
315
323
|
hooksEnabled: boolean;
|
|
316
324
|
cleanupRegistered: boolean;
|
|
325
|
+
cleanupPromise: Promise<void> | null;
|
|
326
|
+
closeCliShortcuts: (() => void) | null;
|
|
317
327
|
chunkHashes: Map<string, string>;
|
|
318
328
|
affectedTestFiles: string[];
|
|
319
329
|
};
|
|
@@ -323,6 +333,8 @@ const watchContext: WatchContext = {
|
|
|
323
333
|
lastTestFiles: [],
|
|
324
334
|
hooksEnabled: false,
|
|
325
335
|
cleanupRegistered: false,
|
|
336
|
+
cleanupPromise: null,
|
|
337
|
+
closeCliShortcuts: null,
|
|
326
338
|
chunkHashes: new Map(),
|
|
327
339
|
affectedTestFiles: [],
|
|
328
340
|
};
|
|
@@ -378,6 +390,81 @@ const ensureProcessExitCode = (code: number): void => {
|
|
|
378
390
|
}
|
|
379
391
|
};
|
|
380
392
|
|
|
393
|
+
const castArray = <T>(arr?: T | T[]): T[] => {
|
|
394
|
+
if (arr === undefined) {
|
|
395
|
+
return [];
|
|
396
|
+
}
|
|
397
|
+
return Array.isArray(arr) ? arr : [arr];
|
|
398
|
+
};
|
|
399
|
+
|
|
400
|
+
const applyDefaultWatchOptions = (
|
|
401
|
+
rspackConfig: Rspack.Configuration,
|
|
402
|
+
isWatchMode: boolean,
|
|
403
|
+
) => {
|
|
404
|
+
rspackConfig.watchOptions ??= {};
|
|
405
|
+
|
|
406
|
+
if (!isWatchMode) {
|
|
407
|
+
rspackConfig.watchOptions.ignored = '**/**';
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
rspackConfig.watchOptions.ignored = castArray(
|
|
412
|
+
rspackConfig.watchOptions.ignored || [],
|
|
413
|
+
) as string[];
|
|
414
|
+
|
|
415
|
+
if (rspackConfig.watchOptions.ignored.length === 0) {
|
|
416
|
+
rspackConfig.watchOptions.ignored.push('**/.git', '**/node_modules');
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
rspackConfig.output?.path &&
|
|
420
|
+
rspackConfig.watchOptions.ignored.push(rspackConfig.output.path);
|
|
421
|
+
};
|
|
422
|
+
|
|
423
|
+
type LazyCompilationModule = {
|
|
424
|
+
nameForCondition?: () => string | null | undefined;
|
|
425
|
+
};
|
|
426
|
+
|
|
427
|
+
type BrowserLazyCompilationConfig = {
|
|
428
|
+
imports: true;
|
|
429
|
+
entries: false;
|
|
430
|
+
test?: (module: LazyCompilationModule) => boolean;
|
|
431
|
+
};
|
|
432
|
+
|
|
433
|
+
export const createBrowserLazyCompilationConfig = (
|
|
434
|
+
setupFiles: string[],
|
|
435
|
+
): BrowserLazyCompilationConfig => {
|
|
436
|
+
const eagerSetupFiles = new Set(
|
|
437
|
+
setupFiles.map((filePath) => normalize(filePath)),
|
|
438
|
+
);
|
|
439
|
+
|
|
440
|
+
if (eagerSetupFiles.size === 0) {
|
|
441
|
+
return {
|
|
442
|
+
imports: true,
|
|
443
|
+
entries: false,
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
return {
|
|
448
|
+
imports: true,
|
|
449
|
+
entries: false,
|
|
450
|
+
test(module: LazyCompilationModule) {
|
|
451
|
+
const filePath = module.nameForCondition?.();
|
|
452
|
+
return !filePath || !eagerSetupFiles.has(normalize(filePath));
|
|
453
|
+
},
|
|
454
|
+
};
|
|
455
|
+
};
|
|
456
|
+
|
|
457
|
+
export const createBrowserRsbuildDevConfig = (isWatchMode: boolean) => {
|
|
458
|
+
return {
|
|
459
|
+
// Disable HMR in non-watch mode (tests run once and exit).
|
|
460
|
+
// Aligns with node mode behavior (packages/core/src/core/rsbuild.ts).
|
|
461
|
+
hmr: isWatchMode,
|
|
462
|
+
client: {
|
|
463
|
+
logLevel: 'error' as const,
|
|
464
|
+
},
|
|
465
|
+
};
|
|
466
|
+
};
|
|
467
|
+
|
|
381
468
|
/**
|
|
382
469
|
* Convert a single glob pattern to RegExp using picomatch
|
|
383
470
|
* Based on Storybook's implementation
|
|
@@ -920,27 +1007,39 @@ const destroyBrowserRuntime = async (
|
|
|
920
1007
|
.catch(() => {});
|
|
921
1008
|
};
|
|
922
1009
|
|
|
923
|
-
const
|
|
924
|
-
if (watchContext.
|
|
925
|
-
return;
|
|
1010
|
+
const cleanupWatchRuntime = (): Promise<void> => {
|
|
1011
|
+
if (watchContext.cleanupPromise) {
|
|
1012
|
+
return watchContext.cleanupPromise;
|
|
926
1013
|
}
|
|
927
1014
|
|
|
928
|
-
|
|
1015
|
+
watchContext.cleanupPromise = (async () => {
|
|
1016
|
+
watchContext.closeCliShortcuts?.();
|
|
1017
|
+
watchContext.closeCliShortcuts = null;
|
|
1018
|
+
|
|
929
1019
|
if (!watchContext.runtime) {
|
|
930
1020
|
return;
|
|
931
1021
|
}
|
|
1022
|
+
|
|
932
1023
|
await destroyBrowserRuntime(watchContext.runtime);
|
|
933
1024
|
watchContext.runtime = null;
|
|
934
|
-
};
|
|
1025
|
+
})();
|
|
1026
|
+
|
|
1027
|
+
return watchContext.cleanupPromise;
|
|
1028
|
+
};
|
|
1029
|
+
|
|
1030
|
+
const registerWatchCleanup = (): void => {
|
|
1031
|
+
if (watchContext.cleanupRegistered) {
|
|
1032
|
+
return;
|
|
1033
|
+
}
|
|
935
1034
|
|
|
936
|
-
for (const signal of ['SIGINT', 'SIGTERM'] as const) {
|
|
1035
|
+
for (const signal of ['SIGINT', 'SIGTERM', 'SIGTSTP'] as const) {
|
|
937
1036
|
process.once(signal, () => {
|
|
938
|
-
void
|
|
1037
|
+
void cleanupWatchRuntime();
|
|
939
1038
|
});
|
|
940
1039
|
}
|
|
941
1040
|
|
|
942
1041
|
process.once('exit', () => {
|
|
943
|
-
void
|
|
1042
|
+
void cleanupWatchRuntime();
|
|
944
1043
|
});
|
|
945
1044
|
|
|
946
1045
|
watchContext.cleanupRegistered = true;
|
|
@@ -1029,11 +1128,7 @@ const createBrowserRuntime = async ({
|
|
|
1029
1128
|
port: browserLaunchOptions.port ?? 4000,
|
|
1030
1129
|
strictPort: browserLaunchOptions.strictPort,
|
|
1031
1130
|
},
|
|
1032
|
-
dev:
|
|
1033
|
-
client: {
|
|
1034
|
-
logLevel: 'error',
|
|
1035
|
-
},
|
|
1036
|
-
},
|
|
1131
|
+
dev: createBrowserRsbuildDevConfig(isWatchMode),
|
|
1037
1132
|
environments: {
|
|
1038
1133
|
...Object.fromEntries(
|
|
1039
1134
|
browserProjects.map((project) => [project.environmentName, {}]),
|
|
@@ -1069,6 +1164,12 @@ const createBrowserRuntime = async ({
|
|
|
1069
1164
|
}
|
|
1070
1165
|
|
|
1071
1166
|
const userRsbuildConfig = project.normalizedConfig;
|
|
1167
|
+
const setupFiles = Object.values(
|
|
1168
|
+
getSetupFiles(
|
|
1169
|
+
project.normalizedConfig.setupFiles,
|
|
1170
|
+
project.rootPath,
|
|
1171
|
+
),
|
|
1172
|
+
);
|
|
1072
1173
|
// Merge order: current config -> userConfig -> rstest required config (highest priority)
|
|
1073
1174
|
const merged = mergeEnvironmentConfig(config, userRsbuildConfig, {
|
|
1074
1175
|
resolve: {
|
|
@@ -1090,13 +1191,13 @@ const createBrowserRuntime = async ({
|
|
|
1090
1191
|
tools: {
|
|
1091
1192
|
rspack: (rspackConfig) => {
|
|
1092
1193
|
rspackConfig.mode = 'development';
|
|
1093
|
-
rspackConfig.lazyCompilation =
|
|
1094
|
-
|
|
1095
|
-
entries: false,
|
|
1096
|
-
};
|
|
1194
|
+
rspackConfig.lazyCompilation =
|
|
1195
|
+
createBrowserLazyCompilationConfig(setupFiles);
|
|
1097
1196
|
rspackConfig.plugins = rspackConfig.plugins || [];
|
|
1098
1197
|
rspackConfig.plugins.push(virtualManifestPlugin);
|
|
1099
1198
|
|
|
1199
|
+
applyDefaultWatchOptions(rspackConfig, isWatchMode);
|
|
1200
|
+
|
|
1100
1201
|
// Extract and merge sourcemaps from pre-built @rstest/core files
|
|
1101
1202
|
// This preserves the sourcemap chain for inline snapshot support
|
|
1102
1203
|
// See: https://rspack.dev/config/module-rules#rulesextractsourcemap
|
|
@@ -1638,6 +1739,7 @@ export const runBrowserController = async (
|
|
|
1638
1739
|
await notifyTestRunStart();
|
|
1639
1740
|
|
|
1640
1741
|
const isWatchMode = context.command === 'watch';
|
|
1742
|
+
const enableCliShortcuts = isWatchMode && isBrowserWatchCliShortcutsEnabled();
|
|
1641
1743
|
const tempDir =
|
|
1642
1744
|
isWatchMode && watchContext.runtime
|
|
1643
1745
|
? watchContext.runtime.tempDir
|
|
@@ -1691,6 +1793,12 @@ export const runBrowserController = async (
|
|
|
1691
1793
|
if (isWatchMode) {
|
|
1692
1794
|
watchContext.runtime = runtime;
|
|
1693
1795
|
registerWatchCleanup();
|
|
1796
|
+
|
|
1797
|
+
if (enableCliShortcuts && !watchContext.closeCliShortcuts) {
|
|
1798
|
+
watchContext.closeCliShortcuts = await setupBrowserWatchCliShortcuts({
|
|
1799
|
+
close: cleanupWatchRuntime,
|
|
1800
|
+
});
|
|
1801
|
+
}
|
|
1694
1802
|
}
|
|
1695
1803
|
}
|
|
1696
1804
|
|
|
@@ -2331,6 +2439,7 @@ export const runBrowserController = async (
|
|
|
2331
2439
|
? [rerunFatalError]
|
|
2332
2440
|
: undefined,
|
|
2333
2441
|
});
|
|
2442
|
+
logBrowserWatchReadyMessage(enableCliShortcuts);
|
|
2334
2443
|
}
|
|
2335
2444
|
},
|
|
2336
2445
|
onError: async (error) => {
|
|
@@ -2375,6 +2484,7 @@ export const runBrowserController = async (
|
|
|
2375
2484
|
logger.log(
|
|
2376
2485
|
color.cyan('No browser test files remain after update.\n'),
|
|
2377
2486
|
);
|
|
2487
|
+
logBrowserWatchReadyMessage(enableCliShortcuts);
|
|
2378
2488
|
return;
|
|
2379
2489
|
}
|
|
2380
2490
|
|
|
@@ -2393,6 +2503,7 @@ export const runBrowserController = async (
|
|
|
2393
2503
|
'No affected browser test files detected, skipping re-run.\n',
|
|
2394
2504
|
),
|
|
2395
2505
|
);
|
|
2506
|
+
logBrowserWatchReadyMessage(enableCliShortcuts);
|
|
2396
2507
|
return;
|
|
2397
2508
|
}
|
|
2398
2509
|
|
|
@@ -2451,23 +2562,104 @@ export const runBrowserController = async (
|
|
|
2451
2562
|
|
|
2452
2563
|
if (isWatchMode && triggerRerun) {
|
|
2453
2564
|
watchContext.hooksEnabled = true;
|
|
2454
|
-
|
|
2455
|
-
color.cyan(
|
|
2456
|
-
'\nWatch mode enabled - will re-run tests on file changes\n',
|
|
2457
|
-
),
|
|
2458
|
-
);
|
|
2565
|
+
logBrowserWatchReadyMessage(enableCliShortcuts);
|
|
2459
2566
|
}
|
|
2460
2567
|
|
|
2461
2568
|
return result;
|
|
2462
2569
|
}
|
|
2463
2570
|
|
|
2464
|
-
let
|
|
2571
|
+
let currentTestFiles = allTestFiles;
|
|
2572
|
+
const RUNNER_FRAMES_READY_TIMEOUT_MS = 30_000;
|
|
2573
|
+
let currentRunnerFramesSignature: string | null = null;
|
|
2574
|
+
const runnerFramesWaiters = new Map<string, Set<() => void>>();
|
|
2465
2575
|
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2576
|
+
const createTestFilesSignature = (testFiles: readonly string[]): string => {
|
|
2577
|
+
return JSON.stringify(testFiles.map((testFile) => normalize(testFile)));
|
|
2578
|
+
};
|
|
2579
|
+
|
|
2580
|
+
const markRunnerFramesReady = (testFiles: string[]): void => {
|
|
2581
|
+
const signature = createTestFilesSignature(testFiles);
|
|
2582
|
+
currentRunnerFramesSignature = signature;
|
|
2583
|
+
const waiters = runnerFramesWaiters.get(signature);
|
|
2584
|
+
if (!waiters) {
|
|
2585
|
+
return;
|
|
2586
|
+
}
|
|
2587
|
+
runnerFramesWaiters.delete(signature);
|
|
2588
|
+
for (const waiter of waiters) {
|
|
2589
|
+
waiter();
|
|
2590
|
+
}
|
|
2591
|
+
};
|
|
2592
|
+
|
|
2593
|
+
const waitForRunnerFramesReady = async (
|
|
2594
|
+
testFiles: readonly string[],
|
|
2595
|
+
): Promise<void> => {
|
|
2596
|
+
const signature = createTestFilesSignature(testFiles);
|
|
2597
|
+
if (currentRunnerFramesSignature === signature) {
|
|
2598
|
+
return;
|
|
2599
|
+
}
|
|
2600
|
+
|
|
2601
|
+
await new Promise<void>((resolve, reject) => {
|
|
2602
|
+
const waiters =
|
|
2603
|
+
runnerFramesWaiters.get(signature) ?? new Set<() => void>();
|
|
2604
|
+
let timeoutId: ReturnType<typeof setTimeout> | undefined;
|
|
2605
|
+
|
|
2606
|
+
const cleanup = () => {
|
|
2607
|
+
const currentWaiters = runnerFramesWaiters.get(signature);
|
|
2608
|
+
if (!currentWaiters) {
|
|
2609
|
+
return;
|
|
2610
|
+
}
|
|
2611
|
+
currentWaiters.delete(onReady);
|
|
2612
|
+
if (currentWaiters.size === 0) {
|
|
2613
|
+
runnerFramesWaiters.delete(signature);
|
|
2614
|
+
}
|
|
2615
|
+
};
|
|
2616
|
+
|
|
2617
|
+
const onReady = () => {
|
|
2618
|
+
if (timeoutId) {
|
|
2619
|
+
clearTimeout(timeoutId);
|
|
2620
|
+
}
|
|
2621
|
+
cleanup();
|
|
2622
|
+
resolve();
|
|
2623
|
+
};
|
|
2624
|
+
|
|
2625
|
+
timeoutId = setTimeout(() => {
|
|
2626
|
+
cleanup();
|
|
2627
|
+
reject(
|
|
2628
|
+
new Error(
|
|
2629
|
+
`Timed out waiting for headed runner frames to be ready for ${testFiles.length} file(s).`,
|
|
2630
|
+
),
|
|
2631
|
+
);
|
|
2632
|
+
}, RUNNER_FRAMES_READY_TIMEOUT_MS);
|
|
2633
|
+
|
|
2634
|
+
waiters.add(onReady);
|
|
2635
|
+
runnerFramesWaiters.set(signature, waiters);
|
|
2636
|
+
|
|
2637
|
+
if (currentRunnerFramesSignature === signature) {
|
|
2638
|
+
onReady();
|
|
2639
|
+
}
|
|
2640
|
+
});
|
|
2641
|
+
};
|
|
2642
|
+
|
|
2643
|
+
const getTestFileInfo = (testFile: string): TestFileInfo => {
|
|
2644
|
+
const normalizedTestFile = normalize(testFile);
|
|
2645
|
+
const fileInfo = currentTestFiles.find(
|
|
2646
|
+
(file) => file.testPath === normalizedTestFile,
|
|
2647
|
+
);
|
|
2648
|
+
if (!fileInfo) {
|
|
2649
|
+
throw new Error(`Unknown browser test file: ${JSON.stringify(testFile)}`);
|
|
2650
|
+
}
|
|
2651
|
+
return fileInfo;
|
|
2652
|
+
};
|
|
2653
|
+
|
|
2654
|
+
const getHeadedPerFileTimeoutMs = (file: TestFileInfo): number => {
|
|
2655
|
+
const projectRuntime = projectRuntimeConfigs.find(
|
|
2656
|
+
(project) => project.name === file.projectName,
|
|
2657
|
+
);
|
|
2658
|
+
return (
|
|
2659
|
+
(projectRuntime?.runtimeConfig.testTimeout ?? maxTestTimeoutForRpc) +
|
|
2660
|
+
30_000
|
|
2661
|
+
);
|
|
2662
|
+
};
|
|
2471
2663
|
|
|
2472
2664
|
// Open a container page for user to view (reuse in watch mode)
|
|
2473
2665
|
let containerContext: BrowserProviderContext;
|
|
@@ -2513,6 +2705,40 @@ export const runBrowserController = async (
|
|
|
2513
2705
|
activeContainerPage = containerPage;
|
|
2514
2706
|
|
|
2515
2707
|
const dispatchRouter = createDispatchRouter();
|
|
2708
|
+
const headedReloadQueue = createHeadedSerialTaskQueue();
|
|
2709
|
+
let enqueueHeadedReload = async (
|
|
2710
|
+
_file: TestFileInfo,
|
|
2711
|
+
_testNamePattern?: string,
|
|
2712
|
+
): Promise<void> => {
|
|
2713
|
+
throw new Error('Headed reload queue is not initialized');
|
|
2714
|
+
};
|
|
2715
|
+
|
|
2716
|
+
const reloadTestFileWithTimeout = async (
|
|
2717
|
+
file: TestFileInfo,
|
|
2718
|
+
testNamePattern?: string,
|
|
2719
|
+
): Promise<void> => {
|
|
2720
|
+
const timeoutMs = getHeadedPerFileTimeoutMs(file);
|
|
2721
|
+
let timeoutId: ReturnType<typeof setTimeout> | undefined;
|
|
2722
|
+
|
|
2723
|
+
try {
|
|
2724
|
+
await Promise.race([
|
|
2725
|
+
rpcManager.reloadTestFile(file.testPath, testNamePattern),
|
|
2726
|
+
new Promise<never>((_, reject) => {
|
|
2727
|
+
timeoutId = setTimeout(() => {
|
|
2728
|
+
reject(
|
|
2729
|
+
new Error(
|
|
2730
|
+
`Headed test execution timeout after ${timeoutMs / 1000}s for ${file.testPath}.`,
|
|
2731
|
+
),
|
|
2732
|
+
);
|
|
2733
|
+
}, timeoutMs);
|
|
2734
|
+
}),
|
|
2735
|
+
]);
|
|
2736
|
+
} finally {
|
|
2737
|
+
if (timeoutId) {
|
|
2738
|
+
clearTimeout(timeoutId);
|
|
2739
|
+
}
|
|
2740
|
+
}
|
|
2741
|
+
};
|
|
2516
2742
|
|
|
2517
2743
|
// Create RPC methods that can access test state variables
|
|
2518
2744
|
const createRpcMethods = (): HostRpcMethods => ({
|
|
@@ -2525,10 +2751,13 @@ export const runBrowserController = async (
|
|
|
2525
2751
|
`\nRe-running test: ${displayPath}${testNamePattern ? ` (pattern: ${testNamePattern})` : ''}\n`,
|
|
2526
2752
|
),
|
|
2527
2753
|
);
|
|
2528
|
-
await
|
|
2754
|
+
await enqueueHeadedReload(getTestFileInfo(testFile), testNamePattern);
|
|
2529
2755
|
},
|
|
2530
2756
|
async getTestFiles() {
|
|
2531
|
-
return
|
|
2757
|
+
return currentTestFiles;
|
|
2758
|
+
},
|
|
2759
|
+
async onRunnerFramesReady(testFiles: string[]) {
|
|
2760
|
+
markRunnerFramesReady(testFiles);
|
|
2532
2761
|
},
|
|
2533
2762
|
async onTestFileStart(payload: TestFileStartPayload) {
|
|
2534
2763
|
await handleTestFileStart(payload);
|
|
@@ -2538,20 +2767,12 @@ export const runBrowserController = async (
|
|
|
2538
2767
|
},
|
|
2539
2768
|
async onTestFileComplete(payload: TestFileResult) {
|
|
2540
2769
|
await handleTestFileComplete(payload);
|
|
2541
|
-
|
|
2542
|
-
completedTests++;
|
|
2543
|
-
if (completedTests >= allTestFiles.length && resolveAllTests) {
|
|
2544
|
-
resolveAllTests();
|
|
2545
|
-
}
|
|
2546
2770
|
},
|
|
2547
2771
|
async onLog(payload: LogPayload) {
|
|
2548
2772
|
await handleLog(payload);
|
|
2549
2773
|
},
|
|
2550
2774
|
async onFatal(payload: FatalPayload) {
|
|
2551
2775
|
await handleFatal(payload);
|
|
2552
|
-
if (resolveAllTests) {
|
|
2553
|
-
resolveAllTests();
|
|
2554
|
-
}
|
|
2555
2776
|
},
|
|
2556
2777
|
async dispatch(request: BrowserDispatchRequest) {
|
|
2557
2778
|
// Headed/container path now shares the same dispatch contract as headless.
|
|
@@ -2593,31 +2814,33 @@ export const runBrowserController = async (
|
|
|
2593
2814
|
);
|
|
2594
2815
|
}
|
|
2595
2816
|
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
color.yellow(
|
|
2608
|
-
`\nTest execution timeout after ${totalTimeoutMs / 1000}s. ` +
|
|
2609
|
-
`Completed: ${completedTests}/${allTestFiles.length}\n`,
|
|
2610
|
-
),
|
|
2611
|
-
);
|
|
2612
|
-
resolve();
|
|
2613
|
-
}, totalTimeoutMs);
|
|
2614
|
-
});
|
|
2817
|
+
enqueueHeadedReload = async (
|
|
2818
|
+
file: TestFileInfo,
|
|
2819
|
+
testNamePattern?: string,
|
|
2820
|
+
): Promise<void> => {
|
|
2821
|
+
return headedReloadQueue.enqueue(async () => {
|
|
2822
|
+
if (fatalError) {
|
|
2823
|
+
return;
|
|
2824
|
+
}
|
|
2825
|
+
await reloadTestFileWithTimeout(file, testNamePattern);
|
|
2826
|
+
});
|
|
2827
|
+
};
|
|
2615
2828
|
|
|
2616
2829
|
const testStart = Date.now();
|
|
2617
|
-
|
|
2830
|
+
try {
|
|
2831
|
+
await waitForRunnerFramesReady(
|
|
2832
|
+
currentTestFiles.map((file) => file.testPath),
|
|
2833
|
+
);
|
|
2618
2834
|
|
|
2619
|
-
|
|
2620
|
-
|
|
2835
|
+
for (const file of currentTestFiles) {
|
|
2836
|
+
await enqueueHeadedReload(file);
|
|
2837
|
+
if (fatalError) {
|
|
2838
|
+
break;
|
|
2839
|
+
}
|
|
2840
|
+
}
|
|
2841
|
+
} catch (error) {
|
|
2842
|
+
fatalError = fatalError ?? toError(error);
|
|
2843
|
+
ensureProcessExitCode(1);
|
|
2621
2844
|
}
|
|
2622
2845
|
|
|
2623
2846
|
const testTime = Date.now() - testStart;
|
|
@@ -2642,7 +2865,11 @@ export const runBrowserController = async (
|
|
|
2642
2865
|
context.updateReporterResultState([], [], deletedTestPaths);
|
|
2643
2866
|
}
|
|
2644
2867
|
watchContext.lastTestFiles = rerunPlan.currentTestFiles;
|
|
2645
|
-
|
|
2868
|
+
currentTestFiles = rerunPlan.currentTestFiles;
|
|
2869
|
+
await rpcManager.notifyTestFileUpdate(currentTestFiles);
|
|
2870
|
+
await waitForRunnerFramesReady(
|
|
2871
|
+
currentTestFiles.map((file) => file.testPath),
|
|
2872
|
+
);
|
|
2646
2873
|
}
|
|
2647
2874
|
|
|
2648
2875
|
if (rerunPlan.normalizedAffectedTestFiles.length > 0) {
|
|
@@ -2659,7 +2886,7 @@ export const runBrowserController = async (
|
|
|
2659
2886
|
|
|
2660
2887
|
try {
|
|
2661
2888
|
for (const testFile of rerunPlan.normalizedAffectedTestFiles) {
|
|
2662
|
-
await
|
|
2889
|
+
await enqueueHeadedReload(getTestFileInfo(testFile));
|
|
2663
2890
|
}
|
|
2664
2891
|
} catch (error) {
|
|
2665
2892
|
rerunError = toError(error);
|
|
@@ -2683,9 +2910,13 @@ export const runBrowserController = async (
|
|
|
2683
2910
|
? [rerunFatalError]
|
|
2684
2911
|
: undefined,
|
|
2685
2912
|
});
|
|
2913
|
+
logBrowserWatchReadyMessage(enableCliShortcuts);
|
|
2686
2914
|
}
|
|
2687
2915
|
} else if (!rerunPlan.filesChanged) {
|
|
2688
2916
|
logger.log(color.cyan('Tests will be re-executed automatically\n'));
|
|
2917
|
+
logBrowserWatchReadyMessage(enableCliShortcuts);
|
|
2918
|
+
} else {
|
|
2919
|
+
logBrowserWatchReadyMessage(enableCliShortcuts);
|
|
2689
2920
|
}
|
|
2690
2921
|
};
|
|
2691
2922
|
}
|
|
@@ -2746,9 +2977,7 @@ export const runBrowserController = async (
|
|
|
2746
2977
|
// Enable watch hooks AFTER initial test run to avoid duplicate runs
|
|
2747
2978
|
if (isWatchMode && triggerRerun) {
|
|
2748
2979
|
watchContext.hooksEnabled = true;
|
|
2749
|
-
|
|
2750
|
-
color.cyan('\nWatch mode enabled - will re-run tests on file changes\n'),
|
|
2751
|
-
);
|
|
2980
|
+
logBrowserWatchReadyMessage(enableCliShortcuts);
|
|
2752
2981
|
}
|
|
2753
2982
|
|
|
2754
2983
|
return result;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { color, logger } from '@rstest/core/browser';
|
|
2
|
+
|
|
3
|
+
const isTTY = (): boolean => Boolean(process.stdin.isTTY && !process.env.CI);
|
|
4
|
+
|
|
5
|
+
export const isBrowserWatchCliShortcutsEnabled = (): boolean => isTTY();
|
|
6
|
+
|
|
7
|
+
export const getBrowserWatchCliShortcutsHintMessage = (): string => {
|
|
8
|
+
return ` ${color.dim('press')} ${color.bold('q')} ${color.dim('to quit')}\n`;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export const logBrowserWatchReadyMessage = (
|
|
12
|
+
enableCliShortcuts: boolean,
|
|
13
|
+
): void => {
|
|
14
|
+
logger.log(color.green(' Waiting for file changes...'));
|
|
15
|
+
|
|
16
|
+
if (enableCliShortcuts) {
|
|
17
|
+
logger.log(getBrowserWatchCliShortcutsHintMessage());
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export async function setupBrowserWatchCliShortcuts({
|
|
22
|
+
close,
|
|
23
|
+
}: {
|
|
24
|
+
close: () => Promise<void>;
|
|
25
|
+
}): Promise<() => void> {
|
|
26
|
+
const { emitKeypressEvents } = await import('node:readline');
|
|
27
|
+
|
|
28
|
+
emitKeypressEvents(process.stdin);
|
|
29
|
+
process.stdin.setRawMode(true);
|
|
30
|
+
process.stdin.resume();
|
|
31
|
+
process.stdin.setEncoding('utf8');
|
|
32
|
+
|
|
33
|
+
let isClosing = false;
|
|
34
|
+
|
|
35
|
+
const handleKeypress = (
|
|
36
|
+
str: string,
|
|
37
|
+
key: { name: string; ctrl: boolean },
|
|
38
|
+
) => {
|
|
39
|
+
if (key.ctrl && key.name === 'c') {
|
|
40
|
+
process.kill(process.pid, 'SIGINT');
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (key.ctrl && key.name === 'z') {
|
|
45
|
+
if (process.platform !== 'win32') {
|
|
46
|
+
process.kill(process.pid, 'SIGTSTP');
|
|
47
|
+
}
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (str !== 'q' || isClosing) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// TODO: Support more browser watch shortcuts only after this path is
|
|
56
|
+
// refactored to share the same shortcut model as node mode.
|
|
57
|
+
isClosing = true;
|
|
58
|
+
void (async () => {
|
|
59
|
+
try {
|
|
60
|
+
await close();
|
|
61
|
+
} finally {
|
|
62
|
+
process.exit(0);
|
|
63
|
+
}
|
|
64
|
+
})();
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
process.stdin.on('keypress', handleKeypress);
|
|
68
|
+
|
|
69
|
+
return () => {
|
|
70
|
+
try {
|
|
71
|
+
process.stdin.setRawMode(false);
|
|
72
|
+
process.stdin.pause();
|
|
73
|
+
} catch {}
|
|
74
|
+
|
|
75
|
+
process.stdin.off('keypress', handleKeypress);
|
|
76
|
+
};
|
|
77
|
+
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
/*! LICENSE: 101.36a8ccdf84.js.LICENSE.txt */
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
/*! LICENSE: lib-react.dcf2a5e57a.js.LICENSE.txt */
|
/package/dist/{361.js → 323.js}
RENAMED
|
File without changes
|