@ralphralphai/playwright 1.6.0 → 1.8.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.
@@ -45,7 +45,463 @@ function snapshot(value) {
45
45
  }
46
46
  //#endregion
47
47
  //#region package.json
48
- var version = "1.6.0";
48
+ var version = "1.8.0";
49
+ //#endregion
50
+ //#region src/actions.ts
51
+ const BINDING = "__ralphAction";
52
+ const MAX_TEXT = 200;
53
+ const text = (key) => {
54
+ return (params) => {
55
+ return typeof params[key] === "string" ? params[key] : void 0;
56
+ };
57
+ };
58
+ /** Keyed by Playwright protocol method, which is what `Locator`, `Page`, `Frame`, `ElementHandle`, `Keyboard` and `Mouse` calls all arrive as. */
59
+ const ACTIONS = {
60
+ click: {
61
+ kind: "click",
62
+ events: ["click"]
63
+ },
64
+ mouseClick: {
65
+ kind: "click",
66
+ events: ["click"]
67
+ },
68
+ dblclick: {
69
+ kind: "dblclick",
70
+ events: ["dblclick", "click"]
71
+ },
72
+ tap: {
73
+ kind: "tap",
74
+ events: ["touchstart", "click"]
75
+ },
76
+ hover: {
77
+ kind: "hover",
78
+ events: ["mouseover"]
79
+ },
80
+ check: {
81
+ kind: "check",
82
+ events: ["click"]
83
+ },
84
+ uncheck: {
85
+ kind: "uncheck",
86
+ events: ["click"]
87
+ },
88
+ fill: {
89
+ kind: "fill",
90
+ events: ["input"],
91
+ value: text("value")
92
+ },
93
+ type: {
94
+ kind: "type",
95
+ events: ["input"],
96
+ value: text("text")
97
+ },
98
+ keyboardType: {
99
+ kind: "type",
100
+ events: ["input"],
101
+ value: text("text")
102
+ },
103
+ keyboardInsertText: {
104
+ kind: "type",
105
+ events: ["input"],
106
+ value: text("text")
107
+ },
108
+ press: {
109
+ kind: "press",
110
+ events: ["keydown"],
111
+ value: text("key")
112
+ },
113
+ keyboardPress: {
114
+ kind: "press",
115
+ events: ["keydown"],
116
+ value: text("key")
117
+ },
118
+ selectOption: {
119
+ kind: "selectOption",
120
+ events: ["input"],
121
+ value: (params) => {
122
+ const options = params["options"];
123
+ if (!Array.isArray(options)) return;
124
+ return options.map((option) => {
125
+ return String(option["valueOrLabel"] ?? option["label"] ?? option["value"] ?? "#" + String(option["index"]));
126
+ }).join(", ");
127
+ }
128
+ },
129
+ setInputFiles: {
130
+ kind: "setInputFiles",
131
+ events: ["input"],
132
+ value: (params) => {
133
+ const payloads = params["payloads"];
134
+ const localPaths = params["localPaths"];
135
+ const names = Array.isArray(payloads) ? payloads.map((payload) => {
136
+ return String(payload.name);
137
+ }) : Array.isArray(localPaths) ? localPaths.map((file) => {
138
+ return path.basename(String(file));
139
+ }) : [];
140
+ return names.length > 0 ? names.join(", ") : void 0;
141
+ }
142
+ },
143
+ dragAndDrop: {
144
+ kind: "dragTo",
145
+ events: ["mousedown", "dragstart"],
146
+ value: text("target")
147
+ },
148
+ goto: {
149
+ kind: "goto",
150
+ events: [],
151
+ value: text("url")
152
+ },
153
+ goBack: {
154
+ kind: "goBack",
155
+ events: []
156
+ },
157
+ goForward: {
158
+ kind: "goForward",
159
+ events: []
160
+ },
161
+ reload: {
162
+ kind: "reload",
163
+ events: []
164
+ }
165
+ };
166
+ /**
167
+ * What Playwright may perform by setting the value from script, whose `input`
168
+ * event is untrusted. An untrusted event the app fires itself is not taken
169
+ * for any other action.
170
+ */
171
+ const SCRIPTED = /* @__PURE__ */ new Set([
172
+ "fill",
173
+ "selectOption",
174
+ "setInputFiles"
175
+ ]);
176
+ const NAVIGATIONS = /* @__PURE__ */ new Set([
177
+ "goto",
178
+ "goBack",
179
+ "goForward",
180
+ "reload"
181
+ ]);
182
+ /** Only the page can tell a password field from another, so these values wait
183
+ * for its report. */
184
+ const VALUE_NEEDS_TARGET = /* @__PURE__ */ new Set([
185
+ "fill",
186
+ "type",
187
+ "press"
188
+ ]);
189
+ /**
190
+ * The actions a test takes between two nodes, each with the element it
191
+ * landed on, for the edge between them. Two sources are needed because
192
+ * neither knows enough alone.
193
+ *
194
+ * 1. What the test asked for. The Playwright client turns every call, such
195
+ * as `locator.click()`, into a protocol message, such as `Frame.click
196
+ * { selector }`, that it sends to the Playwright server driving the
197
+ * browser. Before sending, it calls `onApiCallBegin(zone, { type, method,
198
+ * params })` on its instrumentation, and `onApiCallEnd(zone)` once the
199
+ * reply arrives. `@playwright/test` builds its report steps from the same
200
+ * hook. It gives the exact action and its selector, value, key or URL, but
201
+ * not the tab it ran in or the element the selector resolved to. It is
202
+ * not public API, so when a Playwright release changes it, `attach` finds
203
+ * no listener interface and only the page's reports of clicks, typing and
204
+ * Enter remain.
205
+ *
206
+ * 2. Where it landed. An init script in every page reports the trusted DOM
207
+ * events Playwright's input causes: the element's role, name,
208
+ * `data-track-id` and box in the screenshot's space, whether it is a
209
+ * password field, and when the event fired. The binding it reports
210
+ * through tells which tab it came from. A navigation causes no such
211
+ * event, so its tab is the one that navigates while it runs (see
212
+ * `navigated`).
213
+ *
214
+ * A report travels from the page through the Playwright server separately
215
+ * from the call's reply, so it can arrive after the next call began. It is
216
+ * therefore matched by the time the event fired against each call's start
217
+ * and end, never by arrival (see `landed`). A typed value survives only once
218
+ * the page has shown that its field is not sensitive.
219
+ *
220
+ * Calls and node requests draw from one counter (`next`), so at the end of
221
+ * the test `assign` can give each action to the edge into the first node
222
+ * requested after it.
223
+ */
224
+ var ActionLog = class {
225
+ next;
226
+ recording;
227
+ pageIdOf;
228
+ openPageIds;
229
+ actions = [];
230
+ byZone = /* @__PURE__ */ new WeakMap();
231
+ instrumentation;
232
+ listener = {
233
+ onApiCallBegin: (zone, call) => {
234
+ this.begin(zone, call);
235
+ },
236
+ onApiCallEnd: (zone) => {
237
+ const action = this.byZone.get(zone);
238
+ if (action) action.endedAtMs = Date.now();
239
+ }
240
+ };
241
+ constructor(next, recording, pageIdOf, openPageIds) {
242
+ this.next = next;
243
+ this.recording = recording;
244
+ this.pageIdOf = pageIdOf;
245
+ this.openPageIds = openPageIds;
246
+ }
247
+ attach(playwright) {
248
+ const instrumentation = playwright?._instrumentation;
249
+ if (typeof instrumentation?.addListener === "function" && typeof instrumentation.removeListener === "function") {
250
+ this.instrumentation = instrumentation;
251
+ this.instrumentation.addListener(this.listener);
252
+ }
253
+ }
254
+ detach() {
255
+ this.instrumentation?.removeListener(this.listener);
256
+ }
257
+ async observe(context) {
258
+ await Promise.all([
259
+ context.exposeBinding(BINDING, ({ page }, event) => {
260
+ if (isDomEvent(event)) this.landed(page, event);
261
+ }).catch(() => {}),
262
+ context.addInitScript(installActionReporter, BINDING).catch(() => {}),
263
+ ...context.pages().map((page) => {
264
+ return page.evaluate(installActionReporter, BINDING).catch(() => {});
265
+ })
266
+ ]);
267
+ }
268
+ /**
269
+ * A navigation reaches no element for the page to report, so the tab whose
270
+ * main frame navigates while the call runs is the tab it ran in.
271
+ */
272
+ navigated(page) {
273
+ const pageId = this.pageIdOf(page);
274
+ const running = this.actions.slice(-20).reverse().find((action) => {
275
+ return NAVIGATIONS.has(action.kind) && action.pageId === void 0 && action.endedAtMs === void 0;
276
+ });
277
+ if (running && pageId !== void 0) running.pageId = pageId;
278
+ }
279
+ /**
280
+ * Each window's actions. An action goes to the earliest window
281
+ * that holds it, so the click that opened a popup is on the edge into the
282
+ * popup and not also on the next edge of its own tab. An action whose tab
283
+ * is still unknown goes to none: it could have run in any tab open then.
284
+ */
285
+ assign(windows, limit) {
286
+ const taken = /* @__PURE__ */ new Set();
287
+ const assigned = /* @__PURE__ */ new Map();
288
+ const byEnd = [...windows].sort((a, b) => a.upTo - b.upTo);
289
+ for (const { nodeId, after, upTo, pageIds } of byEnd) {
290
+ const actions = this.actions.filter((action) => {
291
+ return !taken.has(action) && action.order > after && action.order <= upTo && action.pageId !== void 0 && pageIds.has(action.pageId);
292
+ });
293
+ for (const action of actions) taken.add(action);
294
+ if (actions.length > 0) assigned.set(nodeId, actions.slice(-limit).map(toRawAction));
295
+ }
296
+ return assigned;
297
+ }
298
+ begin(zone, call) {
299
+ const spec = ACTIONS[call.method];
300
+ if (!spec || ![
301
+ "Frame",
302
+ "Page",
303
+ "ElementHandle"
304
+ ].includes(call.type) || !this.recording()) return;
305
+ const params = call.params ?? {};
306
+ const selector = typeof params["selector"] === "string" ? params["selector"] : typeof params["source"] === "string" ? params["source"] : void 0;
307
+ const open = this.openPageIds();
308
+ const action = {
309
+ kind: spec.kind,
310
+ order: this.next(),
311
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
312
+ startedAtMs: Date.now(),
313
+ awaits: spec.events,
314
+ selector,
315
+ value: clip(spec.value?.(params)),
316
+ pageId: open.length === 1 ? open[0] : void 0
317
+ };
318
+ this.byZone.set(zone, action);
319
+ this.actions.push(action);
320
+ }
321
+ /**
322
+ * The event goes to the action that was running when it fired. Its report
323
+ * can reach here after the next action started, so arrival order says
324
+ * nothing. When one action ends in the millisecond the next starts, the
325
+ * earlier one takes it, since an event fires before its action returns.
326
+ */
327
+ landed(page, event) {
328
+ const pageId = this.pageIdOf(page);
329
+ const action = this.actions.slice(-20).find((candidate) => {
330
+ return candidate.target === void 0 && candidate.awaits.includes(event.type) && (event.trusted || SCRIPTED.has(candidate.kind)) && candidate.startedAtMs <= event.at && event.at <= (candidate.endedAtMs ?? Infinity);
331
+ });
332
+ if (action) {
333
+ action.target = event.target;
334
+ action.sensitive = event.sensitive;
335
+ if (event.valueLength !== void 0) action.valueLength = event.valueLength;
336
+ if (pageId !== void 0) action.pageId = pageId;
337
+ return;
338
+ }
339
+ if (!this.instrumentation && event.trusted && this.recording()) this.logFromPage(event, pageId);
340
+ }
341
+ logFromPage(event, pageId) {
342
+ const kind = event.type === "click" ? "click" : event.type === "input" ? "fill" : event.type === "keydown" && event.key === "Enter" ? "press" : void 0;
343
+ if (kind === void 0) return;
344
+ const previous = this.actions.at(-1);
345
+ if (kind === "fill" && previous?.kind === "fill" && JSON.stringify(previous.target) === JSON.stringify(event.target)) return;
346
+ this.actions.push({
347
+ kind,
348
+ order: this.next(),
349
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
350
+ startedAtMs: Date.now(),
351
+ awaits: [],
352
+ target: event.target,
353
+ sensitive: event.sensitive,
354
+ value: kind === "press" ? event.key : void 0,
355
+ pageId
356
+ });
357
+ }
358
+ };
359
+ function toRawAction(action) {
360
+ return {
361
+ kind: action.kind,
362
+ startedAt: action.startedAt,
363
+ pageId: action.pageId,
364
+ selector: action.selector,
365
+ value: revealedValue(action),
366
+ target: action.target
367
+ };
368
+ }
369
+ function revealedValue(action) {
370
+ if (!VALUE_NEEDS_TARGET.has(action.kind)) return action.value;
371
+ if (action.kind === "press" && (action.value?.length ?? 0) > 1) return action.value;
372
+ if (action.target === void 0 || action.sensitive !== false) return;
373
+ const length = action.value?.length ?? 0;
374
+ return (action.kind === "fill" ? action.valueLength === length : action.kind === "type" ? (action.valueLength ?? -1) >= length : true) ? action.value : void 0;
375
+ }
376
+ function clip(value) {
377
+ return value === void 0 || value.length <= MAX_TEXT ? value : value.slice(0, 199) + "…";
378
+ }
379
+ function isDomEvent(event) {
380
+ const candidate = event;
381
+ const box = candidate?.target?.box;
382
+ return typeof candidate?.type === "string" && typeof candidate.at === "number" && typeof candidate.trusted === "boolean" && typeof candidate.sensitive === "boolean" && typeof candidate.target?.tag === "string" && typeof box?.x === "number" && typeof box.y === "number" && typeof box.width === "number" && typeof box.height === "number";
383
+ }
384
+ /**
385
+ * Runs in every page. Serialized by Playwright, so it may use nothing from
386
+ * its module.
387
+ */
388
+ function installActionReporter(binding) {
389
+ if (window !== window.top) return;
390
+ const installed = binding + "Installed";
391
+ if (installed in window) return;
392
+ Object.defineProperty(window, installed, { value: true });
393
+ const maxText = 200;
394
+ const clean = (value) => {
395
+ const collapsed = value?.replace(/\s+/g, " ").trim();
396
+ return collapsed ? collapsed.slice(0, maxText) : void 0;
397
+ };
398
+ const inputRoles = {
399
+ checkbox: "checkbox",
400
+ radio: "radio",
401
+ button: "button",
402
+ submit: "button",
403
+ reset: "button",
404
+ image: "button",
405
+ range: "slider",
406
+ number: "spinbutton",
407
+ search: "searchbox"
408
+ };
409
+ const roleOf = (element) => {
410
+ const explicit = element.getAttribute("role")?.trim().split(/\s+/)[0];
411
+ if (explicit) return explicit;
412
+ const tag = element.tagName.toLowerCase();
413
+ if (tag === "a") return element.hasAttribute("href") ? "link" : void 0;
414
+ if (tag === "button" || tag === "summary") return "button";
415
+ if (tag === "select") {
416
+ const select = element;
417
+ return select.multiple || select.size > 1 ? "listbox" : "combobox";
418
+ }
419
+ if (tag === "textarea") return "textbox";
420
+ if (tag === "option") return "option";
421
+ if (tag === "img") return "img";
422
+ if (/^h[1-6]$/.test(tag)) return "heading";
423
+ if (tag === "input") {
424
+ const input = element;
425
+ return inputRoles[input.type] ?? (input.hasAttribute("list") ? "combobox" : "textbox");
426
+ }
427
+ };
428
+ const nameOf = (element) => {
429
+ const labelledBy = element.getAttribute("aria-labelledby");
430
+ if (labelledBy) {
431
+ const labelled = clean(labelledBy.split(/\s+/).map((id) => document.getElementById(id)?.textContent ?? "").join(" "));
432
+ if (labelled) return labelled;
433
+ }
434
+ const ariaLabel = clean(element.getAttribute("aria-label"));
435
+ if (ariaLabel) return ariaLabel;
436
+ if (element instanceof HTMLInputElement || element instanceof HTMLSelectElement || element instanceof HTMLTextAreaElement) {
437
+ const labels = clean([...element.labels ?? []].map((label) => label.textContent ?? "").join(" "));
438
+ if (labels) return labels;
439
+ if (element instanceof HTMLInputElement && [
440
+ "submit",
441
+ "button",
442
+ "reset"
443
+ ].includes(element.type)) return clean(element.value);
444
+ return clean(element.getAttribute("placeholder")) ?? clean(element.getAttribute("title"));
445
+ }
446
+ if (element instanceof HTMLImageElement) return clean(element.alt) ?? clean(element.title);
447
+ return clean(element.innerText ?? element.textContent) ?? clean(element.getAttribute("title"));
448
+ };
449
+ const interactive = "a[href],button,input,select,textarea,summary,label,option,[role],[data-track-id],[contenteditable=\"\"],[contenteditable=\"true\"],[tabindex]";
450
+ let lastHovered;
451
+ const report = (event) => {
452
+ if (!event.isTrusted && event.type !== "input") return;
453
+ const origin = event.composedPath()[0];
454
+ const element = origin instanceof Element ? origin : origin instanceof Node ? origin.parentElement : null;
455
+ if (!element) return;
456
+ const actual = element.closest(interactive) ?? element;
457
+ if (event.type === "mouseover") {
458
+ if (actual === lastHovered) return;
459
+ lastHovered = actual;
460
+ }
461
+ const key = event.key;
462
+ const rect = actual.getBoundingClientRect();
463
+ const trackId = actual.closest("[data-track-id]")?.getAttribute("data-track-id");
464
+ const role = roleOf(actual);
465
+ const name = nameOf(actual);
466
+ const sensitive = actual instanceof HTMLInputElement && (actual.type === "password" || /password|one-time-code|cc-/.test(actual.autocomplete));
467
+ const valueLength = event.type === "input" && (actual instanceof HTMLInputElement || actual instanceof HTMLTextAreaElement) ? actual.value.length : void 0;
468
+ const send = window[binding];
469
+ if (typeof send !== "function") return;
470
+ send({
471
+ type: event.type,
472
+ trusted: event.isTrusted,
473
+ at: Date.now(),
474
+ ...valueLength === void 0 ? {} : { valueLength },
475
+ ...event.type === "keydown" && key && key.length > 1 ? { key } : {},
476
+ sensitive,
477
+ target: {
478
+ tag: actual.tagName.toLowerCase(),
479
+ ...role ? { role } : {},
480
+ ...name ? { name } : {},
481
+ ...trackId ? { trackId } : {},
482
+ box: {
483
+ x: rect.x + window.scrollX,
484
+ y: rect.y + window.scrollY,
485
+ width: rect.width,
486
+ height: rect.height
487
+ }
488
+ }
489
+ }).catch(() => void 0);
490
+ };
491
+ for (const type of [
492
+ "click",
493
+ "dblclick",
494
+ "touchstart",
495
+ "mouseover",
496
+ "mousedown",
497
+ "dragstart",
498
+ "input",
499
+ "keydown"
500
+ ]) window.addEventListener(type, report, {
501
+ capture: true,
502
+ passive: true
503
+ });
504
+ }
49
505
  //#endregion
50
506
  //#region src/animations.ts
51
507
  /**
@@ -249,6 +705,8 @@ const SCREENSHOT_ATTACHMENT_PREFIX = "ralph-screenshot-";
249
705
  let warnedNoTracker = false;
250
706
  const DEFAULT_SETTLE_MS = 150;
251
707
  const DEFAULT_RECORD_TIMEOUT_MS = 5e3;
708
+ /** The actions kept on one edge, the latest ones, so a long setup does not bloat the graph. */
709
+ const MAX_ACTIONS_PER_EDGE = 100;
252
710
  /** Where the raw graph and screenshots are written, inside Playwright's per-test output directory. */
253
711
  const OUTPUT_DIR = "ralph";
254
712
  var Recorder = class {
@@ -291,7 +749,14 @@ var Recorder = class {
291
749
  paused = false;
292
750
  /** The most recently requested node, the anchor for tabs used while paused. */
293
751
  lastRequestedNodeId;
294
- constructor(options, config, testInfo, mode) {
752
+ /**
753
+ * Orders node requests and actions on one counter, which decides the edge
754
+ * an action belongs to. Timestamps could tie.
755
+ */
756
+ order = 0;
757
+ requestOrder = /* @__PURE__ */ new Map();
758
+ 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));
759
+ constructor(options, config, testInfo, mode, playwright) {
295
760
  this.options = options;
296
761
  this.config = config;
297
762
  this.testInfo = testInfo;
@@ -299,6 +764,7 @@ var Recorder = class {
299
764
  this.settleMs = options.settleMs ?? DEFAULT_SETTLE_MS;
300
765
  this.recordTimeoutMs = options.recordTimeoutMs ?? DEFAULT_RECORD_TIMEOUT_MS;
301
766
  if (!(Number.isFinite(this.settleMs) && this.settleMs >= 0 && Number.isFinite(this.recordTimeoutMs) && this.recordTimeoutMs > this.settleMs)) throw new Error("Ralph recordTimeoutMs must be finite and greater than settleMs, which must be non-negative.");
767
+ this.actions.attach(playwright);
302
768
  }
303
769
  /** Starts recording URL changes in every current and future tab of `context`. */
304
770
  observe(context) {
@@ -306,15 +772,16 @@ var Recorder = class {
306
772
  if (this.contexts.has(context)) return;
307
773
  const onPage = (page) => this.watchPage(page);
308
774
  this.contexts.set(context, onPage);
775
+ this.actions.observe(context);
309
776
  context.on("page", onPage);
310
777
  for (const page of context.pages()) onPage(page);
311
778
  }
312
- async recordNode(page) {
779
+ async recordNode(page, options) {
313
780
  this.observe(page.context());
314
781
  const state = this.pages.get(page);
315
782
  if (!state) throw new Error("Cannot record a closed browser page.");
316
783
  if (this.paused) return;
317
- await this.enqueueCreateNodeRequest(state, "explicit");
784
+ await this.enqueueCreateNodeRequest(state, "explicit", options);
318
785
  }
319
786
  async setNodeConditions(page, declaration) {
320
787
  this.observe(page.context());
@@ -357,10 +824,12 @@ var Recorder = class {
357
824
  */
358
825
  async finish() {
359
826
  this.closed = true;
827
+ this.actions.detach();
360
828
  await Promise.all([...this.contexts.keys()].map((context) => this.stopContext(context)));
361
829
  await Promise.all([...this.pages.values()].map((state) => state.opener));
362
830
  const nodes = this.nodes.sort((a, b) => a.pageId.localeCompare(b.pageId) || a.sequence - b.sequence);
363
831
  linkPreviousNodes(nodes, (pageId) => this.pageInfo(pageId)?.openerPageId, (nodeId) => this.requestedAtMs.get(nodeId) ?? 0, (pageId) => this.pageState(pageId)?.pauseAnchor);
832
+ this.attachActions(nodes);
364
833
  const failures = nodes.filter((item) => item.status === "uncaptured" && item.reason === "error");
365
834
  await this.writeRawGraph(this.buildRawGraph(nodes, failures.length));
366
835
  const mismatched = nodes.filter((item) => item.tagMismatches?.length);
@@ -382,7 +851,10 @@ var Recorder = class {
382
851
  tail: Promise.resolve(),
383
852
  opener: Promise.resolve(),
384
853
  onNavigation: (frame) => {
385
- if (frame === page.mainFrame()) this.urlChanged(state);
854
+ if (frame === page.mainFrame()) {
855
+ this.actions.navigated(page);
856
+ this.urlChanged(state);
857
+ }
386
858
  }
387
859
  };
388
860
  this.pages.set(page, state);
@@ -415,7 +887,7 @@ var Recorder = class {
415
887
  return page.evaluate(() => window.__ralphTags?.()).catch(() => void 0);
416
888
  }
417
889
  /** Queues a recording behind the tab's earlier ones. */
418
- enqueueCreateNodeRequest(state, trigger) {
890
+ enqueueCreateNodeRequest(state, trigger, { name, edgeLabel } = {}) {
419
891
  const url = state.page.url();
420
892
  if (!isHttpUrl(url)) throw new Error("Ralph recordings require an HTTP or HTTPS page.");
421
893
  const request = {
@@ -425,9 +897,12 @@ var Recorder = class {
425
897
  requestedAt: (/* @__PURE__ */ new Date()).toISOString(),
426
898
  actualUrl: url,
427
899
  url,
428
- trigger
900
+ trigger,
901
+ ...name === void 0 ? {} : { name },
902
+ ...edgeLabel === void 0 ? {} : { edgeLabel }
429
903
  };
430
904
  this.requestedAtMs.set(request.nodeId, Date.now());
905
+ this.requestOrder.set(request.nodeId, ++this.order);
431
906
  this.lastRequestedNodeId = request.nodeId;
432
907
  const generation = state.generation;
433
908
  const task = state.tail.then(() => this.createNode(state, generation, request));
@@ -493,8 +968,10 @@ var Recorder = class {
493
968
  nodeId: request.nodeId
494
969
  };
495
970
  } else {
496
- const { nodeId, sequence, requestedAt } = this.nodes[retaken];
971
+ const { nodeId, sequence, requestedAt, name, edgeLabel } = this.nodes[retaken];
497
972
  this.nodes[retaken] = {
973
+ ...name === void 0 ? {} : { name },
974
+ ...edgeLabel === void 0 ? {} : { edgeLabel },
498
975
  ...recorded,
499
976
  nodeId,
500
977
  sequence,
@@ -540,6 +1017,24 @@ var Recorder = class {
540
1017
  contentType: image.contentType
541
1018
  };
542
1019
  }
1020
+ /** Gives each node the actions taken since the node it was reached from was requested. */
1021
+ attachActions(nodes) {
1022
+ const byId = new Map(nodes.map((item) => [item.nodeId, item]));
1023
+ const windows = nodes.flatMap((item) => {
1024
+ const from = byId.get(item.previousNodeId ?? "");
1025
+ return from ? [{
1026
+ nodeId: item.nodeId,
1027
+ after: this.requestOrder.get(from.nodeId) ?? 0,
1028
+ upTo: this.requestOrder.get(item.nodeId) ?? 0,
1029
+ pageIds: /* @__PURE__ */ new Set([from.pageId, item.pageId])
1030
+ }] : [];
1031
+ });
1032
+ const actions = this.actions.assign(windows, MAX_ACTIONS_PER_EDGE);
1033
+ for (const item of nodes) {
1034
+ const taken = actions.get(item.nodeId);
1035
+ if (taken) item.actions = taken;
1036
+ }
1037
+ }
543
1038
  pageState(pageId) {
544
1039
  return [...this.pages.values()].find((state) => state.info.pageId === pageId);
545
1040
  }
@@ -836,4 +1331,4 @@ async function responseDetail(response) {
836
1331
  //#endregion
837
1332
  export { resolveUploadOptions as a, MANIFEST_ATTACHMENT as c, version as d, loadConfig as f, prepareArtifactUpload as i, REPORTER_ENV as l, resolveMode as m, bundleImagePath as n, resultLink as o, loadConfigSync as p, createRawGraphBundle as r, uploadRawGraph as s, RAW_GRAPH_BUNDLE_INDEX as t, Recorder as u };
838
1333
 
839
- //# sourceMappingURL=upload-BENmMj46.mjs.map
1334
+ //# sourceMappingURL=upload-D5hePvmA.mjs.map