@ralphralphai/playwright 2.9.0 → 2.11.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/README.md +52 -0
- package/dist/index.cjs +11 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.mts +37 -1
- package/dist/index.mjs +11 -1
- package/dist/index.mjs.map +1 -1
- package/dist/reporter.cjs +1 -1
- package/dist/reporter.mjs +1 -1
- package/dist/{upload-8iYQZaCa.cjs → upload-B7tSC0NA.cjs} +146 -13
- package/dist/upload-B7tSC0NA.cjs.map +1 -0
- package/dist/{upload-DKeVoDQ2.mjs → upload-C_KLRH9y.mjs} +146 -13
- package/dist/upload-C_KLRH9y.mjs.map +1 -0
- package/package.json +2 -2
- package/dist/upload-8iYQZaCa.cjs.map +0 -1
- package/dist/upload-DKeVoDQ2.mjs.map +0 -1
|
@@ -45,7 +45,7 @@ function snapshot(value) {
|
|
|
45
45
|
}
|
|
46
46
|
//#endregion
|
|
47
47
|
//#region package.json
|
|
48
|
-
var version = "2.
|
|
48
|
+
var version = "2.11.0";
|
|
49
49
|
//#endregion
|
|
50
50
|
//#region src/actions.ts
|
|
51
51
|
const BINDING = "__ralphAction";
|
|
@@ -664,7 +664,12 @@ async function captureAriaSnapshot(page, { viewport, scroll }, ignore = []) {
|
|
|
664
664
|
});
|
|
665
665
|
const ignoreSelector = ignore.length === 0 ? void 0 : ignore.join(", ");
|
|
666
666
|
const facts = /* @__PURE__ */ new Map();
|
|
667
|
-
const
|
|
667
|
+
const detached = [];
|
|
668
|
+
const normalized = (await Promise.all(visible.map((node) => readTree(page, node, ignoreSelector, facts, detached)))).filter((node) => node !== void 0);
|
|
669
|
+
if (detached.length > 0) {
|
|
670
|
+
const listed = detached.slice(0, 5).map(({ role, name }) => name ? `${role} "${name}"` : role).join(", ");
|
|
671
|
+
console.warn(`Ralph: ${detached.length} element(s) left ${page.url()} during capture and were dropped from its aria snapshot: ${listed}${detached.length > 5 ? ", ..." : ""}`);
|
|
672
|
+
}
|
|
668
673
|
const described = await Promise.all(normalized.map((node) => describe(page, node, {}, false, facts)));
|
|
669
674
|
const json = Buffer.from(JSON.stringify(fillMissingRefs(described)));
|
|
670
675
|
return json.byteLength > 4194304 ? void 0 : json;
|
|
@@ -879,8 +884,11 @@ function fillMissingRefs(nodes) {
|
|
|
879
884
|
return nodes.map(fill);
|
|
880
885
|
}
|
|
881
886
|
async function readElement(page, ref, isImage, ignoreSelector) {
|
|
887
|
+
const locator = page.locator(`aria-ref=${ref}`);
|
|
888
|
+
const isDetached = async () => await locator.count().catch(() => 0) === 0;
|
|
882
889
|
try {
|
|
883
|
-
|
|
890
|
+
if (await isDetached()) return "detached";
|
|
891
|
+
return await locator.evaluate((element, { properties, isImage, dynamicAttribute, ignoreSelector }) => {
|
|
884
892
|
const computed = getComputedStyle(element);
|
|
885
893
|
const typed = "computedStyleMap" in element ? element.computedStyleMap() : void 0;
|
|
886
894
|
const style = Object.fromEntries(properties.map((property) => [property, property.startsWith("margin-") && typed?.get(property)?.toString() === "auto" ? "auto" : computed.getPropertyValue(property)]));
|
|
@@ -925,7 +933,7 @@ async function readElement(page, ref, isImage, ignoreSelector) {
|
|
|
925
933
|
ignoreSelector
|
|
926
934
|
}, { timeout: ELEMENT_LOOKUP_TIMEOUT_MS });
|
|
927
935
|
} catch {
|
|
928
|
-
return;
|
|
936
|
+
return await isDetached() ? "detached" : void 0;
|
|
929
937
|
}
|
|
930
938
|
}
|
|
931
939
|
async function hashImage(page, image) {
|
|
@@ -944,12 +952,16 @@ function distinctStyle(style, inherited) {
|
|
|
944
952
|
});
|
|
945
953
|
return distinct.length === 0 ? void 0 : Object.fromEntries(distinct);
|
|
946
954
|
}
|
|
947
|
-
async function readTree(page, child, ignoreSelector, factsByRef) {
|
|
955
|
+
async function readTree(page, child, ignoreSelector, factsByRef, detached) {
|
|
948
956
|
if (typeof child === "string") return child;
|
|
949
957
|
const facts = child.ref === void 0 ? void 0 : await readElement(page, child.ref, child.role === "img", ignoreSelector);
|
|
958
|
+
if (facts === "detached") {
|
|
959
|
+
detached.push(child);
|
|
960
|
+
return;
|
|
961
|
+
}
|
|
950
962
|
if (facts?.ignored) return;
|
|
951
963
|
if (child.ref !== void 0 && facts !== void 0) factsByRef.set(child.ref, facts);
|
|
952
|
-
const children = child.children === void 0 ? void 0 : (await Promise.all(child.children.map((grandchild) => readTree(page, grandchild, ignoreSelector, factsByRef)))).filter((grandchild) => grandchild !== void 0);
|
|
964
|
+
const children = child.children === void 0 ? void 0 : (await Promise.all(child.children.map((grandchild) => readTree(page, grandchild, ignoreSelector, factsByRef, detached)))).filter((grandchild) => grandchild !== void 0);
|
|
953
965
|
if (children !== void 0) {
|
|
954
966
|
if (children.length === 0 && (child.ref === void 0 || child.role === "generic" && !child.name && (child.children?.length ?? 0) > 0)) return;
|
|
955
967
|
const only = loneChild(child, children);
|
|
@@ -1204,6 +1216,71 @@ function nodeKey(conditions) {
|
|
|
1204
1216
|
return JSON.stringify([sorted(conditions.visitorTags), sorted(conditions.pageTags)]);
|
|
1205
1217
|
}
|
|
1206
1218
|
//#endregion
|
|
1219
|
+
//#region src/flows.ts
|
|
1220
|
+
const MAX_FLOW_NAME_LENGTH = 64;
|
|
1221
|
+
var FlowRuns = class {
|
|
1222
|
+
runs = [];
|
|
1223
|
+
retainedNodeIds = /* @__PURE__ */ new Map();
|
|
1224
|
+
/** `currentNodeId` is the node on screen, the run's first step. */
|
|
1225
|
+
start(name, currentNodeId) {
|
|
1226
|
+
const flowName = flowNameOf(name);
|
|
1227
|
+
if (this.openRun(flowName)) throw new Error(`Ralph flow "${flowName}" is already open.`);
|
|
1228
|
+
const run = {
|
|
1229
|
+
name: flowName,
|
|
1230
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1231
|
+
nodeIds: currentNodeId === void 0 ? [] : [currentNodeId]
|
|
1232
|
+
};
|
|
1233
|
+
this.runs.push(run);
|
|
1234
|
+
return {
|
|
1235
|
+
name: flowName,
|
|
1236
|
+
end: () => {
|
|
1237
|
+
if (run.ended) throw new Error(`This run of Ralph flow "${flowName}" has already ended.`);
|
|
1238
|
+
endRun(run, "endFlow");
|
|
1239
|
+
}
|
|
1240
|
+
};
|
|
1241
|
+
}
|
|
1242
|
+
end(name) {
|
|
1243
|
+
const flowName = flowNameOf(name);
|
|
1244
|
+
const run = this.openRun(flowName);
|
|
1245
|
+
if (!run) throw new Error(`Ralph flow "${flowName}" is not open.`);
|
|
1246
|
+
endRun(run, "endFlow");
|
|
1247
|
+
}
|
|
1248
|
+
nodeRequested(nodeId) {
|
|
1249
|
+
for (const run of this.runs) if (!run.ended) run.nodeIds.push(nodeId);
|
|
1250
|
+
}
|
|
1251
|
+
/** A retake or unchanged condition update kept an existing node's ID. */
|
|
1252
|
+
nodeReused(requestId, retainedNodeId) {
|
|
1253
|
+
this.retainedNodeIds.set(requestId, retainedNodeId);
|
|
1254
|
+
}
|
|
1255
|
+
/** Ends the runs still open, as the test has, and returns every run. */
|
|
1256
|
+
finish() {
|
|
1257
|
+
return this.runs.map((run) => {
|
|
1258
|
+
if (!run.ended) endRun(run, "test-end");
|
|
1259
|
+
return {
|
|
1260
|
+
name: run.name,
|
|
1261
|
+
startedAt: run.startedAt,
|
|
1262
|
+
endedAt: run.ended.at,
|
|
1263
|
+
endedBy: run.ended.by,
|
|
1264
|
+
nodeIds: run.nodeIds.map((nodeId) => this.retainedNodeIds.get(nodeId) ?? nodeId)
|
|
1265
|
+
};
|
|
1266
|
+
});
|
|
1267
|
+
}
|
|
1268
|
+
openRun(name) {
|
|
1269
|
+
return this.runs.find((run) => run.name === name && !run.ended);
|
|
1270
|
+
}
|
|
1271
|
+
};
|
|
1272
|
+
function endRun(run, by) {
|
|
1273
|
+
run.ended = {
|
|
1274
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1275
|
+
by
|
|
1276
|
+
};
|
|
1277
|
+
}
|
|
1278
|
+
function flowNameOf(name) {
|
|
1279
|
+
const trimmed = name.trim();
|
|
1280
|
+
if (!trimmed || trimmed.length > MAX_FLOW_NAME_LENGTH) throw new Error(`A Ralph flow name must be 1 to ${MAX_FLOW_NAME_LENGTH} characters.`);
|
|
1281
|
+
return trimmed;
|
|
1282
|
+
}
|
|
1283
|
+
//#endregion
|
|
1207
1284
|
//#region src/image.ts
|
|
1208
1285
|
/**
|
|
1209
1286
|
* Matches the crawler's full-page screenshots: lossless measured larger than this
|
|
@@ -1430,6 +1507,7 @@ var Recorder = class {
|
|
|
1430
1507
|
*/
|
|
1431
1508
|
order = 0;
|
|
1432
1509
|
requestOrder = /* @__PURE__ */ new Map();
|
|
1510
|
+
flows = new FlowRuns();
|
|
1433
1511
|
actions = new ActionLog(() => ++this.order, () => !this.paused && !this.closed, (page) => this.pages.get(page)?.info.pageId, () => [...this.pages.values()].filter((state) => !state.page.isClosed()).map((state) => state.info.pageId));
|
|
1434
1512
|
constructor(options, config, testInfo, mode, playwright) {
|
|
1435
1513
|
this.options = options;
|
|
@@ -1474,9 +1552,26 @@ var Recorder = class {
|
|
|
1474
1552
|
const key = Symbol();
|
|
1475
1553
|
state.declared.set(key, declaredConditions(declaration));
|
|
1476
1554
|
await this.recordConditionChange(state);
|
|
1477
|
-
return
|
|
1478
|
-
|
|
1479
|
-
|
|
1555
|
+
return this.undoDeclaration(state, key);
|
|
1556
|
+
}
|
|
1557
|
+
async waitForNode(page, url, declaration = {}) {
|
|
1558
|
+
await this.observe(page.context());
|
|
1559
|
+
const state = this.pages.get(page);
|
|
1560
|
+
if (!state) throw new Error("Cannot wait on a closed browser page.");
|
|
1561
|
+
await page.waitForURL(url, { waitUntil: "commit" });
|
|
1562
|
+
this.urlChanged(state);
|
|
1563
|
+
const generation = state.generation;
|
|
1564
|
+
const key = Symbol();
|
|
1565
|
+
state.declared.set(key, declaredConditions(declaration));
|
|
1566
|
+
await state.tail;
|
|
1567
|
+
if (!this.paused && state.generation === generation && !this.retagVisitNode(state)) await this.recordConditionChange(state);
|
|
1568
|
+
return this.undoDeclaration(state, key);
|
|
1569
|
+
}
|
|
1570
|
+
startFlow(name) {
|
|
1571
|
+
return this.flows.start(name, this.lastRequestedNodeId);
|
|
1572
|
+
}
|
|
1573
|
+
endFlow(name) {
|
|
1574
|
+
this.flows.end(name);
|
|
1480
1575
|
}
|
|
1481
1576
|
/**
|
|
1482
1577
|
* Stops requesting nodes, automatic or explicit, until `resume`. Recordings
|
|
@@ -1521,7 +1616,7 @@ var Recorder = class {
|
|
|
1521
1616
|
linkPreviousNodes(nodes, (pageId) => this.pageInfo(pageId)?.openerPageId, (nodeId) => this.requestedAtMs.get(nodeId) ?? 0, (pageId) => this.pageState(pageId)?.pauseAnchor);
|
|
1522
1617
|
this.attachActions(nodes);
|
|
1523
1618
|
const failures = nodes.filter((item) => item.status === "uncaptured" && item.reason === "error");
|
|
1524
|
-
await this.writeRawGraph(this.buildRawGraph(nodes, failures.length));
|
|
1619
|
+
await this.writeRawGraph(this.buildRawGraph(nodes, this.flows.finish(), failures.length));
|
|
1525
1620
|
const mismatched = nodes.filter((item) => item.tagMismatches?.length);
|
|
1526
1621
|
if (this.options.strictNodeTags && mismatched.length && this.testInfo.status === "passed") throw new Error("Ralph: the app reported tags outside what the test declared, on " + mismatched.map((item) => item.url + " (" + item.tagMismatches.join(", ") + ")").join("; ") + ".");
|
|
1527
1622
|
if (failures.length && this.testInfo.status === "passed") throw new Error("Ralph could not record " + failures.length + " page state(s). See the ralph-raw-graph attachment.");
|
|
@@ -1565,11 +1660,42 @@ var Recorder = class {
|
|
|
1565
1660
|
state.generation++;
|
|
1566
1661
|
state.lastUrl = url;
|
|
1567
1662
|
state.declared.clear();
|
|
1663
|
+
state.visitNodeId = void 0;
|
|
1568
1664
|
if (!this.paused && isHttpUrl(url)) this.enqueueCreateNodeRequest(state, "url-change").catch(() => void 0);
|
|
1569
1665
|
}
|
|
1570
1666
|
async recordConditionChange(state) {
|
|
1571
1667
|
if (!this.paused && isHttpUrl(state.page.url())) await this.enqueueCreateNodeRequest(state, "condition-change");
|
|
1572
1668
|
}
|
|
1669
|
+
undoDeclaration(state, key) {
|
|
1670
|
+
return async () => {
|
|
1671
|
+
if (state.declared.delete(key)) await this.recordConditionChange(state);
|
|
1672
|
+
};
|
|
1673
|
+
}
|
|
1674
|
+
/**
|
|
1675
|
+
* Gives the URL's automatic capture the conditions declared since. They
|
|
1676
|
+
* describe the visit from its start, and declarations change no app state,
|
|
1677
|
+
* so the screenshot already taken stands for the declared node.
|
|
1678
|
+
*/
|
|
1679
|
+
retagVisitNode(state) {
|
|
1680
|
+
const nodeId = state.visitNodeId;
|
|
1681
|
+
if (!nodeId || state.lastRecorded?.nodeId !== nodeId) return false;
|
|
1682
|
+
const index = this.nodes.findIndex((item) => item.nodeId === nodeId);
|
|
1683
|
+
const visit = this.nodes[index];
|
|
1684
|
+
if (visit?.status !== "recorded") return false;
|
|
1685
|
+
const conditions = this.declaredConditions(state);
|
|
1686
|
+
const mismatches = mismatchesOf(conditions, visit.reportedTags);
|
|
1687
|
+
const { conditions: _conditions, tagMismatches: _tagMismatches, ...rest } = visit;
|
|
1688
|
+
this.nodes[index] = {
|
|
1689
|
+
...rest,
|
|
1690
|
+
...withConditions(conditions),
|
|
1691
|
+
...mismatches.length > 0 ? { tagMismatches: mismatches } : {}
|
|
1692
|
+
};
|
|
1693
|
+
state.lastRecorded = {
|
|
1694
|
+
...state.lastRecorded,
|
|
1695
|
+
node: nodeKey(conditions)
|
|
1696
|
+
};
|
|
1697
|
+
return true;
|
|
1698
|
+
}
|
|
1573
1699
|
declaredConditions(state) {
|
|
1574
1700
|
return mergeConditions([...state.declared.values()]);
|
|
1575
1701
|
}
|
|
@@ -1594,6 +1720,7 @@ var Recorder = class {
|
|
|
1594
1720
|
this.requestedAtMs.set(request.nodeId, Date.now());
|
|
1595
1721
|
this.requestOrder.set(request.nodeId, ++this.order);
|
|
1596
1722
|
this.lastRequestedNodeId = request.nodeId;
|
|
1723
|
+
this.flows.nodeRequested(request.nodeId);
|
|
1597
1724
|
const generation = state.generation;
|
|
1598
1725
|
const task = state.tail.then(() => this.createNode(state, generation, request));
|
|
1599
1726
|
state.tail = task.catch(() => void 0);
|
|
@@ -1626,7 +1753,10 @@ var Recorder = class {
|
|
|
1626
1753
|
const mismatches = mismatchesOf(conditions, reported);
|
|
1627
1754
|
const node = nodeKey(conditions);
|
|
1628
1755
|
const sameNode = state.lastRecorded?.url === request.url && state.lastRecorded.node === node;
|
|
1629
|
-
if (sameNode && request.trigger !== "explicit")
|
|
1756
|
+
if (sameNode && request.trigger !== "explicit") {
|
|
1757
|
+
this.flows.nodeReused(request.nodeId, state.lastRecorded.nodeId);
|
|
1758
|
+
return;
|
|
1759
|
+
}
|
|
1630
1760
|
if (reported === void 0 && compactConditions(conditions) !== void 0 && !warnedNoTracker) {
|
|
1631
1761
|
warnedNoTracker = true;
|
|
1632
1762
|
console.warn("Ralph: this page runs no tracker, so the node conditions the test declares cannot be checked against the tags the app reports. Production events must carry the same tags, or these nodes will match no visitors.");
|
|
@@ -1673,6 +1803,7 @@ var Recorder = class {
|
|
|
1673
1803
|
node,
|
|
1674
1804
|
nodeId: request.nodeId
|
|
1675
1805
|
};
|
|
1806
|
+
if (request.trigger === "url-change" && state.generation === generation) state.visitNodeId = request.nodeId;
|
|
1676
1807
|
} else {
|
|
1677
1808
|
const { nodeId, sequence, requestedAt, name, edgeLabel } = this.nodes[retaken];
|
|
1678
1809
|
this.nodes[retaken] = {
|
|
@@ -1683,6 +1814,7 @@ var Recorder = class {
|
|
|
1683
1814
|
sequence,
|
|
1684
1815
|
requestedAt
|
|
1685
1816
|
};
|
|
1817
|
+
this.flows.nodeReused(request.nodeId, nodeId);
|
|
1686
1818
|
}
|
|
1687
1819
|
} catch (error) {
|
|
1688
1820
|
this.nodes.push({
|
|
@@ -1762,7 +1894,7 @@ var Recorder = class {
|
|
|
1762
1894
|
pageInfo(pageId) {
|
|
1763
1895
|
return this.pageState(pageId)?.info;
|
|
1764
1896
|
}
|
|
1765
|
-
buildRawGraph(nodes, failureCount) {
|
|
1897
|
+
buildRawGraph(nodes, flowRuns, failureCount) {
|
|
1766
1898
|
const { testInfo } = this;
|
|
1767
1899
|
const testPassed = testInfo.status === "passed";
|
|
1768
1900
|
return {
|
|
@@ -1790,6 +1922,7 @@ var Recorder = class {
|
|
|
1790
1922
|
finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1791
1923
|
pages: [...this.pages.values()].map((state) => state.info),
|
|
1792
1924
|
nodes,
|
|
1925
|
+
...flowRuns.length ? { flowRuns } : {},
|
|
1793
1926
|
complete: testPassed && nodes.length > 0 && nodes.every((item) => item.status === "recorded" && item.geometryStable)
|
|
1794
1927
|
}
|
|
1795
1928
|
};
|
|
@@ -2082,4 +2215,4 @@ async function responseDetail(response) {
|
|
|
2082
2215
|
//#endregion
|
|
2083
2216
|
export { resolveMode as _, prepareArtifactUpload as a, resultLink as c, REPORTER_ENV as d, Recorder as f, loadConfigSync as g, loadConfig as h, createRawGraphBundle as i, uploadRawGraph as l, version as m, bundleImagePath as n, registerDeployment as o, visitAllLinks as p, bundleSnapshotPath as r, resolveUploadOptions as s, RAW_GRAPH_BUNDLE_INDEX as t, MANIFEST_ATTACHMENT as u };
|
|
2084
2217
|
|
|
2085
|
-
//# sourceMappingURL=upload-
|
|
2218
|
+
//# sourceMappingURL=upload-C_KLRH9y.mjs.map
|