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.
@@ -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
- return waitFor(
93
- describeTarget(target),
94
- async () => {
95
- const strategies = buildStrategies(opts);
96
- for (const { locator, strategy, isLabelLookup } of strategies) {
97
- try {
98
- const result = await session.browsingContext.locateNodes({
99
- context: contextId,
100
- locator,
101
- maxNodeCount: (opts.index ?? 0) + 10
102
- // fetch extra for label resolution
103
- });
104
- if (result.nodes.length > 0) {
105
- if (isLabelLookup) {
106
- const resolved = await resolveLabelsToInputs(session, contextId, result.nodes);
107
- if (resolved) {
108
- return { node: resolved, strategy };
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
- return null;
123
- },
124
- options
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
- if (result.nodes.length > 0) {
138
- return result.nodes.map((node) => ({ node, strategy }));
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
- type: "pointer",
538
- id: "mouse",
539
- parameters: { pointerType: "mouse" },
540
- actions: [
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
- type: "pointer",
865
- id: "touch",
866
- parameters: { pointerType: "touch" },
867
- actions: [
868
- { type: "pointerMove", x: pos.x, y: pos.y, origin: "viewport" },
869
- { type: "pointerDown", button: 0 },
870
- { type: "pointerUp", button: 0 }
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: [ref, { type: "array", value: valuesArray.map((v) => ({ type: "string", value: v })) }],
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
- type: "pointer",
1016
- id: "mouse",
1017
- parameters: { pointerType: "mouse" },
1018
- actions: [
1019
- { type: "pointerMove", x: sourcePos.x, y: sourcePos.y, origin: "viewport" },
1020
- { type: "pointerDown", button: 0 },
1021
- { type: "pointerMove", x: destPos.x, y: destPos.y, origin: "viewport", duration: 300 },
1022
- { type: "pointerUp", button: 0 }
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
- `element to be hidden`,
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
- `element to be visible`,
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: `function(el, name) { return el.getAttribute(name); }`,
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 { x: extract("x"), y: extract("y"), width: extract("width"), height: extract("height") };
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
- this.page.session,
1541
- this.page.contextId,
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
- "[browsecraft] Warning: browser.createUserContext is not supported. Pages will share cookies/storage. " + (err instanceof Error ? err.message : String(err))
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(() => void 0);
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(() => void 0);
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-77HRTGXZ.js.map
2004
- //# sourceMappingURL=chunk-77HRTGXZ.js.map
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