@ralphralphai/playwright 1.7.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.
@@ -70,7 +70,463 @@ function snapshot(value) {
70
70
  }
71
71
  //#endregion
72
72
  //#region package.json
73
- var version = "1.7.0";
73
+ var version = "1.8.0";
74
+ //#endregion
75
+ //#region src/actions.ts
76
+ const BINDING = "__ralphAction";
77
+ const MAX_TEXT = 200;
78
+ const text = (key) => {
79
+ return (params) => {
80
+ return typeof params[key] === "string" ? params[key] : void 0;
81
+ };
82
+ };
83
+ /** Keyed by Playwright protocol method, which is what `Locator`, `Page`, `Frame`, `ElementHandle`, `Keyboard` and `Mouse` calls all arrive as. */
84
+ const ACTIONS = {
85
+ click: {
86
+ kind: "click",
87
+ events: ["click"]
88
+ },
89
+ mouseClick: {
90
+ kind: "click",
91
+ events: ["click"]
92
+ },
93
+ dblclick: {
94
+ kind: "dblclick",
95
+ events: ["dblclick", "click"]
96
+ },
97
+ tap: {
98
+ kind: "tap",
99
+ events: ["touchstart", "click"]
100
+ },
101
+ hover: {
102
+ kind: "hover",
103
+ events: ["mouseover"]
104
+ },
105
+ check: {
106
+ kind: "check",
107
+ events: ["click"]
108
+ },
109
+ uncheck: {
110
+ kind: "uncheck",
111
+ events: ["click"]
112
+ },
113
+ fill: {
114
+ kind: "fill",
115
+ events: ["input"],
116
+ value: text("value")
117
+ },
118
+ type: {
119
+ kind: "type",
120
+ events: ["input"],
121
+ value: text("text")
122
+ },
123
+ keyboardType: {
124
+ kind: "type",
125
+ events: ["input"],
126
+ value: text("text")
127
+ },
128
+ keyboardInsertText: {
129
+ kind: "type",
130
+ events: ["input"],
131
+ value: text("text")
132
+ },
133
+ press: {
134
+ kind: "press",
135
+ events: ["keydown"],
136
+ value: text("key")
137
+ },
138
+ keyboardPress: {
139
+ kind: "press",
140
+ events: ["keydown"],
141
+ value: text("key")
142
+ },
143
+ selectOption: {
144
+ kind: "selectOption",
145
+ events: ["input"],
146
+ value: (params) => {
147
+ const options = params["options"];
148
+ if (!Array.isArray(options)) return;
149
+ return options.map((option) => {
150
+ return String(option["valueOrLabel"] ?? option["label"] ?? option["value"] ?? "#" + String(option["index"]));
151
+ }).join(", ");
152
+ }
153
+ },
154
+ setInputFiles: {
155
+ kind: "setInputFiles",
156
+ events: ["input"],
157
+ value: (params) => {
158
+ const payloads = params["payloads"];
159
+ const localPaths = params["localPaths"];
160
+ const names = Array.isArray(payloads) ? payloads.map((payload) => {
161
+ return String(payload.name);
162
+ }) : Array.isArray(localPaths) ? localPaths.map((file) => {
163
+ return node_path.default.basename(String(file));
164
+ }) : [];
165
+ return names.length > 0 ? names.join(", ") : void 0;
166
+ }
167
+ },
168
+ dragAndDrop: {
169
+ kind: "dragTo",
170
+ events: ["mousedown", "dragstart"],
171
+ value: text("target")
172
+ },
173
+ goto: {
174
+ kind: "goto",
175
+ events: [],
176
+ value: text("url")
177
+ },
178
+ goBack: {
179
+ kind: "goBack",
180
+ events: []
181
+ },
182
+ goForward: {
183
+ kind: "goForward",
184
+ events: []
185
+ },
186
+ reload: {
187
+ kind: "reload",
188
+ events: []
189
+ }
190
+ };
191
+ /**
192
+ * What Playwright may perform by setting the value from script, whose `input`
193
+ * event is untrusted. An untrusted event the app fires itself is not taken
194
+ * for any other action.
195
+ */
196
+ const SCRIPTED = /* @__PURE__ */ new Set([
197
+ "fill",
198
+ "selectOption",
199
+ "setInputFiles"
200
+ ]);
201
+ const NAVIGATIONS = /* @__PURE__ */ new Set([
202
+ "goto",
203
+ "goBack",
204
+ "goForward",
205
+ "reload"
206
+ ]);
207
+ /** Only the page can tell a password field from another, so these values wait
208
+ * for its report. */
209
+ const VALUE_NEEDS_TARGET = /* @__PURE__ */ new Set([
210
+ "fill",
211
+ "type",
212
+ "press"
213
+ ]);
214
+ /**
215
+ * The actions a test takes between two nodes, each with the element it
216
+ * landed on, for the edge between them. Two sources are needed because
217
+ * neither knows enough alone.
218
+ *
219
+ * 1. What the test asked for. The Playwright client turns every call, such
220
+ * as `locator.click()`, into a protocol message, such as `Frame.click
221
+ * { selector }`, that it sends to the Playwright server driving the
222
+ * browser. Before sending, it calls `onApiCallBegin(zone, { type, method,
223
+ * params })` on its instrumentation, and `onApiCallEnd(zone)` once the
224
+ * reply arrives. `@playwright/test` builds its report steps from the same
225
+ * hook. It gives the exact action and its selector, value, key or URL, but
226
+ * not the tab it ran in or the element the selector resolved to. It is
227
+ * not public API, so when a Playwright release changes it, `attach` finds
228
+ * no listener interface and only the page's reports of clicks, typing and
229
+ * Enter remain.
230
+ *
231
+ * 2. Where it landed. An init script in every page reports the trusted DOM
232
+ * events Playwright's input causes: the element's role, name,
233
+ * `data-track-id` and box in the screenshot's space, whether it is a
234
+ * password field, and when the event fired. The binding it reports
235
+ * through tells which tab it came from. A navigation causes no such
236
+ * event, so its tab is the one that navigates while it runs (see
237
+ * `navigated`).
238
+ *
239
+ * A report travels from the page through the Playwright server separately
240
+ * from the call's reply, so it can arrive after the next call began. It is
241
+ * therefore matched by the time the event fired against each call's start
242
+ * and end, never by arrival (see `landed`). A typed value survives only once
243
+ * the page has shown that its field is not sensitive.
244
+ *
245
+ * Calls and node requests draw from one counter (`next`), so at the end of
246
+ * the test `assign` can give each action to the edge into the first node
247
+ * requested after it.
248
+ */
249
+ var ActionLog = class {
250
+ next;
251
+ recording;
252
+ pageIdOf;
253
+ openPageIds;
254
+ actions = [];
255
+ byZone = /* @__PURE__ */ new WeakMap();
256
+ instrumentation;
257
+ listener = {
258
+ onApiCallBegin: (zone, call) => {
259
+ this.begin(zone, call);
260
+ },
261
+ onApiCallEnd: (zone) => {
262
+ const action = this.byZone.get(zone);
263
+ if (action) action.endedAtMs = Date.now();
264
+ }
265
+ };
266
+ constructor(next, recording, pageIdOf, openPageIds) {
267
+ this.next = next;
268
+ this.recording = recording;
269
+ this.pageIdOf = pageIdOf;
270
+ this.openPageIds = openPageIds;
271
+ }
272
+ attach(playwright) {
273
+ const instrumentation = playwright?._instrumentation;
274
+ if (typeof instrumentation?.addListener === "function" && typeof instrumentation.removeListener === "function") {
275
+ this.instrumentation = instrumentation;
276
+ this.instrumentation.addListener(this.listener);
277
+ }
278
+ }
279
+ detach() {
280
+ this.instrumentation?.removeListener(this.listener);
281
+ }
282
+ async observe(context) {
283
+ await Promise.all([
284
+ context.exposeBinding(BINDING, ({ page }, event) => {
285
+ if (isDomEvent(event)) this.landed(page, event);
286
+ }).catch(() => {}),
287
+ context.addInitScript(installActionReporter, BINDING).catch(() => {}),
288
+ ...context.pages().map((page) => {
289
+ return page.evaluate(installActionReporter, BINDING).catch(() => {});
290
+ })
291
+ ]);
292
+ }
293
+ /**
294
+ * A navigation reaches no element for the page to report, so the tab whose
295
+ * main frame navigates while the call runs is the tab it ran in.
296
+ */
297
+ navigated(page) {
298
+ const pageId = this.pageIdOf(page);
299
+ const running = this.actions.slice(-20).reverse().find((action) => {
300
+ return NAVIGATIONS.has(action.kind) && action.pageId === void 0 && action.endedAtMs === void 0;
301
+ });
302
+ if (running && pageId !== void 0) running.pageId = pageId;
303
+ }
304
+ /**
305
+ * Each window's actions. An action goes to the earliest window
306
+ * that holds it, so the click that opened a popup is on the edge into the
307
+ * popup and not also on the next edge of its own tab. An action whose tab
308
+ * is still unknown goes to none: it could have run in any tab open then.
309
+ */
310
+ assign(windows, limit) {
311
+ const taken = /* @__PURE__ */ new Set();
312
+ const assigned = /* @__PURE__ */ new Map();
313
+ const byEnd = [...windows].sort((a, b) => a.upTo - b.upTo);
314
+ for (const { nodeId, after, upTo, pageIds } of byEnd) {
315
+ const actions = this.actions.filter((action) => {
316
+ return !taken.has(action) && action.order > after && action.order <= upTo && action.pageId !== void 0 && pageIds.has(action.pageId);
317
+ });
318
+ for (const action of actions) taken.add(action);
319
+ if (actions.length > 0) assigned.set(nodeId, actions.slice(-limit).map(toRawAction));
320
+ }
321
+ return assigned;
322
+ }
323
+ begin(zone, call) {
324
+ const spec = ACTIONS[call.method];
325
+ if (!spec || ![
326
+ "Frame",
327
+ "Page",
328
+ "ElementHandle"
329
+ ].includes(call.type) || !this.recording()) return;
330
+ const params = call.params ?? {};
331
+ const selector = typeof params["selector"] === "string" ? params["selector"] : typeof params["source"] === "string" ? params["source"] : void 0;
332
+ const open = this.openPageIds();
333
+ const action = {
334
+ kind: spec.kind,
335
+ order: this.next(),
336
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
337
+ startedAtMs: Date.now(),
338
+ awaits: spec.events,
339
+ selector,
340
+ value: clip(spec.value?.(params)),
341
+ pageId: open.length === 1 ? open[0] : void 0
342
+ };
343
+ this.byZone.set(zone, action);
344
+ this.actions.push(action);
345
+ }
346
+ /**
347
+ * The event goes to the action that was running when it fired. Its report
348
+ * can reach here after the next action started, so arrival order says
349
+ * nothing. When one action ends in the millisecond the next starts, the
350
+ * earlier one takes it, since an event fires before its action returns.
351
+ */
352
+ landed(page, event) {
353
+ const pageId = this.pageIdOf(page);
354
+ const action = this.actions.slice(-20).find((candidate) => {
355
+ 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);
356
+ });
357
+ if (action) {
358
+ action.target = event.target;
359
+ action.sensitive = event.sensitive;
360
+ if (event.valueLength !== void 0) action.valueLength = event.valueLength;
361
+ if (pageId !== void 0) action.pageId = pageId;
362
+ return;
363
+ }
364
+ if (!this.instrumentation && event.trusted && this.recording()) this.logFromPage(event, pageId);
365
+ }
366
+ logFromPage(event, pageId) {
367
+ const kind = event.type === "click" ? "click" : event.type === "input" ? "fill" : event.type === "keydown" && event.key === "Enter" ? "press" : void 0;
368
+ if (kind === void 0) return;
369
+ const previous = this.actions.at(-1);
370
+ if (kind === "fill" && previous?.kind === "fill" && JSON.stringify(previous.target) === JSON.stringify(event.target)) return;
371
+ this.actions.push({
372
+ kind,
373
+ order: this.next(),
374
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
375
+ startedAtMs: Date.now(),
376
+ awaits: [],
377
+ target: event.target,
378
+ sensitive: event.sensitive,
379
+ value: kind === "press" ? event.key : void 0,
380
+ pageId
381
+ });
382
+ }
383
+ };
384
+ function toRawAction(action) {
385
+ return {
386
+ kind: action.kind,
387
+ startedAt: action.startedAt,
388
+ pageId: action.pageId,
389
+ selector: action.selector,
390
+ value: revealedValue(action),
391
+ target: action.target
392
+ };
393
+ }
394
+ function revealedValue(action) {
395
+ if (!VALUE_NEEDS_TARGET.has(action.kind)) return action.value;
396
+ if (action.kind === "press" && (action.value?.length ?? 0) > 1) return action.value;
397
+ if (action.target === void 0 || action.sensitive !== false) return;
398
+ const length = action.value?.length ?? 0;
399
+ return (action.kind === "fill" ? action.valueLength === length : action.kind === "type" ? (action.valueLength ?? -1) >= length : true) ? action.value : void 0;
400
+ }
401
+ function clip(value) {
402
+ return value === void 0 || value.length <= MAX_TEXT ? value : value.slice(0, 199) + "…";
403
+ }
404
+ function isDomEvent(event) {
405
+ const candidate = event;
406
+ const box = candidate?.target?.box;
407
+ 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";
408
+ }
409
+ /**
410
+ * Runs in every page. Serialized by Playwright, so it may use nothing from
411
+ * its module.
412
+ */
413
+ function installActionReporter(binding) {
414
+ if (window !== window.top) return;
415
+ const installed = binding + "Installed";
416
+ if (installed in window) return;
417
+ Object.defineProperty(window, installed, { value: true });
418
+ const maxText = 200;
419
+ const clean = (value) => {
420
+ const collapsed = value?.replace(/\s+/g, " ").trim();
421
+ return collapsed ? collapsed.slice(0, maxText) : void 0;
422
+ };
423
+ const inputRoles = {
424
+ checkbox: "checkbox",
425
+ radio: "radio",
426
+ button: "button",
427
+ submit: "button",
428
+ reset: "button",
429
+ image: "button",
430
+ range: "slider",
431
+ number: "spinbutton",
432
+ search: "searchbox"
433
+ };
434
+ const roleOf = (element) => {
435
+ const explicit = element.getAttribute("role")?.trim().split(/\s+/)[0];
436
+ if (explicit) return explicit;
437
+ const tag = element.tagName.toLowerCase();
438
+ if (tag === "a") return element.hasAttribute("href") ? "link" : void 0;
439
+ if (tag === "button" || tag === "summary") return "button";
440
+ if (tag === "select") {
441
+ const select = element;
442
+ return select.multiple || select.size > 1 ? "listbox" : "combobox";
443
+ }
444
+ if (tag === "textarea") return "textbox";
445
+ if (tag === "option") return "option";
446
+ if (tag === "img") return "img";
447
+ if (/^h[1-6]$/.test(tag)) return "heading";
448
+ if (tag === "input") {
449
+ const input = element;
450
+ return inputRoles[input.type] ?? (input.hasAttribute("list") ? "combobox" : "textbox");
451
+ }
452
+ };
453
+ const nameOf = (element) => {
454
+ const labelledBy = element.getAttribute("aria-labelledby");
455
+ if (labelledBy) {
456
+ const labelled = clean(labelledBy.split(/\s+/).map((id) => document.getElementById(id)?.textContent ?? "").join(" "));
457
+ if (labelled) return labelled;
458
+ }
459
+ const ariaLabel = clean(element.getAttribute("aria-label"));
460
+ if (ariaLabel) return ariaLabel;
461
+ if (element instanceof HTMLInputElement || element instanceof HTMLSelectElement || element instanceof HTMLTextAreaElement) {
462
+ const labels = clean([...element.labels ?? []].map((label) => label.textContent ?? "").join(" "));
463
+ if (labels) return labels;
464
+ if (element instanceof HTMLInputElement && [
465
+ "submit",
466
+ "button",
467
+ "reset"
468
+ ].includes(element.type)) return clean(element.value);
469
+ return clean(element.getAttribute("placeholder")) ?? clean(element.getAttribute("title"));
470
+ }
471
+ if (element instanceof HTMLImageElement) return clean(element.alt) ?? clean(element.title);
472
+ return clean(element.innerText ?? element.textContent) ?? clean(element.getAttribute("title"));
473
+ };
474
+ const interactive = "a[href],button,input,select,textarea,summary,label,option,[role],[data-track-id],[contenteditable=\"\"],[contenteditable=\"true\"],[tabindex]";
475
+ let lastHovered;
476
+ const report = (event) => {
477
+ if (!event.isTrusted && event.type !== "input") return;
478
+ const origin = event.composedPath()[0];
479
+ const element = origin instanceof Element ? origin : origin instanceof Node ? origin.parentElement : null;
480
+ if (!element) return;
481
+ const actual = element.closest(interactive) ?? element;
482
+ if (event.type === "mouseover") {
483
+ if (actual === lastHovered) return;
484
+ lastHovered = actual;
485
+ }
486
+ const key = event.key;
487
+ const rect = actual.getBoundingClientRect();
488
+ const trackId = actual.closest("[data-track-id]")?.getAttribute("data-track-id");
489
+ const role = roleOf(actual);
490
+ const name = nameOf(actual);
491
+ const sensitive = actual instanceof HTMLInputElement && (actual.type === "password" || /password|one-time-code|cc-/.test(actual.autocomplete));
492
+ const valueLength = event.type === "input" && (actual instanceof HTMLInputElement || actual instanceof HTMLTextAreaElement) ? actual.value.length : void 0;
493
+ const send = window[binding];
494
+ if (typeof send !== "function") return;
495
+ send({
496
+ type: event.type,
497
+ trusted: event.isTrusted,
498
+ at: Date.now(),
499
+ ...valueLength === void 0 ? {} : { valueLength },
500
+ ...event.type === "keydown" && key && key.length > 1 ? { key } : {},
501
+ sensitive,
502
+ target: {
503
+ tag: actual.tagName.toLowerCase(),
504
+ ...role ? { role } : {},
505
+ ...name ? { name } : {},
506
+ ...trackId ? { trackId } : {},
507
+ box: {
508
+ x: rect.x + window.scrollX,
509
+ y: rect.y + window.scrollY,
510
+ width: rect.width,
511
+ height: rect.height
512
+ }
513
+ }
514
+ }).catch(() => void 0);
515
+ };
516
+ for (const type of [
517
+ "click",
518
+ "dblclick",
519
+ "touchstart",
520
+ "mouseover",
521
+ "mousedown",
522
+ "dragstart",
523
+ "input",
524
+ "keydown"
525
+ ]) window.addEventListener(type, report, {
526
+ capture: true,
527
+ passive: true
528
+ });
529
+ }
74
530
  //#endregion
75
531
  //#region src/animations.ts
76
532
  /**
@@ -274,6 +730,8 @@ const SCREENSHOT_ATTACHMENT_PREFIX = "ralph-screenshot-";
274
730
  let warnedNoTracker = false;
275
731
  const DEFAULT_SETTLE_MS = 150;
276
732
  const DEFAULT_RECORD_TIMEOUT_MS = 5e3;
733
+ /** The actions kept on one edge, the latest ones, so a long setup does not bloat the graph. */
734
+ const MAX_ACTIONS_PER_EDGE = 100;
277
735
  /** Where the raw graph and screenshots are written, inside Playwright's per-test output directory. */
278
736
  const OUTPUT_DIR = "ralph";
279
737
  var Recorder = class {
@@ -316,7 +774,14 @@ var Recorder = class {
316
774
  paused = false;
317
775
  /** The most recently requested node, the anchor for tabs used while paused. */
318
776
  lastRequestedNodeId;
319
- constructor(options, config, testInfo, mode) {
777
+ /**
778
+ * Orders node requests and actions on one counter, which decides the edge
779
+ * an action belongs to. Timestamps could tie.
780
+ */
781
+ order = 0;
782
+ requestOrder = /* @__PURE__ */ new Map();
783
+ 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));
784
+ constructor(options, config, testInfo, mode, playwright) {
320
785
  this.options = options;
321
786
  this.config = config;
322
787
  this.testInfo = testInfo;
@@ -324,6 +789,7 @@ var Recorder = class {
324
789
  this.settleMs = options.settleMs ?? DEFAULT_SETTLE_MS;
325
790
  this.recordTimeoutMs = options.recordTimeoutMs ?? DEFAULT_RECORD_TIMEOUT_MS;
326
791
  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.");
792
+ this.actions.attach(playwright);
327
793
  }
328
794
  /** Starts recording URL changes in every current and future tab of `context`. */
329
795
  observe(context) {
@@ -331,6 +797,7 @@ var Recorder = class {
331
797
  if (this.contexts.has(context)) return;
332
798
  const onPage = (page) => this.watchPage(page);
333
799
  this.contexts.set(context, onPage);
800
+ this.actions.observe(context);
334
801
  context.on("page", onPage);
335
802
  for (const page of context.pages()) onPage(page);
336
803
  }
@@ -339,7 +806,7 @@ var Recorder = class {
339
806
  const state = this.pages.get(page);
340
807
  if (!state) throw new Error("Cannot record a closed browser page.");
341
808
  if (this.paused) return;
342
- await this.enqueueCreateNodeRequest(state, "explicit", options?.name);
809
+ await this.enqueueCreateNodeRequest(state, "explicit", options);
343
810
  }
344
811
  async setNodeConditions(page, declaration) {
345
812
  this.observe(page.context());
@@ -382,10 +849,12 @@ var Recorder = class {
382
849
  */
383
850
  async finish() {
384
851
  this.closed = true;
852
+ this.actions.detach();
385
853
  await Promise.all([...this.contexts.keys()].map((context) => this.stopContext(context)));
386
854
  await Promise.all([...this.pages.values()].map((state) => state.opener));
387
855
  const nodes = this.nodes.sort((a, b) => a.pageId.localeCompare(b.pageId) || a.sequence - b.sequence);
388
856
  linkPreviousNodes(nodes, (pageId) => this.pageInfo(pageId)?.openerPageId, (nodeId) => this.requestedAtMs.get(nodeId) ?? 0, (pageId) => this.pageState(pageId)?.pauseAnchor);
857
+ this.attachActions(nodes);
389
858
  const failures = nodes.filter((item) => item.status === "uncaptured" && item.reason === "error");
390
859
  await this.writeRawGraph(this.buildRawGraph(nodes, failures.length));
391
860
  const mismatched = nodes.filter((item) => item.tagMismatches?.length);
@@ -407,7 +876,10 @@ var Recorder = class {
407
876
  tail: Promise.resolve(),
408
877
  opener: Promise.resolve(),
409
878
  onNavigation: (frame) => {
410
- if (frame === page.mainFrame()) this.urlChanged(state);
879
+ if (frame === page.mainFrame()) {
880
+ this.actions.navigated(page);
881
+ this.urlChanged(state);
882
+ }
411
883
  }
412
884
  };
413
885
  this.pages.set(page, state);
@@ -440,7 +912,7 @@ var Recorder = class {
440
912
  return page.evaluate(() => window.__ralphTags?.()).catch(() => void 0);
441
913
  }
442
914
  /** Queues a recording behind the tab's earlier ones. */
443
- enqueueCreateNodeRequest(state, trigger, name) {
915
+ enqueueCreateNodeRequest(state, trigger, { name, edgeLabel } = {}) {
444
916
  const url = state.page.url();
445
917
  if (!isHttpUrl(url)) throw new Error("Ralph recordings require an HTTP or HTTPS page.");
446
918
  const request = {
@@ -451,9 +923,11 @@ var Recorder = class {
451
923
  actualUrl: url,
452
924
  url,
453
925
  trigger,
454
- ...name === void 0 ? {} : { name }
926
+ ...name === void 0 ? {} : { name },
927
+ ...edgeLabel === void 0 ? {} : { edgeLabel }
455
928
  };
456
929
  this.requestedAtMs.set(request.nodeId, Date.now());
930
+ this.requestOrder.set(request.nodeId, ++this.order);
457
931
  this.lastRequestedNodeId = request.nodeId;
458
932
  const generation = state.generation;
459
933
  const task = state.tail.then(() => this.createNode(state, generation, request));
@@ -519,9 +993,10 @@ var Recorder = class {
519
993
  nodeId: request.nodeId
520
994
  };
521
995
  } else {
522
- const { nodeId, sequence, requestedAt, name } = this.nodes[retaken];
996
+ const { nodeId, sequence, requestedAt, name, edgeLabel } = this.nodes[retaken];
523
997
  this.nodes[retaken] = {
524
998
  ...name === void 0 ? {} : { name },
999
+ ...edgeLabel === void 0 ? {} : { edgeLabel },
525
1000
  ...recorded,
526
1001
  nodeId,
527
1002
  sequence,
@@ -567,6 +1042,24 @@ var Recorder = class {
567
1042
  contentType: image.contentType
568
1043
  };
569
1044
  }
1045
+ /** Gives each node the actions taken since the node it was reached from was requested. */
1046
+ attachActions(nodes) {
1047
+ const byId = new Map(nodes.map((item) => [item.nodeId, item]));
1048
+ const windows = nodes.flatMap((item) => {
1049
+ const from = byId.get(item.previousNodeId ?? "");
1050
+ return from ? [{
1051
+ nodeId: item.nodeId,
1052
+ after: this.requestOrder.get(from.nodeId) ?? 0,
1053
+ upTo: this.requestOrder.get(item.nodeId) ?? 0,
1054
+ pageIds: /* @__PURE__ */ new Set([from.pageId, item.pageId])
1055
+ }] : [];
1056
+ });
1057
+ const actions = this.actions.assign(windows, MAX_ACTIONS_PER_EDGE);
1058
+ for (const item of nodes) {
1059
+ const taken = actions.get(item.nodeId);
1060
+ if (taken) item.actions = taken;
1061
+ }
1062
+ }
570
1063
  pageState(pageId) {
571
1064
  return [...this.pages.values()].find((state) => state.info.pageId === pageId);
572
1065
  }
@@ -952,4 +1445,4 @@ Object.defineProperty(exports, "version", {
952
1445
  }
953
1446
  });
954
1447
 
955
- //# sourceMappingURL=upload-ZMnTCjs2.cjs.map
1448
+ //# sourceMappingURL=upload-BvAi7mMY.cjs.map