@rstest/browser 0.11.5 → 0.11.7
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/css/index.43daaf6c93.css +1 -0
- package/dist/browser-container/container-static/js/260.3739fe815c.js +194 -0
- package/dist/browser-container/container-static/js/index.c0f87bfb3e.js +1 -0
- package/dist/browser-container/index.html +1 -1
- package/dist/browserExecutor.d.ts +7 -5
- package/dist/browserRsbuild.d.ts +139 -0
- package/dist/browserV8Coverage.d.ts +15 -0
- package/dist/client/240~1.js +150 -0
- package/dist/{augmentExpect.d.ts → client/client/augmentExpect.d.ts} +1 -1
- package/dist/client/{browserRpc.d.ts → client/browserRpc.d.ts} +1 -1
- package/dist/client/{dispatchTransport.d.ts → client/dispatchTransport.d.ts} +8 -4
- package/dist/client/client/index.d.ts +2 -0
- package/dist/client/client/runIdentity.d.ts +12 -0
- package/dist/client/client/runner.d.ts +4 -0
- package/dist/client/client/snapshot.d.ts +27 -0
- package/dist/client/client/sourceMapSupport.d.ts +36 -0
- package/dist/{browser.js → client/index.js} +3 -102
- package/dist/client/protocol.d.ts +225 -0
- package/dist/client/rpcProtocol.d.ts +138 -0
- package/dist/client/runner.js +675 -0
- package/dist/client/sourceMap/sourceMapLoader.d.ts +25 -0
- package/dist/containerRpc.d.ts +60 -0
- package/dist/dispatchCapabilities.d.ts +1 -2
- package/dist/headedRunRegistry.d.ts +51 -0
- package/dist/headedScheduler.d.ts +58 -0
- package/dist/headlessScheduler.d.ts +38 -0
- package/dist/headlessTransport.d.ts +4 -2
- package/dist/hostController.d.ts +12 -47
- package/dist/hostPayloads.d.ts +24 -0
- package/dist/index.js +2019 -1516
- package/dist/protocol.d.ts +48 -8
- package/dist/providers/index.d.ts +21 -0
- package/dist/providers/playwright/v8Coverage.d.ts +2 -0
- package/dist/rpcProtocol.d.ts +1 -2
- package/dist/schedulerSeam.d.ts +42 -0
- package/dist/sourceMap/sourceMapLoader.d.ts +12 -1
- package/dist/watchRerunPlanner.d.ts +20 -4
- package/dist/watchRuntime.d.ts +21 -0
- package/dist/watchSignals.d.ts +29 -0
- package/package.json +12 -12
- package/dist/33.js +0 -7
- package/dist/browser-container/container-static/css/index.0521ff95b1.css +0 -1
- package/dist/browser-container/container-static/js/506.b2f920fa67.js +0 -194
- package/dist/browser-container/container-static/js/index.d27fd63f5e.js +0 -1
- package/dist/browser.d.ts +0 -2
- package/dist/headlessLatestRerunScheduler.d.ts +0 -18
- package/src/augmentExpect.ts +0 -62
- package/src/browser.ts +0 -3
- package/src/browserExecutor.ts +0 -140
- package/src/browserRpcRegistry.ts +0 -57
- package/src/client/api.ts +0 -213
- package/src/client/browserRpc.ts +0 -86
- package/src/client/dispatchTransport.ts +0 -240
- package/src/client/entry.ts +0 -807
- package/src/client/locator.ts +0 -448
- package/src/client/snapshot.ts +0 -113
- package/src/client/sourceMapSupport.ts +0 -159
- package/src/concurrency.ts +0 -65
- package/src/configValidation.ts +0 -263
- package/src/dispatchCapabilities.ts +0 -182
- package/src/dispatchRouter.ts +0 -82
- package/src/env.d.ts +0 -54
- package/src/headedSerialTaskQueue.ts +0 -23
- package/src/headlessLatestRerunScheduler.ts +0 -76
- package/src/headlessTransport.ts +0 -28
- package/src/hostController.ts +0 -4633
- package/src/index.ts +0 -51
- package/src/manifest.d.ts +0 -41
- package/src/protocol.ts +0 -225
- package/src/providers/index.ts +0 -126
- package/src/providers/playwright/compileLocator.ts +0 -130
- package/src/providers/playwright/dispatchBrowserRpc.ts +0 -372
- package/src/providers/playwright/expectUtils.ts +0 -57
- package/src/providers/playwright/implementation.ts +0 -35
- package/src/providers/playwright/index.ts +0 -1
- package/src/providers/playwright/runtime.ts +0 -159
- package/src/providers/playwright/textMatcher.ts +0 -10
- package/src/rpcProtocol.ts +0 -218
- package/src/runSession.ts +0 -110
- package/src/sessionRegistry.ts +0 -89
- package/src/sourceMap/sourceMapLoader.ts +0 -96
- package/src/viewportPresets.ts +0 -69
- package/src/watchRerunPlanner.ts +0 -77
- /package/dist/{rslib-runtime.js → 612~0.js} +0 -0
- /package/dist/client/{api.d.ts → client/api.d.ts} +0 -0
- /package/dist/client/{locator.d.ts → client/locator.d.ts} +0 -0
package/dist/index.js
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
|
-
import { DEFAULT_TEST_TIMEOUT, FATAL_SIGNALS, PhaseTracker, RSTEST_ENV_SYMBOL_KEY, applyWatchInvalidation, applyWebMockRspackConfig, browserIgnoredRuntimeConfigKeys, buildBrowserCoverageMap, color, createCoverageProvider, createRunnerEventSink, createSilentConsoleController,
|
|
2
|
-
import { existsSync } from "node:fs";
|
|
1
|
+
import { DEFAULT_TEST_TIMEOUT, FATAL_SIGNALS, FIXTURE_CLEANUP_TIMEOUT_MS, PhaseTracker, RSTEST_ENV_SYMBOL_KEY, applyWatchInvalidation, applyWebMockRspackConfig, browserIgnoredRuntimeConfigKeys, buildBrowserCoverageMap, color, createCoverageProvider, createFileCleanupTimeoutResult, createRunnerEventSink, createSilentConsoleController, getNumCpus as browser_getNumCpus, getPrettyConsoleName, getSetupFiles, getTestEntries, hasUserRstestConfigPlugins, initModifyRstestConfigHooks, isDebug, isTTY, loadCoverageProvider, logWatchReadyMessage, logger, pluginMockRuntime, projectRuntimeConfig, resolveProjectBuildCache, resolveShardedEntries, resolveSnapshotPathDefault, resolveWorkerCount, rsbuild, serializableConfig } from "@rstest/core/internal/browser";
|
|
3
2
|
import promises from "node:fs/promises";
|
|
3
|
+
import { dirname, isAbsolute as external_pathe_isAbsolute, join, normalize, relative, resolve as external_pathe_resolve } from "pathe";
|
|
4
|
+
import { existsSync } from "node:fs";
|
|
4
5
|
import { fileURLToPath } from "node:url";
|
|
5
6
|
import { isDeepStrictEqual } from "node:util";
|
|
6
7
|
import open_editor from "open-editor";
|
|
7
|
-
import { dirname, join, normalize, relative, resolve as external_pathe_resolve } from "pathe";
|
|
8
8
|
import sirv from "sirv";
|
|
9
9
|
import { WebSocketServer } from "ws";
|
|
10
|
+
import { AnyMap, TraceMap, encodedMap } from "@jridgewell/trace-mapping";
|
|
10
11
|
import convert_source_map from "convert-source-map";
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
12
|
+
import { randomUUID } from "node:crypto";
|
|
13
|
+
import { __webpack_require__ } from "./612~0.js";
|
|
13
14
|
__webpack_require__.add({
|
|
14
15
|
"../../node_modules/.pnpm/picomatch@4.0.5/node_modules/picomatch/index.js" (module, __unused_rspack_exports, __webpack_require__) {
|
|
15
16
|
const pico = __webpack_require__("../../node_modules/.pnpm/picomatch@4.0.5/node_modules/picomatch/lib/picomatch.js");
|
|
@@ -1654,218 +1655,6 @@ __webpack_require__.add({
|
|
|
1654
1655
|
};
|
|
1655
1656
|
}
|
|
1656
1657
|
});
|
|
1657
|
-
const TYPE_REQUEST = "q";
|
|
1658
|
-
const TYPE_RESPONSE = "s";
|
|
1659
|
-
function createPromiseWithResolvers() {
|
|
1660
|
-
let resolve;
|
|
1661
|
-
let reject;
|
|
1662
|
-
return {
|
|
1663
|
-
promise: new Promise((res, rej)=>{
|
|
1664
|
-
resolve = res;
|
|
1665
|
-
reject = rej;
|
|
1666
|
-
}),
|
|
1667
|
-
resolve,
|
|
1668
|
-
reject
|
|
1669
|
-
};
|
|
1670
|
-
}
|
|
1671
|
-
const random = Math.random.bind(Math);
|
|
1672
|
-
const urlAlphabet = "useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict";
|
|
1673
|
-
function nanoid(size = 21) {
|
|
1674
|
-
let id = "";
|
|
1675
|
-
let i = size;
|
|
1676
|
-
while(i--)id += urlAlphabet[64 * random() | 0];
|
|
1677
|
-
return id;
|
|
1678
|
-
}
|
|
1679
|
-
const DEFAULT_TIMEOUT = 6e4;
|
|
1680
|
-
const defaultSerialize = (i)=>i;
|
|
1681
|
-
const defaultDeserialize = defaultSerialize;
|
|
1682
|
-
const { clearTimeout: dist_clearTimeout, setTimeout: dist_setTimeout } = globalThis;
|
|
1683
|
-
function createBirpc($functions, options) {
|
|
1684
|
-
const { post, on, off = ()=>{}, eventNames = [], serialize = defaultSerialize, deserialize = defaultDeserialize, resolver, bind = "rpc", timeout = DEFAULT_TIMEOUT, proxify = true } = options;
|
|
1685
|
-
let $closed = false;
|
|
1686
|
-
const _rpcPromiseMap = /* @__PURE__ */ new Map();
|
|
1687
|
-
let _promiseInit;
|
|
1688
|
-
let rpc;
|
|
1689
|
-
async function _call(method, args, event, optional) {
|
|
1690
|
-
if ($closed) throw new Error(`[birpc] rpc is closed, cannot call "${method}"`);
|
|
1691
|
-
const req = {
|
|
1692
|
-
m: method,
|
|
1693
|
-
a: args,
|
|
1694
|
-
t: TYPE_REQUEST
|
|
1695
|
-
};
|
|
1696
|
-
if (optional) req.o = true;
|
|
1697
|
-
const send = async (_req)=>post(serialize(_req));
|
|
1698
|
-
if (event) return void await send(req);
|
|
1699
|
-
if (_promiseInit) try {
|
|
1700
|
-
await _promiseInit;
|
|
1701
|
-
} finally{
|
|
1702
|
-
_promiseInit = void 0;
|
|
1703
|
-
}
|
|
1704
|
-
let { promise, resolve, reject } = createPromiseWithResolvers();
|
|
1705
|
-
const id = nanoid();
|
|
1706
|
-
req.i = id;
|
|
1707
|
-
let timeoutId;
|
|
1708
|
-
async function handler(newReq = req) {
|
|
1709
|
-
if (timeout >= 0) {
|
|
1710
|
-
timeoutId = dist_setTimeout(()=>{
|
|
1711
|
-
try {
|
|
1712
|
-
if (options.onTimeoutError?.call(rpc, method, args) !== true) throw new Error(`[birpc] timeout on calling "${method}"`);
|
|
1713
|
-
} catch (e) {
|
|
1714
|
-
reject(e);
|
|
1715
|
-
}
|
|
1716
|
-
_rpcPromiseMap.delete(id);
|
|
1717
|
-
}, timeout);
|
|
1718
|
-
if ("object" == typeof timeoutId) timeoutId = timeoutId.unref?.();
|
|
1719
|
-
}
|
|
1720
|
-
_rpcPromiseMap.set(id, {
|
|
1721
|
-
resolve,
|
|
1722
|
-
reject,
|
|
1723
|
-
timeoutId,
|
|
1724
|
-
method
|
|
1725
|
-
});
|
|
1726
|
-
await send(newReq);
|
|
1727
|
-
return promise;
|
|
1728
|
-
}
|
|
1729
|
-
try {
|
|
1730
|
-
if (options.onRequest) await options.onRequest.call(rpc, req, handler, resolve);
|
|
1731
|
-
else await handler();
|
|
1732
|
-
} catch (e) {
|
|
1733
|
-
if (options.onGeneralError?.call(rpc, e) !== true) throw e;
|
|
1734
|
-
return;
|
|
1735
|
-
} finally{
|
|
1736
|
-
dist_clearTimeout(timeoutId);
|
|
1737
|
-
_rpcPromiseMap.delete(id);
|
|
1738
|
-
}
|
|
1739
|
-
return promise;
|
|
1740
|
-
}
|
|
1741
|
-
const builtinMethods = {
|
|
1742
|
-
$call: (method, ...args)=>_call(method, args, false),
|
|
1743
|
-
$callOptional: (method, ...args)=>_call(method, args, false, true),
|
|
1744
|
-
$callEvent: (method, ...args)=>_call(method, args, true),
|
|
1745
|
-
$callRaw: (options$1)=>_call(options$1.method, options$1.args, options$1.event, options$1.optional),
|
|
1746
|
-
$rejectPendingCalls,
|
|
1747
|
-
get $closed () {
|
|
1748
|
-
return $closed;
|
|
1749
|
-
},
|
|
1750
|
-
get $meta () {
|
|
1751
|
-
return options.meta;
|
|
1752
|
-
},
|
|
1753
|
-
$close,
|
|
1754
|
-
$functions
|
|
1755
|
-
};
|
|
1756
|
-
rpc = proxify ? new Proxy({}, {
|
|
1757
|
-
get (_, method) {
|
|
1758
|
-
if (Object.prototype.hasOwnProperty.call(builtinMethods, method)) return builtinMethods[method];
|
|
1759
|
-
if ("then" === method && !eventNames.includes("then") && !("then" in $functions)) return;
|
|
1760
|
-
const sendEvent = (...args)=>_call(method, args, true);
|
|
1761
|
-
if (eventNames.includes(method)) {
|
|
1762
|
-
sendEvent.asEvent = sendEvent;
|
|
1763
|
-
return sendEvent;
|
|
1764
|
-
}
|
|
1765
|
-
const sendCall = (...args)=>_call(method, args, false);
|
|
1766
|
-
sendCall.asEvent = sendEvent;
|
|
1767
|
-
return sendCall;
|
|
1768
|
-
}
|
|
1769
|
-
}) : builtinMethods;
|
|
1770
|
-
function $close(customError) {
|
|
1771
|
-
$closed = true;
|
|
1772
|
-
_rpcPromiseMap.forEach(({ reject, method })=>{
|
|
1773
|
-
const error = /* @__PURE__ */ new Error(`[birpc] rpc is closed, cannot call "${method}"`);
|
|
1774
|
-
if (customError) {
|
|
1775
|
-
customError.cause ??= error;
|
|
1776
|
-
return reject(customError);
|
|
1777
|
-
}
|
|
1778
|
-
reject(error);
|
|
1779
|
-
});
|
|
1780
|
-
_rpcPromiseMap.clear();
|
|
1781
|
-
off(onMessage);
|
|
1782
|
-
}
|
|
1783
|
-
function $rejectPendingCalls(handler) {
|
|
1784
|
-
const handlerResults = Array.from(_rpcPromiseMap.values()).map(({ method, reject })=>{
|
|
1785
|
-
if (!handler) return reject(/* @__PURE__ */ new Error(`[birpc]: rejected pending call "${method}".`));
|
|
1786
|
-
return handler({
|
|
1787
|
-
method,
|
|
1788
|
-
reject
|
|
1789
|
-
});
|
|
1790
|
-
});
|
|
1791
|
-
_rpcPromiseMap.clear();
|
|
1792
|
-
return handlerResults;
|
|
1793
|
-
}
|
|
1794
|
-
async function onMessage(data, ...extra) {
|
|
1795
|
-
let msg;
|
|
1796
|
-
try {
|
|
1797
|
-
msg = deserialize(data);
|
|
1798
|
-
} catch (e) {
|
|
1799
|
-
if (options.onGeneralError?.call(rpc, e) !== true) throw e;
|
|
1800
|
-
return;
|
|
1801
|
-
}
|
|
1802
|
-
if (msg.t === TYPE_REQUEST) {
|
|
1803
|
-
const { m: method, a: args, o: optional } = msg;
|
|
1804
|
-
let result, error;
|
|
1805
|
-
let fn = await (resolver ? resolver.call(rpc, method, $functions[method]) : $functions[method]);
|
|
1806
|
-
if (optional) fn ||= ()=>void 0;
|
|
1807
|
-
if (fn) try {
|
|
1808
|
-
result = await fn.apply("rpc" === bind ? rpc : $functions, args);
|
|
1809
|
-
} catch (e) {
|
|
1810
|
-
error = e;
|
|
1811
|
-
}
|
|
1812
|
-
else error = /* @__PURE__ */ new Error(`[birpc] function "${method}" not found`);
|
|
1813
|
-
if (msg.i) {
|
|
1814
|
-
if (error && options.onFunctionError) {
|
|
1815
|
-
if (true === options.onFunctionError.call(rpc, error, method, args)) return;
|
|
1816
|
-
}
|
|
1817
|
-
if (!error) try {
|
|
1818
|
-
await post(serialize({
|
|
1819
|
-
t: TYPE_RESPONSE,
|
|
1820
|
-
i: msg.i,
|
|
1821
|
-
r: result
|
|
1822
|
-
}), ...extra);
|
|
1823
|
-
return;
|
|
1824
|
-
} catch (e) {
|
|
1825
|
-
error = e;
|
|
1826
|
-
if (options.onGeneralError?.call(rpc, e, method, args) !== true) throw e;
|
|
1827
|
-
}
|
|
1828
|
-
try {
|
|
1829
|
-
await post(serialize({
|
|
1830
|
-
t: TYPE_RESPONSE,
|
|
1831
|
-
i: msg.i,
|
|
1832
|
-
e: error
|
|
1833
|
-
}), ...extra);
|
|
1834
|
-
} catch (e) {
|
|
1835
|
-
if (options.onGeneralError?.call(rpc, e, method, args) !== true) throw e;
|
|
1836
|
-
}
|
|
1837
|
-
}
|
|
1838
|
-
} else {
|
|
1839
|
-
const { i: ack, r: result, e: error } = msg;
|
|
1840
|
-
const promise = _rpcPromiseMap.get(ack);
|
|
1841
|
-
if (promise) {
|
|
1842
|
-
dist_clearTimeout(promise.timeoutId);
|
|
1843
|
-
if (error) promise.reject(error);
|
|
1844
|
-
else promise.resolve(result);
|
|
1845
|
-
}
|
|
1846
|
-
_rpcPromiseMap.delete(ack);
|
|
1847
|
-
}
|
|
1848
|
-
}
|
|
1849
|
-
_promiseInit = on(onMessage);
|
|
1850
|
-
return rpc;
|
|
1851
|
-
}
|
|
1852
|
-
const DEFAULT_MAX_HEADLESS_WORKERS = 12;
|
|
1853
|
-
const resolveHeadlessWorkerCount = ({ command, maxWorkers, totalTasks, numCpus = browser_getNumCpus() })=>{
|
|
1854
|
-
const base = Math.max(Math.min(DEFAULT_MAX_HEADLESS_WORKERS, numCpus - 1), 1);
|
|
1855
|
-
return resolveWorkerCount({
|
|
1856
|
-
command,
|
|
1857
|
-
maxWorkers,
|
|
1858
|
-
totalTasks,
|
|
1859
|
-
recommended: base,
|
|
1860
|
-
watchRecommended: Math.max(Math.floor(base / 2), 1),
|
|
1861
|
-
numCpus
|
|
1862
|
-
});
|
|
1863
|
-
};
|
|
1864
|
-
const getHeadlessConcurrency = (context, totalTests)=>resolveHeadlessWorkerCount({
|
|
1865
|
-
command: context.command,
|
|
1866
|
-
maxWorkers: context.normalizedConfig.pool.maxWorkers,
|
|
1867
|
-
totalTasks: totalTests
|
|
1868
|
-
});
|
|
1869
1658
|
const toErrorMessage = (error)=>error instanceof Error ? error.message : String(error);
|
|
1870
1659
|
class HostDispatchRouter {
|
|
1871
1660
|
handlers = new Map();
|
|
@@ -1914,6 +1703,12 @@ class HostDispatchRouter {
|
|
|
1914
1703
|
}
|
|
1915
1704
|
}
|
|
1916
1705
|
}
|
|
1706
|
+
const DISPATCH_MESSAGE_TYPE = '__rstest_dispatch__';
|
|
1707
|
+
const DISPATCH_RPC_BRIDGE_NAME = '__rstest_dispatch_rpc__';
|
|
1708
|
+
const DISPATCH_NAMESPACE_BROWSER = 'browser';
|
|
1709
|
+
const DISPATCH_NAMESPACE_FILE_CLEANUP = 'file-cleanup';
|
|
1710
|
+
const DISPATCH_NAMESPACE_SNAPSHOT = 'snapshot';
|
|
1711
|
+
const RSTEST_BROWSER_CACHE_CLEANERS_KEY = '@rstest/browser/cache-cleaners';
|
|
1917
1712
|
const snapshotRequestBuilders = {
|
|
1918
1713
|
resolveSnapshotPath: (id, args)=>({
|
|
1919
1714
|
id,
|
|
@@ -2054,11 +1849,6 @@ const ignoredKeyWarnings = {
|
|
|
2054
1849
|
message: ()=>"Ignoring isolate: false in browser mode: each test file still runs in a fresh context.",
|
|
2055
1850
|
browserOnly: true
|
|
2056
1851
|
},
|
|
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
|
-
},
|
|
2062
1852
|
detectAsyncLeaks: {
|
|
2063
1853
|
isNonDefault: (config)=>true === config.detectAsyncLeaks,
|
|
2064
1854
|
message: ()=>"Ignoring detectAsyncLeaks in browser mode: it relies on node async_hooks."
|
|
@@ -2109,8 +1899,9 @@ const reportUnsupportedBrowserOptions = (context)=>{
|
|
|
2109
1899
|
const globalConfig = context.normalizedConfig;
|
|
2110
1900
|
const { coverage } = globalConfig;
|
|
2111
1901
|
if ('list' !== context.command && coverage.enabled && 'v8' === coverage.provider) {
|
|
2112
|
-
|
|
2113
|
-
|
|
1902
|
+
const unsupportedProject = browserProjects.find((project)=>'chromium' !== project.normalizedConfig.browser.browser);
|
|
1903
|
+
if (unsupportedProject) if (isBrowserOnlyRun) throw new Error(`Coverage provider 'v8' in browser mode requires Chromium. Project ${JSON.stringify(unsupportedProject.name)} uses ${JSON.stringify(unsupportedProject.normalizedConfig.browser.browser)}. Use Chromium or coverage.provider: 'istanbul'.`);
|
|
1904
|
+
else logger.warn(color.yellow(`Coverage provider 'v8' requires Chromium for browser project ${JSON.stringify(unsupportedProject.name)}. Its files will not produce coverage; node projects will still use v8.`));
|
|
2114
1905
|
}
|
|
2115
1906
|
const warnings = new Set();
|
|
2116
1907
|
if ('forks' !== globalConfig.pool.type) warnings.add(`Ignoring pool.type '${globalConfig.pool.type}' in browser mode.`);
|
|
@@ -2141,104 +1932,6 @@ const validateBrowserConfig = (context)=>{
|
|
|
2141
1932
|
}
|
|
2142
1933
|
reportUnsupportedBrowserOptions(context);
|
|
2143
1934
|
};
|
|
2144
|
-
const createHeadedSerialTaskQueue = ()=>{
|
|
2145
|
-
let queue = Promise.resolve();
|
|
2146
|
-
const enqueue = (task)=>{
|
|
2147
|
-
const next = queue.then(task);
|
|
2148
|
-
queue = next.catch(()=>{});
|
|
2149
|
-
return next;
|
|
2150
|
-
};
|
|
2151
|
-
return {
|
|
2152
|
-
enqueue
|
|
2153
|
-
};
|
|
2154
|
-
};
|
|
2155
|
-
const createHeadlessLatestRerunScheduler = (options)=>{
|
|
2156
|
-
let pendingFiles = null;
|
|
2157
|
-
let draining = null;
|
|
2158
|
-
let latestEnqueueVersion = 0;
|
|
2159
|
-
const runDrainLoop = async ()=>{
|
|
2160
|
-
while(pendingFiles){
|
|
2161
|
-
const nextFiles = pendingFiles;
|
|
2162
|
-
pendingFiles = null;
|
|
2163
|
-
try {
|
|
2164
|
-
await options.runFiles(nextFiles);
|
|
2165
|
-
} catch (error) {
|
|
2166
|
-
try {
|
|
2167
|
-
await options.onError?.(error);
|
|
2168
|
-
} catch {}
|
|
2169
|
-
}
|
|
2170
|
-
}
|
|
2171
|
-
};
|
|
2172
|
-
const ensureDrainLoop = ()=>{
|
|
2173
|
-
if (draining) return;
|
|
2174
|
-
draining = runDrainLoop().finally(()=>{
|
|
2175
|
-
draining = null;
|
|
2176
|
-
});
|
|
2177
|
-
};
|
|
2178
|
-
return {
|
|
2179
|
-
async enqueueLatest (files) {
|
|
2180
|
-
const enqueueVersion = ++latestEnqueueVersion;
|
|
2181
|
-
const activeRun = options.getActiveRun();
|
|
2182
|
-
if (activeRun && !options.isRunCancelled(activeRun)) {
|
|
2183
|
-
options.onInterrupt?.(activeRun);
|
|
2184
|
-
options.invalidateActiveRun();
|
|
2185
|
-
await options.interruptActiveRun(activeRun);
|
|
2186
|
-
}
|
|
2187
|
-
if (enqueueVersion !== latestEnqueueVersion) return;
|
|
2188
|
-
pendingFiles = files;
|
|
2189
|
-
ensureDrainLoop();
|
|
2190
|
-
},
|
|
2191
|
-
async whenIdle () {
|
|
2192
|
-
await draining;
|
|
2193
|
-
}
|
|
2194
|
-
};
|
|
2195
|
-
};
|
|
2196
|
-
const attachHeadlessRunnerTransport = async (page, handlers)=>{
|
|
2197
|
-
await page.exposeFunction(DISPATCH_MESSAGE_TYPE, handlers.onDispatchMessage);
|
|
2198
|
-
await page.exposeFunction(DISPATCH_RPC_BRIDGE_NAME, handlers.onDispatchRpc);
|
|
2199
|
-
};
|
|
2200
|
-
const isRecord = (value)=>'object' == typeof value && null !== value;
|
|
2201
|
-
const readString = (value, key, label)=>{
|
|
2202
|
-
const result = value[key];
|
|
2203
|
-
if ('string' != typeof result) throw new Error(`Invalid browser RPC request: ${label} must be a string`);
|
|
2204
|
-
return result;
|
|
2205
|
-
};
|
|
2206
|
-
const readUnknownArray = (value, key, label)=>{
|
|
2207
|
-
const result = value[key];
|
|
2208
|
-
if (!Array.isArray(result)) throw new Error(`Invalid browser RPC request: ${label} must be an array`);
|
|
2209
|
-
return result;
|
|
2210
|
-
};
|
|
2211
|
-
const parseBrowserLocatorIR = (value, label)=>{
|
|
2212
|
-
if (!isRecord(value)) throw new Error(`Invalid browser RPC request: ${label} must be an object`);
|
|
2213
|
-
const steps = value.steps;
|
|
2214
|
-
if (!Array.isArray(steps)) throw new Error(`Invalid browser RPC request: ${label}.steps must be an array`);
|
|
2215
|
-
return value;
|
|
2216
|
-
};
|
|
2217
|
-
const validateBrowserRpcRequest = (payload)=>{
|
|
2218
|
-
if (!isRecord(payload)) throw new Error('Invalid browser RPC request: payload must be an object');
|
|
2219
|
-
const kind = readString(payload, 'kind', 'kind');
|
|
2220
|
-
if ('locator' !== kind && 'expect' !== kind && 'config' !== kind) throw new Error(`Invalid browser RPC request: unsupported kind ${JSON.stringify(kind)}`);
|
|
2221
|
-
const request = {
|
|
2222
|
-
id: readString(payload, 'id', 'id'),
|
|
2223
|
-
testPath: readString(payload, 'testPath', 'testPath'),
|
|
2224
|
-
runId: readString(payload, 'runId', 'runId'),
|
|
2225
|
-
kind,
|
|
2226
|
-
locator: parseBrowserLocatorIR(payload.locator, 'locator'),
|
|
2227
|
-
method: readString(payload, 'method', 'method'),
|
|
2228
|
-
args: readUnknownArray(payload, 'args', 'args')
|
|
2229
|
-
};
|
|
2230
|
-
const isNot = payload.isNot;
|
|
2231
|
-
if (void 0 !== isNot) {
|
|
2232
|
-
if ('boolean' != typeof isNot) throw new Error('Invalid browser RPC request: isNot must be a boolean');
|
|
2233
|
-
request.isNot = isNot;
|
|
2234
|
-
}
|
|
2235
|
-
const timeout = payload.timeout;
|
|
2236
|
-
if (void 0 !== timeout) {
|
|
2237
|
-
if ('number' != typeof timeout) throw new Error('Invalid browser RPC request: timeout must be a number');
|
|
2238
|
-
request.timeout = timeout;
|
|
2239
|
-
}
|
|
2240
|
-
return request;
|
|
2241
|
-
};
|
|
2242
1935
|
const supportedLocatorActions = new Set([
|
|
2243
1936
|
'click',
|
|
2244
1937
|
'dblclick',
|
|
@@ -2660,6 +2353,17 @@ async function launchPlaywrightBrowser({ browserName, headless, providerOptions
|
|
|
2660
2353
|
browser: wrappedBrowser
|
|
2661
2354
|
};
|
|
2662
2355
|
}
|
|
2356
|
+
const asCoveragePage = (page)=>page;
|
|
2357
|
+
const createPlaywrightV8CoverageCollector = ()=>({
|
|
2358
|
+
async start (page) {
|
|
2359
|
+
await asCoveragePage(page).coverage.startJSCoverage({
|
|
2360
|
+
resetOnNavigation: false
|
|
2361
|
+
});
|
|
2362
|
+
},
|
|
2363
|
+
async take (page) {
|
|
2364
|
+
return asCoveragePage(page).coverage.stopJSCoverage();
|
|
2365
|
+
}
|
|
2366
|
+
});
|
|
2663
2367
|
const playwrightProviderImplementation = {
|
|
2664
2368
|
name: 'playwright',
|
|
2665
2369
|
async launchRuntime ({ browserName, headless, providerOptions }) {
|
|
@@ -2669,6 +2373,9 @@ const playwrightProviderImplementation = {
|
|
|
2669
2373
|
providerOptions
|
|
2670
2374
|
});
|
|
2671
2375
|
},
|
|
2376
|
+
createV8CoverageCollector ({ browserName }) {
|
|
2377
|
+
return 'chromium' === browserName ? createPlaywrightV8CoverageCollector() : null;
|
|
2378
|
+
},
|
|
2672
2379
|
async dispatchRpc ({ containerPage, runnerPage, request, timeoutFallbackMs }) {
|
|
2673
2380
|
return dispatchPlaywrightBrowserRpc({
|
|
2674
2381
|
containerPage: containerPage,
|
|
@@ -2686,165 +2393,18 @@ function getBrowserProviderImplementation(provider) {
|
|
|
2686
2393
|
if (!implementation) throw new Error(`Unsupported browser provider: ${String(provider)}`);
|
|
2687
2394
|
return implementation;
|
|
2688
2395
|
}
|
|
2689
|
-
const createCancelSignal = ()=>{
|
|
2690
|
-
let settled = false;
|
|
2691
|
-
let resolveSignal = ()=>{};
|
|
2692
|
-
const signal = new Promise((resolve)=>{
|
|
2693
|
-
resolveSignal = ()=>{
|
|
2694
|
-
if (!settled) {
|
|
2695
|
-
settled = true;
|
|
2696
|
-
resolve();
|
|
2697
|
-
}
|
|
2698
|
-
};
|
|
2699
|
-
});
|
|
2700
|
-
return {
|
|
2701
|
-
signal,
|
|
2702
|
-
resolve: resolveSignal
|
|
2703
|
-
};
|
|
2704
|
-
};
|
|
2705
|
-
const createRunSession = (token)=>{
|
|
2706
|
-
const { signal, resolve } = createCancelSignal();
|
|
2707
|
-
return {
|
|
2708
|
-
token,
|
|
2709
|
-
cancelled: false,
|
|
2710
|
-
cancelSignal: signal,
|
|
2711
|
-
signalCancel: resolve
|
|
2712
|
-
};
|
|
2713
|
-
};
|
|
2714
|
-
class RunSessionLifecycle {
|
|
2715
|
-
currentToken = 0;
|
|
2716
|
-
active = null;
|
|
2717
|
-
get activeSession() {
|
|
2718
|
-
return this.active;
|
|
2719
|
-
}
|
|
2720
|
-
get activeToken() {
|
|
2721
|
-
return this.currentToken;
|
|
2722
|
-
}
|
|
2723
|
-
createSession(factory) {
|
|
2724
|
-
const session = factory(++this.currentToken);
|
|
2725
|
-
this.active = session;
|
|
2726
|
-
return session;
|
|
2727
|
-
}
|
|
2728
|
-
isTokenActive(token) {
|
|
2729
|
-
return token === this.currentToken;
|
|
2730
|
-
}
|
|
2731
|
-
isTokenStale(token) {
|
|
2732
|
-
return !this.isTokenActive(token);
|
|
2733
|
-
}
|
|
2734
|
-
invalidateActiveToken() {
|
|
2735
|
-
this.currentToken += 1;
|
|
2736
|
-
return this.currentToken;
|
|
2737
|
-
}
|
|
2738
|
-
clearIfActive(session) {
|
|
2739
|
-
if (this.active === session) this.active = null;
|
|
2740
|
-
}
|
|
2741
|
-
async cancel(session, options) {
|
|
2742
|
-
const waitForDone = options?.waitForDone ?? true;
|
|
2743
|
-
if (!session.cancelled) {
|
|
2744
|
-
session.cancelled = true;
|
|
2745
|
-
session.signalCancel();
|
|
2746
|
-
await options?.onCancel?.(session);
|
|
2747
|
-
}
|
|
2748
|
-
if (waitForDone) await session.done?.catch(()=>{});
|
|
2749
|
-
}
|
|
2750
|
-
async cancelActive(options) {
|
|
2751
|
-
if (!this.active) return;
|
|
2752
|
-
await this.cancel(this.active, options);
|
|
2753
|
-
}
|
|
2754
|
-
}
|
|
2755
|
-
class RunnerSessionRegistry {
|
|
2756
|
-
nextId = 0;
|
|
2757
|
-
sessionsById = new Map();
|
|
2758
|
-
sessionIdByTestFile = new Map();
|
|
2759
|
-
register(input) {
|
|
2760
|
-
const id = input.id ?? `runner-session-${++this.nextId}`;
|
|
2761
|
-
const createdAt = input.createdAt ?? Date.now();
|
|
2762
|
-
const record = {
|
|
2763
|
-
...input,
|
|
2764
|
-
id,
|
|
2765
|
-
createdAt
|
|
2766
|
-
};
|
|
2767
|
-
this.sessionsById.set(id, record);
|
|
2768
|
-
this.sessionIdByTestFile.set(record.testFile, id);
|
|
2769
|
-
return record;
|
|
2770
|
-
}
|
|
2771
|
-
getById(id) {
|
|
2772
|
-
return this.sessionsById.get(id);
|
|
2773
|
-
}
|
|
2774
|
-
getByTestFile(testFile) {
|
|
2775
|
-
const id = this.sessionIdByTestFile.get(testFile);
|
|
2776
|
-
if (!id) return;
|
|
2777
|
-
return this.sessionsById.get(id);
|
|
2778
|
-
}
|
|
2779
|
-
list() {
|
|
2780
|
-
return Array.from(this.sessionsById.values());
|
|
2781
|
-
}
|
|
2782
|
-
listByRunToken(runToken) {
|
|
2783
|
-
return this.list().filter((session)=>session.runToken === runToken);
|
|
2784
|
-
}
|
|
2785
|
-
deleteById(id) {
|
|
2786
|
-
const record = this.sessionsById.get(id);
|
|
2787
|
-
if (!record) return false;
|
|
2788
|
-
this.sessionsById.delete(id);
|
|
2789
|
-
if (this.sessionIdByTestFile.get(record.testFile) === id) this.sessionIdByTestFile.delete(record.testFile);
|
|
2790
|
-
return true;
|
|
2791
|
-
}
|
|
2792
|
-
deleteByTestFile(testFile) {
|
|
2793
|
-
const id = this.sessionIdByTestFile.get(testFile);
|
|
2794
|
-
if (!id) return false;
|
|
2795
|
-
return this.deleteById(id);
|
|
2796
|
-
}
|
|
2797
|
-
clear() {
|
|
2798
|
-
this.sessionsById.clear();
|
|
2799
|
-
this.sessionIdByTestFile.clear();
|
|
2800
|
-
}
|
|
2801
|
-
}
|
|
2802
|
-
const normalizeJavaScriptUrl = (value, options)=>{
|
|
2803
|
-
try {
|
|
2804
|
-
const url = options?.origin ? new URL(value, options.origin) : new URL(value);
|
|
2805
|
-
if ('http:' !== url.protocol && 'https:' !== url.protocol) return null;
|
|
2806
|
-
url.search = '';
|
|
2807
|
-
url.hash = '';
|
|
2808
|
-
return url.toString();
|
|
2809
|
-
} catch {
|
|
2810
|
-
return null;
|
|
2811
|
-
}
|
|
2812
|
-
};
|
|
2813
|
-
const resolveInlineSourceMap = (code)=>{
|
|
2814
|
-
const converter = convert_source_map.fromSource(code);
|
|
2815
|
-
if (!converter) return null;
|
|
2816
|
-
return converter.toObject();
|
|
2817
|
-
};
|
|
2818
|
-
const fetchSourceMap = async (jsUrl, fetcher)=>{
|
|
2819
|
-
const jsResponse = await fetcher(jsUrl);
|
|
2820
|
-
if (!jsResponse.ok) return null;
|
|
2821
|
-
const code = await jsResponse.text();
|
|
2822
|
-
const inlineMap = resolveInlineSourceMap(code);
|
|
2823
|
-
if (inlineMap) return inlineMap;
|
|
2824
|
-
const mapResponse = await fetcher(`${jsUrl}.map`);
|
|
2825
|
-
if (!mapResponse.ok) return null;
|
|
2826
|
-
return await mapResponse.json();
|
|
2827
|
-
};
|
|
2828
|
-
const loadSourceMapWithCache = async ({ jsUrl, cache, force = false, origin, fetcher = fetch })=>{
|
|
2829
|
-
const normalizedUrl = normalizeJavaScriptUrl(jsUrl, {
|
|
2830
|
-
origin
|
|
2831
|
-
});
|
|
2832
|
-
if (!normalizedUrl) return null;
|
|
2833
|
-
if (!force && cache.has(normalizedUrl)) return cache.get(normalizedUrl) ?? null;
|
|
2834
|
-
try {
|
|
2835
|
-
const sourceMap = await fetchSourceMap(normalizedUrl, fetcher);
|
|
2836
|
-
cache.set(normalizedUrl, sourceMap);
|
|
2837
|
-
return sourceMap;
|
|
2838
|
-
} catch {
|
|
2839
|
-
cache.set(normalizedUrl, null);
|
|
2840
|
-
return null;
|
|
2841
|
-
}
|
|
2842
|
-
};
|
|
2843
2396
|
const serializeTestFiles = (files)=>JSON.stringify(files.map((f)=>`${f.projectName}:${f.testPath}`).sort());
|
|
2844
2397
|
const normalizeTestFiles = (files)=>files.map((file)=>({
|
|
2845
2398
|
...file,
|
|
2846
2399
|
testPath: normalize(file.testPath)
|
|
2847
2400
|
}));
|
|
2401
|
+
const collectTestPaths = (files)=>[
|
|
2402
|
+
...new Set(files.map((file)=>file.testPath))
|
|
2403
|
+
];
|
|
2404
|
+
const collectDeletedTestPaths = (previous, current)=>{
|
|
2405
|
+
const currentPathSet = new Set(current.map((file)=>file.testPath));
|
|
2406
|
+
return previous.map((file)=>file.testPath).filter((testPath)=>!currentPathSet.has(testPath));
|
|
2407
|
+
};
|
|
2848
2408
|
const collectWatchTestFiles = (projectEntries)=>projectEntries.flatMap((entry)=>entry.testFiles.map((testPath)=>({
|
|
2849
2409
|
testPath: normalize(testPath),
|
|
2850
2410
|
projectName: entry.project.name
|
|
@@ -2852,179 +2412,129 @@ const collectWatchTestFiles = (projectEntries)=>projectEntries.flatMap((entry)=>
|
|
|
2852
2412
|
const planWatchRerun = ({ projectEntries, previousTestFiles, affectedTestFiles })=>{
|
|
2853
2413
|
const currentTestFiles = collectWatchTestFiles(projectEntries);
|
|
2854
2414
|
const normalizedPrevious = normalizeTestFiles(previousTestFiles);
|
|
2855
|
-
const
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2415
|
+
const fileSetChanged = serializeTestFiles(currentTestFiles) !== serializeTestFiles(normalizedPrevious);
|
|
2416
|
+
if (fileSetChanged) {
|
|
2417
|
+
const fileSetUpdate = {
|
|
2418
|
+
currentTestFiles,
|
|
2419
|
+
deletedTestPaths: collectDeletedTestPaths(normalizedPrevious, currentTestFiles)
|
|
2420
|
+
};
|
|
2421
|
+
if (0 === currentTestFiles.length) return {
|
|
2422
|
+
fileSetUpdate,
|
|
2423
|
+
decision: {
|
|
2424
|
+
kind: 'empty',
|
|
2425
|
+
message: 'No browser test files remain after update.\n'
|
|
2426
|
+
}
|
|
2427
|
+
};
|
|
2428
|
+
return {
|
|
2429
|
+
fileSetUpdate,
|
|
2430
|
+
decision: {
|
|
2431
|
+
kind: 'rerun',
|
|
2432
|
+
testPaths: collectTestPaths(currentTestFiles),
|
|
2433
|
+
message: `Test file set changed, re-running ${currentTestFiles.length} file(s)...\n`
|
|
2434
|
+
}
|
|
2435
|
+
};
|
|
2436
|
+
}
|
|
2437
|
+
const affectedPathSet = new Set(affectedTestFiles.map((testFile)=>normalize(testFile)));
|
|
2438
|
+
const matchedAffectedFiles = currentTestFiles.filter(({ testPath })=>affectedPathSet.has(testPath));
|
|
2439
|
+
if (0 === matchedAffectedFiles.length) return {
|
|
2440
|
+
decision: {
|
|
2441
|
+
kind: 'idle',
|
|
2442
|
+
message: 'No affected browser test files detected, skipping re-run.\n'
|
|
2443
|
+
}
|
|
2444
|
+
};
|
|
2862
2445
|
return {
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
2446
|
+
decision: {
|
|
2447
|
+
kind: 'rerun',
|
|
2448
|
+
testPaths: collectTestPaths(matchedAffectedFiles),
|
|
2449
|
+
message: `Re-running ${matchedAffectedFiles.length} affected test file(s)...\n`
|
|
2450
|
+
}
|
|
2867
2451
|
};
|
|
2868
2452
|
};
|
|
2453
|
+
const commitWatchFileSetUpdate = (update, watchState, pruneDeletedTestPaths)=>{
|
|
2454
|
+
if (!update) return;
|
|
2455
|
+
if (update.deletedTestPaths.length > 0) pruneDeletedTestPaths(update.deletedTestPaths);
|
|
2456
|
+
watchState.lastTestFiles = update.currentTestFiles;
|
|
2457
|
+
};
|
|
2869
2458
|
const picomatch = __webpack_require__("../../node_modules/.pnpm/picomatch@4.0.5/node_modules/picomatch/index.js");
|
|
2870
2459
|
var picomatch_default = /*#__PURE__*/ __webpack_require__.n(picomatch);
|
|
2871
2460
|
const { createRsbuild: createRsbuild, rspack: rspack } = rsbuild;
|
|
2872
|
-
const
|
|
2461
|
+
const browserRsbuild_dirname = dirname(fileURLToPath(import.meta.url));
|
|
2873
2462
|
const OPTIONS_PLACEHOLDER = '__RSTEST_OPTIONS_PLACEHOLDER__';
|
|
2874
|
-
let nextBrowserFilePid = 1000000000;
|
|
2875
2463
|
const serializeForInlineScript = (value)=>JSON.stringify(value).replace(/</g, '\\u003c').replace(/\u2028/g, '\\u2028').replace(/\u2029/g, '\\u2029');
|
|
2464
|
+
class RstestBrowserRuntimePlugin {
|
|
2465
|
+
apply(compiler) {
|
|
2466
|
+
const { RuntimeModule } = compiler.webpack;
|
|
2467
|
+
class BrowserRuntimeModule extends RuntimeModule {
|
|
2468
|
+
constructor(){
|
|
2469
|
+
super('rstest_browser_runtime');
|
|
2470
|
+
}
|
|
2471
|
+
generate() {
|
|
2472
|
+
return `
|
|
2473
|
+
var __rstest_browser_test_entry_ids__ = Object.create(null);
|
|
2474
|
+
|
|
2475
|
+
__webpack_require__.rstest_register_browser_test_entry = (path, id) => {
|
|
2476
|
+
__rstest_browser_test_entry_ids__[path] = id;
|
|
2477
|
+
};
|
|
2478
|
+
|
|
2479
|
+
function __rstest_clean_browser_test_entry__(testEntryPath) {
|
|
2480
|
+
var testEntryId = __rstest_browser_test_entry_ids__[testEntryPath];
|
|
2481
|
+
if (testEntryId !== undefined) {
|
|
2482
|
+
delete __webpack_module_cache__[testEntryId];
|
|
2483
|
+
}
|
|
2484
|
+
}
|
|
2485
|
+
|
|
2486
|
+
(globalThis[${JSON.stringify(RSTEST_BROWSER_CACHE_CLEANERS_KEY)}] ??= new Set()).add(__rstest_clean_browser_test_entry__);
|
|
2487
|
+
`;
|
|
2488
|
+
}
|
|
2489
|
+
}
|
|
2490
|
+
compiler.hooks.thisCompilation.tap('RstestBrowserRuntimePlugin', (compilation)=>{
|
|
2491
|
+
compilation.hooks.additionalTreeRuntimeRequirements.tap('RstestAddBrowserRuntimePlugin', (chunk)=>{
|
|
2492
|
+
compilation.addRuntimeModule(chunk, new BrowserRuntimeModule());
|
|
2493
|
+
});
|
|
2494
|
+
});
|
|
2495
|
+
}
|
|
2496
|
+
}
|
|
2876
2497
|
const getBrowserProviderOptions = (project)=>{
|
|
2877
2498
|
const browserConfig = project.normalizedConfig.browser;
|
|
2878
2499
|
return browserConfig.providerOptions ?? {};
|
|
2879
2500
|
};
|
|
2880
|
-
const
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
2501
|
+
const createBrowserWatchState = ()=>({
|
|
2502
|
+
lastTestFiles: [],
|
|
2503
|
+
hooksEnabled: false,
|
|
2504
|
+
headedFileSetVersion: 1,
|
|
2505
|
+
invalidation: new Map(),
|
|
2506
|
+
pendingAffectedTestFiles: new Map(),
|
|
2507
|
+
compileStartTimes: new Map(),
|
|
2508
|
+
pendingBuildTimeMs: 0
|
|
2887
2509
|
});
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
|
|
2510
|
+
const drainPendingBuildTime = (watchState)=>{
|
|
2511
|
+
const buildTime = watchState.pendingBuildTimeMs;
|
|
2512
|
+
watchState.pendingBuildTimeMs = 0;
|
|
2513
|
+
return buildTime;
|
|
2514
|
+
};
|
|
2515
|
+
const drainPendingAffectedTestFiles = (watchState)=>{
|
|
2516
|
+
const affected = new Set();
|
|
2517
|
+
for (const files of watchState.pendingAffectedTestFiles.values())for (const file of files)affected.add(file);
|
|
2518
|
+
watchState.pendingAffectedTestFiles.clear();
|
|
2519
|
+
return Array.from(affected);
|
|
2520
|
+
};
|
|
2521
|
+
const resolveViewport = (viewport)=>{
|
|
2522
|
+
if (!viewport) return null;
|
|
2523
|
+
if ('string' == typeof viewport) return resolveBrowserViewportPreset(viewport);
|
|
2524
|
+
if ('number' == typeof viewport.width && Number.isFinite(viewport.width) && viewport.width > 0 && 'number' == typeof viewport.height && Number.isFinite(viewport.height) && viewport.height > 0) return {
|
|
2525
|
+
width: viewport.width,
|
|
2526
|
+
height: viewport.height
|
|
2892
2527
|
};
|
|
2528
|
+
return null;
|
|
2893
2529
|
};
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
onDisconnect;
|
|
2900
|
-
detachActiveSocketListeners = null;
|
|
2901
|
-
constructor(wss, methods, onDisconnect){
|
|
2902
|
-
this.wss = wss;
|
|
2903
|
-
this.methods = methods;
|
|
2904
|
-
this.onDisconnect = onDisconnect;
|
|
2905
|
-
this.setupConnectionHandler();
|
|
2906
|
-
}
|
|
2907
|
-
updateMethods(methods, onDisconnect) {
|
|
2908
|
-
this.methods = methods;
|
|
2909
|
-
this.onDisconnect = onDisconnect;
|
|
2910
|
-
if (this.ws && this.ws.readyState === this.ws.OPEN) this.attachWebSocket(this.ws);
|
|
2911
|
-
}
|
|
2912
|
-
setupConnectionHandler() {
|
|
2913
|
-
this.wss.on('connection', (ws)=>{
|
|
2914
|
-
logger.debug('[Browser UI] Container WebSocket connected');
|
|
2915
|
-
logger.debug(`[Browser UI] Current ws: ${this.ws ? 'exists' : 'null'}, new ws: ${ws ? 'exists' : 'null'}`);
|
|
2916
|
-
this.attachWebSocket(ws);
|
|
2917
|
-
});
|
|
2918
|
-
}
|
|
2919
|
-
attachWebSocket(ws) {
|
|
2920
|
-
this.detachActiveSocketListeners?.();
|
|
2921
|
-
if (this.rpc && !this.rpc.$closed) this.rpc.$close(new Error('Container RPC transport reattached'));
|
|
2922
|
-
this.ws = ws;
|
|
2923
|
-
const messageHandlers = new WeakMap();
|
|
2924
|
-
this.rpc = createBirpc(this.methods, {
|
|
2925
|
-
timeout: -1,
|
|
2926
|
-
post: (data)=>{
|
|
2927
|
-
if (ws.readyState === ws.OPEN) ws.send(JSON.stringify(data));
|
|
2928
|
-
},
|
|
2929
|
-
on: (fn)=>{
|
|
2930
|
-
const handler = (message)=>{
|
|
2931
|
-
try {
|
|
2932
|
-
const data = JSON.parse(message.toString());
|
|
2933
|
-
fn(data);
|
|
2934
|
-
} catch {}
|
|
2935
|
-
};
|
|
2936
|
-
messageHandlers.set(fn, handler);
|
|
2937
|
-
ws.on('message', handler);
|
|
2938
|
-
},
|
|
2939
|
-
off: (fn)=>{
|
|
2940
|
-
const handler = messageHandlers.get(fn);
|
|
2941
|
-
if (!handler) return;
|
|
2942
|
-
ws.off('message', handler);
|
|
2943
|
-
messageHandlers.delete(fn);
|
|
2944
|
-
}
|
|
2945
|
-
});
|
|
2946
|
-
const handleClose = ()=>{
|
|
2947
|
-
if (this.ws === ws) this.ws = null;
|
|
2948
|
-
this.detachActiveSocketListeners?.();
|
|
2949
|
-
this.detachActiveSocketListeners = null;
|
|
2950
|
-
if (this.rpc && !this.rpc.$closed) {
|
|
2951
|
-
const disconnectError = new Error('Browser UI WebSocket disconnected before reload completed');
|
|
2952
|
-
this.rpc.$close(disconnectError);
|
|
2953
|
-
this.onDisconnect?.(disconnectError);
|
|
2954
|
-
}
|
|
2955
|
-
this.rpc = null;
|
|
2956
|
-
};
|
|
2957
|
-
ws.on('close', handleClose);
|
|
2958
|
-
this.detachActiveSocketListeners = ()=>{
|
|
2959
|
-
ws.off('close', handleClose);
|
|
2960
|
-
};
|
|
2961
|
-
}
|
|
2962
|
-
get isConnected() {
|
|
2963
|
-
return null !== this.ws && this.ws.readyState === this.ws.OPEN;
|
|
2964
|
-
}
|
|
2965
|
-
get currentWebSocket() {
|
|
2966
|
-
return this.ws;
|
|
2967
|
-
}
|
|
2968
|
-
reattach(ws) {
|
|
2969
|
-
this.attachWebSocket(ws);
|
|
2970
|
-
}
|
|
2971
|
-
async notifyTestFileUpdate(files) {
|
|
2972
|
-
await this.rpc?.onTestFileUpdate(files);
|
|
2973
|
-
}
|
|
2974
|
-
async updateHostConfig(config) {
|
|
2975
|
-
await this.rpc?.onHostConfigUpdate(config);
|
|
2976
|
-
}
|
|
2977
|
-
async reloadTestFile(testFile, testNamePattern) {
|
|
2978
|
-
logger.debug(`[Browser UI] reloadTestFile called, rpc: ${this.rpc ? 'exists' : 'null'}, ws: ${this.ws ? 'exists' : 'null'}`);
|
|
2979
|
-
if (!this.rpc) throw new Error('Browser UI RPC not available for reloadTestFile');
|
|
2980
|
-
logger.debug(`[Browser UI] Calling reloadTestFile: ${testFile}`);
|
|
2981
|
-
return this.rpc.reloadTestFile(testFile, testNamePattern);
|
|
2982
|
-
}
|
|
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
|
-
};
|
|
3003
|
-
const watchContext = {
|
|
3004
|
-
runtime: null,
|
|
3005
|
-
cleanupRegistered: false,
|
|
3006
|
-
cleanupPromise: null
|
|
3007
|
-
};
|
|
3008
|
-
const resolveViewport = (viewport)=>{
|
|
3009
|
-
if (!viewport) return null;
|
|
3010
|
-
if ('string' == typeof viewport) return resolveBrowserViewportPreset(viewport);
|
|
3011
|
-
if ('number' == typeof viewport.width && Number.isFinite(viewport.width) && viewport.width > 0 && 'number' == typeof viewport.height && Number.isFinite(viewport.height) && viewport.height > 0) return {
|
|
3012
|
-
width: viewport.width,
|
|
3013
|
-
height: viewport.height
|
|
3014
|
-
};
|
|
3015
|
-
return null;
|
|
3016
|
-
};
|
|
3017
|
-
const mapViewportByProject = (projects)=>{
|
|
3018
|
-
const map = new Map();
|
|
3019
|
-
for (const project of projects){
|
|
3020
|
-
const viewport = resolveViewport(project.viewport);
|
|
3021
|
-
if (viewport) map.set(project.name, viewport);
|
|
2530
|
+
const mapViewportByProject = (projects)=>{
|
|
2531
|
+
const map = new Map();
|
|
2532
|
+
for (const project of projects){
|
|
2533
|
+
const viewport = resolveViewport(project.viewport);
|
|
2534
|
+
if (viewport) map.set(project.name, viewport);
|
|
3022
2535
|
}
|
|
3023
2536
|
return map;
|
|
3024
2537
|
};
|
|
3025
|
-
const ensureProcessExitCode = (code)=>{
|
|
3026
|
-
if (void 0 === process.exitCode || 0 === process.exitCode) process.exitCode = code;
|
|
3027
|
-
};
|
|
3028
2538
|
const castArray = (arr)=>{
|
|
3029
2539
|
if (void 0 === arr) return [];
|
|
3030
2540
|
return Array.isArray(arr) ? arr : [
|
|
@@ -3228,7 +2738,6 @@ const getBrowserRsbuildEnvironmentConfig = (project)=>({
|
|
|
3228
2738
|
plugins: project.normalizedConfig.plugins,
|
|
3229
2739
|
root: project.rootPath
|
|
3230
2740
|
});
|
|
3231
|
-
const getMaxTestTimeoutForRpc = (projects)=>Math.max(...projects.map((p)=>p.normalizedConfig.testTimeout ?? DEFAULT_TEST_TIMEOUT));
|
|
3232
2741
|
const getBrowserLaunchOptions = (project)=>({
|
|
3233
2742
|
provider: project.normalizedConfig.browser.provider,
|
|
3234
2743
|
browser: project.normalizedConfig.browser.browser,
|
|
@@ -3247,15 +2756,7 @@ const ensureConsistentBrowserLaunchOptions = (projects)=>{
|
|
|
3247
2756
|
}
|
|
3248
2757
|
return firstOptions;
|
|
3249
2758
|
};
|
|
3250
|
-
const
|
|
3251
|
-
const normalizedTestPath = normalize(testPath);
|
|
3252
|
-
const sortedProjects = [
|
|
3253
|
-
...browserProjects
|
|
3254
|
-
].sort((a, b)=>b.rootPath.length - a.rootPath.length);
|
|
3255
|
-
for (const project of sortedProjects)if (normalizedTestPath.startsWith(project.rootPath)) return project.provider;
|
|
3256
|
-
throw new Error(`Cannot resolve browser provider for test path: ${JSON.stringify(testPath)}. Known project roots: ${JSON.stringify(sortedProjects.map((p)=>p.rootPath))}`);
|
|
3257
|
-
};
|
|
3258
|
-
const collectProjectEntries = async (context, browserProjects = getBrowserProjects(context))=>Promise.all(browserProjects.map(async (project)=>{
|
|
2759
|
+
const browserRsbuild_collectProjectEntries = async (context, browserProjects = getBrowserProjects(context))=>Promise.all(browserProjects.map(async (project)=>{
|
|
3259
2760
|
const { normalizedConfig: { include, exclude, includeSource, setupFiles } } = project;
|
|
3260
2761
|
const tests = await getTestEntries({
|
|
3261
2762
|
include,
|
|
@@ -3273,16 +2774,16 @@ const collectProjectEntries = async (context, browserProjects = getBrowserProjec
|
|
|
3273
2774
|
testFiles: Object.values(tests)
|
|
3274
2775
|
};
|
|
3275
2776
|
}));
|
|
3276
|
-
const
|
|
2777
|
+
const resolveBrowserDistFile = (relativePath)=>{
|
|
3277
2778
|
const candidates = [
|
|
3278
|
-
external_pathe_resolve(
|
|
3279
|
-
external_pathe_resolve(
|
|
2779
|
+
external_pathe_resolve(browserRsbuild_dirname, relativePath),
|
|
2780
|
+
external_pathe_resolve(browserRsbuild_dirname, '../dist', relativePath)
|
|
3280
2781
|
];
|
|
3281
2782
|
for (const candidate of candidates)if (existsSync(candidate)) return candidate;
|
|
3282
|
-
throw new Error(`Unable to resolve browser
|
|
2783
|
+
throw new Error(`Unable to resolve browser dist file: ${relativePath}`);
|
|
3283
2784
|
};
|
|
3284
2785
|
const resolveContainerDist = ()=>{
|
|
3285
|
-
const distPath = external_pathe_resolve(
|
|
2786
|
+
const distPath = external_pathe_resolve(browserRsbuild_dirname, 'browser-container');
|
|
3286
2787
|
if (existsSync(distPath)) return distPath;
|
|
3287
2788
|
throw new Error(`Browser container build not found at ${distPath}. Please run "pnpm --filter @rstest/browser build".`);
|
|
3288
2789
|
};
|
|
@@ -3424,27 +2925,7 @@ const destroyBrowserRuntime = async (runtime)=>{
|
|
|
3424
2925
|
force: true
|
|
3425
2926
|
}).catch(()=>{});
|
|
3426
2927
|
};
|
|
3427
|
-
const
|
|
3428
|
-
if (watchContext.cleanupPromise) return watchContext.cleanupPromise;
|
|
3429
|
-
watchContext.cleanupPromise = (async ()=>{
|
|
3430
|
-
if (!watchContext.runtime) return;
|
|
3431
|
-
await destroyBrowserRuntime(watchContext.runtime);
|
|
3432
|
-
watchContext.runtime = null;
|
|
3433
|
-
})();
|
|
3434
|
-
return watchContext.cleanupPromise;
|
|
3435
|
-
};
|
|
3436
|
-
const registerWatchCleanup = (embedded)=>{
|
|
3437
|
-
if (watchContext.cleanupRegistered) return;
|
|
3438
|
-
watchContext.cleanupRegistered = true;
|
|
3439
|
-
if (embedded) return;
|
|
3440
|
-
for (const signal of FATAL_SIGNALS)process.once(signal, ()=>{
|
|
3441
|
-
cleanupWatchRuntime();
|
|
3442
|
-
});
|
|
3443
|
-
process.once('exit', ()=>{
|
|
3444
|
-
cleanupWatchRuntime();
|
|
3445
|
-
});
|
|
3446
|
-
};
|
|
3447
|
-
const createBrowserRuntime = async ({ context, projectEntries: initialProjectEntries, browserProjects, shardedEntries, freezeShardedEntries, tempDir, isWatchMode, onTriggerRerun, containerDistPath, containerDevServer, forceHeadless, skipProviderLaunch, appliedModifyRstestConfigEnvironments })=>{
|
|
2928
|
+
const createBrowserRuntime = async ({ context, projectEntries: initialProjectEntries, browserProjects, shardedEntries, freezeShardedEntries, tempDir, isWatchMode, containerDistPath, containerDevServer, forceHeadless, skipProviderLaunch, appliedModifyRstestConfigEnvironments })=>{
|
|
3448
2929
|
const containerHtmlTemplate = containerDistPath ? await promises.readFile(join(containerDistPath, 'index.html'), 'utf-8') : null;
|
|
3449
2930
|
let injectedContainerHtml = null;
|
|
3450
2931
|
let serializedOptions = 'null';
|
|
@@ -3508,7 +2989,7 @@ const createBrowserRuntime = async ({ context, projectEntries: initialProjectEnt
|
|
|
3508
2989
|
};
|
|
3509
2990
|
const browserRuntimePath = fileURLToPath(import.meta.resolve('@rstest/core/internal/browser-runtime'));
|
|
3510
2991
|
const staticRstestAliases = {
|
|
3511
|
-
'@rstest/browser':
|
|
2992
|
+
'@rstest/browser': resolveBrowserDistFile('client/index.js'),
|
|
3512
2993
|
'@rstest/core/internal/browser-runtime': browserRuntimePath
|
|
3513
2994
|
};
|
|
3514
2995
|
const rstestEnvDefine = `globalThis[Symbol.for(${JSON.stringify(RSTEST_ENV_SYMBOL_KEY)})]`;
|
|
@@ -3596,7 +3077,7 @@ const createBrowserRuntime = async ({ context, projectEntries: initialProjectEnt
|
|
|
3596
3077
|
modules: virtualManifestModules
|
|
3597
3078
|
});
|
|
3598
3079
|
const rstestInternalAliases = {
|
|
3599
|
-
|
|
3080
|
+
__rstest_virtual_browser_manifest__: manifestPath,
|
|
3600
3081
|
...staticRstestAliases
|
|
3601
3082
|
};
|
|
3602
3083
|
const isHeadless = forceHeadless || project.normalizedConfig.browser.headless;
|
|
@@ -3634,6 +3115,17 @@ const createBrowserRuntime = async ({ context, projectEntries: initialProjectEnt
|
|
|
3634
3115
|
{
|
|
3635
3116
|
name: 'rstest:browser-user-config',
|
|
3636
3117
|
setup (api) {
|
|
3118
|
+
if ('list' === context.command) {
|
|
3119
|
+
let testEntryPaths;
|
|
3120
|
+
const getTestEntryPaths = ()=>testEntryPaths ??= new Set(getProjectEntry(project)?.testFiles.map(normalize) ?? []);
|
|
3121
|
+
api.transform({
|
|
3122
|
+
test: (resourcePath)=>getTestEntryPaths().has(normalize(resourcePath))
|
|
3123
|
+
}, ({ code, resourcePath })=>({
|
|
3124
|
+
code: `${code}\nif (__webpack_require__.rstest_register_browser_test_entry && __webpack_module__.id != null) {
|
|
3125
|
+
__webpack_require__.rstest_register_browser_test_entry(${JSON.stringify(normalize(resourcePath))}, __webpack_module__.id);
|
|
3126
|
+
}`
|
|
3127
|
+
}));
|
|
3128
|
+
}
|
|
3637
3129
|
api.expose?.('rstest:browser', {
|
|
3638
3130
|
registerDispatchHandler: (namespace, handler)=>{
|
|
3639
3131
|
dispatchHandlers.set(namespace, handler);
|
|
@@ -3661,8 +3153,7 @@ const createBrowserRuntime = async ({ context, projectEntries: initialProjectEnt
|
|
|
3661
3153
|
source: {
|
|
3662
3154
|
define: {
|
|
3663
3155
|
'process.env': rstestEnvDefine,
|
|
3664
|
-
'import.meta.env': rstestEnvDefine
|
|
3665
|
-
'import.meta.rstest': importMetaRstestDefine('web')
|
|
3156
|
+
'import.meta.env': rstestEnvDefine
|
|
3666
3157
|
}
|
|
3667
3158
|
},
|
|
3668
3159
|
output: {
|
|
@@ -3690,6 +3181,7 @@ const createBrowserRuntime = async ({ context, projectEntries: initialProjectEnt
|
|
|
3690
3181
|
});
|
|
3691
3182
|
rspackConfig.lazyCompilation = enableHmr ? createBrowserLazyCompilationConfig(setupFiles) : false;
|
|
3692
3183
|
rspackConfig.plugins = rspackConfig.plugins || [];
|
|
3184
|
+
rspackConfig.plugins.push(new RstestBrowserRuntimePlugin());
|
|
3693
3185
|
rspackConfig.plugins.push(virtualManifestPlugin);
|
|
3694
3186
|
applyDefaultWatchOptions(rspackConfig, isWatchMode);
|
|
3695
3187
|
const browserRuntimeDir = dirname(browserRuntimePath);
|
|
@@ -3706,7 +3198,7 @@ const createBrowserRuntime = async ({ context, projectEntries: initialProjectEnt
|
|
|
3706
3198
|
});
|
|
3707
3199
|
merged.source = merged.source || {};
|
|
3708
3200
|
merged.source.entry = {
|
|
3709
|
-
runner:
|
|
3201
|
+
runner: resolveBrowserDistFile('client/runner.js')
|
|
3710
3202
|
};
|
|
3711
3203
|
return merged;
|
|
3712
3204
|
},
|
|
@@ -3715,7 +3207,7 @@ const createBrowserRuntime = async ({ context, projectEntries: initialProjectEnt
|
|
|
3715
3207
|
}
|
|
3716
3208
|
}
|
|
3717
3209
|
]);
|
|
3718
|
-
if (isWatchMode
|
|
3210
|
+
if (isWatchMode) rsbuildInstance.addPlugins([
|
|
3719
3211
|
{
|
|
3720
3212
|
name: 'rstest:browser-watch',
|
|
3721
3213
|
setup (api) {
|
|
@@ -3731,7 +3223,7 @@ const createBrowserRuntime = async ({ context, projectEntries: initialProjectEnt
|
|
|
3731
3223
|
if (watchState.hooksEnabled) watchState.pendingBuildTimeMs = Math.max(watchState.pendingBuildTimeMs, Date.now() - compileStart);
|
|
3732
3224
|
}
|
|
3733
3225
|
if (stats) {
|
|
3734
|
-
const [projectEntry] = await
|
|
3226
|
+
const [projectEntry] = await browserRsbuild_collectProjectEntries(context, [
|
|
3735
3227
|
project
|
|
3736
3228
|
]);
|
|
3737
3229
|
const entryTestFiles = new Set(collectWatchTestFiles(projectEntry ? [
|
|
@@ -3760,7 +3252,7 @@ const createBrowserRuntime = async ({ context, projectEntries: initialProjectEnt
|
|
|
3760
3252
|
}
|
|
3761
3253
|
}
|
|
3762
3254
|
if (!watchState.hooksEnabled) return;
|
|
3763
|
-
await
|
|
3255
|
+
await watchState.triggerRerun?.();
|
|
3764
3256
|
});
|
|
3765
3257
|
}
|
|
3766
3258
|
}
|
|
@@ -3909,166 +3401,1661 @@ async function resolveProjectEntries(context, shardedEntries, browserProjects) {
|
|
|
3909
3401
|
}
|
|
3910
3402
|
return projectEntries;
|
|
3911
3403
|
}
|
|
3912
|
-
return
|
|
3404
|
+
return browserRsbuild_collectProjectEntries(context, browserProjects);
|
|
3913
3405
|
}
|
|
3914
|
-
const
|
|
3915
|
-
|
|
3916
|
-
|
|
3917
|
-
|
|
3918
|
-
|
|
3919
|
-
|
|
3920
|
-
|
|
3921
|
-
|
|
3922
|
-
|
|
3923
|
-
const isHttpLikeFile = (file)=>/^https?:\/\//.test(file);
|
|
3924
|
-
const resolveBrowserSourcemap = async (sourcePath)=>{
|
|
3925
|
-
if (!isHttpLikeFile(sourcePath)) return {
|
|
3926
|
-
handled: false,
|
|
3927
|
-
sourcemap: null
|
|
3928
|
-
};
|
|
3929
|
-
const normalizedUrl = normalizeJavaScriptUrl(sourcePath);
|
|
3930
|
-
if (!normalizedUrl) return {
|
|
3931
|
-
handled: true,
|
|
3932
|
-
sourcemap: null
|
|
3933
|
-
};
|
|
3934
|
-
if (browserSourceMapCache.has(normalizedUrl)) return {
|
|
3935
|
-
handled: true,
|
|
3936
|
-
sourcemap: browserSourceMapCache.get(normalizedUrl) ?? null
|
|
3937
|
-
};
|
|
3938
|
-
return {
|
|
3939
|
-
handled: true,
|
|
3940
|
-
sourcemap: await loadSourceMapWithCache({
|
|
3941
|
-
jsUrl: normalizedUrl,
|
|
3942
|
-
cache: browserSourceMapCache
|
|
3943
|
-
})
|
|
3944
|
-
};
|
|
3945
|
-
};
|
|
3946
|
-
const getBrowserSourcemap = async (sourcePath)=>{
|
|
3947
|
-
const result = await resolveBrowserSourcemap(sourcePath);
|
|
3948
|
-
return result.handled ? result.sourcemap : null;
|
|
3949
|
-
};
|
|
3950
|
-
const buildErrorResult = async (error, close)=>{
|
|
3951
|
-
const elapsed = Math.max(0, Date.now() - buildStart);
|
|
3952
|
-
const errorResult = {
|
|
3953
|
-
results: [],
|
|
3954
|
-
testResults: [],
|
|
3955
|
-
duration: {
|
|
3956
|
-
totalTime: elapsed,
|
|
3957
|
-
buildTime: elapsed,
|
|
3958
|
-
testTime: 0
|
|
3959
|
-
},
|
|
3960
|
-
hasFailure: true,
|
|
3961
|
-
unhandledErrors: [
|
|
3962
|
-
error
|
|
3963
|
-
],
|
|
3964
|
-
getSourcemap: getBrowserSourcemap,
|
|
3965
|
-
resolveSourcemap: resolveBrowserSourcemap,
|
|
3966
|
-
close
|
|
3967
|
-
};
|
|
3968
|
-
if (isWatchMode) for (const reporter of context.reporters)await reporter.onTestRunEnd?.({
|
|
3969
|
-
results: [],
|
|
3970
|
-
testResults: [],
|
|
3971
|
-
duration: errorResult.duration,
|
|
3972
|
-
snapshotSummary: context.snapshotManager.summary,
|
|
3973
|
-
getSourcemap: getBrowserSourcemap,
|
|
3974
|
-
unhandledErrors: errorResult.unhandledErrors
|
|
3975
|
-
});
|
|
3976
|
-
return errorResult;
|
|
3977
|
-
};
|
|
3978
|
-
const toError = (error)=>error instanceof Error ? error : new Error(String(error));
|
|
3979
|
-
const failWithError = async (error, cleanup)=>{
|
|
3980
|
-
if (isWatchMode) ensureProcessExitCode(1);
|
|
3981
|
-
const normalizedError = toError(error);
|
|
3982
|
-
if (cleanup && !isWatchMode) return buildErrorResult(normalizedError, cleanup);
|
|
3983
|
-
try {
|
|
3984
|
-
return await buildErrorResult(normalizedError);
|
|
3985
|
-
} finally{
|
|
3986
|
-
await cleanup?.();
|
|
3987
|
-
}
|
|
3988
|
-
};
|
|
3989
|
-
const collectDeletedTestPaths = (previous, current)=>{
|
|
3990
|
-
const currentPathSet = new Set(current.map((file)=>file.testPath));
|
|
3991
|
-
return previous.map((file)=>file.testPath).filter((testPath)=>!currentPathSet.has(testPath));
|
|
3992
|
-
};
|
|
3993
|
-
const notifyTestRunStart = async ()=>{
|
|
3994
|
-
if (!isWatchMode) return;
|
|
3995
|
-
for (const reporter of context.reporters)await reporter.onTestRunStart?.();
|
|
3996
|
-
};
|
|
3997
|
-
const coverageConfig = browserProjects.find((project)=>project.normalizedConfig.coverage?.enabled)?.normalizedConfig.coverage;
|
|
3998
|
-
const coverageProvider = coverageConfig?.enabled ? await createCoverageProvider(coverageConfig, context.rootPath) : null;
|
|
3999
|
-
const notifyTestRunEnd = async ({ duration, coverage })=>{
|
|
4000
|
-
if (!isWatchMode) return;
|
|
4001
|
-
for (const reporter of context.reporters)await reporter.onTestRunEnd?.({
|
|
4002
|
-
results: context.reporterResults.results,
|
|
4003
|
-
coverage,
|
|
4004
|
-
testResults: context.reporterResults.testResults,
|
|
4005
|
-
duration,
|
|
4006
|
-
snapshotSummary: context.snapshotManager.summary,
|
|
4007
|
-
getSourcemap: getBrowserSourcemap
|
|
4008
|
-
});
|
|
4009
|
-
};
|
|
4010
|
-
const containerDevServerEnv = process.env.RSTEST_CONTAINER_DEV_SERVER;
|
|
4011
|
-
let containerDevServer;
|
|
4012
|
-
let containerDistPath;
|
|
4013
|
-
if (!useHeadlessDirect) {
|
|
4014
|
-
if (containerDevServerEnv) try {
|
|
4015
|
-
containerDevServer = new URL(containerDevServerEnv).toString();
|
|
4016
|
-
logger.debug(`[Browser UI] Using dev server for container: ${containerDevServer}`);
|
|
4017
|
-
} catch (error) {
|
|
4018
|
-
const originalError = toError(error);
|
|
4019
|
-
originalError.message = `Invalid RSTEST_CONTAINER_DEV_SERVER value: ${originalError.message}`;
|
|
4020
|
-
return failWithError(originalError);
|
|
4021
|
-
}
|
|
4022
|
-
if (!containerDevServer) try {
|
|
4023
|
-
containerDistPath = resolveContainerDist();
|
|
4024
|
-
} catch (error) {
|
|
4025
|
-
return failWithError(error);
|
|
4026
|
-
}
|
|
3406
|
+
const flattenSourceMap = (sourceMap, sourceMapUrl)=>encodedMap(new AnyMap(sourceMap, sourceMapUrl));
|
|
3407
|
+
const normalizeJavaScriptUrl = (value, options)=>{
|
|
3408
|
+
try {
|
|
3409
|
+
const url = options?.origin ? new URL(value, options.origin) : new URL(value);
|
|
3410
|
+
if ('http:' !== url.protocol && 'https:' !== url.protocol) return null;
|
|
3411
|
+
url.hash = '';
|
|
3412
|
+
return url.toString();
|
|
3413
|
+
} catch {
|
|
3414
|
+
return null;
|
|
4027
3415
|
}
|
|
4028
|
-
|
|
4029
|
-
|
|
4030
|
-
|
|
4031
|
-
|
|
4032
|
-
|
|
4033
|
-
|
|
4034
|
-
|
|
4035
|
-
testResults: [],
|
|
4036
|
-
duration: {
|
|
4037
|
-
totalTime: elapsed,
|
|
4038
|
-
buildTime: elapsed,
|
|
4039
|
-
testTime: 0
|
|
4040
|
-
},
|
|
4041
|
-
hasFailure: false,
|
|
4042
|
-
getSourcemap: getBrowserSourcemap,
|
|
4043
|
-
resolveSourcemap: resolveBrowserSourcemap
|
|
4044
|
-
};
|
|
4045
|
-
};
|
|
4046
|
-
const reportEmptyTestSet = ()=>{
|
|
4047
|
-
const code = context.normalizedConfig.passWithNoTests ? 0 : 1;
|
|
4048
|
-
if (isWatchMode || !allowEmptyRun) {
|
|
4049
|
-
const message = getNoTestFilesMessage({
|
|
4050
|
-
context,
|
|
4051
|
-
code,
|
|
4052
|
-
defaultMessage: `No test files found, exiting with code ${code}.`
|
|
4053
|
-
});
|
|
4054
|
-
if (0 === code) logger.log(color.yellow(message));
|
|
4055
|
-
else logger.error(color.red(message));
|
|
4056
|
-
if (context.relatedFilters?.length) logger.log(color.gray('related: '), context.relatedFilters.join(color.gray(', ')));
|
|
4057
|
-
else if (context.fileFilters?.length) logger.log(color.gray('filter: '), context.fileFilters.join(color.gray(', ')));
|
|
4058
|
-
}
|
|
4059
|
-
if (isWatchMode && 0 !== code && !allowEmptyRun) ensureProcessExitCode(code);
|
|
4060
|
-
};
|
|
4061
|
-
if (0 === totalTests && !shouldInitializeEmptyBrowserHooks) {
|
|
4062
|
-
reportEmptyTestSet();
|
|
4063
|
-
return allowEmptyRun ? createEmptyRunResult() : void 0;
|
|
3416
|
+
};
|
|
3417
|
+
const resolveInlineSourceMap = (code, sourceMapUrl)=>{
|
|
3418
|
+
try {
|
|
3419
|
+
const converter = convert_source_map.fromSource(code);
|
|
3420
|
+
return converter ? flattenSourceMap(converter.toObject(), sourceMapUrl) : null;
|
|
3421
|
+
} catch {
|
|
3422
|
+
return null;
|
|
4064
3423
|
}
|
|
4065
|
-
|
|
4066
|
-
|
|
4067
|
-
const
|
|
3424
|
+
};
|
|
3425
|
+
const loadSourceMapForSource = async ({ jsUrl, signal, source, fetcher = fetch })=>{
|
|
3426
|
+
const normalizedUrl = normalizeJavaScriptUrl(jsUrl);
|
|
3427
|
+
if (!normalizedUrl) return null;
|
|
3428
|
+
try {
|
|
3429
|
+
const matches = [
|
|
3430
|
+
...source.matchAll(convert_source_map.mapFileCommentRegex)
|
|
3431
|
+
];
|
|
3432
|
+
const finalMatch = matches.at(-1);
|
|
3433
|
+
const sourceMapUrl = finalMatch?.[1] ?? finalMatch?.[2]?.trim();
|
|
3434
|
+
if (!sourceMapUrl || sourceMapUrl.startsWith('data:')) return null;
|
|
3435
|
+
const resolvedSourceMapUrl = new URL(sourceMapUrl, normalizedUrl);
|
|
3436
|
+
if ('http:' !== resolvedSourceMapUrl.protocol && 'https:' !== resolvedSourceMapUrl.protocol) return null;
|
|
3437
|
+
const mapResponse = await fetcher(resolvedSourceMapUrl.href, signal ? {
|
|
3438
|
+
signal
|
|
3439
|
+
} : void 0);
|
|
3440
|
+
return mapResponse.ok ? {
|
|
3441
|
+
sourceMap: flattenSourceMap(await mapResponse.json(), resolvedSourceMapUrl.href),
|
|
3442
|
+
sourceMapUrl: resolvedSourceMapUrl.href
|
|
3443
|
+
} : null;
|
|
3444
|
+
} catch {
|
|
3445
|
+
return null;
|
|
3446
|
+
}
|
|
3447
|
+
};
|
|
3448
|
+
const fetchSourceMap = async (jsUrl, fetcher)=>{
|
|
3449
|
+
const jsResponse = await fetcher(jsUrl);
|
|
3450
|
+
if (!jsResponse.ok) return null;
|
|
3451
|
+
const code = await jsResponse.text();
|
|
3452
|
+
const inlineMap = resolveInlineSourceMap(code, jsUrl);
|
|
3453
|
+
if (inlineMap) return inlineMap;
|
|
3454
|
+
const mapUrl = new URL(jsUrl);
|
|
3455
|
+
mapUrl.pathname += '.map';
|
|
3456
|
+
const mapResponse = await fetcher(mapUrl.href);
|
|
3457
|
+
if (!mapResponse.ok) return null;
|
|
3458
|
+
return flattenSourceMap(await mapResponse.json(), mapUrl.href);
|
|
3459
|
+
};
|
|
3460
|
+
const loadSourceMapWithCache = async ({ jsUrl, cache, force = false, origin, fetcher = fetch })=>{
|
|
3461
|
+
const normalizedUrl = normalizeJavaScriptUrl(jsUrl, {
|
|
3462
|
+
origin
|
|
3463
|
+
});
|
|
3464
|
+
if (!normalizedUrl) return null;
|
|
3465
|
+
if (!force && cache.has(normalizedUrl)) return cache.get(normalizedUrl) ?? null;
|
|
3466
|
+
try {
|
|
3467
|
+
const sourceMap = await fetchSourceMap(normalizedUrl, fetcher);
|
|
3468
|
+
cache.set(normalizedUrl, sourceMap);
|
|
3469
|
+
return sourceMap;
|
|
3470
|
+
} catch {
|
|
3471
|
+
cache.set(normalizedUrl, null);
|
|
3472
|
+
return null;
|
|
3473
|
+
}
|
|
3474
|
+
};
|
|
3475
|
+
const resolveCoverageResourceUrl = (value)=>{
|
|
3476
|
+
try {
|
|
3477
|
+
const url = new URL(value);
|
|
3478
|
+
return 'http:' === url.protocol || 'https:' === url.protocol ? url.href : null;
|
|
3479
|
+
} catch {
|
|
3480
|
+
return null;
|
|
3481
|
+
}
|
|
3482
|
+
};
|
|
3483
|
+
const resolveProjectHttpSource = (source, rootPath, projectOrigin)=>{
|
|
3484
|
+
let sourceUrl;
|
|
3485
|
+
try {
|
|
3486
|
+
sourceUrl = new URL(source);
|
|
3487
|
+
} catch {
|
|
3488
|
+
return source;
|
|
3489
|
+
}
|
|
3490
|
+
if ('http:' !== sourceUrl.protocol && 'https:' !== sourceUrl.protocol || sourceUrl.origin !== projectOrigin) return source;
|
|
3491
|
+
let sourcePath = sourceUrl.pathname;
|
|
3492
|
+
try {
|
|
3493
|
+
sourcePath = decodeURIComponent(sourcePath);
|
|
3494
|
+
} catch {}
|
|
3495
|
+
return external_pathe_resolve(rootPath, sourcePath.replace(/^\/+/, ''));
|
|
3496
|
+
};
|
|
3497
|
+
const normalizeSourceMap = (sourceMap, rootPath, sourceMapUrl, scriptOrigin, projectOrigin)=>{
|
|
3498
|
+
const webpackSourceRoot = 'string' == typeof sourceMap.sourceRoot && sourceMap.sourceRoot.startsWith('webpack:') ? sourceMap.sourceRoot : void 0;
|
|
3499
|
+
const resolvedSources = new TraceMap(sourceMap, sourceMapUrl).resolvedSources;
|
|
3500
|
+
const { sourceRoot: _, ...sourceMapWithoutRoot } = sourceMap;
|
|
3501
|
+
return {
|
|
3502
|
+
...sourceMapWithoutRoot,
|
|
3503
|
+
sources: sourceMap.sources.map((source, index)=>{
|
|
3504
|
+
if ('string' != typeof source) return source;
|
|
3505
|
+
if ('rstest runtime' === source || source.startsWith('webpack/runtime/') || source.startsWith('data:') || source.startsWith('blob:')) return source;
|
|
3506
|
+
if (external_pathe_isAbsolute(source)) return source;
|
|
3507
|
+
let sourceUrl;
|
|
3508
|
+
try {
|
|
3509
|
+
sourceUrl = webpackSourceRoot ? new URL(source, webpackSourceRoot) : new URL(source);
|
|
3510
|
+
} catch {
|
|
3511
|
+
const resolvedSource = resolvedSources[index] ?? source;
|
|
3512
|
+
return resolveProjectHttpSource(resolvedSource, rootPath, projectOrigin);
|
|
3513
|
+
}
|
|
3514
|
+
if ('webpack:' !== sourceUrl.protocol) {
|
|
3515
|
+
const resolvedSource = resolvedSources[index] ?? source;
|
|
3516
|
+
return resolveProjectHttpSource(resolvedSource, rootPath, projectOrigin);
|
|
3517
|
+
}
|
|
3518
|
+
let sourcePath = sourceUrl.pathname;
|
|
3519
|
+
try {
|
|
3520
|
+
sourcePath = decodeURIComponent(sourcePath);
|
|
3521
|
+
} catch {}
|
|
3522
|
+
sourcePath = sourcePath.replace(/^\/+/, '');
|
|
3523
|
+
if ('rstest runtime' === sourcePath || sourcePath.startsWith('data:') || sourcePath.startsWith('blob:')) return sourcePath;
|
|
3524
|
+
if (sourcePath.startsWith('webpack/runtime/')) return source;
|
|
3525
|
+
return scriptOrigin === projectOrigin ? external_pathe_resolve(rootPath, sourcePath) : new URL(sourcePath, `${scriptOrigin}/`).href;
|
|
3526
|
+
})
|
|
3527
|
+
};
|
|
3528
|
+
};
|
|
3529
|
+
const takeBrowserV8Coverage = async ({ collector, page, projectUrl, rootPath, fetchTimeout, sourceMapCache, resourceStore })=>{
|
|
3530
|
+
const rawEntries = await collector.take(page);
|
|
3531
|
+
const projectOrigin = new URL(projectUrl).origin;
|
|
3532
|
+
const entries = [];
|
|
3533
|
+
const assetFiles = {};
|
|
3534
|
+
const sourceMaps = {};
|
|
3535
|
+
await Promise.all(rawEntries.map(async (entry)=>{
|
|
3536
|
+
const url = resolveCoverageResourceUrl(entry.url);
|
|
3537
|
+
const cacheUrl = normalizeJavaScriptUrl(entry.url);
|
|
3538
|
+
if (!url || !cacheUrl || !entry.source) return;
|
|
3539
|
+
const inlineSourceMap = resolveInlineSourceMap(entry.source, url);
|
|
3540
|
+
const loadedSourceMap = inlineSourceMap ? null : await loadSourceMapForSource({
|
|
3541
|
+
jsUrl: url,
|
|
3542
|
+
signal: fetchTimeout > 0 ? AbortSignal.timeout(fetchTimeout) : void 0,
|
|
3543
|
+
source: entry.source
|
|
3544
|
+
});
|
|
3545
|
+
const sourceMap = inlineSourceMap ?? loadedSourceMap?.sourceMap;
|
|
3546
|
+
const normalizedSourceMap = sourceMap ? normalizeSourceMap(sourceMap, rootPath, loadedSourceMap?.sourceMapUrl ?? url, new URL(url).origin, projectOrigin) : void 0;
|
|
3547
|
+
const filePath = url;
|
|
3548
|
+
entries.push({
|
|
3549
|
+
url,
|
|
3550
|
+
scriptId: entry.scriptId,
|
|
3551
|
+
filePath,
|
|
3552
|
+
functions: entry.functions
|
|
3553
|
+
});
|
|
3554
|
+
if (resourceStore?.assetFiles.get(filePath) !== entry.source) {
|
|
3555
|
+
assetFiles[filePath] = entry.source;
|
|
3556
|
+
resourceStore?.assetFiles.set(filePath, entry.source);
|
|
3557
|
+
}
|
|
3558
|
+
if (normalizedSourceMap) {
|
|
3559
|
+
sourceMapCache.set(cacheUrl, normalizedSourceMap);
|
|
3560
|
+
const serializedSourceMap = JSON.stringify(normalizedSourceMap);
|
|
3561
|
+
if (resourceStore?.sourceMaps.get(filePath) !== serializedSourceMap) {
|
|
3562
|
+
sourceMaps[filePath] = serializedSourceMap;
|
|
3563
|
+
resourceStore?.sourceMaps.set(filePath, serializedSourceMap);
|
|
3564
|
+
}
|
|
3565
|
+
} else {
|
|
3566
|
+
sourceMapCache.delete(cacheUrl);
|
|
3567
|
+
resourceStore?.sourceMaps.delete(filePath);
|
|
3568
|
+
}
|
|
3569
|
+
}));
|
|
3570
|
+
if (!entries.length) return null;
|
|
3571
|
+
return {
|
|
3572
|
+
entries,
|
|
3573
|
+
...Object.keys(assetFiles).length || Object.keys(sourceMaps).length ? {
|
|
3574
|
+
options: {
|
|
3575
|
+
assetFiles,
|
|
3576
|
+
sourceMaps
|
|
3577
|
+
}
|
|
3578
|
+
} : {},
|
|
3579
|
+
root: rootPath
|
|
3580
|
+
};
|
|
3581
|
+
};
|
|
3582
|
+
function createPromiseWithResolvers() {
|
|
3583
|
+
let resolve;
|
|
3584
|
+
let reject;
|
|
3585
|
+
return {
|
|
3586
|
+
promise: new Promise((res, rej)=>{
|
|
3587
|
+
resolve = res;
|
|
3588
|
+
reject = rej;
|
|
3589
|
+
}),
|
|
3590
|
+
resolve,
|
|
3591
|
+
reject
|
|
3592
|
+
};
|
|
3593
|
+
}
|
|
3594
|
+
const random = Math.random.bind(Math);
|
|
3595
|
+
const urlAlphabet = "useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict";
|
|
3596
|
+
function nanoid(size = 21) {
|
|
3597
|
+
let id = "";
|
|
3598
|
+
let i = size;
|
|
3599
|
+
while(i--)id += urlAlphabet[64 * random() | 0];
|
|
3600
|
+
return id;
|
|
3601
|
+
}
|
|
3602
|
+
const DEFAULT_TIMEOUT = 6e4;
|
|
3603
|
+
const defaultSerialize = (i)=>i;
|
|
3604
|
+
const defaultDeserialize = defaultSerialize;
|
|
3605
|
+
const { clearTimeout: dist_clearTimeout, setTimeout: dist_setTimeout } = globalThis;
|
|
3606
|
+
function createBirpc($functions, options) {
|
|
3607
|
+
const { post, on, off = ()=>{}, eventNames = [], serialize = defaultSerialize, deserialize = defaultDeserialize, resolver, bind = "rpc", timeout = DEFAULT_TIMEOUT, proxify = true } = options;
|
|
3608
|
+
let $closed = false;
|
|
3609
|
+
const _rpcPromiseMap = /* @__PURE__ */ new Map();
|
|
3610
|
+
let _promiseInit;
|
|
3611
|
+
let rpc;
|
|
3612
|
+
async function _call(method, args, event, optional) {
|
|
3613
|
+
if ($closed) throw new Error(`[birpc] rpc is closed, cannot call "${method}"`);
|
|
3614
|
+
const req = {
|
|
3615
|
+
m: method,
|
|
3616
|
+
a: args,
|
|
3617
|
+
t: "q"
|
|
3618
|
+
};
|
|
3619
|
+
if (optional) req.o = true;
|
|
3620
|
+
const send = async (_req)=>post(serialize(_req));
|
|
3621
|
+
if (event) return void await send(req);
|
|
3622
|
+
if (_promiseInit) try {
|
|
3623
|
+
await _promiseInit;
|
|
3624
|
+
} finally{
|
|
3625
|
+
_promiseInit = void 0;
|
|
3626
|
+
}
|
|
3627
|
+
let { promise, resolve, reject } = createPromiseWithResolvers();
|
|
3628
|
+
const id = nanoid();
|
|
3629
|
+
req.i = id;
|
|
3630
|
+
let timeoutId;
|
|
3631
|
+
async function handler(newReq = req) {
|
|
3632
|
+
if (timeout >= 0) {
|
|
3633
|
+
timeoutId = dist_setTimeout(()=>{
|
|
3634
|
+
try {
|
|
3635
|
+
if (options.onTimeoutError?.call(rpc, method, args) !== true) throw new Error(`[birpc] timeout on calling "${method}"`);
|
|
3636
|
+
} catch (e) {
|
|
3637
|
+
reject(e);
|
|
3638
|
+
}
|
|
3639
|
+
_rpcPromiseMap.delete(id);
|
|
3640
|
+
}, timeout);
|
|
3641
|
+
if ("object" == typeof timeoutId) timeoutId = timeoutId.unref?.();
|
|
3642
|
+
}
|
|
3643
|
+
_rpcPromiseMap.set(id, {
|
|
3644
|
+
resolve,
|
|
3645
|
+
reject,
|
|
3646
|
+
timeoutId,
|
|
3647
|
+
method
|
|
3648
|
+
});
|
|
3649
|
+
await send(newReq);
|
|
3650
|
+
return promise;
|
|
3651
|
+
}
|
|
3652
|
+
try {
|
|
3653
|
+
if (options.onRequest) await options.onRequest.call(rpc, req, handler, resolve);
|
|
3654
|
+
else await handler();
|
|
3655
|
+
} catch (e) {
|
|
3656
|
+
if (options.onGeneralError?.call(rpc, e) !== true) throw e;
|
|
3657
|
+
return;
|
|
3658
|
+
} finally{
|
|
3659
|
+
dist_clearTimeout(timeoutId);
|
|
3660
|
+
_rpcPromiseMap.delete(id);
|
|
3661
|
+
}
|
|
3662
|
+
return promise;
|
|
3663
|
+
}
|
|
3664
|
+
const builtinMethods = {
|
|
3665
|
+
$call: (method, ...args)=>_call(method, args, false),
|
|
3666
|
+
$callOptional: (method, ...args)=>_call(method, args, false, true),
|
|
3667
|
+
$callEvent: (method, ...args)=>_call(method, args, true),
|
|
3668
|
+
$callRaw: (options)=>_call(options.method, options.args, options.event, options.optional),
|
|
3669
|
+
$rejectPendingCalls,
|
|
3670
|
+
get $closed () {
|
|
3671
|
+
return $closed;
|
|
3672
|
+
},
|
|
3673
|
+
get $meta () {
|
|
3674
|
+
return options.meta;
|
|
3675
|
+
},
|
|
3676
|
+
$close,
|
|
3677
|
+
$functions
|
|
3678
|
+
};
|
|
3679
|
+
rpc = proxify ? new Proxy({}, {
|
|
3680
|
+
get (_, method) {
|
|
3681
|
+
if (Object.hasOwn(builtinMethods, method)) return builtinMethods[method];
|
|
3682
|
+
if ("then" === method && !eventNames.includes("then") && !("then" in $functions)) return;
|
|
3683
|
+
const sendEvent = (...args)=>_call(method, args, true);
|
|
3684
|
+
if (eventNames.includes(method)) {
|
|
3685
|
+
sendEvent.asEvent = sendEvent;
|
|
3686
|
+
return sendEvent;
|
|
3687
|
+
}
|
|
3688
|
+
const sendCall = (...args)=>_call(method, args, false);
|
|
3689
|
+
sendCall.asEvent = sendEvent;
|
|
3690
|
+
return sendCall;
|
|
3691
|
+
}
|
|
3692
|
+
}) : builtinMethods;
|
|
3693
|
+
function $close(customError) {
|
|
3694
|
+
$closed = true;
|
|
3695
|
+
_rpcPromiseMap.forEach(({ reject, method })=>{
|
|
3696
|
+
const error = /* @__PURE__ */ new Error(`[birpc] rpc is closed, cannot call "${method}"`);
|
|
3697
|
+
if (customError) {
|
|
3698
|
+
customError.cause ??= error;
|
|
3699
|
+
return reject(customError);
|
|
3700
|
+
}
|
|
3701
|
+
reject(error);
|
|
3702
|
+
});
|
|
3703
|
+
_rpcPromiseMap.clear();
|
|
3704
|
+
off(onMessage);
|
|
3705
|
+
}
|
|
3706
|
+
function $rejectPendingCalls(handler) {
|
|
3707
|
+
const handlerResults = Array.from(_rpcPromiseMap.values()).map(({ method, reject })=>{
|
|
3708
|
+
if (!handler) return reject(/* @__PURE__ */ new Error(`[birpc]: rejected pending call "${method}".`));
|
|
3709
|
+
return handler({
|
|
3710
|
+
method,
|
|
3711
|
+
reject
|
|
3712
|
+
});
|
|
3713
|
+
});
|
|
3714
|
+
_rpcPromiseMap.clear();
|
|
3715
|
+
return handlerResults;
|
|
3716
|
+
}
|
|
3717
|
+
async function onMessage(data, ...extra) {
|
|
3718
|
+
let msg;
|
|
3719
|
+
try {
|
|
3720
|
+
msg = deserialize(data);
|
|
3721
|
+
} catch (e) {
|
|
3722
|
+
if (options.onGeneralError?.call(rpc, e) !== true) throw e;
|
|
3723
|
+
return;
|
|
3724
|
+
}
|
|
3725
|
+
if ("q" === msg.t) {
|
|
3726
|
+
const { m: method, a: args, o: optional } = msg;
|
|
3727
|
+
let result, error;
|
|
3728
|
+
let fn = await (resolver ? resolver.call(rpc, method, $functions[method]) : $functions[method]);
|
|
3729
|
+
if (optional) fn ||= ()=>void 0;
|
|
3730
|
+
if (fn) try {
|
|
3731
|
+
result = await fn.apply("rpc" === bind ? rpc : $functions, args);
|
|
3732
|
+
} catch (e) {
|
|
3733
|
+
error = e;
|
|
3734
|
+
}
|
|
3735
|
+
else error = /* @__PURE__ */ new Error(`[birpc] function "${method}" not found`);
|
|
3736
|
+
if (msg.i) {
|
|
3737
|
+
if (error && options.onFunctionError) {
|
|
3738
|
+
if (true === options.onFunctionError.call(rpc, error, method, args)) return;
|
|
3739
|
+
}
|
|
3740
|
+
if (!error) try {
|
|
3741
|
+
await post(serialize({
|
|
3742
|
+
t: "s",
|
|
3743
|
+
i: msg.i,
|
|
3744
|
+
r: result
|
|
3745
|
+
}), ...extra);
|
|
3746
|
+
return;
|
|
3747
|
+
} catch (e) {
|
|
3748
|
+
error = e;
|
|
3749
|
+
if (options.onGeneralError?.call(rpc, e, method, args) !== true) throw e;
|
|
3750
|
+
}
|
|
3751
|
+
try {
|
|
3752
|
+
await post(serialize({
|
|
3753
|
+
t: "s",
|
|
3754
|
+
i: msg.i,
|
|
3755
|
+
e: error
|
|
3756
|
+
}), ...extra);
|
|
3757
|
+
} catch (e) {
|
|
3758
|
+
if (options.onGeneralError?.call(rpc, e, method, args) !== true) throw e;
|
|
3759
|
+
}
|
|
3760
|
+
}
|
|
3761
|
+
} else {
|
|
3762
|
+
const { i: ack, r: result, e: error } = msg;
|
|
3763
|
+
const promise = _rpcPromiseMap.get(ack);
|
|
3764
|
+
if (promise) {
|
|
3765
|
+
dist_clearTimeout(promise.timeoutId);
|
|
3766
|
+
if (error) promise.reject(error);
|
|
3767
|
+
else promise.resolve(result);
|
|
3768
|
+
}
|
|
3769
|
+
_rpcPromiseMap.delete(ack);
|
|
3770
|
+
}
|
|
3771
|
+
}
|
|
3772
|
+
_promiseInit = on(onMessage);
|
|
3773
|
+
return rpc;
|
|
3774
|
+
}
|
|
3775
|
+
class ContainerRpcManager {
|
|
3776
|
+
wss;
|
|
3777
|
+
ws = null;
|
|
3778
|
+
rpc = null;
|
|
3779
|
+
methods;
|
|
3780
|
+
onDisconnect;
|
|
3781
|
+
onAttach;
|
|
3782
|
+
detachActiveSocketListeners = null;
|
|
3783
|
+
transportEpoch = 0;
|
|
3784
|
+
constructor(wss, methods, onDisconnect, onAttach){
|
|
3785
|
+
this.wss = wss;
|
|
3786
|
+
this.methods = methods;
|
|
3787
|
+
this.onDisconnect = onDisconnect;
|
|
3788
|
+
this.onAttach = onAttach;
|
|
3789
|
+
this.setupConnectionHandler();
|
|
3790
|
+
}
|
|
3791
|
+
updateMethods(methods, onDisconnect, onAttach) {
|
|
3792
|
+
this.methods = methods;
|
|
3793
|
+
this.onDisconnect = onDisconnect;
|
|
3794
|
+
this.onAttach = onAttach;
|
|
3795
|
+
if (this.ws && this.ws.readyState === this.ws.OPEN) this.attachWebSocket(this.ws);
|
|
3796
|
+
}
|
|
3797
|
+
setupConnectionHandler() {
|
|
3798
|
+
this.wss.on('connection', (ws)=>{
|
|
3799
|
+
logger.debug('[Browser UI] Container WebSocket connected');
|
|
3800
|
+
logger.debug(`[Browser UI] Current ws: ${this.ws ? 'exists' : 'null'}, new ws: ${ws ? 'exists' : 'null'}`);
|
|
3801
|
+
this.attachWebSocket(ws);
|
|
3802
|
+
});
|
|
3803
|
+
}
|
|
3804
|
+
attachWebSocket(ws) {
|
|
3805
|
+
this.detachActiveSocketListeners?.();
|
|
3806
|
+
if (this.rpc && !this.rpc.$closed) this.rpc.$close(new Error('Container RPC transport reattached'));
|
|
3807
|
+
this.transportEpoch += 1;
|
|
3808
|
+
this.onAttach?.(this.transportEpoch);
|
|
3809
|
+
this.ws = ws;
|
|
3810
|
+
const messageHandlers = new WeakMap();
|
|
3811
|
+
this.rpc = createBirpc(this.methods, {
|
|
3812
|
+
timeout: -1,
|
|
3813
|
+
post: (data)=>{
|
|
3814
|
+
if (ws.readyState === ws.OPEN) ws.send(JSON.stringify(data));
|
|
3815
|
+
},
|
|
3816
|
+
on: (fn)=>{
|
|
3817
|
+
const handler = (message)=>{
|
|
3818
|
+
try {
|
|
3819
|
+
const data = JSON.parse(message.toString());
|
|
3820
|
+
fn(data);
|
|
3821
|
+
} catch {}
|
|
3822
|
+
};
|
|
3823
|
+
messageHandlers.set(fn, handler);
|
|
3824
|
+
ws.on('message', handler);
|
|
3825
|
+
},
|
|
3826
|
+
off: (fn)=>{
|
|
3827
|
+
const handler = messageHandlers.get(fn);
|
|
3828
|
+
if (!handler) return;
|
|
3829
|
+
ws.off('message', handler);
|
|
3830
|
+
messageHandlers.delete(fn);
|
|
3831
|
+
}
|
|
3832
|
+
});
|
|
3833
|
+
const handleClose = ()=>{
|
|
3834
|
+
if (this.ws === ws) this.ws = null;
|
|
3835
|
+
this.detachActiveSocketListeners?.();
|
|
3836
|
+
this.detachActiveSocketListeners = null;
|
|
3837
|
+
if (this.rpc && !this.rpc.$closed) {
|
|
3838
|
+
const disconnectError = new Error('Browser UI WebSocket disconnected before reload completed');
|
|
3839
|
+
this.rpc.$close(disconnectError);
|
|
3840
|
+
this.onDisconnect?.(disconnectError);
|
|
3841
|
+
}
|
|
3842
|
+
this.rpc = null;
|
|
3843
|
+
};
|
|
3844
|
+
ws.on('close', handleClose);
|
|
3845
|
+
this.detachActiveSocketListeners = ()=>{
|
|
3846
|
+
ws.off('close', handleClose);
|
|
3847
|
+
};
|
|
3848
|
+
}
|
|
3849
|
+
get isConnected() {
|
|
3850
|
+
return null !== this.ws && this.ws.readyState === this.ws.OPEN;
|
|
3851
|
+
}
|
|
3852
|
+
get currentWebSocket() {
|
|
3853
|
+
return this.ws;
|
|
3854
|
+
}
|
|
3855
|
+
reattach(ws) {
|
|
3856
|
+
this.attachWebSocket(ws);
|
|
3857
|
+
}
|
|
3858
|
+
async notifyTestFileUpdate(files, version) {
|
|
3859
|
+
await this.rpc?.onTestFileUpdate(files, version);
|
|
3860
|
+
}
|
|
3861
|
+
async updateHostConfig(config) {
|
|
3862
|
+
await this.rpc?.onHostConfigUpdate(config);
|
|
3863
|
+
}
|
|
3864
|
+
async reloadTestFile(testFile, runId, testNamePattern) {
|
|
3865
|
+
logger.debug(`[Browser UI] reloadTestFile called, rpc: ${this.rpc ? 'exists' : 'null'}, ws: ${this.ws ? 'exists' : 'null'}`);
|
|
3866
|
+
if (!this.rpc) throw new Error('Browser UI RPC not available for reloadTestFile');
|
|
3867
|
+
logger.debug(`[Browser UI] Calling reloadTestFile: ${testFile} (run ${runId})`);
|
|
3868
|
+
await this.rpc.reloadTestFile(testFile, runId, testNamePattern);
|
|
3869
|
+
}
|
|
3870
|
+
}
|
|
3871
|
+
const getFileTaskId = (testPath)=>`file:${testPath}`;
|
|
3872
|
+
const toError = (error)=>error instanceof Error ? error : new Error(String(error));
|
|
3873
|
+
const createDeferredPromise = ()=>{
|
|
3874
|
+
let resolve;
|
|
3875
|
+
let reject;
|
|
3876
|
+
const promise = new Promise((res, rej)=>{
|
|
3877
|
+
resolve = res;
|
|
3878
|
+
reject = rej;
|
|
3879
|
+
});
|
|
3880
|
+
return {
|
|
3881
|
+
promise,
|
|
3882
|
+
resolve,
|
|
3883
|
+
reject
|
|
3884
|
+
};
|
|
3885
|
+
};
|
|
3886
|
+
const RUN_BOOT_DEADLINE_MS = 45000;
|
|
3887
|
+
const createHeadedRunRegistry = ()=>{
|
|
3888
|
+
const runsById = new Map();
|
|
3889
|
+
let transportEpoch = 0;
|
|
3890
|
+
const settle = (record, error)=>{
|
|
3891
|
+
if (!runsById.delete(record.runId)) return;
|
|
3892
|
+
clearTimeout(record.bootTimer);
|
|
3893
|
+
clearTimeout(record.cleanupTimer);
|
|
3894
|
+
if (error) record.deferred.reject(error);
|
|
3895
|
+
else record.deferred.resolve();
|
|
3896
|
+
};
|
|
3897
|
+
const sweepOpen = (match, error)=>{
|
|
3898
|
+
for (const record of runsById.values())if ('open' === record.phase && match(record)) settle(record, error);
|
|
3899
|
+
};
|
|
3900
|
+
return {
|
|
3901
|
+
mint (testPath) {
|
|
3902
|
+
sweepOpen((record)=>record.testPath === testPath);
|
|
3903
|
+
const runId = randomUUID();
|
|
3904
|
+
const bootTimer = setTimeout(()=>{
|
|
3905
|
+
const record = runsById.get(runId);
|
|
3906
|
+
if (record) settle(record, new Error(`Headed run for ${testPath} produced no message within ${RUN_BOOT_DEADLINE_MS / 1000}s; its document never booted`));
|
|
3907
|
+
}, RUN_BOOT_DEADLINE_MS);
|
|
3908
|
+
bootTimer.unref?.();
|
|
3909
|
+
const record = {
|
|
3910
|
+
runId,
|
|
3911
|
+
testPath,
|
|
3912
|
+
epoch: transportEpoch,
|
|
3913
|
+
phase: 'open',
|
|
3914
|
+
deferred: createDeferredPromise(),
|
|
3915
|
+
bootTimer
|
|
3916
|
+
};
|
|
3917
|
+
runsById.set(runId, record);
|
|
3918
|
+
return {
|
|
3919
|
+
runId,
|
|
3920
|
+
settled: record.deferred.promise
|
|
3921
|
+
};
|
|
3922
|
+
},
|
|
3923
|
+
admit (runId, terminal) {
|
|
3924
|
+
const record = runsById.get(runId);
|
|
3925
|
+
if (!record) return false;
|
|
3926
|
+
clearTimeout(record.bootTimer);
|
|
3927
|
+
if (!terminal) return true;
|
|
3928
|
+
if ('open' !== record.phase) return false;
|
|
3929
|
+
record.phase = 'claimed';
|
|
3930
|
+
return true;
|
|
3931
|
+
},
|
|
3932
|
+
armCleanupDeadline (runId, ms, onExpire) {
|
|
3933
|
+
const record = runsById.get(runId);
|
|
3934
|
+
if (!record || 'open' !== record.phase) return;
|
|
3935
|
+
clearTimeout(record.cleanupTimer);
|
|
3936
|
+
const cleanupTimer = setTimeout(()=>{
|
|
3937
|
+
const current = runsById.get(runId);
|
|
3938
|
+
if (!current || 'open' !== current.phase) return;
|
|
3939
|
+
current.cleanupTimer = void 0;
|
|
3940
|
+
current.phase = 'claimed';
|
|
3941
|
+
onExpire();
|
|
3942
|
+
}, ms);
|
|
3943
|
+
cleanupTimer.unref?.();
|
|
3944
|
+
record.cleanupTimer = cleanupTimer;
|
|
3945
|
+
},
|
|
3946
|
+
disarmCleanupDeadline (runId) {
|
|
3947
|
+
const record = runsById.get(runId);
|
|
3948
|
+
if (record) {
|
|
3949
|
+
clearTimeout(record.cleanupTimer);
|
|
3950
|
+
record.cleanupTimer = void 0;
|
|
3951
|
+
}
|
|
3952
|
+
},
|
|
3953
|
+
resolve (runId) {
|
|
3954
|
+
const record = runsById.get(runId);
|
|
3955
|
+
if (record) settle(record);
|
|
3956
|
+
},
|
|
3957
|
+
reject (runId, error) {
|
|
3958
|
+
const record = runsById.get(runId);
|
|
3959
|
+
if (record) settle(record, toError(error));
|
|
3960
|
+
},
|
|
3961
|
+
retainPaths (paths) {
|
|
3962
|
+
const retained = new Set(paths);
|
|
3963
|
+
sweepOpen((record)=>!retained.has(record.testPath));
|
|
3964
|
+
},
|
|
3965
|
+
rejectAll (error) {
|
|
3966
|
+
sweepOpen(()=>true, error);
|
|
3967
|
+
},
|
|
3968
|
+
setTransportEpoch (epoch) {
|
|
3969
|
+
transportEpoch = epoch;
|
|
3970
|
+
sweepOpen((record)=>record.epoch < epoch, new Error('Container transport was replaced before the run completed'));
|
|
3971
|
+
}
|
|
3972
|
+
};
|
|
3973
|
+
};
|
|
3974
|
+
const createHeadedSerialTaskQueue = ()=>{
|
|
3975
|
+
let queue = Promise.resolve();
|
|
3976
|
+
const enqueue = (task)=>{
|
|
3977
|
+
const next = queue.then(task);
|
|
3978
|
+
queue = next.catch(()=>{});
|
|
3979
|
+
return next;
|
|
3980
|
+
};
|
|
3981
|
+
return {
|
|
3982
|
+
enqueue
|
|
3983
|
+
};
|
|
3984
|
+
};
|
|
3985
|
+
const claimHeadedCycleScope = (testPaths, currentTestFiles, pendingTestNamePatterns)=>{
|
|
3986
|
+
const scope = [];
|
|
3987
|
+
const filesByPath = new Map(currentTestFiles.map((file)=>[
|
|
3988
|
+
file.testPath,
|
|
3989
|
+
file
|
|
3990
|
+
]));
|
|
3991
|
+
for (const testPath of testPaths){
|
|
3992
|
+
const normalizedTestPath = normalize(testPath);
|
|
3993
|
+
const file = filesByPath.get(normalizedTestPath);
|
|
3994
|
+
if (file) {
|
|
3995
|
+
scope.push({
|
|
3996
|
+
file,
|
|
3997
|
+
testNamePattern: pendingTestNamePatterns.get(normalizedTestPath)
|
|
3998
|
+
});
|
|
3999
|
+
pendingTestNamePatterns.delete(normalizedTestPath);
|
|
4000
|
+
}
|
|
4001
|
+
}
|
|
4002
|
+
return scope;
|
|
4003
|
+
};
|
|
4004
|
+
const createHeadedScheduler = async ({ context, runtime, allTestFiles, hostOptions, v8Coverage, projectRoots, isWatchMode, createDispatchRouter, handlers: { handleTestFileComplete }, fatalErrorRef, watchSignals, setDispatchPageResolver, createWatchSession, collectProjectEntries, logWatchReady, destroyRuntime })=>{
|
|
4005
|
+
const { browser, browserLaunchOptions, watchState, wss } = runtime;
|
|
4006
|
+
const containerUrl = `http://localhost:${runtime.containerServer.port}/`;
|
|
4007
|
+
let currentTestFiles = allTestFiles;
|
|
4008
|
+
const RUNNER_FRAMES_READY_TIMEOUT_MS = 30000;
|
|
4009
|
+
let ackedFrameSetVersion = 0;
|
|
4010
|
+
const frameSetWaiters = new Set();
|
|
4011
|
+
const markFrameSetReady = (version)=>{
|
|
4012
|
+
if (version > watchState.headedFileSetVersion) return;
|
|
4013
|
+
if (version > ackedFrameSetVersion) ackedFrameSetVersion = version;
|
|
4014
|
+
for (const waiter of frameSetWaiters)if (waiter.version <= ackedFrameSetVersion) {
|
|
4015
|
+
clearTimeout(waiter.timeoutId);
|
|
4016
|
+
frameSetWaiters.delete(waiter);
|
|
4017
|
+
waiter.resolve();
|
|
4018
|
+
}
|
|
4019
|
+
};
|
|
4020
|
+
const waitForFrameSet = async (version)=>{
|
|
4021
|
+
if (ackedFrameSetVersion >= version) return;
|
|
4022
|
+
await new Promise((resolve, reject)=>{
|
|
4023
|
+
const waiter = {
|
|
4024
|
+
version,
|
|
4025
|
+
resolve,
|
|
4026
|
+
timeoutId: setTimeout(()=>{
|
|
4027
|
+
frameSetWaiters.delete(waiter);
|
|
4028
|
+
reject(new Error(`Timed out waiting for headed runner frames to be ready (file-set version ${version}).`));
|
|
4029
|
+
}, RUNNER_FRAMES_READY_TIMEOUT_MS)
|
|
4030
|
+
};
|
|
4031
|
+
frameSetWaiters.add(waiter);
|
|
4032
|
+
});
|
|
4033
|
+
};
|
|
4034
|
+
const findTestFileInfo = (testFile)=>{
|
|
4035
|
+
const normalizedTestFile = normalize(testFile);
|
|
4036
|
+
return currentTestFiles.find((file)=>file.testPath === normalizedTestFile);
|
|
4037
|
+
};
|
|
4038
|
+
const getTestFileInfo = (testFile)=>{
|
|
4039
|
+
const fileInfo = findTestFileInfo(testFile);
|
|
4040
|
+
if (!fileInfo) throw new Error(`Unknown browser test file: ${JSON.stringify(testFile)}`);
|
|
4041
|
+
return fileInfo;
|
|
4042
|
+
};
|
|
4043
|
+
let containerContext;
|
|
4044
|
+
let containerPage;
|
|
4045
|
+
let isNewPage = false;
|
|
4046
|
+
const attachContainerPageHandlers = (activeContext, activePage)=>{
|
|
4047
|
+
activePage.on('popup', async (popup)=>{
|
|
4048
|
+
await popup.close().catch(()=>{});
|
|
4049
|
+
});
|
|
4050
|
+
activeContext.on('page', async (page)=>{
|
|
4051
|
+
if (page !== activePage) await page.close().catch(()=>{});
|
|
4052
|
+
});
|
|
4053
|
+
activePage.on('console', (msg)=>{
|
|
4054
|
+
const text = msg.text();
|
|
4055
|
+
if (text.startsWith('[Container]') || text.startsWith('[Runner]')) return void logger.log(color.gray(`[Browser Console] ${text}`));
|
|
4056
|
+
logger.debug(`[Browser Console] ${text}`);
|
|
4057
|
+
});
|
|
4058
|
+
};
|
|
4059
|
+
const createContainerPage = async ()=>{
|
|
4060
|
+
const nextContext = await browser.newContext({
|
|
4061
|
+
providerOptions: browserLaunchOptions.providerOptions,
|
|
4062
|
+
viewport: null
|
|
4063
|
+
});
|
|
4064
|
+
const nextPage = await nextContext.newPage();
|
|
4065
|
+
attachContainerPageHandlers(nextContext, nextPage);
|
|
4066
|
+
return {
|
|
4067
|
+
context: nextContext,
|
|
4068
|
+
page: nextPage
|
|
4069
|
+
};
|
|
4070
|
+
};
|
|
4071
|
+
if (isWatchMode && runtime.containerPage && runtime.containerContext) {
|
|
4072
|
+
containerContext = runtime.containerContext;
|
|
4073
|
+
containerPage = runtime.containerPage;
|
|
4074
|
+
logger.log(color.gray('\n[Watch] Reusing existing container page\n'));
|
|
4075
|
+
} else {
|
|
4076
|
+
isNewPage = true;
|
|
4077
|
+
const created = await createContainerPage();
|
|
4078
|
+
containerContext = created.context;
|
|
4079
|
+
containerPage = created.page;
|
|
4080
|
+
if (isWatchMode) {
|
|
4081
|
+
runtime.containerPage = containerPage;
|
|
4082
|
+
runtime.containerContext = containerContext;
|
|
4083
|
+
}
|
|
4084
|
+
}
|
|
4085
|
+
setDispatchPageResolver(()=>({
|
|
4086
|
+
containerPage
|
|
4087
|
+
}));
|
|
4088
|
+
const dispatchRouter = createDispatchRouter();
|
|
4089
|
+
const headedReloadQueue = createHeadedSerialTaskQueue();
|
|
4090
|
+
let rawCoverage = [];
|
|
4091
|
+
let coverageProject;
|
|
4092
|
+
let coverageStarted = false;
|
|
4093
|
+
const startCoverage = async (projectName)=>{
|
|
4094
|
+
if (!v8Coverage) return;
|
|
4095
|
+
await v8Coverage.start(containerPage);
|
|
4096
|
+
coverageStarted = true;
|
|
4097
|
+
coverageProject = projectName;
|
|
4098
|
+
};
|
|
4099
|
+
const collectCoverage = async ()=>{
|
|
4100
|
+
if (!coverageStarted || !v8Coverage) return;
|
|
4101
|
+
coverageStarted = false;
|
|
4102
|
+
const coverage = await v8Coverage.take(containerPage, projectRoots.get(coverageProject ?? '') ?? context.rootPath, coverageProject);
|
|
4103
|
+
if (coverage) rawCoverage.push(coverage);
|
|
4104
|
+
};
|
|
4105
|
+
let enqueueHeadedReload = async (_file, _testNamePattern)=>{
|
|
4106
|
+
throw new Error('Headed reload queue is not initialized');
|
|
4107
|
+
};
|
|
4108
|
+
const isCurrentTestFile = (testPath)=>void 0 !== findTestFileInfo(testPath);
|
|
4109
|
+
const runs = watchState.headedRuns ??= createHeadedRunRegistry();
|
|
4110
|
+
const recoverHeadedContainer = async ()=>{
|
|
4111
|
+
ackedFrameSetVersion = 0;
|
|
4112
|
+
await containerContext.close().catch(()=>{});
|
|
4113
|
+
const created = await createContainerPage();
|
|
4114
|
+
containerContext = created.context;
|
|
4115
|
+
containerPage = created.page;
|
|
4116
|
+
if (isWatchMode) {
|
|
4117
|
+
runtime.containerPage = containerPage;
|
|
4118
|
+
runtime.containerContext = containerContext;
|
|
4119
|
+
}
|
|
4120
|
+
await containerPage.goto(containerUrl, {
|
|
4121
|
+
waitUntil: 'load'
|
|
4122
|
+
});
|
|
4123
|
+
await waitForFrameSet(watchState.headedFileSetVersion);
|
|
4124
|
+
};
|
|
4125
|
+
dispatchRouter.register(DISPATCH_NAMESPACE_FILE_CLEANUP, async (request)=>{
|
|
4126
|
+
const { runId } = request;
|
|
4127
|
+
if (!runId) return;
|
|
4128
|
+
if ('start' === request.method) {
|
|
4129
|
+
const payload = request.args;
|
|
4130
|
+
runs.armCleanupDeadline(runId, FIXTURE_CLEANUP_TIMEOUT_MS, ()=>{
|
|
4131
|
+
(async ()=>{
|
|
4132
|
+
try {
|
|
4133
|
+
await collectCoverage();
|
|
4134
|
+
await handleTestFileComplete(createFileCleanupTimeoutResult({
|
|
4135
|
+
message: `File fixture cleanup did not finish within ${FIXTURE_CLEANUP_TIMEOUT_MS}ms`,
|
|
4136
|
+
projectName: payload.projectName,
|
|
4137
|
+
result: payload.result,
|
|
4138
|
+
testPath: payload.testPath
|
|
4139
|
+
}));
|
|
4140
|
+
await recoverHeadedContainer();
|
|
4141
|
+
runs.resolve(runId);
|
|
4142
|
+
} catch (error) {
|
|
4143
|
+
runs.reject(runId, toError(error));
|
|
4144
|
+
}
|
|
4145
|
+
})();
|
|
4146
|
+
});
|
|
4147
|
+
} else if ('end' === request.method) runs.disarmCleanupDeadline(runId);
|
|
4148
|
+
});
|
|
4149
|
+
const reloadTestFileAndWait = async (file, testNamePattern)=>{
|
|
4150
|
+
await startCoverage(file.projectName);
|
|
4151
|
+
const { runId, settled } = runs.mint(file.testPath);
|
|
4152
|
+
rpcManager.reloadTestFile(file.testPath, runId, testNamePattern).catch((error)=>runs.reject(runId, error));
|
|
4153
|
+
try {
|
|
4154
|
+
await settled;
|
|
4155
|
+
} finally{
|
|
4156
|
+
await collectCoverage().catch((error)=>{
|
|
4157
|
+
logger.debug(`[Headed] Failed to flush coverage: ${error}`);
|
|
4158
|
+
});
|
|
4159
|
+
}
|
|
4160
|
+
};
|
|
4161
|
+
let runUiRequestedRerun = async (file, testNamePattern)=>{
|
|
4162
|
+
await enqueueHeadedReload(file, testNamePattern);
|
|
4163
|
+
};
|
|
4164
|
+
const createRpcMethods = ()=>({
|
|
4165
|
+
async rerunTest (testFile, testNamePattern) {
|
|
4166
|
+
const projectName = context.normalizedConfig.name || 'project';
|
|
4167
|
+
const relativePath = relative(context.rootPath, testFile);
|
|
4168
|
+
const displayPath = `<${projectName}>/${relativePath}`;
|
|
4169
|
+
logger.log(color.cyan(`\nRe-running test: ${displayPath}${testNamePattern ? ` (pattern: ${testNamePattern})` : ''}\n`));
|
|
4170
|
+
await runUiRequestedRerun(getTestFileInfo(testFile), testNamePattern);
|
|
4171
|
+
},
|
|
4172
|
+
async getTestFiles () {
|
|
4173
|
+
return {
|
|
4174
|
+
files: currentTestFiles,
|
|
4175
|
+
version: watchState.headedFileSetVersion
|
|
4176
|
+
};
|
|
4177
|
+
},
|
|
4178
|
+
async onFrameSetReady (version) {
|
|
4179
|
+
markFrameSetReady(version);
|
|
4180
|
+
},
|
|
4181
|
+
async dispatch (request) {
|
|
4182
|
+
const { method, runId } = request;
|
|
4183
|
+
const isTerminal = "runner" === request.namespace && ('file-complete' === method || 'fatal' === method);
|
|
4184
|
+
if (!runId || !runs.admit(runId, isTerminal)) {
|
|
4185
|
+
logger.debug(`[Headed] Dropped stale "${method}" from run ${runId ?? '(none)'}`);
|
|
4186
|
+
return {
|
|
4187
|
+
requestId: request.requestId,
|
|
4188
|
+
stale: true
|
|
4189
|
+
};
|
|
4190
|
+
}
|
|
4191
|
+
let coverageError;
|
|
4192
|
+
if (isTerminal) try {
|
|
4193
|
+
await collectCoverage();
|
|
4194
|
+
} catch (error) {
|
|
4195
|
+
coverageError = toError(error);
|
|
4196
|
+
}
|
|
4197
|
+
const response = await dispatchRouter.dispatch(request);
|
|
4198
|
+
if (!isTerminal) return response;
|
|
4199
|
+
if ('file-complete' === method) {
|
|
4200
|
+
const failure = coverageError ?? (response.error ? new Error(response.error) : void 0);
|
|
4201
|
+
if (failure) runs.reject(runId, failure);
|
|
4202
|
+
else runs.resolve(runId);
|
|
4203
|
+
} else {
|
|
4204
|
+
if (coverageError) logger.error(`[Headed] Failed to take coverage during fatal handling: ${coverageError.message}`);
|
|
4205
|
+
const payload = request.args;
|
|
4206
|
+
const error = new Error(payload?.message ?? 'Fatal browser error');
|
|
4207
|
+
if (payload?.stack) error.stack = payload.stack;
|
|
4208
|
+
runs.reject(runId, error);
|
|
4209
|
+
}
|
|
4210
|
+
return response;
|
|
4211
|
+
}
|
|
4212
|
+
});
|
|
4213
|
+
let rpcManager;
|
|
4214
|
+
if (isWatchMode && runtime.rpcManager) {
|
|
4215
|
+
rpcManager = runtime.rpcManager;
|
|
4216
|
+
rpcManager.updateMethods(createRpcMethods(), runs.rejectAll, runs.setTransportEpoch);
|
|
4217
|
+
const existingWs = rpcManager.currentWebSocket;
|
|
4218
|
+
if (existingWs) rpcManager.reattach(existingWs);
|
|
4219
|
+
} else {
|
|
4220
|
+
rpcManager = new ContainerRpcManager(wss, createRpcMethods(), runs.rejectAll, runs.setTransportEpoch);
|
|
4221
|
+
if (isWatchMode) runtime.rpcManager = rpcManager;
|
|
4222
|
+
}
|
|
4223
|
+
if (isNewPage) {
|
|
4224
|
+
await containerPage.goto(containerUrl, {
|
|
4225
|
+
waitUntil: 'load'
|
|
4226
|
+
});
|
|
4227
|
+
logger.log(color.cyan(`\nBrowser mode opened at ${containerUrl}\n`));
|
|
4228
|
+
}
|
|
4229
|
+
enqueueHeadedReload = async (file, testNamePattern)=>headedReloadQueue.enqueue(async ()=>{
|
|
4230
|
+
if (fatalErrorRef.current) return;
|
|
4231
|
+
if (!isCurrentTestFile(file.testPath)) return void logger.debug(`[Browser UI] Skipping reload for removed test file: ${file.testPath}`);
|
|
4232
|
+
await reloadTestFileAndWait(file, testNamePattern);
|
|
4233
|
+
});
|
|
4234
|
+
let testTime = 0;
|
|
4235
|
+
if (currentTestFiles.length > 0) {
|
|
4236
|
+
const testStart = Date.now();
|
|
4237
|
+
try {
|
|
4238
|
+
if (rpcManager.isConnected) await rpcManager.notifyTestFileUpdate(currentTestFiles, watchState.headedFileSetVersion);
|
|
4239
|
+
await waitForFrameSet(watchState.headedFileSetVersion);
|
|
4240
|
+
for (const file of currentTestFiles){
|
|
4241
|
+
await enqueueHeadedReload(file);
|
|
4242
|
+
if (fatalErrorRef.current) break;
|
|
4243
|
+
}
|
|
4244
|
+
} catch (error) {
|
|
4245
|
+
fatalErrorRef.current = fatalErrorRef.current ?? toError(error);
|
|
4246
|
+
}
|
|
4247
|
+
testTime = Date.now() - testStart;
|
|
4248
|
+
}
|
|
4249
|
+
let watchSession;
|
|
4250
|
+
if (isWatchMode) {
|
|
4251
|
+
const pendingTestNamePatterns = new Map();
|
|
4252
|
+
const runScope = async (testPaths)=>{
|
|
4253
|
+
rawCoverage = [];
|
|
4254
|
+
const cycleScope = claimHeadedCycleScope(testPaths, currentTestFiles, pendingTestNamePatterns);
|
|
4255
|
+
for (const { file, testNamePattern } of cycleScope)await enqueueHeadedReload(file, testNamePattern);
|
|
4256
|
+
return rawCoverage;
|
|
4257
|
+
};
|
|
4258
|
+
const refreshHostConfig = async ()=>{
|
|
4259
|
+
const refreshedHostOptions = {
|
|
4260
|
+
...hostOptions,
|
|
4261
|
+
snapshot: {
|
|
4262
|
+
updateSnapshot: context.snapshotManager.options.updateSnapshot
|
|
4263
|
+
}
|
|
4264
|
+
};
|
|
4265
|
+
runtime.setContainerOptions(refreshedHostOptions);
|
|
4266
|
+
await rpcManager.updateHostConfig(refreshedHostOptions);
|
|
4267
|
+
};
|
|
4268
|
+
const runDispatchRerunOnce = async ()=>{
|
|
4269
|
+
try {
|
|
4270
|
+
const [, newProjectEntries] = await Promise.all([
|
|
4271
|
+
refreshHostConfig(),
|
|
4272
|
+
collectProjectEntries()
|
|
4273
|
+
]);
|
|
4274
|
+
const rerunPlan = planWatchRerun({
|
|
4275
|
+
projectEntries: newProjectEntries,
|
|
4276
|
+
previousTestFiles: watchState.lastTestFiles,
|
|
4277
|
+
affectedTestFiles: drainPendingAffectedTestFiles(watchState)
|
|
4278
|
+
});
|
|
4279
|
+
commitWatchFileSetUpdate(rerunPlan.fileSetUpdate, watchState, (deletedTestPaths)=>context.updateReporterResultState([], [], deletedTestPaths));
|
|
4280
|
+
if (rerunPlan.fileSetUpdate) {
|
|
4281
|
+
currentTestFiles = rerunPlan.fileSetUpdate.currentTestFiles;
|
|
4282
|
+
const version = watchState.headedFileSetVersion += 1;
|
|
4283
|
+
runs.retainPaths(currentTestFiles.map((file)=>file.testPath));
|
|
4284
|
+
await rpcManager.notifyTestFileUpdate(currentTestFiles, version);
|
|
4285
|
+
if (currentTestFiles.length > 0) await waitForFrameSet(version);
|
|
4286
|
+
}
|
|
4287
|
+
logger.log(color.cyan(rerunPlan.decision.message));
|
|
4288
|
+
if ('idle' === rerunPlan.decision.kind) return void logWatchReady();
|
|
4289
|
+
await watchSignals.signalInvalidation('empty' === rerunPlan.decision.kind ? [] : rerunPlan.decision.testPaths);
|
|
4290
|
+
} catch (error) {
|
|
4291
|
+
logWatchReady();
|
|
4292
|
+
throw error;
|
|
4293
|
+
}
|
|
4294
|
+
};
|
|
4295
|
+
const dispatchRerunQueue = createHeadedSerialTaskQueue();
|
|
4296
|
+
watchSignals.setDispatchRerun(()=>dispatchRerunQueue.enqueue(runDispatchRerunOnce));
|
|
4297
|
+
runUiRequestedRerun = async (file, testNamePattern)=>{
|
|
4298
|
+
await refreshHostConfig();
|
|
4299
|
+
const { cycle } = await watchSignals.signalInvalidation([
|
|
4300
|
+
file.testPath
|
|
4301
|
+
], ()=>{
|
|
4302
|
+
if (void 0 === testNamePattern) pendingTestNamePatterns.delete(normalize(file.testPath));
|
|
4303
|
+
else pendingTestNamePatterns.set(normalize(file.testPath), testNamePattern);
|
|
4304
|
+
});
|
|
4305
|
+
await cycle;
|
|
4306
|
+
};
|
|
4307
|
+
watchSession = createWatchSession(runScope);
|
|
4308
|
+
}
|
|
4309
|
+
const closeContainerRuntime = isWatchMode ? void 0 : async ()=>{
|
|
4310
|
+
try {
|
|
4311
|
+
await containerPage.close();
|
|
4312
|
+
} catch {}
|
|
4313
|
+
try {
|
|
4314
|
+
await containerContext.close();
|
|
4315
|
+
} catch {}
|
|
4316
|
+
await destroyRuntime();
|
|
4317
|
+
};
|
|
4318
|
+
return {
|
|
4319
|
+
testTime,
|
|
4320
|
+
rawCoverage,
|
|
4321
|
+
watchSession,
|
|
4322
|
+
close: closeContainerRuntime
|
|
4323
|
+
};
|
|
4324
|
+
};
|
|
4325
|
+
const DEFAULT_MAX_HEADLESS_WORKERS = 12;
|
|
4326
|
+
const resolveHeadlessWorkerCount = ({ command, maxWorkers, totalTasks, numCpus = browser_getNumCpus() })=>{
|
|
4327
|
+
const base = Math.max(Math.min(DEFAULT_MAX_HEADLESS_WORKERS, numCpus - 1), 1);
|
|
4328
|
+
return resolveWorkerCount({
|
|
4329
|
+
command,
|
|
4330
|
+
maxWorkers,
|
|
4331
|
+
totalTasks,
|
|
4332
|
+
recommended: base,
|
|
4333
|
+
watchRecommended: Math.max(Math.floor(base / 2), 1),
|
|
4334
|
+
numCpus
|
|
4335
|
+
});
|
|
4336
|
+
};
|
|
4337
|
+
const getHeadlessConcurrency = (context, totalTests)=>resolveHeadlessWorkerCount({
|
|
4338
|
+
command: context.command,
|
|
4339
|
+
maxWorkers: context.normalizedConfig.pool.maxWorkers,
|
|
4340
|
+
totalTasks: totalTests
|
|
4341
|
+
});
|
|
4342
|
+
const attachHeadlessRunnerTransport = async (page, handlers)=>{
|
|
4343
|
+
await page.exposeFunction(DISPATCH_MESSAGE_TYPE, handlers.onDispatchMessage);
|
|
4344
|
+
await page.exposeFunction(DISPATCH_RPC_BRIDGE_NAME, handlers.onDispatchRpc);
|
|
4345
|
+
};
|
|
4346
|
+
const createCancelSignal = ()=>{
|
|
4347
|
+
let settled = false;
|
|
4348
|
+
let resolveSignal = ()=>{};
|
|
4349
|
+
const signal = new Promise((resolve)=>{
|
|
4350
|
+
resolveSignal = ()=>{
|
|
4351
|
+
if (!settled) {
|
|
4352
|
+
settled = true;
|
|
4353
|
+
resolve();
|
|
4354
|
+
}
|
|
4355
|
+
};
|
|
4356
|
+
});
|
|
4357
|
+
return {
|
|
4358
|
+
signal,
|
|
4359
|
+
resolve: resolveSignal
|
|
4360
|
+
};
|
|
4361
|
+
};
|
|
4362
|
+
const createRunSession = (token)=>{
|
|
4363
|
+
const { signal, resolve } = createCancelSignal();
|
|
4364
|
+
return {
|
|
4365
|
+
token,
|
|
4366
|
+
cancelled: false,
|
|
4367
|
+
cancelSignal: signal,
|
|
4368
|
+
signalCancel: resolve
|
|
4369
|
+
};
|
|
4370
|
+
};
|
|
4371
|
+
class RunSessionLifecycle {
|
|
4372
|
+
currentToken = 0;
|
|
4373
|
+
active = null;
|
|
4374
|
+
get activeSession() {
|
|
4375
|
+
return this.active;
|
|
4376
|
+
}
|
|
4377
|
+
get activeToken() {
|
|
4378
|
+
return this.currentToken;
|
|
4379
|
+
}
|
|
4380
|
+
createSession(factory) {
|
|
4381
|
+
const session = factory(++this.currentToken);
|
|
4382
|
+
this.active = session;
|
|
4383
|
+
return session;
|
|
4384
|
+
}
|
|
4385
|
+
isTokenActive(token) {
|
|
4386
|
+
return token === this.currentToken;
|
|
4387
|
+
}
|
|
4388
|
+
isTokenStale(token) {
|
|
4389
|
+
return !this.isTokenActive(token);
|
|
4390
|
+
}
|
|
4391
|
+
invalidateActiveToken() {
|
|
4392
|
+
this.currentToken += 1;
|
|
4393
|
+
return this.currentToken;
|
|
4394
|
+
}
|
|
4395
|
+
clearIfActive(session) {
|
|
4396
|
+
if (this.active === session) this.active = null;
|
|
4397
|
+
}
|
|
4398
|
+
async cancel(session, options) {
|
|
4399
|
+
const waitForDone = options?.waitForDone ?? true;
|
|
4400
|
+
if (!session.cancelled) {
|
|
4401
|
+
session.cancelled = true;
|
|
4402
|
+
session.signalCancel();
|
|
4403
|
+
await options?.onCancel?.(session);
|
|
4404
|
+
}
|
|
4405
|
+
if (waitForDone) await session.done?.catch(()=>{});
|
|
4406
|
+
}
|
|
4407
|
+
async cancelActive(options) {
|
|
4408
|
+
if (!this.active) return;
|
|
4409
|
+
await this.cancel(this.active, options);
|
|
4410
|
+
}
|
|
4411
|
+
}
|
|
4412
|
+
class RunnerSessionRegistry {
|
|
4413
|
+
nextId = 0;
|
|
4414
|
+
sessionsById = new Map();
|
|
4415
|
+
sessionIdByTestFile = new Map();
|
|
4416
|
+
register(input) {
|
|
4417
|
+
const id = input.id ?? `runner-session-${++this.nextId}`;
|
|
4418
|
+
const createdAt = input.createdAt ?? Date.now();
|
|
4419
|
+
const record = {
|
|
4420
|
+
...input,
|
|
4421
|
+
id,
|
|
4422
|
+
createdAt
|
|
4423
|
+
};
|
|
4424
|
+
this.sessionsById.set(id, record);
|
|
4425
|
+
this.sessionIdByTestFile.set(record.testFile, id);
|
|
4426
|
+
return record;
|
|
4427
|
+
}
|
|
4428
|
+
getById(id) {
|
|
4429
|
+
return this.sessionsById.get(id);
|
|
4430
|
+
}
|
|
4431
|
+
getByTestFile(testFile) {
|
|
4432
|
+
const id = this.sessionIdByTestFile.get(testFile);
|
|
4433
|
+
if (!id) return;
|
|
4434
|
+
return this.sessionsById.get(id);
|
|
4435
|
+
}
|
|
4436
|
+
list() {
|
|
4437
|
+
return Array.from(this.sessionsById.values());
|
|
4438
|
+
}
|
|
4439
|
+
listByRunToken(runToken) {
|
|
4440
|
+
return this.list().filter((session)=>session.runToken === runToken);
|
|
4441
|
+
}
|
|
4442
|
+
deleteById(id) {
|
|
4443
|
+
const record = this.sessionsById.get(id);
|
|
4444
|
+
if (!record) return false;
|
|
4445
|
+
this.sessionsById.delete(id);
|
|
4446
|
+
if (this.sessionIdByTestFile.get(record.testFile) === id) this.sessionIdByTestFile.delete(record.testFile);
|
|
4447
|
+
return true;
|
|
4448
|
+
}
|
|
4449
|
+
deleteByTestFile(testFile) {
|
|
4450
|
+
const id = this.sessionIdByTestFile.get(testFile);
|
|
4451
|
+
if (!id) return false;
|
|
4452
|
+
return this.deleteById(id);
|
|
4453
|
+
}
|
|
4454
|
+
clear() {
|
|
4455
|
+
this.sessionsById.clear();
|
|
4456
|
+
this.sessionIdByTestFile.clear();
|
|
4457
|
+
}
|
|
4458
|
+
}
|
|
4459
|
+
const createHeadlessScheduler = async ({ context, browser, browserLaunchOptions, projectServers, v8Coverage, allTestFiles, projectRuntimeConfigs, hostOptions, watchState, isWatchMode, createDispatchRouter, handlers: { handleFatal, handleTestFileComplete }, watchSignals, setDispatchPageResolver, createWatchSession, collectProjectEntries, logWatchReady, destroyRuntime })=>{
|
|
4460
|
+
const viewportByProject = mapViewportByProject(projectRuntimeConfigs);
|
|
4461
|
+
const projectRootByName = new Map(projectRuntimeConfigs.map((project)=>[
|
|
4462
|
+
project.name,
|
|
4463
|
+
project.projectRoot
|
|
4464
|
+
]));
|
|
4465
|
+
const runLifecycle = new RunSessionLifecycle();
|
|
4466
|
+
const sessionRegistry = new RunnerSessionRegistry();
|
|
4467
|
+
setDispatchPageResolver((target)=>({
|
|
4468
|
+
runnerPage: target?.sessionId ? sessionRegistry.getById(target.sessionId)?.page : void 0
|
|
4469
|
+
}));
|
|
4470
|
+
let dispatchRequestCounter = 0;
|
|
4471
|
+
const nextDispatchRequestId = (namespace)=>`${namespace}-${++dispatchRequestCounter}`;
|
|
4472
|
+
const closeContextSafely = async (browserContext)=>{
|
|
4473
|
+
try {
|
|
4474
|
+
await browserContext.close();
|
|
4475
|
+
} catch {}
|
|
4476
|
+
};
|
|
4477
|
+
const cancelRun = async (run, waitForDone = true)=>{
|
|
4478
|
+
await runLifecycle.cancel(run, {
|
|
4479
|
+
waitForDone,
|
|
4480
|
+
onCancel: async (session)=>{
|
|
4481
|
+
await Promise.all(Array.from(session.contexts).map((browserContext)=>closeContextSafely(browserContext)));
|
|
4482
|
+
}
|
|
4483
|
+
});
|
|
4484
|
+
};
|
|
4485
|
+
const collectRunCoverage = async (run)=>{
|
|
4486
|
+
const results = await Promise.allSettled(Array.from(run.coverageCollectors, (collect)=>collect()));
|
|
4487
|
+
for (const result of results)if ('rejected' === result.status) {
|
|
4488
|
+
const error = toError(result.reason);
|
|
4489
|
+
await handleFatal({
|
|
4490
|
+
message: error.message,
|
|
4491
|
+
stack: error.stack
|
|
4492
|
+
});
|
|
4493
|
+
}
|
|
4494
|
+
};
|
|
4495
|
+
const dispatchRouter = createDispatchRouter({
|
|
4496
|
+
isRunTokenStale: (runToken)=>runLifecycle.isTokenStale(runToken),
|
|
4497
|
+
onStale: (request)=>{
|
|
4498
|
+
if ("runner" === request.namespace) logger.debug(`[Headless] Dropped stale message "${request.method}" for ${request.target?.testFile ?? 'unknown'}`);
|
|
4499
|
+
}
|
|
4500
|
+
});
|
|
4501
|
+
const fileCleanupHandlers = new Map();
|
|
4502
|
+
dispatchRouter.register(DISPATCH_NAMESPACE_FILE_CLEANUP, async (request)=>{
|
|
4503
|
+
const sessionId = request.target?.sessionId;
|
|
4504
|
+
if (!sessionId) throw new Error('File cleanup dispatch is missing a browser session.');
|
|
4505
|
+
const handler = fileCleanupHandlers.get(sessionId);
|
|
4506
|
+
if (!handler) return;
|
|
4507
|
+
const payload = request.args;
|
|
4508
|
+
if ('start' === request.method) handler.start(payload.result);
|
|
4509
|
+
else if ('end' === request.method) handler.end();
|
|
4510
|
+
});
|
|
4511
|
+
const dispatchRunnerMessage = async (run, file, sessionId, message)=>{
|
|
4512
|
+
const response = await dispatchRouter.dispatch({
|
|
4513
|
+
requestId: nextDispatchRequestId("runner"),
|
|
4514
|
+
runToken: run.token,
|
|
4515
|
+
namespace: "runner",
|
|
4516
|
+
method: message.type,
|
|
4517
|
+
args: 'payload' in message ? message.payload : void 0,
|
|
4518
|
+
target: {
|
|
4519
|
+
sessionId,
|
|
4520
|
+
testFile: file.testPath,
|
|
4521
|
+
projectName: file.projectName
|
|
4522
|
+
}
|
|
4523
|
+
});
|
|
4524
|
+
if (response.stale) return;
|
|
4525
|
+
if (response.error) throw new Error(response.error);
|
|
4526
|
+
};
|
|
4527
|
+
const runSingleFile = async (run, file, rawCoverage)=>{
|
|
4528
|
+
if (run.cancelled || runLifecycle.isTokenStale(run.token)) return;
|
|
4529
|
+
const viewport = viewportByProject.get(file.projectName);
|
|
4530
|
+
const browserContext = await browser.newContext({
|
|
4531
|
+
providerOptions: browserLaunchOptions.providerOptions,
|
|
4532
|
+
viewport: viewport ?? null
|
|
4533
|
+
});
|
|
4534
|
+
run.contexts.add(browserContext);
|
|
4535
|
+
let page = null;
|
|
4536
|
+
let sessionId = null;
|
|
4537
|
+
let coverageCollection;
|
|
4538
|
+
let settled = false;
|
|
4539
|
+
let resolveDone = null;
|
|
4540
|
+
let cleanupTimer;
|
|
4541
|
+
let fileCleanupFinished = false;
|
|
4542
|
+
let provisionalResult;
|
|
4543
|
+
const markDone = ()=>{
|
|
4544
|
+
if (!settled) {
|
|
4545
|
+
settled = true;
|
|
4546
|
+
resolveDone?.();
|
|
4547
|
+
}
|
|
4548
|
+
};
|
|
4549
|
+
const donePromise = new Promise((resolve)=>{
|
|
4550
|
+
resolveDone = resolve;
|
|
4551
|
+
});
|
|
4552
|
+
const crashDeferred = createDeferredPromise();
|
|
4553
|
+
const onPageDead = (reason)=>{
|
|
4554
|
+
if (settled || run.cancelled || !runLifecycle.isTokenActive(run.token)) return;
|
|
4555
|
+
settled = true;
|
|
4556
|
+
crashDeferred.resolve(reason);
|
|
4557
|
+
};
|
|
4558
|
+
const collectCoverage = async ()=>{
|
|
4559
|
+
if (!page || !v8Coverage) return;
|
|
4560
|
+
if (!coverageCollection) coverageCollection = (async ()=>{
|
|
4561
|
+
const coverage = await v8Coverage.take(page, projectRootByName.get(file.projectName) ?? context.rootPath, file.projectName);
|
|
4562
|
+
if (coverage) rawCoverage.push(coverage);
|
|
4563
|
+
})();
|
|
4564
|
+
await coverageCollection;
|
|
4565
|
+
};
|
|
4566
|
+
try {
|
|
4567
|
+
page = await browserContext.newPage();
|
|
4568
|
+
await v8Coverage?.start(page);
|
|
4569
|
+
run.coverageCollectors.add(collectCoverage);
|
|
4570
|
+
page.on('crash', ()=>onPageDead(`Browser page crashed while running ${file.testPath}.`));
|
|
4571
|
+
page.on('close', ()=>onPageDead(`Browser page closed unexpectedly while running ${file.testPath}.`));
|
|
4572
|
+
const session = sessionRegistry.register({
|
|
4573
|
+
testFile: file.testPath,
|
|
4574
|
+
projectName: file.projectName,
|
|
4575
|
+
runToken: run.token,
|
|
4576
|
+
mode: 'headless-page',
|
|
4577
|
+
context: browserContext,
|
|
4578
|
+
page
|
|
4579
|
+
});
|
|
4580
|
+
sessionId = session.id;
|
|
4581
|
+
const clearFileCleanupTimeout = ()=>{
|
|
4582
|
+
if (cleanupTimer) {
|
|
4583
|
+
clearTimeout(cleanupTimer);
|
|
4584
|
+
cleanupTimer = void 0;
|
|
4585
|
+
}
|
|
4586
|
+
};
|
|
4587
|
+
const finishFileCleanup = ()=>{
|
|
4588
|
+
fileCleanupFinished = true;
|
|
4589
|
+
clearFileCleanupTimeout();
|
|
4590
|
+
};
|
|
4591
|
+
fileCleanupHandlers.set(session.id, {
|
|
4592
|
+
end: finishFileCleanup,
|
|
4593
|
+
start: (result)=>{
|
|
4594
|
+
if (fileCleanupFinished) return;
|
|
4595
|
+
provisionalResult = result;
|
|
4596
|
+
clearFileCleanupTimeout();
|
|
4597
|
+
cleanupTimer = setTimeout(()=>{
|
|
4598
|
+
(async ()=>{
|
|
4599
|
+
if (settled) return;
|
|
4600
|
+
settled = true;
|
|
4601
|
+
const message = `File fixture cleanup did not finish within ${FIXTURE_CLEANUP_TIMEOUT_MS}ms`;
|
|
4602
|
+
try {
|
|
4603
|
+
await collectCoverage();
|
|
4604
|
+
await handleTestFileComplete(createFileCleanupTimeoutResult({
|
|
4605
|
+
message,
|
|
4606
|
+
projectName: file.projectName,
|
|
4607
|
+
result: provisionalResult,
|
|
4608
|
+
testPath: file.testPath
|
|
4609
|
+
}));
|
|
4610
|
+
} catch (error) {
|
|
4611
|
+
const formatted = toError(error);
|
|
4612
|
+
await handleFatal({
|
|
4613
|
+
message: formatted.message,
|
|
4614
|
+
stack: formatted.stack
|
|
4615
|
+
});
|
|
4616
|
+
await collectRunCoverage(run);
|
|
4617
|
+
await cancelRun(run, false);
|
|
4618
|
+
} finally{
|
|
4619
|
+
resolveDone?.();
|
|
4620
|
+
}
|
|
4621
|
+
})();
|
|
4622
|
+
}, FIXTURE_CLEANUP_TIMEOUT_MS);
|
|
4623
|
+
}
|
|
4624
|
+
});
|
|
4625
|
+
await attachHeadlessRunnerTransport(page, {
|
|
4626
|
+
onDispatchMessage: async (envelope)=>{
|
|
4627
|
+
const message = envelope.message;
|
|
4628
|
+
try {
|
|
4629
|
+
if (settled) return;
|
|
4630
|
+
if ('file-complete' === message.type || 'fatal' === message.type) await collectCoverage();
|
|
4631
|
+
await dispatchRunnerMessage(run, file, session.id, message);
|
|
4632
|
+
if ('file-complete' === message.type) markDone();
|
|
4633
|
+
else if ('fatal' === message.type) {
|
|
4634
|
+
markDone();
|
|
4635
|
+
await collectRunCoverage(run);
|
|
4636
|
+
await cancelRun(run, false);
|
|
4637
|
+
}
|
|
4638
|
+
} catch (error) {
|
|
4639
|
+
const formatted = toError(error);
|
|
4640
|
+
await handleFatal({
|
|
4641
|
+
message: formatted.message,
|
|
4642
|
+
stack: formatted.stack
|
|
4643
|
+
});
|
|
4644
|
+
markDone();
|
|
4645
|
+
await collectRunCoverage(run);
|
|
4646
|
+
await cancelRun(run, false);
|
|
4647
|
+
}
|
|
4648
|
+
},
|
|
4649
|
+
onDispatchRpc: async (request)=>dispatchRouter.dispatch({
|
|
4650
|
+
...request,
|
|
4651
|
+
runToken: run.token,
|
|
4652
|
+
target: {
|
|
4653
|
+
sessionId: session.id,
|
|
4654
|
+
testFile: file.testPath,
|
|
4655
|
+
projectName: file.projectName,
|
|
4656
|
+
...request.target
|
|
4657
|
+
}
|
|
4658
|
+
})
|
|
4659
|
+
});
|
|
4660
|
+
const inlineOptions = {
|
|
4661
|
+
...hostOptions,
|
|
4662
|
+
snapshot: {
|
|
4663
|
+
updateSnapshot: context.snapshotManager.options.updateSnapshot
|
|
4664
|
+
},
|
|
4665
|
+
testFile: file.testPath,
|
|
4666
|
+
runId: `${run.token}:${session.id}`
|
|
4667
|
+
};
|
|
4668
|
+
const serializedOptions = serializeForInlineScript(inlineOptions);
|
|
4669
|
+
await page.addInitScript(`window.__RSTEST_BROWSER_OPTIONS__ = ${serializedOptions};`);
|
|
4670
|
+
const projectServer = projectServers.get(file.projectName);
|
|
4671
|
+
if (!projectServer) throw new Error(`No browser dev server for project "${file.projectName}" (test file: ${file.testPath}).`);
|
|
4672
|
+
await page.goto(`http://localhost:${projectServer.port}/runner.html`, {
|
|
4673
|
+
waitUntil: 'load'
|
|
4674
|
+
});
|
|
4675
|
+
const state = await Promise.race([
|
|
4676
|
+
donePromise.then(()=>({
|
|
4677
|
+
type: 'done'
|
|
4678
|
+
})),
|
|
4679
|
+
crashDeferred.promise.then((reason)=>({
|
|
4680
|
+
type: 'crash',
|
|
4681
|
+
reason
|
|
4682
|
+
})),
|
|
4683
|
+
run.cancelSignal.then(()=>({
|
|
4684
|
+
type: 'cancelled'
|
|
4685
|
+
}))
|
|
4686
|
+
]);
|
|
4687
|
+
if ('cancelled' === state.type) return;
|
|
4688
|
+
if ('crash' === state.type && runLifecycle.isTokenActive(run.token) && !run.cancelled) {
|
|
4689
|
+
await collectRunCoverage(run);
|
|
4690
|
+
await handleFatal({
|
|
4691
|
+
message: state.reason
|
|
4692
|
+
});
|
|
4693
|
+
await cancelRun(run, false);
|
|
4694
|
+
}
|
|
4695
|
+
} catch (error) {
|
|
4696
|
+
if (runLifecycle.isTokenActive(run.token) && !run.cancelled) {
|
|
4697
|
+
await collectRunCoverage(run);
|
|
4698
|
+
const formatted = toError(error);
|
|
4699
|
+
await handleFatal({
|
|
4700
|
+
message: formatted.message,
|
|
4701
|
+
stack: formatted.stack
|
|
4702
|
+
});
|
|
4703
|
+
await cancelRun(run, false);
|
|
4704
|
+
}
|
|
4705
|
+
} finally{
|
|
4706
|
+
if (cleanupTimer) clearTimeout(cleanupTimer);
|
|
4707
|
+
if (sessionId) fileCleanupHandlers.delete(sessionId);
|
|
4708
|
+
run.coverageCollectors.delete(collectCoverage);
|
|
4709
|
+
const abandoned = run.cancelled || runLifecycle.isTokenStale(run.token);
|
|
4710
|
+
const teardown = async ()=>{
|
|
4711
|
+
if (page) try {
|
|
4712
|
+
await page.close();
|
|
4713
|
+
} catch {}
|
|
4714
|
+
await closeContextSafely(browserContext);
|
|
4715
|
+
};
|
|
4716
|
+
if (sessionId) sessionRegistry.deleteById(sessionId);
|
|
4717
|
+
run.contexts.delete(browserContext);
|
|
4718
|
+
if (abandoned) teardown();
|
|
4719
|
+
else await teardown();
|
|
4720
|
+
}
|
|
4721
|
+
};
|
|
4722
|
+
const makeSkippedFileResult = (file)=>({
|
|
4723
|
+
testId: getFileTaskId(file.testPath),
|
|
4724
|
+
status: 'skip',
|
|
4725
|
+
name: '',
|
|
4726
|
+
testPath: file.testPath,
|
|
4727
|
+
project: file.projectName,
|
|
4728
|
+
results: []
|
|
4729
|
+
});
|
|
4730
|
+
const runFilesWithPool = async (files)=>{
|
|
4731
|
+
const rawCoverage = [];
|
|
4732
|
+
if (0 === files.length) return rawCoverage;
|
|
4733
|
+
const previous = runLifecycle.activeSession;
|
|
4734
|
+
if (previous) await cancelRun(previous);
|
|
4735
|
+
const run = runLifecycle.createSession((token)=>({
|
|
4736
|
+
...createRunSession(token),
|
|
4737
|
+
contexts: new Set(),
|
|
4738
|
+
coverageCollectors: new Set()
|
|
4739
|
+
}));
|
|
4740
|
+
const queue = [
|
|
4741
|
+
...files
|
|
4742
|
+
];
|
|
4743
|
+
const concurrency = getHeadlessConcurrency(context, queue.length);
|
|
4744
|
+
const bail = context.normalizedConfig.bail;
|
|
4745
|
+
const worker = async ()=>{
|
|
4746
|
+
while(queue.length > 0 && !run.cancelled && runLifecycle.isTokenActive(run.token)){
|
|
4747
|
+
if (bail && context.stateManager.getCountOfFailedTests() >= bail) {
|
|
4748
|
+
let skipped = queue.shift();
|
|
4749
|
+
while(skipped){
|
|
4750
|
+
await handleTestFileComplete(makeSkippedFileResult(skipped));
|
|
4751
|
+
skipped = queue.shift();
|
|
4752
|
+
}
|
|
4753
|
+
return;
|
|
4754
|
+
}
|
|
4755
|
+
const next = queue.shift();
|
|
4756
|
+
if (!next) return;
|
|
4757
|
+
await runSingleFile(run, next, rawCoverage);
|
|
4758
|
+
}
|
|
4759
|
+
};
|
|
4760
|
+
run.done = Promise.all(Array.from({
|
|
4761
|
+
length: Math.min(queue.length, Math.max(concurrency, 1))
|
|
4762
|
+
}, ()=>worker())).then(()=>{});
|
|
4763
|
+
await run.done;
|
|
4764
|
+
runLifecycle.clearIfActive(run);
|
|
4765
|
+
return rawCoverage;
|
|
4766
|
+
};
|
|
4767
|
+
const testStart = Date.now();
|
|
4768
|
+
const rawCoverage = await runFilesWithPool(allTestFiles);
|
|
4769
|
+
const testTime = Date.now() - testStart;
|
|
4770
|
+
let watchSession;
|
|
4771
|
+
if (isWatchMode) {
|
|
4772
|
+
const runScope = async (testPaths)=>{
|
|
4773
|
+
const pathSet = new Set(testPaths.map((testPath)=>normalize(testPath)));
|
|
4774
|
+
return runFilesWithPool(watchState.lastTestFiles.filter((file)=>pathSet.has(file.testPath)));
|
|
4775
|
+
};
|
|
4776
|
+
watchSignals.setInterrupt(async ()=>{
|
|
4777
|
+
const active = runLifecycle.activeSession;
|
|
4778
|
+
if (!active || active.cancelled) return;
|
|
4779
|
+
runLifecycle.invalidateActiveToken();
|
|
4780
|
+
await runLifecycle.cancel(active, {
|
|
4781
|
+
waitForDone: false
|
|
4782
|
+
});
|
|
4783
|
+
});
|
|
4784
|
+
watchSignals.setDispatchRerun(async ()=>{
|
|
4785
|
+
const newProjectEntries = await collectProjectEntries();
|
|
4786
|
+
const rerunPlan = planWatchRerun({
|
|
4787
|
+
projectEntries: newProjectEntries,
|
|
4788
|
+
previousTestFiles: watchState.lastTestFiles,
|
|
4789
|
+
affectedTestFiles: drainPendingAffectedTestFiles(watchState)
|
|
4790
|
+
});
|
|
4791
|
+
commitWatchFileSetUpdate(rerunPlan.fileSetUpdate, watchState, (deletedTestPaths)=>context.updateReporterResultState([], [], deletedTestPaths));
|
|
4792
|
+
logger.log(color.cyan(rerunPlan.decision.message));
|
|
4793
|
+
if ('idle' === rerunPlan.decision.kind) return void logWatchReady();
|
|
4794
|
+
await watchSignals.signalInvalidation('empty' === rerunPlan.decision.kind ? [] : rerunPlan.decision.testPaths);
|
|
4795
|
+
});
|
|
4796
|
+
watchSession = createWatchSession(runScope);
|
|
4797
|
+
}
|
|
4798
|
+
const closeHeadlessRuntime = isWatchMode ? void 0 : async ()=>{
|
|
4799
|
+
sessionRegistry.clear();
|
|
4800
|
+
await destroyRuntime();
|
|
4801
|
+
};
|
|
4802
|
+
return {
|
|
4803
|
+
testTime,
|
|
4804
|
+
rawCoverage,
|
|
4805
|
+
watchSession,
|
|
4806
|
+
close: closeHeadlessRuntime
|
|
4807
|
+
};
|
|
4808
|
+
};
|
|
4809
|
+
const isRecord = (value)=>'object' == typeof value && null !== value;
|
|
4810
|
+
const readString = (value, key, label)=>{
|
|
4811
|
+
const result = value[key];
|
|
4812
|
+
if ('string' != typeof result) throw new Error(`Invalid browser RPC request: ${label} must be a string`);
|
|
4813
|
+
return result;
|
|
4814
|
+
};
|
|
4815
|
+
const readUnknownArray = (value, key, label)=>{
|
|
4816
|
+
const result = value[key];
|
|
4817
|
+
if (!Array.isArray(result)) throw new Error(`Invalid browser RPC request: ${label} must be an array`);
|
|
4818
|
+
return result;
|
|
4819
|
+
};
|
|
4820
|
+
const parseBrowserLocatorIR = (value, label)=>{
|
|
4821
|
+
if (!isRecord(value)) throw new Error(`Invalid browser RPC request: ${label} must be an object`);
|
|
4822
|
+
const steps = value.steps;
|
|
4823
|
+
if (!Array.isArray(steps)) throw new Error(`Invalid browser RPC request: ${label}.steps must be an array`);
|
|
4824
|
+
return value;
|
|
4825
|
+
};
|
|
4826
|
+
const validateBrowserRpcRequest = (payload)=>{
|
|
4827
|
+
if (!isRecord(payload)) throw new Error('Invalid browser RPC request: payload must be an object');
|
|
4828
|
+
const kind = readString(payload, 'kind', 'kind');
|
|
4829
|
+
if ('locator' !== kind && 'expect' !== kind && 'config' !== kind) throw new Error(`Invalid browser RPC request: unsupported kind ${JSON.stringify(kind)}`);
|
|
4830
|
+
const request = {
|
|
4831
|
+
id: readString(payload, 'id', 'id'),
|
|
4832
|
+
testPath: readString(payload, 'testPath', 'testPath'),
|
|
4833
|
+
kind,
|
|
4834
|
+
locator: parseBrowserLocatorIR(payload.locator, 'locator'),
|
|
4835
|
+
method: readString(payload, 'method', 'method'),
|
|
4836
|
+
args: readUnknownArray(payload, 'args', 'args')
|
|
4837
|
+
};
|
|
4838
|
+
const isNot = payload.isNot;
|
|
4839
|
+
if (void 0 !== isNot) {
|
|
4840
|
+
if ('boolean' != typeof isNot) throw new Error('Invalid browser RPC request: isNot must be a boolean');
|
|
4841
|
+
request.isNot = isNot;
|
|
4842
|
+
}
|
|
4843
|
+
const timeout = payload.timeout;
|
|
4844
|
+
if (void 0 !== timeout) {
|
|
4845
|
+
if ('number' != typeof timeout) throw new Error('Invalid browser RPC request: timeout must be a number');
|
|
4846
|
+
request.timeout = timeout;
|
|
4847
|
+
}
|
|
4848
|
+
return request;
|
|
4849
|
+
};
|
|
4850
|
+
const watchContext = {
|
|
4851
|
+
runtime: null,
|
|
4852
|
+
cleanupRegistered: false,
|
|
4853
|
+
cleanupPromise: null
|
|
4854
|
+
};
|
|
4855
|
+
const runWatchRuntimeTeardown = (state, destroy)=>{
|
|
4856
|
+
if (state.cleanupPromise) return state.cleanupPromise;
|
|
4857
|
+
state.cleanupPromise = (async ()=>{
|
|
4858
|
+
if (!state.runtime) return;
|
|
4859
|
+
await destroy(state.runtime);
|
|
4860
|
+
state.runtime = null;
|
|
4861
|
+
})();
|
|
4862
|
+
return state.cleanupPromise.finally(()=>{
|
|
4863
|
+
state.cleanupPromise = null;
|
|
4864
|
+
});
|
|
4865
|
+
};
|
|
4866
|
+
const cleanupWatchRuntime = ()=>runWatchRuntimeTeardown(watchContext, destroyBrowserRuntime);
|
|
4867
|
+
const registerWatchCleanup = (embedded)=>{
|
|
4868
|
+
if (watchContext.cleanupRegistered) return;
|
|
4869
|
+
watchContext.cleanupRegistered = true;
|
|
4870
|
+
if (embedded) return;
|
|
4871
|
+
for (const signal of FATAL_SIGNALS)process.once(signal, ()=>{
|
|
4872
|
+
cleanupWatchRuntime();
|
|
4873
|
+
});
|
|
4874
|
+
process.once('exit', ()=>{
|
|
4875
|
+
cleanupWatchRuntime();
|
|
4876
|
+
});
|
|
4877
|
+
};
|
|
4878
|
+
const createWatchSignals = (onInvalidate)=>{
|
|
4879
|
+
let dispatchRerun;
|
|
4880
|
+
let interruptInFlightRun;
|
|
4881
|
+
let signalledCycle;
|
|
4882
|
+
return {
|
|
4883
|
+
setDispatchRerun (fn) {
|
|
4884
|
+
dispatchRerun = fn;
|
|
4885
|
+
},
|
|
4886
|
+
async runDispatchRerun () {
|
|
4887
|
+
await dispatchRerun?.();
|
|
4888
|
+
},
|
|
4889
|
+
setInterrupt (fn) {
|
|
4890
|
+
interruptInFlightRun = fn;
|
|
4891
|
+
},
|
|
4892
|
+
async signalInvalidation (fileFilters, claimScope) {
|
|
4893
|
+
await interruptInFlightRun?.();
|
|
4894
|
+
claimScope?.();
|
|
4895
|
+
const cycle = Promise.resolve(onInvalidate?.({
|
|
4896
|
+
isFirstBuild: false,
|
|
4897
|
+
fileFilters
|
|
4898
|
+
})).then(()=>{}, (error)=>{
|
|
4899
|
+
logger.error(color.red('Browser Mode watch cycle failed:'), error);
|
|
4900
|
+
});
|
|
4901
|
+
signalledCycle = cycle;
|
|
4902
|
+
return {
|
|
4903
|
+
cycle
|
|
4904
|
+
};
|
|
4905
|
+
},
|
|
4906
|
+
async awaitSignalledCycle () {
|
|
4907
|
+
await signalledCycle;
|
|
4908
|
+
}
|
|
4909
|
+
};
|
|
4910
|
+
};
|
|
4911
|
+
let nextBrowserFilePid = 1000000000;
|
|
4912
|
+
const resolveEmptyLaunchExitCode = (current, { allowEmptyRun, isWatchMode, passWithNoTests })=>{
|
|
4913
|
+
if (allowEmptyRun || !isWatchMode || passWithNoTests) return current;
|
|
4914
|
+
return void 0 === current || 0 === current ? 1 : current;
|
|
4915
|
+
};
|
|
4916
|
+
const getMaxTestTimeoutForRpc = (projects)=>Math.max(...projects.map((p)=>p.normalizedConfig.testTimeout ?? DEFAULT_TEST_TIMEOUT));
|
|
4917
|
+
const resolveProviderForTestPath = ({ testPath, browserProjects })=>{
|
|
4918
|
+
const normalizedTestPath = normalize(testPath);
|
|
4919
|
+
const sortedProjects = [
|
|
4920
|
+
...browserProjects
|
|
4921
|
+
].sort((a, b)=>b.rootPath.length - a.rootPath.length);
|
|
4922
|
+
for (const project of sortedProjects)if (normalizedTestPath.startsWith(project.rootPath)) return project.provider;
|
|
4923
|
+
throw new Error(`Cannot resolve browser provider for test path: ${JSON.stringify(testPath)}. Known project roots: ${JSON.stringify(sortedProjects.map((p)=>p.rootPath))}`);
|
|
4924
|
+
};
|
|
4925
|
+
const runBrowserController = async (context, options)=>{
|
|
4926
|
+
const { allowEmptyRun = false, filesOnly = false, onTraceEvents, env, onInvalidate } = options ?? {};
|
|
4927
|
+
const buildStart = Date.now();
|
|
4928
|
+
const isWatchMode = 'watch' === context.command;
|
|
4929
|
+
const phaseTrackers = onTraceEvents ? new Map() : void 0;
|
|
4930
|
+
const trackerKey = (project, testPath)=>`${project}\u0000${testPath}`;
|
|
4931
|
+
const browserProjects = options?.projects ?? getBrowserProjects(context);
|
|
4932
|
+
const useHeadlessDirect = browserProjects.every((project)=>project.normalizedConfig.browser.headless);
|
|
4933
|
+
const browserSourceMapCache = new Map();
|
|
4934
|
+
const browserCoverageResources = {
|
|
4935
|
+
assetFiles: new Map(),
|
|
4936
|
+
sourceMaps: new Map()
|
|
4937
|
+
};
|
|
4938
|
+
const loadBrowserCoverageResources = async (filenames, resource)=>{
|
|
4939
|
+
const store = browserCoverageResources[resource];
|
|
4940
|
+
const loaded = {};
|
|
4941
|
+
for (const filename of filenames){
|
|
4942
|
+
const value = store.get(filename);
|
|
4943
|
+
if (void 0 !== value) loaded[filename] = value;
|
|
4944
|
+
}
|
|
4945
|
+
return loaded;
|
|
4946
|
+
};
|
|
4947
|
+
const loadBrowserCoverageAssetFiles = (filenames)=>loadBrowserCoverageResources(filenames, 'assetFiles');
|
|
4948
|
+
const loadBrowserCoverageSourceMaps = (filenames)=>loadBrowserCoverageResources(filenames, 'sourceMaps');
|
|
4949
|
+
const isHttpLikeFile = (file)=>/^https?:\/\//.test(file);
|
|
4950
|
+
const resolveBrowserSourcemap = async (sourcePath)=>{
|
|
4951
|
+
if (!isHttpLikeFile(sourcePath)) return {
|
|
4952
|
+
handled: false,
|
|
4953
|
+
sourcemap: null
|
|
4954
|
+
};
|
|
4955
|
+
const normalizedUrl = normalizeJavaScriptUrl(sourcePath);
|
|
4956
|
+
if (!normalizedUrl) return {
|
|
4957
|
+
handled: true,
|
|
4958
|
+
sourcemap: null
|
|
4959
|
+
};
|
|
4960
|
+
if (browserSourceMapCache.has(normalizedUrl)) return {
|
|
4961
|
+
handled: true,
|
|
4962
|
+
sourcemap: browserSourceMapCache.get(normalizedUrl) ?? null
|
|
4963
|
+
};
|
|
4964
|
+
return {
|
|
4965
|
+
handled: true,
|
|
4966
|
+
sourcemap: await loadSourceMapWithCache({
|
|
4967
|
+
jsUrl: normalizedUrl,
|
|
4968
|
+
cache: browserSourceMapCache
|
|
4969
|
+
})
|
|
4970
|
+
};
|
|
4971
|
+
};
|
|
4972
|
+
const getBrowserSourcemap = async (sourcePath)=>{
|
|
4973
|
+
const result = await resolveBrowserSourcemap(sourcePath);
|
|
4974
|
+
return result.handled ? result.sourcemap : null;
|
|
4975
|
+
};
|
|
4976
|
+
const buildErrorResult = (error, close)=>{
|
|
4977
|
+
const elapsed = Math.max(0, Date.now() - buildStart);
|
|
4978
|
+
return {
|
|
4979
|
+
results: [],
|
|
4980
|
+
testResults: [],
|
|
4981
|
+
duration: {
|
|
4982
|
+
totalTime: elapsed,
|
|
4983
|
+
buildTime: elapsed,
|
|
4984
|
+
testTime: 0
|
|
4985
|
+
},
|
|
4986
|
+
hasFailure: true,
|
|
4987
|
+
unhandledErrors: [
|
|
4988
|
+
error
|
|
4989
|
+
],
|
|
4990
|
+
getSourcemap: getBrowserSourcemap,
|
|
4991
|
+
resolveSourcemap: resolveBrowserSourcemap,
|
|
4992
|
+
close
|
|
4993
|
+
};
|
|
4994
|
+
};
|
|
4995
|
+
const failWithError = async (error, cleanup)=>{
|
|
4996
|
+
const normalizedError = toError(error);
|
|
4997
|
+
if (cleanup && !isWatchMode) return buildErrorResult(normalizedError, cleanup);
|
|
4998
|
+
try {
|
|
4999
|
+
return buildErrorResult(normalizedError);
|
|
5000
|
+
} finally{
|
|
5001
|
+
await cleanup?.();
|
|
5002
|
+
}
|
|
5003
|
+
};
|
|
5004
|
+
const coverageConfig = browserProjects.find((project)=>project.normalizedConfig.coverage?.enabled)?.normalizedConfig.coverage;
|
|
5005
|
+
const coverageProvider = !filesOnly && 'list' !== context.command && coverageConfig?.enabled ? await createCoverageProvider(coverageConfig, context.rootPath) : null;
|
|
5006
|
+
const browserCoverageCapabilityError = filesOnly || 'list' === context.command || coverageConfig?.provider !== 'v8' || (browserProjects[0]?.normalizedConfig.browser.browser ?? 'chromium') !== 'chromium' || coverageProvider?.supportsBrowserCoverage === true ? void 0 : new Error("The installed @rstest/coverage-v8 provider does not support Chromium Browser Mode coverage. Upgrade @rstest/coverage-v8 to the version matching @rstest/core, or use coverage.provider: 'istanbul'.");
|
|
5007
|
+
const containerDevServerEnv = process.env.RSTEST_CONTAINER_DEV_SERVER;
|
|
5008
|
+
let containerDevServer;
|
|
5009
|
+
let containerDistPath;
|
|
5010
|
+
if (!useHeadlessDirect) {
|
|
5011
|
+
if (containerDevServerEnv) try {
|
|
5012
|
+
containerDevServer = new URL(containerDevServerEnv).toString();
|
|
5013
|
+
logger.debug(`[Browser UI] Using dev server for container: ${containerDevServer}`);
|
|
5014
|
+
} catch (error) {
|
|
5015
|
+
const originalError = toError(error);
|
|
5016
|
+
originalError.message = `Invalid RSTEST_CONTAINER_DEV_SERVER value: ${originalError.message}`;
|
|
5017
|
+
return failWithError(originalError);
|
|
5018
|
+
}
|
|
5019
|
+
if (!containerDevServer) try {
|
|
5020
|
+
containerDistPath = resolveContainerDist();
|
|
5021
|
+
} catch (error) {
|
|
5022
|
+
return failWithError(error);
|
|
5023
|
+
}
|
|
5024
|
+
}
|
|
5025
|
+
let projectEntries = await resolveProjectEntries(context, options?.shardedEntries, browserProjects);
|
|
5026
|
+
let totalTests = projectEntries.reduce((total, item)=>total + item.testFiles.length, 0);
|
|
5027
|
+
const shouldInitializeEmptyBrowserHooks = 0 === totalTests && hasUserRstestConfigPlugins(browserProjects);
|
|
5028
|
+
const createEmptyRunResult = ()=>{
|
|
5029
|
+
const elapsed = Math.max(0, Date.now() - buildStart);
|
|
5030
|
+
return {
|
|
5031
|
+
results: [],
|
|
5032
|
+
testResults: [],
|
|
5033
|
+
duration: {
|
|
5034
|
+
totalTime: elapsed,
|
|
5035
|
+
buildTime: elapsed,
|
|
5036
|
+
testTime: 0
|
|
5037
|
+
},
|
|
5038
|
+
hasFailure: false,
|
|
5039
|
+
getSourcemap: getBrowserSourcemap,
|
|
5040
|
+
resolveSourcemap: resolveBrowserSourcemap
|
|
5041
|
+
};
|
|
5042
|
+
};
|
|
5043
|
+
const writeEmptyLaunchExitCode = ()=>{
|
|
5044
|
+
process.exitCode = resolveEmptyLaunchExitCode(process.exitCode, {
|
|
5045
|
+
allowEmptyRun,
|
|
5046
|
+
isWatchMode,
|
|
5047
|
+
passWithNoTests: context.normalizedConfig.passWithNoTests
|
|
5048
|
+
});
|
|
5049
|
+
};
|
|
5050
|
+
if (0 === totalTests && !shouldInitializeEmptyBrowserHooks) {
|
|
5051
|
+
writeEmptyLaunchExitCode();
|
|
5052
|
+
return allowEmptyRun ? createEmptyRunResult() : void 0;
|
|
5053
|
+
}
|
|
5054
|
+
const enableCliShortcuts = isWatchMode && isTTY('stdin');
|
|
5055
|
+
const browserTempOutputRoot = context.normalizedConfig.output.distPath.root;
|
|
4068
5056
|
const tempDir = isWatchMode && watchContext.runtime ? watchContext.runtime.tempDir : isWatchMode ? join(context.rootPath, browserTempOutputRoot, 'browser', 'watch') : join(context.rootPath, browserTempOutputRoot, 'browser', Date.now().toString());
|
|
4069
5057
|
let runtime = isWatchMode ? watchContext.runtime : null;
|
|
4070
|
-
|
|
4071
|
-
let awaitHeadlessRerunIdle;
|
|
5058
|
+
const watchSignals = createWatchSignals(onInvalidate);
|
|
4072
5059
|
if (!runtime) {
|
|
4073
5060
|
try {
|
|
4074
5061
|
runtime = await createBrowserRuntime({
|
|
@@ -4079,12 +5066,9 @@ const runBrowserController = async (context, options)=>{
|
|
|
4079
5066
|
freezeShardedEntries: options?.freezeShardedEntries,
|
|
4080
5067
|
tempDir,
|
|
4081
5068
|
isWatchMode,
|
|
4082
|
-
onTriggerRerun: isWatchMode ? async ()=>{
|
|
4083
|
-
await triggerRerun?.();
|
|
4084
|
-
} : void 0,
|
|
4085
5069
|
containerDistPath,
|
|
4086
5070
|
containerDevServer,
|
|
4087
|
-
skipProviderLaunch: filesOnly,
|
|
5071
|
+
skipProviderLaunch: filesOnly || Boolean(browserCoverageCapabilityError),
|
|
4088
5072
|
appliedModifyRstestConfigEnvironments: options?.appliedModifyRstestConfigEnvironments
|
|
4089
5073
|
});
|
|
4090
5074
|
} catch (error) {
|
|
@@ -4095,13 +5079,18 @@ const runBrowserController = async (context, options)=>{
|
|
|
4095
5079
|
}).catch(()=>{});
|
|
4096
5080
|
});
|
|
4097
5081
|
}
|
|
4098
|
-
if (isWatchMode) {
|
|
5082
|
+
if (isWatchMode && !filesOnly && !browserCoverageCapabilityError) {
|
|
4099
5083
|
watchContext.runtime = runtime;
|
|
4100
5084
|
registerWatchCleanup(context.embedded);
|
|
4101
5085
|
}
|
|
4102
5086
|
}
|
|
4103
5087
|
const watchState = runtime.watchState;
|
|
4104
|
-
if (isWatchMode)
|
|
5088
|
+
if (isWatchMode) {
|
|
5089
|
+
watchState.lastTestFiles = collectWatchTestFiles(projectEntries);
|
|
5090
|
+
watchState.triggerRerun = async ()=>{
|
|
5091
|
+
await watchSignals.runDispatchRerun();
|
|
5092
|
+
};
|
|
5093
|
+
}
|
|
4105
5094
|
const seedPendingRerun = (testPaths)=>{
|
|
4106
5095
|
const wanted = testPaths ? new Set(testPaths.map((testPath)=>normalize(testPath))) : null;
|
|
4107
5096
|
let seeded = 0;
|
|
@@ -4114,22 +5103,11 @@ const runBrowserController = async (context, options)=>{
|
|
|
4114
5103
|
}
|
|
4115
5104
|
return seeded;
|
|
4116
5105
|
};
|
|
4117
|
-
const
|
|
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 })=>{
|
|
5106
|
+
const buildRerunOutcome = ({ rerunTestPaths, testTime, rawCoverage, unhandledErrors })=>{
|
|
4127
5107
|
const rerunPathSet = new Set(rerunTestPaths);
|
|
4128
5108
|
const rerunResults = context.reporterResults.results.filter((result)=>rerunPathSet.has(result.testPath));
|
|
4129
|
-
let rerunCoverage;
|
|
4130
5109
|
const coverageMap = buildBrowserCoverageMap(rerunResults, coverageProvider);
|
|
4131
|
-
|
|
4132
|
-
const outcome = {
|
|
5110
|
+
return {
|
|
4133
5111
|
results: rerunResults,
|
|
4134
5112
|
testResults: context.reporterResults.testResults.filter((result)=>rerunPathSet.has(result.testPath)),
|
|
4135
5113
|
errors: unhandledErrors ?? [],
|
|
@@ -4138,21 +5116,45 @@ const runBrowserController = async (context, options)=>{
|
|
|
4138
5116
|
buildTime: drainPendingBuildTime(watchState),
|
|
4139
5117
|
testTime
|
|
4140
5118
|
},
|
|
4141
|
-
coverage:
|
|
4142
|
-
map:
|
|
5119
|
+
coverage: coverageMap?.files().length || rawCoverage.length ? {
|
|
5120
|
+
map: coverageMap?.toJSON(),
|
|
5121
|
+
raw: rawCoverage,
|
|
5122
|
+
loadAssetFiles: loadBrowserCoverageAssetFiles,
|
|
5123
|
+
loadSourceMaps: loadBrowserCoverageSourceMaps
|
|
4143
5124
|
} : void 0,
|
|
4144
5125
|
resolveSourcemap: resolveBrowserSourcemap
|
|
4145
5126
|
};
|
|
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
5127
|
};
|
|
5128
|
+
const createWatchSession = (execute)=>({
|
|
5129
|
+
runCycle: async (testPaths)=>{
|
|
5130
|
+
const rerunStartTime = Date.now();
|
|
5131
|
+
fatalErrorRef.current = null;
|
|
5132
|
+
let rerunError;
|
|
5133
|
+
let rawCoverage = [];
|
|
5134
|
+
try {
|
|
5135
|
+
rawCoverage = await execute(testPaths);
|
|
5136
|
+
} catch (error) {
|
|
5137
|
+
rerunError = toError(error);
|
|
5138
|
+
}
|
|
5139
|
+
const rerunFatalError = fatalErrorRef.current ?? void 0;
|
|
5140
|
+
return buildRerunOutcome({
|
|
5141
|
+
rerunTestPaths: testPaths,
|
|
5142
|
+
testTime: Math.max(0, Date.now() - rerunStartTime),
|
|
5143
|
+
rawCoverage,
|
|
5144
|
+
unhandledErrors: rerunError ? [
|
|
5145
|
+
rerunError
|
|
5146
|
+
] : rerunFatalError ? [
|
|
5147
|
+
rerunFatalError
|
|
5148
|
+
] : void 0
|
|
5149
|
+
});
|
|
5150
|
+
},
|
|
5151
|
+
requestRerun: async (testPaths)=>{
|
|
5152
|
+
const seeded = seedPendingRerun(testPaths);
|
|
5153
|
+
if (testPaths && 0 === seeded) return;
|
|
5154
|
+
await watchSignals.runDispatchRerun();
|
|
5155
|
+
await watchSignals.awaitSignalledCycle();
|
|
5156
|
+
}
|
|
5157
|
+
});
|
|
4156
5158
|
projectEntries = runtime.projectEntries;
|
|
4157
5159
|
totalTests = projectEntries.reduce((total, item)=>total + item.testFiles.length, 0);
|
|
4158
5160
|
const buildTime = Date.now() - buildStart;
|
|
@@ -4170,11 +5172,15 @@ const runBrowserController = async (context, options)=>{
|
|
|
4170
5172
|
close: ()=>destroyBrowserRuntime(runtime)
|
|
4171
5173
|
};
|
|
4172
5174
|
if (0 === totalTests) {
|
|
4173
|
-
|
|
5175
|
+
writeEmptyLaunchExitCode();
|
|
4174
5176
|
await destroyBrowserRuntime(runtime);
|
|
4175
5177
|
return allowEmptyRun ? createEmptyRunResult() : void 0;
|
|
4176
5178
|
}
|
|
4177
|
-
|
|
5179
|
+
if (browserCoverageCapabilityError) {
|
|
5180
|
+
await destroyBrowserRuntime(runtime);
|
|
5181
|
+
return failWithError(browserCoverageCapabilityError);
|
|
5182
|
+
}
|
|
5183
|
+
const { browser, browserLaunchOptions, wsPort } = runtime;
|
|
4178
5184
|
const allTestFiles = projectEntries.flatMap((entry)=>entry.testFiles.map((testPath)=>({
|
|
4179
5185
|
testPath: normalize(testPath),
|
|
4180
5186
|
projectName: entry.project.name
|
|
@@ -4196,17 +5202,18 @@ const runBrowserController = async (context, options)=>{
|
|
|
4196
5202
|
name,
|
|
4197
5203
|
`http://localhost:${server.port}`
|
|
4198
5204
|
]));
|
|
5205
|
+
const containerRunnerUrl = `http://localhost:${runtime.containerServer.port}`;
|
|
4199
5206
|
const hostOptions = {
|
|
4200
5207
|
rootPath: normalize(context.rootPath),
|
|
4201
5208
|
projects: projectRuntimeConfigs,
|
|
4202
5209
|
snapshot: {
|
|
4203
5210
|
updateSnapshot: options?.updateSnapshot ?? context.snapshotManager.options.updateSnapshot
|
|
4204
5211
|
},
|
|
4205
|
-
runnerUrl:
|
|
5212
|
+
runnerUrl: containerRunnerUrl,
|
|
4206
5213
|
projectRunnerUrls,
|
|
4207
5214
|
wsPort,
|
|
4208
5215
|
debug: isDebug(),
|
|
4209
|
-
rpcTimeout:
|
|
5216
|
+
rpcTimeout: -1
|
|
4210
5217
|
};
|
|
4211
5218
|
const browserProviderProjects = browserProjects.map((project)=>({
|
|
4212
5219
|
rootPath: normalize(project.rootPath),
|
|
@@ -4214,8 +5221,26 @@ const runBrowserController = async (context, options)=>{
|
|
|
4214
5221
|
}));
|
|
4215
5222
|
const implementationByProvider = new Map();
|
|
4216
5223
|
for (const browserProject of browserProviderProjects)if (!implementationByProvider.has(browserProject.provider)) implementationByProvider.set(browserProject.provider, getBrowserProviderImplementation(browserProject.provider));
|
|
4217
|
-
|
|
4218
|
-
|
|
5224
|
+
const browserName = browserLaunchOptions.browser ?? 'chromium';
|
|
5225
|
+
const v8CoverageCollector = coverageConfig?.provider === 'v8' ? implementationByProvider.get(browserLaunchOptions.provider)?.createV8CoverageCollector?.({
|
|
5226
|
+
browserName
|
|
5227
|
+
}) ?? null : null;
|
|
5228
|
+
const v8Coverage = v8CoverageCollector ? {
|
|
5229
|
+
start: v8CoverageCollector.start,
|
|
5230
|
+
take: (page, projectRoot, projectName)=>takeBrowserV8Coverage({
|
|
5231
|
+
collector: v8CoverageCollector,
|
|
5232
|
+
fetchTimeout: maxTestTimeoutForRpc,
|
|
5233
|
+
page,
|
|
5234
|
+
projectUrl: projectRunnerUrls[projectName ?? ''] ?? containerRunnerUrl,
|
|
5235
|
+
rootPath: normalize(projectRoot),
|
|
5236
|
+
sourceMapCache: browserSourceMapCache,
|
|
5237
|
+
resourceStore: browserCoverageResources
|
|
5238
|
+
})
|
|
5239
|
+
} : void 0;
|
|
5240
|
+
let resolveDispatchPages = ()=>({});
|
|
5241
|
+
const setDispatchPageResolver = (resolver)=>{
|
|
5242
|
+
resolveDispatchPages = resolver;
|
|
5243
|
+
};
|
|
4219
5244
|
const dispatchBrowserRpcRequest = async ({ request, target })=>{
|
|
4220
5245
|
const timeoutFallbackMs = maxTestTimeoutForRpc;
|
|
4221
5246
|
const provider = resolveProviderForTestPath({
|
|
@@ -4224,12 +5249,12 @@ const runBrowserController = async (context, options)=>{
|
|
|
4224
5249
|
});
|
|
4225
5250
|
const implementation = implementationByProvider.get(provider);
|
|
4226
5251
|
if (!implementation) throw new Error(`Browser provider implementation not found: ${provider}`);
|
|
4227
|
-
const runnerPage
|
|
5252
|
+
const { runnerPage, containerPage } = resolveDispatchPages(target);
|
|
4228
5253
|
if (target?.sessionId && !runnerPage) throw new Error(`Runner page session not found for browser dispatch: ${target.sessionId}`);
|
|
4229
|
-
if (!runnerPage && !
|
|
5254
|
+
if (!runnerPage && !containerPage) throw new Error('Browser container page is not initialized');
|
|
4230
5255
|
try {
|
|
4231
5256
|
return await implementation.dispatchRpc({
|
|
4232
|
-
containerPage: runnerPage ? void 0 :
|
|
5257
|
+
containerPage: runnerPage ? void 0 : containerPage,
|
|
4233
5258
|
runnerPage,
|
|
4234
5259
|
request,
|
|
4235
5260
|
timeoutFallbackMs
|
|
@@ -4249,7 +5274,9 @@ const runBrowserController = async (context, options)=>{
|
|
|
4249
5274
|
runtime.setContainerOptions(hostOptions);
|
|
4250
5275
|
const reporterResults = [];
|
|
4251
5276
|
const caseResults = [];
|
|
4252
|
-
|
|
5277
|
+
const fatalErrorRef = {
|
|
5278
|
+
current: null
|
|
5279
|
+
};
|
|
4253
5280
|
const runnerSinks = new Map(browserProjects.map((project)=>[
|
|
4254
5281
|
project.name,
|
|
4255
5282
|
createRunnerEventSink(context, project.normalizedConfig)
|
|
@@ -4259,14 +5286,22 @@ const runBrowserController = async (context, options)=>{
|
|
|
4259
5286
|
if (!sink) throw new Error(`No runner event sink for project "${projectName}"`);
|
|
4260
5287
|
return sink;
|
|
4261
5288
|
};
|
|
4262
|
-
const
|
|
4263
|
-
|
|
4264
|
-
|
|
4265
|
-
|
|
4266
|
-
|
|
4267
|
-
|
|
4268
|
-
|
|
4269
|
-
|
|
5289
|
+
const silentConsoleControllers = new Map(browserProjects.map((project)=>[
|
|
5290
|
+
project.name,
|
|
5291
|
+
createSilentConsoleController({
|
|
5292
|
+
runtimeConfig: {
|
|
5293
|
+
silent: project.normalizedConfig.silent,
|
|
5294
|
+
disableConsoleIntercept: project.normalizedConfig.disableConsoleIntercept
|
|
5295
|
+
},
|
|
5296
|
+
emitInterceptedLog: (log)=>sinkForProjectName(log.project).onConsoleLog(log),
|
|
5297
|
+
writeOriginalLog: ()=>{}
|
|
5298
|
+
})
|
|
5299
|
+
]));
|
|
5300
|
+
const silentConsoleControllerForProjectName = (projectName)=>{
|
|
5301
|
+
const controller = silentConsoleControllers.get(projectName);
|
|
5302
|
+
if (!controller) throw new Error(`No silent console controller for project "${projectName}"`);
|
|
5303
|
+
return controller;
|
|
5304
|
+
};
|
|
4270
5305
|
const snapshotRpcMethods = {
|
|
4271
5306
|
async resolveSnapshotPath (testPath) {
|
|
4272
5307
|
return resolveSnapshotPathDefault(testPath, context.normalizedConfig.resolveSnapshotPath);
|
|
@@ -4321,7 +5356,7 @@ const runBrowserController = async (context, options)=>{
|
|
|
4321
5356
|
const handleTestSuiteResult = async (payload)=>{
|
|
4322
5357
|
phaseTrackers?.get(trackerKey(payload.project, payload.testPath))?.recordSuiteResult(payload);
|
|
4323
5358
|
await sinkForProjectName(payload.project).onTestSuiteResult(payload);
|
|
4324
|
-
|
|
5359
|
+
silentConsoleControllerForProjectName(payload.project).flushBufferedLogsForTask({
|
|
4325
5360
|
taskId: payload.testId,
|
|
4326
5361
|
status: payload.status,
|
|
4327
5362
|
taskParentNames: payload.parentNames,
|
|
@@ -4337,7 +5372,7 @@ const runBrowserController = async (context, options)=>{
|
|
|
4337
5372
|
caseResults.push(payload);
|
|
4338
5373
|
phaseTrackers?.get(trackerKey(payload.project, payload.testPath))?.recordCaseResult(payload);
|
|
4339
5374
|
await sinkForProjectName(payload.project).onTestCaseResult(payload);
|
|
4340
|
-
|
|
5375
|
+
silentConsoleControllerForProjectName(payload.project).flushBufferedLogsForTask({
|
|
4341
5376
|
taskId: payload.testId,
|
|
4342
5377
|
status: payload.status,
|
|
4343
5378
|
taskParentNames: payload.parentNames,
|
|
@@ -4360,7 +5395,7 @@ const runBrowserController = async (context, options)=>{
|
|
|
4360
5395
|
phaseTrackers.delete(key);
|
|
4361
5396
|
}
|
|
4362
5397
|
}
|
|
4363
|
-
|
|
5398
|
+
silentConsoleControllerForProjectName(payload.project).flushBufferedLogsForTask({
|
|
4364
5399
|
taskId: payload.testId,
|
|
4365
5400
|
status: payload.status,
|
|
4366
5401
|
taskParentNames: payload.parentNames,
|
|
@@ -4368,7 +5403,6 @@ const runBrowserController = async (context, options)=>{
|
|
|
4368
5403
|
testPath: payload.testPath
|
|
4369
5404
|
});
|
|
4370
5405
|
await sinkForProjectName(payload.project).onTestFileResult(payload);
|
|
4371
|
-
if (isWatchMode && 'fail' === payload.status) ensureProcessExitCode(1);
|
|
4372
5406
|
};
|
|
4373
5407
|
const handleLog = async (payload)=>{
|
|
4374
5408
|
const log = {
|
|
@@ -4383,13 +5417,12 @@ const runBrowserController = async (context, options)=>{
|
|
|
4383
5417
|
type: payload.type,
|
|
4384
5418
|
trace: payload.trace
|
|
4385
5419
|
};
|
|
4386
|
-
|
|
5420
|
+
silentConsoleControllerForProjectName(payload.projectName).onConsoleLog(log);
|
|
4387
5421
|
};
|
|
4388
5422
|
const handleFatal = async (payload)=>{
|
|
4389
5423
|
const error = new Error(payload.message);
|
|
4390
5424
|
error.stack = payload.stack;
|
|
4391
|
-
|
|
4392
|
-
if (isWatchMode) ensureProcessExitCode(1);
|
|
5425
|
+
fatalErrorRef.current = error;
|
|
4393
5426
|
};
|
|
4394
5427
|
const runSnapshotRpc = async (request)=>{
|
|
4395
5428
|
switch(request.method){
|
|
@@ -4427,637 +5460,73 @@ const runBrowserController = async (context, options)=>{
|
|
|
4427
5460
|
logger.debug(`[Dispatch] Skip registering dispatch namespace "${namespace}" because it is already reserved`);
|
|
4428
5461
|
}
|
|
4429
5462
|
});
|
|
4430
|
-
|
|
4431
|
-
|
|
4432
|
-
|
|
4433
|
-
|
|
4434
|
-
|
|
4435
|
-
|
|
4436
|
-
|
|
4437
|
-
|
|
4438
|
-
|
|
4439
|
-
|
|
4440
|
-
|
|
4441
|
-
|
|
4442
|
-
|
|
4443
|
-
|
|
4444
|
-
|
|
4445
|
-
|
|
4446
|
-
|
|
4447
|
-
}
|
|
4448
|
-
});
|
|
4449
|
-
};
|
|
4450
|
-
const dispatchRouter = createDispatchRouter({
|
|
4451
|
-
isRunTokenStale: (runToken)=>runLifecycle.isTokenStale(runToken),
|
|
4452
|
-
onStale: (request)=>{
|
|
4453
|
-
if ("runner" === request.namespace) logger.debug(`[Headless] Dropped stale message "${request.method}" for ${request.target?.testFile ?? 'unknown'}`);
|
|
4454
|
-
}
|
|
4455
|
-
});
|
|
4456
|
-
const dispatchRunnerMessage = async (run, file, sessionId, message)=>{
|
|
4457
|
-
const response = await dispatchRouter.dispatch({
|
|
4458
|
-
requestId: nextDispatchRequestId("runner"),
|
|
4459
|
-
runToken: run.token,
|
|
4460
|
-
namespace: "runner",
|
|
4461
|
-
method: message.type,
|
|
4462
|
-
args: 'payload' in message ? message.payload : void 0,
|
|
4463
|
-
target: {
|
|
4464
|
-
sessionId,
|
|
4465
|
-
testFile: file.testPath,
|
|
4466
|
-
projectName: file.projectName
|
|
4467
|
-
}
|
|
4468
|
-
});
|
|
4469
|
-
if (response.stale) return;
|
|
4470
|
-
if (response.error) throw new Error(response.error);
|
|
4471
|
-
};
|
|
4472
|
-
const runSingleFile = async (run, file)=>{
|
|
4473
|
-
if (run.cancelled || runLifecycle.isTokenStale(run.token)) return;
|
|
4474
|
-
const viewport = viewportByProject.get(file.projectName);
|
|
4475
|
-
const browserContext = await browser.newContext({
|
|
4476
|
-
providerOptions: browserLaunchOptions.providerOptions,
|
|
4477
|
-
viewport: viewport ?? null
|
|
4478
|
-
});
|
|
4479
|
-
run.contexts.add(browserContext);
|
|
4480
|
-
let page = null;
|
|
4481
|
-
let sessionId = null;
|
|
4482
|
-
let settled = false;
|
|
4483
|
-
let resolveDone = null;
|
|
4484
|
-
const markDone = ()=>{
|
|
4485
|
-
if (!settled) {
|
|
4486
|
-
settled = true;
|
|
4487
|
-
resolveDone?.();
|
|
4488
|
-
}
|
|
4489
|
-
};
|
|
4490
|
-
const donePromise = new Promise((resolve)=>{
|
|
4491
|
-
resolveDone = resolve;
|
|
4492
|
-
});
|
|
4493
|
-
const crashDeferred = createDeferredPromise();
|
|
4494
|
-
const onPageDead = (reason)=>{
|
|
4495
|
-
if (settled || run.cancelled || !runLifecycle.isTokenActive(run.token)) return;
|
|
4496
|
-
settled = true;
|
|
4497
|
-
crashDeferred.resolve(reason);
|
|
4498
|
-
};
|
|
4499
|
-
try {
|
|
4500
|
-
page = await browserContext.newPage();
|
|
4501
|
-
page.on('crash', ()=>onPageDead(`Browser page crashed while running ${file.testPath}.`));
|
|
4502
|
-
page.on('close', ()=>onPageDead(`Browser page closed unexpectedly while running ${file.testPath}.`));
|
|
4503
|
-
const session = sessionRegistry.register({
|
|
4504
|
-
testFile: file.testPath,
|
|
4505
|
-
projectName: file.projectName,
|
|
4506
|
-
runToken: run.token,
|
|
4507
|
-
mode: 'headless-page',
|
|
4508
|
-
context: browserContext,
|
|
4509
|
-
page
|
|
4510
|
-
});
|
|
4511
|
-
sessionId = session.id;
|
|
4512
|
-
await attachHeadlessRunnerTransport(page, {
|
|
4513
|
-
onDispatchMessage: async (message)=>{
|
|
4514
|
-
try {
|
|
4515
|
-
await dispatchRunnerMessage(run, file, session.id, message);
|
|
4516
|
-
if ('file-complete' === message.type || 'complete' === message.type) markDone();
|
|
4517
|
-
else if ('fatal' === message.type) {
|
|
4518
|
-
markDone();
|
|
4519
|
-
await cancelRun(run, false);
|
|
4520
|
-
}
|
|
4521
|
-
} catch (error) {
|
|
4522
|
-
const formatted = toError(error);
|
|
4523
|
-
await handleFatal({
|
|
4524
|
-
message: formatted.message,
|
|
4525
|
-
stack: formatted.stack
|
|
4526
|
-
});
|
|
4527
|
-
markDone();
|
|
4528
|
-
await cancelRun(run, false);
|
|
4529
|
-
}
|
|
4530
|
-
},
|
|
4531
|
-
onDispatchRpc: async (request)=>dispatchRouter.dispatch({
|
|
4532
|
-
...request,
|
|
4533
|
-
runToken: run.token,
|
|
4534
|
-
target: {
|
|
4535
|
-
sessionId: session.id,
|
|
4536
|
-
testFile: file.testPath,
|
|
4537
|
-
projectName: file.projectName,
|
|
4538
|
-
...request.target
|
|
4539
|
-
}
|
|
4540
|
-
})
|
|
4541
|
-
});
|
|
4542
|
-
const inlineOptions = {
|
|
4543
|
-
...hostOptions,
|
|
4544
|
-
snapshot: {
|
|
4545
|
-
updateSnapshot: context.snapshotManager.options.updateSnapshot
|
|
4546
|
-
},
|
|
4547
|
-
testFile: file.testPath,
|
|
4548
|
-
runId: `${run.token}:${session.id}`
|
|
4549
|
-
};
|
|
4550
|
-
const serializedOptions = serializeForInlineScript(inlineOptions);
|
|
4551
|
-
await page.addInitScript(`window.__RSTEST_BROWSER_OPTIONS__ = ${serializedOptions};`);
|
|
4552
|
-
const projectServer = runtime.projectServers.get(file.projectName);
|
|
4553
|
-
if (!projectServer) throw new Error(`No browser dev server for project "${file.projectName}" (test file: ${file.testPath}).`);
|
|
4554
|
-
await page.goto(`http://localhost:${projectServer.port}/runner.html`, {
|
|
4555
|
-
waitUntil: 'load'
|
|
4556
|
-
});
|
|
4557
|
-
const state = await Promise.race([
|
|
4558
|
-
donePromise.then(()=>({
|
|
4559
|
-
type: 'done'
|
|
4560
|
-
})),
|
|
4561
|
-
crashDeferred.promise.then((reason)=>({
|
|
4562
|
-
type: 'crash',
|
|
4563
|
-
reason
|
|
4564
|
-
})),
|
|
4565
|
-
run.cancelSignal.then(()=>({
|
|
4566
|
-
type: 'cancelled'
|
|
4567
|
-
}))
|
|
4568
|
-
]);
|
|
4569
|
-
if ('cancelled' === state.type) return;
|
|
4570
|
-
if ('crash' === state.type && runLifecycle.isTokenActive(run.token) && !run.cancelled) {
|
|
4571
|
-
await handleFatal({
|
|
4572
|
-
message: state.reason
|
|
4573
|
-
});
|
|
4574
|
-
await cancelRun(run, false);
|
|
4575
|
-
}
|
|
4576
|
-
} catch (error) {
|
|
4577
|
-
if (runLifecycle.isTokenActive(run.token) && !run.cancelled) {
|
|
4578
|
-
const formatted = toError(error);
|
|
4579
|
-
await handleFatal({
|
|
4580
|
-
message: formatted.message,
|
|
4581
|
-
stack: formatted.stack
|
|
4582
|
-
});
|
|
4583
|
-
await cancelRun(run, false);
|
|
4584
|
-
}
|
|
4585
|
-
} finally{
|
|
4586
|
-
if (page) try {
|
|
4587
|
-
await page.close();
|
|
4588
|
-
} catch {}
|
|
4589
|
-
if (sessionId) sessionRegistry.deleteById(sessionId);
|
|
4590
|
-
run.contexts.delete(browserContext);
|
|
4591
|
-
await closeContextSafely(browserContext);
|
|
4592
|
-
}
|
|
4593
|
-
};
|
|
4594
|
-
const makeSkippedFileResult = (file)=>({
|
|
4595
|
-
testId: getFileTaskId(file.testPath),
|
|
4596
|
-
status: 'skip',
|
|
4597
|
-
name: '',
|
|
4598
|
-
testPath: file.testPath,
|
|
4599
|
-
project: file.projectName,
|
|
4600
|
-
results: []
|
|
4601
|
-
});
|
|
4602
|
-
const runFilesWithPool = async (files)=>{
|
|
4603
|
-
if (0 === files.length) return;
|
|
4604
|
-
const previous = runLifecycle.activeSession;
|
|
4605
|
-
if (previous) await cancelRun(previous);
|
|
4606
|
-
const run = runLifecycle.createSession((token)=>({
|
|
4607
|
-
...createRunSession(token),
|
|
4608
|
-
contexts: new Set()
|
|
4609
|
-
}));
|
|
4610
|
-
const queue = [
|
|
4611
|
-
...files
|
|
4612
|
-
];
|
|
4613
|
-
const concurrency = getHeadlessConcurrency(context, queue.length);
|
|
4614
|
-
const bail = context.normalizedConfig.bail;
|
|
4615
|
-
const worker = async ()=>{
|
|
4616
|
-
while(queue.length > 0 && !run.cancelled && runLifecycle.isTokenActive(run.token)){
|
|
4617
|
-
if (bail && context.stateManager.getCountOfFailedTests() >= bail) {
|
|
4618
|
-
let skipped = queue.shift();
|
|
4619
|
-
while(skipped){
|
|
4620
|
-
await handleTestFileComplete(makeSkippedFileResult(skipped));
|
|
4621
|
-
skipped = queue.shift();
|
|
4622
|
-
}
|
|
4623
|
-
return;
|
|
4624
|
-
}
|
|
4625
|
-
const next = queue.shift();
|
|
4626
|
-
if (!next) return;
|
|
4627
|
-
await runSingleFile(run, next);
|
|
4628
|
-
}
|
|
4629
|
-
};
|
|
4630
|
-
run.done = Promise.all(Array.from({
|
|
4631
|
-
length: Math.min(queue.length, Math.max(concurrency, 1))
|
|
4632
|
-
}, ()=>worker())).then(()=>{});
|
|
4633
|
-
await run.done;
|
|
4634
|
-
runLifecycle.clearIfActive(run);
|
|
4635
|
-
};
|
|
4636
|
-
const latestRerunScheduler = createHeadlessLatestRerunScheduler({
|
|
4637
|
-
getActiveRun: ()=>runLifecycle.activeSession,
|
|
4638
|
-
isRunCancelled: (run)=>run.cancelled,
|
|
4639
|
-
invalidateActiveRun: ()=>{
|
|
4640
|
-
runLifecycle.invalidateActiveToken();
|
|
4641
|
-
},
|
|
4642
|
-
interruptActiveRun: async (run)=>{
|
|
4643
|
-
await cancelRun(run, false);
|
|
4644
|
-
},
|
|
4645
|
-
runFiles: async (files)=>{
|
|
4646
|
-
prepareWatchRerunState(context);
|
|
4647
|
-
await notifyTestRunStart();
|
|
4648
|
-
const rerunStartTime = Date.now();
|
|
4649
|
-
const fatalErrorBeforeRun = fatalError;
|
|
4650
|
-
let rerunError;
|
|
4651
|
-
try {
|
|
4652
|
-
await runFilesWithPool(files);
|
|
4653
|
-
} catch (error) {
|
|
4654
|
-
rerunError = toError(error);
|
|
4655
|
-
throw error;
|
|
4656
|
-
} finally{
|
|
4657
|
-
const testTime = Math.max(0, Date.now() - rerunStartTime);
|
|
4658
|
-
const rerunFatalError = fatalError && fatalError !== fatalErrorBeforeRun ? fatalError : void 0;
|
|
4659
|
-
await finalizeWatchRerun({
|
|
4660
|
-
rerunTestPaths: files.map((file)=>file.testPath),
|
|
4661
|
-
testTime,
|
|
4662
|
-
unhandledErrors: rerunError ? [
|
|
4663
|
-
rerunError
|
|
4664
|
-
] : rerunFatalError ? [
|
|
4665
|
-
rerunFatalError
|
|
4666
|
-
] : void 0
|
|
4667
|
-
});
|
|
4668
|
-
logWatchReadyMessage(context, enableCliShortcuts);
|
|
4669
|
-
}
|
|
4670
|
-
},
|
|
4671
|
-
onError: async (error)=>{
|
|
4672
|
-
const formatted = toError(error);
|
|
4673
|
-
await handleFatal({
|
|
4674
|
-
message: formatted.message,
|
|
4675
|
-
stack: formatted.stack
|
|
4676
|
-
});
|
|
4677
|
-
},
|
|
4678
|
-
onInterrupt: (run)=>{
|
|
4679
|
-
logger.debug(`[Headless] Interrupting active run token ${run.token} before scheduling latest rerun`);
|
|
4680
|
-
}
|
|
4681
|
-
});
|
|
4682
|
-
awaitHeadlessRerunIdle = ()=>latestRerunScheduler.whenIdle();
|
|
4683
|
-
const testStart = Date.now();
|
|
4684
|
-
await runFilesWithPool(allTestFiles);
|
|
4685
|
-
const testTime = Date.now() - testStart;
|
|
4686
|
-
if (isWatchMode) triggerRerun = async ()=>{
|
|
4687
|
-
const newProjectEntries = await collectProjectEntries(context);
|
|
4688
|
-
const rerunPlan = planWatchRerun({
|
|
4689
|
-
projectEntries: newProjectEntries,
|
|
4690
|
-
previousTestFiles: watchState.lastTestFiles,
|
|
4691
|
-
affectedTestFiles: drainPendingAffectedTestFiles(watchState)
|
|
4692
|
-
});
|
|
4693
|
-
if (rerunPlan.filesChanged) {
|
|
4694
|
-
const deletedTestPaths = collectDeletedTestPaths(watchState.lastTestFiles, rerunPlan.currentTestFiles);
|
|
4695
|
-
if (deletedTestPaths.length > 0) context.updateReporterResultState([], [], deletedTestPaths);
|
|
4696
|
-
watchState.lastTestFiles = rerunPlan.currentTestFiles;
|
|
4697
|
-
if (0 === rerunPlan.currentTestFiles.length) {
|
|
4698
|
-
await latestRerunScheduler.enqueueLatest([]);
|
|
4699
|
-
logger.log(color.cyan('No browser test files remain after update.\n'));
|
|
4700
|
-
logWatchReadyMessage(context, enableCliShortcuts);
|
|
4701
|
-
return;
|
|
4702
|
-
}
|
|
4703
|
-
logger.log(color.cyan(`Test file set changed, re-running ${rerunPlan.currentTestFiles.length} file(s)...\n`));
|
|
4704
|
-
await latestRerunScheduler.enqueueLatest(rerunPlan.currentTestFiles);
|
|
4705
|
-
return;
|
|
4706
|
-
}
|
|
4707
|
-
if (0 === rerunPlan.affectedTestFiles.length) {
|
|
4708
|
-
logger.log(color.cyan('No affected browser test files detected, skipping re-run.\n'));
|
|
4709
|
-
logWatchReadyMessage(context, enableCliShortcuts);
|
|
4710
|
-
return;
|
|
4711
|
-
}
|
|
4712
|
-
logger.log(color.cyan(`Re-running ${rerunPlan.affectedTestFiles.length} affected test file(s)...\n`));
|
|
4713
|
-
await latestRerunScheduler.enqueueLatest(rerunPlan.affectedTestFiles);
|
|
4714
|
-
};
|
|
4715
|
-
const closeHeadlessRuntime = isWatchMode ? void 0 : async ()=>{
|
|
4716
|
-
sessionRegistry.clear();
|
|
4717
|
-
await destroyBrowserRuntime(runtime);
|
|
4718
|
-
};
|
|
4719
|
-
if (fatalError) return failWithError(fatalError, closeHeadlessRuntime);
|
|
4720
|
-
const duration = {
|
|
4721
|
-
totalTime: buildTime + testTime,
|
|
4722
|
-
buildTime,
|
|
4723
|
-
testTime
|
|
4724
|
-
};
|
|
4725
|
-
context.updateReporterResultState(reporterResults, caseResults);
|
|
4726
|
-
const isFailure = reporterResults.some((result)=>'fail' === result.status);
|
|
4727
|
-
if (isWatchMode && isFailure) ensureProcessExitCode(1);
|
|
4728
|
-
const cycleCoverageMap = isWatchMode && coverageProvider ? buildBrowserCoverageMap(reporterResults, coverageProvider) : void 0;
|
|
4729
|
-
const result = {
|
|
4730
|
-
results: reporterResults,
|
|
4731
|
-
testResults: caseResults,
|
|
4732
|
-
duration,
|
|
4733
|
-
hasFailure: isFailure,
|
|
4734
|
-
getSourcemap: getBrowserSourcemap,
|
|
4735
|
-
resolveSourcemap: resolveBrowserSourcemap,
|
|
4736
|
-
close: closeHeadlessRuntime,
|
|
4737
|
-
coverage: cycleCoverageMap,
|
|
4738
|
-
watch: watchHandles
|
|
4739
|
-
};
|
|
4740
|
-
if (isWatchMode) try {
|
|
4741
|
-
await notifyTestRunEnd({
|
|
4742
|
-
duration,
|
|
4743
|
-
coverage: cycleCoverageMap?.files().length ? cycleCoverageMap.toJSON() : void 0
|
|
4744
|
-
});
|
|
4745
|
-
} finally{
|
|
4746
|
-
await closeHeadlessRuntime?.();
|
|
4747
|
-
}
|
|
4748
|
-
if (isWatchMode && triggerRerun) {
|
|
4749
|
-
watchState.hooksEnabled = true;
|
|
4750
|
-
logWatchReadyMessage(context, enableCliShortcuts);
|
|
4751
|
-
}
|
|
4752
|
-
return result;
|
|
4753
|
-
}
|
|
4754
|
-
let currentTestFiles = allTestFiles;
|
|
4755
|
-
const RUNNER_FRAMES_READY_TIMEOUT_MS = 30000;
|
|
4756
|
-
let currentRunnerFramesSignature = null;
|
|
4757
|
-
const runnerFramesWaiters = new Map();
|
|
4758
|
-
const createTestFilesSignature = (testFiles)=>JSON.stringify(testFiles.map((testFile)=>normalize(testFile)));
|
|
4759
|
-
const markRunnerFramesReady = (testFiles)=>{
|
|
4760
|
-
const signature = createTestFilesSignature(testFiles);
|
|
4761
|
-
currentRunnerFramesSignature = signature;
|
|
4762
|
-
const waiters = runnerFramesWaiters.get(signature);
|
|
4763
|
-
if (!waiters) return;
|
|
4764
|
-
runnerFramesWaiters.delete(signature);
|
|
4765
|
-
for (const waiter of waiters)waiter();
|
|
4766
|
-
};
|
|
4767
|
-
const waitForRunnerFramesReady = async (testFiles)=>{
|
|
4768
|
-
const signature = createTestFilesSignature(testFiles);
|
|
4769
|
-
if (currentRunnerFramesSignature === signature) return;
|
|
4770
|
-
await new Promise((resolve, reject)=>{
|
|
4771
|
-
const waiters = runnerFramesWaiters.get(signature) ?? new Set();
|
|
4772
|
-
const cleanup = ()=>{
|
|
4773
|
-
const currentWaiters = runnerFramesWaiters.get(signature);
|
|
4774
|
-
if (!currentWaiters) return;
|
|
4775
|
-
currentWaiters.delete(onReady);
|
|
4776
|
-
if (0 === currentWaiters.size) runnerFramesWaiters.delete(signature);
|
|
4777
|
-
};
|
|
4778
|
-
const onReady = ()=>{
|
|
4779
|
-
if (timeoutId) clearTimeout(timeoutId);
|
|
4780
|
-
cleanup();
|
|
4781
|
-
resolve();
|
|
4782
|
-
};
|
|
4783
|
-
const timeoutId = setTimeout(()=>{
|
|
4784
|
-
cleanup();
|
|
4785
|
-
reject(new Error(`Timed out waiting for headed runner frames to be ready for ${testFiles.length} file(s).`));
|
|
4786
|
-
}, RUNNER_FRAMES_READY_TIMEOUT_MS);
|
|
4787
|
-
waiters.add(onReady);
|
|
4788
|
-
runnerFramesWaiters.set(signature, waiters);
|
|
4789
|
-
if (currentRunnerFramesSignature === signature) onReady();
|
|
4790
|
-
});
|
|
4791
|
-
};
|
|
4792
|
-
const getTestFileInfo = (testFile)=>{
|
|
4793
|
-
const normalizedTestFile = normalize(testFile);
|
|
4794
|
-
const fileInfo = currentTestFiles.find((file)=>file.testPath === normalizedTestFile);
|
|
4795
|
-
if (!fileInfo) throw new Error(`Unknown browser test file: ${JSON.stringify(testFile)}`);
|
|
4796
|
-
return fileInfo;
|
|
4797
|
-
};
|
|
4798
|
-
let containerContext;
|
|
4799
|
-
let containerPage;
|
|
4800
|
-
let isNewPage = false;
|
|
4801
|
-
if (isWatchMode && runtime.containerPage && runtime.containerContext) {
|
|
4802
|
-
containerContext = runtime.containerContext;
|
|
4803
|
-
containerPage = runtime.containerPage;
|
|
4804
|
-
logger.log(color.gray('\n[Watch] Reusing existing container page\n'));
|
|
4805
|
-
} else {
|
|
4806
|
-
isNewPage = true;
|
|
4807
|
-
containerContext = await browser.newContext({
|
|
4808
|
-
providerOptions: browserLaunchOptions.providerOptions,
|
|
4809
|
-
viewport: null
|
|
4810
|
-
});
|
|
4811
|
-
containerPage = await containerContext.newPage();
|
|
4812
|
-
containerPage.on('popup', async (popup)=>{
|
|
4813
|
-
await popup.close().catch(()=>{});
|
|
4814
|
-
});
|
|
4815
|
-
containerContext.on('page', async (page)=>{
|
|
4816
|
-
if (page !== containerPage) await page.close().catch(()=>{});
|
|
4817
|
-
});
|
|
4818
|
-
if (isWatchMode) {
|
|
4819
|
-
runtime.containerPage = containerPage;
|
|
4820
|
-
runtime.containerContext = containerContext;
|
|
4821
|
-
}
|
|
4822
|
-
containerPage.on('console', (msg)=>{
|
|
4823
|
-
const text = msg.text();
|
|
4824
|
-
if (text.startsWith('[Container]') || text.startsWith('[Runner]')) logger.log(color.gray(`[Browser Console] ${text}`));
|
|
4825
|
-
});
|
|
4826
|
-
}
|
|
4827
|
-
activeContainerPage = containerPage;
|
|
4828
|
-
const dispatchRouter = createDispatchRouter();
|
|
4829
|
-
const headedReloadQueue = createHeadedSerialTaskQueue();
|
|
4830
|
-
const pendingHeadedReloads = new Map();
|
|
4831
|
-
let enqueueHeadedReload = async (_file, _testNamePattern)=>{
|
|
4832
|
-
throw new Error('Headed reload queue is not initialized');
|
|
4833
|
-
};
|
|
4834
|
-
const rejectPendingHeadedReload = (testPath, error, runId)=>{
|
|
4835
|
-
const pending = pendingHeadedReloads.get(testPath);
|
|
4836
|
-
if (!pending) return;
|
|
4837
|
-
if (runId && pending.runId !== runId) return;
|
|
4838
|
-
pendingHeadedReloads.delete(testPath);
|
|
4839
|
-
pending.deferred.reject(error);
|
|
4840
|
-
};
|
|
4841
|
-
const rejectAllPendingHeadedReloads = (error)=>{
|
|
4842
|
-
for (const [testPath, pending] of pendingHeadedReloads){
|
|
4843
|
-
pendingHeadedReloads.delete(testPath);
|
|
4844
|
-
pending.deferred.reject(error);
|
|
4845
|
-
}
|
|
4846
|
-
};
|
|
4847
|
-
const registerPendingHeadedReload = (testPath, runId)=>{
|
|
4848
|
-
const previousPending = pendingHeadedReloads.get(testPath);
|
|
4849
|
-
if (previousPending) {
|
|
4850
|
-
previousPending.deferred.reject(new Error(`Reload for "${testPath}" was superseded by a newer request.`));
|
|
4851
|
-
pendingHeadedReloads.delete(testPath);
|
|
4852
|
-
}
|
|
4853
|
-
const deferred = createDeferredPromise();
|
|
4854
|
-
pendingHeadedReloads.set(testPath, {
|
|
4855
|
-
runId,
|
|
4856
|
-
deferred
|
|
4857
|
-
});
|
|
4858
|
-
return deferred.promise;
|
|
5463
|
+
const schedulerDeps = {
|
|
5464
|
+
context,
|
|
5465
|
+
allTestFiles,
|
|
5466
|
+
hostOptions,
|
|
5467
|
+
projectRoots: new Map(projectRuntimeConfigs.map((project)=>[
|
|
5468
|
+
project.name,
|
|
5469
|
+
project.projectRoot
|
|
5470
|
+
])),
|
|
5471
|
+
isWatchMode,
|
|
5472
|
+
createDispatchRouter,
|
|
5473
|
+
fatalErrorRef,
|
|
5474
|
+
watchSignals,
|
|
5475
|
+
setDispatchPageResolver,
|
|
5476
|
+
createWatchSession,
|
|
5477
|
+
collectProjectEntries: ()=>browserRsbuild_collectProjectEntries(context),
|
|
5478
|
+
logWatchReady: ()=>logWatchReadyMessage(context, enableCliShortcuts),
|
|
5479
|
+
destroyRuntime: ()=>destroyBrowserRuntime(runtime)
|
|
4859
5480
|
};
|
|
4860
|
-
const
|
|
4861
|
-
|
|
4862
|
-
|
|
4863
|
-
|
|
4864
|
-
|
|
4865
|
-
|
|
4866
|
-
|
|
4867
|
-
|
|
4868
|
-
|
|
4869
|
-
|
|
4870
|
-
|
|
4871
|
-
await registerPendingHeadedReload(file.testPath, reloadAck.runId);
|
|
4872
|
-
} catch (error) {
|
|
4873
|
-
if (reloadAck?.runId) rejectPendingHeadedReload(file.testPath, toError(error), reloadAck.runId);
|
|
4874
|
-
throw error;
|
|
5481
|
+
const { testTime, rawCoverage, watchSession, close } = useHeadlessDirect ? await createHeadlessScheduler({
|
|
5482
|
+
...schedulerDeps,
|
|
5483
|
+
browser,
|
|
5484
|
+
browserLaunchOptions,
|
|
5485
|
+
projectServers: runtime.projectServers,
|
|
5486
|
+
v8Coverage,
|
|
5487
|
+
projectRuntimeConfigs,
|
|
5488
|
+
watchState,
|
|
5489
|
+
handlers: {
|
|
5490
|
+
handleFatal,
|
|
5491
|
+
handleTestFileComplete
|
|
4875
5492
|
}
|
|
4876
|
-
}
|
|
4877
|
-
|
|
4878
|
-
|
|
4879
|
-
|
|
4880
|
-
|
|
4881
|
-
|
|
4882
|
-
logger.log(color.cyan(`\nRe-running test: ${displayPath}${testNamePattern ? ` (pattern: ${testNamePattern})` : ''}\n`));
|
|
4883
|
-
await enqueueHeadedReload(getTestFileInfo(testFile), testNamePattern);
|
|
4884
|
-
},
|
|
4885
|
-
async getTestFiles () {
|
|
4886
|
-
return currentTestFiles;
|
|
4887
|
-
},
|
|
4888
|
-
async onRunnerFramesReady (testFiles) {
|
|
4889
|
-
markRunnerFramesReady(testFiles);
|
|
4890
|
-
},
|
|
4891
|
-
async onTestFileStart (payload) {
|
|
4892
|
-
await handleTestFileStart(payload);
|
|
4893
|
-
},
|
|
4894
|
-
async onTestCaseResult (payload) {
|
|
4895
|
-
await handleTestCaseResult(payload);
|
|
4896
|
-
},
|
|
4897
|
-
async onTestFileComplete (payload) {
|
|
4898
|
-
try {
|
|
4899
|
-
await handleTestFileComplete(payload);
|
|
4900
|
-
resolvePendingHeadedReload(payload.testPath, payload.runId);
|
|
4901
|
-
} catch (error) {
|
|
4902
|
-
rejectPendingHeadedReload(payload.testPath, toError(error), payload.runId);
|
|
4903
|
-
throw error;
|
|
4904
|
-
}
|
|
4905
|
-
},
|
|
4906
|
-
async onLog (payload) {
|
|
4907
|
-
await handleLog(payload);
|
|
4908
|
-
},
|
|
4909
|
-
async onFatal (payload) {
|
|
4910
|
-
const error = new Error(payload.message);
|
|
4911
|
-
error.stack = payload.stack;
|
|
4912
|
-
rejectAllPendingHeadedReloads(error);
|
|
4913
|
-
await handleFatal(payload);
|
|
4914
|
-
},
|
|
4915
|
-
async dispatch (request) {
|
|
4916
|
-
return dispatchRouter.dispatch(request);
|
|
4917
|
-
}
|
|
4918
|
-
});
|
|
4919
|
-
let rpcManager;
|
|
4920
|
-
if (isWatchMode && runtime.rpcManager) {
|
|
4921
|
-
rpcManager = runtime.rpcManager;
|
|
4922
|
-
rpcManager.updateMethods(createRpcMethods(), rejectAllPendingHeadedReloads);
|
|
4923
|
-
const existingWs = rpcManager.currentWebSocket;
|
|
4924
|
-
if (existingWs) rpcManager.reattach(existingWs);
|
|
4925
|
-
} else {
|
|
4926
|
-
rpcManager = new ContainerRpcManager(wss, createRpcMethods(), rejectAllPendingHeadedReloads);
|
|
4927
|
-
if (isWatchMode) runtime.rpcManager = rpcManager;
|
|
4928
|
-
}
|
|
4929
|
-
if (isNewPage) {
|
|
4930
|
-
const pagePath = '/';
|
|
4931
|
-
const containerPort = runtime.containerServer.port;
|
|
4932
|
-
await containerPage.goto(`http://localhost:${containerPort}${pagePath}`, {
|
|
4933
|
-
waitUntil: 'load'
|
|
4934
|
-
});
|
|
4935
|
-
logger.log(color.cyan(`\nBrowser mode opened at http://localhost:${containerPort}${pagePath}\n`));
|
|
4936
|
-
}
|
|
4937
|
-
enqueueHeadedReload = async (file, testNamePattern)=>headedReloadQueue.enqueue(async ()=>{
|
|
4938
|
-
if (fatalError) return;
|
|
4939
|
-
await reloadTestFileAndWait(file, testNamePattern);
|
|
4940
|
-
});
|
|
4941
|
-
let testTime = 0;
|
|
4942
|
-
if (currentTestFiles.length > 0) {
|
|
4943
|
-
const testStart = Date.now();
|
|
4944
|
-
try {
|
|
4945
|
-
await waitForRunnerFramesReady(currentTestFiles.map((file)=>file.testPath));
|
|
4946
|
-
for (const file of currentTestFiles){
|
|
4947
|
-
await enqueueHeadedReload(file);
|
|
4948
|
-
if (fatalError) break;
|
|
4949
|
-
}
|
|
4950
|
-
} catch (error) {
|
|
4951
|
-
fatalError = fatalError ?? toError(error);
|
|
4952
|
-
if (isWatchMode) ensureProcessExitCode(1);
|
|
5493
|
+
}) : await createHeadedScheduler({
|
|
5494
|
+
...schedulerDeps,
|
|
5495
|
+
runtime,
|
|
5496
|
+
v8Coverage,
|
|
5497
|
+
handlers: {
|
|
5498
|
+
handleTestFileComplete
|
|
4953
5499
|
}
|
|
4954
|
-
|
|
4955
|
-
|
|
4956
|
-
if (
|
|
4957
|
-
const
|
|
4958
|
-
|
|
4959
|
-
|
|
4960
|
-
|
|
4961
|
-
|
|
5500
|
+
});
|
|
5501
|
+
watchState.hooksEnabled = void 0 !== watchSession;
|
|
5502
|
+
if (fatalErrorRef.current) {
|
|
5503
|
+
const errorResult = await failWithError(fatalErrorRef.current, close);
|
|
5504
|
+
return {
|
|
5505
|
+
...errorResult,
|
|
5506
|
+
rawCoverage,
|
|
5507
|
+
loadAssetFiles: loadBrowserCoverageAssetFiles,
|
|
5508
|
+
loadSourceMaps: loadBrowserCoverageSourceMaps,
|
|
5509
|
+
watchSession
|
|
4962
5510
|
};
|
|
4963
|
-
|
|
4964
|
-
const [, newProjectEntries] = await Promise.all([
|
|
4965
|
-
rpcManager.updateHostConfig(refreshedHostOptions),
|
|
4966
|
-
collectProjectEntries(context)
|
|
4967
|
-
]);
|
|
4968
|
-
const rerunPlan = planWatchRerun({
|
|
4969
|
-
projectEntries: newProjectEntries,
|
|
4970
|
-
previousTestFiles: watchState.lastTestFiles,
|
|
4971
|
-
affectedTestFiles: drainPendingAffectedTestFiles(watchState)
|
|
4972
|
-
});
|
|
4973
|
-
if (rerunPlan.filesChanged) {
|
|
4974
|
-
const deletedTestPaths = collectDeletedTestPaths(watchState.lastTestFiles, rerunPlan.currentTestFiles);
|
|
4975
|
-
if (deletedTestPaths.length > 0) context.updateReporterResultState([], [], deletedTestPaths);
|
|
4976
|
-
watchState.lastTestFiles = rerunPlan.currentTestFiles;
|
|
4977
|
-
currentTestFiles = rerunPlan.currentTestFiles;
|
|
4978
|
-
await rpcManager.notifyTestFileUpdate(currentTestFiles);
|
|
4979
|
-
if (0 === currentTestFiles.length) {
|
|
4980
|
-
logger.log(color.cyan('No browser test files remain after update.\n'));
|
|
4981
|
-
logWatchReadyMessage(context, enableCliShortcuts);
|
|
4982
|
-
return;
|
|
4983
|
-
}
|
|
4984
|
-
await waitForRunnerFramesReady(currentTestFiles.map((file)=>file.testPath));
|
|
4985
|
-
}
|
|
4986
|
-
if (rerunPlan.normalizedAffectedTestFiles.length > 0) {
|
|
4987
|
-
logger.log(color.cyan(`Re-running ${rerunPlan.normalizedAffectedTestFiles.length} affected test file(s)...\n`));
|
|
4988
|
-
prepareWatchRerunState(context);
|
|
4989
|
-
await notifyTestRunStart();
|
|
4990
|
-
const rerunStartTime = Date.now();
|
|
4991
|
-
const fatalErrorBeforeRun = fatalError;
|
|
4992
|
-
let rerunError;
|
|
4993
|
-
try {
|
|
4994
|
-
for (const testFile of rerunPlan.normalizedAffectedTestFiles)await enqueueHeadedReload(getTestFileInfo(testFile));
|
|
4995
|
-
} catch (error) {
|
|
4996
|
-
rerunError = toError(error);
|
|
4997
|
-
throw error;
|
|
4998
|
-
} finally{
|
|
4999
|
-
const testTime = Math.max(0, Date.now() - rerunStartTime);
|
|
5000
|
-
const rerunFatalError = fatalError && fatalError !== fatalErrorBeforeRun ? fatalError : void 0;
|
|
5001
|
-
await finalizeWatchRerun({
|
|
5002
|
-
rerunTestPaths: rerunPlan.normalizedAffectedTestFiles,
|
|
5003
|
-
testTime,
|
|
5004
|
-
unhandledErrors: rerunError ? [
|
|
5005
|
-
rerunError
|
|
5006
|
-
] : rerunFatalError ? [
|
|
5007
|
-
rerunFatalError
|
|
5008
|
-
] : void 0
|
|
5009
|
-
});
|
|
5010
|
-
logWatchReadyMessage(context, enableCliShortcuts);
|
|
5011
|
-
}
|
|
5012
|
-
} else if (rerunPlan.filesChanged) logWatchReadyMessage(context, enableCliShortcuts);
|
|
5013
|
-
else {
|
|
5014
|
-
logger.log(color.cyan('Tests will be re-executed automatically\n'));
|
|
5015
|
-
logWatchReadyMessage(context, enableCliShortcuts);
|
|
5016
|
-
}
|
|
5017
|
-
};
|
|
5018
|
-
const closeContainerRuntime = isWatchMode ? void 0 : async ()=>{
|
|
5019
|
-
try {
|
|
5020
|
-
await containerPage.close();
|
|
5021
|
-
} catch {}
|
|
5022
|
-
try {
|
|
5023
|
-
await containerContext.close();
|
|
5024
|
-
} catch {}
|
|
5025
|
-
await destroyBrowserRuntime(runtime);
|
|
5026
|
-
};
|
|
5027
|
-
if (fatalError) return failWithError(fatalError, closeContainerRuntime);
|
|
5028
|
-
const duration = {
|
|
5029
|
-
totalTime: buildTime + testTime,
|
|
5030
|
-
buildTime,
|
|
5031
|
-
testTime
|
|
5032
|
-
};
|
|
5511
|
+
}
|
|
5033
5512
|
context.updateReporterResultState(reporterResults, caseResults);
|
|
5034
|
-
|
|
5035
|
-
if (isWatchMode && isFailure) ensureProcessExitCode(1);
|
|
5036
|
-
const cycleCoverageMap = isWatchMode && coverageProvider ? buildBrowserCoverageMap(reporterResults, coverageProvider) : void 0;
|
|
5037
|
-
const result = {
|
|
5513
|
+
return {
|
|
5038
5514
|
results: reporterResults,
|
|
5039
5515
|
testResults: caseResults,
|
|
5040
|
-
duration
|
|
5041
|
-
|
|
5516
|
+
duration: {
|
|
5517
|
+
totalTime: buildTime + testTime,
|
|
5518
|
+
buildTime,
|
|
5519
|
+
testTime
|
|
5520
|
+
},
|
|
5521
|
+
hasFailure: reporterResults.some((result)=>'fail' === result.status),
|
|
5522
|
+
rawCoverage,
|
|
5523
|
+
loadAssetFiles: loadBrowserCoverageAssetFiles,
|
|
5524
|
+
loadSourceMaps: loadBrowserCoverageSourceMaps,
|
|
5042
5525
|
getSourcemap: getBrowserSourcemap,
|
|
5043
5526
|
resolveSourcemap: resolveBrowserSourcemap,
|
|
5044
|
-
close
|
|
5045
|
-
|
|
5046
|
-
watch: watchHandles
|
|
5527
|
+
close,
|
|
5528
|
+
watchSession
|
|
5047
5529
|
};
|
|
5048
|
-
if (isWatchMode) try {
|
|
5049
|
-
await notifyTestRunEnd({
|
|
5050
|
-
duration,
|
|
5051
|
-
coverage: cycleCoverageMap?.files().length ? cycleCoverageMap.toJSON() : void 0
|
|
5052
|
-
});
|
|
5053
|
-
} finally{
|
|
5054
|
-
await closeContainerRuntime?.();
|
|
5055
|
-
}
|
|
5056
|
-
if (isWatchMode && triggerRerun) {
|
|
5057
|
-
watchState.hooksEnabled = true;
|
|
5058
|
-
logWatchReadyMessage(context, enableCliShortcuts);
|
|
5059
|
-
}
|
|
5060
|
-
return result;
|
|
5061
5530
|
};
|
|
5062
5531
|
const listBrowserTests = async (context, options)=>{
|
|
5063
5532
|
const browserProjects = options?.projects ?? getBrowserProjects(context);
|
|
@@ -5116,11 +5585,11 @@ const listBrowserTests = async (context, options)=>{
|
|
|
5116
5585
|
environmentName: project.environmentName,
|
|
5117
5586
|
projectRoot: normalize(project.rootPath),
|
|
5118
5587
|
runtimeConfig: serializableConfig(projectRuntimeConfig(project, {
|
|
5119
|
-
envMode: 'static'
|
|
5588
|
+
envMode: 'static',
|
|
5589
|
+
envOverlay: options?.env
|
|
5120
5590
|
})),
|
|
5121
5591
|
viewport: project.normalizedConfig.browser.viewport
|
|
5122
5592
|
}));
|
|
5123
|
-
const maxTestTimeoutForRpc = getMaxTestTimeoutForRpc(browserProjects);
|
|
5124
5593
|
const hostOptions = {
|
|
5125
5594
|
rootPath: normalize(context.rootPath),
|
|
5126
5595
|
projects: projectRuntimeConfigs,
|
|
@@ -5129,7 +5598,7 @@ const listBrowserTests = async (context, options)=>{
|
|
|
5129
5598
|
},
|
|
5130
5599
|
mode: 'collect',
|
|
5131
5600
|
debug: isDebug(),
|
|
5132
|
-
rpcTimeout:
|
|
5601
|
+
rpcTimeout: -1
|
|
5133
5602
|
};
|
|
5134
5603
|
runtime.setContainerOptions(hostOptions);
|
|
5135
5604
|
const browserContext = await browser.newContext({
|
|
@@ -5147,7 +5616,8 @@ const listBrowserTests = async (context, options)=>{
|
|
|
5147
5616
|
resolveCollect = resolve;
|
|
5148
5617
|
});
|
|
5149
5618
|
const page = await browserContext.newPage();
|
|
5150
|
-
await page.exposeFunction(DISPATCH_MESSAGE_TYPE, (
|
|
5619
|
+
await page.exposeFunction(DISPATCH_MESSAGE_TYPE, (envelope)=>{
|
|
5620
|
+
const message = envelope.message;
|
|
5151
5621
|
switch(message.type){
|
|
5152
5622
|
case 'collect-result':
|
|
5153
5623
|
{
|
|
@@ -5249,8 +5719,11 @@ const emptyOutcome = ()=>({
|
|
|
5249
5719
|
});
|
|
5250
5720
|
async function createBrowserExecutor(context, options) {
|
|
5251
5721
|
const { projects, coverageProvider, shardedEntries, freezeShardedEntries, filesOnly, allowEmptyRun, appliedModifyRstestConfigEnvironments } = options;
|
|
5722
|
+
const isWatchMode = 'watch' === context.command;
|
|
5252
5723
|
let deferredClose;
|
|
5253
5724
|
let inFlightCycle;
|
|
5725
|
+
let invalidationCallback;
|
|
5726
|
+
let watchSession;
|
|
5254
5727
|
const foldOutcome = (result)=>{
|
|
5255
5728
|
if (!result) return emptyOutcome();
|
|
5256
5729
|
const map = buildBrowserCoverageMap(result.results, coverageProvider);
|
|
@@ -5263,9 +5736,12 @@ async function createBrowserExecutor(context, options) {
|
|
|
5263
5736
|
buildTime: result.duration.buildTime,
|
|
5264
5737
|
testTime: result.duration.testTime
|
|
5265
5738
|
},
|
|
5266
|
-
coverage: {
|
|
5267
|
-
map: map?.toJSON()
|
|
5268
|
-
|
|
5739
|
+
coverage: map?.files().length || result.rawCoverage?.length ? {
|
|
5740
|
+
map: map?.toJSON(),
|
|
5741
|
+
raw: result.rawCoverage,
|
|
5742
|
+
loadAssetFiles: result.loadAssetFiles,
|
|
5743
|
+
loadSourceMaps: result.loadSourceMaps
|
|
5744
|
+
} : void 0,
|
|
5269
5745
|
resolveSourcemap: result.resolveSourcemap
|
|
5270
5746
|
};
|
|
5271
5747
|
};
|
|
@@ -5274,6 +5750,15 @@ async function createBrowserExecutor(context, options) {
|
|
|
5274
5750
|
projects,
|
|
5275
5751
|
async init () {},
|
|
5276
5752
|
async runCycle (opts) {
|
|
5753
|
+
if (watchSession) {
|
|
5754
|
+
const cycle = watchSession.runCycle(opts.fileFilters ?? []);
|
|
5755
|
+
inFlightCycle = cycle;
|
|
5756
|
+
try {
|
|
5757
|
+
return await cycle;
|
|
5758
|
+
} finally{
|
|
5759
|
+
inFlightCycle = void 0;
|
|
5760
|
+
}
|
|
5761
|
+
}
|
|
5277
5762
|
const cycle = runBrowserController(context, {
|
|
5278
5763
|
projects,
|
|
5279
5764
|
shardedEntries,
|
|
@@ -5282,24 +5767,37 @@ async function createBrowserExecutor(context, options) {
|
|
|
5282
5767
|
appliedModifyRstestConfigEnvironments,
|
|
5283
5768
|
onTraceEvents: opts.onTraceEvents,
|
|
5284
5769
|
env: opts.env,
|
|
5285
|
-
updateSnapshot: opts.updateSnapshot
|
|
5770
|
+
updateSnapshot: opts.updateSnapshot,
|
|
5771
|
+
onInvalidate: isWatchMode ? (hint)=>invalidationCallback?.(hint) : void 0
|
|
5286
5772
|
});
|
|
5287
5773
|
inFlightCycle = cycle;
|
|
5288
5774
|
try {
|
|
5289
5775
|
const result = await cycle;
|
|
5290
5776
|
deferredClose = result?.close;
|
|
5777
|
+
watchSession = result?.watchSession;
|
|
5291
5778
|
return foldOutcome(result);
|
|
5292
5779
|
} finally{
|
|
5293
5780
|
inFlightCycle = void 0;
|
|
5294
5781
|
}
|
|
5295
5782
|
},
|
|
5296
|
-
|
|
5783
|
+
onInvalidate (cb) {
|
|
5784
|
+
invalidationCallback = cb;
|
|
5785
|
+
},
|
|
5786
|
+
hasWatchSession () {
|
|
5787
|
+
return void 0 !== watchSession;
|
|
5788
|
+
},
|
|
5789
|
+
async requestRerun (testPaths) {
|
|
5790
|
+
if (!watchSession) return void logger.log(color.yellow('\nBrowser Mode has no live watch session, so this rerun skipped it.'));
|
|
5791
|
+
await watchSession.requestRerun(testPaths);
|
|
5792
|
+
},
|
|
5793
|
+
async collect (opts) {
|
|
5297
5794
|
const pending = listBrowserTests(context, {
|
|
5298
5795
|
projects,
|
|
5299
5796
|
shardedEntries,
|
|
5300
5797
|
freezeShardedEntries,
|
|
5301
5798
|
filesOnly,
|
|
5302
|
-
appliedModifyRstestConfigEnvironments
|
|
5799
|
+
appliedModifyRstestConfigEnvironments,
|
|
5800
|
+
env: opts.env
|
|
5303
5801
|
});
|
|
5304
5802
|
inFlightCycle = pending;
|
|
5305
5803
|
try {
|
|
@@ -5314,6 +5812,11 @@ async function createBrowserExecutor(context, options) {
|
|
|
5314
5812
|
},
|
|
5315
5813
|
async close () {
|
|
5316
5814
|
if (inFlightCycle) await inFlightCycle.catch(()=>void 0);
|
|
5815
|
+
if (isWatchMode) {
|
|
5816
|
+
watchSession = void 0;
|
|
5817
|
+
await cleanupWatchRuntime();
|
|
5818
|
+
return;
|
|
5819
|
+
}
|
|
5317
5820
|
const close = deferredClose;
|
|
5318
5821
|
deferredClose = void 0;
|
|
5319
5822
|
await close?.();
|