@rstest/browser 0.9.9 → 0.10.0
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/augmentExpect.d.ts +1 -1
- package/dist/browser-container/container-static/js/{464.89762ff658.js → 484.af891b432a.js} +3183 -3152
- package/dist/browser-container/container-static/js/484.af891b432a.js.LICENSE.txt +1 -0
- package/dist/browser-container/container-static/js/{index.c13dfe1ecf.js → index.463ae8de87.js} +12 -12
- package/dist/browser-container/container-static/js/{lib-react.9e703e1b29.js → lib-react.5ae9b90ee5.js} +24 -24
- package/dist/browser-container/container-static/js/lib-react.5ae9b90ee5.js.LICENSE.txt +1 -0
- package/dist/browser-container/index.html +1 -1
- package/dist/browser.d.ts +2 -2
- package/dist/client/api.d.ts +2 -2
- package/dist/client/browserRpc.d.ts +2 -2
- package/dist/client/dispatchTransport.d.ts +1 -1
- package/dist/client/locator.d.ts +2 -2
- package/dist/concurrency.d.ts +1 -1
- package/dist/dispatchCapabilities.d.ts +2 -2
- package/dist/dispatchRouter.d.ts +1 -1
- package/dist/headlessTransport.d.ts +2 -2
- package/dist/hostController.d.ts +2 -2
- package/dist/index.d.ts +3 -3
- package/dist/index.js +205 -24
- package/dist/protocol.d.ts +6 -2
- package/dist/providers/index.d.ts +1 -1
- package/dist/providers/playwright/compileLocator.d.ts +1 -1
- package/dist/providers/playwright/dispatchBrowserRpc.d.ts +1 -1
- package/dist/providers/playwright/expectUtils.d.ts +1 -1
- package/dist/providers/playwright/implementation.d.ts +1 -1
- package/dist/providers/playwright/index.d.ts +1 -1
- package/dist/providers/playwright/runtime.d.ts +1 -1
- package/dist/providers/playwright/textMatcher.d.ts +2 -2
- package/dist/sessionRegistry.d.ts +1 -1
- package/dist/viewportPresets.d.ts +1 -1
- package/dist/watchRerunPlanner.d.ts +1 -1
- package/package.json +9 -9
- package/src/client/entry.ts +75 -14
- package/src/hostController.ts +405 -71
- package/src/protocol.ts +4 -0
- package/dist/browser-container/container-static/js/464.89762ff658.js.LICENSE.txt +0 -1
- package/dist/browser-container/container-static/js/lib-react.9e703e1b29.js.LICENSE.txt +0 -1
package/src/hostController.ts
CHANGED
|
@@ -12,16 +12,19 @@ import {
|
|
|
12
12
|
color,
|
|
13
13
|
createCoverageProvider,
|
|
14
14
|
type FormattedError,
|
|
15
|
+
getNoTestFilesMessage,
|
|
15
16
|
getSetupFiles,
|
|
16
17
|
getTestEntries,
|
|
17
18
|
isDebug,
|
|
18
19
|
type ListCommandResult,
|
|
19
20
|
loadCoverageProvider,
|
|
20
21
|
logger,
|
|
22
|
+
PhaseTracker,
|
|
21
23
|
type ProjectContext,
|
|
22
24
|
type Reporter,
|
|
23
25
|
type Rstest,
|
|
24
26
|
type RuntimeConfig,
|
|
27
|
+
resolveProjectBuildCache,
|
|
25
28
|
rsbuild,
|
|
26
29
|
serializableConfig,
|
|
27
30
|
type Test,
|
|
@@ -94,6 +97,18 @@ type RsbuildInstance = rsbuild.RsbuildInstance;
|
|
|
94
97
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
95
98
|
const OPTIONS_PLACEHOLDER = '__RSTEST_OPTIONS_PLACEHOLDER__';
|
|
96
99
|
|
|
100
|
+
/**
|
|
101
|
+
* Monotonic counter for synthetic per-file Perfetto `pid` values in `--trace`
|
|
102
|
+
* mode. Browser host runs every test file inside the same Node process, so
|
|
103
|
+
* without an override every file would emit events under the same `pid` and
|
|
104
|
+
* share a single track labelled `worker <hostPid>`. Giving each file its own
|
|
105
|
+
* synthetic `pid` makes the track title surface the file path instead,
|
|
106
|
+
* matching node mode's default `isolate: true` behavior. The 1_000_000_000
|
|
107
|
+
* base keeps each synthetic `pid` well clear of real OS `pid` values in
|
|
108
|
+
* mixed-mode traces.
|
|
109
|
+
*/
|
|
110
|
+
let nextBrowserFilePid = 1_000_000_000;
|
|
111
|
+
|
|
97
112
|
/**
|
|
98
113
|
* Serialize JSON for inline <script> injection.
|
|
99
114
|
* Escapes '<' to prevent accidental </script> break-out.
|
|
@@ -154,6 +169,10 @@ type TestFileStartPayload = {
|
|
|
154
169
|
type LogPayload = {
|
|
155
170
|
level: 'log' | 'warn' | 'error' | 'info' | 'debug';
|
|
156
171
|
content: string;
|
|
172
|
+
taskId?: string;
|
|
173
|
+
taskName?: string;
|
|
174
|
+
taskParentNames?: string[];
|
|
175
|
+
taskType?: 'file' | 'suite' | 'case';
|
|
157
176
|
testPath: string;
|
|
158
177
|
type: 'stdout' | 'stderr';
|
|
159
178
|
trace?: string;
|
|
@@ -186,6 +205,14 @@ type DeferredPromise<T> = {
|
|
|
186
205
|
reject: (reason?: unknown) => void;
|
|
187
206
|
};
|
|
188
207
|
|
|
208
|
+
const getFileTaskId = (testPath: string): string => {
|
|
209
|
+
return `file:${testPath}`;
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
const getBufferedLogTaskId = (log: UserConsoleLog): string => {
|
|
213
|
+
return log.taskId ?? getFileTaskId(log.testPath);
|
|
214
|
+
};
|
|
215
|
+
|
|
189
216
|
const createDeferredPromise = <T>(): DeferredPromise<T> => {
|
|
190
217
|
let resolve!: DeferredPromise<T>['resolve'];
|
|
191
218
|
let reject!: DeferredPromise<T>['reject'];
|
|
@@ -506,8 +533,9 @@ const applyDefaultWatchOptions = (
|
|
|
506
533
|
rspackConfig.watchOptions.ignored.push('**/.git', '**/node_modules');
|
|
507
534
|
}
|
|
508
535
|
|
|
509
|
-
rspackConfig.output?.path
|
|
536
|
+
if (rspackConfig.output?.path) {
|
|
510
537
|
rspackConfig.watchOptions.ignored.push(rspackConfig.output.path);
|
|
538
|
+
}
|
|
511
539
|
};
|
|
512
540
|
|
|
513
541
|
type LazyCompilationModule = {
|
|
@@ -796,10 +824,17 @@ const getRuntimeConfigFromProject = (
|
|
|
796
824
|
logHeapUsage,
|
|
797
825
|
chaiConfig,
|
|
798
826
|
includeTaskLocation,
|
|
827
|
+
silent,
|
|
799
828
|
} = project.normalizedConfig;
|
|
800
829
|
|
|
801
830
|
return {
|
|
802
|
-
env
|
|
831
|
+
// Propagate NODE_ENV from the host so `import.meta.env.NODE_ENV` resolves
|
|
832
|
+
// to `'test'` in browser tests (matches Node mode). User-supplied `env`
|
|
833
|
+
// wins so explicit overrides still take effect.
|
|
834
|
+
env: {
|
|
835
|
+
NODE_ENV: process.env.NODE_ENV,
|
|
836
|
+
...env,
|
|
837
|
+
},
|
|
803
838
|
testNamePattern,
|
|
804
839
|
testTimeout,
|
|
805
840
|
hookTimeout,
|
|
@@ -822,6 +857,7 @@ const getRuntimeConfigFromProject = (
|
|
|
822
857
|
logHeapUsage,
|
|
823
858
|
chaiConfig,
|
|
824
859
|
includeTaskLocation,
|
|
860
|
+
silent,
|
|
825
861
|
};
|
|
826
862
|
};
|
|
827
863
|
|
|
@@ -915,6 +951,7 @@ const collectProjectEntries = async (
|
|
|
915
951
|
rootPath: context.rootPath,
|
|
916
952
|
projectRoot: project.rootPath,
|
|
917
953
|
fileFilters: context.fileFilters || [],
|
|
954
|
+
fileFilterMode: context.fileFilterMode,
|
|
918
955
|
});
|
|
919
956
|
|
|
920
957
|
const setup = getSetupFiles(setupFiles, project.rootPath);
|
|
@@ -1279,6 +1316,10 @@ const createBrowserRuntime = async ({
|
|
|
1279
1316
|
}
|
|
1280
1317
|
|
|
1281
1318
|
const userRsbuildConfig = project.normalizedConfig;
|
|
1319
|
+
const buildCache = resolveProjectBuildCache({
|
|
1320
|
+
context,
|
|
1321
|
+
project,
|
|
1322
|
+
});
|
|
1282
1323
|
const setupFiles = Object.values(
|
|
1283
1324
|
getSetupFiles(
|
|
1284
1325
|
project.normalizedConfig.setupFiles,
|
|
@@ -1286,53 +1327,65 @@ const createBrowserRuntime = async ({
|
|
|
1286
1327
|
),
|
|
1287
1328
|
);
|
|
1288
1329
|
// Merge order: current config -> userConfig -> rstest required config (highest priority)
|
|
1289
|
-
const merged = mergeEnvironmentConfig(
|
|
1290
|
-
|
|
1291
|
-
|
|
1330
|
+
const merged = mergeEnvironmentConfig(
|
|
1331
|
+
config,
|
|
1332
|
+
{
|
|
1333
|
+
...userRsbuildConfig,
|
|
1334
|
+
performance: buildCache
|
|
1335
|
+
? {
|
|
1336
|
+
...userRsbuildConfig.performance,
|
|
1337
|
+
buildCache,
|
|
1338
|
+
}
|
|
1339
|
+
: userRsbuildConfig.performance,
|
|
1292
1340
|
},
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
'import.meta.env': 'globalThis[Symbol.for("rstest.env")]',
|
|
1341
|
+
{
|
|
1342
|
+
resolve: {
|
|
1343
|
+
alias: rstestInternalAliases,
|
|
1297
1344
|
},
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
js: 'source-map',
|
|
1345
|
+
source: {
|
|
1346
|
+
define: {
|
|
1347
|
+
'process.env': 'globalThis[Symbol.for("rstest.env")]',
|
|
1348
|
+
'import.meta.env': 'globalThis[Symbol.for("rstest.env")]',
|
|
1349
|
+
},
|
|
1304
1350
|
},
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1351
|
+
output: {
|
|
1352
|
+
target: 'web',
|
|
1353
|
+
// Enable source map for inline snapshot support
|
|
1354
|
+
sourceMap: {
|
|
1355
|
+
js: 'source-map',
|
|
1356
|
+
},
|
|
1357
|
+
},
|
|
1358
|
+
tools: {
|
|
1359
|
+
rspack: (rspackConfig) => {
|
|
1360
|
+
rspackConfig.mode = 'development';
|
|
1361
|
+
rspackConfig.lazyCompilation =
|
|
1362
|
+
createBrowserLazyCompilationConfig(setupFiles);
|
|
1363
|
+
rspackConfig.plugins = rspackConfig.plugins || [];
|
|
1364
|
+
rspackConfig.plugins.push(virtualManifestPlugin);
|
|
1365
|
+
|
|
1366
|
+
applyDefaultWatchOptions(rspackConfig, isWatchMode);
|
|
1367
|
+
|
|
1368
|
+
// Extract and merge sourcemaps from pre-built @rstest/core files
|
|
1369
|
+
// This preserves the sourcemap chain for inline snapshot support
|
|
1370
|
+
// See: https://rspack.dev/config/module-rules#rulesextractsourcemap
|
|
1371
|
+
const browserRuntimeDir = dirname(browserRuntimePath);
|
|
1372
|
+
rspackConfig.module = rspackConfig.module || {};
|
|
1373
|
+
rspackConfig.module.rules = rspackConfig.module.rules || [];
|
|
1374
|
+
rspackConfig.module.rules.unshift({
|
|
1375
|
+
test: /\.js$/,
|
|
1376
|
+
include: browserRuntimeDir,
|
|
1377
|
+
extractSourceMap: true,
|
|
1378
|
+
});
|
|
1379
|
+
|
|
1380
|
+
if (isDebug()) {
|
|
1381
|
+
logger.log(
|
|
1382
|
+
`[rstest:browser] extractSourceMap rule added for: ${browserRuntimeDir}`,
|
|
1383
|
+
);
|
|
1384
|
+
}
|
|
1385
|
+
},
|
|
1333
1386
|
},
|
|
1334
1387
|
},
|
|
1335
|
-
|
|
1388
|
+
);
|
|
1336
1389
|
|
|
1337
1390
|
// Completely overwrite entry to prevent Rsbuild default entry detection from taking effect.
|
|
1338
1391
|
// In browser mode, entry is fully controlled by rstest (not user's src/index.ts).
|
|
@@ -1654,8 +1707,22 @@ export const runBrowserController = async (
|
|
|
1654
1707
|
context: Rstest,
|
|
1655
1708
|
options?: BrowserTestRunOptions,
|
|
1656
1709
|
): Promise<BrowserTestRunResult | void> => {
|
|
1657
|
-
const {
|
|
1710
|
+
const {
|
|
1711
|
+
skipOnTestRunEnd = false,
|
|
1712
|
+
allowEmptyWatchRun = false,
|
|
1713
|
+
onTraceEvents,
|
|
1714
|
+
} = options ?? {};
|
|
1658
1715
|
const buildStart = Date.now();
|
|
1716
|
+
const isWatchMode = context.command === 'watch';
|
|
1717
|
+
|
|
1718
|
+
// Per-file PhaseTrackers, populated only when `--trace` is on (caller
|
|
1719
|
+
// passes `onTraceEvents`). The browser host shares one Node process across
|
|
1720
|
+
// every test file, so each tracker is assigned a synthetic per-file pid
|
|
1721
|
+
// (`nextBrowserFilePid`) that lets Perfetto render each file as its own
|
|
1722
|
+
// process track with the file path as the title.
|
|
1723
|
+
const phaseTrackers = onTraceEvents
|
|
1724
|
+
? new Map<string, PhaseTracker>()
|
|
1725
|
+
: undefined;
|
|
1659
1726
|
const browserProjects = getBrowserProjects(context);
|
|
1660
1727
|
const useHeadlessDirect = browserProjects.every(
|
|
1661
1728
|
(project) => project.normalizedConfig.browser.headless,
|
|
@@ -1872,27 +1939,47 @@ export const runBrowserController = async (
|
|
|
1872
1939
|
(total, item) => total + item.testFiles.length,
|
|
1873
1940
|
0,
|
|
1874
1941
|
);
|
|
1942
|
+
const shouldKeepWatchingWithEmptySet = isWatchMode && allowEmptyWatchRun;
|
|
1875
1943
|
|
|
1876
1944
|
if (totalTests === 0) {
|
|
1877
1945
|
const code = context.normalizedConfig.passWithNoTests ? 0 : 1;
|
|
1878
1946
|
if (!skipOnTestRunEnd) {
|
|
1879
|
-
const message =
|
|
1947
|
+
const message = shouldKeepWatchingWithEmptySet
|
|
1948
|
+
? 'No test files found.'
|
|
1949
|
+
: getNoTestFilesMessage({
|
|
1950
|
+
context,
|
|
1951
|
+
code,
|
|
1952
|
+
defaultMessage: `No test files found, exiting with code ${code}.`,
|
|
1953
|
+
});
|
|
1880
1954
|
if (code === 0) {
|
|
1881
1955
|
logger.log(color.yellow(message));
|
|
1882
1956
|
} else {
|
|
1883
1957
|
logger.error(color.red(message));
|
|
1884
1958
|
}
|
|
1959
|
+
|
|
1960
|
+
if (context.relatedFilters?.length) {
|
|
1961
|
+
logger.log(
|
|
1962
|
+
color.gray('related: '),
|
|
1963
|
+
context.relatedFilters.join(color.gray(', ')),
|
|
1964
|
+
);
|
|
1965
|
+
} else if (context.fileFilters?.length) {
|
|
1966
|
+
logger.log(
|
|
1967
|
+
color.gray('filter: '),
|
|
1968
|
+
context.fileFilters.join(color.gray(', ')),
|
|
1969
|
+
);
|
|
1970
|
+
}
|
|
1885
1971
|
}
|
|
1886
1972
|
|
|
1887
|
-
if (code !== 0) {
|
|
1973
|
+
if (code !== 0 && !shouldKeepWatchingWithEmptySet) {
|
|
1888
1974
|
ensureProcessExitCode(code);
|
|
1889
1975
|
}
|
|
1890
|
-
|
|
1976
|
+
if (!shouldKeepWatchingWithEmptySet) {
|
|
1977
|
+
return;
|
|
1978
|
+
}
|
|
1891
1979
|
}
|
|
1892
1980
|
|
|
1893
1981
|
await notifyTestRunStart();
|
|
1894
1982
|
|
|
1895
|
-
const isWatchMode = context.command === 'watch';
|
|
1896
1983
|
const enableCliShortcuts = isWatchMode && isBrowserWatchCliShortcutsEnabled();
|
|
1897
1984
|
const browserTempOutputRoot = context.normalizedConfig.output.distPath.root;
|
|
1898
1985
|
const tempDir =
|
|
@@ -2123,9 +2210,21 @@ export const runBrowserController = async (
|
|
|
2123
2210
|
const handleTestFileStart = async (
|
|
2124
2211
|
payload: TestFileStartPayload,
|
|
2125
2212
|
): Promise<void> => {
|
|
2213
|
+
if (phaseTrackers) {
|
|
2214
|
+
const tracker = new PhaseTracker({
|
|
2215
|
+
trace: {
|
|
2216
|
+
testPath: payload.testPath,
|
|
2217
|
+
project: payload.projectName,
|
|
2218
|
+
},
|
|
2219
|
+
pid: nextBrowserFilePid++,
|
|
2220
|
+
});
|
|
2221
|
+
tracker.transition('prepare');
|
|
2222
|
+
phaseTrackers.set(payload.testPath, tracker);
|
|
2223
|
+
}
|
|
2126
2224
|
await Promise.all(
|
|
2127
2225
|
context.reporters.map((reporter) =>
|
|
2128
2226
|
(reporter as Reporter).onTestFileStart?.({
|
|
2227
|
+
testId: getFileTaskId(payload.testPath),
|
|
2129
2228
|
testPath: payload.testPath,
|
|
2130
2229
|
tests: [],
|
|
2131
2230
|
}),
|
|
@@ -2136,6 +2235,7 @@ export const runBrowserController = async (
|
|
|
2136
2235
|
const handleTestFileReady = async (
|
|
2137
2236
|
payload: TestFileReadyPayload,
|
|
2138
2237
|
): Promise<void> => {
|
|
2238
|
+
phaseTrackers?.get(payload.testPath)?.transition('tests');
|
|
2139
2239
|
await Promise.all(
|
|
2140
2240
|
context.reporters.map((reporter) =>
|
|
2141
2241
|
(reporter as Reporter).onTestFileReady?.(payload),
|
|
@@ -2146,6 +2246,7 @@ export const runBrowserController = async (
|
|
|
2146
2246
|
const handleTestSuiteStart = async (
|
|
2147
2247
|
payload: TestSuiteStartPayload,
|
|
2148
2248
|
): Promise<void> => {
|
|
2249
|
+
phaseTrackers?.get(payload.testPath)?.recordSuiteStart(payload);
|
|
2149
2250
|
await Promise.all(
|
|
2150
2251
|
context.reporters.map((reporter) =>
|
|
2151
2252
|
(reporter as Reporter).onTestSuiteStart?.(payload),
|
|
@@ -2156,16 +2257,28 @@ export const runBrowserController = async (
|
|
|
2156
2257
|
const handleTestSuiteResult = async (
|
|
2157
2258
|
payload: TestSuiteResultPayload,
|
|
2158
2259
|
): Promise<void> => {
|
|
2260
|
+
phaseTrackers?.get(payload.testPath)?.recordSuiteResult(payload);
|
|
2159
2261
|
await Promise.all(
|
|
2160
2262
|
context.reporters.map((reporter) =>
|
|
2161
2263
|
(reporter as Reporter).onTestSuiteResult?.(payload),
|
|
2162
2264
|
),
|
|
2163
2265
|
);
|
|
2266
|
+
|
|
2267
|
+
if (context.normalizedConfig.silent === 'passed-only') {
|
|
2268
|
+
await flushBufferedLogsForTask({
|
|
2269
|
+
taskId: payload.testId,
|
|
2270
|
+
status: payload.status,
|
|
2271
|
+
taskParentNames: payload.parentNames,
|
|
2272
|
+
taskType: 'suite',
|
|
2273
|
+
testPath: payload.testPath,
|
|
2274
|
+
});
|
|
2275
|
+
}
|
|
2164
2276
|
};
|
|
2165
2277
|
|
|
2166
2278
|
const handleTestCaseStart = async (
|
|
2167
2279
|
payload: TestCaseStartPayload,
|
|
2168
2280
|
): Promise<void> => {
|
|
2281
|
+
phaseTrackers?.get(payload.testPath)?.recordCaseStart(payload);
|
|
2169
2282
|
await Promise.all(
|
|
2170
2283
|
context.reporters.map((reporter) =>
|
|
2171
2284
|
(reporter as Reporter).onTestCaseStart?.(payload),
|
|
@@ -2175,11 +2288,22 @@ export const runBrowserController = async (
|
|
|
2175
2288
|
|
|
2176
2289
|
const handleTestCaseResult = async (payload: TestResult): Promise<void> => {
|
|
2177
2290
|
caseResults.push(payload);
|
|
2291
|
+
phaseTrackers?.get(payload.testPath)?.recordCaseResult(payload);
|
|
2178
2292
|
await Promise.all(
|
|
2179
2293
|
context.reporters.map((reporter) =>
|
|
2180
2294
|
(reporter as Reporter).onTestCaseResult?.(payload),
|
|
2181
2295
|
),
|
|
2182
2296
|
);
|
|
2297
|
+
|
|
2298
|
+
if (context.normalizedConfig.silent === 'passed-only') {
|
|
2299
|
+
await flushBufferedLogsForTask({
|
|
2300
|
+
taskId: payload.testId,
|
|
2301
|
+
status: payload.status,
|
|
2302
|
+
taskParentNames: payload.parentNames,
|
|
2303
|
+
taskType: 'case',
|
|
2304
|
+
testPath: payload.testPath,
|
|
2305
|
+
});
|
|
2306
|
+
}
|
|
2183
2307
|
};
|
|
2184
2308
|
|
|
2185
2309
|
const handleTestFileComplete = async (
|
|
@@ -2190,6 +2314,27 @@ export const runBrowserController = async (
|
|
|
2190
2314
|
if (payload.snapshotResult) {
|
|
2191
2315
|
context.snapshotManager.add(payload.snapshotResult);
|
|
2192
2316
|
}
|
|
2317
|
+
|
|
2318
|
+
if (phaseTrackers) {
|
|
2319
|
+
const tracker = phaseTrackers.get(payload.testPath);
|
|
2320
|
+
if (tracker) {
|
|
2321
|
+
tracker.end();
|
|
2322
|
+
const events = tracker.getTraceEvents();
|
|
2323
|
+
if (events) onTraceEvents?.(events);
|
|
2324
|
+
phaseTrackers.delete(payload.testPath);
|
|
2325
|
+
}
|
|
2326
|
+
}
|
|
2327
|
+
|
|
2328
|
+
if (context.normalizedConfig.silent === 'passed-only') {
|
|
2329
|
+
await flushBufferedLogsForTask({
|
|
2330
|
+
taskId: payload.testId,
|
|
2331
|
+
status: payload.status,
|
|
2332
|
+
taskParentNames: payload.parentNames,
|
|
2333
|
+
taskType: 'file',
|
|
2334
|
+
testPath: payload.testPath,
|
|
2335
|
+
});
|
|
2336
|
+
}
|
|
2337
|
+
|
|
2193
2338
|
await Promise.all(
|
|
2194
2339
|
context.reporters.map((reporter) =>
|
|
2195
2340
|
(reporter as Reporter).onTestFileResult?.(payload),
|
|
@@ -2204,19 +2349,28 @@ export const runBrowserController = async (
|
|
|
2204
2349
|
const log: UserConsoleLog = {
|
|
2205
2350
|
content: payload.content,
|
|
2206
2351
|
name: payload.level,
|
|
2352
|
+
taskId: payload.taskId,
|
|
2353
|
+
taskName: payload.taskName,
|
|
2354
|
+
taskParentNames: payload.taskParentNames,
|
|
2355
|
+
taskType: payload.taskType,
|
|
2207
2356
|
testPath: payload.testPath,
|
|
2208
2357
|
type: payload.type,
|
|
2209
2358
|
trace: payload.trace,
|
|
2210
2359
|
};
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
);
|
|
2360
|
+
if (context.normalizedConfig.silent === true) {
|
|
2361
|
+
return;
|
|
2362
|
+
}
|
|
2363
|
+
|
|
2364
|
+
if (context.normalizedConfig.silent === 'passed-only') {
|
|
2365
|
+
bufferConsoleLog(log);
|
|
2366
|
+
return;
|
|
2219
2367
|
}
|
|
2368
|
+
|
|
2369
|
+
if (context.normalizedConfig.disableConsoleIntercept) {
|
|
2370
|
+
return;
|
|
2371
|
+
}
|
|
2372
|
+
|
|
2373
|
+
await emitUserConsoleLog(log);
|
|
2220
2374
|
};
|
|
2221
2375
|
|
|
2222
2376
|
const handleFatal = async (payload: FatalPayload): Promise<void> => {
|
|
@@ -2226,6 +2380,113 @@ export const runBrowserController = async (
|
|
|
2226
2380
|
ensureProcessExitCode(1);
|
|
2227
2381
|
};
|
|
2228
2382
|
|
|
2383
|
+
const bufferedConsoleLogs = new Map<string, UserConsoleLog[]>();
|
|
2384
|
+
const suiteIdsByChain = new Map<string, string>();
|
|
2385
|
+
|
|
2386
|
+
const getSuiteChainKey = (names: string[]): string => {
|
|
2387
|
+
return names.join('\u0000');
|
|
2388
|
+
};
|
|
2389
|
+
|
|
2390
|
+
const pushTaskId = (taskIds: string[], taskId: string): void => {
|
|
2391
|
+
if (!taskIds.includes(taskId)) {
|
|
2392
|
+
taskIds.push(taskId);
|
|
2393
|
+
}
|
|
2394
|
+
};
|
|
2395
|
+
|
|
2396
|
+
const shouldEmitUserConsoleLog = (log: UserConsoleLog): boolean => {
|
|
2397
|
+
return context.normalizedConfig.onConsoleLog?.(log.content) !== false;
|
|
2398
|
+
};
|
|
2399
|
+
|
|
2400
|
+
const emitUserConsoleLog = async (log: UserConsoleLog): Promise<void> => {
|
|
2401
|
+
if (!shouldEmitUserConsoleLog(log)) {
|
|
2402
|
+
return;
|
|
2403
|
+
}
|
|
2404
|
+
|
|
2405
|
+
await Promise.all(
|
|
2406
|
+
context.reporters.map((reporter) =>
|
|
2407
|
+
(reporter as Reporter).onUserConsoleLog?.(log),
|
|
2408
|
+
),
|
|
2409
|
+
);
|
|
2410
|
+
};
|
|
2411
|
+
|
|
2412
|
+
const bufferConsoleLog = (log: UserConsoleLog): void => {
|
|
2413
|
+
const taskId = getBufferedLogTaskId(log);
|
|
2414
|
+
const logs = bufferedConsoleLogs.get(taskId) || [];
|
|
2415
|
+
logs.push(log);
|
|
2416
|
+
bufferedConsoleLogs.set(taskId, logs);
|
|
2417
|
+
|
|
2418
|
+
if (log.taskType === 'suite' && log.taskId) {
|
|
2419
|
+
suiteIdsByChain.set(
|
|
2420
|
+
getSuiteChainKey([...(log.taskParentNames || []), log.taskName || '']),
|
|
2421
|
+
log.taskId,
|
|
2422
|
+
);
|
|
2423
|
+
}
|
|
2424
|
+
};
|
|
2425
|
+
|
|
2426
|
+
const flushBufferedLogsForTask = async ({
|
|
2427
|
+
taskId,
|
|
2428
|
+
status,
|
|
2429
|
+
taskParentNames,
|
|
2430
|
+
taskType,
|
|
2431
|
+
testPath,
|
|
2432
|
+
}: {
|
|
2433
|
+
taskId: string;
|
|
2434
|
+
status: TestResult['status'];
|
|
2435
|
+
taskParentNames?: string[];
|
|
2436
|
+
taskType?: 'file' | 'suite' | 'case';
|
|
2437
|
+
testPath: string;
|
|
2438
|
+
}): Promise<void> => {
|
|
2439
|
+
if (status !== 'fail') {
|
|
2440
|
+
bufferedConsoleLogs.delete(taskId);
|
|
2441
|
+
return;
|
|
2442
|
+
}
|
|
2443
|
+
|
|
2444
|
+
const taskIdsToFlush: string[] = [];
|
|
2445
|
+
|
|
2446
|
+
if (taskType === 'case') {
|
|
2447
|
+
pushTaskId(taskIdsToFlush, getFileTaskId(testPath));
|
|
2448
|
+
|
|
2449
|
+
const suiteNames = taskParentNames || [];
|
|
2450
|
+
for (let i = 0; i < suiteNames.length; i++) {
|
|
2451
|
+
const suiteId = suiteIdsByChain.get(
|
|
2452
|
+
getSuiteChainKey(suiteNames.slice(0, i + 1)),
|
|
2453
|
+
);
|
|
2454
|
+
|
|
2455
|
+
if (suiteId) {
|
|
2456
|
+
pushTaskId(taskIdsToFlush, suiteId);
|
|
2457
|
+
}
|
|
2458
|
+
}
|
|
2459
|
+
|
|
2460
|
+
pushTaskId(taskIdsToFlush, taskId);
|
|
2461
|
+
}
|
|
2462
|
+
|
|
2463
|
+
if (taskType === 'suite') {
|
|
2464
|
+
pushTaskId(taskIdsToFlush, getFileTaskId(testPath));
|
|
2465
|
+
pushTaskId(taskIdsToFlush, taskId);
|
|
2466
|
+
}
|
|
2467
|
+
|
|
2468
|
+
if (taskType === 'file') {
|
|
2469
|
+
pushTaskId(taskIdsToFlush, taskId);
|
|
2470
|
+
}
|
|
2471
|
+
|
|
2472
|
+
for (const bufferedTaskId of taskIdsToFlush) {
|
|
2473
|
+
const logs = bufferedConsoleLogs.get(bufferedTaskId);
|
|
2474
|
+
if (!logs) {
|
|
2475
|
+
continue;
|
|
2476
|
+
}
|
|
2477
|
+
|
|
2478
|
+
bufferedConsoleLogs.delete(bufferedTaskId);
|
|
2479
|
+
|
|
2480
|
+
for (const log of logs) {
|
|
2481
|
+
await Promise.all(
|
|
2482
|
+
context.reporters.map((reporter) =>
|
|
2483
|
+
(reporter as Reporter).onUserConsoleLog?.(log),
|
|
2484
|
+
),
|
|
2485
|
+
);
|
|
2486
|
+
}
|
|
2487
|
+
}
|
|
2488
|
+
};
|
|
2489
|
+
|
|
2229
2490
|
const runSnapshotRpc = async (
|
|
2230
2491
|
request: SnapshotRpcRequest,
|
|
2231
2492
|
): Promise<unknown> => {
|
|
@@ -2612,6 +2873,69 @@ export const runBrowserController = async (
|
|
|
2612
2873
|
},
|
|
2613
2874
|
});
|
|
2614
2875
|
|
|
2876
|
+
if (allTestFiles.length === 0) {
|
|
2877
|
+
const duration = {
|
|
2878
|
+
totalTime: buildTime,
|
|
2879
|
+
buildTime,
|
|
2880
|
+
testTime: 0,
|
|
2881
|
+
};
|
|
2882
|
+
const result = {
|
|
2883
|
+
results: reporterResults,
|
|
2884
|
+
testResults: caseResults,
|
|
2885
|
+
duration,
|
|
2886
|
+
hasFailure: false,
|
|
2887
|
+
getSourcemap: getBrowserSourcemap,
|
|
2888
|
+
resolveSourcemap: resolveBrowserSourcemap,
|
|
2889
|
+
close: skipOnTestRunEnd
|
|
2890
|
+
? async () => {
|
|
2891
|
+
sessionRegistry.clear();
|
|
2892
|
+
await destroyBrowserRuntime(runtime);
|
|
2893
|
+
}
|
|
2894
|
+
: undefined,
|
|
2895
|
+
};
|
|
2896
|
+
|
|
2897
|
+
if (!skipOnTestRunEnd) {
|
|
2898
|
+
await notifyTestRunEnd({ duration });
|
|
2899
|
+
}
|
|
2900
|
+
|
|
2901
|
+
if (isWatchMode) {
|
|
2902
|
+
triggerRerun = async () => {
|
|
2903
|
+
const newProjectEntries = await collectProjectEntries(context);
|
|
2904
|
+
const rerunPlan = planWatchRerun({
|
|
2905
|
+
projectEntries: newProjectEntries,
|
|
2906
|
+
previousTestFiles: watchContext.lastTestFiles,
|
|
2907
|
+
affectedTestFiles: watchContext.affectedTestFiles,
|
|
2908
|
+
});
|
|
2909
|
+
watchContext.affectedTestFiles = [];
|
|
2910
|
+
|
|
2911
|
+
if (rerunPlan.filesChanged) {
|
|
2912
|
+
watchContext.lastTestFiles = rerunPlan.currentTestFiles;
|
|
2913
|
+
if (rerunPlan.currentTestFiles.length === 0) {
|
|
2914
|
+
logger.log(
|
|
2915
|
+
color.cyan('No browser test files remain after update.\n'),
|
|
2916
|
+
);
|
|
2917
|
+
logBrowserWatchReadyMessage(enableCliShortcuts);
|
|
2918
|
+
return;
|
|
2919
|
+
}
|
|
2920
|
+
|
|
2921
|
+
logger.log(
|
|
2922
|
+
color.cyan(
|
|
2923
|
+
`Test file set changed, re-running ${rerunPlan.currentTestFiles.length} file(s)...\n`,
|
|
2924
|
+
),
|
|
2925
|
+
);
|
|
2926
|
+
void latestRerunScheduler.enqueueLatest(rerunPlan.currentTestFiles);
|
|
2927
|
+
return;
|
|
2928
|
+
}
|
|
2929
|
+
|
|
2930
|
+
logBrowserWatchReadyMessage(enableCliShortcuts);
|
|
2931
|
+
};
|
|
2932
|
+
watchContext.hooksEnabled = true;
|
|
2933
|
+
logBrowserWatchReadyMessage(enableCliShortcuts);
|
|
2934
|
+
}
|
|
2935
|
+
|
|
2936
|
+
return result;
|
|
2937
|
+
}
|
|
2938
|
+
|
|
2615
2939
|
const testStart = Date.now();
|
|
2616
2940
|
await runFilesWithPool(allTestFiles);
|
|
2617
2941
|
const testTime = Date.now() - testStart;
|
|
@@ -3088,24 +3412,27 @@ export const runBrowserController = async (
|
|
|
3088
3412
|
});
|
|
3089
3413
|
};
|
|
3090
3414
|
|
|
3091
|
-
|
|
3092
|
-
|
|
3093
|
-
|
|
3094
|
-
|
|
3095
|
-
|
|
3415
|
+
let testTime = 0;
|
|
3416
|
+
if (currentTestFiles.length > 0) {
|
|
3417
|
+
const testStart = Date.now();
|
|
3418
|
+
try {
|
|
3419
|
+
await waitForRunnerFramesReady(
|
|
3420
|
+
currentTestFiles.map((file) => file.testPath),
|
|
3421
|
+
);
|
|
3096
3422
|
|
|
3097
|
-
|
|
3098
|
-
|
|
3099
|
-
|
|
3100
|
-
|
|
3423
|
+
for (const file of currentTestFiles) {
|
|
3424
|
+
await enqueueHeadedReload(file);
|
|
3425
|
+
if (fatalError) {
|
|
3426
|
+
break;
|
|
3427
|
+
}
|
|
3101
3428
|
}
|
|
3429
|
+
} catch (error) {
|
|
3430
|
+
fatalError = fatalError ?? toError(error);
|
|
3431
|
+
ensureProcessExitCode(1);
|
|
3102
3432
|
}
|
|
3103
|
-
} catch (error) {
|
|
3104
|
-
fatalError = fatalError ?? toError(error);
|
|
3105
|
-
ensureProcessExitCode(1);
|
|
3106
|
-
}
|
|
3107
3433
|
|
|
3108
|
-
|
|
3434
|
+
testTime = Date.now() - testStart;
|
|
3435
|
+
}
|
|
3109
3436
|
|
|
3110
3437
|
// Define rerun logic for watch mode
|
|
3111
3438
|
if (isWatchMode) {
|
|
@@ -3129,6 +3456,13 @@ export const runBrowserController = async (
|
|
|
3129
3456
|
watchContext.lastTestFiles = rerunPlan.currentTestFiles;
|
|
3130
3457
|
currentTestFiles = rerunPlan.currentTestFiles;
|
|
3131
3458
|
await rpcManager.notifyTestFileUpdate(currentTestFiles);
|
|
3459
|
+
if (currentTestFiles.length === 0) {
|
|
3460
|
+
logger.log(
|
|
3461
|
+
color.cyan('No browser test files remain after update.\n'),
|
|
3462
|
+
);
|
|
3463
|
+
logBrowserWatchReadyMessage(enableCliShortcuts);
|
|
3464
|
+
return;
|
|
3465
|
+
}
|
|
3132
3466
|
await waitForRunnerFramesReady(
|
|
3133
3467
|
currentTestFiles.map((file) => file.testPath),
|
|
3134
3468
|
);
|