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