@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
|
@@ -15,8 +15,8 @@ import { pack } from "tar-stream";
|
|
|
15
15
|
//#region src/config.ts
|
|
16
16
|
function resolveMode(options) {
|
|
17
17
|
const mode = options.mode ?? process.env["RALPH_MODE"] ?? (process.env["RALPH_UPLOAD_KEY"] ? "upload" : "off");
|
|
18
|
-
if (mode === "off" || mode === "local" || mode === "upload") return mode;
|
|
19
|
-
throw new Error("RALPH_MODE must be off, local, or
|
|
18
|
+
if (mode === "off" || mode === "local" || mode === "upload" || mode === "deploy") return mode;
|
|
19
|
+
throw new Error("RALPH_MODE must be off, local, upload, or deploy.");
|
|
20
20
|
}
|
|
21
21
|
async function loadConfig(options, root) {
|
|
22
22
|
const file = configFile(options, root);
|
|
@@ -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);
|
|
@@ -970,6 +982,7 @@ async function describe(page, child, inherited, insideDynamic, factsByRef) {
|
|
|
970
982
|
return {
|
|
971
983
|
...child,
|
|
972
984
|
...style && { style },
|
|
985
|
+
...facts === void 0 && { unread: true },
|
|
973
986
|
...imageHash && { imageHash },
|
|
974
987
|
...dynamic && { dynamic },
|
|
975
988
|
...children && { children }
|
|
@@ -1203,6 +1216,71 @@ function nodeKey(conditions) {
|
|
|
1203
1216
|
return JSON.stringify([sorted(conditions.visitorTags), sorted(conditions.pageTags)]);
|
|
1204
1217
|
}
|
|
1205
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
|
|
1206
1284
|
//#region src/image.ts
|
|
1207
1285
|
/**
|
|
1208
1286
|
* Matches the crawler's full-page screenshots: lossless measured larger than this
|
|
@@ -1429,6 +1507,7 @@ var Recorder = class {
|
|
|
1429
1507
|
*/
|
|
1430
1508
|
order = 0;
|
|
1431
1509
|
requestOrder = /* @__PURE__ */ new Map();
|
|
1510
|
+
flows = new FlowRuns();
|
|
1432
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));
|
|
1433
1512
|
constructor(options, config, testInfo, mode, playwright) {
|
|
1434
1513
|
this.options = options;
|
|
@@ -1477,6 +1556,12 @@ var Recorder = class {
|
|
|
1477
1556
|
if (state.declared.delete(key)) await this.recordConditionChange(state);
|
|
1478
1557
|
};
|
|
1479
1558
|
}
|
|
1559
|
+
startFlow(name) {
|
|
1560
|
+
return this.flows.start(name, this.lastRequestedNodeId);
|
|
1561
|
+
}
|
|
1562
|
+
endFlow(name) {
|
|
1563
|
+
this.flows.end(name);
|
|
1564
|
+
}
|
|
1480
1565
|
/**
|
|
1481
1566
|
* Stops requesting nodes, automatic or explicit, until `resume`. Recordings
|
|
1482
1567
|
* already requested still complete. The next node after `resume` links to
|
|
@@ -1520,7 +1605,7 @@ var Recorder = class {
|
|
|
1520
1605
|
linkPreviousNodes(nodes, (pageId) => this.pageInfo(pageId)?.openerPageId, (nodeId) => this.requestedAtMs.get(nodeId) ?? 0, (pageId) => this.pageState(pageId)?.pauseAnchor);
|
|
1521
1606
|
this.attachActions(nodes);
|
|
1522
1607
|
const failures = nodes.filter((item) => item.status === "uncaptured" && item.reason === "error");
|
|
1523
|
-
await this.writeRawGraph(this.buildRawGraph(nodes, failures.length));
|
|
1608
|
+
await this.writeRawGraph(this.buildRawGraph(nodes, this.flows.finish(), failures.length));
|
|
1524
1609
|
const mismatched = nodes.filter((item) => item.tagMismatches?.length);
|
|
1525
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("; ") + ".");
|
|
1526
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.");
|
|
@@ -1593,6 +1678,7 @@ var Recorder = class {
|
|
|
1593
1678
|
this.requestedAtMs.set(request.nodeId, Date.now());
|
|
1594
1679
|
this.requestOrder.set(request.nodeId, ++this.order);
|
|
1595
1680
|
this.lastRequestedNodeId = request.nodeId;
|
|
1681
|
+
this.flows.nodeRequested(request.nodeId);
|
|
1596
1682
|
const generation = state.generation;
|
|
1597
1683
|
const task = state.tail.then(() => this.createNode(state, generation, request));
|
|
1598
1684
|
state.tail = task.catch(() => void 0);
|
|
@@ -1625,7 +1711,10 @@ var Recorder = class {
|
|
|
1625
1711
|
const mismatches = mismatchesOf(conditions, reported);
|
|
1626
1712
|
const node = nodeKey(conditions);
|
|
1627
1713
|
const sameNode = state.lastRecorded?.url === request.url && state.lastRecorded.node === node;
|
|
1628
|
-
if (sameNode && request.trigger !== "explicit")
|
|
1714
|
+
if (sameNode && request.trigger !== "explicit") {
|
|
1715
|
+
this.flows.nodeReused(request.nodeId, state.lastRecorded.nodeId);
|
|
1716
|
+
return;
|
|
1717
|
+
}
|
|
1629
1718
|
if (reported === void 0 && compactConditions(conditions) !== void 0 && !warnedNoTracker) {
|
|
1630
1719
|
warnedNoTracker = true;
|
|
1631
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.");
|
|
@@ -1682,6 +1771,7 @@ var Recorder = class {
|
|
|
1682
1771
|
sequence,
|
|
1683
1772
|
requestedAt
|
|
1684
1773
|
};
|
|
1774
|
+
this.flows.nodeReused(request.nodeId, nodeId);
|
|
1685
1775
|
}
|
|
1686
1776
|
} catch (error) {
|
|
1687
1777
|
this.nodes.push({
|
|
@@ -1761,7 +1851,7 @@ var Recorder = class {
|
|
|
1761
1851
|
pageInfo(pageId) {
|
|
1762
1852
|
return this.pageState(pageId)?.info;
|
|
1763
1853
|
}
|
|
1764
|
-
buildRawGraph(nodes, failureCount) {
|
|
1854
|
+
buildRawGraph(nodes, flowRuns, failureCount) {
|
|
1765
1855
|
const { testInfo } = this;
|
|
1766
1856
|
const testPassed = testInfo.status === "passed";
|
|
1767
1857
|
return {
|
|
@@ -1789,6 +1879,7 @@ var Recorder = class {
|
|
|
1789
1879
|
finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1790
1880
|
pages: [...this.pages.values()].map((state) => state.info),
|
|
1791
1881
|
nodes,
|
|
1882
|
+
...flowRuns.length ? { flowRuns } : {},
|
|
1792
1883
|
complete: testPassed && nodes.length > 0 && nodes.every((item) => item.status === "recorded" && item.geometryStable)
|
|
1793
1884
|
}
|
|
1794
1885
|
};
|
|
@@ -2081,4 +2172,4 @@ async function responseDetail(response) {
|
|
|
2081
2172
|
//#endregion
|
|
2082
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 };
|
|
2083
2174
|
|
|
2084
|
-
//# sourceMappingURL=upload-
|
|
2175
|
+
//# sourceMappingURL=upload-sjRLrwTO.mjs.map
|