@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.
- package/README.md +19 -0
- package/dist/index.cjs +3 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.mts +56 -2
- package/dist/index.mjs +3 -3
- package/dist/index.mjs.map +1 -1
- package/dist/reporter.cjs +1 -1
- package/dist/reporter.mjs +1 -1
- package/dist/{upload-Cs6oMofL.cjs → upload-BvAi7mMY.cjs} +504 -9
- package/dist/upload-BvAi7mMY.cjs.map +1 -0
- package/dist/{upload-BENmMj46.mjs → upload-D5hePvmA.mjs} +504 -9
- package/dist/upload-D5hePvmA.mjs.map +1 -0
- package/package.json +1 -1
- package/dist/upload-BENmMj46.mjs.map +0 -1
- package/dist/upload-Cs6oMofL.cjs.map +0 -1
|
@@ -45,7 +45,463 @@ function snapshot(value) {
|
|
|
45
45
|
}
|
|
46
46
|
//#endregion
|
|
47
47
|
//#region package.json
|
|
48
|
-
var version = "1.
|
|
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
|
-
|
|
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())
|
|
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-
|
|
1334
|
+
//# sourceMappingURL=upload-D5hePvmA.mjs.map
|