@ralphralphai/playwright 1.7.0 → 1.9.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 +106 -54
- package/dist/index.cjs +3 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.mts +47 -1
- package/dist/index.mjs +3 -3
- package/dist/index.mjs.map +1 -1
- package/dist/reporter.cjs +11 -2
- package/dist/reporter.cjs.map +1 -1
- package/dist/reporter.d.mts +7 -0
- package/dist/reporter.mjs +11 -2
- package/dist/reporter.mjs.map +1 -1
- package/dist/{upload-ZMnTCjs2.cjs → upload-BQSCs8By.cjs} +523 -10
- package/dist/upload-BQSCs8By.cjs.map +1 -0
- package/dist/{upload-DQn3CLvx.mjs → upload-BZs6Vqix.mjs} +518 -11
- package/dist/upload-BZs6Vqix.mjs.map +1 -0
- package/package.json +1 -1
- package/dist/upload-DQn3CLvx.mjs.map +0 -1
- package/dist/upload-ZMnTCjs2.cjs.map +0 -1
|
@@ -70,7 +70,463 @@ function snapshot(value) {
|
|
|
70
70
|
}
|
|
71
71
|
//#endregion
|
|
72
72
|
//#region package.json
|
|
73
|
-
var version = "1.
|
|
73
|
+
var version = "1.9.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
|
-
|
|
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
|
|
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())
|
|
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
|
}
|
|
@@ -792,10 +1285,24 @@ const PREPARE_TIMEOUT_MS = 3e4;
|
|
|
792
1285
|
* run can be followed while it uploads and while Ralph processes it.
|
|
793
1286
|
*/
|
|
794
1287
|
async function prepareArtifactUpload(options) {
|
|
795
|
-
|
|
1288
|
+
return reserve(options, "/api/upload/artifact/prepare", { externalAppId: options.appId });
|
|
1289
|
+
}
|
|
1290
|
+
/**
|
|
1291
|
+
* Records that `buildId` went live under `config`, and reserves the artifact
|
|
1292
|
+
* its run uploads into. The run then becomes that build's graph in Ralph,
|
|
1293
|
+
* where an artifact from `prepareArtifactUpload` stays a standalone result.
|
|
1294
|
+
*/
|
|
1295
|
+
async function registerDeployment(options, deployment) {
|
|
1296
|
+
return reserve(options, "/api/upload/deployment/register", {
|
|
1297
|
+
externalAppId: options.appId,
|
|
1298
|
+
...deployment
|
|
1299
|
+
});
|
|
1300
|
+
}
|
|
1301
|
+
async function reserve(options, route, body) {
|
|
1302
|
+
const url = checkedApiUrl(options, route);
|
|
796
1303
|
const reply = await request(url, {
|
|
797
1304
|
headers: { "content-type": "application/json" },
|
|
798
|
-
body: JSON.stringify(
|
|
1305
|
+
body: JSON.stringify(body)
|
|
799
1306
|
}, options, PREPARE_TIMEOUT_MS);
|
|
800
1307
|
if (reply["result"] !== "ok" || typeof reply["artifactId"] !== "string" || reply["resultUrl"] !== void 0 && typeof reply["resultUrl"] !== "string") throw unreadableReply(url);
|
|
801
1308
|
return {
|
|
@@ -921,6 +1428,12 @@ Object.defineProperty(exports, "prepareArtifactUpload", {
|
|
|
921
1428
|
return prepareArtifactUpload;
|
|
922
1429
|
}
|
|
923
1430
|
});
|
|
1431
|
+
Object.defineProperty(exports, "registerDeployment", {
|
|
1432
|
+
enumerable: true,
|
|
1433
|
+
get: function() {
|
|
1434
|
+
return registerDeployment;
|
|
1435
|
+
}
|
|
1436
|
+
});
|
|
924
1437
|
Object.defineProperty(exports, "resolveMode", {
|
|
925
1438
|
enumerable: true,
|
|
926
1439
|
get: function() {
|
|
@@ -952,4 +1465,4 @@ Object.defineProperty(exports, "version", {
|
|
|
952
1465
|
}
|
|
953
1466
|
});
|
|
954
1467
|
|
|
955
|
-
//# sourceMappingURL=upload-
|
|
1468
|
+
//# sourceMappingURL=upload-BQSCs8By.cjs.map
|