@ralphralphai/playwright 2.9.0 → 2.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +34 -0
- package/dist/index.cjs +6 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.mts +30 -1
- package/dist/index.mjs +6 -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-D_OHCpvL.cjs} +100 -10
- package/dist/upload-D_OHCpvL.cjs.map +1 -0
- package/dist/{upload-DKeVoDQ2.mjs → upload-sjRLrwTO.mjs} +100 -10
- package/dist/upload-sjRLrwTO.mjs.map +1 -0
- package/package.json +1 -1
- 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.10.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;
|
|
@@ -1478,6 +1556,12 @@ var Recorder = class {
|
|
|
1478
1556
|
if (state.declared.delete(key)) await this.recordConditionChange(state);
|
|
1479
1557
|
};
|
|
1480
1558
|
}
|
|
1559
|
+
startFlow(name) {
|
|
1560
|
+
return this.flows.start(name, this.lastRequestedNodeId);
|
|
1561
|
+
}
|
|
1562
|
+
endFlow(name) {
|
|
1563
|
+
this.flows.end(name);
|
|
1564
|
+
}
|
|
1481
1565
|
/**
|
|
1482
1566
|
* Stops requesting nodes, automatic or explicit, until `resume`. Recordings
|
|
1483
1567
|
* already requested still complete. The next node after `resume` links to
|
|
@@ -1521,7 +1605,7 @@ var Recorder = class {
|
|
|
1521
1605
|
linkPreviousNodes(nodes, (pageId) => this.pageInfo(pageId)?.openerPageId, (nodeId) => this.requestedAtMs.get(nodeId) ?? 0, (pageId) => this.pageState(pageId)?.pauseAnchor);
|
|
1522
1606
|
this.attachActions(nodes);
|
|
1523
1607
|
const failures = nodes.filter((item) => item.status === "uncaptured" && item.reason === "error");
|
|
1524
|
-
await this.writeRawGraph(this.buildRawGraph(nodes, failures.length));
|
|
1608
|
+
await this.writeRawGraph(this.buildRawGraph(nodes, this.flows.finish(), failures.length));
|
|
1525
1609
|
const mismatched = nodes.filter((item) => item.tagMismatches?.length);
|
|
1526
1610
|
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
1611
|
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.");
|
|
@@ -1594,6 +1678,7 @@ var Recorder = class {
|
|
|
1594
1678
|
this.requestedAtMs.set(request.nodeId, Date.now());
|
|
1595
1679
|
this.requestOrder.set(request.nodeId, ++this.order);
|
|
1596
1680
|
this.lastRequestedNodeId = request.nodeId;
|
|
1681
|
+
this.flows.nodeRequested(request.nodeId);
|
|
1597
1682
|
const generation = state.generation;
|
|
1598
1683
|
const task = state.tail.then(() => this.createNode(state, generation, request));
|
|
1599
1684
|
state.tail = task.catch(() => void 0);
|
|
@@ -1626,7 +1711,10 @@ var Recorder = class {
|
|
|
1626
1711
|
const mismatches = mismatchesOf(conditions, reported);
|
|
1627
1712
|
const node = nodeKey(conditions);
|
|
1628
1713
|
const sameNode = state.lastRecorded?.url === request.url && state.lastRecorded.node === node;
|
|
1629
|
-
if (sameNode && request.trigger !== "explicit")
|
|
1714
|
+
if (sameNode && request.trigger !== "explicit") {
|
|
1715
|
+
this.flows.nodeReused(request.nodeId, state.lastRecorded.nodeId);
|
|
1716
|
+
return;
|
|
1717
|
+
}
|
|
1630
1718
|
if (reported === void 0 && compactConditions(conditions) !== void 0 && !warnedNoTracker) {
|
|
1631
1719
|
warnedNoTracker = true;
|
|
1632
1720
|
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.");
|
|
@@ -1683,6 +1771,7 @@ var Recorder = class {
|
|
|
1683
1771
|
sequence,
|
|
1684
1772
|
requestedAt
|
|
1685
1773
|
};
|
|
1774
|
+
this.flows.nodeReused(request.nodeId, nodeId);
|
|
1686
1775
|
}
|
|
1687
1776
|
} catch (error) {
|
|
1688
1777
|
this.nodes.push({
|
|
@@ -1762,7 +1851,7 @@ var Recorder = class {
|
|
|
1762
1851
|
pageInfo(pageId) {
|
|
1763
1852
|
return this.pageState(pageId)?.info;
|
|
1764
1853
|
}
|
|
1765
|
-
buildRawGraph(nodes, failureCount) {
|
|
1854
|
+
buildRawGraph(nodes, flowRuns, failureCount) {
|
|
1766
1855
|
const { testInfo } = this;
|
|
1767
1856
|
const testPassed = testInfo.status === "passed";
|
|
1768
1857
|
return {
|
|
@@ -1790,6 +1879,7 @@ var Recorder = class {
|
|
|
1790
1879
|
finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1791
1880
|
pages: [...this.pages.values()].map((state) => state.info),
|
|
1792
1881
|
nodes,
|
|
1882
|
+
...flowRuns.length ? { flowRuns } : {},
|
|
1793
1883
|
complete: testPassed && nodes.length > 0 && nodes.every((item) => item.status === "recorded" && item.geometryStable)
|
|
1794
1884
|
}
|
|
1795
1885
|
};
|
|
@@ -2082,4 +2172,4 @@ async function responseDetail(response) {
|
|
|
2082
2172
|
//#endregion
|
|
2083
2173
|
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
2174
|
|
|
2085
|
-
//# sourceMappingURL=upload-
|
|
2175
|
+
//# sourceMappingURL=upload-sjRLrwTO.mjs.map
|