@ralphralphai/playwright 2.8.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 +296 -222
- package/dist/index.cjs +7 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.mts +36 -4
- package/dist/index.mjs +7 -2
- package/dist/index.mjs.map +1 -1
- package/dist/reporter.cjs +9 -6
- package/dist/reporter.cjs.map +1 -1
- package/dist/reporter.d.mts +6 -10
- package/dist/reporter.mjs +9 -6
- package/dist/reporter.mjs.map +1 -1
- package/dist/{upload-KEwXpYxG.cjs → upload-D_OHCpvL.cjs} +103 -12
- package/dist/upload-D_OHCpvL.cjs.map +1 -0
- package/dist/{upload-CcdDM33Z.mjs → upload-sjRLrwTO.mjs} +103 -12
- package/dist/upload-sjRLrwTO.mjs.map +1 -0
- package/package.json +1 -1
- package/dist/upload-CcdDM33Z.mjs.map +0 -1
- package/dist/upload-KEwXpYxG.cjs.map +0 -1
|
@@ -40,8 +40,8 @@ let tar_stream = require("tar-stream");
|
|
|
40
40
|
//#region src/config.ts
|
|
41
41
|
function resolveMode(options) {
|
|
42
42
|
const mode = options.mode ?? process.env["RALPH_MODE"] ?? (process.env["RALPH_UPLOAD_KEY"] ? "upload" : "off");
|
|
43
|
-
if (mode === "off" || mode === "local" || mode === "upload") return mode;
|
|
44
|
-
throw new Error("RALPH_MODE must be off, local, or
|
|
43
|
+
if (mode === "off" || mode === "local" || mode === "upload" || mode === "deploy") return mode;
|
|
44
|
+
throw new Error("RALPH_MODE must be off, local, upload, or deploy.");
|
|
45
45
|
}
|
|
46
46
|
async function loadConfig(options, root) {
|
|
47
47
|
const file = configFile(options, root);
|
|
@@ -70,7 +70,7 @@ function snapshot(value) {
|
|
|
70
70
|
}
|
|
71
71
|
//#endregion
|
|
72
72
|
//#region package.json
|
|
73
|
-
var version = "2.
|
|
73
|
+
var version = "2.10.0";
|
|
74
74
|
//#endregion
|
|
75
75
|
//#region src/actions.ts
|
|
76
76
|
const BINDING = "__ralphAction";
|
|
@@ -689,7 +689,12 @@ async function captureAriaSnapshot(page, { viewport, scroll }, ignore = []) {
|
|
|
689
689
|
});
|
|
690
690
|
const ignoreSelector = ignore.length === 0 ? void 0 : ignore.join(", ");
|
|
691
691
|
const facts = /* @__PURE__ */ new Map();
|
|
692
|
-
const
|
|
692
|
+
const detached = [];
|
|
693
|
+
const normalized = (await Promise.all(visible.map((node) => readTree(page, node, ignoreSelector, facts, detached)))).filter((node) => node !== void 0);
|
|
694
|
+
if (detached.length > 0) {
|
|
695
|
+
const listed = detached.slice(0, 5).map(({ role, name }) => name ? `${role} "${name}"` : role).join(", ");
|
|
696
|
+
console.warn(`Ralph: ${detached.length} element(s) left ${page.url()} during capture and were dropped from its aria snapshot: ${listed}${detached.length > 5 ? ", ..." : ""}`);
|
|
697
|
+
}
|
|
693
698
|
const described = await Promise.all(normalized.map((node) => describe(page, node, {}, false, facts)));
|
|
694
699
|
const json = Buffer.from(JSON.stringify(fillMissingRefs(described)));
|
|
695
700
|
return json.byteLength > 4194304 ? void 0 : json;
|
|
@@ -904,8 +909,11 @@ function fillMissingRefs(nodes) {
|
|
|
904
909
|
return nodes.map(fill);
|
|
905
910
|
}
|
|
906
911
|
async function readElement(page, ref, isImage, ignoreSelector) {
|
|
912
|
+
const locator = page.locator(`aria-ref=${ref}`);
|
|
913
|
+
const isDetached = async () => await locator.count().catch(() => 0) === 0;
|
|
907
914
|
try {
|
|
908
|
-
|
|
915
|
+
if (await isDetached()) return "detached";
|
|
916
|
+
return await locator.evaluate((element, { properties, isImage, dynamicAttribute, ignoreSelector }) => {
|
|
909
917
|
const computed = getComputedStyle(element);
|
|
910
918
|
const typed = "computedStyleMap" in element ? element.computedStyleMap() : void 0;
|
|
911
919
|
const style = Object.fromEntries(properties.map((property) => [property, property.startsWith("margin-") && typed?.get(property)?.toString() === "auto" ? "auto" : computed.getPropertyValue(property)]));
|
|
@@ -950,7 +958,7 @@ async function readElement(page, ref, isImage, ignoreSelector) {
|
|
|
950
958
|
ignoreSelector
|
|
951
959
|
}, { timeout: ELEMENT_LOOKUP_TIMEOUT_MS });
|
|
952
960
|
} catch {
|
|
953
|
-
return;
|
|
961
|
+
return await isDetached() ? "detached" : void 0;
|
|
954
962
|
}
|
|
955
963
|
}
|
|
956
964
|
async function hashImage(page, image) {
|
|
@@ -969,12 +977,16 @@ function distinctStyle(style, inherited) {
|
|
|
969
977
|
});
|
|
970
978
|
return distinct.length === 0 ? void 0 : Object.fromEntries(distinct);
|
|
971
979
|
}
|
|
972
|
-
async function readTree(page, child, ignoreSelector, factsByRef) {
|
|
980
|
+
async function readTree(page, child, ignoreSelector, factsByRef, detached) {
|
|
973
981
|
if (typeof child === "string") return child;
|
|
974
982
|
const facts = child.ref === void 0 ? void 0 : await readElement(page, child.ref, child.role === "img", ignoreSelector);
|
|
983
|
+
if (facts === "detached") {
|
|
984
|
+
detached.push(child);
|
|
985
|
+
return;
|
|
986
|
+
}
|
|
975
987
|
if (facts?.ignored) return;
|
|
976
988
|
if (child.ref !== void 0 && facts !== void 0) factsByRef.set(child.ref, facts);
|
|
977
|
-
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);
|
|
989
|
+
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);
|
|
978
990
|
if (children !== void 0) {
|
|
979
991
|
if (children.length === 0 && (child.ref === void 0 || child.role === "generic" && !child.name && (child.children?.length ?? 0) > 0)) return;
|
|
980
992
|
const only = loneChild(child, children);
|
|
@@ -995,6 +1007,7 @@ async function describe(page, child, inherited, insideDynamic, factsByRef) {
|
|
|
995
1007
|
return {
|
|
996
1008
|
...child,
|
|
997
1009
|
...style && { style },
|
|
1010
|
+
...facts === void 0 && { unread: true },
|
|
998
1011
|
...imageHash && { imageHash },
|
|
999
1012
|
...dynamic && { dynamic },
|
|
1000
1013
|
...children && { children }
|
|
@@ -1228,6 +1241,71 @@ function nodeKey(conditions) {
|
|
|
1228
1241
|
return JSON.stringify([sorted(conditions.visitorTags), sorted(conditions.pageTags)]);
|
|
1229
1242
|
}
|
|
1230
1243
|
//#endregion
|
|
1244
|
+
//#region src/flows.ts
|
|
1245
|
+
const MAX_FLOW_NAME_LENGTH = 64;
|
|
1246
|
+
var FlowRuns = class {
|
|
1247
|
+
runs = [];
|
|
1248
|
+
retainedNodeIds = /* @__PURE__ */ new Map();
|
|
1249
|
+
/** `currentNodeId` is the node on screen, the run's first step. */
|
|
1250
|
+
start(name, currentNodeId) {
|
|
1251
|
+
const flowName = flowNameOf(name);
|
|
1252
|
+
if (this.openRun(flowName)) throw new Error(`Ralph flow "${flowName}" is already open.`);
|
|
1253
|
+
const run = {
|
|
1254
|
+
name: flowName,
|
|
1255
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1256
|
+
nodeIds: currentNodeId === void 0 ? [] : [currentNodeId]
|
|
1257
|
+
};
|
|
1258
|
+
this.runs.push(run);
|
|
1259
|
+
return {
|
|
1260
|
+
name: flowName,
|
|
1261
|
+
end: () => {
|
|
1262
|
+
if (run.ended) throw new Error(`This run of Ralph flow "${flowName}" has already ended.`);
|
|
1263
|
+
endRun(run, "endFlow");
|
|
1264
|
+
}
|
|
1265
|
+
};
|
|
1266
|
+
}
|
|
1267
|
+
end(name) {
|
|
1268
|
+
const flowName = flowNameOf(name);
|
|
1269
|
+
const run = this.openRun(flowName);
|
|
1270
|
+
if (!run) throw new Error(`Ralph flow "${flowName}" is not open.`);
|
|
1271
|
+
endRun(run, "endFlow");
|
|
1272
|
+
}
|
|
1273
|
+
nodeRequested(nodeId) {
|
|
1274
|
+
for (const run of this.runs) if (!run.ended) run.nodeIds.push(nodeId);
|
|
1275
|
+
}
|
|
1276
|
+
/** A retake or unchanged condition update kept an existing node's ID. */
|
|
1277
|
+
nodeReused(requestId, retainedNodeId) {
|
|
1278
|
+
this.retainedNodeIds.set(requestId, retainedNodeId);
|
|
1279
|
+
}
|
|
1280
|
+
/** Ends the runs still open, as the test has, and returns every run. */
|
|
1281
|
+
finish() {
|
|
1282
|
+
return this.runs.map((run) => {
|
|
1283
|
+
if (!run.ended) endRun(run, "test-end");
|
|
1284
|
+
return {
|
|
1285
|
+
name: run.name,
|
|
1286
|
+
startedAt: run.startedAt,
|
|
1287
|
+
endedAt: run.ended.at,
|
|
1288
|
+
endedBy: run.ended.by,
|
|
1289
|
+
nodeIds: run.nodeIds.map((nodeId) => this.retainedNodeIds.get(nodeId) ?? nodeId)
|
|
1290
|
+
};
|
|
1291
|
+
});
|
|
1292
|
+
}
|
|
1293
|
+
openRun(name) {
|
|
1294
|
+
return this.runs.find((run) => run.name === name && !run.ended);
|
|
1295
|
+
}
|
|
1296
|
+
};
|
|
1297
|
+
function endRun(run, by) {
|
|
1298
|
+
run.ended = {
|
|
1299
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1300
|
+
by
|
|
1301
|
+
};
|
|
1302
|
+
}
|
|
1303
|
+
function flowNameOf(name) {
|
|
1304
|
+
const trimmed = name.trim();
|
|
1305
|
+
if (!trimmed || trimmed.length > MAX_FLOW_NAME_LENGTH) throw new Error(`A Ralph flow name must be 1 to ${MAX_FLOW_NAME_LENGTH} characters.`);
|
|
1306
|
+
return trimmed;
|
|
1307
|
+
}
|
|
1308
|
+
//#endregion
|
|
1231
1309
|
//#region src/image.ts
|
|
1232
1310
|
/**
|
|
1233
1311
|
* Matches the crawler's full-page screenshots: lossless measured larger than this
|
|
@@ -1454,6 +1532,7 @@ var Recorder = class {
|
|
|
1454
1532
|
*/
|
|
1455
1533
|
order = 0;
|
|
1456
1534
|
requestOrder = /* @__PURE__ */ new Map();
|
|
1535
|
+
flows = new FlowRuns();
|
|
1457
1536
|
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));
|
|
1458
1537
|
constructor(options, config, testInfo, mode, playwright) {
|
|
1459
1538
|
this.options = options;
|
|
@@ -1502,6 +1581,12 @@ var Recorder = class {
|
|
|
1502
1581
|
if (state.declared.delete(key)) await this.recordConditionChange(state);
|
|
1503
1582
|
};
|
|
1504
1583
|
}
|
|
1584
|
+
startFlow(name) {
|
|
1585
|
+
return this.flows.start(name, this.lastRequestedNodeId);
|
|
1586
|
+
}
|
|
1587
|
+
endFlow(name) {
|
|
1588
|
+
this.flows.end(name);
|
|
1589
|
+
}
|
|
1505
1590
|
/**
|
|
1506
1591
|
* Stops requesting nodes, automatic or explicit, until `resume`. Recordings
|
|
1507
1592
|
* already requested still complete. The next node after `resume` links to
|
|
@@ -1545,7 +1630,7 @@ var Recorder = class {
|
|
|
1545
1630
|
linkPreviousNodes(nodes, (pageId) => this.pageInfo(pageId)?.openerPageId, (nodeId) => this.requestedAtMs.get(nodeId) ?? 0, (pageId) => this.pageState(pageId)?.pauseAnchor);
|
|
1546
1631
|
this.attachActions(nodes);
|
|
1547
1632
|
const failures = nodes.filter((item) => item.status === "uncaptured" && item.reason === "error");
|
|
1548
|
-
await this.writeRawGraph(this.buildRawGraph(nodes, failures.length));
|
|
1633
|
+
await this.writeRawGraph(this.buildRawGraph(nodes, this.flows.finish(), failures.length));
|
|
1549
1634
|
const mismatched = nodes.filter((item) => item.tagMismatches?.length);
|
|
1550
1635
|
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("; ") + ".");
|
|
1551
1636
|
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.");
|
|
@@ -1618,6 +1703,7 @@ var Recorder = class {
|
|
|
1618
1703
|
this.requestedAtMs.set(request.nodeId, Date.now());
|
|
1619
1704
|
this.requestOrder.set(request.nodeId, ++this.order);
|
|
1620
1705
|
this.lastRequestedNodeId = request.nodeId;
|
|
1706
|
+
this.flows.nodeRequested(request.nodeId);
|
|
1621
1707
|
const generation = state.generation;
|
|
1622
1708
|
const task = state.tail.then(() => this.createNode(state, generation, request));
|
|
1623
1709
|
state.tail = task.catch(() => void 0);
|
|
@@ -1650,7 +1736,10 @@ var Recorder = class {
|
|
|
1650
1736
|
const mismatches = mismatchesOf(conditions, reported);
|
|
1651
1737
|
const node = nodeKey(conditions);
|
|
1652
1738
|
const sameNode = state.lastRecorded?.url === request.url && state.lastRecorded.node === node;
|
|
1653
|
-
if (sameNode && request.trigger !== "explicit")
|
|
1739
|
+
if (sameNode && request.trigger !== "explicit") {
|
|
1740
|
+
this.flows.nodeReused(request.nodeId, state.lastRecorded.nodeId);
|
|
1741
|
+
return;
|
|
1742
|
+
}
|
|
1654
1743
|
if (reported === void 0 && compactConditions(conditions) !== void 0 && !warnedNoTracker) {
|
|
1655
1744
|
warnedNoTracker = true;
|
|
1656
1745
|
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.");
|
|
@@ -1707,6 +1796,7 @@ var Recorder = class {
|
|
|
1707
1796
|
sequence,
|
|
1708
1797
|
requestedAt
|
|
1709
1798
|
};
|
|
1799
|
+
this.flows.nodeReused(request.nodeId, nodeId);
|
|
1710
1800
|
}
|
|
1711
1801
|
} catch (error) {
|
|
1712
1802
|
this.nodes.push({
|
|
@@ -1786,7 +1876,7 @@ var Recorder = class {
|
|
|
1786
1876
|
pageInfo(pageId) {
|
|
1787
1877
|
return this.pageState(pageId)?.info;
|
|
1788
1878
|
}
|
|
1789
|
-
buildRawGraph(nodes, failureCount) {
|
|
1879
|
+
buildRawGraph(nodes, flowRuns, failureCount) {
|
|
1790
1880
|
const { testInfo } = this;
|
|
1791
1881
|
const testPassed = testInfo.status === "passed";
|
|
1792
1882
|
return {
|
|
@@ -1814,6 +1904,7 @@ var Recorder = class {
|
|
|
1814
1904
|
finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1815
1905
|
pages: [...this.pages.values()].map((state) => state.info),
|
|
1816
1906
|
nodes,
|
|
1907
|
+
...flowRuns.length ? { flowRuns } : {},
|
|
1817
1908
|
complete: testPassed && nodes.length > 0 && nodes.every((item) => item.status === "recorded" && item.geometryStable)
|
|
1818
1909
|
}
|
|
1819
1910
|
};
|
|
@@ -2213,4 +2304,4 @@ Object.defineProperty(exports, "visitAllLinks", {
|
|
|
2213
2304
|
}
|
|
2214
2305
|
});
|
|
2215
2306
|
|
|
2216
|
-
//# sourceMappingURL=upload-
|
|
2307
|
+
//# sourceMappingURL=upload-D_OHCpvL.cjs.map
|