@rstest/browser 0.9.10 → 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/{102.e63e4aa16d.js → 484.af891b432a.js} +666 -668
- package/dist/browser-container/container-static/js/484.af891b432a.js.LICENSE.txt +1 -0
- package/dist/browser-container/container-static/js/{index.af52585634.js → index.463ae8de87.js} +10 -10
- 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 +204 -23
- 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 +8 -8
- package/src/client/entry.ts +75 -14
- package/src/hostController.ts +403 -70
- package/src/protocol.ts +4 -0
- package/dist/browser-container/container-static/js/102.e63e4aa16d.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'];
|
|
@@ -797,10 +824,17 @@ const getRuntimeConfigFromProject = (
|
|
|
797
824
|
logHeapUsage,
|
|
798
825
|
chaiConfig,
|
|
799
826
|
includeTaskLocation,
|
|
827
|
+
silent,
|
|
800
828
|
} = project.normalizedConfig;
|
|
801
829
|
|
|
802
830
|
return {
|
|
803
|
-
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
|
+
},
|
|
804
838
|
testNamePattern,
|
|
805
839
|
testTimeout,
|
|
806
840
|
hookTimeout,
|
|
@@ -823,6 +857,7 @@ const getRuntimeConfigFromProject = (
|
|
|
823
857
|
logHeapUsage,
|
|
824
858
|
chaiConfig,
|
|
825
859
|
includeTaskLocation,
|
|
860
|
+
silent,
|
|
826
861
|
};
|
|
827
862
|
};
|
|
828
863
|
|
|
@@ -916,6 +951,7 @@ const collectProjectEntries = async (
|
|
|
916
951
|
rootPath: context.rootPath,
|
|
917
952
|
projectRoot: project.rootPath,
|
|
918
953
|
fileFilters: context.fileFilters || [],
|
|
954
|
+
fileFilterMode: context.fileFilterMode,
|
|
919
955
|
});
|
|
920
956
|
|
|
921
957
|
const setup = getSetupFiles(setupFiles, project.rootPath);
|
|
@@ -1280,6 +1316,10 @@ const createBrowserRuntime = async ({
|
|
|
1280
1316
|
}
|
|
1281
1317
|
|
|
1282
1318
|
const userRsbuildConfig = project.normalizedConfig;
|
|
1319
|
+
const buildCache = resolveProjectBuildCache({
|
|
1320
|
+
context,
|
|
1321
|
+
project,
|
|
1322
|
+
});
|
|
1283
1323
|
const setupFiles = Object.values(
|
|
1284
1324
|
getSetupFiles(
|
|
1285
1325
|
project.normalizedConfig.setupFiles,
|
|
@@ -1287,53 +1327,65 @@ const createBrowserRuntime = async ({
|
|
|
1287
1327
|
),
|
|
1288
1328
|
);
|
|
1289
1329
|
// Merge order: current config -> userConfig -> rstest required config (highest priority)
|
|
1290
|
-
const merged = mergeEnvironmentConfig(
|
|
1291
|
-
|
|
1292
|
-
|
|
1330
|
+
const merged = mergeEnvironmentConfig(
|
|
1331
|
+
config,
|
|
1332
|
+
{
|
|
1333
|
+
...userRsbuildConfig,
|
|
1334
|
+
performance: buildCache
|
|
1335
|
+
? {
|
|
1336
|
+
...userRsbuildConfig.performance,
|
|
1337
|
+
buildCache,
|
|
1338
|
+
}
|
|
1339
|
+
: userRsbuildConfig.performance,
|
|
1293
1340
|
},
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
'import.meta.env': 'globalThis[Symbol.for("rstest.env")]',
|
|
1341
|
+
{
|
|
1342
|
+
resolve: {
|
|
1343
|
+
alias: rstestInternalAliases,
|
|
1298
1344
|
},
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
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
|
+
},
|
|
1305
1350
|
},
|
|
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
|
-
|
|
1333
|
-
|
|
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
|
+
},
|
|
1334
1386
|
},
|
|
1335
1387
|
},
|
|
1336
|
-
|
|
1388
|
+
);
|
|
1337
1389
|
|
|
1338
1390
|
// Completely overwrite entry to prevent Rsbuild default entry detection from taking effect.
|
|
1339
1391
|
// In browser mode, entry is fully controlled by rstest (not user's src/index.ts).
|
|
@@ -1655,8 +1707,22 @@ export const runBrowserController = async (
|
|
|
1655
1707
|
context: Rstest,
|
|
1656
1708
|
options?: BrowserTestRunOptions,
|
|
1657
1709
|
): Promise<BrowserTestRunResult | void> => {
|
|
1658
|
-
const {
|
|
1710
|
+
const {
|
|
1711
|
+
skipOnTestRunEnd = false,
|
|
1712
|
+
allowEmptyWatchRun = false,
|
|
1713
|
+
onTraceEvents,
|
|
1714
|
+
} = options ?? {};
|
|
1659
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;
|
|
1660
1726
|
const browserProjects = getBrowserProjects(context);
|
|
1661
1727
|
const useHeadlessDirect = browserProjects.every(
|
|
1662
1728
|
(project) => project.normalizedConfig.browser.headless,
|
|
@@ -1873,27 +1939,47 @@ export const runBrowserController = async (
|
|
|
1873
1939
|
(total, item) => total + item.testFiles.length,
|
|
1874
1940
|
0,
|
|
1875
1941
|
);
|
|
1942
|
+
const shouldKeepWatchingWithEmptySet = isWatchMode && allowEmptyWatchRun;
|
|
1876
1943
|
|
|
1877
1944
|
if (totalTests === 0) {
|
|
1878
1945
|
const code = context.normalizedConfig.passWithNoTests ? 0 : 1;
|
|
1879
1946
|
if (!skipOnTestRunEnd) {
|
|
1880
|
-
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
|
+
});
|
|
1881
1954
|
if (code === 0) {
|
|
1882
1955
|
logger.log(color.yellow(message));
|
|
1883
1956
|
} else {
|
|
1884
1957
|
logger.error(color.red(message));
|
|
1885
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
|
+
}
|
|
1886
1971
|
}
|
|
1887
1972
|
|
|
1888
|
-
if (code !== 0) {
|
|
1973
|
+
if (code !== 0 && !shouldKeepWatchingWithEmptySet) {
|
|
1889
1974
|
ensureProcessExitCode(code);
|
|
1890
1975
|
}
|
|
1891
|
-
|
|
1976
|
+
if (!shouldKeepWatchingWithEmptySet) {
|
|
1977
|
+
return;
|
|
1978
|
+
}
|
|
1892
1979
|
}
|
|
1893
1980
|
|
|
1894
1981
|
await notifyTestRunStart();
|
|
1895
1982
|
|
|
1896
|
-
const isWatchMode = context.command === 'watch';
|
|
1897
1983
|
const enableCliShortcuts = isWatchMode && isBrowserWatchCliShortcutsEnabled();
|
|
1898
1984
|
const browserTempOutputRoot = context.normalizedConfig.output.distPath.root;
|
|
1899
1985
|
const tempDir =
|
|
@@ -2124,9 +2210,21 @@ export const runBrowserController = async (
|
|
|
2124
2210
|
const handleTestFileStart = async (
|
|
2125
2211
|
payload: TestFileStartPayload,
|
|
2126
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
|
+
}
|
|
2127
2224
|
await Promise.all(
|
|
2128
2225
|
context.reporters.map((reporter) =>
|
|
2129
2226
|
(reporter as Reporter).onTestFileStart?.({
|
|
2227
|
+
testId: getFileTaskId(payload.testPath),
|
|
2130
2228
|
testPath: payload.testPath,
|
|
2131
2229
|
tests: [],
|
|
2132
2230
|
}),
|
|
@@ -2137,6 +2235,7 @@ export const runBrowserController = async (
|
|
|
2137
2235
|
const handleTestFileReady = async (
|
|
2138
2236
|
payload: TestFileReadyPayload,
|
|
2139
2237
|
): Promise<void> => {
|
|
2238
|
+
phaseTrackers?.get(payload.testPath)?.transition('tests');
|
|
2140
2239
|
await Promise.all(
|
|
2141
2240
|
context.reporters.map((reporter) =>
|
|
2142
2241
|
(reporter as Reporter).onTestFileReady?.(payload),
|
|
@@ -2147,6 +2246,7 @@ export const runBrowserController = async (
|
|
|
2147
2246
|
const handleTestSuiteStart = async (
|
|
2148
2247
|
payload: TestSuiteStartPayload,
|
|
2149
2248
|
): Promise<void> => {
|
|
2249
|
+
phaseTrackers?.get(payload.testPath)?.recordSuiteStart(payload);
|
|
2150
2250
|
await Promise.all(
|
|
2151
2251
|
context.reporters.map((reporter) =>
|
|
2152
2252
|
(reporter as Reporter).onTestSuiteStart?.(payload),
|
|
@@ -2157,16 +2257,28 @@ export const runBrowserController = async (
|
|
|
2157
2257
|
const handleTestSuiteResult = async (
|
|
2158
2258
|
payload: TestSuiteResultPayload,
|
|
2159
2259
|
): Promise<void> => {
|
|
2260
|
+
phaseTrackers?.get(payload.testPath)?.recordSuiteResult(payload);
|
|
2160
2261
|
await Promise.all(
|
|
2161
2262
|
context.reporters.map((reporter) =>
|
|
2162
2263
|
(reporter as Reporter).onTestSuiteResult?.(payload),
|
|
2163
2264
|
),
|
|
2164
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
|
+
}
|
|
2165
2276
|
};
|
|
2166
2277
|
|
|
2167
2278
|
const handleTestCaseStart = async (
|
|
2168
2279
|
payload: TestCaseStartPayload,
|
|
2169
2280
|
): Promise<void> => {
|
|
2281
|
+
phaseTrackers?.get(payload.testPath)?.recordCaseStart(payload);
|
|
2170
2282
|
await Promise.all(
|
|
2171
2283
|
context.reporters.map((reporter) =>
|
|
2172
2284
|
(reporter as Reporter).onTestCaseStart?.(payload),
|
|
@@ -2176,11 +2288,22 @@ export const runBrowserController = async (
|
|
|
2176
2288
|
|
|
2177
2289
|
const handleTestCaseResult = async (payload: TestResult): Promise<void> => {
|
|
2178
2290
|
caseResults.push(payload);
|
|
2291
|
+
phaseTrackers?.get(payload.testPath)?.recordCaseResult(payload);
|
|
2179
2292
|
await Promise.all(
|
|
2180
2293
|
context.reporters.map((reporter) =>
|
|
2181
2294
|
(reporter as Reporter).onTestCaseResult?.(payload),
|
|
2182
2295
|
),
|
|
2183
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
|
+
}
|
|
2184
2307
|
};
|
|
2185
2308
|
|
|
2186
2309
|
const handleTestFileComplete = async (
|
|
@@ -2191,6 +2314,27 @@ export const runBrowserController = async (
|
|
|
2191
2314
|
if (payload.snapshotResult) {
|
|
2192
2315
|
context.snapshotManager.add(payload.snapshotResult);
|
|
2193
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
|
+
|
|
2194
2338
|
await Promise.all(
|
|
2195
2339
|
context.reporters.map((reporter) =>
|
|
2196
2340
|
(reporter as Reporter).onTestFileResult?.(payload),
|
|
@@ -2205,19 +2349,28 @@ export const runBrowserController = async (
|
|
|
2205
2349
|
const log: UserConsoleLog = {
|
|
2206
2350
|
content: payload.content,
|
|
2207
2351
|
name: payload.level,
|
|
2352
|
+
taskId: payload.taskId,
|
|
2353
|
+
taskName: payload.taskName,
|
|
2354
|
+
taskParentNames: payload.taskParentNames,
|
|
2355
|
+
taskType: payload.taskType,
|
|
2208
2356
|
testPath: payload.testPath,
|
|
2209
2357
|
type: payload.type,
|
|
2210
2358
|
trace: payload.trace,
|
|
2211
2359
|
};
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2360
|
+
if (context.normalizedConfig.silent === true) {
|
|
2361
|
+
return;
|
|
2362
|
+
}
|
|
2363
|
+
|
|
2364
|
+
if (context.normalizedConfig.silent === 'passed-only') {
|
|
2365
|
+
bufferConsoleLog(log);
|
|
2366
|
+
return;
|
|
2367
|
+
}
|
|
2368
|
+
|
|
2369
|
+
if (context.normalizedConfig.disableConsoleIntercept) {
|
|
2370
|
+
return;
|
|
2220
2371
|
}
|
|
2372
|
+
|
|
2373
|
+
await emitUserConsoleLog(log);
|
|
2221
2374
|
};
|
|
2222
2375
|
|
|
2223
2376
|
const handleFatal = async (payload: FatalPayload): Promise<void> => {
|
|
@@ -2227,6 +2380,113 @@ export const runBrowserController = async (
|
|
|
2227
2380
|
ensureProcessExitCode(1);
|
|
2228
2381
|
};
|
|
2229
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
|
+
|
|
2230
2490
|
const runSnapshotRpc = async (
|
|
2231
2491
|
request: SnapshotRpcRequest,
|
|
2232
2492
|
): Promise<unknown> => {
|
|
@@ -2613,6 +2873,69 @@ export const runBrowserController = async (
|
|
|
2613
2873
|
},
|
|
2614
2874
|
});
|
|
2615
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
|
+
|
|
2616
2939
|
const testStart = Date.now();
|
|
2617
2940
|
await runFilesWithPool(allTestFiles);
|
|
2618
2941
|
const testTime = Date.now() - testStart;
|
|
@@ -3089,24 +3412,27 @@ export const runBrowserController = async (
|
|
|
3089
3412
|
});
|
|
3090
3413
|
};
|
|
3091
3414
|
|
|
3092
|
-
|
|
3093
|
-
|
|
3094
|
-
|
|
3095
|
-
|
|
3096
|
-
|
|
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
|
+
);
|
|
3097
3422
|
|
|
3098
|
-
|
|
3099
|
-
|
|
3100
|
-
|
|
3101
|
-
|
|
3423
|
+
for (const file of currentTestFiles) {
|
|
3424
|
+
await enqueueHeadedReload(file);
|
|
3425
|
+
if (fatalError) {
|
|
3426
|
+
break;
|
|
3427
|
+
}
|
|
3102
3428
|
}
|
|
3429
|
+
} catch (error) {
|
|
3430
|
+
fatalError = fatalError ?? toError(error);
|
|
3431
|
+
ensureProcessExitCode(1);
|
|
3103
3432
|
}
|
|
3104
|
-
} catch (error) {
|
|
3105
|
-
fatalError = fatalError ?? toError(error);
|
|
3106
|
-
ensureProcessExitCode(1);
|
|
3107
|
-
}
|
|
3108
3433
|
|
|
3109
|
-
|
|
3434
|
+
testTime = Date.now() - testStart;
|
|
3435
|
+
}
|
|
3110
3436
|
|
|
3111
3437
|
// Define rerun logic for watch mode
|
|
3112
3438
|
if (isWatchMode) {
|
|
@@ -3130,6 +3456,13 @@ export const runBrowserController = async (
|
|
|
3130
3456
|
watchContext.lastTestFiles = rerunPlan.currentTestFiles;
|
|
3131
3457
|
currentTestFiles = rerunPlan.currentTestFiles;
|
|
3132
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
|
+
}
|
|
3133
3466
|
await waitForRunnerFramesReady(
|
|
3134
3467
|
currentTestFiles.map((file) => file.testPath),
|
|
3135
3468
|
);
|
package/src/protocol.ts
CHANGED
|
@@ -106,6 +106,10 @@ export type BrowserClientMessage =
|
|
|
106
106
|
payload: {
|
|
107
107
|
level: 'log' | 'warn' | 'error' | 'info' | 'debug';
|
|
108
108
|
content: string;
|
|
109
|
+
taskId?: string;
|
|
110
|
+
taskName?: string;
|
|
111
|
+
taskParentNames?: string[];
|
|
112
|
+
taskType?: 'file' | 'suite' | 'case';
|
|
109
113
|
testPath: string;
|
|
110
114
|
type: 'stdout' | 'stderr';
|
|
111
115
|
trace?: string;
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
/*! LICENSE: 102.e63e4aa16d.js.LICENSE.txt */
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
/*! LICENSE: lib-react.9e703e1b29.js.LICENSE.txt */
|