browsecraft 0.1.1 → 0.2.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/dist/{chunk-77HRTGXZ.js → chunk-3OBA6D2X.js} +706 -90
- package/dist/chunk-3OBA6D2X.js.map +1 -0
- package/dist/{chunk-KIPQFK3Y.cjs → chunk-75VSZDYK.cjs} +710 -89
- package/dist/chunk-75VSZDYK.cjs.map +1 -0
- package/dist/cli.cjs +17 -11
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +12 -6
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +46 -21
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +204 -33
- package/dist/index.d.ts +204 -33
- package/dist/index.js +3 -6
- package/dist/index.js.map +1 -1
- package/package.json +6 -6
- package/dist/chunk-77HRTGXZ.js.map +0 -1
- package/dist/chunk-KIPQFK3Y.cjs.map +0 -1
|
@@ -31,6 +31,121 @@ function resolveConfig(userConfig) {
|
|
|
31
31
|
};
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
// src/errors.ts
|
|
35
|
+
var BrowsecraftError = class extends Error {
|
|
36
|
+
name = "BrowsecraftError";
|
|
37
|
+
/** What action was being performed */
|
|
38
|
+
action;
|
|
39
|
+
/** What target was being acted on */
|
|
40
|
+
target;
|
|
41
|
+
/** Element state at the time of failure */
|
|
42
|
+
elementState;
|
|
43
|
+
/** Hint for how to fix the issue */
|
|
44
|
+
hint;
|
|
45
|
+
/** How long we waited before giving up (ms) */
|
|
46
|
+
elapsed;
|
|
47
|
+
constructor(options) {
|
|
48
|
+
const parts = [];
|
|
49
|
+
parts.push(`Could not ${options.action} '${options.target}'`);
|
|
50
|
+
parts.push(`\u2014 ${options.message}`);
|
|
51
|
+
if (options.elementState) {
|
|
52
|
+
parts.push("");
|
|
53
|
+
parts.push(formatElementState(options.elementState));
|
|
54
|
+
}
|
|
55
|
+
if (options.hint) {
|
|
56
|
+
parts.push("");
|
|
57
|
+
parts.push(`Hint: ${options.hint}`);
|
|
58
|
+
}
|
|
59
|
+
if (options.elapsed !== void 0) {
|
|
60
|
+
parts.push(`(waited ${options.elapsed}ms)`);
|
|
61
|
+
}
|
|
62
|
+
super(parts.join("\n"));
|
|
63
|
+
this.action = options.action;
|
|
64
|
+
this.target = options.target;
|
|
65
|
+
this.elementState = options.elementState;
|
|
66
|
+
this.hint = options.hint;
|
|
67
|
+
this.elapsed = options.elapsed;
|
|
68
|
+
if (options.cause) {
|
|
69
|
+
this.cause = options.cause;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
var ElementNotFoundError = class extends BrowsecraftError {
|
|
74
|
+
name = "ElementNotFoundError";
|
|
75
|
+
constructor(options) {
|
|
76
|
+
const hint = options.suggestions?.length ? `Did you mean one of these?
|
|
77
|
+
${options.suggestions.map((s) => ` - ${s}`).join("\n")}` : "Check that the element exists on the page and the text/selector is correct.";
|
|
78
|
+
super({
|
|
79
|
+
action: options.action,
|
|
80
|
+
target: options.target,
|
|
81
|
+
message: "no matching element found on the page.",
|
|
82
|
+
elementState: { found: false },
|
|
83
|
+
hint,
|
|
84
|
+
elapsed: options.elapsed
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
var ElementNotActionableError = class extends BrowsecraftError {
|
|
89
|
+
name = "ElementNotActionableError";
|
|
90
|
+
reason;
|
|
91
|
+
constructor(options) {
|
|
92
|
+
const messages = {
|
|
93
|
+
"not-visible": "the element was found but is not visible (display:none, visibility:hidden, opacity:0, or zero size).",
|
|
94
|
+
disabled: "the element was found but is disabled.",
|
|
95
|
+
obscured: `the element was found but is obscured by another element${options.elementState.obscuredBy ? ` (${options.elementState.obscuredBy})` : ""}.`,
|
|
96
|
+
"zero-size": "the element was found but has zero width/height.",
|
|
97
|
+
detached: "the element was found but is no longer attached to the DOM."
|
|
98
|
+
};
|
|
99
|
+
const hints = {
|
|
100
|
+
"not-visible": "Wait for a loading state to complete, or check CSS visibility rules.",
|
|
101
|
+
disabled: "Wait for the element to become enabled, or check if a prerequisite action is needed first.",
|
|
102
|
+
obscured: "Scroll the page, close overlapping modals/tooltips, or use { force: true } to bypass.",
|
|
103
|
+
"zero-size": "Check if the element has proper dimensions. It may be collapsed or off-screen.",
|
|
104
|
+
detached: "The element was removed from the DOM while waiting. This often happens with dynamic content."
|
|
105
|
+
};
|
|
106
|
+
super({
|
|
107
|
+
action: options.action,
|
|
108
|
+
target: options.target,
|
|
109
|
+
message: messages[options.reason] ?? `the element is not actionable (${options.reason}).`,
|
|
110
|
+
elementState: options.elementState,
|
|
111
|
+
hint: hints[options.reason],
|
|
112
|
+
elapsed: options.elapsed
|
|
113
|
+
});
|
|
114
|
+
this.reason = options.reason;
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
var NetworkError = class extends BrowsecraftError {
|
|
118
|
+
name = "NetworkError";
|
|
119
|
+
constructor(options) {
|
|
120
|
+
super({
|
|
121
|
+
action: options.action,
|
|
122
|
+
target: options.target,
|
|
123
|
+
message: options.message,
|
|
124
|
+
hint: "Check that the URL pattern is correct and network interception is set up before navigation.",
|
|
125
|
+
elapsed: options.elapsed
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
var TimeoutError = class extends BrowsecraftError {
|
|
130
|
+
name = "TimeoutError";
|
|
131
|
+
};
|
|
132
|
+
function formatElementState(state) {
|
|
133
|
+
if (!state.found) return "Element state: NOT FOUND in DOM";
|
|
134
|
+
const lines = ["Element state:"];
|
|
135
|
+
if (state.tagName) lines.push(` Tag: <${state.tagName.toLowerCase()}>`);
|
|
136
|
+
if (state.id) lines.push(` Id: #${state.id}`);
|
|
137
|
+
if (state.classes) lines.push(` Classes: ${state.classes}`);
|
|
138
|
+
if (state.textPreview) lines.push(` Text: "${state.textPreview}"`);
|
|
139
|
+
if (state.visible !== void 0) lines.push(` Visible: ${state.visible}`);
|
|
140
|
+
if (state.enabled !== void 0) lines.push(` Enabled: ${state.enabled}`);
|
|
141
|
+
if (state.obscured !== void 0) lines.push(` Obscured: ${state.obscured}`);
|
|
142
|
+
if (state.boundingBox) {
|
|
143
|
+
const b = state.boundingBox;
|
|
144
|
+
lines.push(` Position: (${b.x}, ${b.y}) Size: ${b.width}x${b.height}`);
|
|
145
|
+
}
|
|
146
|
+
return lines.join("\n");
|
|
147
|
+
}
|
|
148
|
+
|
|
34
149
|
// src/wait.ts
|
|
35
150
|
async function waitFor(description, fn, options) {
|
|
36
151
|
const { timeout, interval = 100 } = options;
|
|
@@ -50,9 +165,7 @@ async function waitFor(description, fn, options) {
|
|
|
50
165
|
const elapsed = Date.now() - startTime;
|
|
51
166
|
const errorMsg = lastError ? `
|
|
52
167
|
Last error: ${lastError.message}` : "";
|
|
53
|
-
throw new Error(
|
|
54
|
-
`Timed out after ${elapsed}ms waiting for: ${description}${errorMsg}`
|
|
55
|
-
);
|
|
168
|
+
throw new Error(`Timed out after ${elapsed}ms waiting for: ${description}${errorMsg}`);
|
|
56
169
|
}
|
|
57
170
|
async function waitForLoadState(session, contextId, state = "load", timeout = 3e4) {
|
|
58
171
|
const result = await session.script.evaluate({
|
|
@@ -85,44 +198,258 @@ async function waitForLoadState(session, contextId, state = "load", timeout = 3e
|
|
|
85
198
|
function sleep(ms) {
|
|
86
199
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
87
200
|
}
|
|
201
|
+
async function checkActionability(session, contextId, ref, checks = {}) {
|
|
202
|
+
const doVisible = checks.visible !== false;
|
|
203
|
+
const doEnabled = checks.enabled !== false;
|
|
204
|
+
const doObscured = checks.notObscured === true;
|
|
205
|
+
try {
|
|
206
|
+
const result = await session.script.callFunction({
|
|
207
|
+
functionDeclaration: `function(el, doVisible, doEnabled, doObscured) {
|
|
208
|
+
// Check if element is still in the DOM
|
|
209
|
+
if (!el.isConnected) {
|
|
210
|
+
return {
|
|
211
|
+
actionable: false,
|
|
212
|
+
reason: 'detached',
|
|
213
|
+
state: { found: true, visible: false, enabled: false }
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const style = window.getComputedStyle(el);
|
|
218
|
+
const rect = el.getBoundingClientRect();
|
|
219
|
+
const tagName = el.tagName || '';
|
|
220
|
+
const textPreview = (el.innerText || el.textContent || '').slice(0, 80).trim();
|
|
221
|
+
const classes = el.className || '';
|
|
222
|
+
const id = el.id || '';
|
|
223
|
+
|
|
224
|
+
const state = {
|
|
225
|
+
found: true,
|
|
226
|
+
tagName: tagName,
|
|
227
|
+
textPreview: textPreview,
|
|
228
|
+
classes: typeof classes === 'string' ? classes : '',
|
|
229
|
+
id: id,
|
|
230
|
+
boundingBox: {
|
|
231
|
+
x: Math.round(rect.x),
|
|
232
|
+
y: Math.round(rect.y),
|
|
233
|
+
width: Math.round(rect.width),
|
|
234
|
+
height: Math.round(rect.height)
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
// Visibility check
|
|
239
|
+
if (doVisible) {
|
|
240
|
+
const isVisible = style.display !== 'none'
|
|
241
|
+
&& style.visibility !== 'hidden'
|
|
242
|
+
&& style.opacity !== '0'
|
|
243
|
+
&& rect.width > 0
|
|
244
|
+
&& rect.height > 0;
|
|
245
|
+
|
|
246
|
+
state.visible = isVisible;
|
|
247
|
+
|
|
248
|
+
if (!isVisible) {
|
|
249
|
+
if (rect.width === 0 || rect.height === 0) {
|
|
250
|
+
return { actionable: false, reason: 'zero-size', state: state };
|
|
251
|
+
}
|
|
252
|
+
return { actionable: false, reason: 'not-visible', state: state };
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Enabled check (only for form elements)
|
|
257
|
+
if (doEnabled) {
|
|
258
|
+
const formTags = ['INPUT', 'BUTTON', 'SELECT', 'TEXTAREA', 'FIELDSET'];
|
|
259
|
+
const isFormElement = formTags.includes(tagName);
|
|
260
|
+
const isDisabled = isFormElement && el.disabled === true;
|
|
261
|
+
state.enabled = !isDisabled;
|
|
262
|
+
|
|
263
|
+
if (isDisabled) {
|
|
264
|
+
return { actionable: false, reason: 'disabled', state: state };
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Obscured check (elementFromPoint)
|
|
269
|
+
if (doObscured) {
|
|
270
|
+
const cx = rect.x + rect.width / 2;
|
|
271
|
+
const cy = rect.y + rect.height / 2;
|
|
272
|
+
const topEl = document.elementFromPoint(cx, cy);
|
|
273
|
+
|
|
274
|
+
if (topEl && topEl !== el && !el.contains(topEl)) {
|
|
275
|
+
state.obscured = true;
|
|
276
|
+
state.obscuredBy = '<' + topEl.tagName.toLowerCase()
|
|
277
|
+
+ (topEl.id ? '#' + topEl.id : '')
|
|
278
|
+
+ (topEl.className ? '.' + topEl.className.split(' ').join('.') : '')
|
|
279
|
+
+ '>';
|
|
280
|
+
return { actionable: false, reason: 'obscured', state: state };
|
|
281
|
+
}
|
|
282
|
+
state.obscured = false;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
state.visible = true;
|
|
286
|
+
state.enabled = true;
|
|
287
|
+
return { actionable: true, state: state };
|
|
288
|
+
}`,
|
|
289
|
+
target: { context: contextId },
|
|
290
|
+
arguments: [
|
|
291
|
+
ref,
|
|
292
|
+
{ type: "boolean", value: doVisible },
|
|
293
|
+
{ type: "boolean", value: doEnabled },
|
|
294
|
+
{ type: "boolean", value: doObscured }
|
|
295
|
+
],
|
|
296
|
+
awaitPromise: false
|
|
297
|
+
});
|
|
298
|
+
if (result.type === "success" && result.result?.type === "object") {
|
|
299
|
+
return deserializeActionabilityResult(result.result.value);
|
|
300
|
+
}
|
|
301
|
+
return {
|
|
302
|
+
actionable: true,
|
|
303
|
+
state: { found: true, visible: true, enabled: true }
|
|
304
|
+
};
|
|
305
|
+
} catch {
|
|
306
|
+
return {
|
|
307
|
+
actionable: false,
|
|
308
|
+
reason: "detached",
|
|
309
|
+
state: { found: true, visible: false, enabled: false }
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
async function waitForActionable(session, contextId, ref, description, options, checks) {
|
|
314
|
+
let lastResult = null;
|
|
315
|
+
try {
|
|
316
|
+
return await waitFor(
|
|
317
|
+
`${description} to be actionable`,
|
|
318
|
+
async () => {
|
|
319
|
+
const result = await checkActionability(session, contextId, ref, checks);
|
|
320
|
+
lastResult = result;
|
|
321
|
+
return result.actionable ? result : null;
|
|
322
|
+
},
|
|
323
|
+
options
|
|
324
|
+
);
|
|
325
|
+
} catch {
|
|
326
|
+
return lastResult ?? {
|
|
327
|
+
actionable: false,
|
|
328
|
+
reason: "not-visible",
|
|
329
|
+
state: { found: true }
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
function deserializeActionabilityResult(value) {
|
|
334
|
+
if (!Array.isArray(value)) {
|
|
335
|
+
return { actionable: true, state: { found: true } };
|
|
336
|
+
}
|
|
337
|
+
const map = /* @__PURE__ */ new Map();
|
|
338
|
+
for (const entry of value) {
|
|
339
|
+
if (Array.isArray(entry) && entry.length === 2) {
|
|
340
|
+
const val = entry[1];
|
|
341
|
+
map.set(entry[0], val && typeof val === "object" && "value" in val ? val.value : val);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
const stateRaw = map.get("state");
|
|
345
|
+
let state = { found: true };
|
|
346
|
+
if (Array.isArray(stateRaw)) {
|
|
347
|
+
const stateMap = /* @__PURE__ */ new Map();
|
|
348
|
+
for (const entry of stateRaw) {
|
|
349
|
+
if (Array.isArray(entry) && entry.length === 2) {
|
|
350
|
+
const val = entry[1];
|
|
351
|
+
stateMap.set(entry[0], val && typeof val === "object" && "value" in val ? val.value : val);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
let boundingBox;
|
|
355
|
+
const bbRaw = stateMap.get("boundingBox");
|
|
356
|
+
if (Array.isArray(bbRaw)) {
|
|
357
|
+
const bbMap = /* @__PURE__ */ new Map();
|
|
358
|
+
for (const entry of bbRaw) {
|
|
359
|
+
if (Array.isArray(entry) && entry.length === 2) {
|
|
360
|
+
const val = entry[1];
|
|
361
|
+
bbMap.set(entry[0], val && typeof val === "object" && "value" in val ? val.value : val);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
boundingBox = {
|
|
365
|
+
x: bbMap.get("x") ?? 0,
|
|
366
|
+
y: bbMap.get("y") ?? 0,
|
|
367
|
+
width: bbMap.get("width") ?? 0,
|
|
368
|
+
height: bbMap.get("height") ?? 0
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
state = {
|
|
372
|
+
found: true,
|
|
373
|
+
visible: stateMap.get("visible"),
|
|
374
|
+
enabled: stateMap.get("enabled"),
|
|
375
|
+
tagName: stateMap.get("tagName"),
|
|
376
|
+
textPreview: stateMap.get("textPreview"),
|
|
377
|
+
classes: stateMap.get("classes"),
|
|
378
|
+
id: stateMap.get("id"),
|
|
379
|
+
obscured: stateMap.get("obscured"),
|
|
380
|
+
obscuredBy: stateMap.get("obscuredBy"),
|
|
381
|
+
boundingBox
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
return {
|
|
385
|
+
actionable: map.get("actionable") ?? true,
|
|
386
|
+
reason: map.get("reason"),
|
|
387
|
+
state
|
|
388
|
+
};
|
|
389
|
+
}
|
|
88
390
|
|
|
89
391
|
// src/locator.ts
|
|
392
|
+
var HEAD_ONLY_ELEMENTS = /* @__PURE__ */ new Set([
|
|
393
|
+
"title",
|
|
394
|
+
"meta",
|
|
395
|
+
"link",
|
|
396
|
+
"style",
|
|
397
|
+
"base",
|
|
398
|
+
"head",
|
|
399
|
+
"noscript"
|
|
400
|
+
// can appear in head too
|
|
401
|
+
]);
|
|
90
402
|
async function locateElement(session, contextId, target, options) {
|
|
91
403
|
const opts = normalizeTarget(target);
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
404
|
+
const description = describeTarget(target);
|
|
405
|
+
const startTime = Date.now();
|
|
406
|
+
try {
|
|
407
|
+
return await waitFor(
|
|
408
|
+
description,
|
|
409
|
+
async () => {
|
|
410
|
+
const strategies = buildStrategies(opts);
|
|
411
|
+
for (const { locator, strategy, isLabelLookup } of strategies) {
|
|
412
|
+
try {
|
|
413
|
+
const result = await session.browsingContext.locateNodes({
|
|
414
|
+
context: contextId,
|
|
415
|
+
locator,
|
|
416
|
+
maxNodeCount: (opts.index ?? 0) + 10
|
|
417
|
+
// fetch extra for label resolution
|
|
418
|
+
});
|
|
419
|
+
const nodes = result.nodes.filter(
|
|
420
|
+
(n) => !HEAD_ONLY_ELEMENTS.has(n.value?.localName ?? "")
|
|
421
|
+
);
|
|
422
|
+
if (nodes.length > 0) {
|
|
423
|
+
if (isLabelLookup) {
|
|
424
|
+
const resolved = await resolveLabelsToInputs(session, contextId, nodes);
|
|
425
|
+
if (resolved) {
|
|
426
|
+
return { node: resolved, strategy };
|
|
427
|
+
}
|
|
428
|
+
continue;
|
|
429
|
+
}
|
|
430
|
+
const nodeIndex = opts.index ?? 0;
|
|
431
|
+
const node = nodes[nodeIndex];
|
|
432
|
+
if (node) {
|
|
433
|
+
return { node, strategy };
|
|
109
434
|
}
|
|
110
|
-
continue;
|
|
111
|
-
}
|
|
112
|
-
const nodeIndex = opts.index ?? 0;
|
|
113
|
-
const node = result.nodes[nodeIndex];
|
|
114
|
-
if (node) {
|
|
115
|
-
return { node, strategy };
|
|
116
435
|
}
|
|
436
|
+
} catch {
|
|
117
437
|
}
|
|
118
|
-
} catch {
|
|
119
|
-
continue;
|
|
120
438
|
}
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
)
|
|
439
|
+
return null;
|
|
440
|
+
},
|
|
441
|
+
options
|
|
442
|
+
);
|
|
443
|
+
} catch (err) {
|
|
444
|
+
const elapsed = Date.now() - startTime;
|
|
445
|
+
const suggestions = await findSimilarElements(session, contextId, target);
|
|
446
|
+
throw new ElementNotFoundError({
|
|
447
|
+
action: "find",
|
|
448
|
+
target: typeof target === "string" ? target : description,
|
|
449
|
+
elapsed,
|
|
450
|
+
suggestions
|
|
451
|
+
});
|
|
452
|
+
}
|
|
126
453
|
}
|
|
127
454
|
async function locateAllElements(session, contextId, target) {
|
|
128
455
|
const opts = normalizeTarget(target);
|
|
@@ -134,11 +461,11 @@ async function locateAllElements(session, contextId, target) {
|
|
|
134
461
|
locator,
|
|
135
462
|
maxNodeCount: 1e3
|
|
136
463
|
});
|
|
137
|
-
|
|
138
|
-
|
|
464
|
+
const nodes = result.nodes.filter((n) => !HEAD_ONLY_ELEMENTS.has(n.value?.localName ?? ""));
|
|
465
|
+
if (nodes.length > 0) {
|
|
466
|
+
return nodes.map((node) => ({ node, strategy }));
|
|
139
467
|
}
|
|
140
468
|
} catch {
|
|
141
|
-
continue;
|
|
142
469
|
}
|
|
143
470
|
}
|
|
144
471
|
return [];
|
|
@@ -209,6 +536,13 @@ function buildStrategies(opts) {
|
|
|
209
536
|
},
|
|
210
537
|
strategy: `innerText("${name}")`
|
|
211
538
|
});
|
|
539
|
+
strategies.push({
|
|
540
|
+
locator: {
|
|
541
|
+
type: "css",
|
|
542
|
+
value: `[data-testid="${name}"], [data-test="${name}"], [data-test-id="${name}"]`
|
|
543
|
+
},
|
|
544
|
+
strategy: `data-testid("${name}")`
|
|
545
|
+
});
|
|
212
546
|
if (name.match(/^[#.\[a-z]/i)) {
|
|
213
547
|
strategies.push({
|
|
214
548
|
locator: { type: "css", value: name },
|
|
@@ -281,11 +615,57 @@ async function resolveLabelsToInputs(session, contextId, nodes) {
|
|
|
281
615
|
return result.result;
|
|
282
616
|
}
|
|
283
617
|
} catch {
|
|
284
|
-
continue;
|
|
285
618
|
}
|
|
286
619
|
}
|
|
287
620
|
return null;
|
|
288
621
|
}
|
|
622
|
+
async function findSimilarElements(session, contextId, target) {
|
|
623
|
+
const searchText = typeof target === "string" ? target : target.name ?? target.text ?? target.label ?? "";
|
|
624
|
+
if (!searchText) return [];
|
|
625
|
+
try {
|
|
626
|
+
const result = await session.script.callFunction({
|
|
627
|
+
functionDeclaration: `function(searchText) {
|
|
628
|
+
const suggestions = [];
|
|
629
|
+
const lower = searchText.toLowerCase();
|
|
630
|
+
|
|
631
|
+
// Look at all interactive elements and visible text
|
|
632
|
+
const interactiveSelectors = 'button, a, input, select, textarea, [role="button"], [role="link"], [role="tab"], [role="menuitem"]';
|
|
633
|
+
const elements = document.querySelectorAll(interactiveSelectors);
|
|
634
|
+
|
|
635
|
+
for (const el of elements) {
|
|
636
|
+
const text = (el.innerText || el.textContent || '').trim();
|
|
637
|
+
const ariaLabel = el.getAttribute('aria-label') || '';
|
|
638
|
+
const placeholder = el.getAttribute('placeholder') || '';
|
|
639
|
+
const value = el.getAttribute('value') || '';
|
|
640
|
+
const title = el.getAttribute('title') || '';
|
|
641
|
+
|
|
642
|
+
const candidates = [text, ariaLabel, placeholder, value, title].filter(Boolean);
|
|
643
|
+
for (const c of candidates) {
|
|
644
|
+
if (c.toLowerCase().includes(lower.slice(0, 3)) || lower.includes(c.toLowerCase().slice(0, 3))) {
|
|
645
|
+
const tag = el.tagName.toLowerCase();
|
|
646
|
+
const desc = c.length > 40 ? c.slice(0, 40) + '...' : c;
|
|
647
|
+
suggestions.push('<' + tag + '> "' + desc + '"');
|
|
648
|
+
break;
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
if (suggestions.length >= 5) break;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
return suggestions;
|
|
656
|
+
}`,
|
|
657
|
+
target: { context: contextId },
|
|
658
|
+
arguments: [{ type: "string", value: searchText }],
|
|
659
|
+
awaitPromise: false
|
|
660
|
+
});
|
|
661
|
+
if (result.type === "success" && result.result?.type === "array") {
|
|
662
|
+
const arr = result.result.value;
|
|
663
|
+
return arr.filter((item) => item?.type === "string").map((item) => item.value);
|
|
664
|
+
}
|
|
665
|
+
} catch {
|
|
666
|
+
}
|
|
667
|
+
return [];
|
|
668
|
+
}
|
|
289
669
|
|
|
290
670
|
// src/page.ts
|
|
291
671
|
var Page = class {
|
|
@@ -366,6 +746,7 @@ var Page = class {
|
|
|
366
746
|
async click(target, options) {
|
|
367
747
|
const timeout = options?.timeout ?? this.config.timeout;
|
|
368
748
|
const located = await locateElement(this.session, this.contextId, target, { timeout });
|
|
749
|
+
await this.ensureActionable(located, "click", target, { timeout });
|
|
369
750
|
await this.scrollIntoViewAndClick(located, options);
|
|
370
751
|
}
|
|
371
752
|
/**
|
|
@@ -381,6 +762,7 @@ var Page = class {
|
|
|
381
762
|
const timeout = options?.timeout ?? this.config.timeout;
|
|
382
763
|
const resolvedTarget = typeof target === "string" ? { label: target, name: target } : target;
|
|
383
764
|
const located = await locateElement(this.session, this.contextId, resolvedTarget, { timeout });
|
|
765
|
+
await this.ensureActionable(located, "fill", target, { timeout });
|
|
384
766
|
const ref = this.getSharedRef(located.node);
|
|
385
767
|
await this.session.script.callFunction({
|
|
386
768
|
functionDeclaration: `function(element, value) {
|
|
@@ -419,6 +801,7 @@ var Page = class {
|
|
|
419
801
|
const timeout = options?.timeout ?? this.config.timeout;
|
|
420
802
|
const resolvedTarget = typeof target === "string" ? { label: target, name: target } : target;
|
|
421
803
|
const located = await locateElement(this.session, this.contextId, resolvedTarget, { timeout });
|
|
804
|
+
await this.ensureActionable(located, "type", target, { timeout });
|
|
422
805
|
const ref = this.getSharedRef(located.node);
|
|
423
806
|
await this.session.script.callFunction({
|
|
424
807
|
functionDeclaration: "function(el) { el.focus(); }",
|
|
@@ -447,6 +830,7 @@ var Page = class {
|
|
|
447
830
|
const timeout = options?.timeout ?? this.config.timeout;
|
|
448
831
|
const resolvedTarget = typeof target === "string" ? { label: target, name: target } : target;
|
|
449
832
|
const located = await locateElement(this.session, this.contextId, resolvedTarget, { timeout });
|
|
833
|
+
await this.ensureActionable(located, "select", target, { timeout });
|
|
450
834
|
const ref = this.getSharedRef(located.node);
|
|
451
835
|
await this.session.script.callFunction({
|
|
452
836
|
functionDeclaration: `function(element, value) {
|
|
@@ -474,6 +858,7 @@ var Page = class {
|
|
|
474
858
|
async check(target, options) {
|
|
475
859
|
const timeout = options?.timeout ?? this.config.timeout;
|
|
476
860
|
const located = await locateElement(this.session, this.contextId, target, { timeout });
|
|
861
|
+
await this.ensureActionable(located, "check", target, { timeout });
|
|
477
862
|
const ref = this.getSharedRef(located.node);
|
|
478
863
|
const result = await this.session.script.callFunction({
|
|
479
864
|
functionDeclaration: "function(el) { return el.checked; }",
|
|
@@ -492,6 +877,7 @@ var Page = class {
|
|
|
492
877
|
async uncheck(target, options) {
|
|
493
878
|
const timeout = options?.timeout ?? this.config.timeout;
|
|
494
879
|
const located = await locateElement(this.session, this.contextId, target, { timeout });
|
|
880
|
+
await this.ensureActionable(located, "uncheck", target, { timeout });
|
|
495
881
|
const ref = this.getSharedRef(located.node);
|
|
496
882
|
const result = await this.session.script.callFunction({
|
|
497
883
|
functionDeclaration: "function(el) { return el.checked; }",
|
|
@@ -514,6 +900,7 @@ var Page = class {
|
|
|
514
900
|
async hover(target, options) {
|
|
515
901
|
const timeout = options?.timeout ?? this.config.timeout;
|
|
516
902
|
const located = await locateElement(this.session, this.contextId, target, { timeout });
|
|
903
|
+
await this.ensureActionable(located, "hover", target, { timeout });
|
|
517
904
|
const ref = this.getSharedRef(located.node);
|
|
518
905
|
await this.session.script.callFunction({
|
|
519
906
|
functionDeclaration: 'function(el) { el.scrollIntoView({ block: "center", behavior: "instant" }); }',
|
|
@@ -533,14 +920,14 @@ var Page = class {
|
|
|
533
920
|
const pos = await this.getElementCenter(ref);
|
|
534
921
|
await this.session.input.performActions({
|
|
535
922
|
context: this.contextId,
|
|
536
|
-
actions: [
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
{ type: "pointerMove", x: pos.x, y: pos.y, origin: "viewport" }
|
|
542
|
-
|
|
543
|
-
|
|
923
|
+
actions: [
|
|
924
|
+
{
|
|
925
|
+
type: "pointer",
|
|
926
|
+
id: "mouse",
|
|
927
|
+
parameters: { pointerType: "mouse" },
|
|
928
|
+
actions: [{ type: "pointerMove", x: pos.x, y: pos.y, origin: "viewport" }]
|
|
929
|
+
}
|
|
930
|
+
]
|
|
544
931
|
});
|
|
545
932
|
}
|
|
546
933
|
// -----------------------------------------------------------------------
|
|
@@ -750,10 +1137,7 @@ var Page = class {
|
|
|
750
1137
|
*/
|
|
751
1138
|
async mock(pattern, response) {
|
|
752
1139
|
const { method, urlPattern } = parseMockPattern(pattern);
|
|
753
|
-
await this.session.subscribe(
|
|
754
|
-
["network.beforeRequestSent"],
|
|
755
|
-
[this.contextId]
|
|
756
|
-
);
|
|
1140
|
+
await this.session.subscribe(["network.beforeRequestSent"], [this.contextId]);
|
|
757
1141
|
const result = await this.session.network.addIntercept({
|
|
758
1142
|
phases: ["beforeRequestSent"],
|
|
759
1143
|
urlPatterns: [urlPattern],
|
|
@@ -850,6 +1234,7 @@ var Page = class {
|
|
|
850
1234
|
async tap(target, options) {
|
|
851
1235
|
const timeout = options?.timeout ?? this.config.timeout;
|
|
852
1236
|
const located = await locateElement(this.session, this.contextId, target, { timeout });
|
|
1237
|
+
await this.ensureActionable(located, "tap", target, { timeout });
|
|
853
1238
|
const ref = this.getSharedRef(located.node);
|
|
854
1239
|
await this.session.script.callFunction({
|
|
855
1240
|
functionDeclaration: 'function(el) { el.scrollIntoView({ block: "center", behavior: "instant" }); }',
|
|
@@ -860,16 +1245,18 @@ var Page = class {
|
|
|
860
1245
|
const pos = await this.getElementCenter(ref);
|
|
861
1246
|
await this.session.input.performActions({
|
|
862
1247
|
context: this.contextId,
|
|
863
|
-
actions: [
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
1248
|
+
actions: [
|
|
1249
|
+
{
|
|
1250
|
+
type: "pointer",
|
|
1251
|
+
id: "touch",
|
|
1252
|
+
parameters: { pointerType: "touch" },
|
|
1253
|
+
actions: [
|
|
1254
|
+
{ type: "pointerMove", x: pos.x, y: pos.y, origin: "viewport" },
|
|
1255
|
+
{ type: "pointerDown", button: 0 },
|
|
1256
|
+
{ type: "pointerUp", button: 0 }
|
|
1257
|
+
]
|
|
1258
|
+
}
|
|
1259
|
+
]
|
|
873
1260
|
});
|
|
874
1261
|
}
|
|
875
1262
|
/**
|
|
@@ -883,6 +1270,7 @@ var Page = class {
|
|
|
883
1270
|
const timeout = options?.timeout ?? this.config.timeout;
|
|
884
1271
|
const resolvedTarget = typeof target === "string" ? { label: target, name: target } : target;
|
|
885
1272
|
const located = await locateElement(this.session, this.contextId, resolvedTarget, { timeout });
|
|
1273
|
+
await this.ensureActionable(located, "focus", target, { timeout, enabled: false });
|
|
886
1274
|
const ref = this.getSharedRef(located.node);
|
|
887
1275
|
await this.session.script.callFunction({
|
|
888
1276
|
functionDeclaration: "function(el) { el.focus(); }",
|
|
@@ -979,6 +1367,7 @@ var Page = class {
|
|
|
979
1367
|
const timeout = options?.timeout ?? this.config.timeout;
|
|
980
1368
|
const resolvedTarget = typeof target === "string" ? { label: target, name: target } : target;
|
|
981
1369
|
const located = await locateElement(this.session, this.contextId, resolvedTarget, { timeout });
|
|
1370
|
+
await this.ensureActionable(located, "selectOption", target, { timeout });
|
|
982
1371
|
const ref = this.getSharedRef(located.node);
|
|
983
1372
|
const valuesArray = Array.isArray(values) ? values : [values];
|
|
984
1373
|
await this.session.script.callFunction({
|
|
@@ -990,7 +1379,10 @@ var Page = class {
|
|
|
990
1379
|
element.dispatchEvent(new Event('change', { bubbles: true }));
|
|
991
1380
|
}`,
|
|
992
1381
|
target: { context: this.contextId },
|
|
993
|
-
arguments: [
|
|
1382
|
+
arguments: [
|
|
1383
|
+
ref,
|
|
1384
|
+
{ type: "array", value: valuesArray.map((v) => ({ type: "string", value: v })) }
|
|
1385
|
+
],
|
|
994
1386
|
awaitPromise: false
|
|
995
1387
|
});
|
|
996
1388
|
}
|
|
@@ -1011,17 +1403,19 @@ var Page = class {
|
|
|
1011
1403
|
const destPos = await this.getElementCenter(destRef);
|
|
1012
1404
|
await this.session.input.performActions({
|
|
1013
1405
|
context: this.contextId,
|
|
1014
|
-
actions: [
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1406
|
+
actions: [
|
|
1407
|
+
{
|
|
1408
|
+
type: "pointer",
|
|
1409
|
+
id: "mouse",
|
|
1410
|
+
parameters: { pointerType: "mouse" },
|
|
1411
|
+
actions: [
|
|
1412
|
+
{ type: "pointerMove", x: sourcePos.x, y: sourcePos.y, origin: "viewport" },
|
|
1413
|
+
{ type: "pointerDown", button: 0 },
|
|
1414
|
+
{ type: "pointerMove", x: destPos.x, y: destPos.y, origin: "viewport", duration: 300 },
|
|
1415
|
+
{ type: "pointerUp", button: 0 }
|
|
1416
|
+
]
|
|
1417
|
+
}
|
|
1418
|
+
]
|
|
1025
1419
|
});
|
|
1026
1420
|
}
|
|
1027
1421
|
// -----------------------------------------------------------------------
|
|
@@ -1040,7 +1434,7 @@ var Page = class {
|
|
|
1040
1434
|
const state = options?.state ?? "visible";
|
|
1041
1435
|
if (state === "hidden") {
|
|
1042
1436
|
await waitFor(
|
|
1043
|
-
|
|
1437
|
+
"element to be hidden",
|
|
1044
1438
|
async () => {
|
|
1045
1439
|
try {
|
|
1046
1440
|
const elements = await locateAllElements(this.session, this.contextId, target);
|
|
@@ -1069,7 +1463,7 @@ var Page = class {
|
|
|
1069
1463
|
if (state === "visible") {
|
|
1070
1464
|
const ref = this.getSharedRef(located.node);
|
|
1071
1465
|
await waitFor(
|
|
1072
|
-
|
|
1466
|
+
"element to be visible",
|
|
1073
1467
|
async () => {
|
|
1074
1468
|
const result = await this.session.script.callFunction({
|
|
1075
1469
|
functionDeclaration: `function(el) {
|
|
@@ -1140,6 +1534,185 @@ var Page = class {
|
|
|
1140
1534
|
await waitForLoadState(this.session, this.contextId, state, this.config.timeout);
|
|
1141
1535
|
}
|
|
1142
1536
|
// -----------------------------------------------------------------------
|
|
1537
|
+
// English API aliases — reads like instructions to a person
|
|
1538
|
+
// -----------------------------------------------------------------------
|
|
1539
|
+
/**
|
|
1540
|
+
* Navigate to a URL. English-friendly alias for goto().
|
|
1541
|
+
*
|
|
1542
|
+
* ```ts
|
|
1543
|
+
* await page.go('https://example.com');
|
|
1544
|
+
* await page.go('/login');
|
|
1545
|
+
* ```
|
|
1546
|
+
*/
|
|
1547
|
+
async go(url, options) {
|
|
1548
|
+
return this.goto(url, options);
|
|
1549
|
+
}
|
|
1550
|
+
/**
|
|
1551
|
+
* Assert that an element with the given text is visible on the page.
|
|
1552
|
+
* Returns the ElementHandle for further assertions.
|
|
1553
|
+
*
|
|
1554
|
+
* ```ts
|
|
1555
|
+
* await page.see('Welcome back!');
|
|
1556
|
+
* await page.see('Products');
|
|
1557
|
+
* await page.see({ role: 'heading', name: 'Dashboard' });
|
|
1558
|
+
* ```
|
|
1559
|
+
*/
|
|
1560
|
+
async see(target, options) {
|
|
1561
|
+
const timeout = options?.timeout ?? this.config.timeout;
|
|
1562
|
+
const startTime = Date.now();
|
|
1563
|
+
const located = await locateElement(this.session, this.contextId, target, { timeout });
|
|
1564
|
+
const ref = this.getSharedRef(located.node);
|
|
1565
|
+
const elapsed = Date.now() - startTime;
|
|
1566
|
+
const remaining = Math.max(timeout - elapsed, 5e3);
|
|
1567
|
+
await waitFor(
|
|
1568
|
+
`${describeTarget2(target)} to be visible`,
|
|
1569
|
+
async () => {
|
|
1570
|
+
try {
|
|
1571
|
+
const result = await this.session.script.callFunction({
|
|
1572
|
+
functionDeclaration: `function(el) {
|
|
1573
|
+
const style = window.getComputedStyle(el);
|
|
1574
|
+
const rect = el.getBoundingClientRect();
|
|
1575
|
+
return style.display !== 'none' && style.visibility !== 'hidden' &&
|
|
1576
|
+
style.opacity !== '0' && rect.width > 0 && rect.height > 0;
|
|
1577
|
+
}`,
|
|
1578
|
+
target: { context: this.contextId },
|
|
1579
|
+
arguments: [ref],
|
|
1580
|
+
awaitPromise: false
|
|
1581
|
+
});
|
|
1582
|
+
if (result.type === "success" && result.result?.type === "boolean" && result.result.value === true) {
|
|
1583
|
+
return true;
|
|
1584
|
+
}
|
|
1585
|
+
return null;
|
|
1586
|
+
} catch {
|
|
1587
|
+
return null;
|
|
1588
|
+
}
|
|
1589
|
+
},
|
|
1590
|
+
{ timeout: remaining }
|
|
1591
|
+
);
|
|
1592
|
+
return new ElementHandle(this, target);
|
|
1593
|
+
}
|
|
1594
|
+
// -----------------------------------------------------------------------
|
|
1595
|
+
// Network interception & observation
|
|
1596
|
+
// -----------------------------------------------------------------------
|
|
1597
|
+
/**
|
|
1598
|
+
* Intercept network requests matching a pattern. Calls your handler
|
|
1599
|
+
* for each matching request, letting you modify or respond to it.
|
|
1600
|
+
*
|
|
1601
|
+
* ```ts
|
|
1602
|
+
* await page.intercept('POST /api/login', async (request) => {
|
|
1603
|
+
* // Modify request, or provide a response
|
|
1604
|
+
* return { status: 200, body: { token: 'abc' } };
|
|
1605
|
+
* });
|
|
1606
|
+
* ```
|
|
1607
|
+
*/
|
|
1608
|
+
async intercept(pattern, handler) {
|
|
1609
|
+
const { method, urlPattern } = parseMockPattern(pattern);
|
|
1610
|
+
await this.session.subscribe(["network.beforeRequestSent"], [this.contextId]);
|
|
1611
|
+
const result = await this.session.network.addIntercept({
|
|
1612
|
+
phases: ["beforeRequestSent"],
|
|
1613
|
+
urlPatterns: [urlPattern],
|
|
1614
|
+
contexts: [this.contextId]
|
|
1615
|
+
});
|
|
1616
|
+
this.interceptIds.push(result.intercept);
|
|
1617
|
+
const unsubscribe = this.session.on("network.beforeRequestSent", async (event) => {
|
|
1618
|
+
const params = event.params;
|
|
1619
|
+
if (!params.isBlocked || params.context !== this.contextId) return;
|
|
1620
|
+
if (!params.intercepts?.includes(result.intercept)) return;
|
|
1621
|
+
if (method && params.request.method.toUpperCase() !== method.toUpperCase()) {
|
|
1622
|
+
await this.session.network.continueRequest({ request: params.request.request });
|
|
1623
|
+
return;
|
|
1624
|
+
}
|
|
1625
|
+
const reqInfo = {
|
|
1626
|
+
url: params.request.url,
|
|
1627
|
+
method: params.request.method,
|
|
1628
|
+
headers: Object.fromEntries(params.request.headers.map((h) => [h.name, h.value.value]))
|
|
1629
|
+
};
|
|
1630
|
+
try {
|
|
1631
|
+
const response = await handler(reqInfo);
|
|
1632
|
+
if (response) {
|
|
1633
|
+
const headers = buildMockHeaders(response);
|
|
1634
|
+
const body = buildMockBody(response);
|
|
1635
|
+
await this.session.network.provideResponse({
|
|
1636
|
+
request: params.request.request,
|
|
1637
|
+
statusCode: response.status ?? 200,
|
|
1638
|
+
headers,
|
|
1639
|
+
body: body ? { type: "string", value: body } : void 0
|
|
1640
|
+
});
|
|
1641
|
+
} else {
|
|
1642
|
+
await this.session.network.continueRequest({ request: params.request.request });
|
|
1643
|
+
}
|
|
1644
|
+
} catch {
|
|
1645
|
+
await this.session.network.continueRequest({ request: params.request.request });
|
|
1646
|
+
}
|
|
1647
|
+
});
|
|
1648
|
+
this.eventCleanups.push(unsubscribe);
|
|
1649
|
+
}
|
|
1650
|
+
/**
|
|
1651
|
+
* Wait for a network response matching a URL pattern.
|
|
1652
|
+
*
|
|
1653
|
+
* ```ts
|
|
1654
|
+
* const response = await page.waitForResponse('/api/users');
|
|
1655
|
+
* console.log(response.status); // 200
|
|
1656
|
+
* ```
|
|
1657
|
+
*/
|
|
1658
|
+
async waitForResponse(urlPattern, options) {
|
|
1659
|
+
const timeout = options?.timeout ?? this.config.timeout;
|
|
1660
|
+
const method = options?.method?.toUpperCase();
|
|
1661
|
+
await this.session.subscribe(["network.responseCompleted"], [this.contextId]);
|
|
1662
|
+
try {
|
|
1663
|
+
const event = await this.session.waitForEvent(
|
|
1664
|
+
"network.responseCompleted",
|
|
1665
|
+
(e) => {
|
|
1666
|
+
const params2 = e.params;
|
|
1667
|
+
if (params2.context !== this.contextId) return false;
|
|
1668
|
+
if (method && params2.request.method.toUpperCase() !== method) return false;
|
|
1669
|
+
const url = params2.request.url;
|
|
1670
|
+
if (typeof urlPattern === "string") {
|
|
1671
|
+
return url.includes(urlPattern);
|
|
1672
|
+
}
|
|
1673
|
+
return urlPattern.test(url);
|
|
1674
|
+
},
|
|
1675
|
+
timeout
|
|
1676
|
+
);
|
|
1677
|
+
const params = event.params;
|
|
1678
|
+
return {
|
|
1679
|
+
url: params.request.url,
|
|
1680
|
+
status: params.response.status,
|
|
1681
|
+
method: params.request.method
|
|
1682
|
+
};
|
|
1683
|
+
} finally {
|
|
1684
|
+
await this.session.unsubscribe(["network.responseCompleted"], [this.contextId]).catch(() => {
|
|
1685
|
+
});
|
|
1686
|
+
}
|
|
1687
|
+
}
|
|
1688
|
+
/**
|
|
1689
|
+
* Block network requests matching URL patterns (e.g., ads, analytics).
|
|
1690
|
+
*
|
|
1691
|
+
* ```ts
|
|
1692
|
+
* await page.blockRequests(['*.google-analytics.com*', '*.doubleclick.net*']);
|
|
1693
|
+
* await page.blockRequests(['/api/telemetry']);
|
|
1694
|
+
* ```
|
|
1695
|
+
*/
|
|
1696
|
+
async blockRequests(patterns) {
|
|
1697
|
+
await this.session.subscribe(["network.beforeRequestSent"], [this.contextId]);
|
|
1698
|
+
for (const pattern of patterns) {
|
|
1699
|
+
const urlPattern = pattern.startsWith("http") ? { type: "string", pattern } : { type: "pattern", pathname: pattern };
|
|
1700
|
+
const result = await this.session.network.addIntercept({
|
|
1701
|
+
phases: ["beforeRequestSent"],
|
|
1702
|
+
urlPatterns: [urlPattern],
|
|
1703
|
+
contexts: [this.contextId]
|
|
1704
|
+
});
|
|
1705
|
+
this.interceptIds.push(result.intercept);
|
|
1706
|
+
const unsubscribe = this.session.on("network.beforeRequestSent", async (event) => {
|
|
1707
|
+
const params = event.params;
|
|
1708
|
+
if (!params.isBlocked || params.context !== this.contextId) return;
|
|
1709
|
+
if (!params.intercepts?.includes(result.intercept)) return;
|
|
1710
|
+
await this.session.network.failRequest({ request: params.request.request });
|
|
1711
|
+
});
|
|
1712
|
+
this.eventCleanups.push(unsubscribe);
|
|
1713
|
+
}
|
|
1714
|
+
}
|
|
1715
|
+
// -----------------------------------------------------------------------
|
|
1143
1716
|
// Lifecycle
|
|
1144
1717
|
// -----------------------------------------------------------------------
|
|
1145
1718
|
/**
|
|
@@ -1177,6 +1750,34 @@ var Page = class {
|
|
|
1177
1750
|
// -----------------------------------------------------------------------
|
|
1178
1751
|
// Private helpers
|
|
1179
1752
|
// -----------------------------------------------------------------------
|
|
1753
|
+
/**
|
|
1754
|
+
* Ensure an element is actionable (visible + enabled) before interacting.
|
|
1755
|
+
* Throws a rich ElementNotActionableError if it's not ready within the timeout.
|
|
1756
|
+
*/
|
|
1757
|
+
async ensureActionable(located, action, target, options) {
|
|
1758
|
+
const ref = this.getSharedRef(located.node);
|
|
1759
|
+
const targetDesc = typeof target === "string" ? target : describeTarget2(target);
|
|
1760
|
+
const result = await waitForActionable(
|
|
1761
|
+
this.session,
|
|
1762
|
+
this.contextId,
|
|
1763
|
+
ref,
|
|
1764
|
+
targetDesc,
|
|
1765
|
+
{ timeout: Math.min(options.timeout, 5e3) },
|
|
1766
|
+
{
|
|
1767
|
+
visible: true,
|
|
1768
|
+
enabled: options.enabled !== false
|
|
1769
|
+
}
|
|
1770
|
+
);
|
|
1771
|
+
if (!result.actionable && result.reason) {
|
|
1772
|
+
throw new ElementNotActionableError({
|
|
1773
|
+
action,
|
|
1774
|
+
target: targetDesc,
|
|
1775
|
+
reason: result.reason,
|
|
1776
|
+
elementState: result.state,
|
|
1777
|
+
elapsed: options.timeout
|
|
1778
|
+
});
|
|
1779
|
+
}
|
|
1780
|
+
}
|
|
1180
1781
|
/** Resolve a relative URL against the baseURL */
|
|
1181
1782
|
resolveURL(url) {
|
|
1182
1783
|
if (url.startsWith("http://") || url.startsWith("https://") || url.startsWith("about:") || url.startsWith("data:")) {
|
|
@@ -1342,7 +1943,7 @@ var ElementHandle = class {
|
|
|
1342
1943
|
const located = await this.locate();
|
|
1343
1944
|
const ref = this.getRef(located);
|
|
1344
1945
|
const result = await this.page.session.script.callFunction({
|
|
1345
|
-
functionDeclaration:
|
|
1946
|
+
functionDeclaration: "function(el, name) { return el.getAttribute(name); }",
|
|
1346
1947
|
target: { context: this.page.contextId },
|
|
1347
1948
|
arguments: [ref, { type: "string", value: name }],
|
|
1348
1949
|
awaitPromise: false
|
|
@@ -1480,7 +2081,12 @@ var ElementHandle = class {
|
|
|
1480
2081
|
const v = map.get(key);
|
|
1481
2082
|
return typeof v === "object" && v !== null && "value" in v ? v.value : 0;
|
|
1482
2083
|
};
|
|
1483
|
-
return {
|
|
2084
|
+
return {
|
|
2085
|
+
x: extract("x"),
|
|
2086
|
+
y: extract("y"),
|
|
2087
|
+
width: extract("width"),
|
|
2088
|
+
height: extract("height")
|
|
2089
|
+
};
|
|
1484
2090
|
}
|
|
1485
2091
|
}
|
|
1486
2092
|
return null;
|
|
@@ -1536,12 +2142,9 @@ var ElementHandle = class {
|
|
|
1536
2142
|
}
|
|
1537
2143
|
/** @internal Locate the element with auto-wait */
|
|
1538
2144
|
async locate(timeout) {
|
|
1539
|
-
return locateElement(
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
this.target,
|
|
1543
|
-
{ timeout: timeout ?? 3e4 }
|
|
1544
|
-
);
|
|
2145
|
+
return locateElement(this.page.session, this.page.contextId, this.target, {
|
|
2146
|
+
timeout: timeout ?? 3e4
|
|
2147
|
+
});
|
|
1545
2148
|
}
|
|
1546
2149
|
getRef(located) {
|
|
1547
2150
|
if (located.node.sharedId) {
|
|
@@ -1550,6 +2153,17 @@ var ElementHandle = class {
|
|
|
1550
2153
|
throw new Error("Element has no shared reference");
|
|
1551
2154
|
}
|
|
1552
2155
|
};
|
|
2156
|
+
function describeTarget2(target) {
|
|
2157
|
+
if (typeof target === "string") return target;
|
|
2158
|
+
const parts = [];
|
|
2159
|
+
if (target.role) parts.push(`role="${target.role}"`);
|
|
2160
|
+
if (target.name) parts.push(`name="${target.name}"`);
|
|
2161
|
+
if (target.text) parts.push(`text="${target.text}"`);
|
|
2162
|
+
if (target.label) parts.push(`label="${target.label}"`);
|
|
2163
|
+
if (target.testId) parts.push(`testId="${target.testId}"`);
|
|
2164
|
+
if (target.selector) parts.push(`selector="${target.selector}"`);
|
|
2165
|
+
return `[${parts.join(", ")}]`;
|
|
2166
|
+
}
|
|
1553
2167
|
function mapKey(key) {
|
|
1554
2168
|
const keyMap = {
|
|
1555
2169
|
Enter: "\uE007",
|
|
@@ -1734,7 +2348,7 @@ var Browser = class _Browser {
|
|
|
1734
2348
|
return new BrowserContext(this.session, this.config, userContext);
|
|
1735
2349
|
} catch (err) {
|
|
1736
2350
|
console.warn(
|
|
1737
|
-
|
|
2351
|
+
`[browsecraft] Warning: browser.createUserContext is not supported. Pages will share cookies/storage. ${err instanceof Error ? err.message : String(err)}`
|
|
1738
2352
|
);
|
|
1739
2353
|
return new BrowserContext(this.session, this.config, null);
|
|
1740
2354
|
}
|
|
@@ -1926,7 +2540,9 @@ async function runTest(testCase, sharedBrowser, userConfig) {
|
|
|
1926
2540
|
}
|
|
1927
2541
|
let screenshotPath;
|
|
1928
2542
|
if (config.screenshot === "always" && page) {
|
|
1929
|
-
screenshotPath = await captureScreenshot(page, testCase, config.outputDir).catch(
|
|
2543
|
+
screenshotPath = await captureScreenshot(page, testCase, config.outputDir).catch(
|
|
2544
|
+
() => void 0
|
|
2545
|
+
);
|
|
1930
2546
|
}
|
|
1931
2547
|
const duration = Date.now() - startTime;
|
|
1932
2548
|
return {
|
|
@@ -1940,7 +2556,9 @@ async function runTest(testCase, sharedBrowser, userConfig) {
|
|
|
1940
2556
|
const duration = Date.now() - startTime;
|
|
1941
2557
|
let screenshotPath;
|
|
1942
2558
|
if ((config.screenshot === "on-failure" || config.screenshot === "always") && page) {
|
|
1943
|
-
screenshotPath = await captureScreenshot(page, testCase, config.outputDir).catch(
|
|
2559
|
+
screenshotPath = await captureScreenshot(page, testCase, config.outputDir).catch(
|
|
2560
|
+
() => void 0
|
|
2561
|
+
);
|
|
1944
2562
|
}
|
|
1945
2563
|
return {
|
|
1946
2564
|
title: testCase.title,
|
|
@@ -1960,9 +2578,7 @@ async function runTest(testCase, sharedBrowser, userConfig) {
|
|
|
1960
2578
|
}
|
|
1961
2579
|
}
|
|
1962
2580
|
async function runAfterAllHooks(suitePath, fixtures) {
|
|
1963
|
-
const applicableAfterAll = hooks.afterAll.filter(
|
|
1964
|
-
(h) => isHookApplicable(h.suitePath, suitePath)
|
|
1965
|
-
);
|
|
2581
|
+
const applicableAfterAll = hooks.afterAll.filter((h) => isHookApplicable(h.suitePath, suitePath));
|
|
1966
2582
|
for (const hook of applicableAfterAll) {
|
|
1967
2583
|
const hookKey = `${hook.suitePath.join(">")}:${hooks.afterAll.indexOf(hook)}`;
|
|
1968
2584
|
if (!executedAfterAllHooks.has(hookKey)) {
|
|
@@ -1999,6 +2615,6 @@ async function captureScreenshot(page, testCase, outputDir) {
|
|
|
1999
2615
|
return filePath;
|
|
2000
2616
|
}
|
|
2001
2617
|
|
|
2002
|
-
export { Browser, BrowserContext, ElementHandle, Page, afterAll, afterEach, beforeAll, beforeEach, defineConfig, describe, resetTestState, resolveConfig, runAfterAllHooks, runTest, test, testRegistry };
|
|
2003
|
-
//# sourceMappingURL=chunk-
|
|
2004
|
-
//# sourceMappingURL=chunk-
|
|
2618
|
+
export { BrowsecraftError, Browser, BrowserContext, ElementHandle, ElementNotActionableError, ElementNotFoundError, NetworkError, Page, TimeoutError, afterAll, afterEach, beforeAll, beforeEach, defineConfig, describe, resetTestState, resolveConfig, runAfterAllHooks, runTest, test, testRegistry };
|
|
2619
|
+
//# sourceMappingURL=chunk-3OBA6D2X.js.map
|
|
2620
|
+
//# sourceMappingURL=chunk-3OBA6D2X.js.map
|