@toolstackhq/cdpwright 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js ADDED
@@ -0,0 +1,2161 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/browser/ChromiumManager.ts
4
+ import fs4 from "fs";
5
+ import path4 from "path";
6
+ import os2 from "os";
7
+ import http from "http";
8
+ import { spawn as spawn2 } from "child_process";
9
+
10
+ // src/logging/Logger.ts
11
+ var LEVEL_ORDER = {
12
+ error: 0,
13
+ warn: 1,
14
+ info: 2,
15
+ debug: 3,
16
+ trace: 4
17
+ };
18
+ var REDACT_KEYS = ["password", "token", "secret", "authorization", "cookie"];
19
+ function redactValue(value) {
20
+ if (typeof value === "string") {
21
+ try {
22
+ const url = new URL(value);
23
+ if (url.search) {
24
+ url.search = "?redacted";
25
+ }
26
+ return url.toString();
27
+ } catch {
28
+ return value;
29
+ }
30
+ }
31
+ if (value && typeof value === "object") {
32
+ return JSON.stringify(value, (key, val) => {
33
+ if (REDACT_KEYS.some((k) => key.toLowerCase().includes(k))) {
34
+ return "[redacted]";
35
+ }
36
+ return val;
37
+ });
38
+ }
39
+ return String(value);
40
+ }
41
+ var Logger = class {
42
+ level;
43
+ constructor(level = "info") {
44
+ this.level = level;
45
+ }
46
+ setLevel(level) {
47
+ this.level = level;
48
+ }
49
+ error(message, ...args) {
50
+ this.log("error", message, ...args);
51
+ }
52
+ warn(message, ...args) {
53
+ this.log("warn", message, ...args);
54
+ }
55
+ info(message, ...args) {
56
+ this.log("info", message, ...args);
57
+ }
58
+ debug(message, ...args) {
59
+ this.log("debug", message, ...args);
60
+ }
61
+ trace(message, ...args) {
62
+ this.log("trace", message, ...args);
63
+ }
64
+ log(level, message, ...args) {
65
+ if (LEVEL_ORDER[level] > LEVEL_ORDER[this.level]) {
66
+ return;
67
+ }
68
+ const time = (/* @__PURE__ */ new Date()).toISOString();
69
+ const suffix = args.length ? " " + args.map(redactValue).join(" ") : "";
70
+ const line = `[${time}] [${level}] ${message}${suffix}`;
71
+ if (level === "error") {
72
+ console.error(line);
73
+ } else if (level === "warn") {
74
+ console.warn(line);
75
+ } else {
76
+ console.log(line);
77
+ }
78
+ }
79
+ };
80
+
81
+ // src/core/Events.ts
82
+ import { EventEmitter } from "events";
83
+ var AutomationEvents = class {
84
+ emitter = new EventEmitter();
85
+ on(event, handler) {
86
+ this.emitter.on(event, handler);
87
+ }
88
+ off(event, handler) {
89
+ this.emitter.off(event, handler);
90
+ }
91
+ emit(event, payload) {
92
+ this.emitter.emit(event, payload);
93
+ }
94
+ };
95
+
96
+ // src/cdp/Connection.ts
97
+ import WebSocket from "ws";
98
+ import { EventEmitter as EventEmitter3 } from "events";
99
+
100
+ // src/cdp/Session.ts
101
+ import { EventEmitter as EventEmitter2 } from "events";
102
+ var Session = class {
103
+ connection;
104
+ sessionId;
105
+ emitter = new EventEmitter2();
106
+ constructor(connection, sessionId) {
107
+ this.connection = connection;
108
+ this.sessionId = sessionId;
109
+ }
110
+ on(event, handler) {
111
+ this.emitter.on(event, handler);
112
+ }
113
+ once(event, handler) {
114
+ this.emitter.once(event, handler);
115
+ }
116
+ async send(method, params = {}) {
117
+ return this.connection.send(method, params, this.sessionId);
118
+ }
119
+ dispatch(method, params) {
120
+ this.emitter.emit(method, params);
121
+ }
122
+ };
123
+
124
+ // src/cdp/Connection.ts
125
+ var Connection = class {
126
+ ws;
127
+ id = 0;
128
+ callbacks = /* @__PURE__ */ new Map();
129
+ sessions = /* @__PURE__ */ new Map();
130
+ emitter = new EventEmitter3();
131
+ logger;
132
+ closed = false;
133
+ constructor(url, logger) {
134
+ this.logger = logger;
135
+ this.ws = new WebSocket(url);
136
+ this.ws.on("message", (data) => this.onMessage(data.toString()));
137
+ this.ws.on("error", (err) => this.onError(err));
138
+ this.ws.on("close", (code, reason) => this.onClose(code, reason));
139
+ }
140
+ async waitForOpen() {
141
+ if (this.ws.readyState === WebSocket.OPEN) {
142
+ return;
143
+ }
144
+ if (this.ws.readyState === WebSocket.CLOSED) {
145
+ throw new Error("CDP socket is closed");
146
+ }
147
+ await new Promise((resolve, reject) => {
148
+ const onOpen = () => {
149
+ cleanup();
150
+ resolve();
151
+ };
152
+ const onError = (err) => {
153
+ cleanup();
154
+ reject(err);
155
+ };
156
+ const onClose = () => {
157
+ cleanup();
158
+ reject(new Error("CDP socket closed before opening"));
159
+ };
160
+ const cleanup = () => {
161
+ this.ws.off("open", onOpen);
162
+ this.ws.off("error", onError);
163
+ this.ws.off("close", onClose);
164
+ };
165
+ this.ws.on("open", onOpen);
166
+ this.ws.on("error", onError);
167
+ this.ws.on("close", onClose);
168
+ });
169
+ }
170
+ createSession(sessionId) {
171
+ const session = new Session(this, sessionId);
172
+ this.sessions.set(sessionId, session);
173
+ return session;
174
+ }
175
+ removeSession(sessionId) {
176
+ this.sessions.delete(sessionId);
177
+ }
178
+ on(event, handler) {
179
+ this.emitter.on(event, handler);
180
+ }
181
+ async send(method, params = {}, sessionId) {
182
+ await this.waitForOpen();
183
+ const id = ++this.id;
184
+ const payload = sessionId ? { id, method, params, sessionId } : { id, method, params };
185
+ const start = Date.now();
186
+ const promise = new Promise((resolve, reject) => {
187
+ this.callbacks.set(id, { resolve, reject, method, start });
188
+ });
189
+ if (this.logger) {
190
+ this.logger.trace("CDP send", method);
191
+ }
192
+ this.ws.send(JSON.stringify(payload));
193
+ return promise;
194
+ }
195
+ async close() {
196
+ if (this.ws.readyState === WebSocket.CLOSED) {
197
+ return;
198
+ }
199
+ await new Promise((resolve) => {
200
+ this.ws.once("close", () => resolve());
201
+ if (this.ws.readyState === WebSocket.CLOSING) {
202
+ return;
203
+ }
204
+ this.ws.close();
205
+ });
206
+ }
207
+ onError(err) {
208
+ this.logger.error("CDP socket error", err);
209
+ }
210
+ onClose(code, reason) {
211
+ if (this.closed) {
212
+ return;
213
+ }
214
+ this.closed = true;
215
+ const reasonText = reason.toString() || "no reason";
216
+ this.failPending(new Error(`CDP socket closed (${code}): ${reasonText}`));
217
+ this.sessions.clear();
218
+ }
219
+ failPending(error) {
220
+ if (this.callbacks.size === 0) {
221
+ return;
222
+ }
223
+ for (const [, callback] of this.callbacks) {
224
+ callback.reject(error);
225
+ }
226
+ this.callbacks.clear();
227
+ }
228
+ onMessage(message) {
229
+ let parsed;
230
+ try {
231
+ parsed = JSON.parse(message);
232
+ } catch (err) {
233
+ this.logger.warn("Failed to parse CDP message", err);
234
+ return;
235
+ }
236
+ if (typeof parsed.id === "number") {
237
+ const callback = this.callbacks.get(parsed.id);
238
+ if (!callback) {
239
+ return;
240
+ }
241
+ this.callbacks.delete(parsed.id);
242
+ const duration = Date.now() - callback.start;
243
+ this.logger.debug("CDP recv", callback.method, `${duration}ms`);
244
+ if (parsed.error) {
245
+ callback.reject(new Error(parsed.error.message));
246
+ } else {
247
+ callback.resolve(parsed.result);
248
+ }
249
+ return;
250
+ }
251
+ if (parsed.sessionId) {
252
+ const session = this.sessions.get(parsed.sessionId);
253
+ if (session) {
254
+ session.dispatch(parsed.method, parsed.params);
255
+ }
256
+ return;
257
+ }
258
+ if (parsed.method) {
259
+ this.emitter.emit(parsed.method, parsed.params);
260
+ }
261
+ }
262
+ };
263
+
264
+ // src/core/Page.ts
265
+ import fs2 from "fs";
266
+ import path2 from "path";
267
+
268
+ // src/core/Frame.ts
269
+ import fs from "fs";
270
+ import path from "path";
271
+
272
+ // src/core/Waiter.ts
273
+ async function waitFor(predicate, options = {}) {
274
+ const timeoutMs = options.timeoutMs ?? 3e4;
275
+ const intervalMs = options.intervalMs ?? 100;
276
+ const start = Date.now();
277
+ let lastError;
278
+ while (Date.now() - start < timeoutMs) {
279
+ try {
280
+ const result = await predicate();
281
+ if (result) {
282
+ return result;
283
+ }
284
+ } catch (err) {
285
+ lastError = err;
286
+ }
287
+ await new Promise((resolve) => setTimeout(resolve, intervalMs));
288
+ }
289
+ const description = options.description ? ` (${options.description})` : "";
290
+ const error = new Error(`Timeout after ${timeoutMs}ms${description}`);
291
+ error.cause = lastError;
292
+ throw error;
293
+ }
294
+
295
+ // src/core/Selectors.ts
296
+ function isXPathSelector(input) {
297
+ if (input.startsWith("/")) return true;
298
+ if (input.startsWith("./")) return true;
299
+ if (input.startsWith(".//")) return true;
300
+ if (input.startsWith("..")) return true;
301
+ if (input.startsWith("(")) {
302
+ const trimmed = input.trimStart();
303
+ if (trimmed.startsWith("(")) {
304
+ const inner = trimmed.slice(1).trimStart();
305
+ return inner.startsWith("/") || inner.startsWith(".");
306
+ }
307
+ }
308
+ return false;
309
+ }
310
+ function parseSelector(input) {
311
+ const value = input.trim();
312
+ const pierceShadowDom = value.includes(">>>");
313
+ return {
314
+ type: isXPathSelector(value) ? "xpath" : "css",
315
+ value,
316
+ pierceShadowDom
317
+ };
318
+ }
319
+
320
+ // src/core/ShadowDom.ts
321
+ function getElementCtor(root) {
322
+ if (typeof Element !== "undefined") return Element;
323
+ const doc = root.ownerDocument;
324
+ const view = (doc || root).defaultView;
325
+ return view?.Element ?? null;
326
+ }
327
+ function isElementNode(node, ElementCtor) {
328
+ return node instanceof ElementCtor;
329
+ }
330
+ function nodeChildren(node) {
331
+ if (!("children" in node)) {
332
+ return [];
333
+ }
334
+ return Array.from(node.children);
335
+ }
336
+ function querySelectorDeep(root, selector) {
337
+ const ElementCtor = getElementCtor(root);
338
+ if (!ElementCtor) return null;
339
+ const elementCtor = ElementCtor;
340
+ function walk(node, sel, results2) {
341
+ if (isElementNode(node, elementCtor) && node.matches(sel)) {
342
+ results2.push(node);
343
+ }
344
+ if (isElementNode(node, elementCtor) && node.shadowRoot) {
345
+ walk(node.shadowRoot, sel, results2);
346
+ }
347
+ for (const child of nodeChildren(node)) {
348
+ walk(child, sel, results2);
349
+ }
350
+ }
351
+ function findAll(rootNode, sel) {
352
+ const results2 = [];
353
+ walk(rootNode, sel, results2);
354
+ return results2;
355
+ }
356
+ if (selector.includes(">>>")) {
357
+ const parts = selector.split(">>>").map((p) => p.trim()).filter(Boolean);
358
+ let scope = [root];
359
+ for (const part of parts) {
360
+ const matches = [];
361
+ for (const item of scope) {
362
+ matches.push(...findAll(item, part));
363
+ }
364
+ if (matches.length === 0) return null;
365
+ scope = matches;
366
+ }
367
+ return scope[0] ?? null;
368
+ }
369
+ const results = findAll(root, selector);
370
+ return results[0] ?? null;
371
+ }
372
+ function querySelectorAllDeep(root, selector) {
373
+ const ElementCtor = getElementCtor(root);
374
+ if (!ElementCtor) return [];
375
+ const elementCtor = ElementCtor;
376
+ function walk(node, sel, results) {
377
+ if (isElementNode(node, elementCtor) && node.matches(sel)) {
378
+ results.push(node);
379
+ }
380
+ if (isElementNode(node, elementCtor) && node.shadowRoot) {
381
+ walk(node.shadowRoot, sel, results);
382
+ }
383
+ for (const child of nodeChildren(node)) {
384
+ walk(child, sel, results);
385
+ }
386
+ }
387
+ function findAll(rootNode, sel) {
388
+ const results = [];
389
+ walk(rootNode, sel, results);
390
+ return results;
391
+ }
392
+ if (selector.includes(">>>")) {
393
+ const parts = selector.split(">>>").map((p) => p.trim()).filter(Boolean);
394
+ let scope = [root];
395
+ for (const part of parts) {
396
+ const matches = [];
397
+ for (const item of scope) {
398
+ matches.push(...findAll(item, part));
399
+ }
400
+ scope = matches;
401
+ if (scope.length === 0) return [];
402
+ }
403
+ return scope.filter((el) => el instanceof Element);
404
+ }
405
+ return findAll(root, selector);
406
+ }
407
+ function serializeShadowDomHelpers() {
408
+ return {
409
+ querySelectorDeep: querySelectorDeep.toString(),
410
+ querySelectorAllDeep: querySelectorAllDeep.toString()
411
+ };
412
+ }
413
+
414
+ // src/core/Locator.ts
415
+ var Locator = class {
416
+ frame;
417
+ selector;
418
+ options;
419
+ constructor(frame, selector, options = {}) {
420
+ this.frame = frame;
421
+ this.selector = selector;
422
+ this.options = options;
423
+ }
424
+ async click(options = {}) {
425
+ return this.frame.click(this.selector, { ...this.options, ...options });
426
+ }
427
+ async dblclick(options = {}) {
428
+ return this.frame.dblclick(this.selector, { ...this.options, ...options });
429
+ }
430
+ async type(text, options = {}) {
431
+ return this.frame.type(this.selector, text, { ...this.options, ...options });
432
+ }
433
+ async exists() {
434
+ return this.frame.exists(this.selector, this.options);
435
+ }
436
+ async text() {
437
+ return this.frame.text(this.selector, this.options);
438
+ }
439
+ };
440
+
441
+ // src/core/Frame.ts
442
+ var Frame = class {
443
+ id;
444
+ name;
445
+ url;
446
+ parentId;
447
+ session;
448
+ logger;
449
+ events;
450
+ contextId;
451
+ defaultTimeout = 3e4;
452
+ constructor(id, session, logger, events) {
453
+ this.id = id;
454
+ this.session = session;
455
+ this.logger = logger;
456
+ this.events = events;
457
+ }
458
+ setExecutionContext(contextId) {
459
+ this.contextId = contextId;
460
+ }
461
+ getExecutionContext() {
462
+ return this.contextId;
463
+ }
464
+ setMeta(meta) {
465
+ this.name = meta.name;
466
+ this.url = meta.url;
467
+ this.parentId = meta.parentId;
468
+ }
469
+ async evaluate(fnOrString, ...args) {
470
+ return this.evaluateInContext(fnOrString, args);
471
+ }
472
+ async query(selector, options = {}) {
473
+ return this.querySelectorInternal(selector, options, false);
474
+ }
475
+ async queryAll(selector, options = {}) {
476
+ return this.querySelectorAllInternal(selector, options, false);
477
+ }
478
+ async queryXPath(selector, options = {}) {
479
+ return this.querySelectorInternal(selector, options, true);
480
+ }
481
+ async queryAllXPath(selector, options = {}) {
482
+ return this.querySelectorAllInternal(selector, options, true);
483
+ }
484
+ locator(selector, options = {}) {
485
+ return new Locator(this, selector, options);
486
+ }
487
+ async click(selector, options = {}) {
488
+ await this.performClick(selector, options, false);
489
+ }
490
+ async dblclick(selector, options = {}) {
491
+ await this.performClick(selector, options, true);
492
+ }
493
+ async type(selector, text, options = {}) {
494
+ const start = Date.now();
495
+ const parsed = parseSelector(selector);
496
+ const pierce = Boolean(parsed.pierceShadowDom);
497
+ this.events.emit("action:start", { name: "type", selector, frameId: this.id, sensitive: options.sensitive });
498
+ await waitFor(async () => {
499
+ const box = await this.resolveElementBox(selector, options);
500
+ if (!box || !box.visible) {
501
+ return false;
502
+ }
503
+ return true;
504
+ }, { timeoutMs: options.timeoutMs ?? this.defaultTimeout, description: `type ${selector}` });
505
+ const helpers = serializeShadowDomHelpers();
506
+ const focusExpression = `(function() {
507
+ const querySelectorDeep = ${helpers.querySelectorDeep};
508
+ const root = document;
509
+ const selector = ${JSON.stringify(selector)};
510
+ const el = ${pierce ? "querySelectorDeep(root, selector)" : "root.querySelector(selector)"};
511
+ if (!el) {
512
+ return;
513
+ }
514
+ el.focus();
515
+ })()`;
516
+ const focusParams = {
517
+ expression: focusExpression,
518
+ returnByValue: true
519
+ };
520
+ if (this.contextId) {
521
+ focusParams.contextId = this.contextId;
522
+ }
523
+ await this.session.send("Runtime.evaluate", focusParams);
524
+ await this.session.send("Input.insertText", { text });
525
+ const duration = Date.now() - start;
526
+ this.events.emit("action:end", { name: "type", selector, frameId: this.id, durationMs: duration, sensitive: options.sensitive });
527
+ this.logger.debug("Type", selector, `${duration}ms`);
528
+ }
529
+ async typeSecure(selector, text, options = {}) {
530
+ return this.type(selector, text, { ...options, sensitive: true });
531
+ }
532
+ async fillInput(selector, value, options = {}) {
533
+ const start = Date.now();
534
+ this.events.emit("action:start", { name: "fillInput", selector, frameId: this.id });
535
+ await waitFor(async () => {
536
+ const expression = `(function() {
537
+ const selector = ${JSON.stringify(selector)};
538
+ const findDeep = (sel) => {
539
+ if (sel.includes(">>>")) {
540
+ const parts = sel.split(">>>").map((s) => s.trim()).filter(Boolean);
541
+ let scope = [document];
542
+ for (const part of parts) {
543
+ const next = [];
544
+ for (const node of scope) {
545
+ const roots = [node];
546
+ if (node instanceof Element && node.shadowRoot) roots.push(node.shadowRoot);
547
+ for (const root of roots) {
548
+ next.push(...root.querySelectorAll(part));
549
+ }
550
+ }
551
+ if (!next.length) return null;
552
+ scope = next;
553
+ }
554
+ return scope[0] || null;
555
+ }
556
+ return document.querySelector(sel);
557
+ };
558
+ const el = findDeep(selector);
559
+ if (!el) return false;
560
+ if (!(el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement || el instanceof HTMLSelectElement)) {
561
+ return false;
562
+ }
563
+ el.value = ${JSON.stringify(value)};
564
+ el.dispatchEvent(new Event("input", { bubbles: true }));
565
+ el.dispatchEvent(new Event("change", { bubbles: true }));
566
+ return true;
567
+ })()`;
568
+ const params = {
569
+ expression,
570
+ returnByValue: true
571
+ };
572
+ if (this.contextId) {
573
+ params.contextId = this.contextId;
574
+ }
575
+ const result = await this.session.send("Runtime.evaluate", params);
576
+ return Boolean(result.result?.value);
577
+ }, { timeoutMs: options.timeoutMs ?? this.defaultTimeout, description: `fillInput ${selector}` });
578
+ const duration = Date.now() - start;
579
+ this.events.emit("action:end", { name: "fillInput", selector, frameId: this.id, durationMs: duration });
580
+ this.logger.debug("FillInput", selector, `${duration}ms`);
581
+ }
582
+ async findLocators(options = {}) {
583
+ const start = Date.now();
584
+ this.events.emit("action:start", { name: "findLocators", frameId: this.id });
585
+ const artifactsDir = path.resolve(process.cwd(), "artifacts");
586
+ try {
587
+ fs.mkdirSync(artifactsDir, { recursive: true });
588
+ } catch {
589
+ }
590
+ const resolveOut = (name) => {
591
+ if (!name) return null;
592
+ const base = path.basename(name);
593
+ return path.join(artifactsDir, base);
594
+ };
595
+ const outputJson = resolveOut(options.outputJson || options.outputPath);
596
+ const outputHtml = resolveOut(options.outputHtml);
597
+ const expression = `(function() {
598
+ const highlight = ${options.highlight !== false};
599
+ const previous = Array.from(document.querySelectorAll(".__cdpwright-locator-overlay"));
600
+ previous.forEach((el) => el.remove());
601
+
602
+ const cssEscape = (value) => {
603
+ if (typeof CSS !== "undefined" && CSS.escape) return CSS.escape(value);
604
+ return value.replace(/[^a-zA-Z0-9_-]/g, (c) => "\\\\" + c.charCodeAt(0).toString(16) + " ");
605
+ };
606
+
607
+ const visible = (el) => {
608
+ const rect = el.getBoundingClientRect();
609
+ const style = window.getComputedStyle(el);
610
+ return rect.width > 0 && rect.height > 0 && style.visibility !== "hidden" && style.display !== "none" && Number(style.opacity || "1") > 0;
611
+ };
612
+
613
+ const siblingsIndex = (el) => {
614
+ if (!el.parentElement) return 1;
615
+ const sibs = Array.from(el.parentElement.children).filter((n) => n.tagName === el.tagName);
616
+ return sibs.indexOf(el) + 1;
617
+ };
618
+
619
+ const xpathFor = (el) => {
620
+ const parts = [];
621
+ let node = el;
622
+ while (node && node.nodeType === 1 && node !== document.documentElement) {
623
+ const idx = siblingsIndex(node);
624
+ parts.unshift(node.tagName.toLowerCase() + "[" + idx + "]");
625
+ node = node.parentElement;
626
+ }
627
+ return "//" + parts.join("/");
628
+ };
629
+
630
+ const buildLocator = (el) => {
631
+ const testid = el.getAttribute("data-testid");
632
+ const id = el.id;
633
+ const name = el.getAttribute("name");
634
+ const aria = el.getAttribute("aria-label");
635
+ const labelledBy = el.getAttribute("aria-labelledby");
636
+ const placeholder = el.getAttribute("placeholder");
637
+ const role = el.getAttribute("role");
638
+ const labelText = (() => {
639
+ if (labelledBy) {
640
+ const ref = document.getElementById(labelledBy);
641
+ if (ref) return ref.textContent?.trim() || "";
642
+ }
643
+ if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement || el instanceof HTMLSelectElement) {
644
+ const idRef = el.id && document.querySelector("label[for='" + el.id.replace(/'/g, "\\\\'") + "']");
645
+ if (idRef) return idRef.textContent?.trim() || "";
646
+ const wrap = el.closest("label");
647
+ if (wrap) return wrap.textContent?.trim() || "";
648
+ }
649
+ return "";
650
+ })();
651
+
652
+ let css = "";
653
+ let quality = "low";
654
+ let reason = "fallback";
655
+ if (testid) {
656
+ css = "[data-testid="" + testid.replace(/"/g, '\\\\"') + ""]";
657
+ quality = "high";
658
+ reason = "data-testid";
659
+ } else if (id) {
660
+ css = "#" + cssEscape(id);
661
+ quality = "high";
662
+ reason = "id";
663
+ } else if (name) {
664
+ css = "[name="" + name.replace(/"/g, '\\\\"') + ""]";
665
+ quality = "ok";
666
+ reason = "name";
667
+ } else if (aria) {
668
+ css = "[aria-label="" + aria.replace(/"/g, '\\\\"') + ""]";
669
+ quality = "ok";
670
+ reason = "aria-label";
671
+ } else if (placeholder) {
672
+ css = "[placeholder="" + placeholder.replace(/"/g, '\\\\"') + ""]";
673
+ quality = "low";
674
+ reason = "placeholder";
675
+ } else if (labelText) {
676
+ css = el.tagName.toLowerCase() + "[aria-label="" + labelText.replace(/"/g, '\\\\"') + ""]";
677
+ quality = "low";
678
+ reason = "label text";
679
+ } else {
680
+ const nth = siblingsIndex(el);
681
+ css = el.tagName.toLowerCase() + ":nth-of-type(" + nth + ")";
682
+ quality = "low";
683
+ reason = "nth-of-type";
684
+ }
685
+
686
+ const tag = el.tagName.toLowerCase();
687
+ const type = el.getAttribute("type") || "";
688
+ const text = (el.textContent || "").trim().slice(0, 80);
689
+
690
+ return {
691
+ name: labelText || aria || placeholder || name || testid || id || tag,
692
+ css,
693
+ xpath: xpathFor(el),
694
+ quality,
695
+ reason,
696
+ visible: true,
697
+ tag,
698
+ type,
699
+ role,
700
+ text,
701
+ id,
702
+ nameAttr: name,
703
+ dataTestid: testid
704
+ };
705
+ };
706
+
707
+ const preferredSelectors = ["input", "select", "textarea", "button", "a[href]", "[role='button']", "[contenteditable='true']", "h1", "h2", "h3", "h4", "h5", "h6", "label", "legend", "fieldset"];
708
+ let nodes = Array.from(document.querySelectorAll(preferredSelectors.join(", ")));
709
+ if (nodes.length === 0) {
710
+ nodes = Array.from(document.querySelectorAll("*")).filter((el) => {
711
+ if (!(el instanceof HTMLElement)) return false;
712
+ const tag = el.tagName.toLowerCase();
713
+ if (preferredSelectors.includes(tag)) return true;
714
+ if (tag === "a" && el.hasAttribute("href")) return true;
715
+ if (el.getAttribute("role") === "button") return true;
716
+ if (el.hasAttribute("contenteditable")) return true;
717
+ return false;
718
+ });
719
+ }
720
+ const results = [];
721
+ nodes.forEach((el) => {
722
+ const isVisible = visible(el);
723
+ const locator = buildLocator(el);
724
+ locator.visible = isVisible;
725
+ results.push(locator);
726
+ });
727
+
728
+ if (highlight) {
729
+ results.forEach((loc, index) => {
730
+ const el = nodes[index];
731
+ if (!el) return;
732
+ const rect = el.getBoundingClientRect();
733
+ const overlay = document.createElement("div");
734
+ overlay.className = "__cdpwright-locator-overlay";
735
+ overlay.style.position = "absolute";
736
+ overlay.style.left = rect.x + window.scrollX + "px";
737
+ overlay.style.top = rect.y + window.scrollY + "px";
738
+ overlay.style.width = rect.width + "px";
739
+ overlay.style.height = rect.height + "px";
740
+ overlay.style.border = "2px solid #e67e22";
741
+ overlay.style.borderRadius = "6px";
742
+ overlay.style.pointerEvents = "none";
743
+ overlay.style.zIndex = "99999";
744
+ const badge = document.createElement("div");
745
+ badge.textContent = String(index);
746
+ badge.style.position = "absolute";
747
+ badge.style.top = "-10px";
748
+ badge.style.left = "-10px";
749
+ badge.style.background = "#e67e22";
750
+ badge.style.color = "#fff";
751
+ badge.style.fontSize = "12px";
752
+ badge.style.padding = "2px 6px";
753
+ badge.style.borderRadius = "10px";
754
+ overlay.appendChild(badge);
755
+ document.body.appendChild(overlay);
756
+ });
757
+ }
758
+
759
+ if (console && console.table) {
760
+ console.table(results.map((r, idx) => ({ idx, name: r.name, css: r.css, quality: r.quality, reason: r.reason, tag: r.tag, type: r.type })));
761
+ }
762
+ return results;
763
+ })()`;
764
+ const params = {
765
+ expression,
766
+ returnByValue: true
767
+ };
768
+ if (this.contextId) {
769
+ params.contextId = this.contextId;
770
+ }
771
+ let result = await this.session.send("Runtime.evaluate", params);
772
+ const duration = Date.now() - start;
773
+ this.events.emit("action:end", { name: "findLocators", frameId: this.id, durationMs: duration });
774
+ const value = result.result?.value ?? [];
775
+ if (Array.isArray(value) && value.length > 0) {
776
+ this.logger.info("FindLocators", `${value.length} candidates`, value.slice(0, 5).map((v) => v.css || v.name || v.tag));
777
+ if (outputJson) {
778
+ try {
779
+ fs.writeFileSync(outputJson, JSON.stringify(value, null, 2), "utf-8");
780
+ this.logger.info("FindLocators", `written to ${outputJson}`);
781
+ } catch (err) {
782
+ this.logger.warn("FindLocators write failed", err);
783
+ }
784
+ }
785
+ if (outputHtml) {
786
+ this.writeLocatorHtml(outputHtml, value);
787
+ }
788
+ return value;
789
+ }
790
+ result = await this.session.send("Runtime.evaluate", {
791
+ expression: `(function() {
792
+ const allowed = ["input","select","textarea","button","a","label","legend","fieldset","h1","h2","h3","h4","h5","h6"];
793
+ const skip = ["html","head","body","meta","link","script","style"];
794
+ return Array.from(document.querySelectorAll("*"))
795
+ .filter((el) => {
796
+ if (!(el instanceof HTMLElement)) return false;
797
+ const tag = el.tagName.toLowerCase();
798
+ if (skip.includes(tag)) return false;
799
+ if (tag === "a" && el.hasAttribute("href")) return true;
800
+ if (el.getAttribute("role") === "button") return true;
801
+ if (el.hasAttribute("contenteditable")) return true;
802
+ return allowed.includes(tag);
803
+ })
804
+ .slice(0, 200)
805
+ .map((el, idx) => ({
806
+ name: el.getAttribute("aria-label") || el.getAttribute("name") || el.getAttribute("data-testid") || el.id || el.tagName.toLowerCase() + "-" + idx,
807
+ css: el.id ? "#" + el.id : el.getAttribute("data-testid") ? "[data-testid=\\"" + el.getAttribute("data-testid") + "\\"]" : el.tagName.toLowerCase(),
808
+ xpath: "",
809
+ quality: "low",
810
+ reason: "fallback",
811
+ visible: true,
812
+ tag: el.tagName.toLowerCase(),
813
+ type: el.getAttribute("type") || "",
814
+ role: el.getAttribute("role") || ""
815
+ }));
816
+ })()`,
817
+ returnByValue: true
818
+ });
819
+ const fallback = result.result?.value ?? [];
820
+ this.logger.info("FindLocators", `${fallback.length} candidates`, fallback.slice(0, 5).map((v) => v.css || v.name || v.tag));
821
+ if (outputJson) {
822
+ try {
823
+ fs.writeFileSync(outputJson, JSON.stringify(fallback, null, 2), "utf-8");
824
+ this.logger.info("FindLocators", `written to ${outputJson}`);
825
+ } catch (err) {
826
+ this.logger.warn("FindLocators write failed", err);
827
+ }
828
+ }
829
+ if (outputHtml) {
830
+ this.writeLocatorHtml(outputHtml, fallback);
831
+ }
832
+ return fallback;
833
+ }
834
+ writeLocatorHtml(filePath, data) {
835
+ const html = `<!doctype html>
836
+ <html>
837
+ <head>
838
+ <meta charset="utf-8" />
839
+ <title>Locators</title>
840
+ <style>
841
+ body { font-family: "Segoe UI", system-ui, -apple-system, sans-serif; padding: 16px; background: #f8f9fa; color: #222; }
842
+ h1 { font-size: 20px; margin-bottom: 12px; }
843
+ table { border-collapse: collapse; width: 100%; background: #fff; box-shadow: 0 2px 6px rgba(0,0,0,0.08); }
844
+ th, td { border: 1px solid #e5e7eb; padding: 8px; font-size: 13px; text-align: left; }
845
+ th { background: linear-gradient(180deg, #f6f7fb, #edf0f7); font-weight: 600; }
846
+ tr:nth-child(even) { background: #fafbfc; }
847
+ .copy { color: #2563eb; text-decoration: underline dotted; cursor: pointer; font-size: 12px; background: none; border: none; padding: 0; opacity: 0; transition: opacity 0.15s ease; }
848
+ tr:hover .copy { opacity: 1; }
849
+ .copy:hover { color: #1d4ed8; }
850
+ .loc-value { margin-left: 6px; font-family: ui-monospace, SFMono-Regular, SFMono, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; }
851
+ </style>
852
+ </head>
853
+ <body>
854
+ <h1>Locator candidates</h1>
855
+ <table>
856
+ <thead>
857
+ <tr><th>#</th><th>Name</th><th>CSS</th><th>XPath</th><th>Quality</th><th>Reason</th><th>Visible</th></tr>
858
+ </thead>
859
+ <tbody id="rows"></tbody>
860
+ </table>
861
+ <script>
862
+ const data = ${JSON.stringify(data)};
863
+ const rows = document.getElementById("rows");
864
+ data.forEach((loc, idx) => {
865
+ const tr = document.createElement("tr");
866
+ const cells = [
867
+ idx,
868
+ loc.name || "",
869
+ loc.css || "",
870
+ loc.xpath || "",
871
+ loc.quality || "",
872
+ loc.reason || "",
873
+ String(loc.visible)
874
+ ];
875
+ cells.forEach((val, i) => {
876
+ const td = document.createElement("td");
877
+ if (i === 2 || i === 3) {
878
+ const link = document.createElement("button");
879
+ link.className = "copy";
880
+ link.textContent = "copy";
881
+ link.addEventListener("click", async (e) => {
882
+ e.preventDefault();
883
+ try { await navigator.clipboard.writeText(val); link.textContent = "copied"; setTimeout(() => link.textContent = "copy", 1000); }
884
+ catch { link.textContent = "error"; }
885
+ });
886
+ const span = document.createElement("span");
887
+ span.className = "loc-value";
888
+ span.textContent = val;
889
+ td.appendChild(link);
890
+ td.appendChild(span);
891
+ } else {
892
+ td.textContent = val;
893
+ }
894
+ tr.appendChild(td);
895
+ });
896
+ rows.appendChild(tr);
897
+ });
898
+ </script>
899
+ </body>
900
+ </html>`;
901
+ try {
902
+ fs.writeFileSync(filePath, html, "utf-8");
903
+ this.logger.info("FindLocators", `HTML written to ${filePath}`);
904
+ } catch (err) {
905
+ this.logger.warn("FindLocators HTML write failed", err);
906
+ }
907
+ }
908
+ async exists(selector, options = {}) {
909
+ const handle = await this.query(selector, options);
910
+ if (handle) {
911
+ await this.releaseObject(handle.objectId);
912
+ return true;
913
+ }
914
+ return false;
915
+ }
916
+ async isVisible(selector, options = {}) {
917
+ const box = await this.resolveElementBox(selector, options);
918
+ return Boolean(box && box.visible);
919
+ }
920
+ async text(selector, options = {}) {
921
+ return this.evalOnSelector(selector, options, false, `
922
+ if (!el) {
923
+ return null;
924
+ }
925
+ return el.textContent || "";
926
+ `);
927
+ }
928
+ async textSecure(selector, options = {}) {
929
+ const start = Date.now();
930
+ this.events.emit("action:start", { name: "text", selector, frameId: this.id, sensitive: true });
931
+ const result = await this.text(selector, options);
932
+ const duration = Date.now() - start;
933
+ this.events.emit("action:end", { name: "text", selector, frameId: this.id, durationMs: duration, sensitive: true });
934
+ return result;
935
+ }
936
+ async selectOption(selector, value) {
937
+ await this.evaluate(
938
+ (sel, val) => {
939
+ const el = document.querySelector(sel);
940
+ if (!(el instanceof HTMLSelectElement)) return false;
941
+ el.value = val;
942
+ el.dispatchEvent(new Event("input", { bubbles: true }));
943
+ el.dispatchEvent(new Event("change", { bubbles: true }));
944
+ return true;
945
+ },
946
+ selector,
947
+ value
948
+ );
949
+ }
950
+ async setFileInput(selector, name, contents, options = {}) {
951
+ await this.evaluate(
952
+ (sel, fileName, text, mime) => {
953
+ const input = document.querySelector(sel);
954
+ if (!(input instanceof HTMLInputElement)) return false;
955
+ const file = new File([text], fileName, { type: mime || "text/plain" });
956
+ const data = new DataTransfer();
957
+ data.items.add(file);
958
+ input.files = data.files;
959
+ input.dispatchEvent(new Event("input", { bubbles: true }));
960
+ input.dispatchEvent(new Event("change", { bubbles: true }));
961
+ return true;
962
+ },
963
+ selector,
964
+ name,
965
+ contents,
966
+ options.mimeType || "text/plain"
967
+ );
968
+ }
969
+ async attribute(selector, name, options = {}) {
970
+ return this.evalOnSelector(selector, options, false, `
971
+ if (!el || !(el instanceof Element)) {
972
+ return null;
973
+ }
974
+ return el.getAttribute(${JSON.stringify(name)});
975
+ `);
976
+ }
977
+ async value(selector, options = {}) {
978
+ return this.evalOnSelector(selector, options, false, `
979
+ if (!el) {
980
+ return null;
981
+ }
982
+ if ("value" in el) {
983
+ return el.value ?? "";
984
+ }
985
+ return el.getAttribute("value");
986
+ `);
987
+ }
988
+ async valueSecure(selector, options = {}) {
989
+ const start = Date.now();
990
+ this.events.emit("action:start", { name: "value", selector, frameId: this.id, sensitive: true });
991
+ const result = await this.value(selector, options);
992
+ const duration = Date.now() - start;
993
+ this.events.emit("action:end", { name: "value", selector, frameId: this.id, durationMs: duration, sensitive: true });
994
+ return result;
995
+ }
996
+ async isEnabled(selector, options = {}) {
997
+ return this.evalOnSelector(selector, options, false, `
998
+ if (!el) {
999
+ return null;
1000
+ }
1001
+ const disabled = Boolean(el.disabled) || el.hasAttribute("disabled");
1002
+ const ariaDisabled = el.getAttribute && el.getAttribute("aria-disabled") === "true";
1003
+ return !(disabled || ariaDisabled);
1004
+ `);
1005
+ }
1006
+ async isChecked(selector, options = {}) {
1007
+ return this.evalOnSelector(selector, options, false, `
1008
+ if (!el) {
1009
+ return null;
1010
+ }
1011
+ const aria = el.getAttribute && el.getAttribute("aria-checked");
1012
+ if (aria === "true") {
1013
+ return true;
1014
+ }
1015
+ if (aria === "false") {
1016
+ return false;
1017
+ }
1018
+ if ("checked" in el) {
1019
+ return Boolean(el.checked);
1020
+ }
1021
+ return null;
1022
+ `);
1023
+ }
1024
+ async count(selector, options = {}) {
1025
+ const parsed = parseSelector(selector);
1026
+ const pierce = Boolean(parsed.pierceShadowDom);
1027
+ const helpers = serializeShadowDomHelpers();
1028
+ const expression = parsed.type === "xpath" ? `(function() {
1029
+ const result = document.evaluate(${JSON.stringify(parsed.value)}, document, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
1030
+ return result.snapshotLength;
1031
+ })()` : `(function() {
1032
+ const querySelectorAllDeep = ${helpers.querySelectorAllDeep};
1033
+ const root = document;
1034
+ const selector = ${JSON.stringify(parsed.value)};
1035
+ const nodes = ${pierce ? "querySelectorAllDeep(root, selector)" : "root.querySelectorAll(selector)"};
1036
+ return nodes.length;
1037
+ })()`;
1038
+ const params = {
1039
+ expression,
1040
+ returnByValue: true
1041
+ };
1042
+ if (this.contextId) {
1043
+ params.contextId = this.contextId;
1044
+ }
1045
+ const result = await this.session.send("Runtime.evaluate", params);
1046
+ return result.result.value ?? 0;
1047
+ }
1048
+ async classes(selector, options = {}) {
1049
+ return this.evalOnSelector(selector, options, false, `
1050
+ if (!el) {
1051
+ return null;
1052
+ }
1053
+ if (!el.classList) {
1054
+ return [];
1055
+ }
1056
+ return Array.from(el.classList);
1057
+ `);
1058
+ }
1059
+ async css(selector, property, options = {}) {
1060
+ return this.evalOnSelector(selector, options, false, `
1061
+ if (!el) {
1062
+ return null;
1063
+ }
1064
+ const style = window.getComputedStyle(el);
1065
+ return style.getPropertyValue(${JSON.stringify(property)}) || "";
1066
+ `);
1067
+ }
1068
+ async hasFocus(selector, options = {}) {
1069
+ return this.evalOnSelector(selector, options, false, `
1070
+ if (!el) {
1071
+ return null;
1072
+ }
1073
+ return document.activeElement === el;
1074
+ `);
1075
+ }
1076
+ async isInViewport(selector, options = {}, fully = false) {
1077
+ return this.evalOnSelector(selector, options, false, `
1078
+ if (!el) {
1079
+ return null;
1080
+ }
1081
+ const rect = el.getBoundingClientRect();
1082
+ const viewWidth = window.innerWidth || document.documentElement.clientWidth;
1083
+ const viewHeight = window.innerHeight || document.documentElement.clientHeight;
1084
+ if (${fully ? "true" : "false"}) {
1085
+ return rect.top >= 0 && rect.left >= 0 && rect.bottom <= viewHeight && rect.right <= viewWidth;
1086
+ }
1087
+ return rect.bottom > 0 && rect.right > 0 && rect.top < viewHeight && rect.left < viewWidth;
1088
+ `);
1089
+ }
1090
+ async isEditable(selector, options = {}) {
1091
+ return this.evalOnSelector(selector, options, false, `
1092
+ if (!el) {
1093
+ return null;
1094
+ }
1095
+ const disabled = Boolean(el.disabled) || el.hasAttribute("disabled");
1096
+ const readOnly = Boolean(el.readOnly) || el.hasAttribute("readonly");
1097
+ const ariaDisabled = el.getAttribute && el.getAttribute("aria-disabled") === "true";
1098
+ return !(disabled || readOnly || ariaDisabled);
1099
+ `);
1100
+ }
1101
+ async performClick(selector, options, isDouble) {
1102
+ const start = Date.now();
1103
+ const actionName = isDouble ? "dblclick" : "click";
1104
+ this.events.emit("action:start", { name: actionName, selector, frameId: this.id });
1105
+ const box = await waitFor(async () => {
1106
+ const result = await this.resolveElementBox(selector, options);
1107
+ if (!result || !result.visible) {
1108
+ return null;
1109
+ }
1110
+ return result;
1111
+ }, { timeoutMs: options.timeoutMs ?? this.defaultTimeout, description: `${actionName} ${selector}` });
1112
+ const centerX = box.x + box.width / 2;
1113
+ const centerY = box.y + box.height / 2;
1114
+ await this.session.send("Input.dispatchMouseEvent", { type: "mouseMoved", x: centerX, y: centerY });
1115
+ await this.session.send("Input.dispatchMouseEvent", { type: "mousePressed", x: centerX, y: centerY, button: "left", clickCount: 1, buttons: 1 });
1116
+ await this.session.send("Input.dispatchMouseEvent", { type: "mouseReleased", x: centerX, y: centerY, button: "left", clickCount: 1, buttons: 0 });
1117
+ if (isDouble) {
1118
+ await this.session.send("Input.dispatchMouseEvent", { type: "mouseMoved", x: centerX, y: centerY });
1119
+ await this.session.send("Input.dispatchMouseEvent", { type: "mousePressed", x: centerX, y: centerY, button: "left", clickCount: 2, buttons: 1 });
1120
+ await this.session.send("Input.dispatchMouseEvent", { type: "mouseReleased", x: centerX, y: centerY, button: "left", clickCount: 2, buttons: 0 });
1121
+ }
1122
+ const duration = Date.now() - start;
1123
+ this.events.emit("action:end", { name: actionName, selector, frameId: this.id, durationMs: duration });
1124
+ this.logger.debug("Click", selector, `${duration}ms`);
1125
+ }
1126
+ async resolveElementBox(selector, options) {
1127
+ const parsed = parseSelector(selector);
1128
+ const pierce = Boolean(parsed.pierceShadowDom);
1129
+ const helpers = serializeShadowDomHelpers();
1130
+ const expression = parsed.type === "xpath" ? `(function() {
1131
+ const result = document.evaluate(${JSON.stringify(parsed.value)}, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
1132
+ if (!result || !(result instanceof Element)) {
1133
+ return null;
1134
+ }
1135
+ result.scrollIntoView({ block: 'center', inline: 'center' });
1136
+ const rect = result.getBoundingClientRect();
1137
+ const style = window.getComputedStyle(result);
1138
+ return { x: rect.x, y: rect.y, width: rect.width, height: rect.height, visible: rect.width > 0 && rect.height > 0 && style.visibility !== 'hidden' && style.display !== 'none' && Number(style.opacity || '1') > 0 };
1139
+ })()` : `(function() {
1140
+ const querySelectorDeep = ${helpers.querySelectorDeep};
1141
+ const root = document;
1142
+ const selector = ${JSON.stringify(parsed.value)};
1143
+ const el = ${pierce ? "querySelectorDeep(root, selector)" : "root.querySelector(selector)"};
1144
+ if (!el) {
1145
+ return null;
1146
+ }
1147
+ el.scrollIntoView({ block: 'center', inline: 'center' });
1148
+ const rect = el.getBoundingClientRect();
1149
+ const style = window.getComputedStyle(el);
1150
+ return { x: rect.x, y: rect.y, width: rect.width, height: rect.height, visible: rect.width > 0 && rect.height > 0 && style.visibility !== 'hidden' && style.display !== 'none' && Number(style.opacity || '1') > 0 };
1151
+ })()`;
1152
+ const boxParams = {
1153
+ expression,
1154
+ returnByValue: true
1155
+ };
1156
+ if (this.contextId) {
1157
+ boxParams.contextId = this.contextId;
1158
+ }
1159
+ const result = await this.session.send("Runtime.evaluate", boxParams);
1160
+ return result?.result?.value ?? null;
1161
+ }
1162
+ async querySelectorInternal(selector, options, forceXPath) {
1163
+ const parsed = forceXPath ? { type: "xpath", value: selector.trim(), pierceShadowDom: void 0 } : parseSelector(selector);
1164
+ const pierce = Boolean(parsed.pierceShadowDom);
1165
+ const helpers = serializeShadowDomHelpers();
1166
+ const expression = parsed.type === "xpath" ? `(function() {
1167
+ const result = document.evaluate(${JSON.stringify(parsed.value)}, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
1168
+ return result || null;
1169
+ })()` : `(function() {
1170
+ const querySelectorDeep = ${helpers.querySelectorDeep};
1171
+ const root = document;
1172
+ const selector = ${JSON.stringify(parsed.value)};
1173
+ return ${pierce ? "querySelectorDeep(root, selector)" : "root.querySelector(selector)"};
1174
+ })()`;
1175
+ const queryParams = {
1176
+ expression,
1177
+ returnByValue: false
1178
+ };
1179
+ if (this.contextId) {
1180
+ queryParams.contextId = this.contextId;
1181
+ }
1182
+ const response = await this.session.send("Runtime.evaluate", queryParams);
1183
+ if (response.result?.subtype === "null" || !response.result?.objectId) {
1184
+ return null;
1185
+ }
1186
+ return { objectId: response.result.objectId, contextId: this.contextId ?? 0 };
1187
+ }
1188
+ async querySelectorAllInternal(selector, options, forceXPath) {
1189
+ const parsed = forceXPath ? { type: "xpath", value: selector.trim(), pierceShadowDom: void 0 } : parseSelector(selector);
1190
+ const pierce = Boolean(parsed.pierceShadowDom);
1191
+ const helpers = serializeShadowDomHelpers();
1192
+ const expression = parsed.type === "xpath" ? `(function() {
1193
+ const result = document.evaluate(${JSON.stringify(parsed.value)}, document, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
1194
+ const nodes = [];
1195
+ for (let i = 0; i < result.snapshotLength; i += 1) {
1196
+ nodes.push(result.snapshotItem(i));
1197
+ }
1198
+ return nodes;
1199
+ })()` : `(function() {
1200
+ const querySelectorAllDeep = ${helpers.querySelectorAllDeep};
1201
+ const root = document;
1202
+ const selector = ${JSON.stringify(parsed.value)};
1203
+ return ${pierce ? "querySelectorAllDeep(root, selector)" : "Array.from(root.querySelectorAll(selector))"};
1204
+ })()`;
1205
+ const listParams = {
1206
+ expression,
1207
+ returnByValue: false
1208
+ };
1209
+ if (this.contextId) {
1210
+ listParams.contextId = this.contextId;
1211
+ }
1212
+ const response = await this.session.send("Runtime.evaluate", listParams);
1213
+ if (!response.result?.objectId) {
1214
+ return [];
1215
+ }
1216
+ const properties = await this.session.send("Runtime.getProperties", {
1217
+ objectId: response.result.objectId,
1218
+ ownProperties: true
1219
+ });
1220
+ const handles = [];
1221
+ for (const prop of properties.result) {
1222
+ if (prop.name && !/^\d+$/.test(prop.name)) {
1223
+ continue;
1224
+ }
1225
+ const objectId = prop.value?.objectId;
1226
+ if (objectId) {
1227
+ handles.push({ objectId, contextId: this.contextId ?? 0 });
1228
+ }
1229
+ }
1230
+ await this.releaseObject(response.result.objectId);
1231
+ return handles;
1232
+ }
1233
+ async evaluateInContext(fnOrString, args) {
1234
+ if (typeof fnOrString === "string") {
1235
+ const params2 = {
1236
+ expression: fnOrString,
1237
+ returnByValue: true,
1238
+ awaitPromise: true
1239
+ };
1240
+ if (this.contextId) {
1241
+ params2.contextId = this.contextId;
1242
+ }
1243
+ const result2 = await this.session.send("Runtime.evaluate", params2);
1244
+ return result2.result.value;
1245
+ }
1246
+ const serializedArgs = args.map((arg) => serializeArgument(arg)).join(", ");
1247
+ const expression = `(${fnOrString.toString()})(${serializedArgs})`;
1248
+ const params = {
1249
+ expression,
1250
+ returnByValue: true,
1251
+ awaitPromise: true
1252
+ };
1253
+ if (this.contextId) {
1254
+ params.contextId = this.contextId;
1255
+ }
1256
+ const result = await this.session.send("Runtime.evaluate", params);
1257
+ return result.result.value;
1258
+ }
1259
+ async releaseObject(objectId) {
1260
+ try {
1261
+ await this.session.send("Runtime.releaseObject", { objectId });
1262
+ } catch {
1263
+ }
1264
+ }
1265
+ buildElementExpression(selector, options, forceXPath, body) {
1266
+ const parsed = forceXPath ? { type: "xpath", value: selector.trim(), pierceShadowDom: void 0 } : parseSelector(selector);
1267
+ const pierce = options.pierceShadowDom ?? Boolean(parsed.pierceShadowDom);
1268
+ const helpers = serializeShadowDomHelpers();
1269
+ if (parsed.type === "xpath") {
1270
+ return `(function() {
1271
+ const el = document.evaluate(${JSON.stringify(parsed.value)}, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
1272
+ ${body}
1273
+ })()`;
1274
+ }
1275
+ return `(function() {
1276
+ const querySelectorDeep = ${helpers.querySelectorDeep};
1277
+ const root = document;
1278
+ const selector = ${JSON.stringify(parsed.value)};
1279
+ const el = ${pierce ? "querySelectorDeep(root, selector)" : "root.querySelector(selector)"};
1280
+ ${body}
1281
+ })()`;
1282
+ }
1283
+ async evalOnSelector(selector, options, forceXPath, body) {
1284
+ const expression = this.buildElementExpression(selector, options, forceXPath, body);
1285
+ const params = {
1286
+ expression,
1287
+ returnByValue: true
1288
+ };
1289
+ if (this.contextId) {
1290
+ params.contextId = this.contextId;
1291
+ }
1292
+ const result = await this.session.send("Runtime.evaluate", params);
1293
+ return result.result.value;
1294
+ }
1295
+ };
1296
+ function serializeArgument(value) {
1297
+ if (value === void 0) {
1298
+ return "undefined";
1299
+ }
1300
+ return JSON.stringify(value);
1301
+ }
1302
+
1303
+ // src/core/UrlGuard.ts
1304
+ function ensureAllowedUrl(url, options = {}) {
1305
+ let parsed;
1306
+ try {
1307
+ parsed = new URL(url);
1308
+ } catch {
1309
+ throw new Error(`Invalid URL: ${url}`);
1310
+ }
1311
+ if (parsed.protocol === "http:" || parsed.protocol === "https:") {
1312
+ return;
1313
+ }
1314
+ if (parsed.protocol === "file:" && options.allowFileUrl) {
1315
+ return;
1316
+ }
1317
+ throw new Error(`URL protocol not allowed: ${parsed.protocol}`);
1318
+ }
1319
+
1320
+ // src/core/Page.ts
1321
+ var Page = class {
1322
+ session;
1323
+ logger;
1324
+ events;
1325
+ framesById = /* @__PURE__ */ new Map();
1326
+ mainFrameId;
1327
+ lifecycleEvents = /* @__PURE__ */ new Map();
1328
+ defaultTimeout = 3e4;
1329
+ constructor(session, logger, events) {
1330
+ this.session = session;
1331
+ this.logger = logger;
1332
+ this.events = events;
1333
+ }
1334
+ async initialize() {
1335
+ await this.session.send("Page.enable");
1336
+ await this.session.send("DOM.enable");
1337
+ await this.session.send("Runtime.enable");
1338
+ await this.session.send("Network.enable");
1339
+ await this.session.send("Page.setLifecycleEventsEnabled", { enabled: true });
1340
+ this.session.on("Page.frameAttached", (params) => this.onFrameAttached(params));
1341
+ this.session.on("Page.frameNavigated", (params) => this.onFrameNavigated(params));
1342
+ this.session.on("Page.frameDetached", (params) => this.onFrameDetached(params));
1343
+ this.session.on("Runtime.executionContextCreated", (params) => this.onExecutionContextCreated(params));
1344
+ this.session.on("Runtime.executionContextDestroyed", (params) => this.onExecutionContextDestroyed(params));
1345
+ this.session.on("Runtime.executionContextsCleared", () => this.onExecutionContextsCleared());
1346
+ this.session.on("Page.lifecycleEvent", (params) => this.onLifecycleEvent(params));
1347
+ const tree = await this.session.send("Page.getFrameTree");
1348
+ this.buildFrameTree(tree.frameTree);
1349
+ }
1350
+ frames() {
1351
+ return Array.from(this.framesById.values());
1352
+ }
1353
+ mainFrame() {
1354
+ if (!this.mainFrameId) {
1355
+ throw new Error("Main frame not initialized");
1356
+ }
1357
+ const frame = this.framesById.get(this.mainFrameId);
1358
+ if (!frame) {
1359
+ throw new Error("Main frame missing");
1360
+ }
1361
+ return frame;
1362
+ }
1363
+ frame(options) {
1364
+ for (const frame of this.framesById.values()) {
1365
+ if (options.name && frame.name !== options.name) continue;
1366
+ if (options.urlIncludes && !frame.url?.includes(options.urlIncludes)) continue;
1367
+ if (options.predicate && !options.predicate(frame)) continue;
1368
+ return frame;
1369
+ }
1370
+ return null;
1371
+ }
1372
+ locator(selector) {
1373
+ return new Locator(this.mainFrame(), selector);
1374
+ }
1375
+ async goto(url, options = {}) {
1376
+ ensureAllowedUrl(url, { allowFileUrl: options.allowFileUrl });
1377
+ const waitUntil = options.waitUntil ?? "load";
1378
+ const lifecycleName = waitUntil === "domcontentloaded" ? "DOMContentLoaded" : waitUntil === "load" ? "load" : null;
1379
+ if (!lifecycleName) {
1380
+ throw new Error(`Invalid waitUntil "${waitUntil}". Use "load" or "domcontentloaded".`);
1381
+ }
1382
+ const timeoutMs = options.timeoutMs ?? this.defaultTimeout;
1383
+ this.events.emit("action:start", { name: "goto", selector: url, frameId: this.mainFrameId });
1384
+ const start = Date.now();
1385
+ this.lifecycleEvents.clear();
1386
+ const navigation = await this.session.send("Page.navigate", { url });
1387
+ if (navigation?.errorText) {
1388
+ throw new Error(`Navigation failed: ${navigation.errorText}`);
1389
+ }
1390
+ await this.waitForLifecycle(this.mainFrameId, lifecycleName, timeoutMs);
1391
+ const duration = Date.now() - start;
1392
+ this.events.emit("action:end", { name: "goto", selector: url, frameId: this.mainFrameId, durationMs: duration });
1393
+ this.logger.debug("Goto", url, `${duration}ms`);
1394
+ }
1395
+ async query(selector) {
1396
+ return this.mainFrame().query(selector);
1397
+ }
1398
+ async queryAll(selector) {
1399
+ return this.mainFrame().queryAll(selector);
1400
+ }
1401
+ async queryXPath(selector) {
1402
+ return this.mainFrame().queryXPath(selector);
1403
+ }
1404
+ async queryAllXPath(selector) {
1405
+ return this.mainFrame().queryAllXPath(selector);
1406
+ }
1407
+ async click(selector, options) {
1408
+ return this.mainFrame().click(selector, options);
1409
+ }
1410
+ async dblclick(selector, options) {
1411
+ return this.mainFrame().dblclick(selector, options);
1412
+ }
1413
+ async type(selector, text, options) {
1414
+ return this.mainFrame().type(selector, text, options);
1415
+ }
1416
+ async typeSecure(selector, text, options) {
1417
+ return this.mainFrame().typeSecure(selector, text, options);
1418
+ }
1419
+ async fillInput(selector, value, options = {}) {
1420
+ return this.mainFrame().fillInput(selector, value, options);
1421
+ }
1422
+ async evaluate(fnOrString, ...args) {
1423
+ return this.mainFrame().evaluate(fnOrString, ...args);
1424
+ }
1425
+ async textSecure(selector) {
1426
+ return this.mainFrame().textSecure(selector);
1427
+ }
1428
+ async valueSecure(selector) {
1429
+ return this.mainFrame().valueSecure(selector);
1430
+ }
1431
+ async selectOption(selector, value) {
1432
+ return this.mainFrame().selectOption(selector, value);
1433
+ }
1434
+ async setFileInput(selector, name, contents, options = {}) {
1435
+ return this.mainFrame().setFileInput(selector, name, contents, options);
1436
+ }
1437
+ async findLocators(options = {}) {
1438
+ return this.mainFrame().findLocators(options);
1439
+ }
1440
+ async screenshot(options = {}) {
1441
+ const start = Date.now();
1442
+ this.events.emit("action:start", { name: "screenshot", frameId: this.mainFrameId });
1443
+ const result = await this.session.send("Page.captureScreenshot", {
1444
+ format: options.format ?? "png",
1445
+ quality: options.quality,
1446
+ fromSurface: true
1447
+ });
1448
+ const buffer = Buffer.from(result.data, "base64");
1449
+ if (options.path) {
1450
+ const resolved = path2.resolve(options.path);
1451
+ fs2.writeFileSync(resolved, buffer);
1452
+ }
1453
+ const duration = Date.now() - start;
1454
+ this.events.emit("action:end", { name: "screenshot", frameId: this.mainFrameId, durationMs: duration });
1455
+ return buffer;
1456
+ }
1457
+ async screenshotBase64(options = {}) {
1458
+ const start = Date.now();
1459
+ this.events.emit("action:start", { name: "screenshotBase64", frameId: this.mainFrameId });
1460
+ const result = await this.session.send("Page.captureScreenshot", {
1461
+ format: options.format ?? "png",
1462
+ quality: options.quality,
1463
+ fromSurface: true
1464
+ });
1465
+ const duration = Date.now() - start;
1466
+ this.events.emit("action:end", { name: "screenshotBase64", frameId: this.mainFrameId, durationMs: duration });
1467
+ return result.data;
1468
+ }
1469
+ getEvents() {
1470
+ return this.events;
1471
+ }
1472
+ getDefaultTimeout() {
1473
+ return this.defaultTimeout;
1474
+ }
1475
+ buildFrameTree(tree) {
1476
+ const frame = this.ensureFrame(tree.frame.id);
1477
+ frame.setMeta({ name: tree.frame.name, url: tree.frame.url, parentId: tree.frame.parentId });
1478
+ if (!tree.frame.parentId) {
1479
+ this.mainFrameId = tree.frame.id;
1480
+ }
1481
+ if (tree.childFrames) {
1482
+ for (const child of tree.childFrames) {
1483
+ this.buildFrameTree(child);
1484
+ }
1485
+ }
1486
+ }
1487
+ ensureFrame(id) {
1488
+ let frame = this.framesById.get(id);
1489
+ if (!frame) {
1490
+ frame = new Frame(id, this.session, this.logger, this.events);
1491
+ this.framesById.set(id, frame);
1492
+ }
1493
+ return frame;
1494
+ }
1495
+ onFrameAttached(params) {
1496
+ const frame = this.ensureFrame(params.frameId);
1497
+ frame.setMeta({ parentId: params.parentFrameId });
1498
+ }
1499
+ onFrameNavigated(params) {
1500
+ const frame = this.ensureFrame(params.frame.id);
1501
+ frame.setMeta({ name: params.frame.name, url: params.frame.url, parentId: params.frame.parentId });
1502
+ if (!params.frame.parentId) {
1503
+ this.mainFrameId = params.frame.id;
1504
+ }
1505
+ }
1506
+ onFrameDetached(params) {
1507
+ this.framesById.delete(params.frameId);
1508
+ }
1509
+ onExecutionContextCreated(params) {
1510
+ const frameId = params.context.auxData?.frameId;
1511
+ if (!frameId) {
1512
+ return;
1513
+ }
1514
+ const frame = this.ensureFrame(frameId);
1515
+ frame.setExecutionContext(params.context.id);
1516
+ }
1517
+ onExecutionContextDestroyed(params) {
1518
+ for (const frame of this.framesById.values()) {
1519
+ if (frame.getExecutionContext() === params.executionContextId) {
1520
+ frame.setExecutionContext(void 0);
1521
+ }
1522
+ }
1523
+ }
1524
+ onExecutionContextsCleared() {
1525
+ for (const frame of this.framesById.values()) {
1526
+ frame.setExecutionContext(void 0);
1527
+ }
1528
+ }
1529
+ onLifecycleEvent(params) {
1530
+ if (!this.lifecycleEvents.has(params.frameId)) {
1531
+ this.lifecycleEvents.set(params.frameId, /* @__PURE__ */ new Set());
1532
+ }
1533
+ this.lifecycleEvents.get(params.frameId).add(params.name);
1534
+ }
1535
+ async waitForLifecycle(frameId, eventName, timeoutMs) {
1536
+ if (!frameId) {
1537
+ throw new Error("Missing frame id for lifecycle wait");
1538
+ }
1539
+ const start = Date.now();
1540
+ while (Date.now() - start < timeoutMs) {
1541
+ const events = this.lifecycleEvents.get(frameId);
1542
+ if (events && events.has(eventName)) {
1543
+ return;
1544
+ }
1545
+ await new Promise((resolve) => setTimeout(resolve, 100));
1546
+ }
1547
+ throw new Error(`Timeout waiting for lifecycle event: ${eventName}`);
1548
+ }
1549
+ };
1550
+
1551
+ // src/core/Browser.ts
1552
+ var BrowserContext = class {
1553
+ browser;
1554
+ id;
1555
+ constructor(browser, id) {
1556
+ this.browser = browser;
1557
+ this.id = id;
1558
+ }
1559
+ getId() {
1560
+ return this.id;
1561
+ }
1562
+ async newPage() {
1563
+ return this.browser.newPage({ browserContextId: this.id });
1564
+ }
1565
+ async close() {
1566
+ await this.browser.disposeContext(this.id);
1567
+ }
1568
+ };
1569
+ var Browser = class {
1570
+ connection;
1571
+ process;
1572
+ logger;
1573
+ events;
1574
+ cleanupTasks;
1575
+ contexts = /* @__PURE__ */ new Set();
1576
+ constructor(connection, child, logger, events, cleanupTasks = []) {
1577
+ this.connection = connection;
1578
+ this.process = child;
1579
+ this.logger = logger;
1580
+ this.events = events;
1581
+ this.cleanupTasks = cleanupTasks;
1582
+ }
1583
+ on(event, handler) {
1584
+ this.events.on(event, handler);
1585
+ }
1586
+ async newContext() {
1587
+ const { browserContextId } = await this.connection.send("Target.createBrowserContext");
1588
+ this.contexts.add(browserContextId);
1589
+ return new BrowserContext(this, browserContextId);
1590
+ }
1591
+ async newPage(options = {}) {
1592
+ const { browserContextId } = options;
1593
+ const { targetId } = await this.connection.send("Target.createTarget", {
1594
+ url: "about:blank",
1595
+ browserContextId
1596
+ });
1597
+ const { sessionId } = await this.connection.send("Target.attachToTarget", { targetId, flatten: true });
1598
+ const session = this.connection.createSession(sessionId);
1599
+ const page = new Page(session, this.logger, this.events);
1600
+ await page.initialize();
1601
+ return page;
1602
+ }
1603
+ async disposeContext(contextId) {
1604
+ if (!contextId) return;
1605
+ try {
1606
+ await this.connection.send("Target.disposeBrowserContext", { browserContextId: contextId });
1607
+ } catch {
1608
+ }
1609
+ this.contexts.delete(contextId);
1610
+ }
1611
+ async close() {
1612
+ if (this.contexts.size > 0) {
1613
+ for (const contextId of Array.from(this.contexts)) {
1614
+ await this.disposeContext(contextId);
1615
+ }
1616
+ }
1617
+ try {
1618
+ await this.connection.send("Browser.close");
1619
+ } catch {
1620
+ }
1621
+ await this.connection.close();
1622
+ if (!this.process.killed) {
1623
+ this.process.kill();
1624
+ }
1625
+ for (const task of this.cleanupTasks) {
1626
+ try {
1627
+ task();
1628
+ } catch {
1629
+ }
1630
+ }
1631
+ }
1632
+ };
1633
+
1634
+ // src/browser/Downloader.ts
1635
+ import fs3 from "fs";
1636
+ import path3 from "path";
1637
+ import os from "os";
1638
+ import https from "https";
1639
+ import { spawn } from "child_process";
1640
+ import yauzl from "yauzl";
1641
+ var SNAPSHOT_BASE = "https://commondatastorage.googleapis.com/chromium-browser-snapshots";
1642
+ function detectPlatform(platform = process.platform) {
1643
+ if (platform === "linux") return "linux";
1644
+ if (platform === "darwin") return "mac";
1645
+ if (platform === "win32") return "win";
1646
+ throw new Error(`Unsupported platform: ${platform}`);
1647
+ }
1648
+ function platformFolder(platform) {
1649
+ if (platform === "linux") return "Linux_x64";
1650
+ if (platform === "mac") return "Mac";
1651
+ return "Win";
1652
+ }
1653
+ function defaultCacheRoot(platform) {
1654
+ if (platform === "win") {
1655
+ const localAppData = process.env.LOCALAPPDATA || path3.join(os.homedir(), "AppData", "Local");
1656
+ return path3.join(localAppData, "cdpwright");
1657
+ }
1658
+ return path3.join(os.homedir(), ".cache", "cdpwright");
1659
+ }
1660
+ function ensureWithinRoot(root, target) {
1661
+ const resolvedRoot = path3.resolve(root);
1662
+ const resolvedTarget = path3.resolve(target);
1663
+ if (resolvedTarget === resolvedRoot) {
1664
+ return;
1665
+ }
1666
+ if (!resolvedTarget.startsWith(resolvedRoot + path3.sep)) {
1667
+ throw new Error(`Path escapes cache root: ${resolvedTarget}`);
1668
+ }
1669
+ }
1670
+ function chromiumExecutableRelativePath(platform) {
1671
+ if (platform === "linux") return path3.join("chrome-linux", "chrome");
1672
+ if (platform === "mac") return path3.join("chrome-mac", "Chromium.app", "Contents", "MacOS", "Chromium");
1673
+ return path3.join("chrome-win", "chrome.exe");
1674
+ }
1675
+ async function fetchLatestRevision(platform) {
1676
+ const folder = platformFolder(platform);
1677
+ const url = `${SNAPSHOT_BASE}/${folder}/LAST_CHANGE`;
1678
+ return new Promise((resolve, reject) => {
1679
+ https.get(url, (res) => {
1680
+ if (res.statusCode && res.statusCode >= 400) {
1681
+ reject(new Error(`Failed to fetch LAST_CHANGE: ${res.statusCode}`));
1682
+ return;
1683
+ }
1684
+ let data = "";
1685
+ res.on("data", (chunk) => data += chunk.toString());
1686
+ res.on("end", () => resolve(data.trim()));
1687
+ }).on("error", reject);
1688
+ });
1689
+ }
1690
+ async function ensureDownloaded(options) {
1691
+ const { cacheRoot, platform, revision, logger } = options;
1692
+ ensureWithinRoot(cacheRoot, cacheRoot);
1693
+ const platformDir = path3.join(cacheRoot, platform);
1694
+ const revisionDir = path3.join(platformDir, revision);
1695
+ ensureWithinRoot(cacheRoot, revisionDir);
1696
+ const executablePath = path3.join(revisionDir, chromiumExecutableRelativePath(platform));
1697
+ const markerFile = path3.join(revisionDir, "INSTALLATION_COMPLETE");
1698
+ if (fs3.existsSync(executablePath) && fs3.existsSync(markerFile)) {
1699
+ return { executablePath, revisionDir };
1700
+ }
1701
+ fs3.mkdirSync(revisionDir, { recursive: true });
1702
+ const folder = platformFolder(platform);
1703
+ const zipName = platform === "win" ? "chrome-win.zip" : platform === "mac" ? "chrome-mac.zip" : "chrome-linux.zip";
1704
+ const downloadUrl = `${SNAPSHOT_BASE}/${folder}/${revision}/${zipName}`;
1705
+ const tempZipPath = path3.join(os.tmpdir(), `cdpwright-${platform}-${revision}.zip`);
1706
+ logger.info("Downloading Chromium snapshot", downloadUrl);
1707
+ await downloadFile(downloadUrl, tempZipPath, logger);
1708
+ logger.info("Extracting Chromium snapshot", tempZipPath);
1709
+ await extractZipSafe(tempZipPath, revisionDir);
1710
+ fs3.writeFileSync(markerFile, (/* @__PURE__ */ new Date()).toISOString());
1711
+ fs3.unlinkSync(tempZipPath);
1712
+ if (!fs3.existsSync(executablePath)) {
1713
+ throw new Error(`Executable not found after extraction: ${executablePath}`);
1714
+ }
1715
+ ensureExecutable(executablePath, platform);
1716
+ return { executablePath, revisionDir };
1717
+ }
1718
+ function downloadFile(url, dest, logger) {
1719
+ return new Promise((resolve, reject) => {
1720
+ const file = fs3.createWriteStream(dest);
1721
+ https.get(url, (res) => {
1722
+ if (res.statusCode && res.statusCode >= 400) {
1723
+ reject(new Error(`Failed to download: ${res.statusCode}`));
1724
+ return;
1725
+ }
1726
+ const total = Number(res.headers["content-length"] || 0);
1727
+ let downloaded = 0;
1728
+ let lastLoggedPercent = -1;
1729
+ let lastLoggedTime = Date.now();
1730
+ res.pipe(file);
1731
+ res.on("data", (chunk) => {
1732
+ downloaded += chunk.length;
1733
+ if (!total) {
1734
+ const now = Date.now();
1735
+ if (now - lastLoggedTime > 2e3) {
1736
+ logger.info("Download progress", `${(downloaded / (1024 * 1024)).toFixed(1)} MB`);
1737
+ lastLoggedTime = now;
1738
+ }
1739
+ return;
1740
+ }
1741
+ const percent = Math.floor(downloaded / total * 100);
1742
+ if (percent >= lastLoggedPercent + 5) {
1743
+ logger.info("Download progress", `${percent}%`);
1744
+ lastLoggedPercent = percent;
1745
+ }
1746
+ });
1747
+ file.on("finish", () => file.close(() => resolve()));
1748
+ }).on("error", (err) => {
1749
+ fs3.unlink(dest, () => reject(err));
1750
+ });
1751
+ });
1752
+ }
1753
+ async function extractZipSafe(zipPath, destDir) {
1754
+ return new Promise((resolve, reject) => {
1755
+ yauzl.open(zipPath, { lazyEntries: true }, (err, zipfile) => {
1756
+ if (err || !zipfile) {
1757
+ reject(err || new Error("Unable to open zip"));
1758
+ return;
1759
+ }
1760
+ zipfile.readEntry();
1761
+ zipfile.on("entry", (entry) => {
1762
+ const entryPath = entry.fileName.replace(/\\/g, "/");
1763
+ const targetPath = path3.join(destDir, entryPath);
1764
+ try {
1765
+ ensureWithinRoot(destDir, targetPath);
1766
+ } catch (error) {
1767
+ zipfile.close();
1768
+ reject(error);
1769
+ return;
1770
+ }
1771
+ if (/\/$/.test(entry.fileName)) {
1772
+ fs3.mkdirSync(targetPath, { recursive: true });
1773
+ zipfile.readEntry();
1774
+ return;
1775
+ }
1776
+ fs3.mkdirSync(path3.dirname(targetPath), { recursive: true });
1777
+ zipfile.openReadStream(entry, (streamErr, readStream) => {
1778
+ if (streamErr || !readStream) {
1779
+ zipfile.close();
1780
+ reject(streamErr || new Error("Unable to read zip entry"));
1781
+ return;
1782
+ }
1783
+ const rawMode = entry.externalFileAttributes ? entry.externalFileAttributes >>> 16 & 65535 : 0;
1784
+ const mode = rawMode > 0 ? rawMode : void 0;
1785
+ const writeStream = fs3.createWriteStream(targetPath);
1786
+ readStream.pipe(writeStream);
1787
+ writeStream.on("error", (writeErr) => {
1788
+ zipfile.close();
1789
+ reject(writeErr);
1790
+ });
1791
+ writeStream.on("close", () => {
1792
+ if (mode && mode <= 511) {
1793
+ try {
1794
+ fs3.chmodSync(targetPath, mode);
1795
+ } catch {
1796
+ }
1797
+ }
1798
+ zipfile.readEntry();
1799
+ });
1800
+ });
1801
+ });
1802
+ zipfile.on("end", () => {
1803
+ zipfile.close();
1804
+ resolve();
1805
+ });
1806
+ zipfile.on("error", (zipErr) => {
1807
+ zipfile.close();
1808
+ reject(zipErr);
1809
+ });
1810
+ });
1811
+ });
1812
+ }
1813
+ async function chromiumVersion(executablePath) {
1814
+ return new Promise((resolve, reject) => {
1815
+ const child = spawn(executablePath, ["--version"], { stdio: ["ignore", "pipe", "pipe"] });
1816
+ let output = "";
1817
+ child.stdout.on("data", (chunk) => output += chunk.toString());
1818
+ child.on("close", (code) => {
1819
+ if (code === 0) {
1820
+ resolve(output.trim());
1821
+ } else {
1822
+ reject(new Error(`Failed to get Chromium version: ${code}`));
1823
+ }
1824
+ });
1825
+ child.on("error", reject);
1826
+ });
1827
+ }
1828
+ function ensureExecutable(executablePath, platform) {
1829
+ if (platform === "win") {
1830
+ return;
1831
+ }
1832
+ try {
1833
+ const stat = fs3.statSync(executablePath);
1834
+ const isExecutable = (stat.mode & 73) !== 0;
1835
+ if (!isExecutable) {
1836
+ fs3.chmodSync(executablePath, 493);
1837
+ }
1838
+ } catch {
1839
+ }
1840
+ }
1841
+
1842
+ // src/browser/Revision.ts
1843
+ var PINNED_REVISION = "1567454";
1844
+ function resolveRevision(envRevision) {
1845
+ if (envRevision && envRevision.trim()) {
1846
+ return envRevision.trim();
1847
+ }
1848
+ return PINNED_REVISION;
1849
+ }
1850
+
1851
+ // src/browser/ChromiumManager.ts
1852
+ var ChromiumManager = class {
1853
+ logger;
1854
+ constructor(logger) {
1855
+ const envLevel = process.env.CDPWRIGHT_LOG_LEVEL ?? "info";
1856
+ this.logger = logger ?? new Logger(envLevel);
1857
+ }
1858
+ getLogger() {
1859
+ return this.logger;
1860
+ }
1861
+ async download(options = {}) {
1862
+ const platform = detectPlatform();
1863
+ const cacheRoot = this.resolveCacheRoot(platform);
1864
+ const overrideExecutable = process.env.CDPWRIGHT_EXECUTABLE_PATH;
1865
+ let revision = options.latest ? await fetchLatestRevision(platform) : resolveRevision(process.env.CDPWRIGHT_REVISION);
1866
+ let executablePath;
1867
+ let revisionDir = "";
1868
+ if (overrideExecutable) {
1869
+ executablePath = path4.resolve(overrideExecutable);
1870
+ } else {
1871
+ const downloaded = await ensureDownloaded({
1872
+ cacheRoot,
1873
+ platform,
1874
+ revision,
1875
+ logger: this.logger
1876
+ });
1877
+ executablePath = downloaded.executablePath;
1878
+ revisionDir = downloaded.revisionDir;
1879
+ }
1880
+ if (!fs4.existsSync(executablePath)) {
1881
+ throw new Error(`Chromium executable not found: ${executablePath}`);
1882
+ }
1883
+ const version = await chromiumVersion(executablePath);
1884
+ this.logger.info("Chromium cache root", cacheRoot);
1885
+ this.logger.info("Platform", platform);
1886
+ this.logger.info("Revision", revision);
1887
+ this.logger.info("Chromium version", version);
1888
+ return {
1889
+ cacheRoot,
1890
+ platform,
1891
+ revision,
1892
+ executablePath,
1893
+ revisionDir,
1894
+ chromiumVersion: version
1895
+ };
1896
+ }
1897
+ async launch(options = {}) {
1898
+ const logger = this.logger;
1899
+ if (options.logLevel) {
1900
+ logger.setLevel(options.logLevel);
1901
+ }
1902
+ const executablePath = options.executablePath || process.env.CDPWRIGHT_EXECUTABLE_PATH;
1903
+ let resolvedExecutable = executablePath;
1904
+ if (!resolvedExecutable) {
1905
+ const platform = detectPlatform();
1906
+ const cacheRoot = this.resolveCacheRoot(platform);
1907
+ const revision = resolveRevision(process.env.CDPWRIGHT_REVISION);
1908
+ const downloaded = await ensureDownloaded({
1909
+ cacheRoot,
1910
+ platform,
1911
+ revision,
1912
+ logger
1913
+ });
1914
+ resolvedExecutable = downloaded.executablePath;
1915
+ }
1916
+ if (!resolvedExecutable || !fs4.existsSync(resolvedExecutable)) {
1917
+ throw new Error(`Chromium executable not found: ${resolvedExecutable}`);
1918
+ }
1919
+ const stats = fs4.statSync(resolvedExecutable);
1920
+ if (!stats.isFile()) {
1921
+ throw new Error(`Chromium executable is not a file: ${resolvedExecutable}`);
1922
+ }
1923
+ ensureExecutable2(resolvedExecutable);
1924
+ const cleanupTasks = [];
1925
+ let userDataDir = options.userDataDir ?? process.env.CDPWRIGHT_USER_DATA_DIR;
1926
+ if (!userDataDir) {
1927
+ userDataDir = fs4.mkdtempSync(path4.join(os2.tmpdir(), "cdpwright-"));
1928
+ cleanupTasks.push(() => fs4.rmSync(userDataDir, { recursive: true, force: true }));
1929
+ }
1930
+ const args = [
1931
+ "--remote-debugging-port=0",
1932
+ "--no-first-run",
1933
+ "--no-default-browser-check",
1934
+ "--disable-background-networking",
1935
+ "--disable-background-timer-throttling",
1936
+ "--disable-backgrounding-occluded-windows",
1937
+ "--disable-renderer-backgrounding"
1938
+ ];
1939
+ if (userDataDir) {
1940
+ args.push(`--user-data-dir=${userDataDir}`);
1941
+ }
1942
+ if (process.platform === "linux") {
1943
+ args.push("--disable-crash-reporter", "--disable-crashpad");
1944
+ }
1945
+ if (options.headless ?? true) {
1946
+ args.push("--headless=new");
1947
+ }
1948
+ if (options.maximize) {
1949
+ args.push("--start-maximized");
1950
+ }
1951
+ if (options.args) {
1952
+ args.push(...options.args);
1953
+ }
1954
+ logger.info("Launching Chromium", resolvedExecutable);
1955
+ const child = spawn2(resolvedExecutable, args, { stdio: ["ignore", "pipe", "pipe"] });
1956
+ const websocketUrl = await waitForWebSocketEndpoint(child, logger, options.timeoutMs ?? 3e4);
1957
+ const httpUrl = toHttpVersionUrl(websocketUrl);
1958
+ const wsEndpoint = await fetchWebSocketDebuggerUrl(httpUrl);
1959
+ const connection = new Connection(wsEndpoint, logger);
1960
+ await closeInitialPages(connection, logger);
1961
+ const events = new AutomationEvents();
1962
+ const logEvents = resolveLogFlag(options.logEvents, process.env.CDPWRIGHT_LOG, true);
1963
+ const logActions = resolveLogFlag(options.logActions, process.env.CDPWRIGHT_LOG_ACTIONS, true);
1964
+ const logAssertions = resolveLogFlag(options.logAssertions, process.env.CDPWRIGHT_LOG_ASSERTIONS, true);
1965
+ if (logEvents && logActions) {
1966
+ events.on("action:end", (payload) => {
1967
+ const selector = payload.sensitive ? void 0 : payload.selector;
1968
+ const args2 = buildLogArgs(selector, payload.durationMs);
1969
+ logger.info(`Action ${payload.name}`, ...args2);
1970
+ });
1971
+ }
1972
+ if (logEvents && logAssertions) {
1973
+ events.on("assertion:end", (payload) => {
1974
+ const args2 = buildLogArgs(payload.selector, payload.durationMs);
1975
+ logger.info(`Assertion ${payload.name}`, ...args2);
1976
+ });
1977
+ }
1978
+ const browser = new Browser(connection, child, logger, events, cleanupTasks);
1979
+ return browser;
1980
+ }
1981
+ resolveCacheRoot(platform) {
1982
+ const envRoot = process.env.CDPWRIGHT_CACHE_DIR;
1983
+ if (envRoot && envRoot.trim()) {
1984
+ return path4.resolve(envRoot.trim());
1985
+ }
1986
+ return defaultCacheRoot(platform);
1987
+ }
1988
+ };
1989
+ async function closeInitialPages(connection, logger) {
1990
+ try {
1991
+ const targets = await connection.send("Target.getTargets");
1992
+ for (const info of targets.targetInfos) {
1993
+ if (info.type === "page") {
1994
+ await connection.send("Target.closeTarget", { targetId: info.targetId });
1995
+ }
1996
+ }
1997
+ } catch (err) {
1998
+ logger.warn("Failed to close initial pages", err);
1999
+ }
2000
+ }
2001
+ function resolveLogFlag(explicit, envValue, defaultValue) {
2002
+ if (explicit !== void 0) {
2003
+ return explicit;
2004
+ }
2005
+ if (envValue == null) {
2006
+ return defaultValue;
2007
+ }
2008
+ const normalized = envValue.trim().toLowerCase();
2009
+ return !["0", "false", "no", "off"].includes(normalized);
2010
+ }
2011
+ function buildLogArgs(selector, durationMs) {
2012
+ const args = [];
2013
+ if (selector) {
2014
+ args.push(selector);
2015
+ }
2016
+ if (typeof durationMs === "number") {
2017
+ args.push(`${durationMs}ms`);
2018
+ }
2019
+ return args;
2020
+ }
2021
+ function ensureExecutable2(executablePath) {
2022
+ if (process.platform === "win32") {
2023
+ return;
2024
+ }
2025
+ try {
2026
+ const stat = fs4.statSync(executablePath);
2027
+ const isExecutable = (stat.mode & 73) !== 0;
2028
+ if (!isExecutable) {
2029
+ fs4.chmodSync(executablePath, 493);
2030
+ }
2031
+ } catch {
2032
+ }
2033
+ const dir = path4.dirname(executablePath);
2034
+ const helpers = [
2035
+ "chrome_crashpad_handler",
2036
+ "chrome_sandbox",
2037
+ "chrome-wrapper",
2038
+ "xdg-mime",
2039
+ "xdg-settings"
2040
+ ];
2041
+ for (const name of helpers) {
2042
+ const helperPath = path4.join(dir, name);
2043
+ if (!fs4.existsSync(helperPath)) {
2044
+ continue;
2045
+ }
2046
+ try {
2047
+ const stat = fs4.statSync(helperPath);
2048
+ const isExecutable = (stat.mode & 73) !== 0;
2049
+ if (!isExecutable) {
2050
+ fs4.chmodSync(helperPath, 493);
2051
+ }
2052
+ } catch {
2053
+ }
2054
+ }
2055
+ }
2056
+ function waitForWebSocketEndpoint(child, logger, timeoutMs) {
2057
+ return new Promise((resolve, reject) => {
2058
+ const start = Date.now();
2059
+ const timeout = setTimeout(() => {
2060
+ reject(new Error("Timed out waiting for DevTools endpoint"));
2061
+ }, timeoutMs);
2062
+ const outputLines = [];
2063
+ const pushOutput = (data) => {
2064
+ const text = data.toString();
2065
+ for (const line of text.split(/\r?\n/)) {
2066
+ if (!line.trim()) continue;
2067
+ outputLines.push(line);
2068
+ if (outputLines.length > 50) {
2069
+ outputLines.shift();
2070
+ }
2071
+ }
2072
+ };
2073
+ const onData = (data) => {
2074
+ const text = data.toString();
2075
+ const match = text.match(/DevTools listening on (ws:\/\/[^\s]+)/);
2076
+ if (match) {
2077
+ clearTimeout(timeout);
2078
+ cleanup();
2079
+ logger.info("DevTools endpoint", match[1]);
2080
+ resolve(match[1]);
2081
+ }
2082
+ pushOutput(data);
2083
+ };
2084
+ const onExit = (code, signal) => {
2085
+ cleanup();
2086
+ const tail = outputLines.length ? `
2087
+ Chromium output:
2088
+ ${outputLines.join("\n")}` : "";
2089
+ reject(new Error(`Chromium exited early with code ${code ?? "null"} signal ${signal ?? "null"}${tail}`));
2090
+ };
2091
+ const cleanup = () => {
2092
+ child.stdout?.off("data", onData);
2093
+ child.stderr?.off("data", onData);
2094
+ child.off("exit", onExit);
2095
+ };
2096
+ child.stdout?.on("data", onData);
2097
+ child.stderr?.on("data", onData);
2098
+ child.on("exit", onExit);
2099
+ if (Date.now() - start > timeoutMs) {
2100
+ cleanup();
2101
+ }
2102
+ });
2103
+ }
2104
+ function toHttpVersionUrl(wsUrl) {
2105
+ try {
2106
+ const url = new URL(wsUrl);
2107
+ const port = url.port || "9222";
2108
+ return `http://127.0.0.1:${port}/json/version`;
2109
+ } catch {
2110
+ throw new Error(`Invalid DevTools endpoint: ${wsUrl}`);
2111
+ }
2112
+ }
2113
+ function fetchWebSocketDebuggerUrl(versionUrl) {
2114
+ return new Promise((resolve, reject) => {
2115
+ http.get(versionUrl, (res) => {
2116
+ if (res.statusCode && res.statusCode >= 400) {
2117
+ reject(new Error(`Failed to fetch /json/version: ${res.statusCode}`));
2118
+ return;
2119
+ }
2120
+ let data = "";
2121
+ res.on("data", (chunk) => data += chunk.toString());
2122
+ res.on("end", () => {
2123
+ try {
2124
+ const parsed = JSON.parse(data);
2125
+ if (!parsed.webSocketDebuggerUrl) {
2126
+ reject(new Error("webSocketDebuggerUrl missing"));
2127
+ return;
2128
+ }
2129
+ resolve(parsed.webSocketDebuggerUrl);
2130
+ } catch (err) {
2131
+ reject(err);
2132
+ }
2133
+ });
2134
+ }).on("error", reject);
2135
+ });
2136
+ }
2137
+
2138
+ // src/cli.ts
2139
+ function printHelp() {
2140
+ console.log("cdpwright (cpw) download [--latest]");
2141
+ }
2142
+ async function main() {
2143
+ const [, , command, ...rest] = process.argv;
2144
+ if (!command || command === "--help" || command === "-h") {
2145
+ printHelp();
2146
+ process.exit(0);
2147
+ }
2148
+ if (command !== "download") {
2149
+ console.error(`Unknown command: ${command}`);
2150
+ printHelp();
2151
+ process.exit(1);
2152
+ }
2153
+ const latest = rest.includes("--latest");
2154
+ const manager = new ChromiumManager();
2155
+ await manager.download({ latest });
2156
+ }
2157
+ main().catch((err) => {
2158
+ console.error(err instanceof Error ? err.message : String(err));
2159
+ process.exit(1);
2160
+ });
2161
+ //# sourceMappingURL=cli.js.map