@rstest/browser 0.8.4 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE-APACHE-2.0 +202 -0
- package/NOTICE +11 -0
- package/dist/361.js +8 -0
- package/dist/augmentExpect.d.ts +73 -0
- package/dist/browser-container/container-static/css/index.5c72297783.css +1 -0
- package/dist/browser-container/container-static/js/{392.28f9a733.js → 101.36a8ccdf84.js} +4068 -3904
- package/dist/browser-container/container-static/js/101.36a8ccdf84.js.LICENSE.txt +1 -0
- package/dist/browser-container/container-static/js/{index.129eaf9c.js → index.0687a8142a.js} +742 -692
- package/dist/browser-container/container-static/js/{lib-react.97ee79b0.js → lib-react.dcf2a5e57a.js} +10 -10
- package/dist/browser-container/container-static/js/lib-react.dcf2a5e57a.js.LICENSE.txt +1 -0
- package/dist/browser-container/index.html +1 -1
- package/dist/browser.d.ts +2 -0
- package/dist/browser.js +583 -0
- package/dist/browserRpcRegistry.d.ts +18 -0
- package/dist/client/api.d.ts +3 -0
- package/dist/client/browserRpc.d.ts +2 -0
- package/dist/client/dispatchTransport.d.ts +11 -0
- package/dist/client/entry.d.ts +1 -5
- package/dist/client/locator.d.ts +125 -0
- package/dist/client/snapshot.d.ts +0 -6
- package/dist/concurrency.d.ts +12 -0
- package/dist/dispatchCapabilities.d.ts +34 -0
- package/dist/dispatchRouter.d.ts +20 -0
- package/dist/headlessLatestRerunScheduler.d.ts +19 -0
- package/dist/headlessTransport.d.ts +12 -0
- package/dist/index.js +1608 -296
- package/dist/protocol.d.ts +44 -33
- package/dist/providers/index.d.ts +79 -0
- package/dist/providers/playwright/compileLocator.d.ts +3 -0
- package/dist/providers/playwright/dispatchBrowserRpc.d.ts +13 -0
- package/dist/providers/playwright/expectUtils.d.ts +24 -0
- package/dist/providers/playwright/implementation.d.ts +2 -0
- package/dist/providers/playwright/index.d.ts +1 -0
- package/dist/providers/playwright/runtime.d.ts +5 -0
- package/dist/providers/playwright/textMatcher.d.ts +8 -0
- package/dist/rpcProtocol.d.ts +145 -0
- package/dist/runSession.d.ts +33 -0
- package/dist/sessionRegistry.d.ts +34 -0
- package/dist/sourceMap/sourceMapLoader.d.ts +14 -0
- package/dist/watchRerunPlanner.d.ts +21 -0
- package/package.json +16 -11
- package/src/AGENTS.md +128 -0
- package/src/augmentExpect.ts +62 -0
- package/src/browser.ts +3 -0
- package/src/browserRpcRegistry.ts +57 -0
- package/src/client/AGENTS.md +82 -0
- package/src/client/api.ts +213 -0
- package/src/client/browserRpc.ts +86 -0
- package/src/client/dispatchTransport.ts +178 -0
- package/src/client/entry.ts +109 -39
- package/src/client/locator.ts +452 -0
- package/src/client/snapshot.ts +32 -97
- package/src/client/sourceMapSupport.ts +26 -37
- package/src/concurrency.ts +62 -0
- package/src/dispatchCapabilities.ts +162 -0
- package/src/dispatchRouter.ts +82 -0
- package/src/env.d.ts +8 -1
- package/src/headlessLatestRerunScheduler.ts +76 -0
- package/src/headlessTransport.ts +28 -0
- package/src/hostController.ts +1292 -367
- package/src/protocol.ts +66 -31
- package/src/providers/index.ts +103 -0
- package/src/providers/playwright/compileLocator.ts +130 -0
- package/src/providers/playwright/dispatchBrowserRpc.ts +372 -0
- package/src/providers/playwright/expectUtils.ts +57 -0
- package/src/providers/playwright/implementation.ts +33 -0
- package/src/providers/playwright/index.ts +1 -0
- package/src/providers/playwright/runtime.ts +32 -0
- package/src/providers/playwright/textMatcher.ts +10 -0
- package/src/rpcProtocol.ts +220 -0
- package/src/runSession.ts +110 -0
- package/src/sessionRegistry.ts +89 -0
- package/src/sourceMap/sourceMapLoader.ts +96 -0
- package/src/watchRerunPlanner.ts +77 -0
- package/dist/browser-container/container-static/css/index.5a71c757.css +0 -1
- package/dist/browser-container/container-static/js/392.28f9a733.js.LICENSE.txt +0 -1
- package/dist/browser-container/container-static/js/lib-react.97ee79b0.js.LICENSE.txt +0 -1
- package/dist/browser-container/container-static/js/scheduler.6976de44.js +0 -411
- package/dist/browser-container/scheduler.html +0 -19
package/dist/index.js
CHANGED
|
@@ -7,6 +7,9 @@ import open_editor from "open-editor";
|
|
|
7
7
|
import { basename, dirname, join, normalize, relative, resolve as external_pathe_resolve } from "pathe";
|
|
8
8
|
import sirv from "sirv";
|
|
9
9
|
import { WebSocketServer } from "ws";
|
|
10
|
+
import node_os from "node:os";
|
|
11
|
+
import convert_source_map from "convert-source-map";
|
|
12
|
+
import { DISPATCH_RPC_BRIDGE_NAME, DISPATCH_MESSAGE_TYPE, DISPATCH_NAMESPACE_RUNNER } from "./361.js";
|
|
10
13
|
__webpack_require__.add({
|
|
11
14
|
"../../node_modules/.pnpm/picomatch@4.0.3/node_modules/picomatch/index.js" (module, __unused_rspack_exports, __webpack_require__) {
|
|
12
15
|
const pico = __webpack_require__("../../node_modules/.pnpm/picomatch@4.0.3/node_modules/picomatch/lib/picomatch.js");
|
|
@@ -1417,15 +1420,32 @@ __webpack_require__.add({
|
|
|
1417
1420
|
});
|
|
1418
1421
|
const TYPE_REQUEST = "q";
|
|
1419
1422
|
const TYPE_RESPONSE = "s";
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
+
function createPromiseWithResolvers() {
|
|
1424
|
+
let resolve;
|
|
1425
|
+
let reject;
|
|
1426
|
+
return {
|
|
1427
|
+
promise: new Promise((res, rej)=>{
|
|
1428
|
+
resolve = res;
|
|
1429
|
+
reject = rej;
|
|
1430
|
+
}),
|
|
1431
|
+
resolve,
|
|
1432
|
+
reject
|
|
1433
|
+
};
|
|
1423
1434
|
}
|
|
1435
|
+
const random = Math.random.bind(Math);
|
|
1436
|
+
const urlAlphabet = "useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict";
|
|
1437
|
+
function nanoid(size = 21) {
|
|
1438
|
+
let id = "";
|
|
1439
|
+
let i = size;
|
|
1440
|
+
while(i--)id += urlAlphabet[64 * random() | 0];
|
|
1441
|
+
return id;
|
|
1442
|
+
}
|
|
1443
|
+
const DEFAULT_TIMEOUT = 6e4;
|
|
1444
|
+
const defaultSerialize = (i)=>i;
|
|
1424
1445
|
const defaultDeserialize = defaultSerialize;
|
|
1425
1446
|
const { clearTimeout: dist_clearTimeout, setTimeout: dist_setTimeout } = globalThis;
|
|
1426
|
-
const random = Math.random.bind(Math);
|
|
1427
1447
|
function createBirpc($functions, options) {
|
|
1428
|
-
const { post, on, off = ()=>{}, eventNames = [], serialize = defaultSerialize, deserialize = defaultDeserialize, resolver, bind = "rpc", timeout = DEFAULT_TIMEOUT } = options;
|
|
1448
|
+
const { post, on, off = ()=>{}, eventNames = [], serialize = defaultSerialize, deserialize = defaultDeserialize, resolver, bind = "rpc", timeout = DEFAULT_TIMEOUT, proxify = true } = options;
|
|
1429
1449
|
let $closed = false;
|
|
1430
1450
|
const _rpcPromiseMap = /* @__PURE__ */ new Map();
|
|
1431
1451
|
let _promiseInit;
|
|
@@ -1453,8 +1473,7 @@ function createBirpc($functions, options) {
|
|
|
1453
1473
|
if (timeout >= 0) {
|
|
1454
1474
|
timeoutId = dist_setTimeout(()=>{
|
|
1455
1475
|
try {
|
|
1456
|
-
|
|
1457
|
-
if (true !== handleResult) throw new Error(`[birpc] timeout on calling "${method}"`);
|
|
1476
|
+
if (options.onTimeoutError?.call(rpc, method, args) !== true) throw new Error(`[birpc] timeout on calling "${method}"`);
|
|
1458
1477
|
} catch (e) {
|
|
1459
1478
|
reject(e);
|
|
1460
1479
|
}
|
|
@@ -1483,15 +1502,11 @@ function createBirpc($functions, options) {
|
|
|
1483
1502
|
}
|
|
1484
1503
|
return promise;
|
|
1485
1504
|
}
|
|
1486
|
-
const $call = (method, ...args)=>_call(method, args, false);
|
|
1487
|
-
const $callOptional = (method, ...args)=>_call(method, args, false, true);
|
|
1488
|
-
const $callEvent = (method, ...args)=>_call(method, args, true);
|
|
1489
|
-
const $callRaw = (options2)=>_call(options2.method, options2.args, options2.event, options2.optional);
|
|
1490
1505
|
const builtinMethods = {
|
|
1491
|
-
$call,
|
|
1492
|
-
$callOptional,
|
|
1493
|
-
$callEvent,
|
|
1494
|
-
$callRaw,
|
|
1506
|
+
$call: (method, ...args)=>_call(method, args, false),
|
|
1507
|
+
$callOptional: (method, ...args)=>_call(method, args, false, true),
|
|
1508
|
+
$callEvent: (method, ...args)=>_call(method, args, true),
|
|
1509
|
+
$callRaw: (options$1)=>_call(options$1.method, options$1.args, options$1.event, options$1.optional),
|
|
1495
1510
|
$rejectPendingCalls,
|
|
1496
1511
|
get $closed () {
|
|
1497
1512
|
return $closed;
|
|
@@ -1502,7 +1517,7 @@ function createBirpc($functions, options) {
|
|
|
1502
1517
|
$close,
|
|
1503
1518
|
$functions
|
|
1504
1519
|
};
|
|
1505
|
-
rpc = new Proxy({}, {
|
|
1520
|
+
rpc = proxify ? new Proxy({}, {
|
|
1506
1521
|
get (_, method) {
|
|
1507
1522
|
if (Object.prototype.hasOwnProperty.call(builtinMethods, method)) return builtinMethods[method];
|
|
1508
1523
|
if ("then" === method && !eventNames.includes("then") && !("then" in $functions)) return;
|
|
@@ -1515,11 +1530,11 @@ function createBirpc($functions, options) {
|
|
|
1515
1530
|
sendCall.asEvent = sendEvent;
|
|
1516
1531
|
return sendCall;
|
|
1517
1532
|
}
|
|
1518
|
-
});
|
|
1533
|
+
}) : builtinMethods;
|
|
1519
1534
|
function $close(customError) {
|
|
1520
1535
|
$closed = true;
|
|
1521
1536
|
_rpcPromiseMap.forEach(({ reject, method })=>{
|
|
1522
|
-
const error = new Error(`[birpc] rpc is closed, cannot call "${method}"`);
|
|
1537
|
+
const error = /* @__PURE__ */ new Error(`[birpc] rpc is closed, cannot call "${method}"`);
|
|
1523
1538
|
if (customError) {
|
|
1524
1539
|
customError.cause ??= error;
|
|
1525
1540
|
return reject(customError);
|
|
@@ -1530,9 +1545,8 @@ function createBirpc($functions, options) {
|
|
|
1530
1545
|
off(onMessage);
|
|
1531
1546
|
}
|
|
1532
1547
|
function $rejectPendingCalls(handler) {
|
|
1533
|
-
const
|
|
1534
|
-
|
|
1535
|
-
if (!handler) return reject(new Error(`[birpc]: rejected pending call "${method}".`));
|
|
1548
|
+
const handlerResults = Array.from(_rpcPromiseMap.values()).map(({ method, reject })=>{
|
|
1549
|
+
if (!handler) return reject(/* @__PURE__ */ new Error(`[birpc]: rejected pending call "${method}".`));
|
|
1536
1550
|
return handler({
|
|
1537
1551
|
method,
|
|
1538
1552
|
reject
|
|
@@ -1559,9 +1573,8 @@ function createBirpc($functions, options) {
|
|
|
1559
1573
|
} catch (e) {
|
|
1560
1574
|
error = e;
|
|
1561
1575
|
}
|
|
1562
|
-
else error = new Error(`[birpc] function "${method}" not found`);
|
|
1576
|
+
else error = /* @__PURE__ */ new Error(`[birpc] function "${method}" not found`);
|
|
1563
1577
|
if (msg.i) {
|
|
1564
|
-
if (error && options.onError) options.onError.call(rpc, error, method, args);
|
|
1565
1578
|
if (error && options.onFunctionError) {
|
|
1566
1579
|
if (true === options.onFunctionError.call(rpc, error, method, args)) return;
|
|
1567
1580
|
}
|
|
@@ -1600,26 +1613,909 @@ function createBirpc($functions, options) {
|
|
|
1600
1613
|
_promiseInit = on(onMessage);
|
|
1601
1614
|
return rpc;
|
|
1602
1615
|
}
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
const
|
|
1607
|
-
|
|
1608
|
-
|
|
1616
|
+
const DEFAULT_MAX_HEADLESS_WORKERS = 12;
|
|
1617
|
+
const getNumCpus = ()=>node_os.availableParallelism?.() ?? node_os.cpus().length;
|
|
1618
|
+
const parseWorkers = (maxWorkers, numCpus = getNumCpus())=>{
|
|
1619
|
+
const parsed = Number.parseInt(maxWorkers.toString(), 10);
|
|
1620
|
+
if ('string' == typeof maxWorkers && maxWorkers.trim().endsWith('%')) {
|
|
1621
|
+
const workers = Math.floor(parsed / 100 * numCpus);
|
|
1622
|
+
return Math.max(workers, 1);
|
|
1623
|
+
}
|
|
1624
|
+
return parsed > 0 ? parsed : 1;
|
|
1625
|
+
};
|
|
1626
|
+
const resolveDefaultHeadlessWorkers = (command, numCpus = getNumCpus())=>{
|
|
1627
|
+
const baseWorkers = Math.max(Math.min(DEFAULT_MAX_HEADLESS_WORKERS, numCpus - 1), 1);
|
|
1628
|
+
return 'watch' === command ? Math.max(Math.floor(baseWorkers / 2), 1) : baseWorkers;
|
|
1629
|
+
};
|
|
1630
|
+
const getHeadlessConcurrency = (context, totalTests)=>{
|
|
1631
|
+
if (totalTests <= 0) return 1;
|
|
1632
|
+
const maxWorkers = context.normalizedConfig.pool.maxWorkers;
|
|
1633
|
+
if (void 0 !== maxWorkers) return Math.min(parseWorkers(maxWorkers), totalTests);
|
|
1634
|
+
return Math.min(resolveDefaultHeadlessWorkers(context.command), totalTests);
|
|
1635
|
+
};
|
|
1636
|
+
const toErrorMessage = (error)=>error instanceof Error ? error.message : String(error);
|
|
1637
|
+
class HostDispatchRouter {
|
|
1638
|
+
handlers = new Map();
|
|
1639
|
+
options;
|
|
1640
|
+
constructor(options){
|
|
1641
|
+
this.options = options ?? {};
|
|
1642
|
+
}
|
|
1643
|
+
register(namespace, handler) {
|
|
1644
|
+
this.handlers.set(namespace, handler);
|
|
1645
|
+
}
|
|
1646
|
+
unregister(namespace) {
|
|
1647
|
+
this.handlers.delete(namespace);
|
|
1648
|
+
}
|
|
1649
|
+
has(namespace) {
|
|
1650
|
+
return this.handlers.has(namespace);
|
|
1651
|
+
}
|
|
1652
|
+
async dispatch(request) {
|
|
1653
|
+
const runToken = request.runToken;
|
|
1654
|
+
if ('number' == typeof runToken && this.options.isRunTokenStale?.(runToken)) {
|
|
1655
|
+
this.options.onStale?.(request);
|
|
1656
|
+
return {
|
|
1657
|
+
requestId: request.requestId,
|
|
1658
|
+
runToken,
|
|
1659
|
+
stale: true
|
|
1660
|
+
};
|
|
1661
|
+
}
|
|
1662
|
+
const handler = this.handlers.get(request.namespace);
|
|
1663
|
+
if (!handler) return {
|
|
1664
|
+
requestId: request.requestId,
|
|
1665
|
+
runToken,
|
|
1666
|
+
error: `No dispatch handler registered for namespace "${request.namespace}"`
|
|
1667
|
+
};
|
|
1668
|
+
try {
|
|
1669
|
+
const result = await handler(request);
|
|
1670
|
+
return {
|
|
1671
|
+
requestId: request.requestId,
|
|
1672
|
+
runToken,
|
|
1673
|
+
result
|
|
1674
|
+
};
|
|
1675
|
+
} catch (error) {
|
|
1676
|
+
return {
|
|
1677
|
+
requestId: request.requestId,
|
|
1678
|
+
runToken,
|
|
1679
|
+
error: toErrorMessage(error)
|
|
1680
|
+
};
|
|
1681
|
+
}
|
|
1682
|
+
}
|
|
1683
|
+
}
|
|
1684
|
+
const toSnapshotRpcRequest = (request)=>{
|
|
1685
|
+
switch(request.method){
|
|
1686
|
+
case 'resolveSnapshotPath':
|
|
1687
|
+
return {
|
|
1688
|
+
id: request.requestId,
|
|
1689
|
+
method: 'resolveSnapshotPath',
|
|
1690
|
+
args: request.args
|
|
1691
|
+
};
|
|
1692
|
+
case 'readSnapshotFile':
|
|
1693
|
+
return {
|
|
1694
|
+
id: request.requestId,
|
|
1695
|
+
method: 'readSnapshotFile',
|
|
1696
|
+
args: request.args
|
|
1697
|
+
};
|
|
1698
|
+
case 'saveSnapshotFile':
|
|
1699
|
+
return {
|
|
1700
|
+
id: request.requestId,
|
|
1701
|
+
method: 'saveSnapshotFile',
|
|
1702
|
+
args: request.args
|
|
1703
|
+
};
|
|
1704
|
+
case 'removeSnapshotFile':
|
|
1705
|
+
return {
|
|
1706
|
+
id: request.requestId,
|
|
1707
|
+
method: 'removeSnapshotFile',
|
|
1708
|
+
args: request.args
|
|
1709
|
+
};
|
|
1710
|
+
default:
|
|
1711
|
+
return null;
|
|
1712
|
+
}
|
|
1713
|
+
};
|
|
1714
|
+
const createHostDispatchRouter = ({ routerOptions, runnerCallbacks, runSnapshotRpc, extensionHandlers, onDuplicateNamespace })=>{
|
|
1715
|
+
const router = new HostDispatchRouter(routerOptions);
|
|
1716
|
+
router.register('runner', async (request)=>{
|
|
1717
|
+
switch(request.method){
|
|
1718
|
+
case 'file-start':
|
|
1719
|
+
await runnerCallbacks.onTestFileStart(request.args);
|
|
1720
|
+
break;
|
|
1721
|
+
case 'file-ready':
|
|
1722
|
+
await runnerCallbacks.onTestFileReady(request.args);
|
|
1723
|
+
break;
|
|
1724
|
+
case 'suite-start':
|
|
1725
|
+
await runnerCallbacks.onTestSuiteStart(request.args);
|
|
1726
|
+
break;
|
|
1727
|
+
case 'suite-result':
|
|
1728
|
+
await runnerCallbacks.onTestSuiteResult(request.args);
|
|
1729
|
+
break;
|
|
1730
|
+
case 'case-start':
|
|
1731
|
+
await runnerCallbacks.onTestCaseStart(request.args);
|
|
1732
|
+
break;
|
|
1733
|
+
case 'case-result':
|
|
1734
|
+
await runnerCallbacks.onTestCaseResult(request.args);
|
|
1735
|
+
break;
|
|
1736
|
+
case 'file-complete':
|
|
1737
|
+
await runnerCallbacks.onTestFileComplete(request.args);
|
|
1738
|
+
break;
|
|
1739
|
+
case 'log':
|
|
1740
|
+
await runnerCallbacks.onLog(request.args);
|
|
1741
|
+
break;
|
|
1742
|
+
case 'fatal':
|
|
1743
|
+
await runnerCallbacks.onFatal(request.args);
|
|
1744
|
+
break;
|
|
1745
|
+
default:
|
|
1746
|
+
break;
|
|
1747
|
+
}
|
|
1748
|
+
});
|
|
1749
|
+
router.register('snapshot', async (request)=>{
|
|
1750
|
+
const snapshotRequest = toSnapshotRpcRequest(request);
|
|
1751
|
+
if (!snapshotRequest) return;
|
|
1752
|
+
return runSnapshotRpc(snapshotRequest);
|
|
1609
1753
|
});
|
|
1754
|
+
for (const [namespace, handler] of extensionHandlers ?? []){
|
|
1755
|
+
if (router.has(namespace)) {
|
|
1756
|
+
onDuplicateNamespace?.(namespace);
|
|
1757
|
+
continue;
|
|
1758
|
+
}
|
|
1759
|
+
router.register(namespace, handler);
|
|
1760
|
+
}
|
|
1761
|
+
return router;
|
|
1762
|
+
};
|
|
1763
|
+
const createHeadlessLatestRerunScheduler = (options)=>{
|
|
1764
|
+
let pendingFiles = null;
|
|
1765
|
+
let draining = null;
|
|
1766
|
+
let latestEnqueueVersion = 0;
|
|
1767
|
+
const runDrainLoop = async ()=>{
|
|
1768
|
+
while(pendingFiles){
|
|
1769
|
+
const nextFiles = pendingFiles;
|
|
1770
|
+
pendingFiles = null;
|
|
1771
|
+
try {
|
|
1772
|
+
await options.runFiles(nextFiles);
|
|
1773
|
+
} catch (error) {
|
|
1774
|
+
try {
|
|
1775
|
+
await options.onError?.(error);
|
|
1776
|
+
} catch {}
|
|
1777
|
+
}
|
|
1778
|
+
}
|
|
1779
|
+
};
|
|
1780
|
+
const ensureDrainLoop = ()=>{
|
|
1781
|
+
if (draining) return;
|
|
1782
|
+
draining = runDrainLoop().finally(()=>{
|
|
1783
|
+
draining = null;
|
|
1784
|
+
});
|
|
1785
|
+
};
|
|
1610
1786
|
return {
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1787
|
+
async enqueueLatest (files) {
|
|
1788
|
+
const enqueueVersion = ++latestEnqueueVersion;
|
|
1789
|
+
const activeRun = options.getActiveRun();
|
|
1790
|
+
if (activeRun && !options.isRunCancelled(activeRun)) {
|
|
1791
|
+
options.onInterrupt?.(activeRun);
|
|
1792
|
+
options.invalidateActiveRun();
|
|
1793
|
+
await options.interruptActiveRun(activeRun);
|
|
1794
|
+
}
|
|
1795
|
+
if (enqueueVersion !== latestEnqueueVersion) return;
|
|
1796
|
+
pendingFiles = files;
|
|
1797
|
+
ensureDrainLoop();
|
|
1798
|
+
},
|
|
1799
|
+
async whenIdle () {
|
|
1800
|
+
await draining;
|
|
1801
|
+
}
|
|
1802
|
+
};
|
|
1803
|
+
};
|
|
1804
|
+
const attachHeadlessRunnerTransport = async (page, handlers)=>{
|
|
1805
|
+
await page.exposeFunction(DISPATCH_MESSAGE_TYPE, handlers.onDispatchMessage);
|
|
1806
|
+
await page.exposeFunction(DISPATCH_RPC_BRIDGE_NAME, handlers.onDispatchRpc);
|
|
1807
|
+
};
|
|
1808
|
+
const isRecord = (value)=>'object' == typeof value && null !== value;
|
|
1809
|
+
const readString = (value, key, label)=>{
|
|
1810
|
+
const result = value[key];
|
|
1811
|
+
if ('string' != typeof result) throw new Error(`Invalid browser RPC request: ${label} must be a string`);
|
|
1812
|
+
return result;
|
|
1813
|
+
};
|
|
1814
|
+
const readUnknownArray = (value, key, label)=>{
|
|
1815
|
+
const result = value[key];
|
|
1816
|
+
if (!Array.isArray(result)) throw new Error(`Invalid browser RPC request: ${label} must be an array`);
|
|
1817
|
+
return result;
|
|
1818
|
+
};
|
|
1819
|
+
const parseBrowserLocatorIR = (value, label)=>{
|
|
1820
|
+
if (!isRecord(value)) throw new Error(`Invalid browser RPC request: ${label} must be an object`);
|
|
1821
|
+
const steps = value.steps;
|
|
1822
|
+
if (!Array.isArray(steps)) throw new Error(`Invalid browser RPC request: ${label}.steps must be an array`);
|
|
1823
|
+
return value;
|
|
1824
|
+
};
|
|
1825
|
+
const validateBrowserRpcRequest = (payload)=>{
|
|
1826
|
+
if (!isRecord(payload)) throw new Error('Invalid browser RPC request: payload must be an object');
|
|
1827
|
+
const kind = readString(payload, 'kind', 'kind');
|
|
1828
|
+
if ('locator' !== kind && 'expect' !== kind && 'config' !== kind) throw new Error(`Invalid browser RPC request: unsupported kind ${JSON.stringify(kind)}`);
|
|
1829
|
+
const request = {
|
|
1830
|
+
id: readString(payload, 'id', 'id'),
|
|
1831
|
+
testPath: readString(payload, 'testPath', 'testPath'),
|
|
1832
|
+
runId: readString(payload, 'runId', 'runId'),
|
|
1833
|
+
kind,
|
|
1834
|
+
locator: parseBrowserLocatorIR(payload.locator, 'locator'),
|
|
1835
|
+
method: readString(payload, 'method', 'method'),
|
|
1836
|
+
args: readUnknownArray(payload, 'args', 'args')
|
|
1614
1837
|
};
|
|
1838
|
+
const isNot = payload.isNot;
|
|
1839
|
+
if (void 0 !== isNot) {
|
|
1840
|
+
if ('boolean' != typeof isNot) throw new Error('Invalid browser RPC request: isNot must be a boolean');
|
|
1841
|
+
request.isNot = isNot;
|
|
1842
|
+
}
|
|
1843
|
+
const timeout = payload.timeout;
|
|
1844
|
+
if (void 0 !== timeout) {
|
|
1845
|
+
if ('number' != typeof timeout) throw new Error('Invalid browser RPC request: timeout must be a number');
|
|
1846
|
+
request.timeout = timeout;
|
|
1847
|
+
}
|
|
1848
|
+
return request;
|
|
1849
|
+
};
|
|
1850
|
+
const supportedLocatorActions = new Set([
|
|
1851
|
+
'click',
|
|
1852
|
+
'dblclick',
|
|
1853
|
+
'fill',
|
|
1854
|
+
'hover',
|
|
1855
|
+
'press',
|
|
1856
|
+
'clear',
|
|
1857
|
+
'check',
|
|
1858
|
+
'uncheck',
|
|
1859
|
+
'focus',
|
|
1860
|
+
'blur',
|
|
1861
|
+
'scrollIntoViewIfNeeded',
|
|
1862
|
+
'waitFor',
|
|
1863
|
+
'dispatchEvent',
|
|
1864
|
+
'selectOption',
|
|
1865
|
+
'setInputFiles'
|
|
1866
|
+
]);
|
|
1867
|
+
const supportedExpectElementMatchers = new Set([
|
|
1868
|
+
'toBeVisible',
|
|
1869
|
+
'toBeHidden',
|
|
1870
|
+
'toBeEnabled',
|
|
1871
|
+
'toBeDisabled',
|
|
1872
|
+
'toBeAttached',
|
|
1873
|
+
'toBeDetached',
|
|
1874
|
+
'toBeEditable',
|
|
1875
|
+
'toBeFocused',
|
|
1876
|
+
'toBeEmpty',
|
|
1877
|
+
'toBeInViewport',
|
|
1878
|
+
'toHaveText',
|
|
1879
|
+
'toContainText',
|
|
1880
|
+
'toHaveValue',
|
|
1881
|
+
'toHaveAttribute',
|
|
1882
|
+
'toHaveClass',
|
|
1883
|
+
'toHaveCount',
|
|
1884
|
+
'toBeChecked',
|
|
1885
|
+
'toBeUnchecked',
|
|
1886
|
+
'toHaveId',
|
|
1887
|
+
'toHaveCSS',
|
|
1888
|
+
'toHaveJSProperty'
|
|
1889
|
+
]);
|
|
1890
|
+
const reviveBrowserLocatorText = (text)=>{
|
|
1891
|
+
if ('string' === text.type) return text.value;
|
|
1892
|
+
return new RegExp(text.source, text.flags);
|
|
1893
|
+
};
|
|
1894
|
+
const compilePlaywrightLocator = (frame, locatorIR)=>{
|
|
1895
|
+
const compileFromFrame = (ir)=>{
|
|
1896
|
+
let current = frame;
|
|
1897
|
+
const ensureLocator = ()=>{
|
|
1898
|
+
if (current.filter) return current;
|
|
1899
|
+
current = current.locator(':root');
|
|
1900
|
+
return current;
|
|
1901
|
+
};
|
|
1902
|
+
for (const step of ir.steps)switch(step.type){
|
|
1903
|
+
case 'getByRole':
|
|
1904
|
+
{
|
|
1905
|
+
const name = step.options?.name ? reviveBrowserLocatorText(step.options.name) : void 0;
|
|
1906
|
+
const options = step.options ? {
|
|
1907
|
+
...step.options,
|
|
1908
|
+
name
|
|
1909
|
+
} : void 0;
|
|
1910
|
+
current = current.getByRole(step.role, options);
|
|
1911
|
+
break;
|
|
1912
|
+
}
|
|
1913
|
+
case 'locator':
|
|
1914
|
+
current = current.locator(step.selector);
|
|
1915
|
+
break;
|
|
1916
|
+
case 'getByText':
|
|
1917
|
+
current = current.getByText(reviveBrowserLocatorText(step.text), step.options);
|
|
1918
|
+
break;
|
|
1919
|
+
case 'getByLabel':
|
|
1920
|
+
current = current.getByLabel(reviveBrowserLocatorText(step.text), step.options);
|
|
1921
|
+
break;
|
|
1922
|
+
case 'getByPlaceholder':
|
|
1923
|
+
current = current.getByPlaceholder(reviveBrowserLocatorText(step.text), step.options);
|
|
1924
|
+
break;
|
|
1925
|
+
case 'getByAltText':
|
|
1926
|
+
current = current.getByAltText(reviveBrowserLocatorText(step.text), step.options);
|
|
1927
|
+
break;
|
|
1928
|
+
case 'getByTitle':
|
|
1929
|
+
current = current.getByTitle(reviveBrowserLocatorText(step.text), step.options);
|
|
1930
|
+
break;
|
|
1931
|
+
case 'getByTestId':
|
|
1932
|
+
current = current.getByTestId(reviveBrowserLocatorText(step.text));
|
|
1933
|
+
break;
|
|
1934
|
+
case 'filter':
|
|
1935
|
+
{
|
|
1936
|
+
const locator = ensureLocator();
|
|
1937
|
+
const options = {};
|
|
1938
|
+
if (step.options?.hasText) options.hasText = reviveBrowserLocatorText(step.options.hasText);
|
|
1939
|
+
if (step.options?.hasNotText) options.hasNotText = reviveBrowserLocatorText(step.options.hasNotText);
|
|
1940
|
+
if (step.options?.has) options.has = compileFromFrame(step.options.has);
|
|
1941
|
+
if (step.options?.hasNot) options.hasNot = compileFromFrame(step.options.hasNot);
|
|
1942
|
+
current = locator.filter(options);
|
|
1943
|
+
break;
|
|
1944
|
+
}
|
|
1945
|
+
case 'and':
|
|
1946
|
+
{
|
|
1947
|
+
const locator = ensureLocator();
|
|
1948
|
+
const other = compileFromFrame(step.locator);
|
|
1949
|
+
current = locator.and(other);
|
|
1950
|
+
break;
|
|
1951
|
+
}
|
|
1952
|
+
case 'or':
|
|
1953
|
+
{
|
|
1954
|
+
const locator = ensureLocator();
|
|
1955
|
+
const other = compileFromFrame(step.locator);
|
|
1956
|
+
current = locator.or(other);
|
|
1957
|
+
break;
|
|
1958
|
+
}
|
|
1959
|
+
case 'nth':
|
|
1960
|
+
{
|
|
1961
|
+
const locator = ensureLocator();
|
|
1962
|
+
current = locator.nth(step.index);
|
|
1963
|
+
break;
|
|
1964
|
+
}
|
|
1965
|
+
case 'first':
|
|
1966
|
+
{
|
|
1967
|
+
const locator = ensureLocator();
|
|
1968
|
+
current = locator.first();
|
|
1969
|
+
break;
|
|
1970
|
+
}
|
|
1971
|
+
case 'last':
|
|
1972
|
+
{
|
|
1973
|
+
const locator = ensureLocator();
|
|
1974
|
+
current = locator.last();
|
|
1975
|
+
break;
|
|
1976
|
+
}
|
|
1977
|
+
default:
|
|
1978
|
+
throw new Error(`Unknown locator step: ${String(step?.type)}`);
|
|
1979
|
+
}
|
|
1980
|
+
return ensureLocator();
|
|
1981
|
+
};
|
|
1982
|
+
return compileFromFrame(locatorIR);
|
|
1983
|
+
};
|
|
1984
|
+
const serializeExpectedText = (text, options)=>{
|
|
1985
|
+
const base = {
|
|
1986
|
+
matchSubstring: options?.matchSubstring,
|
|
1987
|
+
ignoreCase: options?.ignoreCase,
|
|
1988
|
+
normalizeWhiteSpace: options?.normalizeWhiteSpace
|
|
1989
|
+
};
|
|
1990
|
+
if ('string' === text.type) return [
|
|
1991
|
+
{
|
|
1992
|
+
...base,
|
|
1993
|
+
string: text.value
|
|
1994
|
+
}
|
|
1995
|
+
];
|
|
1996
|
+
return [
|
|
1997
|
+
{
|
|
1998
|
+
...base,
|
|
1999
|
+
regexSource: text.source,
|
|
2000
|
+
regexFlags: text.flags
|
|
2001
|
+
}
|
|
2002
|
+
];
|
|
2003
|
+
};
|
|
2004
|
+
const formatExpectError = (result)=>{
|
|
2005
|
+
const parts = [];
|
|
2006
|
+
if (result.errorMessage) parts.push(result.errorMessage);
|
|
2007
|
+
if (result.log?.length) {
|
|
2008
|
+
parts.push('Call log:');
|
|
2009
|
+
parts.push(...result.log.map((l)=>`- ${l}`));
|
|
2010
|
+
}
|
|
2011
|
+
return parts.join('\n');
|
|
2012
|
+
};
|
|
2013
|
+
const escapeCssAttrValue = (value)=>value.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
|
|
2014
|
+
const getRunnerFrame = async (containerPage, testPath, timeoutMs)=>{
|
|
2015
|
+
const selector = `iframe[data-test-file='${escapeCssAttrValue(testPath)}']`;
|
|
2016
|
+
const iframe = containerPage.locator(selector);
|
|
2017
|
+
const count = await iframe.count();
|
|
2018
|
+
if (0 === count) {
|
|
2019
|
+
const known = await containerPage.locator('iframe[data-test-file]').evaluateAll((nodes)=>nodes.map((n)=>n.dataset.testFile));
|
|
2020
|
+
throw new Error(`Runner iframe not found for testPath: ${JSON.stringify(testPath)}. Known iframes: ${JSON.stringify(known)}. Timeout: ${timeoutMs}ms`);
|
|
2021
|
+
}
|
|
2022
|
+
return containerPage.frameLocator(selector);
|
|
2023
|
+
};
|
|
2024
|
+
const callExpect = async (locator, expectMethod, options, fallbackMessage)=>{
|
|
2025
|
+
const result = await locator._expect(expectMethod, options);
|
|
2026
|
+
if (!options.isNot !== result.matches) throw new Error(formatExpectError(result) || fallbackMessage);
|
|
2027
|
+
return null;
|
|
2028
|
+
};
|
|
2029
|
+
const assertSerializedText = (value, matcherName)=>{
|
|
2030
|
+
const t = value;
|
|
2031
|
+
if (!t || 'string' !== t.type && 'regexp' !== t.type) throw new Error(`${matcherName} expects a serialized text matcher`);
|
|
2032
|
+
return t;
|
|
2033
|
+
};
|
|
2034
|
+
const assertStringArg = (value, matcherName, label)=>{
|
|
2035
|
+
if ('string' != typeof value || !value) throw new Error(`${matcherName} expects ${label}`);
|
|
2036
|
+
return value;
|
|
2037
|
+
};
|
|
2038
|
+
const simpleMatchers = {
|
|
2039
|
+
toBeVisible: 'to.be.visible',
|
|
2040
|
+
toBeHidden: 'to.be.hidden',
|
|
2041
|
+
toBeEnabled: 'to.be.enabled',
|
|
2042
|
+
toBeDisabled: 'to.be.disabled',
|
|
2043
|
+
toBeAttached: 'to.be.attached',
|
|
2044
|
+
toBeDetached: 'to.be.detached',
|
|
2045
|
+
toBeEditable: 'to.be.editable',
|
|
2046
|
+
toBeFocused: 'to.be.focused',
|
|
2047
|
+
toBeEmpty: 'to.be.empty'
|
|
2048
|
+
};
|
|
2049
|
+
const textMatchers = {
|
|
2050
|
+
toHaveId: {
|
|
2051
|
+
expectMethod: 'to.have.id'
|
|
2052
|
+
},
|
|
2053
|
+
toHaveText: {
|
|
2054
|
+
expectMethod: 'to.have.text',
|
|
2055
|
+
textOptions: {
|
|
2056
|
+
normalizeWhiteSpace: true
|
|
2057
|
+
}
|
|
2058
|
+
},
|
|
2059
|
+
toContainText: {
|
|
2060
|
+
expectMethod: 'to.have.text',
|
|
2061
|
+
textOptions: {
|
|
2062
|
+
matchSubstring: true,
|
|
2063
|
+
normalizeWhiteSpace: true
|
|
2064
|
+
}
|
|
2065
|
+
},
|
|
2066
|
+
toHaveValue: {
|
|
2067
|
+
expectMethod: 'to.have.value'
|
|
2068
|
+
},
|
|
2069
|
+
toHaveClass: {
|
|
2070
|
+
expectMethod: 'to.have.class'
|
|
2071
|
+
}
|
|
2072
|
+
};
|
|
2073
|
+
const dispatchExpectMatcher = (locator, request, isNot, timeout)=>{
|
|
2074
|
+
const { method, args } = request;
|
|
2075
|
+
const simpleExpect = simpleMatchers[method];
|
|
2076
|
+
if (simpleExpect) return callExpect(locator, simpleExpect, {
|
|
2077
|
+
isNot,
|
|
2078
|
+
timeout
|
|
2079
|
+
}, `Expected element ${method.replace('toBe', 'to be ').replace(/([A-Z])/g, ' $1').trim().toLowerCase()}`);
|
|
2080
|
+
const textDef = textMatchers[method];
|
|
2081
|
+
if (textDef) {
|
|
2082
|
+
const expected = assertSerializedText(args[0], method);
|
|
2083
|
+
return callExpect(locator, textDef.expectMethod, {
|
|
2084
|
+
isNot,
|
|
2085
|
+
timeout,
|
|
2086
|
+
expectedText: serializeExpectedText(expected, textDef.textOptions)
|
|
2087
|
+
}, `Expected element ${method}`);
|
|
2088
|
+
}
|
|
2089
|
+
switch(method){
|
|
2090
|
+
case 'toBeInViewport':
|
|
2091
|
+
{
|
|
2092
|
+
const ratio = args[0];
|
|
2093
|
+
if (void 0 !== ratio && 'number' != typeof ratio) throw new Error(`toBeInViewport expects ratio to be a number, got ${typeof ratio}`);
|
|
2094
|
+
return callExpect(locator, 'to.be.in.viewport', {
|
|
2095
|
+
isNot,
|
|
2096
|
+
timeout,
|
|
2097
|
+
expectedNumber: ratio
|
|
2098
|
+
}, 'Expected element to be in viewport');
|
|
2099
|
+
}
|
|
2100
|
+
case 'toBeChecked':
|
|
2101
|
+
return callExpect(locator, 'to.be.checked', {
|
|
2102
|
+
isNot,
|
|
2103
|
+
timeout,
|
|
2104
|
+
expectedValue: {
|
|
2105
|
+
checked: true
|
|
2106
|
+
}
|
|
2107
|
+
}, 'Expected element to be checked');
|
|
2108
|
+
case 'toBeUnchecked':
|
|
2109
|
+
return callExpect(locator, 'to.be.checked', {
|
|
2110
|
+
isNot,
|
|
2111
|
+
timeout,
|
|
2112
|
+
expectedValue: {
|
|
2113
|
+
checked: false
|
|
2114
|
+
}
|
|
2115
|
+
}, 'Expected element to be unchecked');
|
|
2116
|
+
case 'toHaveCount':
|
|
2117
|
+
{
|
|
2118
|
+
const expected = args[0];
|
|
2119
|
+
if ('number' != typeof expected) throw new Error(`toHaveCount expects a number, got ${typeof expected}`);
|
|
2120
|
+
return callExpect(locator, 'to.have.count', {
|
|
2121
|
+
isNot,
|
|
2122
|
+
timeout,
|
|
2123
|
+
expectedNumber: expected
|
|
2124
|
+
}, `Expected count ${expected}`);
|
|
2125
|
+
}
|
|
2126
|
+
case 'toHaveAttribute':
|
|
2127
|
+
{
|
|
2128
|
+
const name = assertStringArg(args[0], 'toHaveAttribute', 'an attribute name');
|
|
2129
|
+
if (args.length < 2) return callExpect(locator, 'to.have.attribute', {
|
|
2130
|
+
isNot,
|
|
2131
|
+
timeout,
|
|
2132
|
+
expressionArg: name
|
|
2133
|
+
}, `Expected attribute ${name} to be present`);
|
|
2134
|
+
const expected = assertSerializedText(args[1], 'toHaveAttribute');
|
|
2135
|
+
return callExpect(locator, 'to.have.attribute.value', {
|
|
2136
|
+
isNot,
|
|
2137
|
+
timeout,
|
|
2138
|
+
expressionArg: name,
|
|
2139
|
+
expectedText: serializeExpectedText(expected)
|
|
2140
|
+
}, `Expected attribute ${name} to match`);
|
|
2141
|
+
}
|
|
2142
|
+
case 'toHaveCSS':
|
|
2143
|
+
{
|
|
2144
|
+
const name = assertStringArg(args[0], 'toHaveCSS', 'a CSS property name');
|
|
2145
|
+
const expected = assertSerializedText(args[1], 'toHaveCSS');
|
|
2146
|
+
return callExpect(locator, 'to.have.css', {
|
|
2147
|
+
isNot,
|
|
2148
|
+
timeout,
|
|
2149
|
+
expressionArg: name,
|
|
2150
|
+
expectedText: serializeExpectedText(expected)
|
|
2151
|
+
}, `Expected CSS ${name} to match`);
|
|
2152
|
+
}
|
|
2153
|
+
case 'toHaveJSProperty':
|
|
2154
|
+
{
|
|
2155
|
+
const name = assertStringArg(args[0], 'toHaveJSProperty', 'a property name');
|
|
2156
|
+
const expectedValue = args[1];
|
|
2157
|
+
try {
|
|
2158
|
+
JSON.stringify(expectedValue);
|
|
2159
|
+
} catch {
|
|
2160
|
+
throw new Error('toHaveJSProperty expects a JSON-serializable expected value');
|
|
2161
|
+
}
|
|
2162
|
+
return callExpect(locator, 'to.have.property', {
|
|
2163
|
+
isNot,
|
|
2164
|
+
timeout,
|
|
2165
|
+
expressionArg: name,
|
|
2166
|
+
expectedValue
|
|
2167
|
+
}, `Expected JS property ${name} to match`);
|
|
2168
|
+
}
|
|
2169
|
+
}
|
|
2170
|
+
throw new Error(`Unhandled expect matcher: ${method}`);
|
|
2171
|
+
};
|
|
2172
|
+
const dispatchConfigMethod = async (request)=>{
|
|
2173
|
+
switch(request.method){
|
|
2174
|
+
case 'setTestIdAttribute':
|
|
2175
|
+
{
|
|
2176
|
+
const attr = request.args[0];
|
|
2177
|
+
if ('string' != typeof attr || !attr) throw new Error('setTestIdAttribute expects a non-empty string argument');
|
|
2178
|
+
const playwright = await import("playwright");
|
|
2179
|
+
playwright.selectors.setTestIdAttribute(attr);
|
|
2180
|
+
return null;
|
|
2181
|
+
}
|
|
2182
|
+
default:
|
|
2183
|
+
throw new Error(`Unknown config method: ${request.method}`);
|
|
2184
|
+
}
|
|
2185
|
+
};
|
|
2186
|
+
async function dispatchPlaywrightBrowserRpc({ containerPage, runnerPage, request, timeoutFallbackMs }) {
|
|
2187
|
+
if ('config' === request.kind) return dispatchConfigMethod(request);
|
|
2188
|
+
const testPath = request.testPath;
|
|
2189
|
+
if (!testPath) throw new Error('Browser RPC request is missing testPath');
|
|
2190
|
+
const timeout = 'number' == typeof request.timeout ? request.timeout : timeoutFallbackMs;
|
|
2191
|
+
const locatorRoot = runnerPage ? runnerPage : await getRunnerFrame(containerPage ?? (()=>{
|
|
2192
|
+
throw new Error('Browser container page is not initialized');
|
|
2193
|
+
})(), testPath, timeout);
|
|
2194
|
+
const locator = compilePlaywrightLocator(locatorRoot, request.locator);
|
|
2195
|
+
if ('locator' === request.kind) {
|
|
2196
|
+
if (!supportedLocatorActions.has(request.method)) throw new Error(`Locator method not supported: ${request.method}`);
|
|
2197
|
+
const target = locator;
|
|
2198
|
+
return await target[request.method](...request.args);
|
|
2199
|
+
}
|
|
2200
|
+
if ('expect' === request.kind) {
|
|
2201
|
+
if (!supportedExpectElementMatchers.has(request.method)) throw new Error(`Expect matcher not supported: ${request.method}`);
|
|
2202
|
+
return dispatchExpectMatcher(locator, request, !!request.isNot, timeout);
|
|
2203
|
+
}
|
|
2204
|
+
throw new Error(`Unknown browser rpc kind: ${request.kind}`);
|
|
1615
2205
|
}
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
2206
|
+
async function launchPlaywrightBrowser({ browserName, headless }) {
|
|
2207
|
+
const playwright = await import("playwright");
|
|
2208
|
+
const browserType = playwright[browserName];
|
|
2209
|
+
const browser = await browserType.launch({
|
|
2210
|
+
headless,
|
|
2211
|
+
args: 'chromium' === browserName ? [
|
|
2212
|
+
'--disable-popup-blocking',
|
|
2213
|
+
'--no-first-run',
|
|
2214
|
+
'--no-default-browser-check'
|
|
2215
|
+
] : void 0
|
|
2216
|
+
});
|
|
2217
|
+
return {
|
|
2218
|
+
browser: browser
|
|
2219
|
+
};
|
|
2220
|
+
}
|
|
2221
|
+
const playwrightProviderImplementation = {
|
|
2222
|
+
name: 'playwright',
|
|
2223
|
+
async launchRuntime ({ browserName, headless }) {
|
|
2224
|
+
return launchPlaywrightBrowser({
|
|
2225
|
+
browserName,
|
|
2226
|
+
headless
|
|
2227
|
+
});
|
|
2228
|
+
},
|
|
2229
|
+
async dispatchRpc ({ containerPage, runnerPage, request, timeoutFallbackMs }) {
|
|
2230
|
+
return dispatchPlaywrightBrowserRpc({
|
|
2231
|
+
containerPage: containerPage,
|
|
2232
|
+
runnerPage: runnerPage,
|
|
2233
|
+
request,
|
|
2234
|
+
timeoutFallbackMs
|
|
2235
|
+
});
|
|
2236
|
+
}
|
|
2237
|
+
};
|
|
2238
|
+
const providerImplementations = {
|
|
2239
|
+
playwright: playwrightProviderImplementation
|
|
2240
|
+
};
|
|
2241
|
+
function getBrowserProviderImplementation(provider) {
|
|
2242
|
+
const implementation = providerImplementations[provider];
|
|
2243
|
+
if (!implementation) throw new Error(`Unsupported browser provider: ${String(provider)}`);
|
|
2244
|
+
return implementation;
|
|
1622
2245
|
}
|
|
2246
|
+
const createCancelSignal = ()=>{
|
|
2247
|
+
let settled = false;
|
|
2248
|
+
let resolveSignal = ()=>{};
|
|
2249
|
+
const signal = new Promise((resolve)=>{
|
|
2250
|
+
resolveSignal = ()=>{
|
|
2251
|
+
if (!settled) {
|
|
2252
|
+
settled = true;
|
|
2253
|
+
resolve();
|
|
2254
|
+
}
|
|
2255
|
+
};
|
|
2256
|
+
});
|
|
2257
|
+
return {
|
|
2258
|
+
signal,
|
|
2259
|
+
resolve: resolveSignal
|
|
2260
|
+
};
|
|
2261
|
+
};
|
|
2262
|
+
const createRunSession = (token)=>{
|
|
2263
|
+
const { signal, resolve } = createCancelSignal();
|
|
2264
|
+
return {
|
|
2265
|
+
token,
|
|
2266
|
+
cancelled: false,
|
|
2267
|
+
cancelSignal: signal,
|
|
2268
|
+
signalCancel: resolve
|
|
2269
|
+
};
|
|
2270
|
+
};
|
|
2271
|
+
class RunSessionLifecycle {
|
|
2272
|
+
currentToken = 0;
|
|
2273
|
+
active = null;
|
|
2274
|
+
get activeSession() {
|
|
2275
|
+
return this.active;
|
|
2276
|
+
}
|
|
2277
|
+
get activeToken() {
|
|
2278
|
+
return this.currentToken;
|
|
2279
|
+
}
|
|
2280
|
+
createSession(factory) {
|
|
2281
|
+
const session = factory(++this.currentToken);
|
|
2282
|
+
this.active = session;
|
|
2283
|
+
return session;
|
|
2284
|
+
}
|
|
2285
|
+
isTokenActive(token) {
|
|
2286
|
+
return token === this.currentToken;
|
|
2287
|
+
}
|
|
2288
|
+
isTokenStale(token) {
|
|
2289
|
+
return !this.isTokenActive(token);
|
|
2290
|
+
}
|
|
2291
|
+
invalidateActiveToken() {
|
|
2292
|
+
this.currentToken += 1;
|
|
2293
|
+
return this.currentToken;
|
|
2294
|
+
}
|
|
2295
|
+
clearIfActive(session) {
|
|
2296
|
+
if (this.active === session) this.active = null;
|
|
2297
|
+
}
|
|
2298
|
+
async cancel(session, options) {
|
|
2299
|
+
const waitForDone = options?.waitForDone ?? true;
|
|
2300
|
+
if (!session.cancelled) {
|
|
2301
|
+
session.cancelled = true;
|
|
2302
|
+
session.signalCancel();
|
|
2303
|
+
await options?.onCancel?.(session);
|
|
2304
|
+
}
|
|
2305
|
+
if (waitForDone) await session.done?.catch(()=>{});
|
|
2306
|
+
}
|
|
2307
|
+
async cancelActive(options) {
|
|
2308
|
+
if (!this.active) return;
|
|
2309
|
+
await this.cancel(this.active, options);
|
|
2310
|
+
}
|
|
2311
|
+
}
|
|
2312
|
+
class RunnerSessionRegistry {
|
|
2313
|
+
nextId = 0;
|
|
2314
|
+
sessionsById = new Map();
|
|
2315
|
+
sessionIdByTestFile = new Map();
|
|
2316
|
+
register(input) {
|
|
2317
|
+
const id = input.id ?? `runner-session-${++this.nextId}`;
|
|
2318
|
+
const createdAt = input.createdAt ?? Date.now();
|
|
2319
|
+
const record = {
|
|
2320
|
+
...input,
|
|
2321
|
+
id,
|
|
2322
|
+
createdAt
|
|
2323
|
+
};
|
|
2324
|
+
this.sessionsById.set(id, record);
|
|
2325
|
+
this.sessionIdByTestFile.set(record.testFile, id);
|
|
2326
|
+
return record;
|
|
2327
|
+
}
|
|
2328
|
+
getById(id) {
|
|
2329
|
+
return this.sessionsById.get(id);
|
|
2330
|
+
}
|
|
2331
|
+
getByTestFile(testFile) {
|
|
2332
|
+
const id = this.sessionIdByTestFile.get(testFile);
|
|
2333
|
+
if (!id) return;
|
|
2334
|
+
return this.sessionsById.get(id);
|
|
2335
|
+
}
|
|
2336
|
+
list() {
|
|
2337
|
+
return Array.from(this.sessionsById.values());
|
|
2338
|
+
}
|
|
2339
|
+
listByRunToken(runToken) {
|
|
2340
|
+
return this.list().filter((session)=>session.runToken === runToken);
|
|
2341
|
+
}
|
|
2342
|
+
deleteById(id) {
|
|
2343
|
+
const record = this.sessionsById.get(id);
|
|
2344
|
+
if (!record) return false;
|
|
2345
|
+
this.sessionsById.delete(id);
|
|
2346
|
+
if (this.sessionIdByTestFile.get(record.testFile) === id) this.sessionIdByTestFile.delete(record.testFile);
|
|
2347
|
+
return true;
|
|
2348
|
+
}
|
|
2349
|
+
deleteByTestFile(testFile) {
|
|
2350
|
+
const id = this.sessionIdByTestFile.get(testFile);
|
|
2351
|
+
if (!id) return false;
|
|
2352
|
+
return this.deleteById(id);
|
|
2353
|
+
}
|
|
2354
|
+
clear() {
|
|
2355
|
+
this.sessionsById.clear();
|
|
2356
|
+
this.sessionIdByTestFile.clear();
|
|
2357
|
+
}
|
|
2358
|
+
}
|
|
2359
|
+
const normalizeJavaScriptUrl = (value, options)=>{
|
|
2360
|
+
try {
|
|
2361
|
+
const url = options?.origin ? new URL(value, options.origin) : new URL(value);
|
|
2362
|
+
if ('http:' !== url.protocol && 'https:' !== url.protocol) return null;
|
|
2363
|
+
url.search = '';
|
|
2364
|
+
url.hash = '';
|
|
2365
|
+
return url.toString();
|
|
2366
|
+
} catch {
|
|
2367
|
+
return null;
|
|
2368
|
+
}
|
|
2369
|
+
};
|
|
2370
|
+
const resolveInlineSourceMap = (code)=>{
|
|
2371
|
+
const converter = convert_source_map.fromSource(code);
|
|
2372
|
+
if (!converter) return null;
|
|
2373
|
+
return converter.toObject();
|
|
2374
|
+
};
|
|
2375
|
+
const fetchSourceMap = async (jsUrl, fetcher)=>{
|
|
2376
|
+
const jsResponse = await fetcher(jsUrl);
|
|
2377
|
+
if (!jsResponse.ok) return null;
|
|
2378
|
+
const code = await jsResponse.text();
|
|
2379
|
+
const inlineMap = resolveInlineSourceMap(code);
|
|
2380
|
+
if (inlineMap) return inlineMap;
|
|
2381
|
+
const mapResponse = await fetcher(`${jsUrl}.map`);
|
|
2382
|
+
if (!mapResponse.ok) return null;
|
|
2383
|
+
return await mapResponse.json();
|
|
2384
|
+
};
|
|
2385
|
+
const loadSourceMapWithCache = async ({ jsUrl, cache, force = false, origin, fetcher = fetch })=>{
|
|
2386
|
+
const normalizedUrl = normalizeJavaScriptUrl(jsUrl, {
|
|
2387
|
+
origin
|
|
2388
|
+
});
|
|
2389
|
+
if (!normalizedUrl) return null;
|
|
2390
|
+
if (!force && cache.has(normalizedUrl)) return cache.get(normalizedUrl) ?? null;
|
|
2391
|
+
try {
|
|
2392
|
+
const sourceMap = await fetchSourceMap(normalizedUrl, fetcher);
|
|
2393
|
+
cache.set(normalizedUrl, sourceMap);
|
|
2394
|
+
return sourceMap;
|
|
2395
|
+
} catch {
|
|
2396
|
+
cache.set(normalizedUrl, null);
|
|
2397
|
+
return null;
|
|
2398
|
+
}
|
|
2399
|
+
};
|
|
2400
|
+
const BROWSER_VIEWPORT_PRESET_IDS = [
|
|
2401
|
+
'iPhoneSE',
|
|
2402
|
+
'iPhoneXR',
|
|
2403
|
+
'iPhone12Pro',
|
|
2404
|
+
'iPhone14ProMax',
|
|
2405
|
+
'Pixel7',
|
|
2406
|
+
'SamsungGalaxyS8Plus',
|
|
2407
|
+
'SamsungGalaxyS20Ultra',
|
|
2408
|
+
'iPadMini',
|
|
2409
|
+
'iPadAir',
|
|
2410
|
+
'iPadPro',
|
|
2411
|
+
'SurfacePro7',
|
|
2412
|
+
'SurfaceDuo',
|
|
2413
|
+
'GalaxyZFold5',
|
|
2414
|
+
'AsusZenbookFold',
|
|
2415
|
+
'SamsungGalaxyA51A71',
|
|
2416
|
+
'NestHub',
|
|
2417
|
+
'NestHubMax'
|
|
2418
|
+
];
|
|
2419
|
+
const BROWSER_VIEWPORT_PRESET_DIMENSIONS = {
|
|
2420
|
+
iPhoneSE: {
|
|
2421
|
+
width: 375,
|
|
2422
|
+
height: 667
|
|
2423
|
+
},
|
|
2424
|
+
iPhoneXR: {
|
|
2425
|
+
width: 414,
|
|
2426
|
+
height: 896
|
|
2427
|
+
},
|
|
2428
|
+
iPhone12Pro: {
|
|
2429
|
+
width: 390,
|
|
2430
|
+
height: 844
|
|
2431
|
+
},
|
|
2432
|
+
iPhone14ProMax: {
|
|
2433
|
+
width: 430,
|
|
2434
|
+
height: 932
|
|
2435
|
+
},
|
|
2436
|
+
Pixel7: {
|
|
2437
|
+
width: 412,
|
|
2438
|
+
height: 915
|
|
2439
|
+
},
|
|
2440
|
+
SamsungGalaxyS8Plus: {
|
|
2441
|
+
width: 360,
|
|
2442
|
+
height: 740
|
|
2443
|
+
},
|
|
2444
|
+
SamsungGalaxyS20Ultra: {
|
|
2445
|
+
width: 412,
|
|
2446
|
+
height: 915
|
|
2447
|
+
},
|
|
2448
|
+
iPadMini: {
|
|
2449
|
+
width: 768,
|
|
2450
|
+
height: 1024
|
|
2451
|
+
},
|
|
2452
|
+
iPadAir: {
|
|
2453
|
+
width: 820,
|
|
2454
|
+
height: 1180
|
|
2455
|
+
},
|
|
2456
|
+
iPadPro: {
|
|
2457
|
+
width: 1024,
|
|
2458
|
+
height: 1366
|
|
2459
|
+
},
|
|
2460
|
+
SurfacePro7: {
|
|
2461
|
+
width: 912,
|
|
2462
|
+
height: 1368
|
|
2463
|
+
},
|
|
2464
|
+
SurfaceDuo: {
|
|
2465
|
+
width: 540,
|
|
2466
|
+
height: 720
|
|
2467
|
+
},
|
|
2468
|
+
GalaxyZFold5: {
|
|
2469
|
+
width: 344,
|
|
2470
|
+
height: 882
|
|
2471
|
+
},
|
|
2472
|
+
AsusZenbookFold: {
|
|
2473
|
+
width: 853,
|
|
2474
|
+
height: 1280
|
|
2475
|
+
},
|
|
2476
|
+
SamsungGalaxyA51A71: {
|
|
2477
|
+
width: 412,
|
|
2478
|
+
height: 914
|
|
2479
|
+
},
|
|
2480
|
+
NestHub: {
|
|
2481
|
+
width: 1024,
|
|
2482
|
+
height: 600
|
|
2483
|
+
},
|
|
2484
|
+
NestHubMax: {
|
|
2485
|
+
width: 1280,
|
|
2486
|
+
height: 800
|
|
2487
|
+
}
|
|
2488
|
+
};
|
|
2489
|
+
const resolveBrowserViewportPreset = (presetId)=>{
|
|
2490
|
+
const size = BROWSER_VIEWPORT_PRESET_DIMENSIONS[presetId];
|
|
2491
|
+
return size ?? null;
|
|
2492
|
+
};
|
|
2493
|
+
const serializeTestFiles = (files)=>JSON.stringify(files.map((f)=>`${f.projectName}:${f.testPath}`).sort());
|
|
2494
|
+
const normalizeTestFiles = (files)=>files.map((file)=>({
|
|
2495
|
+
...file,
|
|
2496
|
+
testPath: normalize(file.testPath)
|
|
2497
|
+
}));
|
|
2498
|
+
const collectWatchTestFiles = (projectEntries)=>projectEntries.flatMap((entry)=>entry.testFiles.map((testPath)=>({
|
|
2499
|
+
testPath: normalize(testPath),
|
|
2500
|
+
projectName: entry.project.name
|
|
2501
|
+
})));
|
|
2502
|
+
const planWatchRerun = ({ projectEntries, previousTestFiles, affectedTestFiles })=>{
|
|
2503
|
+
const currentTestFiles = collectWatchTestFiles(projectEntries);
|
|
2504
|
+
const normalizedPrevious = normalizeTestFiles(previousTestFiles);
|
|
2505
|
+
const filesChanged = serializeTestFiles(currentTestFiles) !== serializeTestFiles(normalizedPrevious);
|
|
2506
|
+
const normalizedAffectedTestFiles = affectedTestFiles.map((testFile)=>normalize(testFile));
|
|
2507
|
+
const currentFileMap = new Map(currentTestFiles.map((file)=>[
|
|
2508
|
+
file.testPath,
|
|
2509
|
+
file
|
|
2510
|
+
]));
|
|
2511
|
+
const matchedAffectedFiles = normalizedAffectedTestFiles.map((testFile)=>currentFileMap.get(testFile)).filter((file)=>Boolean(file));
|
|
2512
|
+
return {
|
|
2513
|
+
currentTestFiles,
|
|
2514
|
+
filesChanged,
|
|
2515
|
+
normalizedAffectedTestFiles,
|
|
2516
|
+
affectedTestFiles: matchedAffectedFiles
|
|
2517
|
+
};
|
|
2518
|
+
};
|
|
1623
2519
|
const picomatch = __webpack_require__("../../node_modules/.pnpm/picomatch@4.0.3/node_modules/picomatch/index.js");
|
|
1624
2520
|
const { createRsbuild: createRsbuild, rspack: rspack } = rsbuild;
|
|
1625
2521
|
const hostController_dirname = dirname(fileURLToPath(import.meta.url));
|
|
@@ -1695,6 +2591,23 @@ const watchContext = {
|
|
|
1695
2591
|
chunkHashes: new Map(),
|
|
1696
2592
|
affectedTestFiles: []
|
|
1697
2593
|
};
|
|
2594
|
+
const resolveViewport = (viewport)=>{
|
|
2595
|
+
if (!viewport) return null;
|
|
2596
|
+
if ('string' == typeof viewport) return resolveBrowserViewportPreset(viewport);
|
|
2597
|
+
if ('number' == typeof viewport.width && Number.isFinite(viewport.width) && viewport.width > 0 && 'number' == typeof viewport.height && Number.isFinite(viewport.height) && viewport.height > 0) return {
|
|
2598
|
+
width: viewport.width,
|
|
2599
|
+
height: viewport.height
|
|
2600
|
+
};
|
|
2601
|
+
return null;
|
|
2602
|
+
};
|
|
2603
|
+
const mapViewportByProject = (projects)=>{
|
|
2604
|
+
const map = new Map();
|
|
2605
|
+
for (const project of projects){
|
|
2606
|
+
const viewport = resolveViewport(project.viewport);
|
|
2607
|
+
if (viewport) map.set(project.name, viewport);
|
|
2608
|
+
}
|
|
2609
|
+
return map;
|
|
2610
|
+
};
|
|
1698
2611
|
const ensureProcessExitCode = (code)=>{
|
|
1699
2612
|
if (void 0 === process.exitCode || 0 === process.exitCode) process.exitCode = code;
|
|
1700
2613
|
};
|
|
@@ -1806,6 +2719,31 @@ const getRuntimeConfigFromProject = (project)=>{
|
|
|
1806
2719
|
};
|
|
1807
2720
|
};
|
|
1808
2721
|
const getBrowserProjects = (context)=>context.projects.filter((project)=>project.normalizedConfig.browser.enabled);
|
|
2722
|
+
const getBrowserLaunchOptions = (project)=>({
|
|
2723
|
+
provider: project.normalizedConfig.browser.provider,
|
|
2724
|
+
browser: project.normalizedConfig.browser.browser,
|
|
2725
|
+
headless: project.normalizedConfig.browser.headless,
|
|
2726
|
+
port: project.normalizedConfig.browser.port,
|
|
2727
|
+
strictPort: project.normalizedConfig.browser.strictPort
|
|
2728
|
+
});
|
|
2729
|
+
const ensureConsistentBrowserLaunchOptions = (projects)=>{
|
|
2730
|
+
if (0 === projects.length) throw new Error('No browser-enabled projects found.');
|
|
2731
|
+
const firstProject = projects[0];
|
|
2732
|
+
const firstOptions = getBrowserLaunchOptions(firstProject);
|
|
2733
|
+
for (const project of projects.slice(1)){
|
|
2734
|
+
const options = getBrowserLaunchOptions(project);
|
|
2735
|
+
if (options.provider !== firstOptions.provider || options.browser !== firstOptions.browser || options.headless !== firstOptions.headless || options.port !== firstOptions.port || options.strictPort !== firstOptions.strictPort) throw new Error(`Browser launch config mismatch between projects "${firstProject.name}" and "${project.name}". All browser-enabled projects in one run must share provider/browser/headless/port/strictPort.`);
|
|
2736
|
+
}
|
|
2737
|
+
return firstOptions;
|
|
2738
|
+
};
|
|
2739
|
+
const resolveProviderForTestPath = ({ testPath, browserProjects })=>{
|
|
2740
|
+
const normalizedTestPath = normalize(testPath);
|
|
2741
|
+
const sortedProjects = [
|
|
2742
|
+
...browserProjects
|
|
2743
|
+
].sort((a, b)=>b.rootPath.length - a.rootPath.length);
|
|
2744
|
+
for (const project of sortedProjects)if (normalizedTestPath.startsWith(project.rootPath)) return project.provider;
|
|
2745
|
+
throw new Error(`Cannot resolve browser provider for test path: ${JSON.stringify(testPath)}. Known project roots: ${JSON.stringify(sortedProjects.map((p)=>p.rootPath))}`);
|
|
2746
|
+
};
|
|
1809
2747
|
const collectProjectEntries = async (context)=>{
|
|
1810
2748
|
const projectEntries = [];
|
|
1811
2749
|
const browserProjects = getBrowserProjects(context);
|
|
@@ -1919,20 +2857,6 @@ const htmlTemplate = `<!DOCTYPE html>
|
|
|
1919
2857
|
</body>
|
|
1920
2858
|
</html>
|
|
1921
2859
|
`;
|
|
1922
|
-
const fallbackSchedulerHtmlTemplate = `<!DOCTYPE html>
|
|
1923
|
-
<html lang="en">
|
|
1924
|
-
<head>
|
|
1925
|
-
<meta charset="UTF-8" />
|
|
1926
|
-
<title>Rstest Browser Scheduler</title>
|
|
1927
|
-
<script>
|
|
1928
|
-
window.__RSTEST_BROWSER_OPTIONS__ = ${OPTIONS_PLACEHOLDER};
|
|
1929
|
-
</script>
|
|
1930
|
-
</head>
|
|
1931
|
-
<body>
|
|
1932
|
-
<script type="module" src="/container-static/js/scheduler.js"></script>
|
|
1933
|
-
</body>
|
|
1934
|
-
</html>
|
|
1935
|
-
`;
|
|
1936
2860
|
const VIRTUAL_MANIFEST_FILENAME = 'virtual-manifest.ts';
|
|
1937
2861
|
const destroyBrowserRuntime = async (runtime)=>{
|
|
1938
2862
|
try {
|
|
@@ -1972,24 +2896,25 @@ const createBrowserRuntime = async ({ context, manifestPath, manifestSource, tem
|
|
|
1972
2896
|
[manifestPath]: manifestSource
|
|
1973
2897
|
});
|
|
1974
2898
|
const containerHtmlTemplate = containerDistPath ? await promises.readFile(join(containerDistPath, 'index.html'), 'utf-8') : null;
|
|
1975
|
-
const schedulerHtmlTemplate = containerDistPath ? await promises.readFile(join(containerDistPath, 'scheduler.html'), 'utf-8').catch(()=>null) : null;
|
|
1976
2899
|
let injectedContainerHtml = null;
|
|
1977
|
-
let injectedSchedulerHtml = null;
|
|
1978
2900
|
let serializedOptions = 'null';
|
|
2901
|
+
const dispatchHandlers = new Map();
|
|
1979
2902
|
const setContainerOptions = (options)=>{
|
|
1980
2903
|
serializedOptions = serializeForInlineScript(options);
|
|
1981
2904
|
if (containerHtmlTemplate) injectedContainerHtml = containerHtmlTemplate.replace(OPTIONS_PLACEHOLDER, serializedOptions);
|
|
1982
|
-
injectedSchedulerHtml = (schedulerHtmlTemplate || fallbackSchedulerHtmlTemplate).replace(OPTIONS_PLACEHOLDER, serializedOptions);
|
|
1983
2905
|
};
|
|
1984
2906
|
const browserProjects = getBrowserProjects(context);
|
|
1985
|
-
const
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
2907
|
+
const projectByEnvironmentName = new Map(browserProjects.map((project)=>[
|
|
2908
|
+
project.environmentName,
|
|
2909
|
+
project
|
|
2910
|
+
]));
|
|
2911
|
+
const userPlugins = browserProjects.flatMap((project)=>project.normalizedConfig.plugins || []);
|
|
2912
|
+
const browserLaunchOptions = ensureConsistentBrowserLaunchOptions(browserProjects);
|
|
1989
2913
|
const browserRuntimePath = fileURLToPath(import.meta.resolve('@rstest/core/browser-runtime'));
|
|
1990
2914
|
const rstestInternalAliases = {
|
|
1991
2915
|
'@rstest/browser-manifest': manifestPath,
|
|
1992
2916
|
'@rstest/core': resolveBrowserFile('client/public.ts'),
|
|
2917
|
+
'@rstest/browser': resolveBrowserFile('browser.ts'),
|
|
1993
2918
|
'@rstest/core/browser-runtime': browserRuntimePath,
|
|
1994
2919
|
'@sinonjs/fake-timers': resolveBrowserFile('client/fakeTimersStub.ts')
|
|
1995
2920
|
};
|
|
@@ -2001,8 +2926,8 @@ const createBrowserRuntime = async ({ context, manifestPath, manifestSource, tem
|
|
|
2001
2926
|
plugins: userPlugins,
|
|
2002
2927
|
server: {
|
|
2003
2928
|
printUrls: false,
|
|
2004
|
-
port:
|
|
2005
|
-
strictPort:
|
|
2929
|
+
port: browserLaunchOptions.port ?? 4000,
|
|
2930
|
+
strictPort: browserLaunchOptions.strictPort
|
|
2006
2931
|
},
|
|
2007
2932
|
dev: {
|
|
2008
2933
|
client: {
|
|
@@ -2010,7 +2935,10 @@ const createBrowserRuntime = async ({ context, manifestPath, manifestSource, tem
|
|
|
2010
2935
|
}
|
|
2011
2936
|
},
|
|
2012
2937
|
environments: {
|
|
2013
|
-
[
|
|
2938
|
+
...Object.fromEntries(browserProjects.map((project)=>[
|
|
2939
|
+
project.environmentName,
|
|
2940
|
+
{}
|
|
2941
|
+
]))
|
|
2014
2942
|
}
|
|
2015
2943
|
}
|
|
2016
2944
|
});
|
|
@@ -2018,12 +2946,26 @@ const createBrowserRuntime = async ({ context, manifestPath, manifestSource, tem
|
|
|
2018
2946
|
{
|
|
2019
2947
|
name: 'rstest:browser-user-config',
|
|
2020
2948
|
setup (api) {
|
|
2949
|
+
api.expose?.('rstest:browser', {
|
|
2950
|
+
registerDispatchHandler: (namespace, handler)=>{
|
|
2951
|
+
dispatchHandlers.set(namespace, handler);
|
|
2952
|
+
}
|
|
2953
|
+
});
|
|
2021
2954
|
api.modifyEnvironmentConfig({
|
|
2022
|
-
handler: (config, { mergeEnvironmentConfig })=>{
|
|
2955
|
+
handler: (config, { mergeEnvironmentConfig, name })=>{
|
|
2956
|
+
const project = projectByEnvironmentName.get(name);
|
|
2957
|
+
if (!project) return config;
|
|
2958
|
+
const userRsbuildConfig = project.normalizedConfig;
|
|
2023
2959
|
const merged = mergeEnvironmentConfig(config, userRsbuildConfig, {
|
|
2024
2960
|
resolve: {
|
|
2025
2961
|
alias: rstestInternalAliases
|
|
2026
2962
|
},
|
|
2963
|
+
source: {
|
|
2964
|
+
define: {
|
|
2965
|
+
'process.env': 'globalThis[Symbol.for("rstest.env")]',
|
|
2966
|
+
'import.meta.env': 'globalThis[Symbol.for("rstest.env")]'
|
|
2967
|
+
}
|
|
2968
|
+
},
|
|
2027
2969
|
output: {
|
|
2028
2970
|
target: 'web',
|
|
2029
2971
|
sourceMap: {
|
|
@@ -2073,7 +3015,7 @@ const createBrowserRuntime = async ({ context, manifestPath, manifestSource, tem
|
|
|
2073
3015
|
api.onAfterDevCompile(async ({ stats })=>{
|
|
2074
3016
|
if (stats) {
|
|
2075
3017
|
const projectEntries = await collectProjectEntries(context);
|
|
2076
|
-
const entryTestFiles = new Set(projectEntries
|
|
3018
|
+
const entryTestFiles = new Set(collectWatchTestFiles(projectEntries).map((file)=>file.testPath));
|
|
2077
3019
|
const statsJson = stats.toJson({
|
|
2078
3020
|
all: true
|
|
2079
3021
|
});
|
|
@@ -2087,7 +3029,7 @@ const createBrowserRuntime = async ({ context, manifestPath, manifestSource, tem
|
|
|
2087
3029
|
}
|
|
2088
3030
|
}
|
|
2089
3031
|
]);
|
|
2090
|
-
const coverage =
|
|
3032
|
+
const coverage = browserProjects.find((project)=>project.normalizedConfig.coverage?.enabled)?.normalizedConfig.coverage;
|
|
2091
3033
|
if (coverage?.enabled && 'list' !== context.command) {
|
|
2092
3034
|
const { pluginCoverage } = await loadCoverageProvider(coverage, context.rootPath);
|
|
2093
3035
|
rsbuildInstance.addPlugins([
|
|
@@ -2167,13 +3109,8 @@ const createBrowserRuntime = async ({ context, manifestPath, manifestSource, tem
|
|
|
2167
3109
|
}
|
|
2168
3110
|
return;
|
|
2169
3111
|
}
|
|
2170
|
-
if ('/' === url.pathname
|
|
3112
|
+
if ('/' === url.pathname) {
|
|
2171
3113
|
if (await respondWithDevServerHtml(url, res)) return;
|
|
2172
|
-
if ('/scheduler.html' === url.pathname) {
|
|
2173
|
-
res.setHeader('Content-Type', 'text/html');
|
|
2174
|
-
res.end(injectedSchedulerHtml || (schedulerHtmlTemplate || fallbackSchedulerHtmlTemplate).replace(OPTIONS_PLACEHOLDER, 'null'));
|
|
2175
|
-
return;
|
|
2176
|
-
}
|
|
2177
3114
|
const html = injectedContainerHtml || containerHtmlTemplate?.replace(OPTIONS_PLACEHOLDER, 'null');
|
|
2178
3115
|
if (html) {
|
|
2179
3116
|
res.setHeader('Content-Type', 'text/html');
|
|
@@ -2207,44 +3144,32 @@ const createBrowserRuntime = async ({ context, manifestPath, manifestSource, tem
|
|
|
2207
3144
|
wss.once('error', reject);
|
|
2208
3145
|
});
|
|
2209
3146
|
const wsPort = wss.address().port;
|
|
2210
|
-
logger.debug(`[Browser UI] WebSocket server started on port ${wsPort}`);
|
|
2211
|
-
|
|
2212
|
-
const browserName = browserConfig.browser;
|
|
2213
|
-
try {
|
|
2214
|
-
const playwright = await import("playwright");
|
|
2215
|
-
browserLauncher = playwright[browserName];
|
|
2216
|
-
} catch (_error) {
|
|
2217
|
-
wss.close();
|
|
2218
|
-
await devServer.close();
|
|
2219
|
-
throw _error;
|
|
2220
|
-
}
|
|
2221
|
-
let browser;
|
|
3147
|
+
logger.debug(`[Browser UI] WebSocket server started on port ${wsPort}`);
|
|
3148
|
+
const browserName = browserLaunchOptions.browser ?? 'chromium';
|
|
2222
3149
|
try {
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
'--no-first-run',
|
|
2228
|
-
'--no-default-browser-check'
|
|
2229
|
-
] : void 0
|
|
3150
|
+
const providerImplementation = getBrowserProviderImplementation(browserLaunchOptions.provider);
|
|
3151
|
+
const runtime = await providerImplementation.launchRuntime({
|
|
3152
|
+
browserName,
|
|
3153
|
+
headless: forceHeadless ?? browserLaunchOptions.headless
|
|
2230
3154
|
});
|
|
3155
|
+
return {
|
|
3156
|
+
rsbuildInstance,
|
|
3157
|
+
devServer,
|
|
3158
|
+
browser: runtime.browser,
|
|
3159
|
+
port,
|
|
3160
|
+
wsPort,
|
|
3161
|
+
manifestPath,
|
|
3162
|
+
tempDir,
|
|
3163
|
+
manifestPlugin: virtualManifestPlugin,
|
|
3164
|
+
setContainerOptions,
|
|
3165
|
+
dispatchHandlers,
|
|
3166
|
+
wss
|
|
3167
|
+
};
|
|
2231
3168
|
} catch (_error) {
|
|
2232
3169
|
wss.close();
|
|
2233
3170
|
await devServer.close();
|
|
2234
3171
|
throw _error;
|
|
2235
3172
|
}
|
|
2236
|
-
return {
|
|
2237
|
-
rsbuildInstance,
|
|
2238
|
-
devServer,
|
|
2239
|
-
browser,
|
|
2240
|
-
port,
|
|
2241
|
-
wsPort,
|
|
2242
|
-
manifestPath,
|
|
2243
|
-
tempDir,
|
|
2244
|
-
manifestPlugin: virtualManifestPlugin,
|
|
2245
|
-
setContainerOptions,
|
|
2246
|
-
wss
|
|
2247
|
-
};
|
|
2248
3173
|
};
|
|
2249
3174
|
async function resolveProjectEntries(context, shardedEntries) {
|
|
2250
3175
|
if (shardedEntries) {
|
|
@@ -2269,8 +3194,36 @@ const runBrowserController = async (context, options)=>{
|
|
|
2269
3194
|
const { skipOnTestRunEnd = false } = options ?? {};
|
|
2270
3195
|
const buildStart = Date.now();
|
|
2271
3196
|
const browserProjects = getBrowserProjects(context);
|
|
2272
|
-
const
|
|
2273
|
-
const
|
|
3197
|
+
const useHeadlessDirect = browserProjects.every((project)=>project.normalizedConfig.browser.headless);
|
|
3198
|
+
const browserSourceMapCache = new Map();
|
|
3199
|
+
const isHttpLikeFile = (file)=>/^https?:\/\//.test(file);
|
|
3200
|
+
const resolveBrowserSourcemap = async (sourcePath)=>{
|
|
3201
|
+
if (!isHttpLikeFile(sourcePath)) return {
|
|
3202
|
+
handled: false,
|
|
3203
|
+
sourcemap: null
|
|
3204
|
+
};
|
|
3205
|
+
const normalizedUrl = normalizeJavaScriptUrl(sourcePath);
|
|
3206
|
+
if (!normalizedUrl) return {
|
|
3207
|
+
handled: true,
|
|
3208
|
+
sourcemap: null
|
|
3209
|
+
};
|
|
3210
|
+
if (browserSourceMapCache.has(normalizedUrl)) return {
|
|
3211
|
+
handled: true,
|
|
3212
|
+
sourcemap: browserSourceMapCache.get(normalizedUrl) ?? null
|
|
3213
|
+
};
|
|
3214
|
+
return {
|
|
3215
|
+
handled: true,
|
|
3216
|
+
sourcemap: await loadSourceMapWithCache({
|
|
3217
|
+
jsUrl: normalizedUrl,
|
|
3218
|
+
cache: browserSourceMapCache
|
|
3219
|
+
})
|
|
3220
|
+
};
|
|
3221
|
+
};
|
|
3222
|
+
const getBrowserSourcemap = async (sourcePath)=>{
|
|
3223
|
+
const result = await resolveBrowserSourcemap(sourcePath);
|
|
3224
|
+
return result.handled ? result.sourcemap : null;
|
|
3225
|
+
};
|
|
3226
|
+
const buildErrorResult = async (error, close)=>{
|
|
2274
3227
|
const elapsed = Math.max(0, Date.now() - buildStart);
|
|
2275
3228
|
const errorResult = {
|
|
2276
3229
|
results: [],
|
|
@@ -2283,14 +3236,17 @@ const runBrowserController = async (context, options)=>{
|
|
|
2283
3236
|
hasFailure: true,
|
|
2284
3237
|
unhandledErrors: [
|
|
2285
3238
|
error
|
|
2286
|
-
]
|
|
3239
|
+
],
|
|
3240
|
+
getSourcemap: getBrowserSourcemap,
|
|
3241
|
+
resolveSourcemap: resolveBrowserSourcemap,
|
|
3242
|
+
close
|
|
2287
3243
|
};
|
|
2288
3244
|
if (!skipOnTestRunEnd) for (const reporter of context.reporters)await reporter.onTestRunEnd?.({
|
|
2289
3245
|
results: [],
|
|
2290
3246
|
testResults: [],
|
|
2291
3247
|
duration: errorResult.duration,
|
|
2292
3248
|
snapshotSummary: context.snapshotManager.summary,
|
|
2293
|
-
getSourcemap:
|
|
3249
|
+
getSourcemap: getBrowserSourcemap,
|
|
2294
3250
|
unhandledErrors: errorResult.unhandledErrors
|
|
2295
3251
|
});
|
|
2296
3252
|
return errorResult;
|
|
@@ -2298,24 +3254,51 @@ const runBrowserController = async (context, options)=>{
|
|
|
2298
3254
|
const toError = (error)=>error instanceof Error ? error : new Error(String(error));
|
|
2299
3255
|
const failWithError = async (error, cleanup)=>{
|
|
2300
3256
|
ensureProcessExitCode(1);
|
|
2301
|
-
|
|
2302
|
-
return buildErrorResult(
|
|
3257
|
+
const normalizedError = toError(error);
|
|
3258
|
+
if (cleanup && skipOnTestRunEnd) return buildErrorResult(normalizedError, cleanup);
|
|
3259
|
+
try {
|
|
3260
|
+
return await buildErrorResult(normalizedError);
|
|
3261
|
+
} finally{
|
|
3262
|
+
await cleanup?.();
|
|
3263
|
+
}
|
|
3264
|
+
};
|
|
3265
|
+
const collectDeletedTestPaths = (previous, current)=>{
|
|
3266
|
+
const currentPathSet = new Set(current.map((file)=>file.testPath));
|
|
3267
|
+
return previous.map((file)=>file.testPath).filter((testPath)=>!currentPathSet.has(testPath));
|
|
3268
|
+
};
|
|
3269
|
+
const notifyTestRunStart = async ()=>{
|
|
3270
|
+
if (skipOnTestRunEnd) return;
|
|
3271
|
+
for (const reporter of context.reporters)await reporter.onTestRunStart?.();
|
|
3272
|
+
};
|
|
3273
|
+
const notifyTestRunEnd = async ({ duration, unhandledErrors, filterRerunTestPaths })=>{
|
|
3274
|
+
if (skipOnTestRunEnd) return;
|
|
3275
|
+
for (const reporter of context.reporters)await reporter.onTestRunEnd?.({
|
|
3276
|
+
results: context.reporterResults.results,
|
|
3277
|
+
testResults: context.reporterResults.testResults,
|
|
3278
|
+
duration,
|
|
3279
|
+
snapshotSummary: context.snapshotManager.summary,
|
|
3280
|
+
getSourcemap: getBrowserSourcemap,
|
|
3281
|
+
unhandledErrors,
|
|
3282
|
+
filterRerunTestPaths
|
|
3283
|
+
});
|
|
2303
3284
|
};
|
|
2304
3285
|
const containerDevServerEnv = process.env.RSTEST_CONTAINER_DEV_SERVER;
|
|
2305
3286
|
let containerDevServer;
|
|
2306
3287
|
let containerDistPath;
|
|
2307
|
-
if (
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
3288
|
+
if (!useHeadlessDirect) {
|
|
3289
|
+
if (containerDevServerEnv) try {
|
|
3290
|
+
containerDevServer = new URL(containerDevServerEnv).toString();
|
|
3291
|
+
logger.debug(`[Browser UI] Using dev server for container: ${containerDevServer}`);
|
|
3292
|
+
} catch (error) {
|
|
3293
|
+
const originalError = toError(error);
|
|
3294
|
+
originalError.message = `Invalid RSTEST_CONTAINER_DEV_SERVER value: ${originalError.message}`;
|
|
3295
|
+
return failWithError(originalError);
|
|
3296
|
+
}
|
|
3297
|
+
if (!containerDevServer) try {
|
|
3298
|
+
containerDistPath = resolveContainerDist();
|
|
3299
|
+
} catch (error) {
|
|
3300
|
+
return failWithError(error);
|
|
3301
|
+
}
|
|
2319
3302
|
}
|
|
2320
3303
|
const projectEntries = await resolveProjectEntries(context, options?.shardedEntries);
|
|
2321
3304
|
const totalTests = projectEntries.reduce((total, item)=>total + item.testFiles.length, 0);
|
|
@@ -2329,6 +3312,7 @@ const runBrowserController = async (context, options)=>{
|
|
|
2329
3312
|
if (0 !== code) ensureProcessExitCode(code);
|
|
2330
3313
|
return;
|
|
2331
3314
|
}
|
|
3315
|
+
await notifyTestRunStart();
|
|
2332
3316
|
const isWatchMode = 'watch' === context.command;
|
|
2333
3317
|
const tempDir = isWatchMode && watchContext.runtime ? watchContext.runtime.tempDir : isWatchMode ? join(context.rootPath, TEMP_RSTEST_OUTPUT_DIR, 'browser', 'watch') : join(context.rootPath, TEMP_RSTEST_OUTPUT_DIR, 'browser', Date.now().toString());
|
|
2334
3318
|
const manifestPath = join(tempDir, VIRTUAL_MANIFEST_FILENAME);
|
|
@@ -2336,10 +3320,7 @@ const runBrowserController = async (context, options)=>{
|
|
|
2336
3320
|
manifestPath,
|
|
2337
3321
|
entries: projectEntries
|
|
2338
3322
|
});
|
|
2339
|
-
if (isWatchMode) watchContext.lastTestFiles = projectEntries
|
|
2340
|
-
testPath,
|
|
2341
|
-
projectName: entry.project.name
|
|
2342
|
-
})));
|
|
3323
|
+
if (isWatchMode) watchContext.lastTestFiles = collectWatchTestFiles(projectEntries);
|
|
2343
3324
|
let runtime = isWatchMode ? watchContext.runtime : null;
|
|
2344
3325
|
let triggerRerun;
|
|
2345
3326
|
if (!runtime) {
|
|
@@ -2394,11 +3375,442 @@ const runBrowserController = async (context, options)=>{
|
|
|
2394
3375
|
debug: isDebug(),
|
|
2395
3376
|
rpcTimeout: maxTestTimeoutForRpc
|
|
2396
3377
|
};
|
|
3378
|
+
const browserProviderProjects = browserProjects.map((project)=>({
|
|
3379
|
+
rootPath: normalize(project.rootPath),
|
|
3380
|
+
provider: project.normalizedConfig.browser.provider
|
|
3381
|
+
}));
|
|
3382
|
+
const implementationByProvider = new Map();
|
|
3383
|
+
for (const browserProject of browserProviderProjects)if (!implementationByProvider.has(browserProject.provider)) implementationByProvider.set(browserProject.provider, getBrowserProviderImplementation(browserProject.provider));
|
|
3384
|
+
let activeContainerPage = null;
|
|
3385
|
+
let getHeadlessRunnerPageBySessionId;
|
|
3386
|
+
const dispatchBrowserRpcRequest = async ({ request, target })=>{
|
|
3387
|
+
const timeoutFallbackMs = maxTestTimeoutForRpc;
|
|
3388
|
+
const provider = resolveProviderForTestPath({
|
|
3389
|
+
testPath: request.testPath,
|
|
3390
|
+
browserProjects: browserProviderProjects
|
|
3391
|
+
});
|
|
3392
|
+
const implementation = implementationByProvider.get(provider);
|
|
3393
|
+
if (!implementation) throw new Error(`Browser provider implementation not found: ${provider}`);
|
|
3394
|
+
const runnerPage = target?.sessionId ? getHeadlessRunnerPageBySessionId?.(target.sessionId) : void 0;
|
|
3395
|
+
if (target?.sessionId && !runnerPage) throw new Error(`Runner page session not found for browser dispatch: ${target.sessionId}`);
|
|
3396
|
+
if (!runnerPage && !activeContainerPage) throw new Error('Browser container page is not initialized');
|
|
3397
|
+
try {
|
|
3398
|
+
return await implementation.dispatchRpc({
|
|
3399
|
+
containerPage: runnerPage ? void 0 : activeContainerPage ?? void 0,
|
|
3400
|
+
runnerPage,
|
|
3401
|
+
request,
|
|
3402
|
+
timeoutFallbackMs
|
|
3403
|
+
});
|
|
3404
|
+
} catch (error) {
|
|
3405
|
+
if (error instanceof Error) throw error.message;
|
|
3406
|
+
throw String(error);
|
|
3407
|
+
}
|
|
3408
|
+
};
|
|
3409
|
+
runtime.dispatchHandlers.set('browser', async (dispatchRequest)=>{
|
|
3410
|
+
const request = validateBrowserRpcRequest(dispatchRequest.args);
|
|
3411
|
+
return dispatchBrowserRpcRequest({
|
|
3412
|
+
request,
|
|
3413
|
+
target: dispatchRequest.target
|
|
3414
|
+
});
|
|
3415
|
+
});
|
|
2397
3416
|
runtime.setContainerOptions(hostOptions);
|
|
2398
3417
|
const reporterResults = [];
|
|
2399
3418
|
const caseResults = [];
|
|
2400
|
-
let completedTests = 0;
|
|
2401
3419
|
let fatalError = null;
|
|
3420
|
+
const snapshotRpcMethods = {
|
|
3421
|
+
async resolveSnapshotPath (testPath) {
|
|
3422
|
+
const snapExtension = '.snap';
|
|
3423
|
+
const resolver = context.normalizedConfig.resolveSnapshotPath || (()=>join(dirname(testPath), '__snapshots__', `${basename(testPath)}${snapExtension}`));
|
|
3424
|
+
return resolver(testPath, snapExtension);
|
|
3425
|
+
},
|
|
3426
|
+
async readSnapshotFile (filepath) {
|
|
3427
|
+
try {
|
|
3428
|
+
return await promises.readFile(filepath, 'utf-8');
|
|
3429
|
+
} catch {
|
|
3430
|
+
return null;
|
|
3431
|
+
}
|
|
3432
|
+
},
|
|
3433
|
+
async saveSnapshotFile (filepath, content) {
|
|
3434
|
+
const dir = dirname(filepath);
|
|
3435
|
+
await promises.mkdir(dir, {
|
|
3436
|
+
recursive: true
|
|
3437
|
+
});
|
|
3438
|
+
await promises.writeFile(filepath, content, 'utf-8');
|
|
3439
|
+
},
|
|
3440
|
+
async removeSnapshotFile (filepath) {
|
|
3441
|
+
try {
|
|
3442
|
+
await promises.unlink(filepath);
|
|
3443
|
+
} catch {}
|
|
3444
|
+
}
|
|
3445
|
+
};
|
|
3446
|
+
const handleTestFileStart = async (payload)=>{
|
|
3447
|
+
await Promise.all(context.reporters.map((reporter)=>reporter.onTestFileStart?.({
|
|
3448
|
+
testPath: payload.testPath,
|
|
3449
|
+
tests: []
|
|
3450
|
+
})));
|
|
3451
|
+
};
|
|
3452
|
+
const handleTestFileReady = async (payload)=>{
|
|
3453
|
+
await Promise.all(context.reporters.map((reporter)=>reporter.onTestFileReady?.(payload)));
|
|
3454
|
+
};
|
|
3455
|
+
const handleTestSuiteStart = async (payload)=>{
|
|
3456
|
+
await Promise.all(context.reporters.map((reporter)=>reporter.onTestSuiteStart?.(payload)));
|
|
3457
|
+
};
|
|
3458
|
+
const handleTestSuiteResult = async (payload)=>{
|
|
3459
|
+
await Promise.all(context.reporters.map((reporter)=>reporter.onTestSuiteResult?.(payload)));
|
|
3460
|
+
};
|
|
3461
|
+
const handleTestCaseStart = async (payload)=>{
|
|
3462
|
+
await Promise.all(context.reporters.map((reporter)=>reporter.onTestCaseStart?.(payload)));
|
|
3463
|
+
};
|
|
3464
|
+
const handleTestCaseResult = async (payload)=>{
|
|
3465
|
+
caseResults.push(payload);
|
|
3466
|
+
await Promise.all(context.reporters.map((reporter)=>reporter.onTestCaseResult?.(payload)));
|
|
3467
|
+
};
|
|
3468
|
+
const handleTestFileComplete = async (payload)=>{
|
|
3469
|
+
reporterResults.push(payload);
|
|
3470
|
+
context.updateReporterResultState([
|
|
3471
|
+
payload
|
|
3472
|
+
], payload.results);
|
|
3473
|
+
if (payload.snapshotResult) context.snapshotManager.add(payload.snapshotResult);
|
|
3474
|
+
await Promise.all(context.reporters.map((reporter)=>reporter.onTestFileResult?.(payload)));
|
|
3475
|
+
if ('fail' === payload.status) ensureProcessExitCode(1);
|
|
3476
|
+
};
|
|
3477
|
+
const handleLog = async (payload)=>{
|
|
3478
|
+
const log = {
|
|
3479
|
+
content: payload.content,
|
|
3480
|
+
name: payload.level,
|
|
3481
|
+
testPath: payload.testPath,
|
|
3482
|
+
type: payload.type,
|
|
3483
|
+
trace: payload.trace
|
|
3484
|
+
};
|
|
3485
|
+
const shouldLog = context.normalizedConfig.onConsoleLog?.(log.content) ?? true;
|
|
3486
|
+
if (shouldLog) await Promise.all(context.reporters.map((reporter)=>reporter.onUserConsoleLog?.(log)));
|
|
3487
|
+
};
|
|
3488
|
+
const handleFatal = async (payload)=>{
|
|
3489
|
+
const error = new Error(payload.message);
|
|
3490
|
+
error.stack = payload.stack;
|
|
3491
|
+
fatalError = error;
|
|
3492
|
+
ensureProcessExitCode(1);
|
|
3493
|
+
};
|
|
3494
|
+
const runSnapshotRpc = async (request)=>{
|
|
3495
|
+
switch(request.method){
|
|
3496
|
+
case 'resolveSnapshotPath':
|
|
3497
|
+
return snapshotRpcMethods.resolveSnapshotPath(request.args.testPath);
|
|
3498
|
+
case 'readSnapshotFile':
|
|
3499
|
+
return snapshotRpcMethods.readSnapshotFile(request.args.filepath);
|
|
3500
|
+
case 'saveSnapshotFile':
|
|
3501
|
+
return snapshotRpcMethods.saveSnapshotFile(request.args.filepath, request.args.content);
|
|
3502
|
+
case 'removeSnapshotFile':
|
|
3503
|
+
return snapshotRpcMethods.removeSnapshotFile(request.args.filepath);
|
|
3504
|
+
default:
|
|
3505
|
+
return;
|
|
3506
|
+
}
|
|
3507
|
+
};
|
|
3508
|
+
const createDispatchRouter = (options)=>createHostDispatchRouter({
|
|
3509
|
+
routerOptions: options,
|
|
3510
|
+
runnerCallbacks: {
|
|
3511
|
+
onTestFileStart: handleTestFileStart,
|
|
3512
|
+
onTestFileReady: handleTestFileReady,
|
|
3513
|
+
onTestSuiteStart: handleTestSuiteStart,
|
|
3514
|
+
onTestSuiteResult: handleTestSuiteResult,
|
|
3515
|
+
onTestCaseStart: handleTestCaseStart,
|
|
3516
|
+
onTestCaseResult: handleTestCaseResult,
|
|
3517
|
+
onTestFileComplete: handleTestFileComplete,
|
|
3518
|
+
onLog: handleLog,
|
|
3519
|
+
onFatal: handleFatal
|
|
3520
|
+
},
|
|
3521
|
+
runSnapshotRpc,
|
|
3522
|
+
extensionHandlers: runtime.dispatchHandlers,
|
|
3523
|
+
onDuplicateNamespace: (namespace)=>{
|
|
3524
|
+
logger.debug(`[Dispatch] Skip registering dispatch namespace "${namespace}" because it is already reserved`);
|
|
3525
|
+
}
|
|
3526
|
+
});
|
|
3527
|
+
if (useHeadlessDirect) {
|
|
3528
|
+
const viewportByProject = mapViewportByProject(projectRuntimeConfigs);
|
|
3529
|
+
const runLifecycle = new RunSessionLifecycle();
|
|
3530
|
+
const sessionRegistry = new RunnerSessionRegistry();
|
|
3531
|
+
getHeadlessRunnerPageBySessionId = (sessionId)=>sessionRegistry.getById(sessionId)?.page;
|
|
3532
|
+
let dispatchRequestCounter = 0;
|
|
3533
|
+
const nextDispatchRequestId = (namespace)=>`${namespace}-${++dispatchRequestCounter}`;
|
|
3534
|
+
const closeContextSafely = async (browserContext)=>{
|
|
3535
|
+
try {
|
|
3536
|
+
await browserContext.close();
|
|
3537
|
+
} catch {}
|
|
3538
|
+
};
|
|
3539
|
+
const cancelRun = async (run, waitForDone = true)=>{
|
|
3540
|
+
await runLifecycle.cancel(run, {
|
|
3541
|
+
waitForDone,
|
|
3542
|
+
onCancel: async (session)=>{
|
|
3543
|
+
await Promise.all(Array.from(session.contexts).map((browserContext)=>closeContextSafely(browserContext)));
|
|
3544
|
+
}
|
|
3545
|
+
});
|
|
3546
|
+
};
|
|
3547
|
+
const dispatchRouter = createDispatchRouter({
|
|
3548
|
+
isRunTokenStale: (runToken)=>runLifecycle.isTokenStale(runToken),
|
|
3549
|
+
onStale: (request)=>{
|
|
3550
|
+
if (request.namespace === DISPATCH_NAMESPACE_RUNNER) logger.debug(`[Headless] Dropped stale message "${request.method}" for ${request.target?.testFile ?? 'unknown'}`);
|
|
3551
|
+
}
|
|
3552
|
+
});
|
|
3553
|
+
const dispatchRunnerMessage = async (run, file, sessionId, message)=>{
|
|
3554
|
+
const response = await dispatchRouter.dispatch({
|
|
3555
|
+
requestId: nextDispatchRequestId('runner'),
|
|
3556
|
+
runToken: run.token,
|
|
3557
|
+
namespace: DISPATCH_NAMESPACE_RUNNER,
|
|
3558
|
+
method: message.type,
|
|
3559
|
+
args: 'payload' in message ? message.payload : void 0,
|
|
3560
|
+
target: {
|
|
3561
|
+
sessionId,
|
|
3562
|
+
testFile: file.testPath,
|
|
3563
|
+
projectName: file.projectName
|
|
3564
|
+
}
|
|
3565
|
+
});
|
|
3566
|
+
if (response.stale) return;
|
|
3567
|
+
if (response.error) throw new Error(response.error);
|
|
3568
|
+
};
|
|
3569
|
+
const runSingleFile = async (run, file)=>{
|
|
3570
|
+
if (run.cancelled || runLifecycle.isTokenStale(run.token)) return;
|
|
3571
|
+
const viewport = viewportByProject.get(file.projectName);
|
|
3572
|
+
const browserContext = await browser.newContext({
|
|
3573
|
+
viewport: viewport ?? null
|
|
3574
|
+
});
|
|
3575
|
+
run.contexts.add(browserContext);
|
|
3576
|
+
let page = null;
|
|
3577
|
+
let sessionId = null;
|
|
3578
|
+
let settled = false;
|
|
3579
|
+
let resolveDone = null;
|
|
3580
|
+
const markDone = ()=>{
|
|
3581
|
+
if (!settled) {
|
|
3582
|
+
settled = true;
|
|
3583
|
+
resolveDone?.();
|
|
3584
|
+
}
|
|
3585
|
+
};
|
|
3586
|
+
const donePromise = new Promise((resolve)=>{
|
|
3587
|
+
resolveDone = resolve;
|
|
3588
|
+
});
|
|
3589
|
+
const projectRuntime = projectRuntimeConfigs.find((project)=>project.name === file.projectName);
|
|
3590
|
+
const perFileTimeoutMs = (projectRuntime?.runtimeConfig.testTimeout ?? maxTestTimeoutForRpc) + 30000;
|
|
3591
|
+
let timeoutId;
|
|
3592
|
+
try {
|
|
3593
|
+
page = await browserContext.newPage();
|
|
3594
|
+
const session = sessionRegistry.register({
|
|
3595
|
+
testFile: file.testPath,
|
|
3596
|
+
projectName: file.projectName,
|
|
3597
|
+
runToken: run.token,
|
|
3598
|
+
mode: 'headless-page',
|
|
3599
|
+
context: browserContext,
|
|
3600
|
+
page
|
|
3601
|
+
});
|
|
3602
|
+
sessionId = session.id;
|
|
3603
|
+
await attachHeadlessRunnerTransport(page, {
|
|
3604
|
+
onDispatchMessage: async (message)=>{
|
|
3605
|
+
try {
|
|
3606
|
+
await dispatchRunnerMessage(run, file, session.id, message);
|
|
3607
|
+
if ('file-complete' === message.type || 'complete' === message.type) markDone();
|
|
3608
|
+
else if ('fatal' === message.type) {
|
|
3609
|
+
markDone();
|
|
3610
|
+
await cancelRun(run, false);
|
|
3611
|
+
}
|
|
3612
|
+
} catch (error) {
|
|
3613
|
+
const formatted = toError(error);
|
|
3614
|
+
await handleFatal({
|
|
3615
|
+
message: formatted.message,
|
|
3616
|
+
stack: formatted.stack
|
|
3617
|
+
});
|
|
3618
|
+
markDone();
|
|
3619
|
+
await cancelRun(run, false);
|
|
3620
|
+
}
|
|
3621
|
+
},
|
|
3622
|
+
onDispatchRpc: async (request)=>dispatchRouter.dispatch({
|
|
3623
|
+
...request,
|
|
3624
|
+
runToken: run.token,
|
|
3625
|
+
target: {
|
|
3626
|
+
sessionId: session.id,
|
|
3627
|
+
testFile: file.testPath,
|
|
3628
|
+
projectName: file.projectName,
|
|
3629
|
+
...request.target
|
|
3630
|
+
}
|
|
3631
|
+
})
|
|
3632
|
+
});
|
|
3633
|
+
const inlineOptions = {
|
|
3634
|
+
...hostOptions,
|
|
3635
|
+
testFile: file.testPath,
|
|
3636
|
+
runId: `${run.token}:${session.id}`
|
|
3637
|
+
};
|
|
3638
|
+
const serializedOptions = serializeForInlineScript(inlineOptions);
|
|
3639
|
+
await page.addInitScript(`window.__RSTEST_BROWSER_OPTIONS__ = ${serializedOptions};`);
|
|
3640
|
+
await page.goto(`http://localhost:${port}/runner.html`, {
|
|
3641
|
+
waitUntil: 'load'
|
|
3642
|
+
});
|
|
3643
|
+
const timeoutPromise = new Promise((resolve)=>{
|
|
3644
|
+
timeoutId = setTimeout(()=>resolve('timeout'), perFileTimeoutMs);
|
|
3645
|
+
});
|
|
3646
|
+
const state = await Promise.race([
|
|
3647
|
+
donePromise.then(()=>'done'),
|
|
3648
|
+
timeoutPromise,
|
|
3649
|
+
run.cancelSignal.then(()=>'cancelled')
|
|
3650
|
+
]);
|
|
3651
|
+
if ('cancelled' === state) return;
|
|
3652
|
+
if ('timeout' === state && runLifecycle.isTokenActive(run.token) && !run.cancelled) {
|
|
3653
|
+
await handleFatal({
|
|
3654
|
+
message: `Test execution timeout after ${perFileTimeoutMs / 1000}s for ${file.testPath}.`
|
|
3655
|
+
});
|
|
3656
|
+
await cancelRun(run, false);
|
|
3657
|
+
}
|
|
3658
|
+
} catch (error) {
|
|
3659
|
+
if (runLifecycle.isTokenActive(run.token) && !run.cancelled) {
|
|
3660
|
+
const formatted = toError(error);
|
|
3661
|
+
await handleFatal({
|
|
3662
|
+
message: formatted.message,
|
|
3663
|
+
stack: formatted.stack
|
|
3664
|
+
});
|
|
3665
|
+
await cancelRun(run, false);
|
|
3666
|
+
}
|
|
3667
|
+
} finally{
|
|
3668
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
3669
|
+
if (page) try {
|
|
3670
|
+
await page.close();
|
|
3671
|
+
} catch {}
|
|
3672
|
+
if (sessionId) sessionRegistry.deleteById(sessionId);
|
|
3673
|
+
run.contexts.delete(browserContext);
|
|
3674
|
+
await closeContextSafely(browserContext);
|
|
3675
|
+
}
|
|
3676
|
+
};
|
|
3677
|
+
const runFilesWithPool = async (files)=>{
|
|
3678
|
+
if (0 === files.length) return;
|
|
3679
|
+
const previous = runLifecycle.activeSession;
|
|
3680
|
+
if (previous) await cancelRun(previous);
|
|
3681
|
+
const run = runLifecycle.createSession((token)=>({
|
|
3682
|
+
...createRunSession(token),
|
|
3683
|
+
contexts: new Set()
|
|
3684
|
+
}));
|
|
3685
|
+
const queue = [
|
|
3686
|
+
...files
|
|
3687
|
+
];
|
|
3688
|
+
const concurrency = getHeadlessConcurrency(context, queue.length);
|
|
3689
|
+
const worker = async ()=>{
|
|
3690
|
+
while(queue.length > 0 && !run.cancelled && runLifecycle.isTokenActive(run.token)){
|
|
3691
|
+
const next = queue.shift();
|
|
3692
|
+
if (!next) return;
|
|
3693
|
+
await runSingleFile(run, next);
|
|
3694
|
+
}
|
|
3695
|
+
};
|
|
3696
|
+
run.done = Promise.all(Array.from({
|
|
3697
|
+
length: Math.min(queue.length, Math.max(concurrency, 1))
|
|
3698
|
+
}, ()=>worker())).then(()=>{});
|
|
3699
|
+
await run.done;
|
|
3700
|
+
runLifecycle.clearIfActive(run);
|
|
3701
|
+
};
|
|
3702
|
+
const latestRerunScheduler = createHeadlessLatestRerunScheduler({
|
|
3703
|
+
getActiveRun: ()=>runLifecycle.activeSession,
|
|
3704
|
+
isRunCancelled: (run)=>run.cancelled,
|
|
3705
|
+
invalidateActiveRun: ()=>{
|
|
3706
|
+
runLifecycle.invalidateActiveToken();
|
|
3707
|
+
},
|
|
3708
|
+
interruptActiveRun: async (run)=>{
|
|
3709
|
+
await cancelRun(run, false);
|
|
3710
|
+
},
|
|
3711
|
+
runFiles: async (files)=>{
|
|
3712
|
+
await notifyTestRunStart();
|
|
3713
|
+
const rerunStartTime = Date.now();
|
|
3714
|
+
const fatalErrorBeforeRun = fatalError;
|
|
3715
|
+
let rerunError;
|
|
3716
|
+
try {
|
|
3717
|
+
await runFilesWithPool(files);
|
|
3718
|
+
} catch (error) {
|
|
3719
|
+
rerunError = toError(error);
|
|
3720
|
+
throw error;
|
|
3721
|
+
} finally{
|
|
3722
|
+
const testTime = Math.max(0, Date.now() - rerunStartTime);
|
|
3723
|
+
const rerunFatalError = fatalError && fatalError !== fatalErrorBeforeRun ? fatalError : void 0;
|
|
3724
|
+
await notifyTestRunEnd({
|
|
3725
|
+
duration: {
|
|
3726
|
+
totalTime: testTime,
|
|
3727
|
+
buildTime: 0,
|
|
3728
|
+
testTime
|
|
3729
|
+
},
|
|
3730
|
+
filterRerunTestPaths: files.map((file)=>file.testPath),
|
|
3731
|
+
unhandledErrors: rerunError ? [
|
|
3732
|
+
rerunError
|
|
3733
|
+
] : rerunFatalError ? [
|
|
3734
|
+
rerunFatalError
|
|
3735
|
+
] : void 0
|
|
3736
|
+
});
|
|
3737
|
+
}
|
|
3738
|
+
},
|
|
3739
|
+
onError: async (error)=>{
|
|
3740
|
+
const formatted = toError(error);
|
|
3741
|
+
await handleFatal({
|
|
3742
|
+
message: formatted.message,
|
|
3743
|
+
stack: formatted.stack
|
|
3744
|
+
});
|
|
3745
|
+
},
|
|
3746
|
+
onInterrupt: (run)=>{
|
|
3747
|
+
logger.debug(`[Headless] Interrupting active run token ${run.token} before scheduling latest rerun`);
|
|
3748
|
+
}
|
|
3749
|
+
});
|
|
3750
|
+
const testStart = Date.now();
|
|
3751
|
+
await runFilesWithPool(allTestFiles);
|
|
3752
|
+
const testTime = Date.now() - testStart;
|
|
3753
|
+
if (isWatchMode) triggerRerun = async ()=>{
|
|
3754
|
+
const newProjectEntries = await collectProjectEntries(context);
|
|
3755
|
+
const rerunPlan = planWatchRerun({
|
|
3756
|
+
projectEntries: newProjectEntries,
|
|
3757
|
+
previousTestFiles: watchContext.lastTestFiles,
|
|
3758
|
+
affectedTestFiles: watchContext.affectedTestFiles
|
|
3759
|
+
});
|
|
3760
|
+
watchContext.affectedTestFiles = [];
|
|
3761
|
+
if (rerunPlan.filesChanged) {
|
|
3762
|
+
const deletedTestPaths = collectDeletedTestPaths(watchContext.lastTestFiles, rerunPlan.currentTestFiles);
|
|
3763
|
+
if (deletedTestPaths.length > 0) context.updateReporterResultState([], [], deletedTestPaths);
|
|
3764
|
+
watchContext.lastTestFiles = rerunPlan.currentTestFiles;
|
|
3765
|
+
if (0 === rerunPlan.currentTestFiles.length) {
|
|
3766
|
+
await latestRerunScheduler.enqueueLatest([]);
|
|
3767
|
+
logger.log(color.cyan('No browser test files remain after update.\n'));
|
|
3768
|
+
return;
|
|
3769
|
+
}
|
|
3770
|
+
logger.log(color.cyan(`Test file set changed, re-running ${rerunPlan.currentTestFiles.length} file(s)...\n`));
|
|
3771
|
+
latestRerunScheduler.enqueueLatest(rerunPlan.currentTestFiles);
|
|
3772
|
+
return;
|
|
3773
|
+
}
|
|
3774
|
+
if (0 === rerunPlan.affectedTestFiles.length) return void logger.log(color.cyan('No affected browser test files detected, skipping re-run.\n'));
|
|
3775
|
+
logger.log(color.cyan(`Re-running ${rerunPlan.affectedTestFiles.length} affected test file(s)...\n`));
|
|
3776
|
+
latestRerunScheduler.enqueueLatest(rerunPlan.affectedTestFiles);
|
|
3777
|
+
};
|
|
3778
|
+
const closeHeadlessRuntime = isWatchMode ? void 0 : async ()=>{
|
|
3779
|
+
sessionRegistry.clear();
|
|
3780
|
+
await destroyBrowserRuntime(runtime);
|
|
3781
|
+
};
|
|
3782
|
+
if (fatalError) return failWithError(fatalError, closeHeadlessRuntime);
|
|
3783
|
+
const duration = {
|
|
3784
|
+
totalTime: buildTime + testTime,
|
|
3785
|
+
buildTime,
|
|
3786
|
+
testTime
|
|
3787
|
+
};
|
|
3788
|
+
context.updateReporterResultState(reporterResults, caseResults);
|
|
3789
|
+
const isFailure = reporterResults.some((result)=>'fail' === result.status);
|
|
3790
|
+
if (isFailure) ensureProcessExitCode(1);
|
|
3791
|
+
const result = {
|
|
3792
|
+
results: reporterResults,
|
|
3793
|
+
testResults: caseResults,
|
|
3794
|
+
duration,
|
|
3795
|
+
hasFailure: isFailure,
|
|
3796
|
+
getSourcemap: getBrowserSourcemap,
|
|
3797
|
+
resolveSourcemap: resolveBrowserSourcemap,
|
|
3798
|
+
close: skipOnTestRunEnd ? closeHeadlessRuntime : void 0
|
|
3799
|
+
};
|
|
3800
|
+
if (!skipOnTestRunEnd) try {
|
|
3801
|
+
await notifyTestRunEnd({
|
|
3802
|
+
duration
|
|
3803
|
+
});
|
|
3804
|
+
} finally{
|
|
3805
|
+
await closeHeadlessRuntime?.();
|
|
3806
|
+
}
|
|
3807
|
+
if (isWatchMode && triggerRerun) {
|
|
3808
|
+
watchContext.hooksEnabled = true;
|
|
3809
|
+
logger.log(color.cyan('\nWatch mode enabled - will re-run tests on file changes\n'));
|
|
3810
|
+
}
|
|
3811
|
+
return result;
|
|
3812
|
+
}
|
|
3813
|
+
let completedTests = 0;
|
|
2402
3814
|
let resolveAllTests;
|
|
2403
3815
|
const allTestsPromise = new Promise((resolve)=>{
|
|
2404
3816
|
resolveAllTests = resolve;
|
|
@@ -2428,9 +3840,11 @@ const runBrowserController = async (context, options)=>{
|
|
|
2428
3840
|
}
|
|
2429
3841
|
containerPage.on('console', (msg)=>{
|
|
2430
3842
|
const text = msg.text();
|
|
2431
|
-
if (text.startsWith('[Container]') || text.startsWith('[Runner]')
|
|
3843
|
+
if (text.startsWith('[Container]') || text.startsWith('[Runner]')) logger.log(color.gray(`[Browser Console] ${text}`));
|
|
2432
3844
|
});
|
|
2433
3845
|
}
|
|
3846
|
+
activeContainerPage = containerPage;
|
|
3847
|
+
const dispatchRouter = createDispatchRouter();
|
|
2434
3848
|
const createRpcMethods = ()=>({
|
|
2435
3849
|
async rerunTest (testFile, testNamePattern) {
|
|
2436
3850
|
const projectName = context.normalizedConfig.name || 'project';
|
|
@@ -2443,61 +3857,25 @@ const runBrowserController = async (context, options)=>{
|
|
|
2443
3857
|
return allTestFiles;
|
|
2444
3858
|
},
|
|
2445
3859
|
async onTestFileStart (payload) {
|
|
2446
|
-
await
|
|
2447
|
-
testPath: payload.testPath,
|
|
2448
|
-
tests: []
|
|
2449
|
-
})));
|
|
3860
|
+
await handleTestFileStart(payload);
|
|
2450
3861
|
},
|
|
2451
3862
|
async onTestCaseResult (payload) {
|
|
2452
|
-
|
|
2453
|
-
await Promise.all(context.reporters.map((reporter)=>reporter.onTestCaseResult?.(payload)));
|
|
3863
|
+
await handleTestCaseResult(payload);
|
|
2454
3864
|
},
|
|
2455
3865
|
async onTestFileComplete (payload) {
|
|
2456
|
-
|
|
2457
|
-
if (payload.snapshotResult) context.snapshotManager.add(payload.snapshotResult);
|
|
2458
|
-
await Promise.all(context.reporters.map((reporter)=>reporter.onTestFileResult?.(payload)));
|
|
3866
|
+
await handleTestFileComplete(payload);
|
|
2459
3867
|
completedTests++;
|
|
2460
3868
|
if (completedTests >= allTestFiles.length && resolveAllTests) resolveAllTests();
|
|
2461
3869
|
},
|
|
2462
3870
|
async onLog (payload) {
|
|
2463
|
-
|
|
2464
|
-
content: payload.content,
|
|
2465
|
-
name: payload.level,
|
|
2466
|
-
testPath: payload.testPath,
|
|
2467
|
-
type: payload.type,
|
|
2468
|
-
trace: payload.trace
|
|
2469
|
-
};
|
|
2470
|
-
const shouldLog = context.normalizedConfig.onConsoleLog?.(log.content) ?? true;
|
|
2471
|
-
if (shouldLog) await Promise.all(context.reporters.map((reporter)=>reporter.onUserConsoleLog?.(log)));
|
|
3871
|
+
await handleLog(payload);
|
|
2472
3872
|
},
|
|
2473
3873
|
async onFatal (payload) {
|
|
2474
|
-
|
|
2475
|
-
fatalError.stack = payload.stack;
|
|
3874
|
+
await handleFatal(payload);
|
|
2476
3875
|
if (resolveAllTests) resolveAllTests();
|
|
2477
3876
|
},
|
|
2478
|
-
async
|
|
2479
|
-
|
|
2480
|
-
const resolver = context.normalizedConfig.resolveSnapshotPath || (()=>join(dirname(testPath), '__snapshots__', `${basename(testPath)}${snapExtension}`));
|
|
2481
|
-
return resolver(testPath, snapExtension);
|
|
2482
|
-
},
|
|
2483
|
-
async readSnapshotFile (filepath) {
|
|
2484
|
-
try {
|
|
2485
|
-
return await promises.readFile(filepath, 'utf-8');
|
|
2486
|
-
} catch {
|
|
2487
|
-
return null;
|
|
2488
|
-
}
|
|
2489
|
-
},
|
|
2490
|
-
async saveSnapshotFile (filepath, content) {
|
|
2491
|
-
const dir = dirname(filepath);
|
|
2492
|
-
await promises.mkdir(dir, {
|
|
2493
|
-
recursive: true
|
|
2494
|
-
});
|
|
2495
|
-
await promises.writeFile(filepath, content, 'utf-8');
|
|
2496
|
-
},
|
|
2497
|
-
async removeSnapshotFile (filepath) {
|
|
2498
|
-
try {
|
|
2499
|
-
await promises.unlink(filepath);
|
|
2500
|
-
} catch {}
|
|
3877
|
+
async dispatch (request) {
|
|
3878
|
+
return dispatchRouter.dispatch(request);
|
|
2501
3879
|
}
|
|
2502
3880
|
});
|
|
2503
3881
|
let rpcManager;
|
|
@@ -2511,11 +3889,7 @@ const runBrowserController = async (context, options)=>{
|
|
|
2511
3889
|
if (isWatchMode) runtime.rpcManager = rpcManager;
|
|
2512
3890
|
}
|
|
2513
3891
|
if (isNewPage) {
|
|
2514
|
-
const pagePath =
|
|
2515
|
-
if (useSchedulerPage) {
|
|
2516
|
-
const serializedOptions = serializeForInlineScript(hostOptions);
|
|
2517
|
-
await containerPage.addInitScript(`window.__RSTEST_BROWSER_OPTIONS__ = ${serializedOptions};`);
|
|
2518
|
-
}
|
|
3892
|
+
const pagePath = '/';
|
|
2519
3893
|
await containerPage.goto(`http://localhost:${port}${pagePath}`, {
|
|
2520
3894
|
waitUntil: 'load'
|
|
2521
3895
|
});
|
|
@@ -2539,24 +3913,49 @@ const runBrowserController = async (context, options)=>{
|
|
|
2539
3913
|
const testTime = Date.now() - testStart;
|
|
2540
3914
|
if (isWatchMode) triggerRerun = async ()=>{
|
|
2541
3915
|
const newProjectEntries = await collectProjectEntries(context);
|
|
2542
|
-
const
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
const filesChanged = serialize(currentTestFiles) !== serialize(watchContext.lastTestFiles);
|
|
2548
|
-
if (filesChanged) {
|
|
2549
|
-
watchContext.lastTestFiles = currentTestFiles;
|
|
2550
|
-
await rpcManager.notifyTestFileUpdate(currentTestFiles);
|
|
2551
|
-
}
|
|
2552
|
-
const affectedFiles = watchContext.affectedTestFiles;
|
|
3916
|
+
const rerunPlan = planWatchRerun({
|
|
3917
|
+
projectEntries: newProjectEntries,
|
|
3918
|
+
previousTestFiles: watchContext.lastTestFiles,
|
|
3919
|
+
affectedTestFiles: watchContext.affectedTestFiles
|
|
3920
|
+
});
|
|
2553
3921
|
watchContext.affectedTestFiles = [];
|
|
2554
|
-
if (
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
3922
|
+
if (rerunPlan.filesChanged) {
|
|
3923
|
+
const deletedTestPaths = collectDeletedTestPaths(watchContext.lastTestFiles, rerunPlan.currentTestFiles);
|
|
3924
|
+
if (deletedTestPaths.length > 0) context.updateReporterResultState([], [], deletedTestPaths);
|
|
3925
|
+
watchContext.lastTestFiles = rerunPlan.currentTestFiles;
|
|
3926
|
+
await rpcManager.notifyTestFileUpdate(rerunPlan.currentTestFiles);
|
|
3927
|
+
}
|
|
3928
|
+
if (rerunPlan.normalizedAffectedTestFiles.length > 0) {
|
|
3929
|
+
logger.log(color.cyan(`Re-running ${rerunPlan.normalizedAffectedTestFiles.length} affected test file(s)...\n`));
|
|
3930
|
+
await notifyTestRunStart();
|
|
3931
|
+
const rerunStartTime = Date.now();
|
|
3932
|
+
const fatalErrorBeforeRun = fatalError;
|
|
3933
|
+
let rerunError;
|
|
3934
|
+
try {
|
|
3935
|
+
for (const testFile of rerunPlan.normalizedAffectedTestFiles)await rpcManager.reloadTestFile(testFile);
|
|
3936
|
+
} catch (error) {
|
|
3937
|
+
rerunError = toError(error);
|
|
3938
|
+
throw error;
|
|
3939
|
+
} finally{
|
|
3940
|
+
const testTime = Math.max(0, Date.now() - rerunStartTime);
|
|
3941
|
+
const rerunFatalError = fatalError && fatalError !== fatalErrorBeforeRun ? fatalError : void 0;
|
|
3942
|
+
await notifyTestRunEnd({
|
|
3943
|
+
duration: {
|
|
3944
|
+
totalTime: testTime,
|
|
3945
|
+
buildTime: 0,
|
|
3946
|
+
testTime
|
|
3947
|
+
},
|
|
3948
|
+
filterRerunTestPaths: rerunPlan.normalizedAffectedTestFiles,
|
|
3949
|
+
unhandledErrors: rerunError ? [
|
|
3950
|
+
rerunError
|
|
3951
|
+
] : rerunFatalError ? [
|
|
3952
|
+
rerunFatalError
|
|
3953
|
+
] : void 0
|
|
3954
|
+
});
|
|
3955
|
+
}
|
|
3956
|
+
} else if (!rerunPlan.filesChanged) logger.log(color.cyan('Tests will be re-executed automatically\n'));
|
|
2558
3957
|
};
|
|
2559
|
-
|
|
3958
|
+
const closeContainerRuntime = isWatchMode ? void 0 : async ()=>{
|
|
2560
3959
|
try {
|
|
2561
3960
|
await containerPage.close();
|
|
2562
3961
|
} catch {}
|
|
@@ -2564,8 +3963,8 @@ const runBrowserController = async (context, options)=>{
|
|
|
2564
3963
|
await containerContext.close();
|
|
2565
3964
|
} catch {}
|
|
2566
3965
|
await destroyBrowserRuntime(runtime);
|
|
2567
|
-
}
|
|
2568
|
-
if (fatalError) return failWithError(fatalError);
|
|
3966
|
+
};
|
|
3967
|
+
if (fatalError) return failWithError(fatalError, closeContainerRuntime);
|
|
2569
3968
|
const duration = {
|
|
2570
3969
|
totalTime: buildTime + testTime,
|
|
2571
3970
|
buildTime,
|
|
@@ -2578,15 +3977,18 @@ const runBrowserController = async (context, options)=>{
|
|
|
2578
3977
|
results: reporterResults,
|
|
2579
3978
|
testResults: caseResults,
|
|
2580
3979
|
duration,
|
|
2581
|
-
hasFailure: isFailure
|
|
3980
|
+
hasFailure: isFailure,
|
|
3981
|
+
getSourcemap: getBrowserSourcemap,
|
|
3982
|
+
resolveSourcemap: resolveBrowserSourcemap,
|
|
3983
|
+
close: skipOnTestRunEnd ? closeContainerRuntime : void 0
|
|
2582
3984
|
};
|
|
2583
|
-
if (!skipOnTestRunEnd)
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
}
|
|
3985
|
+
if (!skipOnTestRunEnd) try {
|
|
3986
|
+
await notifyTestRunEnd({
|
|
3987
|
+
duration
|
|
3988
|
+
});
|
|
3989
|
+
} finally{
|
|
3990
|
+
await closeContainerRuntime?.();
|
|
3991
|
+
}
|
|
2590
3992
|
if (isWatchMode && triggerRerun) {
|
|
2591
3993
|
watchContext.hooksEnabled = true;
|
|
2592
3994
|
logger.log(color.cyan('\nWatch mode enabled - will re-run tests on file changes\n'));
|
|
@@ -2606,6 +4008,7 @@ const listBrowserTests = async (context, options)=>{
|
|
|
2606
4008
|
manifestPath,
|
|
2607
4009
|
entries: projectEntries
|
|
2608
4010
|
});
|
|
4011
|
+
const browserProjects = getBrowserProjects(context);
|
|
2609
4012
|
let runtime;
|
|
2610
4013
|
try {
|
|
2611
4014
|
runtime = await createBrowserRuntime({
|
|
@@ -2619,11 +4022,13 @@ const listBrowserTests = async (context, options)=>{
|
|
|
2619
4022
|
forceHeadless: true
|
|
2620
4023
|
});
|
|
2621
4024
|
} catch (error) {
|
|
2622
|
-
|
|
4025
|
+
const providers = [
|
|
4026
|
+
...new Set(browserProjects.map((p)=>p.normalizedConfig.browser.provider))
|
|
4027
|
+
];
|
|
4028
|
+
logger.error(color.red(`Failed to initialize browser provider runtime (${providers.join(', ')}).`), error);
|
|
2623
4029
|
throw error;
|
|
2624
4030
|
}
|
|
2625
4031
|
const { browser, port } = runtime;
|
|
2626
|
-
const browserProjects = getBrowserProjects(context);
|
|
2627
4032
|
const projectRuntimeConfigs = browserProjects.map((project)=>({
|
|
2628
4033
|
name: project.name,
|
|
2629
4034
|
environmentName: project.environmentName,
|
|
@@ -2654,7 +4059,7 @@ const listBrowserTests = async (context, options)=>{
|
|
|
2654
4059
|
viewport: null
|
|
2655
4060
|
});
|
|
2656
4061
|
const page = await browserContext.newPage();
|
|
2657
|
-
await page.exposeFunction(
|
|
4062
|
+
await page.exposeFunction(DISPATCH_MESSAGE_TYPE, (message)=>{
|
|
2658
4063
|
switch(message.type){
|
|
2659
4064
|
case 'collect-result':
|
|
2660
4065
|
{
|
|
@@ -2736,99 +4141,6 @@ const listBrowserTests = async (context, options)=>{
|
|
|
2736
4141
|
close: cleanup
|
|
2737
4142
|
};
|
|
2738
4143
|
};
|
|
2739
|
-
const BROWSER_VIEWPORT_PRESET_IDS = [
|
|
2740
|
-
'iPhoneSE',
|
|
2741
|
-
'iPhoneXR',
|
|
2742
|
-
'iPhone12Pro',
|
|
2743
|
-
'iPhone14ProMax',
|
|
2744
|
-
'Pixel7',
|
|
2745
|
-
'SamsungGalaxyS8Plus',
|
|
2746
|
-
'SamsungGalaxyS20Ultra',
|
|
2747
|
-
'iPadMini',
|
|
2748
|
-
'iPadAir',
|
|
2749
|
-
'iPadPro',
|
|
2750
|
-
'SurfacePro7',
|
|
2751
|
-
'SurfaceDuo',
|
|
2752
|
-
'GalaxyZFold5',
|
|
2753
|
-
'AsusZenbookFold',
|
|
2754
|
-
'SamsungGalaxyA51A71',
|
|
2755
|
-
'NestHub',
|
|
2756
|
-
'NestHubMax'
|
|
2757
|
-
];
|
|
2758
|
-
const BROWSER_VIEWPORT_PRESET_DIMENSIONS = {
|
|
2759
|
-
iPhoneSE: {
|
|
2760
|
-
width: 375,
|
|
2761
|
-
height: 667
|
|
2762
|
-
},
|
|
2763
|
-
iPhoneXR: {
|
|
2764
|
-
width: 414,
|
|
2765
|
-
height: 896
|
|
2766
|
-
},
|
|
2767
|
-
iPhone12Pro: {
|
|
2768
|
-
width: 390,
|
|
2769
|
-
height: 844
|
|
2770
|
-
},
|
|
2771
|
-
iPhone14ProMax: {
|
|
2772
|
-
width: 430,
|
|
2773
|
-
height: 932
|
|
2774
|
-
},
|
|
2775
|
-
Pixel7: {
|
|
2776
|
-
width: 412,
|
|
2777
|
-
height: 915
|
|
2778
|
-
},
|
|
2779
|
-
SamsungGalaxyS8Plus: {
|
|
2780
|
-
width: 360,
|
|
2781
|
-
height: 740
|
|
2782
|
-
},
|
|
2783
|
-
SamsungGalaxyS20Ultra: {
|
|
2784
|
-
width: 412,
|
|
2785
|
-
height: 915
|
|
2786
|
-
},
|
|
2787
|
-
iPadMini: {
|
|
2788
|
-
width: 768,
|
|
2789
|
-
height: 1024
|
|
2790
|
-
},
|
|
2791
|
-
iPadAir: {
|
|
2792
|
-
width: 820,
|
|
2793
|
-
height: 1180
|
|
2794
|
-
},
|
|
2795
|
-
iPadPro: {
|
|
2796
|
-
width: 1024,
|
|
2797
|
-
height: 1366
|
|
2798
|
-
},
|
|
2799
|
-
SurfacePro7: {
|
|
2800
|
-
width: 912,
|
|
2801
|
-
height: 1368
|
|
2802
|
-
},
|
|
2803
|
-
SurfaceDuo: {
|
|
2804
|
-
width: 540,
|
|
2805
|
-
height: 720
|
|
2806
|
-
},
|
|
2807
|
-
GalaxyZFold5: {
|
|
2808
|
-
width: 344,
|
|
2809
|
-
height: 882
|
|
2810
|
-
},
|
|
2811
|
-
AsusZenbookFold: {
|
|
2812
|
-
width: 853,
|
|
2813
|
-
height: 1280
|
|
2814
|
-
},
|
|
2815
|
-
SamsungGalaxyA51A71: {
|
|
2816
|
-
width: 412,
|
|
2817
|
-
height: 914
|
|
2818
|
-
},
|
|
2819
|
-
NestHub: {
|
|
2820
|
-
width: 1024,
|
|
2821
|
-
height: 600
|
|
2822
|
-
},
|
|
2823
|
-
NestHubMax: {
|
|
2824
|
-
width: 1280,
|
|
2825
|
-
height: 800
|
|
2826
|
-
}
|
|
2827
|
-
};
|
|
2828
|
-
const resolveBrowserViewportPreset = (presetId)=>{
|
|
2829
|
-
const size = BROWSER_VIEWPORT_PRESET_DIMENSIONS[presetId];
|
|
2830
|
-
return size ?? null;
|
|
2831
|
-
};
|
|
2832
4144
|
const SUPPORTED_PROVIDERS = [
|
|
2833
4145
|
'playwright'
|
|
2834
4146
|
];
|