browsecraft 0.1.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.
@@ -0,0 +1,2021 @@
1
+ 'use strict';
2
+
3
+ var os = require('os');
4
+ var browsecraftBidi = require('browsecraft-bidi');
5
+ var promises = require('fs/promises');
6
+ var path = require('path');
7
+
8
+ // src/config.ts
9
+ var DEFAULTS = {
10
+ browser: "chrome",
11
+ headless: true,
12
+ timeout: 3e4,
13
+ retries: 0,
14
+ screenshot: "on-failure",
15
+ baseURL: "",
16
+ viewport: { width: 1280, height: 720 },
17
+ maximized: false,
18
+ workers: Math.max(1, Math.floor((typeof os.cpus === "function" ? os.cpus().length : 4) / 2)),
19
+ testMatch: "**/*.test.{ts,js,mts,mjs}",
20
+ outputDir: ".browsecraft",
21
+ ai: "auto",
22
+ debug: false
23
+ };
24
+ function defineConfig(config) {
25
+ return config;
26
+ }
27
+ function resolveConfig(userConfig) {
28
+ if (!userConfig) return { ...DEFAULTS };
29
+ return {
30
+ ...DEFAULTS,
31
+ ...userConfig,
32
+ viewport: userConfig.viewport ?? DEFAULTS.viewport
33
+ };
34
+ }
35
+
36
+ // src/wait.ts
37
+ async function waitFor(description, fn, options) {
38
+ const { timeout, interval = 100 } = options;
39
+ const startTime = Date.now();
40
+ let lastError = null;
41
+ while (Date.now() - startTime < timeout) {
42
+ try {
43
+ const result = await fn();
44
+ if (result !== null && result !== false) {
45
+ return result;
46
+ }
47
+ } catch (err) {
48
+ lastError = err instanceof Error ? err : new Error(String(err));
49
+ }
50
+ await sleep(interval);
51
+ }
52
+ const elapsed = Date.now() - startTime;
53
+ const errorMsg = lastError ? `
54
+ Last error: ${lastError.message}` : "";
55
+ throw new Error(
56
+ `Timed out after ${elapsed}ms waiting for: ${description}${errorMsg}`
57
+ );
58
+ }
59
+ async function waitForLoadState(session, contextId, state = "load", timeout = 3e4) {
60
+ const result = await session.script.evaluate({
61
+ expression: "document.readyState",
62
+ target: { context: contextId },
63
+ awaitPromise: false
64
+ });
65
+ if (result.type === "success" && result.result) {
66
+ const readyState = result.result.value;
67
+ if (state === "domcontentloaded" && (readyState === "interactive" || readyState === "complete")) {
68
+ return;
69
+ }
70
+ if (state === "load" && readyState === "complete") {
71
+ return;
72
+ }
73
+ }
74
+ const eventName = state === "load" ? "browsingContext.load" : "browsingContext.domContentLoaded";
75
+ await session.subscribe([eventName], [contextId]);
76
+ try {
77
+ await session.waitForEvent(
78
+ eventName,
79
+ (event) => event.params.context === contextId,
80
+ timeout
81
+ );
82
+ } finally {
83
+ await session.unsubscribe([eventName], [contextId]).catch(() => {
84
+ });
85
+ }
86
+ }
87
+ function sleep(ms) {
88
+ return new Promise((resolve) => setTimeout(resolve, ms));
89
+ }
90
+
91
+ // src/locator.ts
92
+ async function locateElement(session, contextId, target, options) {
93
+ const opts = normalizeTarget(target);
94
+ return waitFor(
95
+ describeTarget(target),
96
+ async () => {
97
+ const strategies = buildStrategies(opts);
98
+ for (const { locator, strategy, isLabelLookup } of strategies) {
99
+ try {
100
+ const result = await session.browsingContext.locateNodes({
101
+ context: contextId,
102
+ locator,
103
+ maxNodeCount: (opts.index ?? 0) + 10
104
+ // fetch extra for label resolution
105
+ });
106
+ if (result.nodes.length > 0) {
107
+ if (isLabelLookup) {
108
+ const resolved = await resolveLabelsToInputs(session, contextId, result.nodes);
109
+ if (resolved) {
110
+ return { node: resolved, strategy };
111
+ }
112
+ continue;
113
+ }
114
+ const nodeIndex = opts.index ?? 0;
115
+ const node = result.nodes[nodeIndex];
116
+ if (node) {
117
+ return { node, strategy };
118
+ }
119
+ }
120
+ } catch {
121
+ continue;
122
+ }
123
+ }
124
+ return null;
125
+ },
126
+ options
127
+ );
128
+ }
129
+ async function locateAllElements(session, contextId, target) {
130
+ const opts = normalizeTarget(target);
131
+ const strategies = buildStrategies(opts);
132
+ for (const { locator, strategy } of strategies) {
133
+ try {
134
+ const result = await session.browsingContext.locateNodes({
135
+ context: contextId,
136
+ locator,
137
+ maxNodeCount: 1e3
138
+ });
139
+ if (result.nodes.length > 0) {
140
+ return result.nodes.map((node) => ({ node, strategy }));
141
+ }
142
+ } catch {
143
+ continue;
144
+ }
145
+ }
146
+ return [];
147
+ }
148
+ function normalizeTarget(target) {
149
+ if (typeof target === "string") {
150
+ return { name: target };
151
+ }
152
+ return target;
153
+ }
154
+ function describeTarget(target) {
155
+ if (typeof target === "string") {
156
+ return `element "${target}"`;
157
+ }
158
+ const parts = [];
159
+ if (target.role) parts.push(`role="${target.role}"`);
160
+ if (target.name) parts.push(`name="${target.name}"`);
161
+ if (target.text) parts.push(`text="${target.text}"`);
162
+ if (target.label) parts.push(`label="${target.label}"`);
163
+ if (target.testId) parts.push(`testId="${target.testId}"`);
164
+ if (target.selector) parts.push(`selector="${target.selector}"`);
165
+ return `element [${parts.join(", ")}]`;
166
+ }
167
+ function buildStrategies(opts) {
168
+ const strategies = [];
169
+ const matchType = opts.exact ? "full" : "partial";
170
+ if (opts.selector) {
171
+ strategies.push({
172
+ locator: { type: "css", value: opts.selector },
173
+ strategy: "css"
174
+ });
175
+ return strategies;
176
+ }
177
+ if (opts.testId) {
178
+ strategies.push({
179
+ locator: { type: "css", value: `[data-testid="${opts.testId}"]` },
180
+ strategy: "testId"
181
+ });
182
+ return strategies;
183
+ }
184
+ const name = opts.name ?? opts.text;
185
+ if (name) {
186
+ if (opts.role) {
187
+ strategies.push({
188
+ locator: {
189
+ type: "accessibility",
190
+ value: { role: opts.role, name }
191
+ },
192
+ strategy: `accessibility[role="${opts.role}", name="${name}"]`
193
+ });
194
+ } else {
195
+ for (const role of ["button", "link", "menuitem", "tab"]) {
196
+ strategies.push({
197
+ locator: {
198
+ type: "accessibility",
199
+ value: { role, name }
200
+ },
201
+ strategy: `accessibility[role="${role}", name="${name}"]`
202
+ });
203
+ }
204
+ }
205
+ strategies.push({
206
+ locator: {
207
+ type: "innerText",
208
+ value: name,
209
+ matchType,
210
+ ignoreCase: !opts.exact
211
+ },
212
+ strategy: `innerText("${name}")`
213
+ });
214
+ if (name.match(/^[#.\[a-z]/i)) {
215
+ strategies.push({
216
+ locator: { type: "css", value: name },
217
+ strategy: `css("${name}")`
218
+ });
219
+ }
220
+ }
221
+ if (opts.label) {
222
+ strategies.push({
223
+ locator: {
224
+ type: "accessibility",
225
+ value: { name: opts.label }
226
+ },
227
+ strategy: `label("${opts.label}")`
228
+ });
229
+ strategies.push({
230
+ locator: {
231
+ type: "css",
232
+ value: `[aria-label="${opts.label}"], [placeholder="${opts.label}"]`
233
+ },
234
+ strategy: `label-css("${opts.label}")`
235
+ });
236
+ strategies.push({
237
+ locator: {
238
+ type: "innerText",
239
+ value: opts.label,
240
+ matchType,
241
+ ignoreCase: !opts.exact
242
+ },
243
+ strategy: `label-text("${opts.label}")`,
244
+ isLabelLookup: true
245
+ });
246
+ }
247
+ if (opts.role && !name) {
248
+ strategies.push({
249
+ locator: {
250
+ type: "accessibility",
251
+ value: { role: opts.role }
252
+ },
253
+ strategy: `role("${opts.role}")`
254
+ });
255
+ }
256
+ return strategies;
257
+ }
258
+ async function resolveLabelsToInputs(session, contextId, nodes) {
259
+ for (const node of nodes) {
260
+ if (!node.sharedId) continue;
261
+ try {
262
+ const result = await session.script.callFunction({
263
+ functionDeclaration: `function(el) {
264
+ // If the element is a <label> with a 'for' attribute, find the associated input
265
+ if (el.tagName === 'LABEL') {
266
+ const forId = el.getAttribute('for');
267
+ if (forId) {
268
+ const input = document.getElementById(forId);
269
+ if (input) return input;
270
+ }
271
+ // Also check for implicit label association (input nested inside label)
272
+ const nested = el.querySelector('input, textarea, select');
273
+ if (nested) return nested;
274
+ }
275
+ return null;
276
+ }`,
277
+ target: { context: contextId },
278
+ arguments: [{ sharedId: node.sharedId, handle: node.handle }],
279
+ awaitPromise: false,
280
+ resultOwnership: "root"
281
+ });
282
+ if (result.type === "success" && result.result?.type === "node" && result.result.sharedId) {
283
+ return result.result;
284
+ }
285
+ } catch {
286
+ continue;
287
+ }
288
+ }
289
+ return null;
290
+ }
291
+
292
+ // src/page.ts
293
+ var Page = class {
294
+ /** @internal */
295
+ session;
296
+ /** @internal */
297
+ contextId;
298
+ /** @internal */
299
+ config;
300
+ /** @internal */
301
+ interceptIds = [];
302
+ /** @internal -- event listener unsubscribe functions for cleanup */
303
+ eventCleanups = [];
304
+ constructor(session, contextId, config) {
305
+ this.session = session;
306
+ this.contextId = contextId;
307
+ this.config = config;
308
+ }
309
+ // -----------------------------------------------------------------------
310
+ // Navigation
311
+ // -----------------------------------------------------------------------
312
+ /**
313
+ * Navigate to a URL.
314
+ *
315
+ * ```ts
316
+ * await page.goto('https://example.com');
317
+ * await page.goto('/login'); // uses baseURL from config
318
+ * ```
319
+ */
320
+ async goto(url, options) {
321
+ const fullUrl = this.resolveURL(url);
322
+ const waitUntil = options?.waitUntil ?? "complete";
323
+ await this.session.browsingContext.navigate({
324
+ context: this.contextId,
325
+ url: fullUrl,
326
+ wait: waitUntil
327
+ });
328
+ }
329
+ /**
330
+ * Reload the current page.
331
+ */
332
+ async reload(options) {
333
+ await this.session.browsingContext.reload({
334
+ context: this.contextId,
335
+ wait: options?.waitUntil ?? "complete"
336
+ });
337
+ }
338
+ /**
339
+ * Go back in browser history.
340
+ */
341
+ async goBack() {
342
+ await this.session.browsingContext.traverseHistory({
343
+ context: this.contextId,
344
+ delta: -1
345
+ });
346
+ }
347
+ /**
348
+ * Go forward in browser history.
349
+ */
350
+ async goForward() {
351
+ await this.session.browsingContext.traverseHistory({
352
+ context: this.contextId,
353
+ delta: 1
354
+ });
355
+ }
356
+ // -----------------------------------------------------------------------
357
+ // Element interaction - the "stupidly simple" API
358
+ // -----------------------------------------------------------------------
359
+ /**
360
+ * Click an element. Finds it by text, role, or selector -- auto-waits.
361
+ *
362
+ * ```ts
363
+ * await page.click('Submit'); // by text
364
+ * await page.click({ role: 'button', name: 'Submit' }); // precise
365
+ * await page.click({ selector: '#my-button' }); // CSS
366
+ * ```
367
+ */
368
+ async click(target, options) {
369
+ const timeout = options?.timeout ?? this.config.timeout;
370
+ const located = await locateElement(this.session, this.contextId, target, { timeout });
371
+ await this.scrollIntoViewAndClick(located, options);
372
+ }
373
+ /**
374
+ * Fill a text input. First arg finds the input, second is the value.
375
+ *
376
+ * ```ts
377
+ * await page.fill('Email', 'user@test.com'); // by label
378
+ * await page.fill('Search', 'browsecraft'); // by placeholder
379
+ * await page.fill({ selector: '#email' }, 'test'); // by CSS
380
+ * ```
381
+ */
382
+ async fill(target, value, options) {
383
+ const timeout = options?.timeout ?? this.config.timeout;
384
+ const resolvedTarget = typeof target === "string" ? { label: target, name: target } : target;
385
+ const located = await locateElement(this.session, this.contextId, resolvedTarget, { timeout });
386
+ const ref = this.getSharedRef(located.node);
387
+ await this.session.script.callFunction({
388
+ functionDeclaration: `function(element, value) {
389
+ element.focus();
390
+ // Use native setter to bypass React/Vue/Angular internal value trackers.
391
+ // Frameworks like React override the value property on input elements,
392
+ // so setting element.value directly doesn't trigger their state updates.
393
+ const proto = element.tagName === 'TEXTAREA'
394
+ ? HTMLTextAreaElement.prototype
395
+ : HTMLInputElement.prototype;
396
+ const nativeSetter = Object.getOwnPropertyDescriptor(proto, 'value')?.set;
397
+ if (nativeSetter) {
398
+ nativeSetter.call(element, '');
399
+ nativeSetter.call(element, value);
400
+ } else {
401
+ element.value = '';
402
+ element.value = value;
403
+ }
404
+ element.dispatchEvent(new Event('input', { bubbles: true }));
405
+ element.dispatchEvent(new Event('change', { bubbles: true }));
406
+ }`,
407
+ target: { context: this.contextId },
408
+ arguments: [ref, { type: "string", value }],
409
+ awaitPromise: false
410
+ });
411
+ }
412
+ /**
413
+ * Type text character by character (triggers keyboard events).
414
+ * Use this instead of fill() when you need realistic keyboard input.
415
+ *
416
+ * ```ts
417
+ * await page.type('Search', 'browsecraft');
418
+ * ```
419
+ */
420
+ async type(target, text, options) {
421
+ const timeout = options?.timeout ?? this.config.timeout;
422
+ const resolvedTarget = typeof target === "string" ? { label: target, name: target } : target;
423
+ const located = await locateElement(this.session, this.contextId, resolvedTarget, { timeout });
424
+ const ref = this.getSharedRef(located.node);
425
+ await this.session.script.callFunction({
426
+ functionDeclaration: "function(el) { el.focus(); }",
427
+ target: { context: this.contextId },
428
+ arguments: [ref],
429
+ awaitPromise: false
430
+ });
431
+ const actions = [];
432
+ for (const char of text) {
433
+ actions.push({ type: "keyDown", value: char });
434
+ actions.push({ type: "keyUp", value: char });
435
+ }
436
+ await this.session.input.performActions({
437
+ context: this.contextId,
438
+ actions: [{ type: "key", id: "keyboard", actions }]
439
+ });
440
+ }
441
+ /**
442
+ * Select an option from a <select> dropdown.
443
+ *
444
+ * ```ts
445
+ * await page.select('Country', 'United States');
446
+ * ```
447
+ */
448
+ async select(target, value, options) {
449
+ const timeout = options?.timeout ?? this.config.timeout;
450
+ const resolvedTarget = typeof target === "string" ? { label: target, name: target } : target;
451
+ const located = await locateElement(this.session, this.contextId, resolvedTarget, { timeout });
452
+ const ref = this.getSharedRef(located.node);
453
+ await this.session.script.callFunction({
454
+ functionDeclaration: `function(element, value) {
455
+ const options = Array.from(element.options);
456
+ const option = options.find(o => o.value === value || o.text === value || o.textContent.trim() === value);
457
+ if (option) {
458
+ element.value = option.value;
459
+ element.dispatchEvent(new Event('change', { bubbles: true }));
460
+ } else {
461
+ throw new Error('Option "' + value + '" not found in <select>');
462
+ }
463
+ }`,
464
+ target: { context: this.contextId },
465
+ arguments: [ref, { type: "string", value }],
466
+ awaitPromise: false
467
+ });
468
+ }
469
+ /**
470
+ * Check a checkbox or radio button.
471
+ *
472
+ * ```ts
473
+ * await page.check('I agree to the terms');
474
+ * ```
475
+ */
476
+ async check(target, options) {
477
+ const timeout = options?.timeout ?? this.config.timeout;
478
+ const located = await locateElement(this.session, this.contextId, target, { timeout });
479
+ const ref = this.getSharedRef(located.node);
480
+ const result = await this.session.script.callFunction({
481
+ functionDeclaration: "function(el) { return el.checked; }",
482
+ target: { context: this.contextId },
483
+ arguments: [ref],
484
+ awaitPromise: false
485
+ });
486
+ const isChecked = result.type === "success" && result.result?.type === "boolean" && result.result.value === true;
487
+ if (!isChecked) {
488
+ await this.scrollIntoViewAndClick(located, options);
489
+ }
490
+ }
491
+ /**
492
+ * Uncheck a checkbox.
493
+ */
494
+ async uncheck(target, options) {
495
+ const timeout = options?.timeout ?? this.config.timeout;
496
+ const located = await locateElement(this.session, this.contextId, target, { timeout });
497
+ const ref = this.getSharedRef(located.node);
498
+ const result = await this.session.script.callFunction({
499
+ functionDeclaration: "function(el) { return el.checked; }",
500
+ target: { context: this.contextId },
501
+ arguments: [ref],
502
+ awaitPromise: false
503
+ });
504
+ const isChecked = result.type === "success" && result.result?.type === "boolean" && result.result.value === true;
505
+ if (isChecked) {
506
+ await this.scrollIntoViewAndClick(located, options);
507
+ }
508
+ }
509
+ /**
510
+ * Hover over an element.
511
+ *
512
+ * ```ts
513
+ * await page.hover('Profile Menu');
514
+ * ```
515
+ */
516
+ async hover(target, options) {
517
+ const timeout = options?.timeout ?? this.config.timeout;
518
+ const located = await locateElement(this.session, this.contextId, target, { timeout });
519
+ const ref = this.getSharedRef(located.node);
520
+ await this.session.script.callFunction({
521
+ functionDeclaration: 'function(el) { el.scrollIntoView({ block: "center", behavior: "instant" }); }',
522
+ target: { context: this.contextId },
523
+ arguments: [ref],
524
+ awaitPromise: false
525
+ });
526
+ await this.session.script.callFunction({
527
+ functionDeclaration: `function(el) {
528
+ el.dispatchEvent(new MouseEvent('mouseenter', { bubbles: true }));
529
+ el.dispatchEvent(new MouseEvent('mouseover', { bubbles: true }));
530
+ }`,
531
+ target: { context: this.contextId },
532
+ arguments: [ref],
533
+ awaitPromise: false
534
+ });
535
+ const pos = await this.getElementCenter(ref);
536
+ await this.session.input.performActions({
537
+ context: this.contextId,
538
+ actions: [{
539
+ type: "pointer",
540
+ id: "mouse",
541
+ parameters: { pointerType: "mouse" },
542
+ actions: [
543
+ { type: "pointerMove", x: pos.x, y: pos.y, origin: "viewport" }
544
+ ]
545
+ }]
546
+ });
547
+ }
548
+ // -----------------------------------------------------------------------
549
+ // Finding elements (for assertions and inspection)
550
+ // -----------------------------------------------------------------------
551
+ /**
552
+ * Get a single element. Returns an ElementHandle for assertions.
553
+ *
554
+ * ```ts
555
+ * const heading = page.get('Welcome back!');
556
+ * await expect(heading).toBeVisible();
557
+ * ```
558
+ */
559
+ get(target) {
560
+ return new ElementHandle(this, target);
561
+ }
562
+ /**
563
+ * Get a locator by visible text. Alias for get() with text matching.
564
+ *
565
+ * ```ts
566
+ * const btn = page.getByText('Submit');
567
+ * ```
568
+ */
569
+ getByText(text, options) {
570
+ return new ElementHandle(this, { text, exact: options?.exact });
571
+ }
572
+ /**
573
+ * Get a locator by ARIA role and optional name.
574
+ *
575
+ * ```ts
576
+ * const btn = page.getByRole('button', { name: 'Submit' });
577
+ * ```
578
+ */
579
+ getByRole(role, options) {
580
+ return new ElementHandle(this, { role, name: options?.name, exact: options?.exact });
581
+ }
582
+ /**
583
+ * Get a locator by label text (for form inputs).
584
+ *
585
+ * ```ts
586
+ * const email = page.getByLabel('Email Address');
587
+ * ```
588
+ */
589
+ getByLabel(label, options) {
590
+ return new ElementHandle(this, { label, exact: options?.exact });
591
+ }
592
+ /**
593
+ * Get a locator by data-testid attribute.
594
+ *
595
+ * ```ts
596
+ * const card = page.getByTestId('user-card');
597
+ * ```
598
+ */
599
+ getByTestId(testId) {
600
+ return new ElementHandle(this, { testId });
601
+ }
602
+ // -----------------------------------------------------------------------
603
+ // Page state
604
+ // -----------------------------------------------------------------------
605
+ /** Get the current page URL */
606
+ async url() {
607
+ const result = await this.session.script.evaluate({
608
+ expression: "window.location.href",
609
+ target: { context: this.contextId },
610
+ awaitPromise: false
611
+ });
612
+ return this.extractStringResult(result) ?? "";
613
+ }
614
+ /** Get the page title */
615
+ async title() {
616
+ const result = await this.session.script.evaluate({
617
+ expression: "document.title",
618
+ target: { context: this.contextId },
619
+ awaitPromise: false
620
+ });
621
+ return this.extractStringResult(result) ?? "";
622
+ }
623
+ /** Get the full page HTML content */
624
+ async content() {
625
+ const result = await this.session.script.evaluate({
626
+ expression: "document.documentElement.outerHTML",
627
+ target: { context: this.contextId },
628
+ awaitPromise: false
629
+ });
630
+ return this.extractStringResult(result) ?? "";
631
+ }
632
+ // -----------------------------------------------------------------------
633
+ // Cookies
634
+ // -----------------------------------------------------------------------
635
+ /**
636
+ * Get cookies for the current page's browsing context.
637
+ *
638
+ * ```ts
639
+ * const cookies = await page.cookies();
640
+ * const session = cookies.find(c => c.name === 'session_id');
641
+ * ```
642
+ */
643
+ async cookies(filter) {
644
+ const result = await this.session.storage.getCookies({
645
+ filter: filter ? {
646
+ name: filter.name,
647
+ domain: filter.domain,
648
+ path: filter.path
649
+ } : void 0,
650
+ partition: { type: "context", context: this.contextId }
651
+ });
652
+ return result.cookies;
653
+ }
654
+ /**
655
+ * Set one or more cookies.
656
+ *
657
+ * ```ts
658
+ * await page.setCookies([
659
+ * { name: 'token', value: 'abc123', domain: 'example.com' },
660
+ * { name: 'theme', value: 'dark', domain: 'example.com' },
661
+ * ]);
662
+ * ```
663
+ */
664
+ async setCookies(cookies) {
665
+ for (const cookie of cookies) {
666
+ const cookieHeader = {
667
+ name: cookie.name,
668
+ value: { type: "string", value: cookie.value },
669
+ domain: cookie.domain,
670
+ path: cookie.path,
671
+ httpOnly: cookie.httpOnly,
672
+ secure: cookie.secure,
673
+ sameSite: cookie.sameSite,
674
+ expiry: cookie.expiry
675
+ };
676
+ await this.session.storage.setCookie({
677
+ cookie: cookieHeader,
678
+ partition: { type: "context", context: this.contextId }
679
+ });
680
+ }
681
+ }
682
+ /**
683
+ * Clear all cookies (or those matching a filter).
684
+ *
685
+ * ```ts
686
+ * await page.clearCookies(); // all cookies
687
+ * await page.clearCookies({ name: 'session_id' }); // specific cookie
688
+ * await page.clearCookies({ domain: 'example.com' }); // by domain
689
+ * ```
690
+ */
691
+ async clearCookies(filter) {
692
+ await this.session.storage.deleteCookies({
693
+ filter: filter ? {
694
+ name: filter.name,
695
+ domain: filter.domain,
696
+ path: filter.path
697
+ } : void 0,
698
+ partition: { type: "context", context: this.contextId }
699
+ });
700
+ }
701
+ // -----------------------------------------------------------------------
702
+ // JavaScript execution
703
+ // -----------------------------------------------------------------------
704
+ /**
705
+ * Execute JavaScript in the page and return the result.
706
+ *
707
+ * ```ts
708
+ * const title = await page.evaluate('document.title');
709
+ * const count = await page.evaluate(() => document.querySelectorAll('li').length);
710
+ * ```
711
+ */
712
+ async evaluate(expression) {
713
+ const expr = typeof expression === "function" ? `(${expression.toString()})()` : expression;
714
+ const result = await this.session.script.evaluate({
715
+ expression: expr,
716
+ target: { context: this.contextId },
717
+ awaitPromise: true
718
+ });
719
+ if (result.type === "exception") {
720
+ const errorText = result.exceptionDetails?.text ?? "Script evaluation failed";
721
+ throw new Error(errorText);
722
+ }
723
+ return this.deserializeRemoteValue(result.result);
724
+ }
725
+ // -----------------------------------------------------------------------
726
+ // Screenshots
727
+ // -----------------------------------------------------------------------
728
+ /**
729
+ * Take a screenshot of the page.
730
+ *
731
+ * ```ts
732
+ * const buffer = await page.screenshot();
733
+ * ```
734
+ */
735
+ async screenshot() {
736
+ const result = await this.session.browsingContext.captureScreenshot({
737
+ context: this.contextId
738
+ });
739
+ return Buffer.from(result.data, "base64");
740
+ }
741
+ // -----------------------------------------------------------------------
742
+ // Network mocking
743
+ // -----------------------------------------------------------------------
744
+ /**
745
+ * Mock a network request with a fake response.
746
+ *
747
+ * ```ts
748
+ * await page.mock('POST /api/login', { status: 200, body: { token: 'abc' } });
749
+ * await page.mock('GET /api/users', { status: 500 });
750
+ * await page.mock('https://api.example.com/data', { body: { items: [] } });
751
+ * ```
752
+ */
753
+ async mock(pattern, response) {
754
+ const { method, urlPattern } = parseMockPattern(pattern);
755
+ await this.session.subscribe(
756
+ ["network.beforeRequestSent"],
757
+ [this.contextId]
758
+ );
759
+ const result = await this.session.network.addIntercept({
760
+ phases: ["beforeRequestSent"],
761
+ urlPatterns: [urlPattern],
762
+ contexts: [this.contextId]
763
+ });
764
+ this.interceptIds.push(result.intercept);
765
+ const unsubscribe = this.session.on("network.beforeRequestSent", async (event) => {
766
+ const params = event.params;
767
+ if (!params.isBlocked || params.context !== this.contextId) return;
768
+ if (!params.intercepts?.includes(result.intercept)) return;
769
+ if (method && params.request.method.toUpperCase() !== method.toUpperCase()) {
770
+ await this.session.network.continueRequest({ request: params.request.request });
771
+ return;
772
+ }
773
+ const headers = buildMockHeaders(response);
774
+ const body = buildMockBody(response);
775
+ await this.session.network.provideResponse({
776
+ request: params.request.request,
777
+ statusCode: response.status ?? 200,
778
+ headers,
779
+ body: body ? { type: "string", value: body } : void 0
780
+ });
781
+ });
782
+ this.eventCleanups.push(unsubscribe);
783
+ }
784
+ /**
785
+ * Remove all network mocks and clean up event listeners.
786
+ */
787
+ async clearMocks() {
788
+ for (const id of this.interceptIds) {
789
+ await this.session.network.removeIntercept({ intercept: id }).catch(() => {
790
+ });
791
+ }
792
+ this.interceptIds = [];
793
+ for (const cleanup of this.eventCleanups) {
794
+ cleanup();
795
+ }
796
+ this.eventCleanups = [];
797
+ }
798
+ // -----------------------------------------------------------------------
799
+ // Dialog handling
800
+ // -----------------------------------------------------------------------
801
+ /**
802
+ * Accept the next dialog (alert, confirm, prompt).
803
+ *
804
+ * ```ts
805
+ * await page.acceptDialog();
806
+ * page.click('Delete'); // triggers confirm dialog
807
+ * ```
808
+ */
809
+ async acceptDialog(text) {
810
+ await this.session.subscribe(["browsingContext.userPromptOpened"], [this.contextId]);
811
+ await this.session.waitForEvent(
812
+ "browsingContext.userPromptOpened",
813
+ (e) => e.params.context === this.contextId
814
+ );
815
+ await this.session.browsingContext.handleUserPrompt({
816
+ context: this.contextId,
817
+ accept: true,
818
+ userText: text
819
+ });
820
+ }
821
+ /**
822
+ * Dismiss the next dialog.
823
+ */
824
+ async dismissDialog() {
825
+ await this.session.subscribe(["browsingContext.userPromptOpened"], [this.contextId]);
826
+ await this.session.waitForEvent(
827
+ "browsingContext.userPromptOpened",
828
+ (e) => e.params.context === this.contextId
829
+ );
830
+ await this.session.browsingContext.handleUserPrompt({
831
+ context: this.contextId,
832
+ accept: false
833
+ });
834
+ }
835
+ /**
836
+ * Double-click an element.
837
+ *
838
+ * ```ts
839
+ * await page.dblclick('Edit');
840
+ * ```
841
+ */
842
+ async dblclick(target, options) {
843
+ await this.click(target, { ...options, clickCount: 2 });
844
+ }
845
+ /**
846
+ * Tap an element (touch gesture).
847
+ *
848
+ * ```ts
849
+ * await page.tap('Menu');
850
+ * ```
851
+ */
852
+ async tap(target, options) {
853
+ const timeout = options?.timeout ?? this.config.timeout;
854
+ const located = await locateElement(this.session, this.contextId, target, { timeout });
855
+ const ref = this.getSharedRef(located.node);
856
+ await this.session.script.callFunction({
857
+ functionDeclaration: 'function(el) { el.scrollIntoView({ block: "center", behavior: "instant" }); }',
858
+ target: { context: this.contextId },
859
+ arguments: [ref],
860
+ awaitPromise: false
861
+ });
862
+ const pos = await this.getElementCenter(ref);
863
+ await this.session.input.performActions({
864
+ context: this.contextId,
865
+ actions: [{
866
+ type: "pointer",
867
+ id: "touch",
868
+ parameters: { pointerType: "touch" },
869
+ actions: [
870
+ { type: "pointerMove", x: pos.x, y: pos.y, origin: "viewport" },
871
+ { type: "pointerDown", button: 0 },
872
+ { type: "pointerUp", button: 0 }
873
+ ]
874
+ }]
875
+ });
876
+ }
877
+ /**
878
+ * Focus an element.
879
+ *
880
+ * ```ts
881
+ * await page.focus('Email');
882
+ * ```
883
+ */
884
+ async focus(target, options) {
885
+ const timeout = options?.timeout ?? this.config.timeout;
886
+ const resolvedTarget = typeof target === "string" ? { label: target, name: target } : target;
887
+ const located = await locateElement(this.session, this.contextId, resolvedTarget, { timeout });
888
+ const ref = this.getSharedRef(located.node);
889
+ await this.session.script.callFunction({
890
+ functionDeclaration: "function(el) { el.focus(); }",
891
+ target: { context: this.contextId },
892
+ arguments: [ref],
893
+ awaitPromise: false
894
+ });
895
+ }
896
+ /**
897
+ * Remove focus from an element.
898
+ *
899
+ * ```ts
900
+ * await page.blur('Email');
901
+ * ```
902
+ */
903
+ async blur(target, options) {
904
+ const timeout = options?.timeout ?? this.config.timeout;
905
+ const resolvedTarget = typeof target === "string" ? { label: target, name: target } : target;
906
+ const located = await locateElement(this.session, this.contextId, resolvedTarget, { timeout });
907
+ const ref = this.getSharedRef(located.node);
908
+ await this.session.script.callFunction({
909
+ functionDeclaration: "function(el) { el.blur(); }",
910
+ target: { context: this.contextId },
911
+ arguments: [ref],
912
+ awaitPromise: false
913
+ });
914
+ }
915
+ /**
916
+ * Get the visible inner text of an element (like element.innerText).
917
+ *
918
+ * ```ts
919
+ * const text = await page.innerText('h1');
920
+ * ```
921
+ */
922
+ async innerText(target, options) {
923
+ const timeout = options?.timeout ?? this.config.timeout;
924
+ const located = await locateElement(this.session, this.contextId, target, { timeout });
925
+ const ref = this.getSharedRef(located.node);
926
+ const result = await this.session.script.callFunction({
927
+ functionDeclaration: 'function(el) { return el.innerText || ""; }',
928
+ target: { context: this.contextId },
929
+ arguments: [ref],
930
+ awaitPromise: false
931
+ });
932
+ return this.extractStringResult(result) ?? "";
933
+ }
934
+ /**
935
+ * Get the innerHTML of an element.
936
+ *
937
+ * ```ts
938
+ * const html = await page.innerHTML('.container');
939
+ * ```
940
+ */
941
+ async innerHTML(target, options) {
942
+ const timeout = options?.timeout ?? this.config.timeout;
943
+ const located = await locateElement(this.session, this.contextId, target, { timeout });
944
+ const ref = this.getSharedRef(located.node);
945
+ const result = await this.session.script.callFunction({
946
+ functionDeclaration: 'function(el) { return el.innerHTML || ""; }',
947
+ target: { context: this.contextId },
948
+ arguments: [ref],
949
+ awaitPromise: false
950
+ });
951
+ return this.extractStringResult(result) ?? "";
952
+ }
953
+ /**
954
+ * Get the current value of an input/textarea/select.
955
+ *
956
+ * ```ts
957
+ * const email = await page.inputValue('Email');
958
+ * ```
959
+ */
960
+ async inputValue(target, options) {
961
+ const timeout = options?.timeout ?? this.config.timeout;
962
+ const resolvedTarget = typeof target === "string" ? { label: target, name: target } : target;
963
+ const located = await locateElement(this.session, this.contextId, resolvedTarget, { timeout });
964
+ const ref = this.getSharedRef(located.node);
965
+ const result = await this.session.script.callFunction({
966
+ functionDeclaration: 'function(el) { return el.value ?? ""; }',
967
+ target: { context: this.contextId },
968
+ arguments: [ref],
969
+ awaitPromise: false
970
+ });
971
+ return this.extractStringResult(result) ?? "";
972
+ }
973
+ /**
974
+ * Select multiple options from a <select multiple> dropdown.
975
+ *
976
+ * ```ts
977
+ * await page.selectOption('Colors', ['red', 'blue']);
978
+ * ```
979
+ */
980
+ async selectOption(target, values, options) {
981
+ const timeout = options?.timeout ?? this.config.timeout;
982
+ const resolvedTarget = typeof target === "string" ? { label: target, name: target } : target;
983
+ const located = await locateElement(this.session, this.contextId, resolvedTarget, { timeout });
984
+ const ref = this.getSharedRef(located.node);
985
+ const valuesArray = Array.isArray(values) ? values : [values];
986
+ await this.session.script.callFunction({
987
+ functionDeclaration: `function(element, values) {
988
+ const opts = Array.from(element.options);
989
+ for (const opt of opts) {
990
+ opt.selected = values.some(v => opt.value === v || opt.text === v || opt.textContent.trim() === v);
991
+ }
992
+ element.dispatchEvent(new Event('change', { bubbles: true }));
993
+ }`,
994
+ target: { context: this.contextId },
995
+ arguments: [ref, { type: "array", value: valuesArray.map((v) => ({ type: "string", value: v })) }],
996
+ awaitPromise: false
997
+ });
998
+ }
999
+ /**
1000
+ * Drag an element to another element or position.
1001
+ *
1002
+ * ```ts
1003
+ * await page.dragTo('Draggable', 'Drop Zone');
1004
+ * ```
1005
+ */
1006
+ async dragTo(source, dest, options) {
1007
+ const timeout = options?.timeout ?? this.config.timeout;
1008
+ const sourceLoc = await locateElement(this.session, this.contextId, source, { timeout });
1009
+ const sourceRef = this.getSharedRef(sourceLoc.node);
1010
+ const sourcePos = await this.getElementCenter(sourceRef);
1011
+ const destLoc = await locateElement(this.session, this.contextId, dest, { timeout });
1012
+ const destRef = this.getSharedRef(destLoc.node);
1013
+ const destPos = await this.getElementCenter(destRef);
1014
+ await this.session.input.performActions({
1015
+ context: this.contextId,
1016
+ actions: [{
1017
+ type: "pointer",
1018
+ id: "mouse",
1019
+ parameters: { pointerType: "mouse" },
1020
+ actions: [
1021
+ { type: "pointerMove", x: sourcePos.x, y: sourcePos.y, origin: "viewport" },
1022
+ { type: "pointerDown", button: 0 },
1023
+ { type: "pointerMove", x: destPos.x, y: destPos.y, origin: "viewport", duration: 300 },
1024
+ { type: "pointerUp", button: 0 }
1025
+ ]
1026
+ }]
1027
+ });
1028
+ }
1029
+ // -----------------------------------------------------------------------
1030
+ // Waiting (explicit -- but usually you don't need these)
1031
+ // -----------------------------------------------------------------------
1032
+ /**
1033
+ * Wait for an element matching the target to appear in the DOM.
1034
+ *
1035
+ * ```ts
1036
+ * await page.waitForSelector('.loaded');
1037
+ * await page.waitForSelector({ role: 'dialog' });
1038
+ * ```
1039
+ */
1040
+ async waitForSelector(target, options) {
1041
+ const timeout = options?.timeout ?? this.config.timeout;
1042
+ const state = options?.state ?? "visible";
1043
+ if (state === "hidden") {
1044
+ await waitFor(
1045
+ `element to be hidden`,
1046
+ async () => {
1047
+ try {
1048
+ const elements = await locateAllElements(this.session, this.contextId, target);
1049
+ if (elements.length === 0) return true;
1050
+ const ref = this.getSharedRef(elements[0].node);
1051
+ const result = await this.session.script.callFunction({
1052
+ functionDeclaration: `function(el) {
1053
+ const style = window.getComputedStyle(el);
1054
+ return style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0';
1055
+ }`,
1056
+ target: { context: this.contextId },
1057
+ arguments: [ref],
1058
+ awaitPromise: false
1059
+ });
1060
+ const isHidden = result.type === "success" && result.result?.type === "boolean" && result.result.value === true;
1061
+ return isHidden ? true : null;
1062
+ } catch {
1063
+ return true;
1064
+ }
1065
+ },
1066
+ { timeout }
1067
+ );
1068
+ return new ElementHandle(this, target);
1069
+ }
1070
+ const located = await locateElement(this.session, this.contextId, target, { timeout });
1071
+ if (state === "visible") {
1072
+ const ref = this.getSharedRef(located.node);
1073
+ await waitFor(
1074
+ `element to be visible`,
1075
+ async () => {
1076
+ const result = await this.session.script.callFunction({
1077
+ functionDeclaration: `function(el) {
1078
+ const style = window.getComputedStyle(el);
1079
+ const rect = el.getBoundingClientRect();
1080
+ return style.display !== 'none' && style.visibility !== 'hidden' &&
1081
+ style.opacity !== '0' && rect.width > 0 && rect.height > 0;
1082
+ }`,
1083
+ target: { context: this.contextId },
1084
+ arguments: [ref],
1085
+ awaitPromise: false
1086
+ });
1087
+ const isVisible = result.type === "success" && result.result?.type === "boolean" && result.result.value === true;
1088
+ return isVisible ? true : null;
1089
+ },
1090
+ { timeout }
1091
+ );
1092
+ }
1093
+ return new ElementHandle(this, target);
1094
+ }
1095
+ /**
1096
+ * Wait for a JavaScript function to return a truthy value.
1097
+ *
1098
+ * ```ts
1099
+ * await page.waitForFunction('document.querySelectorAll("li").length > 5');
1100
+ * await page.waitForFunction(() => window.appReady === true);
1101
+ * ```
1102
+ */
1103
+ async waitForFunction(expression, options) {
1104
+ const timeout = options?.timeout ?? this.config.timeout;
1105
+ const expr = typeof expression === "function" ? `(${expression.toString()})()` : expression;
1106
+ return waitFor(
1107
+ "function to return truthy",
1108
+ async () => {
1109
+ const result = await this.session.script.evaluate({
1110
+ expression: expr,
1111
+ target: { context: this.contextId },
1112
+ awaitPromise: true
1113
+ });
1114
+ if (result.type === "exception") return null;
1115
+ const value = this.deserializeRemoteValue(result.result);
1116
+ return value ? value : null;
1117
+ },
1118
+ { timeout }
1119
+ );
1120
+ }
1121
+ /**
1122
+ * Wait for a specific URL. Usually you use expect(page).toHaveURL() instead.
1123
+ */
1124
+ async waitForURL(url, options) {
1125
+ const timeout = options?.timeout ?? this.config.timeout;
1126
+ await waitFor(
1127
+ `URL to match ${url}`,
1128
+ async () => {
1129
+ const currentUrl = await this.url();
1130
+ if (typeof url === "string") {
1131
+ return currentUrl.includes(url) ? true : null;
1132
+ }
1133
+ return url.test(currentUrl) ? true : null;
1134
+ },
1135
+ { timeout }
1136
+ );
1137
+ }
1138
+ /**
1139
+ * Wait for the page to finish loading.
1140
+ */
1141
+ async waitForLoadState(state) {
1142
+ await waitForLoadState(this.session, this.contextId, state, this.config.timeout);
1143
+ }
1144
+ // -----------------------------------------------------------------------
1145
+ // Lifecycle
1146
+ // -----------------------------------------------------------------------
1147
+ /**
1148
+ * Close this page/tab.
1149
+ */
1150
+ async close() {
1151
+ await this.clearMocks();
1152
+ await this.session.browsingContext.close({ context: this.contextId });
1153
+ }
1154
+ // -----------------------------------------------------------------------
1155
+ // Keyboard shortcuts
1156
+ // -----------------------------------------------------------------------
1157
+ /**
1158
+ * Press a keyboard key.
1159
+ *
1160
+ * ```ts
1161
+ * await page.press('Enter');
1162
+ * await page.press('Control+a');
1163
+ * ```
1164
+ */
1165
+ async press(key) {
1166
+ const keys = key.split("+");
1167
+ const actions = [];
1168
+ for (const k of keys) {
1169
+ actions.push({ type: "keyDown", value: mapKey(k) });
1170
+ }
1171
+ for (const k of keys.reverse()) {
1172
+ actions.push({ type: "keyUp", value: mapKey(k) });
1173
+ }
1174
+ await this.session.input.performActions({
1175
+ context: this.contextId,
1176
+ actions: [{ type: "key", id: "keyboard", actions }]
1177
+ });
1178
+ }
1179
+ // -----------------------------------------------------------------------
1180
+ // Private helpers
1181
+ // -----------------------------------------------------------------------
1182
+ /** Resolve a relative URL against the baseURL */
1183
+ resolveURL(url) {
1184
+ if (url.startsWith("http://") || url.startsWith("https://") || url.startsWith("about:") || url.startsWith("data:")) {
1185
+ return url;
1186
+ }
1187
+ const base = this.config.baseURL.replace(/\/$/, "");
1188
+ const path = url.startsWith("/") ? url : `/${url}`;
1189
+ return base ? `${base}${path}` : url;
1190
+ }
1191
+ /** Get a shared reference from a located node */
1192
+ getSharedRef(node) {
1193
+ if (node.sharedId) {
1194
+ return { sharedId: node.sharedId, handle: node.handle };
1195
+ }
1196
+ throw new Error("Element has no shared reference. This is a bug in Browsecraft.");
1197
+ }
1198
+ /** Get the center coordinates of an element */
1199
+ async getElementCenter(ref) {
1200
+ const result = await this.session.script.callFunction({
1201
+ functionDeclaration: `function(el) {
1202
+ const rect = el.getBoundingClientRect();
1203
+ if (rect.width === 0 && rect.height === 0) {
1204
+ throw new Error('Element has zero size -- it may be hidden or not rendered');
1205
+ }
1206
+ return { x: Math.round(rect.x + rect.width / 2), y: Math.round(rect.y + rect.height / 2) };
1207
+ }`,
1208
+ target: { context: this.contextId },
1209
+ arguments: [ref],
1210
+ awaitPromise: false
1211
+ });
1212
+ if (result.type === "exception") {
1213
+ const errorText = result.exceptionDetails?.text ?? "Failed to get element position";
1214
+ throw new Error(`Cannot interact with element: ${errorText}`);
1215
+ }
1216
+ if (result.type === "success" && result.result?.type === "object") {
1217
+ const val = result.result.value;
1218
+ if (Array.isArray(val)) {
1219
+ const map = new Map(val);
1220
+ const xVal = map.get("x");
1221
+ const yVal = map.get("y");
1222
+ const x = typeof xVal === "object" && xVal !== null && "value" in xVal ? xVal.value : null;
1223
+ const y = typeof yVal === "object" && yVal !== null && "value" in yVal ? yVal.value : null;
1224
+ if (x !== null && y !== null) {
1225
+ return { x, y };
1226
+ }
1227
+ }
1228
+ }
1229
+ throw new Error(
1230
+ "Cannot get element position: unexpected response from browser. The element may not be visible or may not have a bounding rectangle."
1231
+ );
1232
+ }
1233
+ /**
1234
+ * Scroll element into view and click it.
1235
+ *
1236
+ * Uses JavaScript `.click()` as the primary mechanism because it is
1237
+ * immune to viewport coordinate mismatches, DPI scaling, layout shifts,
1238
+ * and Chrome BiDi's unreliable element-origin support. This works
1239
+ * identically in headless and headed mode, with or without slowMo delays.
1240
+ *
1241
+ * The element is scrolled into view first so it's visible in headed mode
1242
+ * (important when users are watching the test run).
1243
+ */
1244
+ async scrollIntoViewAndClick(located, options) {
1245
+ const ref = this.getSharedRef(located.node);
1246
+ const clickCount = options?.clickCount ?? 1;
1247
+ await this.session.script.callFunction({
1248
+ functionDeclaration: `function(el, clickCount) {
1249
+ el.scrollIntoView({ block: "center", behavior: "instant" });
1250
+ for (let i = 0; i < clickCount; i++) {
1251
+ el.click();
1252
+ }
1253
+ }`,
1254
+ target: { context: this.contextId },
1255
+ arguments: [ref, { type: "number", value: clickCount }],
1256
+ awaitPromise: false
1257
+ });
1258
+ }
1259
+ /** Extract a string from a script evaluation result */
1260
+ extractStringResult(result) {
1261
+ if (result.type === "success" && result.result?.type === "string") {
1262
+ return result.result.value;
1263
+ }
1264
+ return null;
1265
+ }
1266
+ /** Convert a BiDi RemoteValue back to a JS value */
1267
+ deserializeRemoteValue(value) {
1268
+ if (!value || typeof value !== "object") return value;
1269
+ const v = value;
1270
+ switch (v.type) {
1271
+ case "undefined":
1272
+ return void 0;
1273
+ case "null":
1274
+ return null;
1275
+ case "string":
1276
+ return v.value;
1277
+ case "number": {
1278
+ const n = v.value;
1279
+ if (n === "NaN") return Number.NaN;
1280
+ if (n === "-0") return -0;
1281
+ if (n === "Infinity") return Number.POSITIVE_INFINITY;
1282
+ if (n === "-Infinity") return Number.NEGATIVE_INFINITY;
1283
+ return n;
1284
+ }
1285
+ case "boolean":
1286
+ return v.value;
1287
+ case "bigint":
1288
+ return BigInt(v.value);
1289
+ case "array": {
1290
+ if (Array.isArray(v.value)) {
1291
+ return v.value.map((item) => this.deserializeRemoteValue(item));
1292
+ }
1293
+ return [];
1294
+ }
1295
+ case "object": {
1296
+ if (Array.isArray(v.value)) {
1297
+ const obj = {};
1298
+ for (const [key, val] of v.value) {
1299
+ obj[key] = this.deserializeRemoteValue(val);
1300
+ }
1301
+ return obj;
1302
+ }
1303
+ return {};
1304
+ }
1305
+ default:
1306
+ return v.value ?? null;
1307
+ }
1308
+ }
1309
+ };
1310
+ var ElementHandle = class {
1311
+ /** @internal */
1312
+ page;
1313
+ /** @internal */
1314
+ target;
1315
+ constructor(page, target) {
1316
+ this.page = page;
1317
+ this.target = target;
1318
+ }
1319
+ /** Click this element */
1320
+ async click(options) {
1321
+ await this.page.click(this.target, options);
1322
+ }
1323
+ /** Fill this element with text */
1324
+ async fill(value, options) {
1325
+ await this.page.fill(this.target, value, options);
1326
+ }
1327
+ /** Get the visible text content of this element */
1328
+ async textContent() {
1329
+ const located = await this.locate();
1330
+ const ref = this.getRef(located);
1331
+ const result = await this.page.session.script.callFunction({
1332
+ functionDeclaration: 'function(el) { return el.textContent || ""; }',
1333
+ target: { context: this.page.contextId },
1334
+ arguments: [ref],
1335
+ awaitPromise: false
1336
+ });
1337
+ if (result.type === "success" && result.result?.type === "string") {
1338
+ return result.result.value;
1339
+ }
1340
+ return "";
1341
+ }
1342
+ /** Get an attribute value */
1343
+ async getAttribute(name) {
1344
+ const located = await this.locate();
1345
+ const ref = this.getRef(located);
1346
+ const result = await this.page.session.script.callFunction({
1347
+ functionDeclaration: `function(el, name) { return el.getAttribute(name); }`,
1348
+ target: { context: this.page.contextId },
1349
+ arguments: [ref, { type: "string", value: name }],
1350
+ awaitPromise: false
1351
+ });
1352
+ if (result.type === "success" && result.result?.type === "string") {
1353
+ return result.result.value;
1354
+ }
1355
+ return null;
1356
+ }
1357
+ /** Check if the element is visible on the page */
1358
+ async isVisible() {
1359
+ try {
1360
+ const located = await this.locate(5e3);
1361
+ const ref = this.getRef(located);
1362
+ const result = await this.page.session.script.callFunction({
1363
+ functionDeclaration: `function(el) {
1364
+ const style = window.getComputedStyle(el);
1365
+ const rect = el.getBoundingClientRect();
1366
+ return style.display !== 'none' &&
1367
+ style.visibility !== 'hidden' &&
1368
+ style.opacity !== '0' &&
1369
+ rect.width > 0 &&
1370
+ rect.height > 0;
1371
+ }`,
1372
+ target: { context: this.page.contextId },
1373
+ arguments: [ref],
1374
+ awaitPromise: false
1375
+ });
1376
+ return result.type === "success" && result.result?.type === "boolean" && result.result.value === true;
1377
+ } catch {
1378
+ return false;
1379
+ }
1380
+ }
1381
+ /** Count matching elements */
1382
+ async count() {
1383
+ const elements = await locateAllElements(this.page.session, this.page.contextId, this.target);
1384
+ return elements.length;
1385
+ }
1386
+ /** Get the visible inner text (like element.innerText, not textContent) */
1387
+ async innerText() {
1388
+ const located = await this.locate();
1389
+ const ref = this.getRef(located);
1390
+ const result = await this.page.session.script.callFunction({
1391
+ functionDeclaration: 'function(el) { return el.innerText || ""; }',
1392
+ target: { context: this.page.contextId },
1393
+ arguments: [ref],
1394
+ awaitPromise: false
1395
+ });
1396
+ if (result.type === "success" && result.result?.type === "string") {
1397
+ return result.result.value;
1398
+ }
1399
+ return "";
1400
+ }
1401
+ /** Get the innerHTML of this element */
1402
+ async innerHTML() {
1403
+ const located = await this.locate();
1404
+ const ref = this.getRef(located);
1405
+ const result = await this.page.session.script.callFunction({
1406
+ functionDeclaration: 'function(el) { return el.innerHTML || ""; }',
1407
+ target: { context: this.page.contextId },
1408
+ arguments: [ref],
1409
+ awaitPromise: false
1410
+ });
1411
+ if (result.type === "success" && result.result?.type === "string") {
1412
+ return result.result.value;
1413
+ }
1414
+ return "";
1415
+ }
1416
+ /** Get the current value of an input/textarea/select */
1417
+ async inputValue() {
1418
+ const located = await this.locate();
1419
+ const ref = this.getRef(located);
1420
+ const result = await this.page.session.script.callFunction({
1421
+ functionDeclaration: 'function(el) { return el.value ?? ""; }',
1422
+ target: { context: this.page.contextId },
1423
+ arguments: [ref],
1424
+ awaitPromise: false
1425
+ });
1426
+ if (result.type === "success" && result.result?.type === "string") {
1427
+ return result.result.value;
1428
+ }
1429
+ return "";
1430
+ }
1431
+ /** Check if the element is enabled (not disabled) */
1432
+ async isEnabled() {
1433
+ try {
1434
+ const located = await this.locate(5e3);
1435
+ const ref = this.getRef(located);
1436
+ const result = await this.page.session.script.callFunction({
1437
+ functionDeclaration: "function(el) { return !el.disabled; }",
1438
+ target: { context: this.page.contextId },
1439
+ arguments: [ref],
1440
+ awaitPromise: false
1441
+ });
1442
+ return result.type === "success" && result.result?.type === "boolean" && result.result.value === true;
1443
+ } catch {
1444
+ return false;
1445
+ }
1446
+ }
1447
+ /** Check if a checkbox/radio is checked */
1448
+ async isChecked() {
1449
+ try {
1450
+ const located = await this.locate(5e3);
1451
+ const ref = this.getRef(located);
1452
+ const result = await this.page.session.script.callFunction({
1453
+ functionDeclaration: "function(el) { return !!el.checked; }",
1454
+ target: { context: this.page.contextId },
1455
+ arguments: [ref],
1456
+ awaitPromise: false
1457
+ });
1458
+ return result.type === "success" && result.result?.type === "boolean" && result.result.value === true;
1459
+ } catch {
1460
+ return false;
1461
+ }
1462
+ }
1463
+ /** Get the bounding box of the element */
1464
+ async boundingBox() {
1465
+ try {
1466
+ const located = await this.locate(5e3);
1467
+ const ref = this.getRef(located);
1468
+ const result = await this.page.session.script.callFunction({
1469
+ functionDeclaration: `function(el) {
1470
+ const rect = el.getBoundingClientRect();
1471
+ return { x: rect.x, y: rect.y, width: rect.width, height: rect.height };
1472
+ }`,
1473
+ target: { context: this.page.contextId },
1474
+ arguments: [ref],
1475
+ awaitPromise: false
1476
+ });
1477
+ if (result.type === "success" && result.result?.type === "object") {
1478
+ const val = result.result.value;
1479
+ if (Array.isArray(val)) {
1480
+ const map = new Map(val);
1481
+ const extract = (key) => {
1482
+ const v = map.get(key);
1483
+ return typeof v === "object" && v !== null && "value" in v ? v.value : 0;
1484
+ };
1485
+ return { x: extract("x"), y: extract("y"), width: extract("width"), height: extract("height") };
1486
+ }
1487
+ }
1488
+ return null;
1489
+ } catch {
1490
+ return null;
1491
+ }
1492
+ }
1493
+ /** Take a screenshot of just this element */
1494
+ async screenshot() {
1495
+ const located = await this.locate();
1496
+ const ref = this.getRef(located);
1497
+ await this.page.session.script.callFunction({
1498
+ functionDeclaration: 'function(el) { el.scrollIntoView({ block: "center", behavior: "instant" }); }',
1499
+ target: { context: this.page.contextId },
1500
+ arguments: [ref],
1501
+ awaitPromise: false
1502
+ });
1503
+ const box = await this.boundingBox();
1504
+ if (!box || box.width === 0 || box.height === 0) {
1505
+ throw new Error("Cannot screenshot element: element has no size or is not visible");
1506
+ }
1507
+ const result = await this.page.session.browsingContext.captureScreenshot({
1508
+ context: this.page.contextId,
1509
+ clip: {
1510
+ type: "box",
1511
+ x: box.x,
1512
+ y: box.y,
1513
+ width: box.width,
1514
+ height: box.height
1515
+ }
1516
+ });
1517
+ return Buffer.from(result.data, "base64");
1518
+ }
1519
+ /** Double-click this element */
1520
+ async dblclick(options) {
1521
+ await this.page.dblclick(this.target, options);
1522
+ }
1523
+ /** Hover over this element */
1524
+ async hover(options) {
1525
+ await this.page.hover(this.target, options);
1526
+ }
1527
+ /** Type text into this element character by character */
1528
+ async type(text, options) {
1529
+ await this.page.type(this.target, text, options);
1530
+ }
1531
+ /** Focus this element */
1532
+ async focus(options) {
1533
+ await this.page.focus(this.target, options);
1534
+ }
1535
+ /** Remove focus from this element */
1536
+ async blur(options) {
1537
+ await this.page.blur(this.target, options);
1538
+ }
1539
+ /** @internal Locate the element with auto-wait */
1540
+ async locate(timeout) {
1541
+ return locateElement(
1542
+ this.page.session,
1543
+ this.page.contextId,
1544
+ this.target,
1545
+ { timeout: timeout ?? 3e4 }
1546
+ );
1547
+ }
1548
+ getRef(located) {
1549
+ if (located.node.sharedId) {
1550
+ return { sharedId: located.node.sharedId, handle: located.node.handle };
1551
+ }
1552
+ throw new Error("Element has no shared reference");
1553
+ }
1554
+ };
1555
+ function mapKey(key) {
1556
+ const keyMap = {
1557
+ Enter: "\uE007",
1558
+ Tab: "\uE004",
1559
+ Escape: "\uE00C",
1560
+ Backspace: "\uE003",
1561
+ Delete: "\uE017",
1562
+ ArrowUp: "\uE013",
1563
+ ArrowDown: "\uE015",
1564
+ ArrowLeft: "\uE012",
1565
+ ArrowRight: "\uE014",
1566
+ Home: "\uE011",
1567
+ End: "\uE010",
1568
+ PageUp: "\uE00E",
1569
+ PageDown: "\uE00F",
1570
+ Control: "\uE009",
1571
+ Alt: "\uE00A",
1572
+ Shift: "\uE008",
1573
+ Meta: "\uE03D",
1574
+ Space: " "
1575
+ };
1576
+ return keyMap[key] ?? key;
1577
+ }
1578
+ function parseMockPattern(pattern) {
1579
+ const methodMatch = pattern.match(/^(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)\s+(.+)$/i);
1580
+ if (methodMatch) {
1581
+ const method = methodMatch[1].toUpperCase();
1582
+ const path = methodMatch[2];
1583
+ if (path.startsWith("http")) {
1584
+ return { method, urlPattern: { type: "string", pattern: path } };
1585
+ }
1586
+ return { method, urlPattern: { type: "pattern", pathname: path } };
1587
+ }
1588
+ if (pattern.startsWith("http")) {
1589
+ return { method: null, urlPattern: { type: "string", pattern } };
1590
+ }
1591
+ return { method: null, urlPattern: { type: "pattern", pathname: pattern } };
1592
+ }
1593
+ function buildMockHeaders(response) {
1594
+ const headers = [];
1595
+ let contentType = response.contentType;
1596
+ if (!contentType) {
1597
+ if (typeof response.body === "object" && response.body !== null) {
1598
+ contentType = "application/json";
1599
+ } else if (typeof response.body === "string") {
1600
+ contentType = "text/plain";
1601
+ }
1602
+ }
1603
+ if (contentType) {
1604
+ headers.push({ name: "Content-Type", value: { type: "string", value: contentType } });
1605
+ }
1606
+ if (response.headers) {
1607
+ for (const [name, value] of Object.entries(response.headers)) {
1608
+ headers.push({ name, value: { type: "string", value } });
1609
+ }
1610
+ }
1611
+ return headers;
1612
+ }
1613
+ function buildMockBody(response) {
1614
+ if (response.body === void 0 || response.body === null) return null;
1615
+ if (typeof response.body === "string") return response.body;
1616
+ return JSON.stringify(response.body);
1617
+ }
1618
+ async function applyViewport(session, contextId, config) {
1619
+ if (config.maximized && !config.headless) {
1620
+ return;
1621
+ }
1622
+ if (config.headless) {
1623
+ try {
1624
+ await session.browsingContext.setViewport({
1625
+ context: contextId,
1626
+ viewport: config.viewport
1627
+ });
1628
+ } catch {
1629
+ }
1630
+ return;
1631
+ }
1632
+ try {
1633
+ const { width, height } = config.viewport;
1634
+ await session.script.callFunction({
1635
+ functionDeclaration: `function(targetW, targetH) {
1636
+ const chromeW = window.outerWidth - window.innerWidth;
1637
+ const chromeH = window.outerHeight - window.innerHeight;
1638
+ window.resizeTo(targetW + chromeW, targetH + chromeH);
1639
+ }`,
1640
+ target: { context: contextId },
1641
+ arguments: [
1642
+ { type: "number", value: width },
1643
+ { type: "number", value: height }
1644
+ ],
1645
+ awaitPromise: false
1646
+ });
1647
+ } catch {
1648
+ try {
1649
+ await session.browsingContext.setViewport({
1650
+ context: contextId,
1651
+ viewport: config.viewport
1652
+ });
1653
+ } catch {
1654
+ }
1655
+ }
1656
+ }
1657
+ var Browser = class _Browser {
1658
+ /** @internal */
1659
+ session;
1660
+ /** @internal */
1661
+ config;
1662
+ /** @internal */
1663
+ pages = [];
1664
+ constructor(session, config) {
1665
+ this.session = session;
1666
+ this.config = config;
1667
+ }
1668
+ /**
1669
+ * Launch a new browser instance.
1670
+ *
1671
+ * ```ts
1672
+ * const browser = await Browser.launch(); // Chrome, headless
1673
+ * const browser = await Browser.launch({ browser: 'firefox', headless: false });
1674
+ * ```
1675
+ */
1676
+ static async launch(userConfig) {
1677
+ const config = resolveConfig(userConfig);
1678
+ const sessionOptions = {
1679
+ browser: config.browser,
1680
+ headless: config.headless,
1681
+ executablePath: config.executablePath,
1682
+ debug: config.debug,
1683
+ timeout: config.timeout,
1684
+ maximized: config.maximized
1685
+ };
1686
+ const session = await browsecraftBidi.BiDiSession.launch(sessionOptions);
1687
+ return new _Browser(session, config);
1688
+ }
1689
+ /**
1690
+ * Connect to an already-running browser.
1691
+ *
1692
+ * ```ts
1693
+ * const browser = await Browser.connect('ws://localhost:9222/session');
1694
+ * ```
1695
+ */
1696
+ static async connect(wsEndpoint, userConfig) {
1697
+ const config = resolveConfig(userConfig);
1698
+ const session = await browsecraftBidi.BiDiSession.connect(wsEndpoint);
1699
+ return new _Browser(session, config);
1700
+ }
1701
+ /**
1702
+ * Create a new page (tab).
1703
+ *
1704
+ * ```ts
1705
+ * const page = await browser.newPage();
1706
+ * await page.goto('https://example.com');
1707
+ * ```
1708
+ */
1709
+ async newPage() {
1710
+ const result = await this.session.browsingContext.create({ type: "tab" });
1711
+ const contextId = result.context;
1712
+ await applyViewport(this.session, contextId, this.config);
1713
+ const page = new Page(this.session, contextId, this.config);
1714
+ this.pages.push(page);
1715
+ this.session.on("browsingContext.closed", (event) => {
1716
+ const params = event.params;
1717
+ if (params.context === contextId) {
1718
+ const idx = this.pages.indexOf(page);
1719
+ if (idx !== -1) this.pages.splice(idx, 1);
1720
+ }
1721
+ });
1722
+ return page;
1723
+ }
1724
+ /** Get all open pages. */
1725
+ get openPages() {
1726
+ return [...this.pages];
1727
+ }
1728
+ /**
1729
+ * Create an isolated browser context (like incognito).
1730
+ * Returns a BrowserContext that can create its own pages.
1731
+ */
1732
+ async newContext() {
1733
+ try {
1734
+ const result = await this.session.send("browser.createUserContext", {});
1735
+ const userContext = result.userContext;
1736
+ return new BrowserContext(this.session, this.config, userContext);
1737
+ } catch (err) {
1738
+ console.warn(
1739
+ "[browsecraft] Warning: browser.createUserContext is not supported. Pages will share cookies/storage. " + (err instanceof Error ? err.message : String(err))
1740
+ );
1741
+ return new BrowserContext(this.session, this.config, null);
1742
+ }
1743
+ }
1744
+ /** Close the browser and clean up all resources. */
1745
+ async close() {
1746
+ for (const page of this.pages) {
1747
+ await page.close().catch(() => {
1748
+ });
1749
+ }
1750
+ this.pages = [];
1751
+ await this.session.close();
1752
+ }
1753
+ /** Whether the browser is still connected */
1754
+ get isConnected() {
1755
+ return this.session.isConnected;
1756
+ }
1757
+ /** Get the resolved config */
1758
+ getConfig() {
1759
+ return { ...this.config };
1760
+ }
1761
+ };
1762
+ var BrowserContext = class {
1763
+ /** @internal */
1764
+ session;
1765
+ /** @internal */
1766
+ config;
1767
+ /** @internal */
1768
+ userContext;
1769
+ /** @internal */
1770
+ pages = [];
1771
+ constructor(session, config, userContext) {
1772
+ this.session = session;
1773
+ this.config = config;
1774
+ this.userContext = userContext;
1775
+ }
1776
+ /** Create a new page in this context. */
1777
+ async newPage() {
1778
+ const params = { type: "tab" };
1779
+ if (this.userContext) {
1780
+ params.userContext = this.userContext;
1781
+ }
1782
+ const result = await this.session.browsingContext.create(params);
1783
+ const contextId = result.context;
1784
+ await applyViewport(this.session, contextId, this.config);
1785
+ const page = new Page(this.session, contextId, this.config);
1786
+ this.pages.push(page);
1787
+ return page;
1788
+ }
1789
+ /** Close this context and all its pages. */
1790
+ async close() {
1791
+ for (const page of this.pages) {
1792
+ await page.close().catch(() => {
1793
+ });
1794
+ }
1795
+ this.pages = [];
1796
+ if (this.userContext) {
1797
+ await this.session.send("browser.removeUserContext", {
1798
+ userContext: this.userContext
1799
+ }).catch(() => {
1800
+ });
1801
+ }
1802
+ }
1803
+ };
1804
+ var testRegistry = [];
1805
+ var suiteStack = [];
1806
+ var executedBeforeAllHooks = /* @__PURE__ */ new Set();
1807
+ var executedAfterAllHooks = /* @__PURE__ */ new Set();
1808
+ function test(title, fnOrOptions, maybeFn) {
1809
+ const fn = typeof fnOrOptions === "function" ? fnOrOptions : maybeFn;
1810
+ const options = typeof fnOrOptions === "function" ? {} : fnOrOptions;
1811
+ testRegistry.push({
1812
+ title,
1813
+ fn,
1814
+ options,
1815
+ suitePath: [...suiteStack],
1816
+ skip: false,
1817
+ only: false
1818
+ });
1819
+ }
1820
+ test.skip = function skipTest(title, fn) {
1821
+ testRegistry.push({
1822
+ title,
1823
+ fn,
1824
+ options: {},
1825
+ suitePath: [...suiteStack],
1826
+ skip: true,
1827
+ only: false
1828
+ });
1829
+ };
1830
+ test.only = function onlyTest(title, fn) {
1831
+ testRegistry.push({
1832
+ title,
1833
+ fn,
1834
+ options: {},
1835
+ suitePath: [...suiteStack],
1836
+ skip: false,
1837
+ only: true
1838
+ });
1839
+ };
1840
+ function describe(title, fn) {
1841
+ suiteStack.push(title);
1842
+ fn();
1843
+ suiteStack.pop();
1844
+ }
1845
+ describe.skip = function skipDescribe(title, fn) {
1846
+ suiteStack.push(title);
1847
+ const startIndex = testRegistry.length;
1848
+ fn();
1849
+ for (let i = startIndex; i < testRegistry.length; i++) {
1850
+ testRegistry[i].skip = true;
1851
+ }
1852
+ suiteStack.pop();
1853
+ };
1854
+ describe.only = function onlyDescribe(title, fn) {
1855
+ suiteStack.push(title);
1856
+ const startIndex = testRegistry.length;
1857
+ fn();
1858
+ for (let i = startIndex; i < testRegistry.length; i++) {
1859
+ testRegistry[i].only = true;
1860
+ }
1861
+ suiteStack.pop();
1862
+ };
1863
+ var hooks = {
1864
+ beforeAll: [],
1865
+ afterAll: [],
1866
+ beforeEach: [],
1867
+ afterEach: []
1868
+ };
1869
+ function beforeAll(fn) {
1870
+ hooks.beforeAll.push({ fn, suitePath: [...suiteStack] });
1871
+ }
1872
+ function afterAll(fn) {
1873
+ hooks.afterAll.push({ fn, suitePath: [...suiteStack] });
1874
+ }
1875
+ function beforeEach(fn) {
1876
+ hooks.beforeEach.push({ fn, suitePath: [...suiteStack] });
1877
+ }
1878
+ function afterEach(fn) {
1879
+ hooks.afterEach.push({ fn, suitePath: [...suiteStack] });
1880
+ }
1881
+ async function runTest(testCase, sharedBrowser, userConfig) {
1882
+ const startTime = Date.now();
1883
+ const config = resolveConfig(userConfig);
1884
+ if (testCase.skip) {
1885
+ return {
1886
+ title: testCase.title,
1887
+ suitePath: testCase.suitePath,
1888
+ status: "skipped",
1889
+ duration: 0
1890
+ };
1891
+ }
1892
+ let browser;
1893
+ let context;
1894
+ let page;
1895
+ try {
1896
+ browser = sharedBrowser ?? await Browser.launch();
1897
+ context = await browser.newContext();
1898
+ page = await context.newPage();
1899
+ const fixtures = { page, context, browser };
1900
+ const applicableBeforeAll = hooks.beforeAll.filter(
1901
+ (h) => isHookApplicable(h.suitePath, testCase.suitePath)
1902
+ );
1903
+ for (const hook of applicableBeforeAll) {
1904
+ const hookKey = `${hook.suitePath.join(">")}:${hooks.beforeAll.indexOf(hook)}`;
1905
+ if (!executedBeforeAllHooks.has(hookKey)) {
1906
+ await hook.fn(fixtures);
1907
+ executedBeforeAllHooks.add(hookKey);
1908
+ }
1909
+ }
1910
+ const applicableBeforeEach = hooks.beforeEach.filter(
1911
+ (h) => isHookApplicable(h.suitePath, testCase.suitePath)
1912
+ );
1913
+ for (const hook of applicableBeforeEach) {
1914
+ await hook.fn(fixtures);
1915
+ }
1916
+ const timeout = testCase.options.timeout ?? config.timeout;
1917
+ await Promise.race([
1918
+ testCase.fn(fixtures),
1919
+ new Promise(
1920
+ (_, reject) => setTimeout(() => reject(new Error(`Test timed out after ${timeout}ms`)), timeout)
1921
+ )
1922
+ ]);
1923
+ const applicableAfterEach = hooks.afterEach.filter(
1924
+ (h) => isHookApplicable(h.suitePath, testCase.suitePath)
1925
+ );
1926
+ for (const hook of applicableAfterEach) {
1927
+ await hook.fn(fixtures);
1928
+ }
1929
+ let screenshotPath;
1930
+ if (config.screenshot === "always" && page) {
1931
+ screenshotPath = await captureScreenshot(page, testCase, config.outputDir).catch(() => void 0);
1932
+ }
1933
+ const duration = Date.now() - startTime;
1934
+ return {
1935
+ title: testCase.title,
1936
+ suitePath: testCase.suitePath,
1937
+ status: "passed",
1938
+ duration,
1939
+ screenshotPath
1940
+ };
1941
+ } catch (error) {
1942
+ const duration = Date.now() - startTime;
1943
+ let screenshotPath;
1944
+ if ((config.screenshot === "on-failure" || config.screenshot === "always") && page) {
1945
+ screenshotPath = await captureScreenshot(page, testCase, config.outputDir).catch(() => void 0);
1946
+ }
1947
+ return {
1948
+ title: testCase.title,
1949
+ suitePath: testCase.suitePath,
1950
+ status: "failed",
1951
+ duration,
1952
+ error: error instanceof Error ? error : new Error(String(error)),
1953
+ screenshotPath
1954
+ };
1955
+ } finally {
1956
+ await context?.close().catch(() => {
1957
+ });
1958
+ if (!sharedBrowser && browser) {
1959
+ await browser.close().catch(() => {
1960
+ });
1961
+ }
1962
+ }
1963
+ }
1964
+ async function runAfterAllHooks(suitePath, fixtures) {
1965
+ const applicableAfterAll = hooks.afterAll.filter(
1966
+ (h) => isHookApplicable(h.suitePath, suitePath)
1967
+ );
1968
+ for (const hook of applicableAfterAll) {
1969
+ const hookKey = `${hook.suitePath.join(">")}:${hooks.afterAll.indexOf(hook)}`;
1970
+ if (!executedAfterAllHooks.has(hookKey)) {
1971
+ await hook.fn(fixtures);
1972
+ executedAfterAllHooks.add(hookKey);
1973
+ }
1974
+ }
1975
+ }
1976
+ function resetTestState() {
1977
+ testRegistry.length = 0;
1978
+ hooks.beforeAll.length = 0;
1979
+ hooks.afterAll.length = 0;
1980
+ hooks.beforeEach.length = 0;
1981
+ hooks.afterEach.length = 0;
1982
+ executedBeforeAllHooks.clear();
1983
+ executedAfterAllHooks.clear();
1984
+ suiteStack.length = 0;
1985
+ }
1986
+ function isHookApplicable(hookSuitePath, testSuitePath) {
1987
+ if (hookSuitePath.length === 0) return true;
1988
+ if (hookSuitePath.length > testSuitePath.length) return false;
1989
+ return hookSuitePath.every((s, i) => testSuitePath[i] === s);
1990
+ }
1991
+ async function captureScreenshot(page, testCase, outputDir) {
1992
+ const parts = [...testCase.suitePath, testCase.title];
1993
+ const safeName = parts.join("-").replace(/[^a-zA-Z0-9_-]/g, "_").replace(/_+/g, "_").slice(0, 200);
1994
+ const timestamp = Date.now();
1995
+ const filename = `${safeName}-${timestamp}.png`;
1996
+ const screenshotDir = path.join(outputDir, "screenshots");
1997
+ await promises.mkdir(screenshotDir, { recursive: true });
1998
+ const buffer = await page.screenshot();
1999
+ const filePath = path.join(screenshotDir, filename);
2000
+ await promises.writeFile(filePath, buffer);
2001
+ return filePath;
2002
+ }
2003
+
2004
+ exports.Browser = Browser;
2005
+ exports.BrowserContext = BrowserContext;
2006
+ exports.ElementHandle = ElementHandle;
2007
+ exports.Page = Page;
2008
+ exports.afterAll = afterAll;
2009
+ exports.afterEach = afterEach;
2010
+ exports.beforeAll = beforeAll;
2011
+ exports.beforeEach = beforeEach;
2012
+ exports.defineConfig = defineConfig;
2013
+ exports.describe = describe;
2014
+ exports.resetTestState = resetTestState;
2015
+ exports.resolveConfig = resolveConfig;
2016
+ exports.runAfterAllHooks = runAfterAllHooks;
2017
+ exports.runTest = runTest;
2018
+ exports.test = test;
2019
+ exports.testRegistry = testRegistry;
2020
+ //# sourceMappingURL=chunk-KIPQFK3Y.cjs.map
2021
+ //# sourceMappingURL=chunk-KIPQFK3Y.cjs.map