@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.
@@ -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 upload.");
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.8.0";
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 normalized = (await Promise.all(visible.map((node) => readTree(page, node, ignoreSelector, facts)))).filter((node) => node !== void 0);
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
- return await page.locator(`aria-ref=${ref}`).evaluate((element, { properties, isImage, dynamicAttribute, ignoreSelector }) => {
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") return;
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-KEwXpYxG.cjs.map
2307
+ //# sourceMappingURL=upload-D_OHCpvL.cjs.map