@rstest/browser 0.11.5 → 0.11.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/browser-container/container-static/js/683.4f821ce8f1.js +194 -0
- package/dist/browser-container/container-static/js/index.568aa7cb35.js +1 -0
- package/dist/browser-container/index.html +1 -1
- package/dist/browserExecutor.d.ts +7 -5
- package/dist/browserRsbuild.d.ts +119 -0
- package/dist/containerRpc.d.ts +52 -0
- package/dist/dispatchCapabilities.d.ts +1 -2
- package/dist/headedScheduler.d.ts +58 -0
- package/dist/headlessScheduler.d.ts +37 -0
- package/dist/hostController.d.ts +12 -47
- package/dist/hostPayloads.d.ts +30 -0
- package/dist/index.js +1714 -1721
- package/dist/schedulerSeam.d.ts +37 -0
- package/dist/watchRerunPlanner.d.ts +5 -0
- package/dist/watchRuntime.d.ts +21 -0
- package/dist/watchSignals.d.ts +22 -0
- package/package.json +5 -5
- package/src/browserExecutor.ts +90 -8
- package/src/browserRsbuild.ts +1927 -0
- package/src/containerRpc.ts +206 -0
- package/src/dispatchCapabilities.ts +1 -6
- package/src/headedScheduler.ts +664 -0
- package/src/headlessScheduler.ts +566 -0
- package/src/hostController.ts +857 -4125
- package/src/hostPayloads.ts +62 -0
- package/src/schedulerSeam.ts +49 -0
- package/src/watchRerunPlanner.ts +18 -7
- package/src/watchRuntime.ts +83 -0
- package/src/watchSignals.ts +93 -0
- 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/headlessLatestRerunScheduler.d.ts +0 -18
- package/src/headlessLatestRerunScheduler.ts +0 -76
package/dist/index.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
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, PhaseTracker, RSTEST_ENV_SYMBOL_KEY, applyWatchInvalidation, applyWebMockRspackConfig, browserIgnoredRuntimeConfigKeys, buildBrowserCoverageMap, color, createCoverageProvider, createRunnerEventSink, createSilentConsoleController, getNumCpus as browser_getNumCpus, getPrettyConsoleName, getSetupFiles, getTestEntries, hasUserRstestConfigPlugins, importMetaRstestDefine, 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, 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
10
|
import convert_source_map from "convert-source-map";
|
|
@@ -1654,218 +1654,6 @@ __webpack_require__.add({
|
|
|
1654
1654
|
};
|
|
1655
1655
|
}
|
|
1656
1656
|
});
|
|
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
1657
|
const toErrorMessage = (error)=>error instanceof Error ? error.message : String(error);
|
|
1870
1658
|
class HostDispatchRouter {
|
|
1871
1659
|
handlers = new Map();
|
|
@@ -2141,104 +1929,6 @@ const validateBrowserConfig = (context)=>{
|
|
|
2141
1929
|
}
|
|
2142
1930
|
reportUnsupportedBrowserOptions(context);
|
|
2143
1931
|
};
|
|
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
1932
|
const supportedLocatorActions = new Set([
|
|
2243
1933
|
'click',
|
|
2244
1934
|
'dblclick',
|
|
@@ -2686,159 +2376,9 @@ function getBrowserProviderImplementation(provider) {
|
|
|
2686
2376
|
if (!implementation) throw new Error(`Unsupported browser provider: ${String(provider)}`);
|
|
2687
2377
|
return implementation;
|
|
2688
2378
|
}
|
|
2689
|
-
const
|
|
2690
|
-
|
|
2691
|
-
|
|
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
|
-
}
|
|
2379
|
+
const collectDeletedTestPaths = (previous, current)=>{
|
|
2380
|
+
const currentPathSet = new Set(current.map((file)=>file.testPath));
|
|
2381
|
+
return previous.map((file)=>file.testPath).filter((testPath)=>!currentPathSet.has(testPath));
|
|
2842
2382
|
};
|
|
2843
2383
|
const serializeTestFiles = (files)=>JSON.stringify(files.map((f)=>`${f.projectName}:${f.testPath}`).sort());
|
|
2844
2384
|
const normalizeTestFiles = (files)=>files.map((file)=>({
|
|
@@ -2854,11 +2394,8 @@ const planWatchRerun = ({ projectEntries, previousTestFiles, affectedTestFiles }
|
|
|
2854
2394
|
const normalizedPrevious = normalizeTestFiles(previousTestFiles);
|
|
2855
2395
|
const filesChanged = serializeTestFiles(currentTestFiles) !== serializeTestFiles(normalizedPrevious);
|
|
2856
2396
|
const normalizedAffectedTestFiles = affectedTestFiles.map((testFile)=>normalize(testFile));
|
|
2857
|
-
const
|
|
2858
|
-
|
|
2859
|
-
file
|
|
2860
|
-
]));
|
|
2861
|
-
const matchedAffectedFiles = normalizedAffectedTestFiles.map((testFile)=>currentFileMap.get(testFile)).filter((file)=>Boolean(file));
|
|
2397
|
+
const affectedPathSet = new Set(normalizedAffectedTestFiles);
|
|
2398
|
+
const matchedAffectedFiles = currentTestFiles.filter((file)=>affectedPathSet.has(file.testPath));
|
|
2862
2399
|
return {
|
|
2863
2400
|
currentTestFiles,
|
|
2864
2401
|
filesChanged,
|
|
@@ -2869,162 +2406,49 @@ const planWatchRerun = ({ projectEntries, previousTestFiles, affectedTestFiles }
|
|
|
2869
2406
|
const picomatch = __webpack_require__("../../node_modules/.pnpm/picomatch@4.0.5/node_modules/picomatch/index.js");
|
|
2870
2407
|
var picomatch_default = /*#__PURE__*/ __webpack_require__.n(picomatch);
|
|
2871
2408
|
const { createRsbuild: createRsbuild, rspack: rspack } = rsbuild;
|
|
2872
|
-
const
|
|
2409
|
+
const browserRsbuild_dirname = dirname(fileURLToPath(import.meta.url));
|
|
2873
2410
|
const OPTIONS_PLACEHOLDER = '__RSTEST_OPTIONS_PLACEHOLDER__';
|
|
2874
|
-
let nextBrowserFilePid = 1000000000;
|
|
2875
2411
|
const serializeForInlineScript = (value)=>JSON.stringify(value).replace(/</g, '\\u003c').replace(/\u2028/g, '\\u2028').replace(/\u2029/g, '\\u2029');
|
|
2876
2412
|
const getBrowserProviderOptions = (project)=>{
|
|
2877
2413
|
const browserConfig = project.normalizedConfig.browser;
|
|
2878
2414
|
return browserConfig.providerOptions ?? {};
|
|
2879
2415
|
};
|
|
2880
|
-
const
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
2416
|
+
const createBrowserWatchState = ()=>({
|
|
2417
|
+
lastTestFiles: [],
|
|
2418
|
+
hooksEnabled: false,
|
|
2419
|
+
invalidation: new Map(),
|
|
2420
|
+
pendingAffectedTestFiles: new Map(),
|
|
2421
|
+
compileStartTimes: new Map(),
|
|
2422
|
+
pendingBuildTimeMs: 0
|
|
2887
2423
|
});
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
|
|
2424
|
+
const drainPendingBuildTime = (watchState)=>{
|
|
2425
|
+
const buildTime = watchState.pendingBuildTimeMs;
|
|
2426
|
+
watchState.pendingBuildTimeMs = 0;
|
|
2427
|
+
return buildTime;
|
|
2428
|
+
};
|
|
2429
|
+
const drainPendingAffectedTestFiles = (watchState)=>{
|
|
2430
|
+
const affected = new Set();
|
|
2431
|
+
for (const files of watchState.pendingAffectedTestFiles.values())for (const file of files)affected.add(file);
|
|
2432
|
+
watchState.pendingAffectedTestFiles.clear();
|
|
2433
|
+
return Array.from(affected);
|
|
2434
|
+
};
|
|
2435
|
+
const resolveViewport = (viewport)=>{
|
|
2436
|
+
if (!viewport) return null;
|
|
2437
|
+
if ('string' == typeof viewport) return resolveBrowserViewportPreset(viewport);
|
|
2438
|
+
if ('number' == typeof viewport.width && Number.isFinite(viewport.width) && viewport.width > 0 && 'number' == typeof viewport.height && Number.isFinite(viewport.height) && viewport.height > 0) return {
|
|
2439
|
+
width: viewport.width,
|
|
2440
|
+
height: viewport.height
|
|
2892
2441
|
};
|
|
2442
|
+
return null;
|
|
2893
2443
|
};
|
|
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);
|
|
2444
|
+
const mapViewportByProject = (projects)=>{
|
|
2445
|
+
const map = new Map();
|
|
2446
|
+
for (const project of projects){
|
|
2447
|
+
const viewport = resolveViewport(project.viewport);
|
|
2448
|
+
if (viewport) map.set(project.name, viewport);
|
|
3022
2449
|
}
|
|
3023
2450
|
return map;
|
|
3024
2451
|
};
|
|
3025
|
-
const ensureProcessExitCode = (code)=>{
|
|
3026
|
-
if (void 0 === process.exitCode || 0 === process.exitCode) process.exitCode = code;
|
|
3027
|
-
};
|
|
3028
2452
|
const castArray = (arr)=>{
|
|
3029
2453
|
if (void 0 === arr) return [];
|
|
3030
2454
|
return Array.isArray(arr) ? arr : [
|
|
@@ -3228,7 +2652,6 @@ const getBrowserRsbuildEnvironmentConfig = (project)=>({
|
|
|
3228
2652
|
plugins: project.normalizedConfig.plugins,
|
|
3229
2653
|
root: project.rootPath
|
|
3230
2654
|
});
|
|
3231
|
-
const getMaxTestTimeoutForRpc = (projects)=>Math.max(...projects.map((p)=>p.normalizedConfig.testTimeout ?? DEFAULT_TEST_TIMEOUT));
|
|
3232
2655
|
const getBrowserLaunchOptions = (project)=>({
|
|
3233
2656
|
provider: project.normalizedConfig.browser.provider,
|
|
3234
2657
|
browser: project.normalizedConfig.browser.browser,
|
|
@@ -3247,15 +2670,7 @@ const ensureConsistentBrowserLaunchOptions = (projects)=>{
|
|
|
3247
2670
|
}
|
|
3248
2671
|
return firstOptions;
|
|
3249
2672
|
};
|
|
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)=>{
|
|
2673
|
+
const browserRsbuild_collectProjectEntries = async (context, browserProjects = getBrowserProjects(context))=>Promise.all(browserProjects.map(async (project)=>{
|
|
3259
2674
|
const { normalizedConfig: { include, exclude, includeSource, setupFiles } } = project;
|
|
3260
2675
|
const tests = await getTestEntries({
|
|
3261
2676
|
include,
|
|
@@ -3275,14 +2690,14 @@ const collectProjectEntries = async (context, browserProjects = getBrowserProjec
|
|
|
3275
2690
|
}));
|
|
3276
2691
|
const resolveBrowserFile = (relativePath)=>{
|
|
3277
2692
|
const candidates = [
|
|
3278
|
-
external_pathe_resolve(
|
|
3279
|
-
external_pathe_resolve(
|
|
2693
|
+
external_pathe_resolve(browserRsbuild_dirname, '../src', relativePath),
|
|
2694
|
+
external_pathe_resolve(browserRsbuild_dirname, relativePath)
|
|
3280
2695
|
];
|
|
3281
2696
|
for (const candidate of candidates)if (existsSync(candidate)) return candidate;
|
|
3282
2697
|
throw new Error(`Unable to resolve browser client file: ${relativePath}`);
|
|
3283
2698
|
};
|
|
3284
2699
|
const resolveContainerDist = ()=>{
|
|
3285
|
-
const distPath = external_pathe_resolve(
|
|
2700
|
+
const distPath = external_pathe_resolve(browserRsbuild_dirname, 'browser-container');
|
|
3286
2701
|
if (existsSync(distPath)) return distPath;
|
|
3287
2702
|
throw new Error(`Browser container build not found at ${distPath}. Please run "pnpm --filter @rstest/browser build".`);
|
|
3288
2703
|
};
|
|
@@ -3424,26 +2839,6 @@ const destroyBrowserRuntime = async (runtime)=>{
|
|
|
3424
2839
|
force: true
|
|
3425
2840
|
}).catch(()=>{});
|
|
3426
2841
|
};
|
|
3427
|
-
const cleanupWatchRuntime = ()=>{
|
|
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
2842
|
const createBrowserRuntime = async ({ context, projectEntries: initialProjectEntries, browserProjects, shardedEntries, freezeShardedEntries, tempDir, isWatchMode, onTriggerRerun, containerDistPath, containerDevServer, forceHeadless, skipProviderLaunch, appliedModifyRstestConfigEnvironments })=>{
|
|
3448
2843
|
const containerHtmlTemplate = containerDistPath ? await promises.readFile(join(containerDistPath, 'index.html'), 'utf-8') : null;
|
|
3449
2844
|
let injectedContainerHtml = null;
|
|
@@ -3731,7 +3126,7 @@ const createBrowserRuntime = async ({ context, projectEntries: initialProjectEnt
|
|
|
3731
3126
|
if (watchState.hooksEnabled) watchState.pendingBuildTimeMs = Math.max(watchState.pendingBuildTimeMs, Date.now() - compileStart);
|
|
3732
3127
|
}
|
|
3733
3128
|
if (stats) {
|
|
3734
|
-
const [projectEntry] = await
|
|
3129
|
+
const [projectEntry] = await browserRsbuild_collectProjectEntries(context, [
|
|
3735
3130
|
project
|
|
3736
3131
|
]);
|
|
3737
3132
|
const entryTestFiles = new Set(collectWatchTestFiles(projectEntry ? [
|
|
@@ -3909,1155 +3304,1722 @@ async function resolveProjectEntries(context, shardedEntries, browserProjects) {
|
|
|
3909
3304
|
}
|
|
3910
3305
|
return projectEntries;
|
|
3911
3306
|
}
|
|
3912
|
-
return
|
|
3307
|
+
return browserRsbuild_collectProjectEntries(context, browserProjects);
|
|
3913
3308
|
}
|
|
3914
|
-
const
|
|
3915
|
-
|
|
3916
|
-
|
|
3917
|
-
|
|
3918
|
-
|
|
3919
|
-
|
|
3920
|
-
|
|
3921
|
-
|
|
3922
|
-
|
|
3923
|
-
|
|
3924
|
-
|
|
3925
|
-
|
|
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;
|
|
3309
|
+
const TYPE_REQUEST = "q";
|
|
3310
|
+
const TYPE_RESPONSE = "s";
|
|
3311
|
+
function createPromiseWithResolvers() {
|
|
3312
|
+
let resolve;
|
|
3313
|
+
let reject;
|
|
3314
|
+
return {
|
|
3315
|
+
promise: new Promise((res, rej)=>{
|
|
3316
|
+
resolve = res;
|
|
3317
|
+
reject = rej;
|
|
3318
|
+
}),
|
|
3319
|
+
resolve,
|
|
3320
|
+
reject
|
|
3949
3321
|
};
|
|
3950
|
-
|
|
3951
|
-
|
|
3952
|
-
|
|
3953
|
-
|
|
3954
|
-
|
|
3955
|
-
|
|
3956
|
-
|
|
3957
|
-
|
|
3958
|
-
|
|
3959
|
-
|
|
3960
|
-
|
|
3961
|
-
|
|
3962
|
-
|
|
3963
|
-
|
|
3964
|
-
|
|
3965
|
-
|
|
3966
|
-
|
|
3322
|
+
}
|
|
3323
|
+
const random = Math.random.bind(Math);
|
|
3324
|
+
const urlAlphabet = "useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict";
|
|
3325
|
+
function nanoid(size = 21) {
|
|
3326
|
+
let id = "";
|
|
3327
|
+
let i = size;
|
|
3328
|
+
while(i--)id += urlAlphabet[64 * random() | 0];
|
|
3329
|
+
return id;
|
|
3330
|
+
}
|
|
3331
|
+
const DEFAULT_TIMEOUT = 6e4;
|
|
3332
|
+
const defaultSerialize = (i)=>i;
|
|
3333
|
+
const defaultDeserialize = defaultSerialize;
|
|
3334
|
+
const { clearTimeout: dist_clearTimeout, setTimeout: dist_setTimeout } = globalThis;
|
|
3335
|
+
function createBirpc($functions, options) {
|
|
3336
|
+
const { post, on, off = ()=>{}, eventNames = [], serialize = defaultSerialize, deserialize = defaultDeserialize, resolver, bind = "rpc", timeout = DEFAULT_TIMEOUT, proxify = true } = options;
|
|
3337
|
+
let $closed = false;
|
|
3338
|
+
const _rpcPromiseMap = /* @__PURE__ */ new Map();
|
|
3339
|
+
let _promiseInit;
|
|
3340
|
+
let rpc;
|
|
3341
|
+
async function _call(method, args, event, optional) {
|
|
3342
|
+
if ($closed) throw new Error(`[birpc] rpc is closed, cannot call "${method}"`);
|
|
3343
|
+
const req = {
|
|
3344
|
+
m: method,
|
|
3345
|
+
a: args,
|
|
3346
|
+
t: TYPE_REQUEST
|
|
3967
3347
|
};
|
|
3968
|
-
if (
|
|
3969
|
-
|
|
3970
|
-
|
|
3971
|
-
|
|
3972
|
-
|
|
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);
|
|
3348
|
+
if (optional) req.o = true;
|
|
3349
|
+
const send = async (_req)=>post(serialize(_req));
|
|
3350
|
+
if (event) return void await send(req);
|
|
3351
|
+
if (_promiseInit) try {
|
|
3352
|
+
await _promiseInit;
|
|
3985
3353
|
} finally{
|
|
3986
|
-
|
|
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);
|
|
3354
|
+
_promiseInit = void 0;
|
|
4026
3355
|
}
|
|
4027
|
-
|
|
4028
|
-
|
|
4029
|
-
|
|
4030
|
-
|
|
4031
|
-
|
|
4032
|
-
|
|
4033
|
-
|
|
4034
|
-
|
|
4035
|
-
|
|
4036
|
-
|
|
4037
|
-
|
|
4038
|
-
|
|
4039
|
-
|
|
4040
|
-
|
|
4041
|
-
|
|
4042
|
-
|
|
4043
|
-
|
|
4044
|
-
|
|
4045
|
-
|
|
4046
|
-
|
|
4047
|
-
|
|
4048
|
-
if (isWatchMode || !allowEmptyRun) {
|
|
4049
|
-
const message = getNoTestFilesMessage({
|
|
4050
|
-
context,
|
|
4051
|
-
code,
|
|
4052
|
-
defaultMessage: `No test files found, exiting with code ${code}.`
|
|
3356
|
+
let { promise, resolve, reject } = createPromiseWithResolvers();
|
|
3357
|
+
const id = nanoid();
|
|
3358
|
+
req.i = id;
|
|
3359
|
+
let timeoutId;
|
|
3360
|
+
async function handler(newReq = req) {
|
|
3361
|
+
if (timeout >= 0) {
|
|
3362
|
+
timeoutId = dist_setTimeout(()=>{
|
|
3363
|
+
try {
|
|
3364
|
+
if (options.onTimeoutError?.call(rpc, method, args) !== true) throw new Error(`[birpc] timeout on calling "${method}"`);
|
|
3365
|
+
} catch (e) {
|
|
3366
|
+
reject(e);
|
|
3367
|
+
}
|
|
3368
|
+
_rpcPromiseMap.delete(id);
|
|
3369
|
+
}, timeout);
|
|
3370
|
+
if ("object" == typeof timeoutId) timeoutId = timeoutId.unref?.();
|
|
3371
|
+
}
|
|
3372
|
+
_rpcPromiseMap.set(id, {
|
|
3373
|
+
resolve,
|
|
3374
|
+
reject,
|
|
3375
|
+
timeoutId,
|
|
3376
|
+
method
|
|
4053
3377
|
});
|
|
4054
|
-
|
|
4055
|
-
|
|
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(', ')));
|
|
3378
|
+
await send(newReq);
|
|
3379
|
+
return promise;
|
|
4058
3380
|
}
|
|
4059
|
-
if (isWatchMode && 0 !== code && !allowEmptyRun) ensureProcessExitCode(code);
|
|
4060
|
-
};
|
|
4061
|
-
if (0 === totalTests && !shouldInitializeEmptyBrowserHooks) {
|
|
4062
|
-
reportEmptyTestSet();
|
|
4063
|
-
return allowEmptyRun ? createEmptyRunResult() : void 0;
|
|
4064
|
-
}
|
|
4065
|
-
if (!filesOnly) await notifyTestRunStart();
|
|
4066
|
-
const enableCliShortcuts = isWatchMode && isTTY('stdin');
|
|
4067
|
-
const browserTempOutputRoot = context.normalizedConfig.output.distPath.root;
|
|
4068
|
-
const tempDir = isWatchMode && watchContext.runtime ? watchContext.runtime.tempDir : isWatchMode ? join(context.rootPath, browserTempOutputRoot, 'browser', 'watch') : join(context.rootPath, browserTempOutputRoot, 'browser', Date.now().toString());
|
|
4069
|
-
let runtime = isWatchMode ? watchContext.runtime : null;
|
|
4070
|
-
let triggerRerun;
|
|
4071
|
-
let awaitHeadlessRerunIdle;
|
|
4072
|
-
if (!runtime) {
|
|
4073
3381
|
try {
|
|
4074
|
-
|
|
4075
|
-
|
|
4076
|
-
|
|
4077
|
-
|
|
4078
|
-
|
|
4079
|
-
|
|
4080
|
-
|
|
4081
|
-
|
|
4082
|
-
onTriggerRerun: isWatchMode ? async ()=>{
|
|
4083
|
-
await triggerRerun?.();
|
|
4084
|
-
} : void 0,
|
|
4085
|
-
containerDistPath,
|
|
4086
|
-
containerDevServer,
|
|
4087
|
-
skipProviderLaunch: filesOnly,
|
|
4088
|
-
appliedModifyRstestConfigEnvironments: options?.appliedModifyRstestConfigEnvironments
|
|
4089
|
-
});
|
|
4090
|
-
} catch (error) {
|
|
4091
|
-
return failWithError(error, async ()=>{
|
|
4092
|
-
await promises.rm(tempDir, {
|
|
4093
|
-
recursive: true,
|
|
4094
|
-
force: true
|
|
4095
|
-
}).catch(()=>{});
|
|
4096
|
-
});
|
|
4097
|
-
}
|
|
4098
|
-
if (isWatchMode) {
|
|
4099
|
-
watchContext.runtime = runtime;
|
|
4100
|
-
registerWatchCleanup(context.embedded);
|
|
3382
|
+
if (options.onRequest) await options.onRequest.call(rpc, req, handler, resolve);
|
|
3383
|
+
else await handler();
|
|
3384
|
+
} catch (e) {
|
|
3385
|
+
if (options.onGeneralError?.call(rpc, e) !== true) throw e;
|
|
3386
|
+
return;
|
|
3387
|
+
} finally{
|
|
3388
|
+
dist_clearTimeout(timeoutId);
|
|
3389
|
+
_rpcPromiseMap.delete(id);
|
|
4101
3390
|
}
|
|
3391
|
+
return promise;
|
|
4102
3392
|
}
|
|
4103
|
-
const
|
|
4104
|
-
|
|
4105
|
-
|
|
4106
|
-
|
|
4107
|
-
|
|
4108
|
-
|
|
4109
|
-
|
|
4110
|
-
|
|
4111
|
-
pending.add(file.testPath);
|
|
4112
|
-
watchState.pendingAffectedTestFiles.set(file.projectName, pending);
|
|
4113
|
-
seeded += 1;
|
|
4114
|
-
}
|
|
4115
|
-
return seeded;
|
|
4116
|
-
};
|
|
4117
|
-
const watchHandles = isWatchMode ? {
|
|
4118
|
-
rerun: async (testPaths)=>{
|
|
4119
|
-
const seeded = seedPendingRerun(testPaths);
|
|
4120
|
-
if (testPaths && 0 === seeded) return;
|
|
4121
|
-
await triggerRerun?.();
|
|
4122
|
-
await awaitHeadlessRerunIdle?.();
|
|
3393
|
+
const builtinMethods = {
|
|
3394
|
+
$call: (method, ...args)=>_call(method, args, false),
|
|
3395
|
+
$callOptional: (method, ...args)=>_call(method, args, false, true),
|
|
3396
|
+
$callEvent: (method, ...args)=>_call(method, args, true),
|
|
3397
|
+
$callRaw: (options$1)=>_call(options$1.method, options$1.args, options$1.event, options$1.optional),
|
|
3398
|
+
$rejectPendingCalls,
|
|
3399
|
+
get $closed () {
|
|
3400
|
+
return $closed;
|
|
4123
3401
|
},
|
|
4124
|
-
|
|
4125
|
-
|
|
4126
|
-
const finalizeWatchRerun = async ({ rerunTestPaths, testTime, unhandledErrors })=>{
|
|
4127
|
-
const rerunPathSet = new Set(rerunTestPaths);
|
|
4128
|
-
const rerunResults = context.reporterResults.results.filter((result)=>rerunPathSet.has(result.testPath));
|
|
4129
|
-
let rerunCoverage;
|
|
4130
|
-
const coverageMap = buildBrowserCoverageMap(rerunResults, coverageProvider);
|
|
4131
|
-
if (coverageMap && coverageMap.files().length > 0) rerunCoverage = coverageMap.toJSON();
|
|
4132
|
-
const outcome = {
|
|
4133
|
-
results: rerunResults,
|
|
4134
|
-
testResults: context.reporterResults.testResults.filter((result)=>rerunPathSet.has(result.testPath)),
|
|
4135
|
-
errors: unhandledErrors ?? [],
|
|
4136
|
-
testPaths: rerunTestPaths,
|
|
4137
|
-
duration: {
|
|
4138
|
-
buildTime: drainPendingBuildTime(watchState),
|
|
4139
|
-
testTime
|
|
4140
|
-
},
|
|
4141
|
-
coverage: rerunCoverage ? {
|
|
4142
|
-
map: rerunCoverage
|
|
4143
|
-
} : void 0,
|
|
4144
|
-
resolveSourcemap: resolveBrowserSourcemap
|
|
4145
|
-
};
|
|
4146
|
-
await finalizeRunCycle(context, {
|
|
4147
|
-
outcomes: [
|
|
4148
|
-
outcome
|
|
4149
|
-
],
|
|
4150
|
-
mode: 'on-demand',
|
|
4151
|
-
isWatchMode: true,
|
|
4152
|
-
coverageProvider,
|
|
4153
|
-
reportOnFailure: coverageConfig?.reportOnFailure ?? false
|
|
4154
|
-
});
|
|
4155
|
-
};
|
|
4156
|
-
projectEntries = runtime.projectEntries;
|
|
4157
|
-
totalTests = projectEntries.reduce((total, item)=>total + item.testFiles.length, 0);
|
|
4158
|
-
const buildTime = Date.now() - buildStart;
|
|
4159
|
-
if (filesOnly) return {
|
|
4160
|
-
results: [],
|
|
4161
|
-
testResults: [],
|
|
4162
|
-
duration: {
|
|
4163
|
-
totalTime: buildTime,
|
|
4164
|
-
buildTime,
|
|
4165
|
-
testTime: 0
|
|
3402
|
+
get $meta () {
|
|
3403
|
+
return options.meta;
|
|
4166
3404
|
},
|
|
4167
|
-
|
|
4168
|
-
|
|
4169
|
-
resolveSourcemap: resolveBrowserSourcemap,
|
|
4170
|
-
close: ()=>destroyBrowserRuntime(runtime)
|
|
3405
|
+
$close,
|
|
3406
|
+
$functions
|
|
4171
3407
|
};
|
|
4172
|
-
|
|
4173
|
-
|
|
4174
|
-
|
|
4175
|
-
|
|
3408
|
+
rpc = proxify ? new Proxy({}, {
|
|
3409
|
+
get (_, method) {
|
|
3410
|
+
if (Object.prototype.hasOwnProperty.call(builtinMethods, method)) return builtinMethods[method];
|
|
3411
|
+
if ("then" === method && !eventNames.includes("then") && !("then" in $functions)) return;
|
|
3412
|
+
const sendEvent = (...args)=>_call(method, args, true);
|
|
3413
|
+
if (eventNames.includes(method)) {
|
|
3414
|
+
sendEvent.asEvent = sendEvent;
|
|
3415
|
+
return sendEvent;
|
|
3416
|
+
}
|
|
3417
|
+
const sendCall = (...args)=>_call(method, args, false);
|
|
3418
|
+
sendCall.asEvent = sendEvent;
|
|
3419
|
+
return sendCall;
|
|
3420
|
+
}
|
|
3421
|
+
}) : builtinMethods;
|
|
3422
|
+
function $close(customError) {
|
|
3423
|
+
$closed = true;
|
|
3424
|
+
_rpcPromiseMap.forEach(({ reject, method })=>{
|
|
3425
|
+
const error = /* @__PURE__ */ new Error(`[birpc] rpc is closed, cannot call "${method}"`);
|
|
3426
|
+
if (customError) {
|
|
3427
|
+
customError.cause ??= error;
|
|
3428
|
+
return reject(customError);
|
|
3429
|
+
}
|
|
3430
|
+
reject(error);
|
|
3431
|
+
});
|
|
3432
|
+
_rpcPromiseMap.clear();
|
|
3433
|
+
off(onMessage);
|
|
4176
3434
|
}
|
|
4177
|
-
|
|
4178
|
-
|
|
4179
|
-
|
|
4180
|
-
|
|
4181
|
-
|
|
4182
|
-
|
|
4183
|
-
|
|
4184
|
-
environmentName: project.environmentName,
|
|
4185
|
-
projectRoot: normalize(project.rootPath),
|
|
4186
|
-
runtimeConfig: serializableConfig(projectRuntimeConfig(project, {
|
|
4187
|
-
envMode: 'static',
|
|
4188
|
-
envOverlay: env
|
|
4189
|
-
})),
|
|
4190
|
-
viewport: project.normalizedConfig.browser.viewport
|
|
4191
|
-
}));
|
|
4192
|
-
const maxTestTimeoutForRpc = getMaxTestTimeoutForRpc(browserProjects);
|
|
4193
|
-
const projectRunnerUrls = Object.fromEntries([
|
|
4194
|
-
...runtime.projectServers
|
|
4195
|
-
].map(([name, server])=>[
|
|
4196
|
-
name,
|
|
4197
|
-
`http://localhost:${server.port}`
|
|
4198
|
-
]));
|
|
4199
|
-
const hostOptions = {
|
|
4200
|
-
rootPath: normalize(context.rootPath),
|
|
4201
|
-
projects: projectRuntimeConfigs,
|
|
4202
|
-
snapshot: {
|
|
4203
|
-
updateSnapshot: options?.updateSnapshot ?? context.snapshotManager.options.updateSnapshot
|
|
4204
|
-
},
|
|
4205
|
-
runnerUrl: `http://localhost:${runtime.containerServer.port}`,
|
|
4206
|
-
projectRunnerUrls,
|
|
4207
|
-
wsPort,
|
|
4208
|
-
debug: isDebug(),
|
|
4209
|
-
rpcTimeout: maxTestTimeoutForRpc
|
|
4210
|
-
};
|
|
4211
|
-
const browserProviderProjects = browserProjects.map((project)=>({
|
|
4212
|
-
rootPath: normalize(project.rootPath),
|
|
4213
|
-
provider: project.normalizedConfig.browser.provider
|
|
4214
|
-
}));
|
|
4215
|
-
const implementationByProvider = new Map();
|
|
4216
|
-
for (const browserProject of browserProviderProjects)if (!implementationByProvider.has(browserProject.provider)) implementationByProvider.set(browserProject.provider, getBrowserProviderImplementation(browserProject.provider));
|
|
4217
|
-
let activeContainerPage = null;
|
|
4218
|
-
let getHeadlessRunnerPageBySessionId;
|
|
4219
|
-
const dispatchBrowserRpcRequest = async ({ request, target })=>{
|
|
4220
|
-
const timeoutFallbackMs = maxTestTimeoutForRpc;
|
|
4221
|
-
const provider = resolveProviderForTestPath({
|
|
4222
|
-
testPath: request.testPath,
|
|
4223
|
-
browserProjects: browserProviderProjects
|
|
3435
|
+
function $rejectPendingCalls(handler) {
|
|
3436
|
+
const handlerResults = Array.from(_rpcPromiseMap.values()).map(({ method, reject })=>{
|
|
3437
|
+
if (!handler) return reject(/* @__PURE__ */ new Error(`[birpc]: rejected pending call "${method}".`));
|
|
3438
|
+
return handler({
|
|
3439
|
+
method,
|
|
3440
|
+
reject
|
|
3441
|
+
});
|
|
4224
3442
|
});
|
|
4225
|
-
|
|
4226
|
-
|
|
4227
|
-
|
|
4228
|
-
|
|
4229
|
-
|
|
3443
|
+
_rpcPromiseMap.clear();
|
|
3444
|
+
return handlerResults;
|
|
3445
|
+
}
|
|
3446
|
+
async function onMessage(data, ...extra) {
|
|
3447
|
+
let msg;
|
|
4230
3448
|
try {
|
|
4231
|
-
|
|
4232
|
-
|
|
4233
|
-
|
|
4234
|
-
|
|
4235
|
-
timeoutFallbackMs
|
|
4236
|
-
});
|
|
4237
|
-
} catch (error) {
|
|
4238
|
-
if (error instanceof Error) throw error.message;
|
|
4239
|
-
throw String(error);
|
|
3449
|
+
msg = deserialize(data);
|
|
3450
|
+
} catch (e) {
|
|
3451
|
+
if (options.onGeneralError?.call(rpc, e) !== true) throw e;
|
|
3452
|
+
return;
|
|
4240
3453
|
}
|
|
3454
|
+
if (msg.t === TYPE_REQUEST) {
|
|
3455
|
+
const { m: method, a: args, o: optional } = msg;
|
|
3456
|
+
let result, error;
|
|
3457
|
+
let fn = await (resolver ? resolver.call(rpc, method, $functions[method]) : $functions[method]);
|
|
3458
|
+
if (optional) fn ||= ()=>void 0;
|
|
3459
|
+
if (fn) try {
|
|
3460
|
+
result = await fn.apply("rpc" === bind ? rpc : $functions, args);
|
|
3461
|
+
} catch (e) {
|
|
3462
|
+
error = e;
|
|
3463
|
+
}
|
|
3464
|
+
else error = /* @__PURE__ */ new Error(`[birpc] function "${method}" not found`);
|
|
3465
|
+
if (msg.i) {
|
|
3466
|
+
if (error && options.onFunctionError) {
|
|
3467
|
+
if (true === options.onFunctionError.call(rpc, error, method, args)) return;
|
|
3468
|
+
}
|
|
3469
|
+
if (!error) try {
|
|
3470
|
+
await post(serialize({
|
|
3471
|
+
t: TYPE_RESPONSE,
|
|
3472
|
+
i: msg.i,
|
|
3473
|
+
r: result
|
|
3474
|
+
}), ...extra);
|
|
3475
|
+
return;
|
|
3476
|
+
} catch (e) {
|
|
3477
|
+
error = e;
|
|
3478
|
+
if (options.onGeneralError?.call(rpc, e, method, args) !== true) throw e;
|
|
3479
|
+
}
|
|
3480
|
+
try {
|
|
3481
|
+
await post(serialize({
|
|
3482
|
+
t: TYPE_RESPONSE,
|
|
3483
|
+
i: msg.i,
|
|
3484
|
+
e: error
|
|
3485
|
+
}), ...extra);
|
|
3486
|
+
} catch (e) {
|
|
3487
|
+
if (options.onGeneralError?.call(rpc, e, method, args) !== true) throw e;
|
|
3488
|
+
}
|
|
3489
|
+
}
|
|
3490
|
+
} else {
|
|
3491
|
+
const { i: ack, r: result, e: error } = msg;
|
|
3492
|
+
const promise = _rpcPromiseMap.get(ack);
|
|
3493
|
+
if (promise) {
|
|
3494
|
+
dist_clearTimeout(promise.timeoutId);
|
|
3495
|
+
if (error) promise.reject(error);
|
|
3496
|
+
else promise.resolve(result);
|
|
3497
|
+
}
|
|
3498
|
+
_rpcPromiseMap.delete(ack);
|
|
3499
|
+
}
|
|
3500
|
+
}
|
|
3501
|
+
_promiseInit = on(onMessage);
|
|
3502
|
+
return rpc;
|
|
3503
|
+
}
|
|
3504
|
+
class ContainerRpcManager {
|
|
3505
|
+
wss;
|
|
3506
|
+
ws = null;
|
|
3507
|
+
rpc = null;
|
|
3508
|
+
methods;
|
|
3509
|
+
onDisconnect;
|
|
3510
|
+
detachActiveSocketListeners = null;
|
|
3511
|
+
constructor(wss, methods, onDisconnect){
|
|
3512
|
+
this.wss = wss;
|
|
3513
|
+
this.methods = methods;
|
|
3514
|
+
this.onDisconnect = onDisconnect;
|
|
3515
|
+
this.setupConnectionHandler();
|
|
3516
|
+
}
|
|
3517
|
+
updateMethods(methods, onDisconnect) {
|
|
3518
|
+
this.methods = methods;
|
|
3519
|
+
this.onDisconnect = onDisconnect;
|
|
3520
|
+
if (this.ws && this.ws.readyState === this.ws.OPEN) this.attachWebSocket(this.ws);
|
|
3521
|
+
}
|
|
3522
|
+
setupConnectionHandler() {
|
|
3523
|
+
this.wss.on('connection', (ws)=>{
|
|
3524
|
+
logger.debug('[Browser UI] Container WebSocket connected');
|
|
3525
|
+
logger.debug(`[Browser UI] Current ws: ${this.ws ? 'exists' : 'null'}, new ws: ${ws ? 'exists' : 'null'}`);
|
|
3526
|
+
this.attachWebSocket(ws);
|
|
3527
|
+
});
|
|
3528
|
+
}
|
|
3529
|
+
attachWebSocket(ws) {
|
|
3530
|
+
this.detachActiveSocketListeners?.();
|
|
3531
|
+
if (this.rpc && !this.rpc.$closed) this.rpc.$close(new Error('Container RPC transport reattached'));
|
|
3532
|
+
this.ws = ws;
|
|
3533
|
+
const messageHandlers = new WeakMap();
|
|
3534
|
+
this.rpc = createBirpc(this.methods, {
|
|
3535
|
+
timeout: -1,
|
|
3536
|
+
post: (data)=>{
|
|
3537
|
+
if (ws.readyState === ws.OPEN) ws.send(JSON.stringify(data));
|
|
3538
|
+
},
|
|
3539
|
+
on: (fn)=>{
|
|
3540
|
+
const handler = (message)=>{
|
|
3541
|
+
try {
|
|
3542
|
+
const data = JSON.parse(message.toString());
|
|
3543
|
+
fn(data);
|
|
3544
|
+
} catch {}
|
|
3545
|
+
};
|
|
3546
|
+
messageHandlers.set(fn, handler);
|
|
3547
|
+
ws.on('message', handler);
|
|
3548
|
+
},
|
|
3549
|
+
off: (fn)=>{
|
|
3550
|
+
const handler = messageHandlers.get(fn);
|
|
3551
|
+
if (!handler) return;
|
|
3552
|
+
ws.off('message', handler);
|
|
3553
|
+
messageHandlers.delete(fn);
|
|
3554
|
+
}
|
|
3555
|
+
});
|
|
3556
|
+
const handleClose = ()=>{
|
|
3557
|
+
if (this.ws === ws) this.ws = null;
|
|
3558
|
+
this.detachActiveSocketListeners?.();
|
|
3559
|
+
this.detachActiveSocketListeners = null;
|
|
3560
|
+
if (this.rpc && !this.rpc.$closed) {
|
|
3561
|
+
const disconnectError = new Error('Browser UI WebSocket disconnected before reload completed');
|
|
3562
|
+
this.rpc.$close(disconnectError);
|
|
3563
|
+
this.onDisconnect?.(disconnectError);
|
|
3564
|
+
}
|
|
3565
|
+
this.rpc = null;
|
|
3566
|
+
};
|
|
3567
|
+
ws.on('close', handleClose);
|
|
3568
|
+
this.detachActiveSocketListeners = ()=>{
|
|
3569
|
+
ws.off('close', handleClose);
|
|
3570
|
+
};
|
|
3571
|
+
}
|
|
3572
|
+
get isConnected() {
|
|
3573
|
+
return null !== this.ws && this.ws.readyState === this.ws.OPEN;
|
|
3574
|
+
}
|
|
3575
|
+
get currentWebSocket() {
|
|
3576
|
+
return this.ws;
|
|
3577
|
+
}
|
|
3578
|
+
reattach(ws) {
|
|
3579
|
+
this.attachWebSocket(ws);
|
|
3580
|
+
}
|
|
3581
|
+
async notifyTestFileUpdate(files) {
|
|
3582
|
+
await this.rpc?.onTestFileUpdate(files);
|
|
3583
|
+
}
|
|
3584
|
+
async updateHostConfig(config) {
|
|
3585
|
+
await this.rpc?.onHostConfigUpdate(config);
|
|
3586
|
+
}
|
|
3587
|
+
async reloadTestFile(testFile, testNamePattern) {
|
|
3588
|
+
logger.debug(`[Browser UI] reloadTestFile called, rpc: ${this.rpc ? 'exists' : 'null'}, ws: ${this.ws ? 'exists' : 'null'}`);
|
|
3589
|
+
if (!this.rpc) throw new Error('Browser UI RPC not available for reloadTestFile');
|
|
3590
|
+
logger.debug(`[Browser UI] Calling reloadTestFile: ${testFile}`);
|
|
3591
|
+
return this.rpc.reloadTestFile(testFile, testNamePattern);
|
|
3592
|
+
}
|
|
3593
|
+
}
|
|
3594
|
+
const createHeadedSerialTaskQueue = ()=>{
|
|
3595
|
+
let queue = Promise.resolve();
|
|
3596
|
+
const enqueue = (task)=>{
|
|
3597
|
+
const next = queue.then(task);
|
|
3598
|
+
queue = next.catch(()=>{});
|
|
3599
|
+
return next;
|
|
3600
|
+
};
|
|
3601
|
+
return {
|
|
3602
|
+
enqueue
|
|
3603
|
+
};
|
|
3604
|
+
};
|
|
3605
|
+
const getFileTaskId = (testPath)=>`file:${testPath}`;
|
|
3606
|
+
const toError = (error)=>error instanceof Error ? error : new Error(String(error));
|
|
3607
|
+
const createDeferredPromise = ()=>{
|
|
3608
|
+
let resolve;
|
|
3609
|
+
let reject;
|
|
3610
|
+
const promise = new Promise((res, rej)=>{
|
|
3611
|
+
resolve = res;
|
|
3612
|
+
reject = rej;
|
|
3613
|
+
});
|
|
3614
|
+
return {
|
|
3615
|
+
promise,
|
|
3616
|
+
resolve,
|
|
3617
|
+
reject
|
|
3618
|
+
};
|
|
3619
|
+
};
|
|
3620
|
+
const claimHeadedCycleScope = (testPaths, currentTestFiles, pendingTestNamePatterns)=>{
|
|
3621
|
+
const scope = [];
|
|
3622
|
+
const filesByPath = new Map(currentTestFiles.map((file)=>[
|
|
3623
|
+
file.testPath,
|
|
3624
|
+
file
|
|
3625
|
+
]));
|
|
3626
|
+
for (const testPath of testPaths){
|
|
3627
|
+
const normalizedTestPath = normalize(testPath);
|
|
3628
|
+
const file = filesByPath.get(normalizedTestPath);
|
|
3629
|
+
if (file) {
|
|
3630
|
+
scope.push({
|
|
3631
|
+
file,
|
|
3632
|
+
testNamePattern: pendingTestNamePatterns.get(normalizedTestPath)
|
|
3633
|
+
});
|
|
3634
|
+
pendingTestNamePatterns.delete(normalizedTestPath);
|
|
3635
|
+
}
|
|
3636
|
+
}
|
|
3637
|
+
return scope;
|
|
3638
|
+
};
|
|
3639
|
+
const createHeadedScheduler = async ({ context, runtime, allTestFiles, hostOptions, isWatchMode, createDispatchRouter, handlers: { handleTestFileStart, handleTestCaseResult, handleTestFileComplete, handleLog, handleFatal }, fatalErrorRef, watchSignals, setDispatchPageResolver, createWatchSession, collectProjectEntries, logWatchReady, destroyRuntime })=>{
|
|
3640
|
+
const { browser, browserLaunchOptions, watchState, wss } = runtime;
|
|
3641
|
+
let currentTestFiles = allTestFiles;
|
|
3642
|
+
const RUNNER_FRAMES_READY_TIMEOUT_MS = 30000;
|
|
3643
|
+
let currentRunnerFramesSignature = null;
|
|
3644
|
+
const runnerFramesWaiters = new Map();
|
|
3645
|
+
const createTestFilesSignature = (testFiles)=>JSON.stringify(testFiles.map((testFile)=>normalize(testFile)));
|
|
3646
|
+
const markRunnerFramesReady = (testFiles)=>{
|
|
3647
|
+
const signature = createTestFilesSignature(testFiles);
|
|
3648
|
+
currentRunnerFramesSignature = signature;
|
|
3649
|
+
const waiters = runnerFramesWaiters.get(signature);
|
|
3650
|
+
if (!waiters) return;
|
|
3651
|
+
runnerFramesWaiters.delete(signature);
|
|
3652
|
+
for (const waiter of waiters)waiter();
|
|
3653
|
+
};
|
|
3654
|
+
const waitForRunnerFramesReady = async (testFiles)=>{
|
|
3655
|
+
const signature = createTestFilesSignature(testFiles);
|
|
3656
|
+
if (currentRunnerFramesSignature === signature) return;
|
|
3657
|
+
await new Promise((resolve, reject)=>{
|
|
3658
|
+
const waiters = runnerFramesWaiters.get(signature) ?? new Set();
|
|
3659
|
+
const cleanup = ()=>{
|
|
3660
|
+
const currentWaiters = runnerFramesWaiters.get(signature);
|
|
3661
|
+
if (!currentWaiters) return;
|
|
3662
|
+
currentWaiters.delete(onReady);
|
|
3663
|
+
if (0 === currentWaiters.size) runnerFramesWaiters.delete(signature);
|
|
3664
|
+
};
|
|
3665
|
+
const onReady = ()=>{
|
|
3666
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
3667
|
+
cleanup();
|
|
3668
|
+
resolve();
|
|
3669
|
+
};
|
|
3670
|
+
const timeoutId = setTimeout(()=>{
|
|
3671
|
+
cleanup();
|
|
3672
|
+
reject(new Error(`Timed out waiting for headed runner frames to be ready for ${testFiles.length} file(s).`));
|
|
3673
|
+
}, RUNNER_FRAMES_READY_TIMEOUT_MS);
|
|
3674
|
+
waiters.add(onReady);
|
|
3675
|
+
runnerFramesWaiters.set(signature, waiters);
|
|
3676
|
+
if (currentRunnerFramesSignature === signature) onReady();
|
|
3677
|
+
});
|
|
3678
|
+
};
|
|
3679
|
+
const getTestFileInfo = (testFile)=>{
|
|
3680
|
+
const normalizedTestFile = normalize(testFile);
|
|
3681
|
+
const fileInfo = currentTestFiles.find((file)=>file.testPath === normalizedTestFile);
|
|
3682
|
+
if (!fileInfo) throw new Error(`Unknown browser test file: ${JSON.stringify(testFile)}`);
|
|
3683
|
+
return fileInfo;
|
|
3684
|
+
};
|
|
3685
|
+
let containerContext;
|
|
3686
|
+
let containerPage;
|
|
3687
|
+
let isNewPage = false;
|
|
3688
|
+
if (isWatchMode && runtime.containerPage && runtime.containerContext) {
|
|
3689
|
+
containerContext = runtime.containerContext;
|
|
3690
|
+
containerPage = runtime.containerPage;
|
|
3691
|
+
logger.log(color.gray('\n[Watch] Reusing existing container page\n'));
|
|
3692
|
+
} else {
|
|
3693
|
+
isNewPage = true;
|
|
3694
|
+
containerContext = await browser.newContext({
|
|
3695
|
+
providerOptions: browserLaunchOptions.providerOptions,
|
|
3696
|
+
viewport: null
|
|
3697
|
+
});
|
|
3698
|
+
containerPage = await containerContext.newPage();
|
|
3699
|
+
containerPage.on('popup', async (popup)=>{
|
|
3700
|
+
await popup.close().catch(()=>{});
|
|
3701
|
+
});
|
|
3702
|
+
containerContext.on('page', async (page)=>{
|
|
3703
|
+
if (page !== containerPage) await page.close().catch(()=>{});
|
|
3704
|
+
});
|
|
3705
|
+
if (isWatchMode) {
|
|
3706
|
+
runtime.containerPage = containerPage;
|
|
3707
|
+
runtime.containerContext = containerContext;
|
|
3708
|
+
}
|
|
3709
|
+
containerPage.on('console', (msg)=>{
|
|
3710
|
+
const text = msg.text();
|
|
3711
|
+
if (text.startsWith('[Container]') || text.startsWith('[Runner]')) logger.log(color.gray(`[Browser Console] ${text}`));
|
|
3712
|
+
});
|
|
3713
|
+
}
|
|
3714
|
+
setDispatchPageResolver(()=>({
|
|
3715
|
+
containerPage
|
|
3716
|
+
}));
|
|
3717
|
+
const dispatchRouter = createDispatchRouter();
|
|
3718
|
+
const headedReloadQueue = createHeadedSerialTaskQueue();
|
|
3719
|
+
const pendingHeadedReloads = new Map();
|
|
3720
|
+
let enqueueHeadedReload = async (_file, _testNamePattern)=>{
|
|
3721
|
+
throw new Error('Headed reload queue is not initialized');
|
|
3722
|
+
};
|
|
3723
|
+
const rejectPendingHeadedReload = (testPath, error, runId)=>{
|
|
3724
|
+
const pending = pendingHeadedReloads.get(testPath);
|
|
3725
|
+
if (!pending) return;
|
|
3726
|
+
if (runId && pending.runId !== runId) return;
|
|
3727
|
+
pendingHeadedReloads.delete(testPath);
|
|
3728
|
+
pending.deferred.reject(error);
|
|
3729
|
+
};
|
|
3730
|
+
const rejectAllPendingHeadedReloads = (error)=>{
|
|
3731
|
+
for (const [testPath, pending] of pendingHeadedReloads){
|
|
3732
|
+
pendingHeadedReloads.delete(testPath);
|
|
3733
|
+
pending.deferred.reject(error);
|
|
3734
|
+
}
|
|
3735
|
+
};
|
|
3736
|
+
const registerPendingHeadedReload = (testPath, runId)=>{
|
|
3737
|
+
const previousPending = pendingHeadedReloads.get(testPath);
|
|
3738
|
+
if (previousPending) {
|
|
3739
|
+
previousPending.deferred.reject(new Error(`Reload for "${testPath}" was superseded by a newer request.`));
|
|
3740
|
+
pendingHeadedReloads.delete(testPath);
|
|
3741
|
+
}
|
|
3742
|
+
const deferred = createDeferredPromise();
|
|
3743
|
+
pendingHeadedReloads.set(testPath, {
|
|
3744
|
+
runId,
|
|
3745
|
+
deferred
|
|
3746
|
+
});
|
|
3747
|
+
return deferred.promise;
|
|
3748
|
+
};
|
|
3749
|
+
const resolvePendingHeadedReload = (testPath, runId)=>{
|
|
3750
|
+
const pending = pendingHeadedReloads.get(testPath);
|
|
3751
|
+
if (!pending) return;
|
|
3752
|
+
if (runId && pending.runId !== runId) return void logger.debug(`[Browser UI] Ignoring stale file-complete for ${testPath}. current=${pending.runId}, incoming=${runId}`);
|
|
3753
|
+
pendingHeadedReloads.delete(testPath);
|
|
3754
|
+
pending.deferred.resolve();
|
|
3755
|
+
};
|
|
3756
|
+
const reloadTestFileAndWait = async (file, testNamePattern)=>{
|
|
3757
|
+
let reloadAck;
|
|
3758
|
+
try {
|
|
3759
|
+
reloadAck = await rpcManager.reloadTestFile(file.testPath, testNamePattern);
|
|
3760
|
+
await registerPendingHeadedReload(file.testPath, reloadAck.runId);
|
|
3761
|
+
} catch (error) {
|
|
3762
|
+
if (reloadAck?.runId) rejectPendingHeadedReload(file.testPath, toError(error), reloadAck.runId);
|
|
3763
|
+
throw error;
|
|
3764
|
+
}
|
|
3765
|
+
};
|
|
3766
|
+
let runUiRequestedRerun = async (file, testNamePattern)=>{
|
|
3767
|
+
await enqueueHeadedReload(file, testNamePattern);
|
|
3768
|
+
};
|
|
3769
|
+
const createRpcMethods = ()=>({
|
|
3770
|
+
async rerunTest (testFile, testNamePattern) {
|
|
3771
|
+
const projectName = context.normalizedConfig.name || 'project';
|
|
3772
|
+
const relativePath = relative(context.rootPath, testFile);
|
|
3773
|
+
const displayPath = `<${projectName}>/${relativePath}`;
|
|
3774
|
+
logger.log(color.cyan(`\nRe-running test: ${displayPath}${testNamePattern ? ` (pattern: ${testNamePattern})` : ''}\n`));
|
|
3775
|
+
await runUiRequestedRerun(getTestFileInfo(testFile), testNamePattern);
|
|
3776
|
+
},
|
|
3777
|
+
async getTestFiles () {
|
|
3778
|
+
return currentTestFiles;
|
|
3779
|
+
},
|
|
3780
|
+
async onRunnerFramesReady (testFiles) {
|
|
3781
|
+
markRunnerFramesReady(testFiles);
|
|
3782
|
+
},
|
|
3783
|
+
async onTestFileStart (payload) {
|
|
3784
|
+
await handleTestFileStart(payload);
|
|
3785
|
+
},
|
|
3786
|
+
async onTestCaseResult (payload) {
|
|
3787
|
+
await handleTestCaseResult(payload);
|
|
3788
|
+
},
|
|
3789
|
+
async onTestFileComplete (payload) {
|
|
3790
|
+
try {
|
|
3791
|
+
await handleTestFileComplete(payload);
|
|
3792
|
+
resolvePendingHeadedReload(payload.testPath, payload.runId);
|
|
3793
|
+
} catch (error) {
|
|
3794
|
+
rejectPendingHeadedReload(payload.testPath, toError(error), payload.runId);
|
|
3795
|
+
throw error;
|
|
3796
|
+
}
|
|
3797
|
+
},
|
|
3798
|
+
async onLog (payload) {
|
|
3799
|
+
await handleLog(payload);
|
|
3800
|
+
},
|
|
3801
|
+
async onFatal (payload) {
|
|
3802
|
+
const error = new Error(payload.message);
|
|
3803
|
+
error.stack = payload.stack;
|
|
3804
|
+
rejectAllPendingHeadedReloads(error);
|
|
3805
|
+
await handleFatal(payload);
|
|
3806
|
+
},
|
|
3807
|
+
async dispatch (request) {
|
|
3808
|
+
return dispatchRouter.dispatch(request);
|
|
3809
|
+
}
|
|
3810
|
+
});
|
|
3811
|
+
let rpcManager;
|
|
3812
|
+
if (isWatchMode && runtime.rpcManager) {
|
|
3813
|
+
rpcManager = runtime.rpcManager;
|
|
3814
|
+
rpcManager.updateMethods(createRpcMethods(), rejectAllPendingHeadedReloads);
|
|
3815
|
+
const existingWs = rpcManager.currentWebSocket;
|
|
3816
|
+
if (existingWs) rpcManager.reattach(existingWs);
|
|
3817
|
+
} else {
|
|
3818
|
+
rpcManager = new ContainerRpcManager(wss, createRpcMethods(), rejectAllPendingHeadedReloads);
|
|
3819
|
+
if (isWatchMode) runtime.rpcManager = rpcManager;
|
|
3820
|
+
}
|
|
3821
|
+
if (isNewPage) {
|
|
3822
|
+
const pagePath = '/';
|
|
3823
|
+
const containerPort = runtime.containerServer.port;
|
|
3824
|
+
await containerPage.goto(`http://localhost:${containerPort}${pagePath}`, {
|
|
3825
|
+
waitUntil: 'load'
|
|
3826
|
+
});
|
|
3827
|
+
logger.log(color.cyan(`\nBrowser mode opened at http://localhost:${containerPort}${pagePath}\n`));
|
|
3828
|
+
}
|
|
3829
|
+
enqueueHeadedReload = async (file, testNamePattern)=>headedReloadQueue.enqueue(async ()=>{
|
|
3830
|
+
if (fatalErrorRef.current) return;
|
|
3831
|
+
await reloadTestFileAndWait(file, testNamePattern);
|
|
3832
|
+
});
|
|
3833
|
+
let testTime = 0;
|
|
3834
|
+
if (currentTestFiles.length > 0) {
|
|
3835
|
+
const testStart = Date.now();
|
|
3836
|
+
try {
|
|
3837
|
+
await waitForRunnerFramesReady(currentTestFiles.map((file)=>file.testPath));
|
|
3838
|
+
for (const file of currentTestFiles){
|
|
3839
|
+
await enqueueHeadedReload(file);
|
|
3840
|
+
if (fatalErrorRef.current) break;
|
|
3841
|
+
}
|
|
3842
|
+
} catch (error) {
|
|
3843
|
+
fatalErrorRef.current = fatalErrorRef.current ?? toError(error);
|
|
3844
|
+
}
|
|
3845
|
+
testTime = Date.now() - testStart;
|
|
3846
|
+
}
|
|
3847
|
+
let watchSession;
|
|
3848
|
+
if (isWatchMode) {
|
|
3849
|
+
const pendingTestNamePatterns = new Map();
|
|
3850
|
+
const runScope = async (testPaths)=>{
|
|
3851
|
+
const cycleScope = claimHeadedCycleScope(testPaths, currentTestFiles, pendingTestNamePatterns);
|
|
3852
|
+
for (const { file, testNamePattern } of cycleScope)await enqueueHeadedReload(file, testNamePattern);
|
|
3853
|
+
};
|
|
3854
|
+
const refreshHostConfig = async ()=>{
|
|
3855
|
+
const refreshedHostOptions = {
|
|
3856
|
+
...hostOptions,
|
|
3857
|
+
snapshot: {
|
|
3858
|
+
updateSnapshot: context.snapshotManager.options.updateSnapshot
|
|
3859
|
+
}
|
|
3860
|
+
};
|
|
3861
|
+
runtime.setContainerOptions(refreshedHostOptions);
|
|
3862
|
+
await rpcManager.updateHostConfig(refreshedHostOptions);
|
|
3863
|
+
};
|
|
3864
|
+
watchSignals.setDispatchRerun(async ()=>{
|
|
3865
|
+
const [, newProjectEntries] = await Promise.all([
|
|
3866
|
+
refreshHostConfig(),
|
|
3867
|
+
collectProjectEntries()
|
|
3868
|
+
]);
|
|
3869
|
+
const rerunPlan = planWatchRerun({
|
|
3870
|
+
projectEntries: newProjectEntries,
|
|
3871
|
+
previousTestFiles: watchState.lastTestFiles,
|
|
3872
|
+
affectedTestFiles: drainPendingAffectedTestFiles(watchState)
|
|
3873
|
+
});
|
|
3874
|
+
if (rerunPlan.filesChanged) {
|
|
3875
|
+
const deletedTestPaths = collectDeletedTestPaths(watchState.lastTestFiles, rerunPlan.currentTestFiles);
|
|
3876
|
+
if (deletedTestPaths.length > 0) context.updateReporterResultState([], [], deletedTestPaths);
|
|
3877
|
+
watchState.lastTestFiles = rerunPlan.currentTestFiles;
|
|
3878
|
+
currentTestFiles = rerunPlan.currentTestFiles;
|
|
3879
|
+
await rpcManager.notifyTestFileUpdate(currentTestFiles);
|
|
3880
|
+
if (0 === currentTestFiles.length) {
|
|
3881
|
+
logger.log(color.cyan('No browser test files remain after update.\n'));
|
|
3882
|
+
logWatchReady();
|
|
3883
|
+
return;
|
|
3884
|
+
}
|
|
3885
|
+
await waitForRunnerFramesReady(currentTestFiles.map((file)=>file.testPath));
|
|
3886
|
+
}
|
|
3887
|
+
if (rerunPlan.normalizedAffectedTestFiles.length > 0) {
|
|
3888
|
+
logger.log(color.cyan(`Re-running ${rerunPlan.normalizedAffectedTestFiles.length} affected test file(s)...\n`));
|
|
3889
|
+
await watchSignals.signalInvalidation(rerunPlan.normalizedAffectedTestFiles);
|
|
3890
|
+
return;
|
|
3891
|
+
}
|
|
3892
|
+
if (!rerunPlan.filesChanged) logger.log(color.cyan('Tests will be re-executed automatically\n'));
|
|
3893
|
+
logWatchReady();
|
|
3894
|
+
});
|
|
3895
|
+
runUiRequestedRerun = async (file, testNamePattern)=>{
|
|
3896
|
+
await refreshHostConfig();
|
|
3897
|
+
await watchSignals.signalInvalidation([
|
|
3898
|
+
file.testPath
|
|
3899
|
+
], ()=>{
|
|
3900
|
+
if (void 0 === testNamePattern) pendingTestNamePatterns.delete(normalize(file.testPath));
|
|
3901
|
+
else pendingTestNamePatterns.set(normalize(file.testPath), testNamePattern);
|
|
3902
|
+
});
|
|
3903
|
+
await watchSignals.awaitSignalledCycle();
|
|
3904
|
+
};
|
|
3905
|
+
watchSession = createWatchSession(runScope);
|
|
3906
|
+
}
|
|
3907
|
+
const closeContainerRuntime = isWatchMode ? void 0 : async ()=>{
|
|
3908
|
+
try {
|
|
3909
|
+
await containerPage.close();
|
|
3910
|
+
} catch {}
|
|
3911
|
+
try {
|
|
3912
|
+
await containerContext.close();
|
|
3913
|
+
} catch {}
|
|
3914
|
+
await destroyRuntime();
|
|
4241
3915
|
};
|
|
4242
|
-
|
|
4243
|
-
|
|
4244
|
-
|
|
4245
|
-
|
|
4246
|
-
|
|
4247
|
-
|
|
4248
|
-
|
|
4249
|
-
|
|
4250
|
-
const
|
|
4251
|
-
|
|
4252
|
-
|
|
4253
|
-
|
|
4254
|
-
|
|
4255
|
-
|
|
4256
|
-
|
|
4257
|
-
|
|
4258
|
-
const sink = runnerSinks.get(projectName);
|
|
4259
|
-
if (!sink) throw new Error(`No runner event sink for project "${projectName}"`);
|
|
4260
|
-
return sink;
|
|
4261
|
-
};
|
|
4262
|
-
const silentConsoleController = createSilentConsoleController({
|
|
4263
|
-
runtimeConfig: {
|
|
4264
|
-
silent: context.normalizedConfig.silent,
|
|
4265
|
-
disableConsoleIntercept: context.normalizedConfig.disableConsoleIntercept
|
|
4266
|
-
},
|
|
4267
|
-
emitInterceptedLog: (log)=>sinkForProjectName(log.project).onConsoleLog(log),
|
|
4268
|
-
writeOriginalLog: ()=>{}
|
|
3916
|
+
return {
|
|
3917
|
+
testTime,
|
|
3918
|
+
watchSession,
|
|
3919
|
+
close: closeContainerRuntime
|
|
3920
|
+
};
|
|
3921
|
+
};
|
|
3922
|
+
const DEFAULT_MAX_HEADLESS_WORKERS = 12;
|
|
3923
|
+
const resolveHeadlessWorkerCount = ({ command, maxWorkers, totalTasks, numCpus = browser_getNumCpus() })=>{
|
|
3924
|
+
const base = Math.max(Math.min(DEFAULT_MAX_HEADLESS_WORKERS, numCpus - 1), 1);
|
|
3925
|
+
return resolveWorkerCount({
|
|
3926
|
+
command,
|
|
3927
|
+
maxWorkers,
|
|
3928
|
+
totalTasks,
|
|
3929
|
+
recommended: base,
|
|
3930
|
+
watchRecommended: Math.max(Math.floor(base / 2), 1),
|
|
3931
|
+
numCpus
|
|
4269
3932
|
});
|
|
4270
|
-
|
|
4271
|
-
|
|
4272
|
-
|
|
4273
|
-
|
|
4274
|
-
|
|
4275
|
-
|
|
4276
|
-
|
|
4277
|
-
|
|
4278
|
-
|
|
3933
|
+
};
|
|
3934
|
+
const getHeadlessConcurrency = (context, totalTests)=>resolveHeadlessWorkerCount({
|
|
3935
|
+
command: context.command,
|
|
3936
|
+
maxWorkers: context.normalizedConfig.pool.maxWorkers,
|
|
3937
|
+
totalTasks: totalTests
|
|
3938
|
+
});
|
|
3939
|
+
const attachHeadlessRunnerTransport = async (page, handlers)=>{
|
|
3940
|
+
await page.exposeFunction(DISPATCH_MESSAGE_TYPE, handlers.onDispatchMessage);
|
|
3941
|
+
await page.exposeFunction(DISPATCH_RPC_BRIDGE_NAME, handlers.onDispatchRpc);
|
|
3942
|
+
};
|
|
3943
|
+
const createCancelSignal = ()=>{
|
|
3944
|
+
let settled = false;
|
|
3945
|
+
let resolveSignal = ()=>{};
|
|
3946
|
+
const signal = new Promise((resolve)=>{
|
|
3947
|
+
resolveSignal = ()=>{
|
|
3948
|
+
if (!settled) {
|
|
3949
|
+
settled = true;
|
|
3950
|
+
resolve();
|
|
4279
3951
|
}
|
|
4280
|
-
}
|
|
4281
|
-
|
|
4282
|
-
|
|
4283
|
-
|
|
4284
|
-
|
|
4285
|
-
|
|
4286
|
-
|
|
4287
|
-
|
|
4288
|
-
|
|
4289
|
-
|
|
4290
|
-
|
|
4291
|
-
|
|
3952
|
+
};
|
|
3953
|
+
});
|
|
3954
|
+
return {
|
|
3955
|
+
signal,
|
|
3956
|
+
resolve: resolveSignal
|
|
3957
|
+
};
|
|
3958
|
+
};
|
|
3959
|
+
const createRunSession = (token)=>{
|
|
3960
|
+
const { signal, resolve } = createCancelSignal();
|
|
3961
|
+
return {
|
|
3962
|
+
token,
|
|
3963
|
+
cancelled: false,
|
|
3964
|
+
cancelSignal: signal,
|
|
3965
|
+
signalCancel: resolve
|
|
3966
|
+
};
|
|
3967
|
+
};
|
|
3968
|
+
class RunSessionLifecycle {
|
|
3969
|
+
currentToken = 0;
|
|
3970
|
+
active = null;
|
|
3971
|
+
get activeSession() {
|
|
3972
|
+
return this.active;
|
|
3973
|
+
}
|
|
3974
|
+
get activeToken() {
|
|
3975
|
+
return this.currentToken;
|
|
3976
|
+
}
|
|
3977
|
+
createSession(factory) {
|
|
3978
|
+
const session = factory(++this.currentToken);
|
|
3979
|
+
this.active = session;
|
|
3980
|
+
return session;
|
|
3981
|
+
}
|
|
3982
|
+
isTokenActive(token) {
|
|
3983
|
+
return token === this.currentToken;
|
|
3984
|
+
}
|
|
3985
|
+
isTokenStale(token) {
|
|
3986
|
+
return !this.isTokenActive(token);
|
|
3987
|
+
}
|
|
3988
|
+
invalidateActiveToken() {
|
|
3989
|
+
this.currentToken += 1;
|
|
3990
|
+
return this.currentToken;
|
|
3991
|
+
}
|
|
3992
|
+
clearIfActive(session) {
|
|
3993
|
+
if (this.active === session) this.active = null;
|
|
3994
|
+
}
|
|
3995
|
+
async cancel(session, options) {
|
|
3996
|
+
const waitForDone = options?.waitForDone ?? true;
|
|
3997
|
+
if (!session.cancelled) {
|
|
3998
|
+
session.cancelled = true;
|
|
3999
|
+
session.signalCancel();
|
|
4000
|
+
await options?.onCancel?.(session);
|
|
4001
|
+
}
|
|
4002
|
+
if (waitForDone) await session.done?.catch(()=>{});
|
|
4003
|
+
}
|
|
4004
|
+
async cancelActive(options) {
|
|
4005
|
+
if (!this.active) return;
|
|
4006
|
+
await this.cancel(this.active, options);
|
|
4007
|
+
}
|
|
4008
|
+
}
|
|
4009
|
+
class RunnerSessionRegistry {
|
|
4010
|
+
nextId = 0;
|
|
4011
|
+
sessionsById = new Map();
|
|
4012
|
+
sessionIdByTestFile = new Map();
|
|
4013
|
+
register(input) {
|
|
4014
|
+
const id = input.id ?? `runner-session-${++this.nextId}`;
|
|
4015
|
+
const createdAt = input.createdAt ?? Date.now();
|
|
4016
|
+
const record = {
|
|
4017
|
+
...input,
|
|
4018
|
+
id,
|
|
4019
|
+
createdAt
|
|
4020
|
+
};
|
|
4021
|
+
this.sessionsById.set(id, record);
|
|
4022
|
+
this.sessionIdByTestFile.set(record.testFile, id);
|
|
4023
|
+
return record;
|
|
4024
|
+
}
|
|
4025
|
+
getById(id) {
|
|
4026
|
+
return this.sessionsById.get(id);
|
|
4027
|
+
}
|
|
4028
|
+
getByTestFile(testFile) {
|
|
4029
|
+
const id = this.sessionIdByTestFile.get(testFile);
|
|
4030
|
+
if (!id) return;
|
|
4031
|
+
return this.sessionsById.get(id);
|
|
4032
|
+
}
|
|
4033
|
+
list() {
|
|
4034
|
+
return Array.from(this.sessionsById.values());
|
|
4035
|
+
}
|
|
4036
|
+
listByRunToken(runToken) {
|
|
4037
|
+
return this.list().filter((session)=>session.runToken === runToken);
|
|
4038
|
+
}
|
|
4039
|
+
deleteById(id) {
|
|
4040
|
+
const record = this.sessionsById.get(id);
|
|
4041
|
+
if (!record) return false;
|
|
4042
|
+
this.sessionsById.delete(id);
|
|
4043
|
+
if (this.sessionIdByTestFile.get(record.testFile) === id) this.sessionIdByTestFile.delete(record.testFile);
|
|
4044
|
+
return true;
|
|
4045
|
+
}
|
|
4046
|
+
deleteByTestFile(testFile) {
|
|
4047
|
+
const id = this.sessionIdByTestFile.get(testFile);
|
|
4048
|
+
if (!id) return false;
|
|
4049
|
+
return this.deleteById(id);
|
|
4050
|
+
}
|
|
4051
|
+
clear() {
|
|
4052
|
+
this.sessionsById.clear();
|
|
4053
|
+
this.sessionIdByTestFile.clear();
|
|
4054
|
+
}
|
|
4055
|
+
}
|
|
4056
|
+
const createHeadlessScheduler = async ({ context, browser, browserLaunchOptions, projectServers, allTestFiles, projectRuntimeConfigs, hostOptions, watchState, isWatchMode, createDispatchRouter, handlers: { handleFatal, handleTestFileComplete }, watchSignals, setDispatchPageResolver, createWatchSession, collectProjectEntries, logWatchReady, destroyRuntime })=>{
|
|
4057
|
+
const viewportByProject = mapViewportByProject(projectRuntimeConfigs);
|
|
4058
|
+
const runLifecycle = new RunSessionLifecycle();
|
|
4059
|
+
const sessionRegistry = new RunnerSessionRegistry();
|
|
4060
|
+
setDispatchPageResolver((target)=>({
|
|
4061
|
+
runnerPage: target?.sessionId ? sessionRegistry.getById(target.sessionId)?.page : void 0
|
|
4062
|
+
}));
|
|
4063
|
+
let dispatchRequestCounter = 0;
|
|
4064
|
+
const nextDispatchRequestId = (namespace)=>`${namespace}-${++dispatchRequestCounter}`;
|
|
4065
|
+
const closeContextSafely = async (browserContext)=>{
|
|
4066
|
+
try {
|
|
4067
|
+
await browserContext.close();
|
|
4068
|
+
} catch {}
|
|
4069
|
+
};
|
|
4070
|
+
const cancelRun = async (run, waitForDone = true)=>{
|
|
4071
|
+
await runLifecycle.cancel(run, {
|
|
4072
|
+
waitForDone,
|
|
4073
|
+
onCancel: async (session)=>{
|
|
4074
|
+
await Promise.all(Array.from(session.contexts).map((browserContext)=>closeContextSafely(browserContext)));
|
|
4075
|
+
}
|
|
4076
|
+
});
|
|
4077
|
+
};
|
|
4078
|
+
const dispatchRouter = createDispatchRouter({
|
|
4079
|
+
isRunTokenStale: (runToken)=>runLifecycle.isTokenStale(runToken),
|
|
4080
|
+
onStale: (request)=>{
|
|
4081
|
+
if ("runner" === request.namespace) logger.debug(`[Headless] Dropped stale message "${request.method}" for ${request.target?.testFile ?? 'unknown'}`);
|
|
4292
4082
|
}
|
|
4083
|
+
});
|
|
4084
|
+
const dispatchRunnerMessage = async (run, file, sessionId, message)=>{
|
|
4085
|
+
const response = await dispatchRouter.dispatch({
|
|
4086
|
+
requestId: nextDispatchRequestId("runner"),
|
|
4087
|
+
runToken: run.token,
|
|
4088
|
+
namespace: "runner",
|
|
4089
|
+
method: message.type,
|
|
4090
|
+
args: 'payload' in message ? message.payload : void 0,
|
|
4091
|
+
target: {
|
|
4092
|
+
sessionId,
|
|
4093
|
+
testFile: file.testPath,
|
|
4094
|
+
projectName: file.projectName
|
|
4095
|
+
}
|
|
4096
|
+
});
|
|
4097
|
+
if (response.stale) return;
|
|
4098
|
+
if (response.error) throw new Error(response.error);
|
|
4293
4099
|
};
|
|
4294
|
-
const
|
|
4295
|
-
if (
|
|
4296
|
-
|
|
4297
|
-
|
|
4298
|
-
|
|
4299
|
-
|
|
4100
|
+
const runSingleFile = async (run, file)=>{
|
|
4101
|
+
if (run.cancelled || runLifecycle.isTokenStale(run.token)) return;
|
|
4102
|
+
const viewport = viewportByProject.get(file.projectName);
|
|
4103
|
+
const browserContext = await browser.newContext({
|
|
4104
|
+
providerOptions: browserLaunchOptions.providerOptions,
|
|
4105
|
+
viewport: viewport ?? null
|
|
4106
|
+
});
|
|
4107
|
+
run.contexts.add(browserContext);
|
|
4108
|
+
let page = null;
|
|
4109
|
+
let sessionId = null;
|
|
4110
|
+
let settled = false;
|
|
4111
|
+
let resolveDone = null;
|
|
4112
|
+
const markDone = ()=>{
|
|
4113
|
+
if (!settled) {
|
|
4114
|
+
settled = true;
|
|
4115
|
+
resolveDone?.();
|
|
4116
|
+
}
|
|
4117
|
+
};
|
|
4118
|
+
const donePromise = new Promise((resolve)=>{
|
|
4119
|
+
resolveDone = resolve;
|
|
4120
|
+
});
|
|
4121
|
+
const crashDeferred = createDeferredPromise();
|
|
4122
|
+
const onPageDead = (reason)=>{
|
|
4123
|
+
if (settled || run.cancelled || !runLifecycle.isTokenActive(run.token)) return;
|
|
4124
|
+
settled = true;
|
|
4125
|
+
crashDeferred.resolve(reason);
|
|
4126
|
+
};
|
|
4127
|
+
try {
|
|
4128
|
+
page = await browserContext.newPage();
|
|
4129
|
+
page.on('crash', ()=>onPageDead(`Browser page crashed while running ${file.testPath}.`));
|
|
4130
|
+
page.on('close', ()=>onPageDead(`Browser page closed unexpectedly while running ${file.testPath}.`));
|
|
4131
|
+
const session = sessionRegistry.register({
|
|
4132
|
+
testFile: file.testPath,
|
|
4133
|
+
projectName: file.projectName,
|
|
4134
|
+
runToken: run.token,
|
|
4135
|
+
mode: 'headless-page',
|
|
4136
|
+
context: browserContext,
|
|
4137
|
+
page
|
|
4138
|
+
});
|
|
4139
|
+
sessionId = session.id;
|
|
4140
|
+
await attachHeadlessRunnerTransport(page, {
|
|
4141
|
+
onDispatchMessage: async (message)=>{
|
|
4142
|
+
try {
|
|
4143
|
+
await dispatchRunnerMessage(run, file, session.id, message);
|
|
4144
|
+
if ('file-complete' === message.type || 'complete' === message.type) markDone();
|
|
4145
|
+
else if ('fatal' === message.type) {
|
|
4146
|
+
markDone();
|
|
4147
|
+
await cancelRun(run, false);
|
|
4148
|
+
}
|
|
4149
|
+
} catch (error) {
|
|
4150
|
+
const formatted = toError(error);
|
|
4151
|
+
await handleFatal({
|
|
4152
|
+
message: formatted.message,
|
|
4153
|
+
stack: formatted.stack
|
|
4154
|
+
});
|
|
4155
|
+
markDone();
|
|
4156
|
+
await cancelRun(run, false);
|
|
4157
|
+
}
|
|
4300
4158
|
},
|
|
4301
|
-
|
|
4159
|
+
onDispatchRpc: async (request)=>dispatchRouter.dispatch({
|
|
4160
|
+
...request,
|
|
4161
|
+
runToken: run.token,
|
|
4162
|
+
target: {
|
|
4163
|
+
sessionId: session.id,
|
|
4164
|
+
testFile: file.testPath,
|
|
4165
|
+
projectName: file.projectName,
|
|
4166
|
+
...request.target
|
|
4167
|
+
}
|
|
4168
|
+
})
|
|
4302
4169
|
});
|
|
4303
|
-
|
|
4304
|
-
|
|
4170
|
+
const inlineOptions = {
|
|
4171
|
+
...hostOptions,
|
|
4172
|
+
snapshot: {
|
|
4173
|
+
updateSnapshot: context.snapshotManager.options.updateSnapshot
|
|
4174
|
+
},
|
|
4175
|
+
testFile: file.testPath,
|
|
4176
|
+
runId: `${run.token}:${session.id}`
|
|
4177
|
+
};
|
|
4178
|
+
const serializedOptions = serializeForInlineScript(inlineOptions);
|
|
4179
|
+
await page.addInitScript(`window.__RSTEST_BROWSER_OPTIONS__ = ${serializedOptions};`);
|
|
4180
|
+
const projectServer = projectServers.get(file.projectName);
|
|
4181
|
+
if (!projectServer) throw new Error(`No browser dev server for project "${file.projectName}" (test file: ${file.testPath}).`);
|
|
4182
|
+
await page.goto(`http://localhost:${projectServer.port}/runner.html`, {
|
|
4183
|
+
waitUntil: 'load'
|
|
4184
|
+
});
|
|
4185
|
+
const state = await Promise.race([
|
|
4186
|
+
donePromise.then(()=>({
|
|
4187
|
+
type: 'done'
|
|
4188
|
+
})),
|
|
4189
|
+
crashDeferred.promise.then((reason)=>({
|
|
4190
|
+
type: 'crash',
|
|
4191
|
+
reason
|
|
4192
|
+
})),
|
|
4193
|
+
run.cancelSignal.then(()=>({
|
|
4194
|
+
type: 'cancelled'
|
|
4195
|
+
}))
|
|
4196
|
+
]);
|
|
4197
|
+
if ('cancelled' === state.type) return;
|
|
4198
|
+
if ('crash' === state.type && runLifecycle.isTokenActive(run.token) && !run.cancelled) {
|
|
4199
|
+
await handleFatal({
|
|
4200
|
+
message: state.reason
|
|
4201
|
+
});
|
|
4202
|
+
await cancelRun(run, false);
|
|
4203
|
+
}
|
|
4204
|
+
} catch (error) {
|
|
4205
|
+
if (runLifecycle.isTokenActive(run.token) && !run.cancelled) {
|
|
4206
|
+
const formatted = toError(error);
|
|
4207
|
+
await handleFatal({
|
|
4208
|
+
message: formatted.message,
|
|
4209
|
+
stack: formatted.stack
|
|
4210
|
+
});
|
|
4211
|
+
await cancelRun(run, false);
|
|
4212
|
+
}
|
|
4213
|
+
} finally{
|
|
4214
|
+
const abandoned = run.cancelled || runLifecycle.isTokenStale(run.token);
|
|
4215
|
+
const teardown = async ()=>{
|
|
4216
|
+
if (page) try {
|
|
4217
|
+
await page.close();
|
|
4218
|
+
} catch {}
|
|
4219
|
+
await closeContextSafely(browserContext);
|
|
4220
|
+
};
|
|
4221
|
+
if (sessionId) sessionRegistry.deleteById(sessionId);
|
|
4222
|
+
run.contexts.delete(browserContext);
|
|
4223
|
+
if (abandoned) teardown();
|
|
4224
|
+
else await teardown();
|
|
4305
4225
|
}
|
|
4306
|
-
|
|
4307
|
-
|
|
4308
|
-
|
|
4309
|
-
|
|
4310
|
-
|
|
4226
|
+
};
|
|
4227
|
+
const makeSkippedFileResult = (file)=>({
|
|
4228
|
+
testId: getFileTaskId(file.testPath),
|
|
4229
|
+
status: 'skip',
|
|
4230
|
+
name: '',
|
|
4231
|
+
testPath: file.testPath,
|
|
4232
|
+
project: file.projectName,
|
|
4233
|
+
results: []
|
|
4311
4234
|
});
|
|
4235
|
+
const runFilesWithPool = async (files)=>{
|
|
4236
|
+
if (0 === files.length) return;
|
|
4237
|
+
const previous = runLifecycle.activeSession;
|
|
4238
|
+
if (previous) await cancelRun(previous);
|
|
4239
|
+
const run = runLifecycle.createSession((token)=>({
|
|
4240
|
+
...createRunSession(token),
|
|
4241
|
+
contexts: new Set()
|
|
4242
|
+
}));
|
|
4243
|
+
const queue = [
|
|
4244
|
+
...files
|
|
4245
|
+
];
|
|
4246
|
+
const concurrency = getHeadlessConcurrency(context, queue.length);
|
|
4247
|
+
const bail = context.normalizedConfig.bail;
|
|
4248
|
+
const worker = async ()=>{
|
|
4249
|
+
while(queue.length > 0 && !run.cancelled && runLifecycle.isTokenActive(run.token)){
|
|
4250
|
+
if (bail && context.stateManager.getCountOfFailedTests() >= bail) {
|
|
4251
|
+
let skipped = queue.shift();
|
|
4252
|
+
while(skipped){
|
|
4253
|
+
await handleTestFileComplete(makeSkippedFileResult(skipped));
|
|
4254
|
+
skipped = queue.shift();
|
|
4255
|
+
}
|
|
4256
|
+
return;
|
|
4257
|
+
}
|
|
4258
|
+
const next = queue.shift();
|
|
4259
|
+
if (!next) return;
|
|
4260
|
+
await runSingleFile(run, next);
|
|
4261
|
+
}
|
|
4262
|
+
};
|
|
4263
|
+
run.done = Promise.all(Array.from({
|
|
4264
|
+
length: Math.min(queue.length, Math.max(concurrency, 1))
|
|
4265
|
+
}, ()=>worker())).then(()=>{});
|
|
4266
|
+
await run.done;
|
|
4267
|
+
runLifecycle.clearIfActive(run);
|
|
4312
4268
|
};
|
|
4313
|
-
const
|
|
4314
|
-
|
|
4315
|
-
|
|
4269
|
+
const testStart = Date.now();
|
|
4270
|
+
await runFilesWithPool(allTestFiles);
|
|
4271
|
+
const testTime = Date.now() - testStart;
|
|
4272
|
+
let watchSession;
|
|
4273
|
+
if (isWatchMode) {
|
|
4274
|
+
const runScope = async (testPaths)=>{
|
|
4275
|
+
const pathSet = new Set(testPaths.map((testPath)=>normalize(testPath)));
|
|
4276
|
+
await runFilesWithPool(watchState.lastTestFiles.filter((file)=>pathSet.has(file.testPath)));
|
|
4277
|
+
};
|
|
4278
|
+
watchSignals.setInterrupt(async ()=>{
|
|
4279
|
+
const active = runLifecycle.activeSession;
|
|
4280
|
+
if (!active || active.cancelled) return;
|
|
4281
|
+
runLifecycle.invalidateActiveToken();
|
|
4282
|
+
await runLifecycle.cancel(active, {
|
|
4283
|
+
waitForDone: false
|
|
4284
|
+
});
|
|
4285
|
+
});
|
|
4286
|
+
watchSignals.setDispatchRerun(async ()=>{
|
|
4287
|
+
const newProjectEntries = await collectProjectEntries();
|
|
4288
|
+
const rerunPlan = planWatchRerun({
|
|
4289
|
+
projectEntries: newProjectEntries,
|
|
4290
|
+
previousTestFiles: watchState.lastTestFiles,
|
|
4291
|
+
affectedTestFiles: drainPendingAffectedTestFiles(watchState)
|
|
4292
|
+
});
|
|
4293
|
+
if (rerunPlan.filesChanged) {
|
|
4294
|
+
const deletedTestPaths = collectDeletedTestPaths(watchState.lastTestFiles, rerunPlan.currentTestFiles);
|
|
4295
|
+
if (deletedTestPaths.length > 0) context.updateReporterResultState([], [], deletedTestPaths);
|
|
4296
|
+
watchState.lastTestFiles = rerunPlan.currentTestFiles;
|
|
4297
|
+
if (0 === rerunPlan.currentTestFiles.length) {
|
|
4298
|
+
logger.log(color.cyan('No browser test files remain after update.\n'));
|
|
4299
|
+
await watchSignals.signalInvalidation([]);
|
|
4300
|
+
return;
|
|
4301
|
+
}
|
|
4302
|
+
logger.log(color.cyan(`Test file set changed, re-running ${rerunPlan.currentTestFiles.length} file(s)...\n`));
|
|
4303
|
+
await watchSignals.signalInvalidation([
|
|
4304
|
+
...new Set(rerunPlan.currentTestFiles.map((file)=>file.testPath))
|
|
4305
|
+
]);
|
|
4306
|
+
return;
|
|
4307
|
+
}
|
|
4308
|
+
if (0 === rerunPlan.affectedTestFiles.length) {
|
|
4309
|
+
logger.log(color.cyan('No affected browser test files detected, skipping re-run.\n'));
|
|
4310
|
+
logWatchReady();
|
|
4311
|
+
return;
|
|
4312
|
+
}
|
|
4313
|
+
logger.log(color.cyan(`Re-running ${rerunPlan.affectedTestFiles.length} affected test file(s)...\n`));
|
|
4314
|
+
await watchSignals.signalInvalidation([
|
|
4315
|
+
...new Set(rerunPlan.affectedTestFiles.map((file)=>file.testPath))
|
|
4316
|
+
]);
|
|
4317
|
+
});
|
|
4318
|
+
watchSession = createWatchSession(runScope);
|
|
4319
|
+
}
|
|
4320
|
+
const closeHeadlessRuntime = isWatchMode ? void 0 : async ()=>{
|
|
4321
|
+
sessionRegistry.clear();
|
|
4322
|
+
await destroyRuntime();
|
|
4316
4323
|
};
|
|
4317
|
-
|
|
4318
|
-
|
|
4319
|
-
|
|
4324
|
+
return {
|
|
4325
|
+
testTime,
|
|
4326
|
+
watchSession,
|
|
4327
|
+
close: closeHeadlessRuntime
|
|
4320
4328
|
};
|
|
4321
|
-
|
|
4322
|
-
|
|
4323
|
-
|
|
4324
|
-
|
|
4325
|
-
|
|
4326
|
-
|
|
4327
|
-
|
|
4328
|
-
|
|
4329
|
-
|
|
4330
|
-
|
|
4329
|
+
};
|
|
4330
|
+
const isRecord = (value)=>'object' == typeof value && null !== value;
|
|
4331
|
+
const readString = (value, key, label)=>{
|
|
4332
|
+
const result = value[key];
|
|
4333
|
+
if ('string' != typeof result) throw new Error(`Invalid browser RPC request: ${label} must be a string`);
|
|
4334
|
+
return result;
|
|
4335
|
+
};
|
|
4336
|
+
const readUnknownArray = (value, key, label)=>{
|
|
4337
|
+
const result = value[key];
|
|
4338
|
+
if (!Array.isArray(result)) throw new Error(`Invalid browser RPC request: ${label} must be an array`);
|
|
4339
|
+
return result;
|
|
4340
|
+
};
|
|
4341
|
+
const parseBrowserLocatorIR = (value, label)=>{
|
|
4342
|
+
if (!isRecord(value)) throw new Error(`Invalid browser RPC request: ${label} must be an object`);
|
|
4343
|
+
const steps = value.steps;
|
|
4344
|
+
if (!Array.isArray(steps)) throw new Error(`Invalid browser RPC request: ${label}.steps must be an array`);
|
|
4345
|
+
return value;
|
|
4346
|
+
};
|
|
4347
|
+
const validateBrowserRpcRequest = (payload)=>{
|
|
4348
|
+
if (!isRecord(payload)) throw new Error('Invalid browser RPC request: payload must be an object');
|
|
4349
|
+
const kind = readString(payload, 'kind', 'kind');
|
|
4350
|
+
if ('locator' !== kind && 'expect' !== kind && 'config' !== kind) throw new Error(`Invalid browser RPC request: unsupported kind ${JSON.stringify(kind)}`);
|
|
4351
|
+
const request = {
|
|
4352
|
+
id: readString(payload, 'id', 'id'),
|
|
4353
|
+
testPath: readString(payload, 'testPath', 'testPath'),
|
|
4354
|
+
runId: readString(payload, 'runId', 'runId'),
|
|
4355
|
+
kind,
|
|
4356
|
+
locator: parseBrowserLocatorIR(payload.locator, 'locator'),
|
|
4357
|
+
method: readString(payload, 'method', 'method'),
|
|
4358
|
+
args: readUnknownArray(payload, 'args', 'args')
|
|
4331
4359
|
};
|
|
4332
|
-
const
|
|
4333
|
-
|
|
4334
|
-
|
|
4360
|
+
const isNot = payload.isNot;
|
|
4361
|
+
if (void 0 !== isNot) {
|
|
4362
|
+
if ('boolean' != typeof isNot) throw new Error('Invalid browser RPC request: isNot must be a boolean');
|
|
4363
|
+
request.isNot = isNot;
|
|
4364
|
+
}
|
|
4365
|
+
const timeout = payload.timeout;
|
|
4366
|
+
if (void 0 !== timeout) {
|
|
4367
|
+
if ('number' != typeof timeout) throw new Error('Invalid browser RPC request: timeout must be a number');
|
|
4368
|
+
request.timeout = timeout;
|
|
4369
|
+
}
|
|
4370
|
+
return request;
|
|
4371
|
+
};
|
|
4372
|
+
const normalizeJavaScriptUrl = (value, options)=>{
|
|
4373
|
+
try {
|
|
4374
|
+
const url = options?.origin ? new URL(value, options.origin) : new URL(value);
|
|
4375
|
+
if ('http:' !== url.protocol && 'https:' !== url.protocol) return null;
|
|
4376
|
+
url.search = '';
|
|
4377
|
+
url.hash = '';
|
|
4378
|
+
return url.toString();
|
|
4379
|
+
} catch {
|
|
4380
|
+
return null;
|
|
4381
|
+
}
|
|
4382
|
+
};
|
|
4383
|
+
const resolveInlineSourceMap = (code)=>{
|
|
4384
|
+
const converter = convert_source_map.fromSource(code);
|
|
4385
|
+
if (!converter) return null;
|
|
4386
|
+
return converter.toObject();
|
|
4387
|
+
};
|
|
4388
|
+
const fetchSourceMap = async (jsUrl, fetcher)=>{
|
|
4389
|
+
const jsResponse = await fetcher(jsUrl);
|
|
4390
|
+
if (!jsResponse.ok) return null;
|
|
4391
|
+
const code = await jsResponse.text();
|
|
4392
|
+
const inlineMap = resolveInlineSourceMap(code);
|
|
4393
|
+
if (inlineMap) return inlineMap;
|
|
4394
|
+
const mapResponse = await fetcher(`${jsUrl}.map`);
|
|
4395
|
+
if (!mapResponse.ok) return null;
|
|
4396
|
+
return await mapResponse.json();
|
|
4397
|
+
};
|
|
4398
|
+
const loadSourceMapWithCache = async ({ jsUrl, cache, force = false, origin, fetcher = fetch })=>{
|
|
4399
|
+
const normalizedUrl = normalizeJavaScriptUrl(jsUrl, {
|
|
4400
|
+
origin
|
|
4401
|
+
});
|
|
4402
|
+
if (!normalizedUrl) return null;
|
|
4403
|
+
if (!force && cache.has(normalizedUrl)) return cache.get(normalizedUrl) ?? null;
|
|
4404
|
+
try {
|
|
4405
|
+
const sourceMap = await fetchSourceMap(normalizedUrl, fetcher);
|
|
4406
|
+
cache.set(normalizedUrl, sourceMap);
|
|
4407
|
+
return sourceMap;
|
|
4408
|
+
} catch {
|
|
4409
|
+
cache.set(normalizedUrl, null);
|
|
4410
|
+
return null;
|
|
4411
|
+
}
|
|
4412
|
+
};
|
|
4413
|
+
const watchContext = {
|
|
4414
|
+
runtime: null,
|
|
4415
|
+
cleanupRegistered: false,
|
|
4416
|
+
cleanupPromise: null
|
|
4417
|
+
};
|
|
4418
|
+
const runWatchRuntimeTeardown = (state, destroy)=>{
|
|
4419
|
+
if (state.cleanupPromise) return state.cleanupPromise;
|
|
4420
|
+
state.cleanupPromise = (async ()=>{
|
|
4421
|
+
if (!state.runtime) return;
|
|
4422
|
+
await destroy(state.runtime);
|
|
4423
|
+
state.runtime = null;
|
|
4424
|
+
})();
|
|
4425
|
+
return state.cleanupPromise.finally(()=>{
|
|
4426
|
+
state.cleanupPromise = null;
|
|
4427
|
+
});
|
|
4428
|
+
};
|
|
4429
|
+
const cleanupWatchRuntime = ()=>runWatchRuntimeTeardown(watchContext, destroyBrowserRuntime);
|
|
4430
|
+
const registerWatchCleanup = (embedded)=>{
|
|
4431
|
+
if (watchContext.cleanupRegistered) return;
|
|
4432
|
+
watchContext.cleanupRegistered = true;
|
|
4433
|
+
if (embedded) return;
|
|
4434
|
+
for (const signal of FATAL_SIGNALS)process.once(signal, ()=>{
|
|
4435
|
+
cleanupWatchRuntime();
|
|
4436
|
+
});
|
|
4437
|
+
process.once('exit', ()=>{
|
|
4438
|
+
cleanupWatchRuntime();
|
|
4439
|
+
});
|
|
4440
|
+
};
|
|
4441
|
+
const createWatchSignals = (onInvalidate)=>{
|
|
4442
|
+
let dispatchRerun;
|
|
4443
|
+
let interruptInFlightRun;
|
|
4444
|
+
let signalledCycle;
|
|
4445
|
+
return {
|
|
4446
|
+
setDispatchRerun (fn) {
|
|
4447
|
+
dispatchRerun = fn;
|
|
4448
|
+
},
|
|
4449
|
+
async runDispatchRerun () {
|
|
4450
|
+
await dispatchRerun?.();
|
|
4451
|
+
},
|
|
4452
|
+
setInterrupt (fn) {
|
|
4453
|
+
interruptInFlightRun = fn;
|
|
4454
|
+
},
|
|
4455
|
+
async signalInvalidation (fileFilters, claimScope) {
|
|
4456
|
+
await interruptInFlightRun?.();
|
|
4457
|
+
claimScope?.();
|
|
4458
|
+
signalledCycle = Promise.resolve(onInvalidate?.({
|
|
4459
|
+
isFirstBuild: false,
|
|
4460
|
+
fileFilters
|
|
4461
|
+
})).catch((error)=>{
|
|
4462
|
+
logger.error(color.red('Browser Mode watch cycle failed:'), error);
|
|
4463
|
+
});
|
|
4464
|
+
},
|
|
4465
|
+
async awaitSignalledCycle () {
|
|
4466
|
+
await signalledCycle;
|
|
4467
|
+
}
|
|
4335
4468
|
};
|
|
4336
|
-
|
|
4337
|
-
|
|
4338
|
-
|
|
4339
|
-
|
|
4340
|
-
|
|
4341
|
-
|
|
4342
|
-
|
|
4343
|
-
|
|
4344
|
-
|
|
4345
|
-
|
|
4346
|
-
|
|
4469
|
+
};
|
|
4470
|
+
let nextBrowserFilePid = 1000000000;
|
|
4471
|
+
const resolveEmptyLaunchExitCode = (current, { allowEmptyRun, isWatchMode, passWithNoTests })=>{
|
|
4472
|
+
if (allowEmptyRun || !isWatchMode || passWithNoTests) return current;
|
|
4473
|
+
return void 0 === current || 0 === current ? 1 : current;
|
|
4474
|
+
};
|
|
4475
|
+
const getMaxTestTimeoutForRpc = (projects)=>Math.max(...projects.map((p)=>p.normalizedConfig.testTimeout ?? DEFAULT_TEST_TIMEOUT));
|
|
4476
|
+
const resolveProviderForTestPath = ({ testPath, browserProjects })=>{
|
|
4477
|
+
const normalizedTestPath = normalize(testPath);
|
|
4478
|
+
const sortedProjects = [
|
|
4479
|
+
...browserProjects
|
|
4480
|
+
].sort((a, b)=>b.rootPath.length - a.rootPath.length);
|
|
4481
|
+
for (const project of sortedProjects)if (normalizedTestPath.startsWith(project.rootPath)) return project.provider;
|
|
4482
|
+
throw new Error(`Cannot resolve browser provider for test path: ${JSON.stringify(testPath)}. Known project roots: ${JSON.stringify(sortedProjects.map((p)=>p.rootPath))}`);
|
|
4483
|
+
};
|
|
4484
|
+
const runBrowserController = async (context, options)=>{
|
|
4485
|
+
const { allowEmptyRun = false, filesOnly = false, onTraceEvents, env, onInvalidate } = options ?? {};
|
|
4486
|
+
const buildStart = Date.now();
|
|
4487
|
+
const isWatchMode = 'watch' === context.command;
|
|
4488
|
+
const phaseTrackers = onTraceEvents ? new Map() : void 0;
|
|
4489
|
+
const trackerKey = (project, testPath)=>`${project}\u0000${testPath}`;
|
|
4490
|
+
const browserProjects = options?.projects ?? getBrowserProjects(context);
|
|
4491
|
+
const useHeadlessDirect = browserProjects.every((project)=>project.normalizedConfig.browser.headless);
|
|
4492
|
+
const browserSourceMapCache = new Map();
|
|
4493
|
+
const isHttpLikeFile = (file)=>/^https?:\/\//.test(file);
|
|
4494
|
+
const resolveBrowserSourcemap = async (sourcePath)=>{
|
|
4495
|
+
if (!isHttpLikeFile(sourcePath)) return {
|
|
4496
|
+
handled: false,
|
|
4497
|
+
sourcemap: null
|
|
4498
|
+
};
|
|
4499
|
+
const normalizedUrl = normalizeJavaScriptUrl(sourcePath);
|
|
4500
|
+
if (!normalizedUrl) return {
|
|
4501
|
+
handled: true,
|
|
4502
|
+
sourcemap: null
|
|
4503
|
+
};
|
|
4504
|
+
if (browserSourceMapCache.has(normalizedUrl)) return {
|
|
4505
|
+
handled: true,
|
|
4506
|
+
sourcemap: browserSourceMapCache.get(normalizedUrl) ?? null
|
|
4507
|
+
};
|
|
4508
|
+
return {
|
|
4509
|
+
handled: true,
|
|
4510
|
+
sourcemap: await loadSourceMapWithCache({
|
|
4511
|
+
jsUrl: normalizedUrl,
|
|
4512
|
+
cache: browserSourceMapCache
|
|
4513
|
+
})
|
|
4514
|
+
};
|
|
4347
4515
|
};
|
|
4348
|
-
const
|
|
4349
|
-
|
|
4350
|
-
|
|
4351
|
-
payload
|
|
4352
|
-
], payload.results);
|
|
4353
|
-
if (phaseTrackers) {
|
|
4354
|
-
const key = trackerKey(payload.project, payload.testPath);
|
|
4355
|
-
const tracker = phaseTrackers.get(key);
|
|
4356
|
-
if (tracker) {
|
|
4357
|
-
tracker.end();
|
|
4358
|
-
const events = tracker.getTraceEvents();
|
|
4359
|
-
if (events) onTraceEvents?.(events);
|
|
4360
|
-
phaseTrackers.delete(key);
|
|
4361
|
-
}
|
|
4362
|
-
}
|
|
4363
|
-
if ('passed-only' === context.normalizedConfig.silent) silentConsoleController.flushBufferedLogsForTask({
|
|
4364
|
-
taskId: payload.testId,
|
|
4365
|
-
status: payload.status,
|
|
4366
|
-
taskParentNames: payload.parentNames,
|
|
4367
|
-
taskType: 'file',
|
|
4368
|
-
testPath: payload.testPath
|
|
4369
|
-
});
|
|
4370
|
-
await sinkForProjectName(payload.project).onTestFileResult(payload);
|
|
4371
|
-
if (isWatchMode && 'fail' === payload.status) ensureProcessExitCode(1);
|
|
4516
|
+
const getBrowserSourcemap = async (sourcePath)=>{
|
|
4517
|
+
const result = await resolveBrowserSourcemap(sourcePath);
|
|
4518
|
+
return result.handled ? result.sourcemap : null;
|
|
4372
4519
|
};
|
|
4373
|
-
const
|
|
4374
|
-
const
|
|
4375
|
-
|
|
4376
|
-
|
|
4377
|
-
|
|
4378
|
-
|
|
4379
|
-
|
|
4380
|
-
|
|
4381
|
-
|
|
4382
|
-
|
|
4383
|
-
|
|
4384
|
-
|
|
4520
|
+
const buildErrorResult = (error, close)=>{
|
|
4521
|
+
const elapsed = Math.max(0, Date.now() - buildStart);
|
|
4522
|
+
return {
|
|
4523
|
+
results: [],
|
|
4524
|
+
testResults: [],
|
|
4525
|
+
duration: {
|
|
4526
|
+
totalTime: elapsed,
|
|
4527
|
+
buildTime: elapsed,
|
|
4528
|
+
testTime: 0
|
|
4529
|
+
},
|
|
4530
|
+
hasFailure: true,
|
|
4531
|
+
unhandledErrors: [
|
|
4532
|
+
error
|
|
4533
|
+
],
|
|
4534
|
+
getSourcemap: getBrowserSourcemap,
|
|
4535
|
+
resolveSourcemap: resolveBrowserSourcemap,
|
|
4536
|
+
close
|
|
4385
4537
|
};
|
|
4386
|
-
silentConsoleController.onConsoleLog(log);
|
|
4387
4538
|
};
|
|
4388
|
-
const
|
|
4389
|
-
const
|
|
4390
|
-
|
|
4391
|
-
|
|
4392
|
-
|
|
4393
|
-
|
|
4394
|
-
|
|
4395
|
-
switch(request.method){
|
|
4396
|
-
case 'resolveSnapshotPath':
|
|
4397
|
-
return snapshotRpcMethods.resolveSnapshotPath(request.args.testPath);
|
|
4398
|
-
case 'readSnapshotFile':
|
|
4399
|
-
return snapshotRpcMethods.readSnapshotFile(request.args.filepath);
|
|
4400
|
-
case 'saveSnapshotFile':
|
|
4401
|
-
return snapshotRpcMethods.saveSnapshotFile(request.args.filepath, request.args.content);
|
|
4402
|
-
case 'removeSnapshotFile':
|
|
4403
|
-
return snapshotRpcMethods.removeSnapshotFile(request.args.filepath);
|
|
4404
|
-
default:
|
|
4405
|
-
{
|
|
4406
|
-
const _exhaustive = request;
|
|
4407
|
-
return _exhaustive;
|
|
4408
|
-
}
|
|
4539
|
+
const failWithError = async (error, cleanup)=>{
|
|
4540
|
+
const normalizedError = toError(error);
|
|
4541
|
+
if (cleanup && !isWatchMode) return buildErrorResult(normalizedError, cleanup);
|
|
4542
|
+
try {
|
|
4543
|
+
return buildErrorResult(normalizedError);
|
|
4544
|
+
} finally{
|
|
4545
|
+
await cleanup?.();
|
|
4409
4546
|
}
|
|
4410
4547
|
};
|
|
4411
|
-
const
|
|
4412
|
-
|
|
4413
|
-
|
|
4414
|
-
|
|
4415
|
-
|
|
4416
|
-
|
|
4417
|
-
|
|
4418
|
-
|
|
4419
|
-
|
|
4420
|
-
|
|
4421
|
-
|
|
4422
|
-
|
|
4548
|
+
const coverageConfig = browserProjects.find((project)=>project.normalizedConfig.coverage?.enabled)?.normalizedConfig.coverage;
|
|
4549
|
+
const coverageProvider = coverageConfig?.enabled ? await createCoverageProvider(coverageConfig, context.rootPath) : null;
|
|
4550
|
+
const containerDevServerEnv = process.env.RSTEST_CONTAINER_DEV_SERVER;
|
|
4551
|
+
let containerDevServer;
|
|
4552
|
+
let containerDistPath;
|
|
4553
|
+
if (!useHeadlessDirect) {
|
|
4554
|
+
if (containerDevServerEnv) try {
|
|
4555
|
+
containerDevServer = new URL(containerDevServerEnv).toString();
|
|
4556
|
+
logger.debug(`[Browser UI] Using dev server for container: ${containerDevServer}`);
|
|
4557
|
+
} catch (error) {
|
|
4558
|
+
const originalError = toError(error);
|
|
4559
|
+
originalError.message = `Invalid RSTEST_CONTAINER_DEV_SERVER value: ${originalError.message}`;
|
|
4560
|
+
return failWithError(originalError);
|
|
4561
|
+
}
|
|
4562
|
+
if (!containerDevServer) try {
|
|
4563
|
+
containerDistPath = resolveContainerDist();
|
|
4564
|
+
} catch (error) {
|
|
4565
|
+
return failWithError(error);
|
|
4566
|
+
}
|
|
4567
|
+
}
|
|
4568
|
+
let projectEntries = await resolveProjectEntries(context, options?.shardedEntries, browserProjects);
|
|
4569
|
+
let totalTests = projectEntries.reduce((total, item)=>total + item.testFiles.length, 0);
|
|
4570
|
+
const shouldInitializeEmptyBrowserHooks = 0 === totalTests && hasUserRstestConfigPlugins(browserProjects);
|
|
4571
|
+
const createEmptyRunResult = ()=>{
|
|
4572
|
+
const elapsed = Math.max(0, Date.now() - buildStart);
|
|
4573
|
+
return {
|
|
4574
|
+
results: [],
|
|
4575
|
+
testResults: [],
|
|
4576
|
+
duration: {
|
|
4577
|
+
totalTime: elapsed,
|
|
4578
|
+
buildTime: elapsed,
|
|
4579
|
+
testTime: 0
|
|
4423
4580
|
},
|
|
4424
|
-
|
|
4425
|
-
|
|
4426
|
-
|
|
4427
|
-
logger.debug(`[Dispatch] Skip registering dispatch namespace "${namespace}" because it is already reserved`);
|
|
4428
|
-
}
|
|
4429
|
-
});
|
|
4430
|
-
if (useHeadlessDirect) {
|
|
4431
|
-
const viewportByProject = mapViewportByProject(projectRuntimeConfigs);
|
|
4432
|
-
const runLifecycle = new RunSessionLifecycle();
|
|
4433
|
-
const sessionRegistry = new RunnerSessionRegistry();
|
|
4434
|
-
getHeadlessRunnerPageBySessionId = (sessionId)=>sessionRegistry.getById(sessionId)?.page;
|
|
4435
|
-
let dispatchRequestCounter = 0;
|
|
4436
|
-
const nextDispatchRequestId = (namespace)=>`${namespace}-${++dispatchRequestCounter}`;
|
|
4437
|
-
const closeContextSafely = async (browserContext)=>{
|
|
4438
|
-
try {
|
|
4439
|
-
await browserContext.close();
|
|
4440
|
-
} catch {}
|
|
4441
|
-
};
|
|
4442
|
-
const cancelRun = async (run, waitForDone = true)=>{
|
|
4443
|
-
await runLifecycle.cancel(run, {
|
|
4444
|
-
waitForDone,
|
|
4445
|
-
onCancel: async (session)=>{
|
|
4446
|
-
await Promise.all(Array.from(session.contexts).map((browserContext)=>closeContextSafely(browserContext)));
|
|
4447
|
-
}
|
|
4448
|
-
});
|
|
4581
|
+
hasFailure: false,
|
|
4582
|
+
getSourcemap: getBrowserSourcemap,
|
|
4583
|
+
resolveSourcemap: resolveBrowserSourcemap
|
|
4449
4584
|
};
|
|
4450
|
-
|
|
4451
|
-
|
|
4452
|
-
|
|
4453
|
-
|
|
4454
|
-
|
|
4585
|
+
};
|
|
4586
|
+
const writeEmptyLaunchExitCode = ()=>{
|
|
4587
|
+
process.exitCode = resolveEmptyLaunchExitCode(process.exitCode, {
|
|
4588
|
+
allowEmptyRun,
|
|
4589
|
+
isWatchMode,
|
|
4590
|
+
passWithNoTests: context.normalizedConfig.passWithNoTests
|
|
4455
4591
|
});
|
|
4456
|
-
|
|
4457
|
-
|
|
4458
|
-
|
|
4459
|
-
|
|
4460
|
-
|
|
4461
|
-
|
|
4462
|
-
|
|
4463
|
-
|
|
4464
|
-
|
|
4465
|
-
|
|
4466
|
-
|
|
4467
|
-
|
|
4468
|
-
|
|
4469
|
-
|
|
4470
|
-
|
|
4471
|
-
|
|
4472
|
-
|
|
4473
|
-
|
|
4474
|
-
|
|
4475
|
-
|
|
4476
|
-
|
|
4477
|
-
|
|
4478
|
-
|
|
4479
|
-
|
|
4480
|
-
|
|
4481
|
-
|
|
4482
|
-
|
|
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;
|
|
4592
|
+
};
|
|
4593
|
+
if (0 === totalTests && !shouldInitializeEmptyBrowserHooks) {
|
|
4594
|
+
writeEmptyLaunchExitCode();
|
|
4595
|
+
return allowEmptyRun ? createEmptyRunResult() : void 0;
|
|
4596
|
+
}
|
|
4597
|
+
const enableCliShortcuts = isWatchMode && isTTY('stdin');
|
|
4598
|
+
const browserTempOutputRoot = context.normalizedConfig.output.distPath.root;
|
|
4599
|
+
const tempDir = isWatchMode && watchContext.runtime ? watchContext.runtime.tempDir : isWatchMode ? join(context.rootPath, browserTempOutputRoot, 'browser', 'watch') : join(context.rootPath, browserTempOutputRoot, 'browser', Date.now().toString());
|
|
4600
|
+
let runtime = isWatchMode ? watchContext.runtime : null;
|
|
4601
|
+
const watchSignals = createWatchSignals(onInvalidate);
|
|
4602
|
+
if (!runtime) {
|
|
4603
|
+
try {
|
|
4604
|
+
runtime = await createBrowserRuntime({
|
|
4605
|
+
context,
|
|
4606
|
+
projectEntries,
|
|
4607
|
+
browserProjects,
|
|
4608
|
+
shardedEntries: options?.shardedEntries,
|
|
4609
|
+
freezeShardedEntries: options?.freezeShardedEntries,
|
|
4610
|
+
tempDir,
|
|
4611
|
+
isWatchMode,
|
|
4612
|
+
onTriggerRerun: isWatchMode ? async ()=>{
|
|
4613
|
+
await watchSignals.runDispatchRerun();
|
|
4614
|
+
} : void 0,
|
|
4615
|
+
containerDistPath,
|
|
4616
|
+
containerDevServer,
|
|
4617
|
+
skipProviderLaunch: filesOnly,
|
|
4618
|
+
appliedModifyRstestConfigEnvironments: options?.appliedModifyRstestConfigEnvironments
|
|
4492
4619
|
});
|
|
4493
|
-
|
|
4494
|
-
|
|
4495
|
-
|
|
4496
|
-
|
|
4497
|
-
|
|
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: []
|
|
4620
|
+
} catch (error) {
|
|
4621
|
+
return failWithError(error, async ()=>{
|
|
4622
|
+
await promises.rm(tempDir, {
|
|
4623
|
+
recursive: true,
|
|
4624
|
+
force: true
|
|
4625
|
+
}).catch(()=>{});
|
|
4601
4626
|
});
|
|
4602
|
-
|
|
4603
|
-
|
|
4604
|
-
|
|
4605
|
-
|
|
4606
|
-
|
|
4607
|
-
|
|
4608
|
-
|
|
4609
|
-
|
|
4610
|
-
|
|
4611
|
-
|
|
4612
|
-
|
|
4613
|
-
|
|
4614
|
-
|
|
4615
|
-
const
|
|
4616
|
-
|
|
4617
|
-
|
|
4618
|
-
|
|
4619
|
-
|
|
4620
|
-
|
|
4621
|
-
|
|
4622
|
-
|
|
4623
|
-
|
|
4624
|
-
|
|
4625
|
-
|
|
4626
|
-
|
|
4627
|
-
|
|
4628
|
-
|
|
4629
|
-
|
|
4630
|
-
|
|
4631
|
-
|
|
4632
|
-
|
|
4633
|
-
|
|
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);
|
|
4627
|
+
}
|
|
4628
|
+
if (isWatchMode && !filesOnly) {
|
|
4629
|
+
watchContext.runtime = runtime;
|
|
4630
|
+
registerWatchCleanup(context.embedded);
|
|
4631
|
+
}
|
|
4632
|
+
}
|
|
4633
|
+
const watchState = runtime.watchState;
|
|
4634
|
+
if (isWatchMode) watchState.lastTestFiles = collectWatchTestFiles(projectEntries);
|
|
4635
|
+
const seedPendingRerun = (testPaths)=>{
|
|
4636
|
+
const wanted = testPaths ? new Set(testPaths.map((testPath)=>normalize(testPath))) : null;
|
|
4637
|
+
let seeded = 0;
|
|
4638
|
+
for (const file of watchState.lastTestFiles){
|
|
4639
|
+
if (wanted && !wanted.has(file.testPath)) continue;
|
|
4640
|
+
const pending = watchState.pendingAffectedTestFiles.get(file.projectName) ?? new Set();
|
|
4641
|
+
pending.add(file.testPath);
|
|
4642
|
+
watchState.pendingAffectedTestFiles.set(file.projectName, pending);
|
|
4643
|
+
seeded += 1;
|
|
4644
|
+
}
|
|
4645
|
+
return seeded;
|
|
4646
|
+
};
|
|
4647
|
+
const buildRerunOutcome = ({ rerunTestPaths, testTime, unhandledErrors })=>{
|
|
4648
|
+
const rerunPathSet = new Set(rerunTestPaths);
|
|
4649
|
+
const rerunResults = context.reporterResults.results.filter((result)=>rerunPathSet.has(result.testPath));
|
|
4650
|
+
const coverageMap = buildBrowserCoverageMap(rerunResults, coverageProvider);
|
|
4651
|
+
return {
|
|
4652
|
+
results: rerunResults,
|
|
4653
|
+
testResults: context.reporterResults.testResults.filter((result)=>rerunPathSet.has(result.testPath)),
|
|
4654
|
+
errors: unhandledErrors ?? [],
|
|
4655
|
+
testPaths: rerunTestPaths,
|
|
4656
|
+
duration: {
|
|
4657
|
+
buildTime: drainPendingBuildTime(watchState),
|
|
4658
|
+
testTime
|
|
4644
4659
|
},
|
|
4645
|
-
|
|
4646
|
-
|
|
4647
|
-
|
|
4660
|
+
coverage: coverageMap?.files().length ? {
|
|
4661
|
+
map: coverageMap.toJSON()
|
|
4662
|
+
} : void 0,
|
|
4663
|
+
resolveSourcemap: resolveBrowserSourcemap
|
|
4664
|
+
};
|
|
4665
|
+
};
|
|
4666
|
+
const createWatchSession = (execute)=>({
|
|
4667
|
+
runCycle: async (testPaths)=>{
|
|
4648
4668
|
const rerunStartTime = Date.now();
|
|
4649
|
-
const fatalErrorBeforeRun =
|
|
4669
|
+
const fatalErrorBeforeRun = fatalErrorRef.current;
|
|
4650
4670
|
let rerunError;
|
|
4651
4671
|
try {
|
|
4652
|
-
await
|
|
4672
|
+
await execute(testPaths);
|
|
4653
4673
|
} catch (error) {
|
|
4654
4674
|
rerunError = toError(error);
|
|
4655
|
-
|
|
4656
|
-
|
|
4657
|
-
|
|
4658
|
-
|
|
4659
|
-
|
|
4660
|
-
|
|
4661
|
-
|
|
4662
|
-
|
|
4663
|
-
|
|
4664
|
-
|
|
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
|
|
4675
|
+
}
|
|
4676
|
+
const rerunFatalError = fatalErrorRef.current && fatalErrorRef.current !== fatalErrorBeforeRun ? fatalErrorRef.current : void 0;
|
|
4677
|
+
return buildRerunOutcome({
|
|
4678
|
+
rerunTestPaths: testPaths,
|
|
4679
|
+
testTime: Math.max(0, Date.now() - rerunStartTime),
|
|
4680
|
+
unhandledErrors: rerunError ? [
|
|
4681
|
+
rerunError
|
|
4682
|
+
] : rerunFatalError ? [
|
|
4683
|
+
rerunFatalError
|
|
4684
|
+
] : void 0
|
|
4676
4685
|
});
|
|
4677
4686
|
},
|
|
4678
|
-
|
|
4679
|
-
|
|
4687
|
+
requestRerun: async (testPaths)=>{
|
|
4688
|
+
const seeded = seedPendingRerun(testPaths);
|
|
4689
|
+
if (testPaths && 0 === seeded) return;
|
|
4690
|
+
await watchSignals.runDispatchRerun();
|
|
4691
|
+
await watchSignals.awaitSignalledCycle();
|
|
4680
4692
|
}
|
|
4681
4693
|
});
|
|
4682
|
-
|
|
4683
|
-
|
|
4684
|
-
|
|
4685
|
-
|
|
4686
|
-
|
|
4687
|
-
|
|
4688
|
-
|
|
4689
|
-
|
|
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,
|
|
4694
|
+
projectEntries = runtime.projectEntries;
|
|
4695
|
+
totalTests = projectEntries.reduce((total, item)=>total + item.testFiles.length, 0);
|
|
4696
|
+
const buildTime = Date.now() - buildStart;
|
|
4697
|
+
if (filesOnly) return {
|
|
4698
|
+
results: [],
|
|
4699
|
+
testResults: [],
|
|
4700
|
+
duration: {
|
|
4701
|
+
totalTime: buildTime,
|
|
4722
4702
|
buildTime,
|
|
4723
|
-
testTime
|
|
4724
|
-
}
|
|
4725
|
-
|
|
4726
|
-
|
|
4727
|
-
|
|
4728
|
-
|
|
4729
|
-
|
|
4730
|
-
|
|
4731
|
-
|
|
4732
|
-
|
|
4733
|
-
|
|
4734
|
-
|
|
4735
|
-
|
|
4736
|
-
|
|
4737
|
-
|
|
4738
|
-
|
|
4739
|
-
|
|
4740
|
-
|
|
4741
|
-
|
|
4742
|
-
|
|
4743
|
-
|
|
4703
|
+
testTime: 0
|
|
4704
|
+
},
|
|
4705
|
+
hasFailure: false,
|
|
4706
|
+
getSourcemap: getBrowserSourcemap,
|
|
4707
|
+
resolveSourcemap: resolveBrowserSourcemap,
|
|
4708
|
+
close: ()=>destroyBrowserRuntime(runtime)
|
|
4709
|
+
};
|
|
4710
|
+
if (0 === totalTests) {
|
|
4711
|
+
writeEmptyLaunchExitCode();
|
|
4712
|
+
await destroyBrowserRuntime(runtime);
|
|
4713
|
+
return allowEmptyRun ? createEmptyRunResult() : void 0;
|
|
4714
|
+
}
|
|
4715
|
+
const { browser, browserLaunchOptions, wsPort } = runtime;
|
|
4716
|
+
const allTestFiles = projectEntries.flatMap((entry)=>entry.testFiles.map((testPath)=>({
|
|
4717
|
+
testPath: normalize(testPath),
|
|
4718
|
+
projectName: entry.project.name
|
|
4719
|
+
})));
|
|
4720
|
+
const projectRuntimeConfigs = browserProjects.map((project)=>({
|
|
4721
|
+
name: project.name,
|
|
4722
|
+
environmentName: project.environmentName,
|
|
4723
|
+
projectRoot: normalize(project.rootPath),
|
|
4724
|
+
runtimeConfig: serializableConfig(projectRuntimeConfig(project, {
|
|
4725
|
+
envMode: 'static',
|
|
4726
|
+
envOverlay: env
|
|
4727
|
+
})),
|
|
4728
|
+
viewport: project.normalizedConfig.browser.viewport
|
|
4729
|
+
}));
|
|
4730
|
+
const maxTestTimeoutForRpc = getMaxTestTimeoutForRpc(browserProjects);
|
|
4731
|
+
const projectRunnerUrls = Object.fromEntries([
|
|
4732
|
+
...runtime.projectServers
|
|
4733
|
+
].map(([name, server])=>[
|
|
4734
|
+
name,
|
|
4735
|
+
`http://localhost:${server.port}`
|
|
4736
|
+
]));
|
|
4737
|
+
const hostOptions = {
|
|
4738
|
+
rootPath: normalize(context.rootPath),
|
|
4739
|
+
projects: projectRuntimeConfigs,
|
|
4740
|
+
snapshot: {
|
|
4741
|
+
updateSnapshot: options?.updateSnapshot ?? context.snapshotManager.options.updateSnapshot
|
|
4742
|
+
},
|
|
4743
|
+
runnerUrl: `http://localhost:${runtime.containerServer.port}`,
|
|
4744
|
+
projectRunnerUrls,
|
|
4745
|
+
wsPort,
|
|
4746
|
+
debug: isDebug(),
|
|
4747
|
+
rpcTimeout: maxTestTimeoutForRpc
|
|
4748
|
+
};
|
|
4749
|
+
const browserProviderProjects = browserProjects.map((project)=>({
|
|
4750
|
+
rootPath: normalize(project.rootPath),
|
|
4751
|
+
provider: project.normalizedConfig.browser.provider
|
|
4752
|
+
}));
|
|
4753
|
+
const implementationByProvider = new Map();
|
|
4754
|
+
for (const browserProject of browserProviderProjects)if (!implementationByProvider.has(browserProject.provider)) implementationByProvider.set(browserProject.provider, getBrowserProviderImplementation(browserProject.provider));
|
|
4755
|
+
let resolveDispatchPages = ()=>({});
|
|
4756
|
+
const setDispatchPageResolver = (resolver)=>{
|
|
4757
|
+
resolveDispatchPages = resolver;
|
|
4758
|
+
};
|
|
4759
|
+
const dispatchBrowserRpcRequest = async ({ request, target })=>{
|
|
4760
|
+
const timeoutFallbackMs = maxTestTimeoutForRpc;
|
|
4761
|
+
const provider = resolveProviderForTestPath({
|
|
4762
|
+
testPath: request.testPath,
|
|
4763
|
+
browserProjects: browserProviderProjects
|
|
4764
|
+
});
|
|
4765
|
+
const implementation = implementationByProvider.get(provider);
|
|
4766
|
+
if (!implementation) throw new Error(`Browser provider implementation not found: ${provider}`);
|
|
4767
|
+
const { runnerPage, containerPage } = resolveDispatchPages(target);
|
|
4768
|
+
if (target?.sessionId && !runnerPage) throw new Error(`Runner page session not found for browser dispatch: ${target.sessionId}`);
|
|
4769
|
+
if (!runnerPage && !containerPage) throw new Error('Browser container page is not initialized');
|
|
4770
|
+
try {
|
|
4771
|
+
return await implementation.dispatchRpc({
|
|
4772
|
+
containerPage: runnerPage ? void 0 : containerPage,
|
|
4773
|
+
runnerPage,
|
|
4774
|
+
request,
|
|
4775
|
+
timeoutFallbackMs
|
|
4744
4776
|
});
|
|
4745
|
-
}
|
|
4746
|
-
|
|
4747
|
-
|
|
4748
|
-
if (isWatchMode && triggerRerun) {
|
|
4749
|
-
watchState.hooksEnabled = true;
|
|
4750
|
-
logWatchReadyMessage(context, enableCliShortcuts);
|
|
4777
|
+
} catch (error) {
|
|
4778
|
+
if (error instanceof Error) throw error.message;
|
|
4779
|
+
throw String(error);
|
|
4751
4780
|
}
|
|
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
4781
|
};
|
|
4767
|
-
|
|
4768
|
-
const
|
|
4769
|
-
|
|
4770
|
-
|
|
4771
|
-
|
|
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();
|
|
4782
|
+
runtime.dispatchHandlers.set(DISPATCH_NAMESPACE_BROWSER, async (dispatchRequest)=>{
|
|
4783
|
+
const request = validateBrowserRpcRequest(dispatchRequest.args);
|
|
4784
|
+
return dispatchBrowserRpcRequest({
|
|
4785
|
+
request,
|
|
4786
|
+
target: dispatchRequest.target
|
|
4790
4787
|
});
|
|
4788
|
+
});
|
|
4789
|
+
runtime.setContainerOptions(hostOptions);
|
|
4790
|
+
const reporterResults = [];
|
|
4791
|
+
const caseResults = [];
|
|
4792
|
+
const fatalErrorRef = {
|
|
4793
|
+
current: null
|
|
4791
4794
|
};
|
|
4792
|
-
const
|
|
4793
|
-
|
|
4794
|
-
|
|
4795
|
-
|
|
4796
|
-
|
|
4795
|
+
const runnerSinks = new Map(browserProjects.map((project)=>[
|
|
4796
|
+
project.name,
|
|
4797
|
+
createRunnerEventSink(context, project.normalizedConfig)
|
|
4798
|
+
]));
|
|
4799
|
+
const sinkForProjectName = (projectName)=>{
|
|
4800
|
+
const sink = runnerSinks.get(projectName);
|
|
4801
|
+
if (!sink) throw new Error(`No runner event sink for project "${projectName}"`);
|
|
4802
|
+
return sink;
|
|
4797
4803
|
};
|
|
4798
|
-
|
|
4799
|
-
|
|
4800
|
-
|
|
4801
|
-
|
|
4802
|
-
|
|
4803
|
-
|
|
4804
|
-
|
|
4805
|
-
}
|
|
4806
|
-
|
|
4807
|
-
|
|
4808
|
-
|
|
4809
|
-
|
|
4810
|
-
|
|
4811
|
-
|
|
4812
|
-
|
|
4813
|
-
|
|
4814
|
-
|
|
4815
|
-
|
|
4816
|
-
|
|
4817
|
-
|
|
4818
|
-
|
|
4819
|
-
|
|
4820
|
-
|
|
4804
|
+
const silentConsoleController = createSilentConsoleController({
|
|
4805
|
+
runtimeConfig: {
|
|
4806
|
+
silent: context.normalizedConfig.silent,
|
|
4807
|
+
disableConsoleIntercept: context.normalizedConfig.disableConsoleIntercept
|
|
4808
|
+
},
|
|
4809
|
+
emitInterceptedLog: (log)=>sinkForProjectName(log.project).onConsoleLog(log),
|
|
4810
|
+
writeOriginalLog: ()=>{}
|
|
4811
|
+
});
|
|
4812
|
+
const snapshotRpcMethods = {
|
|
4813
|
+
async resolveSnapshotPath (testPath) {
|
|
4814
|
+
return resolveSnapshotPathDefault(testPath, context.normalizedConfig.resolveSnapshotPath);
|
|
4815
|
+
},
|
|
4816
|
+
async readSnapshotFile (filepath) {
|
|
4817
|
+
try {
|
|
4818
|
+
return await promises.readFile(filepath, 'utf-8');
|
|
4819
|
+
} catch {
|
|
4820
|
+
return null;
|
|
4821
|
+
}
|
|
4822
|
+
},
|
|
4823
|
+
async saveSnapshotFile (filepath, content) {
|
|
4824
|
+
const dir = dirname(filepath);
|
|
4825
|
+
await promises.mkdir(dir, {
|
|
4826
|
+
recursive: true
|
|
4827
|
+
});
|
|
4828
|
+
await promises.writeFile(filepath, content, 'utf-8');
|
|
4829
|
+
},
|
|
4830
|
+
async removeSnapshotFile (filepath) {
|
|
4831
|
+
try {
|
|
4832
|
+
await promises.unlink(filepath);
|
|
4833
|
+
} catch {}
|
|
4821
4834
|
}
|
|
4822
|
-
|
|
4823
|
-
|
|
4824
|
-
|
|
4835
|
+
};
|
|
4836
|
+
const handleTestFileStart = async (payload)=>{
|
|
4837
|
+
if (phaseTrackers) {
|
|
4838
|
+
const tracker = new PhaseTracker({
|
|
4839
|
+
trace: {
|
|
4840
|
+
testPath: payload.testPath,
|
|
4841
|
+
project: payload.projectName
|
|
4842
|
+
},
|
|
4843
|
+
pid: nextBrowserFilePid++
|
|
4844
|
+
});
|
|
4845
|
+
tracker.transition('prepare');
|
|
4846
|
+
phaseTrackers.set(trackerKey(payload.projectName, payload.testPath), tracker);
|
|
4847
|
+
}
|
|
4848
|
+
await sinkForProjectName(payload.projectName).onTestFileStart({
|
|
4849
|
+
testId: getFileTaskId(payload.testPath),
|
|
4850
|
+
testPath: payload.testPath,
|
|
4851
|
+
project: payload.projectName,
|
|
4852
|
+
tests: []
|
|
4853
|
+
});
|
|
4854
|
+
};
|
|
4855
|
+
const handleTestFileReady = async (payload)=>{
|
|
4856
|
+
phaseTrackers?.get(trackerKey(payload.project, payload.testPath))?.transition('tests');
|
|
4857
|
+
await sinkForProjectName(payload.project).onTestFileReady(payload);
|
|
4858
|
+
};
|
|
4859
|
+
const handleTestSuiteStart = async (payload)=>{
|
|
4860
|
+
phaseTrackers?.get(trackerKey(payload.project, payload.testPath))?.recordSuiteStart(payload);
|
|
4861
|
+
await sinkForProjectName(payload.project).onTestSuiteStart(payload);
|
|
4862
|
+
};
|
|
4863
|
+
const handleTestSuiteResult = async (payload)=>{
|
|
4864
|
+
phaseTrackers?.get(trackerKey(payload.project, payload.testPath))?.recordSuiteResult(payload);
|
|
4865
|
+
await sinkForProjectName(payload.project).onTestSuiteResult(payload);
|
|
4866
|
+
if ('passed-only' === context.normalizedConfig.silent) silentConsoleController.flushBufferedLogsForTask({
|
|
4867
|
+
taskId: payload.testId,
|
|
4868
|
+
status: payload.status,
|
|
4869
|
+
taskParentNames: payload.parentNames,
|
|
4870
|
+
taskType: 'suite',
|
|
4871
|
+
testPath: payload.testPath
|
|
4825
4872
|
});
|
|
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
4873
|
};
|
|
4834
|
-
const
|
|
4835
|
-
|
|
4836
|
-
|
|
4837
|
-
if (runId && pending.runId !== runId) return;
|
|
4838
|
-
pendingHeadedReloads.delete(testPath);
|
|
4839
|
-
pending.deferred.reject(error);
|
|
4874
|
+
const handleTestCaseStart = async (payload)=>{
|
|
4875
|
+
phaseTrackers?.get(trackerKey(payload.project, payload.testPath))?.recordCaseStart(payload);
|
|
4876
|
+
sinkForProjectName(payload.project).onTestCaseStart(payload);
|
|
4840
4877
|
};
|
|
4841
|
-
const
|
|
4842
|
-
|
|
4843
|
-
|
|
4844
|
-
|
|
4845
|
-
|
|
4878
|
+
const handleTestCaseResult = async (payload)=>{
|
|
4879
|
+
caseResults.push(payload);
|
|
4880
|
+
phaseTrackers?.get(trackerKey(payload.project, payload.testPath))?.recordCaseResult(payload);
|
|
4881
|
+
await sinkForProjectName(payload.project).onTestCaseResult(payload);
|
|
4882
|
+
if ('passed-only' === context.normalizedConfig.silent) silentConsoleController.flushBufferedLogsForTask({
|
|
4883
|
+
taskId: payload.testId,
|
|
4884
|
+
status: payload.status,
|
|
4885
|
+
taskParentNames: payload.parentNames,
|
|
4886
|
+
taskType: 'case',
|
|
4887
|
+
testPath: payload.testPath
|
|
4888
|
+
});
|
|
4846
4889
|
};
|
|
4847
|
-
const
|
|
4848
|
-
|
|
4849
|
-
|
|
4850
|
-
|
|
4851
|
-
|
|
4890
|
+
const handleTestFileComplete = async (payload)=>{
|
|
4891
|
+
reporterResults.push(payload);
|
|
4892
|
+
context.updateReporterResultState([
|
|
4893
|
+
payload
|
|
4894
|
+
], payload.results);
|
|
4895
|
+
if (phaseTrackers) {
|
|
4896
|
+
const key = trackerKey(payload.project, payload.testPath);
|
|
4897
|
+
const tracker = phaseTrackers.get(key);
|
|
4898
|
+
if (tracker) {
|
|
4899
|
+
tracker.end();
|
|
4900
|
+
const events = tracker.getTraceEvents();
|
|
4901
|
+
if (events) onTraceEvents?.(events);
|
|
4902
|
+
phaseTrackers.delete(key);
|
|
4903
|
+
}
|
|
4852
4904
|
}
|
|
4853
|
-
|
|
4854
|
-
|
|
4855
|
-
|
|
4856
|
-
|
|
4905
|
+
if ('passed-only' === context.normalizedConfig.silent) silentConsoleController.flushBufferedLogsForTask({
|
|
4906
|
+
taskId: payload.testId,
|
|
4907
|
+
status: payload.status,
|
|
4908
|
+
taskParentNames: payload.parentNames,
|
|
4909
|
+
taskType: 'file',
|
|
4910
|
+
testPath: payload.testPath
|
|
4857
4911
|
});
|
|
4858
|
-
|
|
4912
|
+
await sinkForProjectName(payload.project).onTestFileResult(payload);
|
|
4859
4913
|
};
|
|
4860
|
-
const
|
|
4861
|
-
const
|
|
4862
|
-
|
|
4863
|
-
|
|
4864
|
-
|
|
4865
|
-
|
|
4914
|
+
const handleLog = async (payload)=>{
|
|
4915
|
+
const log = {
|
|
4916
|
+
content: payload.content,
|
|
4917
|
+
name: getPrettyConsoleName(payload.level),
|
|
4918
|
+
taskId: payload.taskId,
|
|
4919
|
+
taskName: payload.taskName,
|
|
4920
|
+
taskParentNames: payload.taskParentNames,
|
|
4921
|
+
taskType: payload.taskType,
|
|
4922
|
+
testPath: payload.testPath,
|
|
4923
|
+
project: payload.projectName,
|
|
4924
|
+
type: payload.type,
|
|
4925
|
+
trace: payload.trace
|
|
4926
|
+
};
|
|
4927
|
+
silentConsoleController.onConsoleLog(log);
|
|
4866
4928
|
};
|
|
4867
|
-
const
|
|
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;
|
|
4875
|
-
}
|
|
4929
|
+
const handleFatal = async (payload)=>{
|
|
4930
|
+
const error = new Error(payload.message);
|
|
4931
|
+
error.stack = payload.stack;
|
|
4932
|
+
fatalErrorRef.current = error;
|
|
4876
4933
|
};
|
|
4877
|
-
const
|
|
4878
|
-
|
|
4879
|
-
|
|
4880
|
-
|
|
4881
|
-
|
|
4882
|
-
|
|
4883
|
-
|
|
4884
|
-
|
|
4885
|
-
|
|
4886
|
-
return
|
|
4887
|
-
|
|
4888
|
-
|
|
4889
|
-
|
|
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;
|
|
4934
|
+
const runSnapshotRpc = async (request)=>{
|
|
4935
|
+
switch(request.method){
|
|
4936
|
+
case 'resolveSnapshotPath':
|
|
4937
|
+
return snapshotRpcMethods.resolveSnapshotPath(request.args.testPath);
|
|
4938
|
+
case 'readSnapshotFile':
|
|
4939
|
+
return snapshotRpcMethods.readSnapshotFile(request.args.filepath);
|
|
4940
|
+
case 'saveSnapshotFile':
|
|
4941
|
+
return snapshotRpcMethods.saveSnapshotFile(request.args.filepath, request.args.content);
|
|
4942
|
+
case 'removeSnapshotFile':
|
|
4943
|
+
return snapshotRpcMethods.removeSnapshotFile(request.args.filepath);
|
|
4944
|
+
default:
|
|
4945
|
+
{
|
|
4946
|
+
const _exhaustive = request;
|
|
4947
|
+
return _exhaustive;
|
|
4904
4948
|
}
|
|
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);
|
|
4953
4949
|
}
|
|
4954
|
-
|
|
4955
|
-
|
|
4956
|
-
|
|
4957
|
-
|
|
4958
|
-
|
|
4959
|
-
|
|
4960
|
-
|
|
4950
|
+
};
|
|
4951
|
+
const createDispatchRouter = (options)=>createHostDispatchRouter({
|
|
4952
|
+
routerOptions: options,
|
|
4953
|
+
runnerCallbacks: {
|
|
4954
|
+
onTestFileStart: handleTestFileStart,
|
|
4955
|
+
onTestFileReady: handleTestFileReady,
|
|
4956
|
+
onTestSuiteStart: handleTestSuiteStart,
|
|
4957
|
+
onTestSuiteResult: handleTestSuiteResult,
|
|
4958
|
+
onTestCaseStart: handleTestCaseStart,
|
|
4959
|
+
onTestCaseResult: handleTestCaseResult,
|
|
4960
|
+
onTestFileComplete: handleTestFileComplete,
|
|
4961
|
+
onLog: handleLog,
|
|
4962
|
+
onFatal: handleFatal
|
|
4963
|
+
},
|
|
4964
|
+
runSnapshotRpc,
|
|
4965
|
+
extensionHandlers: runtime.dispatchHandlers,
|
|
4966
|
+
onDuplicateNamespace: (namespace)=>{
|
|
4967
|
+
logger.debug(`[Dispatch] Skip registering dispatch namespace "${namespace}" because it is already reserved`);
|
|
4961
4968
|
}
|
|
4962
|
-
};
|
|
4963
|
-
runtime.setContainerOptions(refreshedHostOptions);
|
|
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
4969
|
});
|
|
4973
|
-
|
|
4974
|
-
|
|
4975
|
-
|
|
4976
|
-
|
|
4977
|
-
|
|
4978
|
-
|
|
4979
|
-
|
|
4980
|
-
|
|
4981
|
-
|
|
4982
|
-
|
|
4983
|
-
|
|
4984
|
-
|
|
4970
|
+
const schedulerDeps = {
|
|
4971
|
+
context,
|
|
4972
|
+
allTestFiles,
|
|
4973
|
+
hostOptions,
|
|
4974
|
+
isWatchMode,
|
|
4975
|
+
createDispatchRouter,
|
|
4976
|
+
fatalErrorRef,
|
|
4977
|
+
watchSignals,
|
|
4978
|
+
setDispatchPageResolver,
|
|
4979
|
+
createWatchSession,
|
|
4980
|
+
collectProjectEntries: ()=>browserRsbuild_collectProjectEntries(context),
|
|
4981
|
+
logWatchReady: ()=>logWatchReadyMessage(context, enableCliShortcuts),
|
|
4982
|
+
destroyRuntime: ()=>destroyBrowserRuntime(runtime)
|
|
4983
|
+
};
|
|
4984
|
+
const { testTime, watchSession, close } = useHeadlessDirect ? await createHeadlessScheduler({
|
|
4985
|
+
...schedulerDeps,
|
|
4986
|
+
browser,
|
|
4987
|
+
browserLaunchOptions,
|
|
4988
|
+
projectServers: runtime.projectServers,
|
|
4989
|
+
projectRuntimeConfigs,
|
|
4990
|
+
watchState,
|
|
4991
|
+
handlers: {
|
|
4992
|
+
handleFatal,
|
|
4993
|
+
handleTestFileComplete
|
|
4985
4994
|
}
|
|
4986
|
-
|
|
4987
|
-
|
|
4988
|
-
|
|
4989
|
-
|
|
4990
|
-
|
|
4991
|
-
|
|
4992
|
-
|
|
4993
|
-
|
|
4994
|
-
|
|
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);
|
|
4995
|
+
}) : await createHeadedScheduler({
|
|
4996
|
+
...schedulerDeps,
|
|
4997
|
+
runtime,
|
|
4998
|
+
handlers: {
|
|
4999
|
+
handleTestFileStart,
|
|
5000
|
+
handleTestCaseResult,
|
|
5001
|
+
handleTestFileComplete,
|
|
5002
|
+
handleLog,
|
|
5003
|
+
handleFatal
|
|
5016
5004
|
}
|
|
5017
|
-
};
|
|
5018
|
-
|
|
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
|
-
};
|
|
5005
|
+
});
|
|
5006
|
+
if (fatalErrorRef.current) return failWithError(fatalErrorRef.current, close);
|
|
5033
5007
|
context.updateReporterResultState(reporterResults, caseResults);
|
|
5034
|
-
|
|
5035
|
-
|
|
5036
|
-
const cycleCoverageMap = isWatchMode && coverageProvider ? buildBrowserCoverageMap(reporterResults, coverageProvider) : void 0;
|
|
5037
|
-
const result = {
|
|
5008
|
+
watchState.hooksEnabled = isWatchMode;
|
|
5009
|
+
return {
|
|
5038
5010
|
results: reporterResults,
|
|
5039
5011
|
testResults: caseResults,
|
|
5040
|
-
duration
|
|
5041
|
-
|
|
5012
|
+
duration: {
|
|
5013
|
+
totalTime: buildTime + testTime,
|
|
5014
|
+
buildTime,
|
|
5015
|
+
testTime
|
|
5016
|
+
},
|
|
5017
|
+
hasFailure: reporterResults.some((result)=>'fail' === result.status),
|
|
5042
5018
|
getSourcemap: getBrowserSourcemap,
|
|
5043
5019
|
resolveSourcemap: resolveBrowserSourcemap,
|
|
5044
|
-
close
|
|
5045
|
-
|
|
5046
|
-
watch: watchHandles
|
|
5020
|
+
close,
|
|
5021
|
+
watchSession
|
|
5047
5022
|
};
|
|
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
5023
|
};
|
|
5062
5024
|
const listBrowserTests = async (context, options)=>{
|
|
5063
5025
|
const browserProjects = options?.projects ?? getBrowserProjects(context);
|
|
@@ -5116,7 +5078,8 @@ const listBrowserTests = async (context, options)=>{
|
|
|
5116
5078
|
environmentName: project.environmentName,
|
|
5117
5079
|
projectRoot: normalize(project.rootPath),
|
|
5118
5080
|
runtimeConfig: serializableConfig(projectRuntimeConfig(project, {
|
|
5119
|
-
envMode: 'static'
|
|
5081
|
+
envMode: 'static',
|
|
5082
|
+
envOverlay: options?.env
|
|
5120
5083
|
})),
|
|
5121
5084
|
viewport: project.normalizedConfig.browser.viewport
|
|
5122
5085
|
}));
|
|
@@ -5249,8 +5212,11 @@ const emptyOutcome = ()=>({
|
|
|
5249
5212
|
});
|
|
5250
5213
|
async function createBrowserExecutor(context, options) {
|
|
5251
5214
|
const { projects, coverageProvider, shardedEntries, freezeShardedEntries, filesOnly, allowEmptyRun, appliedModifyRstestConfigEnvironments } = options;
|
|
5215
|
+
const isWatchMode = 'watch' === context.command;
|
|
5252
5216
|
let deferredClose;
|
|
5253
5217
|
let inFlightCycle;
|
|
5218
|
+
let invalidationCallback;
|
|
5219
|
+
let watchSession;
|
|
5254
5220
|
const foldOutcome = (result)=>{
|
|
5255
5221
|
if (!result) return emptyOutcome();
|
|
5256
5222
|
const map = buildBrowserCoverageMap(result.results, coverageProvider);
|
|
@@ -5274,6 +5240,15 @@ async function createBrowserExecutor(context, options) {
|
|
|
5274
5240
|
projects,
|
|
5275
5241
|
async init () {},
|
|
5276
5242
|
async runCycle (opts) {
|
|
5243
|
+
if (watchSession) {
|
|
5244
|
+
const cycle = watchSession.runCycle(opts.fileFilters ?? []);
|
|
5245
|
+
inFlightCycle = cycle;
|
|
5246
|
+
try {
|
|
5247
|
+
return await cycle;
|
|
5248
|
+
} finally{
|
|
5249
|
+
inFlightCycle = void 0;
|
|
5250
|
+
}
|
|
5251
|
+
}
|
|
5277
5252
|
const cycle = runBrowserController(context, {
|
|
5278
5253
|
projects,
|
|
5279
5254
|
shardedEntries,
|
|
@@ -5282,24 +5257,37 @@ async function createBrowserExecutor(context, options) {
|
|
|
5282
5257
|
appliedModifyRstestConfigEnvironments,
|
|
5283
5258
|
onTraceEvents: opts.onTraceEvents,
|
|
5284
5259
|
env: opts.env,
|
|
5285
|
-
updateSnapshot: opts.updateSnapshot
|
|
5260
|
+
updateSnapshot: opts.updateSnapshot,
|
|
5261
|
+
onInvalidate: isWatchMode ? (hint)=>invalidationCallback?.(hint) : void 0
|
|
5286
5262
|
});
|
|
5287
5263
|
inFlightCycle = cycle;
|
|
5288
5264
|
try {
|
|
5289
5265
|
const result = await cycle;
|
|
5290
5266
|
deferredClose = result?.close;
|
|
5267
|
+
watchSession = result?.watchSession;
|
|
5291
5268
|
return foldOutcome(result);
|
|
5292
5269
|
} finally{
|
|
5293
5270
|
inFlightCycle = void 0;
|
|
5294
5271
|
}
|
|
5295
5272
|
},
|
|
5296
|
-
|
|
5273
|
+
onInvalidate (cb) {
|
|
5274
|
+
invalidationCallback = cb;
|
|
5275
|
+
},
|
|
5276
|
+
hasWatchSession () {
|
|
5277
|
+
return void 0 !== watchSession;
|
|
5278
|
+
},
|
|
5279
|
+
async requestRerun (testPaths) {
|
|
5280
|
+
if (!watchSession) return void logger.log(color.yellow('\nBrowser Mode has no live watch session, so this rerun skipped it.'));
|
|
5281
|
+
await watchSession.requestRerun(testPaths);
|
|
5282
|
+
},
|
|
5283
|
+
async collect (opts) {
|
|
5297
5284
|
const pending = listBrowserTests(context, {
|
|
5298
5285
|
projects,
|
|
5299
5286
|
shardedEntries,
|
|
5300
5287
|
freezeShardedEntries,
|
|
5301
5288
|
filesOnly,
|
|
5302
|
-
appliedModifyRstestConfigEnvironments
|
|
5289
|
+
appliedModifyRstestConfigEnvironments,
|
|
5290
|
+
env: opts.env
|
|
5303
5291
|
});
|
|
5304
5292
|
inFlightCycle = pending;
|
|
5305
5293
|
try {
|
|
@@ -5314,6 +5302,11 @@ async function createBrowserExecutor(context, options) {
|
|
|
5314
5302
|
},
|
|
5315
5303
|
async close () {
|
|
5316
5304
|
if (inFlightCycle) await inFlightCycle.catch(()=>void 0);
|
|
5305
|
+
if (isWatchMode) {
|
|
5306
|
+
watchSession = void 0;
|
|
5307
|
+
await cleanupWatchRuntime();
|
|
5308
|
+
return;
|
|
5309
|
+
}
|
|
5317
5310
|
const close = deferredClose;
|
|
5318
5311
|
deferredClose = void 0;
|
|
5319
5312
|
await close?.();
|