@toolstackhq/cdpwright 1.1.0 → 1.3.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 CHANGED
@@ -419,27 +419,77 @@ function serializeShadowDomHelpers() {
419
419
  // src/core/Locator.ts
420
420
  var Locator = class {
421
421
  frame;
422
- selector;
423
- options;
424
- constructor(frame, selector, options = {}) {
422
+ query;
423
+ constructor(frame, query) {
425
424
  this.frame = frame;
426
- this.selector = selector;
427
- this.options = options;
425
+ this.query = query;
426
+ }
427
+ getEvents() {
428
+ return this.frame.getEvents();
429
+ }
430
+ getFrameId() {
431
+ return this.frame.id;
432
+ }
433
+ describe() {
434
+ switch (this.query.kind) {
435
+ case "selector":
436
+ return this.query.selector;
437
+ case "text":
438
+ return typeof this.query.text === "string" ? `text=${JSON.stringify(this.query.text)}` : `text=${this.query.text.toString()}`;
439
+ case "role":
440
+ return `role=${this.query.role}${this.query.options?.name ? ` name=${typeof this.query.options.name === "string" ? JSON.stringify(this.query.options.name) : this.query.options.name.toString()}` : ""}`;
441
+ }
428
442
  }
429
443
  async click(options = {}) {
430
- return this.frame.click(this.selector, { ...this.options, ...options });
444
+ return this.frame.clickLocator(this.query, { ...this.queryTimeoutOptions(), ...options });
431
445
  }
432
446
  async dblclick(options = {}) {
433
- return this.frame.dblclick(this.selector, { ...this.options, ...options });
447
+ return this.frame.dblclickLocator(this.query, { ...this.queryTimeoutOptions(), ...options });
434
448
  }
435
449
  async type(text, options = {}) {
436
- return this.frame.type(this.selector, text, { ...this.options, ...options });
450
+ return this.frame.typeLocator(this.query, text, { ...this.queryTimeoutOptions(), ...options });
437
451
  }
438
452
  async exists() {
439
- return this.frame.exists(this.selector, this.options);
453
+ return this.frame.existsLocator(this.query);
454
+ }
455
+ async isVisible() {
456
+ return this.frame.isVisibleLocator(this.query);
457
+ }
458
+ async isEnabled() {
459
+ return this.frame.isEnabledLocator(this.query);
460
+ }
461
+ async isChecked() {
462
+ return this.frame.isCheckedLocator(this.query);
440
463
  }
441
464
  async text() {
442
- return this.frame.text(this.selector, this.options);
465
+ return this.frame.textLocator(this.query);
466
+ }
467
+ async value() {
468
+ return this.frame.valueLocator(this.query);
469
+ }
470
+ async attribute(name) {
471
+ return this.frame.attributeLocator(this.query, name);
472
+ }
473
+ async classes() {
474
+ return this.frame.classesLocator(this.query);
475
+ }
476
+ async css(property) {
477
+ return this.frame.cssLocator(this.query, property);
478
+ }
479
+ async hasFocus() {
480
+ return this.frame.hasFocusLocator(this.query);
481
+ }
482
+ async isInViewport(fully = false) {
483
+ return this.frame.isInViewportLocator(this.query, fully);
484
+ }
485
+ async isEditable() {
486
+ return this.frame.isEditableLocator(this.query);
487
+ }
488
+ async count() {
489
+ return this.frame.countLocator(this.query);
490
+ }
491
+ queryTimeoutOptions() {
492
+ return "options" in this.query && this.query.options?.timeoutMs ? { timeoutMs: this.query.options.timeoutMs } : {};
443
493
  }
444
494
  };
445
495
 
@@ -471,6 +521,9 @@ var Frame = class {
471
521
  this.url = meta.url;
472
522
  this.parentId = meta.parentId;
473
523
  }
524
+ getEvents() {
525
+ return this.events;
526
+ }
474
527
  async evaluate(fnOrString, ...args) {
475
528
  return this.evaluateInContext(fnOrString, args);
476
529
  }
@@ -487,7 +540,13 @@ var Frame = class {
487
540
  return this.querySelectorAllInternal(selector, options, true);
488
541
  }
489
542
  locator(selector, options = {}) {
490
- return new Locator(this, selector, options);
543
+ return new Locator(this, { kind: "selector", selector, options });
544
+ }
545
+ getByText(text, options = {}) {
546
+ return new Locator(this, { kind: "text", text, options });
547
+ }
548
+ getByRole(role, options = {}) {
549
+ return new Locator(this, { kind: "role", role, options });
491
550
  }
492
551
  async click(selector, options = {}) {
493
552
  await this.performClick(selector, options, false);
@@ -922,6 +981,176 @@ var Frame = class {
922
981
  const box = await this.resolveElementBox(selector, options);
923
982
  return Boolean(box && box.visible);
924
983
  }
984
+ async clickLocator(query, options = {}) {
985
+ await this.performClickLocator(query, options, false);
986
+ }
987
+ async dblclickLocator(query, options = {}) {
988
+ await this.performClickLocator(query, options, true);
989
+ }
990
+ async typeLocator(query, text, options = {}) {
991
+ const start = Date.now();
992
+ const description = this.locatorDescription(query);
993
+ this.events.emit("action:start", { name: "type", selector: description, frameId: this.id, sensitive: options.sensitive });
994
+ await waitFor(async () => {
995
+ const box = await this.resolveLocatorElementBox(query, options);
996
+ if (!box || !box.visible) {
997
+ return false;
998
+ }
999
+ return true;
1000
+ }, { timeoutMs: options.timeoutMs ?? this.defaultTimeout, description: `type ${description}` });
1001
+ const focusExpression = this.buildLocatorExpression(query, `
1002
+ if (!el) {
1003
+ return;
1004
+ }
1005
+ el.focus();
1006
+ `);
1007
+ const focusParams = {
1008
+ expression: focusExpression,
1009
+ returnByValue: true
1010
+ };
1011
+ if (this.contextId) {
1012
+ focusParams.contextId = this.contextId;
1013
+ }
1014
+ await this.session.send("Runtime.evaluate", focusParams);
1015
+ await this.session.send("Input.insertText", { text });
1016
+ const duration = Date.now() - start;
1017
+ this.events.emit("action:end", { name: "type", selector: description, frameId: this.id, durationMs: duration, sensitive: options.sensitive });
1018
+ this.logger.debug("Type", description, `${duration}ms`);
1019
+ }
1020
+ async existsLocator(query) {
1021
+ return Boolean(await this.evalOnLocator(query, false, `
1022
+ return Boolean(el);
1023
+ `));
1024
+ }
1025
+ async isVisibleLocator(query) {
1026
+ return this.evalOnLocator(query, false, `
1027
+ if (!el) {
1028
+ return null;
1029
+ }
1030
+ const rect = el.getBoundingClientRect();
1031
+ const style = window.getComputedStyle(el);
1032
+ return rect.width > 0 && rect.height > 0 && style.visibility !== "hidden" && style.display !== "none" && Number(style.opacity || "1") > 0;
1033
+ `);
1034
+ }
1035
+ async isEnabledLocator(query) {
1036
+ return this.evalOnLocator(query, false, `
1037
+ if (!el) {
1038
+ return null;
1039
+ }
1040
+ const disabled = Boolean(el.disabled) || el.hasAttribute("disabled");
1041
+ const ariaDisabled = el.getAttribute && el.getAttribute("aria-disabled") === "true";
1042
+ return !(disabled || ariaDisabled);
1043
+ `);
1044
+ }
1045
+ async isCheckedLocator(query) {
1046
+ return this.evalOnLocator(query, false, `
1047
+ if (!el) {
1048
+ return null;
1049
+ }
1050
+ const aria = el.getAttribute && el.getAttribute("aria-checked");
1051
+ if (aria === "true") {
1052
+ return true;
1053
+ }
1054
+ if (aria === "false") {
1055
+ return false;
1056
+ }
1057
+ if ("checked" in el) {
1058
+ return Boolean(el.checked);
1059
+ }
1060
+ return null;
1061
+ `);
1062
+ }
1063
+ async textLocator(query) {
1064
+ return this.evalOnLocator(query, false, `
1065
+ if (!el) {
1066
+ return null;
1067
+ }
1068
+ if (el instanceof HTMLInputElement) {
1069
+ const type = (el.getAttribute("type") || "text").toLowerCase();
1070
+ if (type === "button" || type === "submit" || type === "reset") {
1071
+ return el.value || "";
1072
+ }
1073
+ }
1074
+ return el.textContent || "";
1075
+ `);
1076
+ }
1077
+ async valueLocator(query) {
1078
+ return this.evalOnLocator(query, false, `
1079
+ if (!el) {
1080
+ return null;
1081
+ }
1082
+ if ("value" in el) {
1083
+ return el.value ?? "";
1084
+ }
1085
+ return el.getAttribute("value");
1086
+ `);
1087
+ }
1088
+ async attributeLocator(query, name) {
1089
+ return this.evalOnLocator(query, false, `
1090
+ if (!el || !(el instanceof Element)) {
1091
+ return null;
1092
+ }
1093
+ return el.getAttribute(${JSON.stringify(name)});
1094
+ `);
1095
+ }
1096
+ async classesLocator(query) {
1097
+ return this.evalOnLocator(query, false, `
1098
+ if (!el) {
1099
+ return null;
1100
+ }
1101
+ if (!el.classList) {
1102
+ return [];
1103
+ }
1104
+ return Array.from(el.classList);
1105
+ `);
1106
+ }
1107
+ async cssLocator(query, property) {
1108
+ return this.evalOnLocator(query, false, `
1109
+ if (!el) {
1110
+ return null;
1111
+ }
1112
+ const style = window.getComputedStyle(el);
1113
+ return style.getPropertyValue(${JSON.stringify(property)}) || "";
1114
+ `);
1115
+ }
1116
+ async hasFocusLocator(query) {
1117
+ return this.evalOnLocator(query, false, `
1118
+ if (!el) {
1119
+ return null;
1120
+ }
1121
+ return document.activeElement === el;
1122
+ `);
1123
+ }
1124
+ async isInViewportLocator(query, fully = false) {
1125
+ return this.evalOnLocator(query, false, `
1126
+ if (!el) {
1127
+ return null;
1128
+ }
1129
+ const rect = el.getBoundingClientRect();
1130
+ const viewWidth = window.innerWidth || document.documentElement.clientWidth;
1131
+ const viewHeight = window.innerHeight || document.documentElement.clientHeight;
1132
+ if (${fully ? "true" : "false"}) {
1133
+ return rect.top >= 0 && rect.left >= 0 && rect.bottom <= viewHeight && rect.right <= viewWidth;
1134
+ }
1135
+ return rect.bottom > 0 && rect.right > 0 && rect.top < viewHeight && rect.left < viewWidth;
1136
+ `);
1137
+ }
1138
+ async isEditableLocator(query) {
1139
+ return this.evalOnLocator(query, false, `
1140
+ if (!el) {
1141
+ return null;
1142
+ }
1143
+ const disabled = Boolean(el.disabled) || el.hasAttribute("disabled");
1144
+ const readOnly = Boolean(el.readOnly) || el.hasAttribute("readonly");
1145
+ const ariaDisabled = el.getAttribute && el.getAttribute("aria-disabled") === "true";
1146
+ return !(disabled || readOnly || ariaDisabled);
1147
+ `);
1148
+ }
1149
+ async countLocator(query) {
1150
+ return this.evalOnLocator(query, true, `
1151
+ return elements.length;
1152
+ `);
1153
+ }
925
1154
  async text(selector, options = {}) {
926
1155
  return this.evalOnSelector(selector, options, false, `
927
1156
  if (!el) {
@@ -1103,6 +1332,263 @@ var Frame = class {
1103
1332
  return !(disabled || readOnly || ariaDisabled);
1104
1333
  `);
1105
1334
  }
1335
+ async performClickLocator(query, options, isDouble) {
1336
+ const start = Date.now();
1337
+ const actionName = isDouble ? "dblclick" : "click";
1338
+ const description = this.locatorDescription(query);
1339
+ this.events.emit("action:start", { name: actionName, selector: description, frameId: this.id });
1340
+ const box = await waitFor(async () => {
1341
+ const result = await this.resolveLocatorElementBox(query, options);
1342
+ if (!result || !result.visible) {
1343
+ return null;
1344
+ }
1345
+ return result;
1346
+ }, { timeoutMs: options.timeoutMs ?? this.defaultTimeout, description: `${actionName} ${description}` });
1347
+ const centerX = box.x + box.width / 2;
1348
+ const centerY = box.y + box.height / 2;
1349
+ await this.session.send("Input.dispatchMouseEvent", { type: "mouseMoved", x: centerX, y: centerY });
1350
+ await this.session.send("Input.dispatchMouseEvent", { type: "mousePressed", x: centerX, y: centerY, button: "left", clickCount: 1, buttons: 1 });
1351
+ await this.session.send("Input.dispatchMouseEvent", { type: "mouseReleased", x: centerX, y: centerY, button: "left", clickCount: 1, buttons: 0 });
1352
+ if (isDouble) {
1353
+ await this.session.send("Input.dispatchMouseEvent", { type: "mouseMoved", x: centerX, y: centerY });
1354
+ await this.session.send("Input.dispatchMouseEvent", { type: "mousePressed", x: centerX, y: centerY, button: "left", clickCount: 2, buttons: 1 });
1355
+ await this.session.send("Input.dispatchMouseEvent", { type: "mouseReleased", x: centerX, y: centerY, button: "left", clickCount: 2, buttons: 0 });
1356
+ }
1357
+ const duration = Date.now() - start;
1358
+ this.events.emit("action:end", { name: actionName, selector: description, frameId: this.id, durationMs: duration });
1359
+ this.logger.debug("Click", description, `${duration}ms`);
1360
+ }
1361
+ locatorDescription(query) {
1362
+ switch (query.kind) {
1363
+ case "selector":
1364
+ return query.selector;
1365
+ case "text":
1366
+ return typeof query.text === "string" ? `text=${JSON.stringify(query.text)}` : `text=${query.text.toString()}`;
1367
+ case "role":
1368
+ return `role=${query.role}${query.options?.name ? ` name=${typeof query.options.name === "string" ? JSON.stringify(query.options.name) : query.options.name.toString()}` : ""}`;
1369
+ }
1370
+ }
1371
+ async resolveLocatorElementBox(query, options) {
1372
+ return this.evalOnLocator(query, false, `
1373
+ if (!el) {
1374
+ return null;
1375
+ }
1376
+ el.scrollIntoView({ block: "center", inline: "center" });
1377
+ const rect = el.getBoundingClientRect();
1378
+ const style = window.getComputedStyle(el);
1379
+ return {
1380
+ x: rect.x,
1381
+ y: rect.y,
1382
+ width: rect.width,
1383
+ height: rect.height,
1384
+ visible: rect.width > 0 && rect.height > 0 && style.visibility !== "hidden" && style.display !== "none" && Number(style.opacity || "1") > 0
1385
+ };
1386
+ `);
1387
+ }
1388
+ async evalOnLocator(query, all, body) {
1389
+ const expression = this.buildLocatorExpression(query, body, all);
1390
+ const params = {
1391
+ expression,
1392
+ returnByValue: true
1393
+ };
1394
+ if (this.contextId) {
1395
+ params.contextId = this.contextId;
1396
+ }
1397
+ const result = await this.session.send("Runtime.evaluate", params);
1398
+ return result.result.value;
1399
+ }
1400
+ buildLocatorExpression(query, body, all = false) {
1401
+ const helpers = serializeShadowDomHelpers();
1402
+ const serializedQuery = this.serializeLocatorQuery(query);
1403
+ return `(function() {
1404
+ const querySelectorAllDeep = ${helpers.querySelectorAllDeep};
1405
+ const normalizeWhitespace = (value) => String(value ?? "").replace(/\\s+/g, " ").trim();
1406
+ const cssEscape = (value) => {
1407
+ if (typeof CSS !== "undefined" && CSS.escape) return CSS.escape(value);
1408
+ return String(value).replace(/[^a-zA-Z0-9_-]/g, (c) => "\\\\" + c.charCodeAt(0).toString(16) + " ");
1409
+ };
1410
+ const textFromElement = (el) => {
1411
+ if (!el) return "";
1412
+ if (el instanceof HTMLInputElement) {
1413
+ const type = (el.getAttribute("type") || "").toLowerCase();
1414
+ if (type === "button" || type === "submit" || type === "reset" || type === "image") {
1415
+ return el.value || el.getAttribute("alt") || "";
1416
+ }
1417
+ }
1418
+ return el.textContent || "";
1419
+ };
1420
+ const isHidden = (el) => {
1421
+ if (!el || !(el instanceof Element)) return true;
1422
+ const style = window.getComputedStyle(el);
1423
+ if (el.hidden || el.getAttribute("aria-hidden") === "true") return true;
1424
+ return style.display === "none" || style.visibility === "hidden" || Number(style.opacity || "1") <= 0;
1425
+ };
1426
+ const getLabelText = (el) => {
1427
+ if (!el || !(el instanceof Element)) return "";
1428
+ const ariaLabel = el.getAttribute("aria-label");
1429
+ if (ariaLabel) return normalizeWhitespace(ariaLabel);
1430
+ const labelledBy = el.getAttribute("aria-labelledby");
1431
+ if (labelledBy) {
1432
+ const parts = labelledBy.split(/\\s+/).filter(Boolean).map((id) => document.getElementById(id)?.textContent || "");
1433
+ const text = normalizeWhitespace(parts.join(" "));
1434
+ if (text) return text;
1435
+ }
1436
+ if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement || el instanceof HTMLSelectElement) {
1437
+ if (el.id) {
1438
+ const label = document.querySelector("label[for="" + cssEscape(el.id) + ""]");
1439
+ if (label) {
1440
+ const text = normalizeWhitespace(label.textContent || "");
1441
+ if (text) return text;
1442
+ }
1443
+ }
1444
+ const wrap = el.closest("label");
1445
+ if (wrap) {
1446
+ const text = normalizeWhitespace(wrap.textContent || "");
1447
+ if (text) return text;
1448
+ }
1449
+ }
1450
+ if (el instanceof HTMLImageElement) {
1451
+ return normalizeWhitespace(el.getAttribute("alt") || el.getAttribute("title") || "");
1452
+ }
1453
+ return normalizeWhitespace(el.getAttribute("title") || "");
1454
+ };
1455
+ const getImplicitRole = (el) => {
1456
+ if (!el || !(el instanceof Element)) return "";
1457
+ const explicitRole = (el.getAttribute("role") || "").trim().split(/\\s+/)[0];
1458
+ if (explicitRole) return explicitRole;
1459
+ const tag = el.tagName.toLowerCase();
1460
+ if (tag === "button") return "button";
1461
+ if (tag === "summary") return "button";
1462
+ if (tag === "a" && el.hasAttribute("href")) return "link";
1463
+ if (tag === "input") {
1464
+ const type = (el.getAttribute("type") || "text").toLowerCase();
1465
+ if (type === "checkbox") return "checkbox";
1466
+ if (type === "radio") return "radio";
1467
+ if (type === "range") return "slider";
1468
+ if (type === "submit" || type === "button" || type === "reset") return "button";
1469
+ if (type === "file") return "button";
1470
+ return "textbox";
1471
+ }
1472
+ if (tag === "textarea") return "textbox";
1473
+ if (tag === "select") return "combobox";
1474
+ if (tag === "img") return "img";
1475
+ if (tag === "ul" || tag === "ol") return "list";
1476
+ if (tag === "li") return "listitem";
1477
+ if (tag === "table") return "table";
1478
+ if (tag === "tr") return "row";
1479
+ if (tag === "td") return "cell";
1480
+ if (tag === "th") return "columnheader";
1481
+ if (/^h[1-6]$/.test(tag)) return "heading";
1482
+ if (tag === "option") return "option";
1483
+ if (tag === "fieldset") return "group";
1484
+ if (tag === "form") return "form";
1485
+ if (el.hasAttribute("contenteditable")) return "textbox";
1486
+ return explicitRole;
1487
+ };
1488
+ const matchText = (actual, expected, exact) => {
1489
+ const normalizedActual = normalizeWhitespace(actual);
1490
+ if (expected && expected.kind === "regex") {
1491
+ const regex = new RegExp(expected.source, expected.flags);
1492
+ return regex.test(normalizedActual);
1493
+ }
1494
+ const normalizedExpected = normalizeWhitespace(expected.value);
1495
+ if (exact) {
1496
+ return normalizedActual === normalizedExpected;
1497
+ }
1498
+ return normalizedActual.toLowerCase().includes(normalizedExpected.toLowerCase());
1499
+ };
1500
+ const matchName = (actual, expected, exact) => {
1501
+ const normalizedActual = normalizeWhitespace(actual);
1502
+ if (expected && expected.kind === "regex") {
1503
+ const regex = new RegExp(expected.source, expected.flags);
1504
+ return regex.test(normalizedActual);
1505
+ }
1506
+ const normalizedExpected = normalizeWhitespace(expected.value);
1507
+ if (exact) {
1508
+ return normalizedActual === normalizedExpected;
1509
+ }
1510
+ return normalizedActual.toLowerCase().includes(normalizedExpected.toLowerCase());
1511
+ };
1512
+ const query = ${serializedQuery};
1513
+ const nodes = Array.from(querySelectorAllDeep(document, "*"));
1514
+ const selectorMatches = () => {
1515
+ if (query.kind !== "selector") {
1516
+ return [];
1517
+ }
1518
+ if (query.selector.includes(">>>")) {
1519
+ return querySelectorAllDeep(document, query.selector);
1520
+ }
1521
+ if (query.parsed?.type === "xpath") {
1522
+ const result = document.evaluate(query.parsed.value, document, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
1523
+ const list = [];
1524
+ for (let i = 0; i < result.snapshotLength; i += 1) {
1525
+ const item = result.snapshotItem(i);
1526
+ if (item instanceof Element) {
1527
+ list.push(item);
1528
+ }
1529
+ }
1530
+ return list;
1531
+ }
1532
+ return Array.from(document.querySelectorAll(query.selector));
1533
+ };
1534
+ const selectorMatchSet = selectorMatches();
1535
+ const matches = nodes.filter((el) => {
1536
+ if (!(el instanceof Element)) return false;
1537
+ if (query.kind === "selector") {
1538
+ return selectorMatchSet.includes(el);
1539
+ }
1540
+ if (query.kind === "text") {
1541
+ const text = textFromElement(el);
1542
+ return matchText(text, query.text, Boolean(query.options?.exact));
1543
+ }
1544
+ const role = getImplicitRole(el);
1545
+ if (!role || role !== query.role) {
1546
+ return false;
1547
+ }
1548
+ if (!query.options?.includeHidden && isHidden(el)) {
1549
+ return false;
1550
+ }
1551
+ if (query.options?.name == null) {
1552
+ return true;
1553
+ }
1554
+ const name = getLabelText(el) || textFromElement(el);
1555
+ return matchName(name, query.options.name, Boolean(query.options?.exact));
1556
+ });
1557
+ const textMatches = query.kind === "text"
1558
+ ? matches.filter((el) => !matches.some((other) => other !== el && el.contains(other)))
1559
+ : matches;
1560
+ const elements = ${all ? "textMatches" : "textMatches.slice(0, 1)"};
1561
+ const el = elements[0] || null;
1562
+ ${body}
1563
+ })()`;
1564
+ }
1565
+ serializeLocatorQuery(query) {
1566
+ switch (query.kind) {
1567
+ case "selector":
1568
+ return JSON.stringify({ kind: "selector", selector: query.selector, options: query.options, parsed: parseSelector(query.selector) });
1569
+ case "text":
1570
+ return JSON.stringify({
1571
+ kind: "text",
1572
+ text: this.serializeTextQuery(query.text),
1573
+ options: query.options
1574
+ });
1575
+ case "role":
1576
+ return JSON.stringify({
1577
+ kind: "role",
1578
+ role: query.role,
1579
+ options: query.options ? {
1580
+ ...query.options,
1581
+ name: query.options.name != null ? this.serializeTextQuery(query.options.name) : void 0
1582
+ } : void 0
1583
+ });
1584
+ }
1585
+ }
1586
+ serializeTextQuery(text) {
1587
+ if (text instanceof RegExp) {
1588
+ return { kind: "regex", source: text.source, flags: text.flags.replace("g", "") };
1589
+ }
1590
+ return { kind: "string", value: text };
1591
+ }
1106
1592
  async performClick(selector, options, isDouble) {
1107
1593
  const start = Date.now();
1108
1594
  const actionName = isDouble ? "dblclick" : "click";
@@ -1323,6 +1809,11 @@ function ensureAllowedUrl(url, options = {}) {
1323
1809
  }
1324
1810
 
1325
1811
  // src/core/Page.ts
1812
+ function assertPdfBuffer(buffer) {
1813
+ if (buffer.length < 5 || buffer.subarray(0, 5).toString("utf-8") !== "%PDF-") {
1814
+ throw new Error("PDF generation failed: Chromium did not return a valid PDF");
1815
+ }
1816
+ }
1326
1817
  var Page = class {
1327
1818
  session;
1328
1819
  logger;
@@ -1375,7 +1866,13 @@ var Page = class {
1375
1866
  return null;
1376
1867
  }
1377
1868
  locator(selector) {
1378
- return new Locator(this.mainFrame(), selector);
1869
+ return this.mainFrame().locator(selector);
1870
+ }
1871
+ getByText(text, options = {}) {
1872
+ return this.mainFrame().getByText(text, options);
1873
+ }
1874
+ getByRole(role, options = {}) {
1875
+ return this.mainFrame().getByRole(role, options);
1379
1876
  }
1380
1877
  async goto(url, options = {}) {
1381
1878
  ensureAllowedUrl(url, { allowFileUrl: options.allowFileUrl });
@@ -1442,9 +1939,19 @@ var Page = class {
1442
1939
  async findLocators(options = {}) {
1443
1940
  return this.mainFrame().findLocators(options);
1444
1941
  }
1942
+ async content() {
1943
+ await this.waitForLoad();
1944
+ return this.mainFrame().evaluate(() => {
1945
+ const doctype = document.doctype;
1946
+ const doctypeText = doctype ? `<!DOCTYPE ${doctype.name}${doctype.publicId ? ` PUBLIC "${doctype.publicId}"` : ""}${doctype.systemId ? ` "${doctype.systemId}"` : ""}>` : "<!doctype html>";
1947
+ return `${doctypeText}
1948
+ ${document.documentElement.outerHTML}`;
1949
+ });
1950
+ }
1445
1951
  async screenshot(options = {}) {
1446
1952
  const start = Date.now();
1447
1953
  this.events.emit("action:start", { name: "screenshot", frameId: this.mainFrameId });
1954
+ await this.waitForLoad();
1448
1955
  const result = await this.session.send("Page.captureScreenshot", {
1449
1956
  format: options.format ?? "png",
1450
1957
  quality: options.quality,
@@ -1463,6 +1970,7 @@ var Page = class {
1463
1970
  async screenshotBase64(options = {}) {
1464
1971
  const start = Date.now();
1465
1972
  this.events.emit("action:start", { name: "screenshotBase64", frameId: this.mainFrameId });
1973
+ await this.waitForLoad();
1466
1974
  const result = await this.session.send("Page.captureScreenshot", {
1467
1975
  format: options.format ?? "png",
1468
1976
  quality: options.quality,
@@ -1476,26 +1984,37 @@ var Page = class {
1476
1984
  async pdf(options = {}) {
1477
1985
  const start = Date.now();
1478
1986
  this.events.emit("action:start", { name: "pdf", frameId: this.mainFrameId });
1479
- const result = await this.session.send("Page.printToPDF", {
1480
- landscape: options.landscape ?? false,
1481
- printBackground: options.printBackground ?? true,
1482
- scale: options.scale,
1483
- paperWidth: options.paperWidth,
1484
- paperHeight: options.paperHeight,
1485
- marginTop: options.marginTop,
1486
- marginBottom: options.marginBottom,
1487
- marginLeft: options.marginLeft,
1488
- marginRight: options.marginRight,
1489
- pageRanges: options.pageRanges,
1490
- preferCSSPageSize: options.preferCSSPageSize
1491
- });
1492
- const buffer = Buffer.from(result.data, "base64");
1493
- if (options.path) {
1494
- const resolved = path2.resolve(options.path);
1495
- fs2.writeFileSync(resolved, buffer);
1987
+ await this.waitForLoad();
1988
+ await this.session.send("Emulation.setEmulatedMedia", { media: "screen" });
1989
+ let buffer;
1990
+ try {
1991
+ const result = await this.session.send("Page.printToPDF", {
1992
+ landscape: options.landscape ?? false,
1993
+ printBackground: options.printBackground ?? true,
1994
+ scale: options.scale,
1995
+ paperWidth: options.paperWidth,
1996
+ paperHeight: options.paperHeight,
1997
+ marginTop: options.marginTop,
1998
+ marginBottom: options.marginBottom,
1999
+ marginLeft: options.marginLeft,
2000
+ marginRight: options.marginRight,
2001
+ pageRanges: options.pageRanges,
2002
+ preferCSSPageSize: options.preferCSSPageSize
2003
+ });
2004
+ buffer = Buffer.from(result.data, "base64");
2005
+ assertPdfBuffer(buffer);
2006
+ if (options.path) {
2007
+ const resolved = path2.resolve(options.path);
2008
+ fs2.writeFileSync(resolved, buffer);
2009
+ }
2010
+ } finally {
2011
+ try {
2012
+ await this.session.send("Emulation.setEmulatedMedia", { media: "" });
2013
+ } catch {
2014
+ }
2015
+ const duration = Date.now() - start;
2016
+ this.events.emit("action:end", { name: "pdf", frameId: this.mainFrameId, durationMs: duration });
1496
2017
  }
1497
- const duration = Date.now() - start;
1498
- this.events.emit("action:end", { name: "pdf", frameId: this.mainFrameId, durationMs: duration });
1499
2018
  return buffer;
1500
2019
  }
1501
2020
  getEvents() {
@@ -1504,6 +2023,13 @@ var Page = class {
1504
2023
  getDefaultTimeout() {
1505
2024
  return this.defaultTimeout;
1506
2025
  }
2026
+ async waitForLoad(timeoutMs = this.defaultTimeout) {
2027
+ const frame = this.mainFrame();
2028
+ await waitFor(async () => {
2029
+ const readyState = await frame.evaluate("document.readyState");
2030
+ return readyState === "complete";
2031
+ }, { timeoutMs, description: "page load" });
2032
+ }
1507
2033
  buildFrameTree(tree) {
1508
2034
  const frame = this.ensureFrame(tree.frame.id);
1509
2035
  frame.setMeta({ name: tree.frame.name, url: tree.frame.url, parentId: tree.frame.parentId });
@@ -1605,12 +2131,16 @@ var Browser = class {
1605
2131
  events;
1606
2132
  cleanupTasks;
1607
2133
  contexts = /* @__PURE__ */ new Set();
1608
- constructor(connection, child, logger, events, cleanupTasks = []) {
2134
+ wsEndpoint;
2135
+ pid;
2136
+ constructor(connection, child, logger, events, cleanupTasks = [], wsEndpoint = "") {
1609
2137
  this.connection = connection;
1610
2138
  this.process = child;
1611
2139
  this.logger = logger;
1612
2140
  this.events = events;
1613
2141
  this.cleanupTasks = cleanupTasks;
2142
+ this.wsEndpoint = wsEndpoint;
2143
+ this.pid = child?.pid ?? 0;
1614
2144
  }
1615
2145
  on(event, handler) {
1616
2146
  this.events.on(event, handler);
@@ -1632,6 +2162,23 @@ var Browser = class {
1632
2162
  await page.initialize();
1633
2163
  return page;
1634
2164
  }
2165
+ /** Attach to an existing page by target ID. */
2166
+ async attachPage(targetId) {
2167
+ const { sessionId } = await this.connection.send("Target.attachToTarget", { targetId, flatten: true });
2168
+ const session = this.connection.createSession(sessionId);
2169
+ const page = new Page(session, this.logger, this.events);
2170
+ await page.initialize();
2171
+ return page;
2172
+ }
2173
+ /** List open page targets. */
2174
+ async pages() {
2175
+ const result = await this.connection.send("Target.getTargets");
2176
+ return result.targetInfos.filter((t) => t.type === "page").map((t) => ({ targetId: t.targetId, url: t.url, title: t.title }));
2177
+ }
2178
+ /** Disconnect without killing the browser process. */
2179
+ async disconnect() {
2180
+ await this.connection.close();
2181
+ }
1635
2182
  async disposeContext(contextId) {
1636
2183
  if (!contextId) return;
1637
2184
  try {
@@ -1651,7 +2198,7 @@ var Browser = class {
1651
2198
  } catch {
1652
2199
  }
1653
2200
  await this.connection.close();
1654
- if (!this.process.killed) {
2201
+ if (this.process && !this.process.killed) {
1655
2202
  this.process.kill();
1656
2203
  }
1657
2204
  for (const task of this.cleanupTasks) {
@@ -1993,6 +2540,7 @@ var ChromiumManager = class {
1993
2540
  }
1994
2541
  if (options.maximize) {
1995
2542
  args.push("--start-maximized");
2543
+ args.push("--window-size=1920,1080");
1996
2544
  }
1997
2545
  if (options.args) {
1998
2546
  args.push(...options.args);
@@ -2021,7 +2569,7 @@ var ChromiumManager = class {
2021
2569
  logger.info(`Assertion ${payload.name}`, ...args2);
2022
2570
  });
2023
2571
  }
2024
- const browser = new Browser(connection, child, logger, events, cleanupTasks);
2572
+ const browser = new Browser(connection, child, logger, events, cleanupTasks, wsEndpoint);
2025
2573
  return browser;
2026
2574
  }
2027
2575
  resolveCacheRoot(platform) {
@@ -2197,42 +2745,42 @@ var AssertionError = class extends Error {
2197
2745
 
2198
2746
  // src/assert/expect.ts
2199
2747
  var ElementExpectation = class _ElementExpectation {
2200
- frame;
2201
- selector;
2748
+ target;
2202
2749
  options;
2203
2750
  negate;
2204
2751
  events;
2205
- constructor(frame, selector, options, negate, events) {
2206
- this.frame = frame;
2207
- this.selector = selector;
2752
+ constructor(target, options, negate, events) {
2753
+ this.target = target;
2208
2754
  this.options = options;
2209
2755
  this.negate = negate;
2210
2756
  this.events = events;
2211
2757
  }
2212
2758
  get not() {
2213
- return new _ElementExpectation(this.frame, this.selector, this.options, !this.negate, this.events);
2759
+ return new _ElementExpectation(this.target, this.options, !this.negate, this.events);
2214
2760
  }
2215
2761
  async toExist() {
2216
2762
  return this.assert(async () => {
2217
- const exists = await this.frame.exists(this.selector, this.options);
2763
+ const exists = await this.target.exists();
2218
2764
  return this.negate ? !exists : exists;
2219
2765
  }, this.negate ? "Expected element not to exist" : "Expected element to exist");
2220
2766
  }
2221
2767
  async toBeVisible() {
2222
2768
  return this.assert(async () => {
2223
- const visible = await this.frame.isVisible(this.selector, this.options);
2769
+ const visible = await this.target.isVisible();
2770
+ if (visible == null) return this.negate ? true : false;
2224
2771
  return this.negate ? !visible : visible;
2225
2772
  }, this.negate ? "Expected element not to be visible" : "Expected element to be visible");
2226
2773
  }
2227
2774
  async toBeHidden() {
2228
2775
  return this.assert(async () => {
2229
- const visible = await this.frame.isVisible(this.selector, this.options);
2776
+ const visible = await this.target.isVisible();
2777
+ if (visible == null) return this.negate ? true : false;
2230
2778
  return this.negate ? visible : !visible;
2231
2779
  }, this.negate ? "Expected element not to be hidden" : "Expected element to be hidden");
2232
2780
  }
2233
2781
  async toBeEnabled() {
2234
2782
  return this.assert(async () => {
2235
- const enabled = await this.frame.isEnabled(this.selector, this.options);
2783
+ const enabled = await this.target.isEnabled();
2236
2784
  if (enabled == null) {
2237
2785
  return this.negate ? true : false;
2238
2786
  }
@@ -2241,7 +2789,7 @@ var ElementExpectation = class _ElementExpectation {
2241
2789
  }
2242
2790
  async toBeDisabled() {
2243
2791
  return this.assert(async () => {
2244
- const enabled = await this.frame.isEnabled(this.selector, this.options);
2792
+ const enabled = await this.target.isEnabled();
2245
2793
  if (enabled == null) {
2246
2794
  return this.negate ? true : false;
2247
2795
  }
@@ -2251,7 +2799,7 @@ var ElementExpectation = class _ElementExpectation {
2251
2799
  }
2252
2800
  async toBeChecked() {
2253
2801
  return this.assert(async () => {
2254
- const checked = await this.frame.isChecked(this.selector, this.options);
2802
+ const checked = await this.target.isChecked();
2255
2803
  if (checked == null) {
2256
2804
  return this.negate ? true : false;
2257
2805
  }
@@ -2260,7 +2808,7 @@ var ElementExpectation = class _ElementExpectation {
2260
2808
  }
2261
2809
  async toBeUnchecked() {
2262
2810
  return this.assert(async () => {
2263
- const checked = await this.frame.isChecked(this.selector, this.options);
2811
+ const checked = await this.target.isChecked();
2264
2812
  if (checked == null) {
2265
2813
  return this.negate ? true : false;
2266
2814
  }
@@ -2271,7 +2819,7 @@ var ElementExpectation = class _ElementExpectation {
2271
2819
  async toHaveText(textOrRegex) {
2272
2820
  const expected = textOrRegex;
2273
2821
  return this.assert(async () => {
2274
- const text = await this.frame.text(this.selector, this.options);
2822
+ const text = await this.target.text();
2275
2823
  if (text == null) {
2276
2824
  return this.negate ? true : false;
2277
2825
  }
@@ -2282,7 +2830,7 @@ var ElementExpectation = class _ElementExpectation {
2282
2830
  async toHaveExactText(textOrRegex) {
2283
2831
  const expected = textOrRegex;
2284
2832
  return this.assert(async () => {
2285
- const text = await this.frame.text(this.selector, this.options);
2833
+ const text = await this.target.text();
2286
2834
  if (text == null) {
2287
2835
  return this.negate ? true : false;
2288
2836
  }
@@ -2293,7 +2841,7 @@ var ElementExpectation = class _ElementExpectation {
2293
2841
  async toContainText(textOrRegex) {
2294
2842
  const expected = textOrRegex;
2295
2843
  return this.assert(async () => {
2296
- const text = await this.frame.text(this.selector, this.options);
2844
+ const text = await this.target.text();
2297
2845
  if (text == null) {
2298
2846
  return this.negate ? true : false;
2299
2847
  }
@@ -2304,7 +2852,7 @@ var ElementExpectation = class _ElementExpectation {
2304
2852
  async toHaveValue(valueOrRegex) {
2305
2853
  const expected = valueOrRegex;
2306
2854
  return this.assert(async () => {
2307
- const value = await this.frame.value(this.selector, this.options);
2855
+ const value = await this.target.value();
2308
2856
  if (value == null) {
2309
2857
  return this.negate ? true : false;
2310
2858
  }
@@ -2315,7 +2863,7 @@ var ElementExpectation = class _ElementExpectation {
2315
2863
  async toHaveAttribute(name, valueOrRegex) {
2316
2864
  const expected = valueOrRegex;
2317
2865
  return this.assert(async () => {
2318
- const value = await this.frame.attribute(this.selector, name, this.options);
2866
+ const value = await this.target.attribute(name);
2319
2867
  if (expected === void 0) {
2320
2868
  const exists = value != null;
2321
2869
  return this.negate ? !exists : exists;
@@ -2335,7 +2883,7 @@ var ElementExpectation = class _ElementExpectation {
2335
2883
  }
2336
2884
  async toHaveCount(expected) {
2337
2885
  return this.assert(async () => {
2338
- const count = await this.frame.count(this.selector, this.options);
2886
+ const count = await this.target.count();
2339
2887
  const matches = count === expected;
2340
2888
  return this.negate ? !matches : matches;
2341
2889
  }, this.negate ? "Expected element count not to match" : "Expected element count to match", { expected });
@@ -2343,7 +2891,7 @@ var ElementExpectation = class _ElementExpectation {
2343
2891
  async toHaveClass(nameOrRegex) {
2344
2892
  const expected = nameOrRegex;
2345
2893
  return this.assert(async () => {
2346
- const classes = await this.frame.classes(this.selector, this.options);
2894
+ const classes = await this.target.classes();
2347
2895
  if (classes == null) {
2348
2896
  return this.negate ? true : false;
2349
2897
  }
@@ -2353,7 +2901,7 @@ var ElementExpectation = class _ElementExpectation {
2353
2901
  }
2354
2902
  async toHaveClasses(expected) {
2355
2903
  return this.assert(async () => {
2356
- const classes = await this.frame.classes(this.selector, this.options);
2904
+ const classes = await this.target.classes();
2357
2905
  if (classes == null) {
2358
2906
  return this.negate ? true : false;
2359
2907
  }
@@ -2364,7 +2912,7 @@ var ElementExpectation = class _ElementExpectation {
2364
2912
  async toHaveCss(property, valueOrRegex) {
2365
2913
  const expected = valueOrRegex;
2366
2914
  return this.assert(async () => {
2367
- const value = await this.frame.css(this.selector, property, this.options);
2915
+ const value = await this.target.css(property);
2368
2916
  if (value == null) {
2369
2917
  return this.negate ? true : false;
2370
2918
  }
@@ -2375,7 +2923,7 @@ var ElementExpectation = class _ElementExpectation {
2375
2923
  }
2376
2924
  async toHaveFocus() {
2377
2925
  return this.assert(async () => {
2378
- const focused = await this.frame.hasFocus(this.selector, this.options);
2926
+ const focused = await this.target.hasFocus();
2379
2927
  if (focused == null) {
2380
2928
  return this.negate ? true : false;
2381
2929
  }
@@ -2384,7 +2932,7 @@ var ElementExpectation = class _ElementExpectation {
2384
2932
  }
2385
2933
  async toBeInViewport(options = {}) {
2386
2934
  return this.assert(async () => {
2387
- const inViewport = await this.frame.isInViewport(this.selector, this.options, Boolean(options.fully));
2935
+ const inViewport = await this.target.isInViewport(Boolean(options.fully));
2388
2936
  if (inViewport == null) {
2389
2937
  return this.negate ? true : false;
2390
2938
  }
@@ -2393,7 +2941,7 @@ var ElementExpectation = class _ElementExpectation {
2393
2941
  }
2394
2942
  async toBeEditable() {
2395
2943
  return this.assert(async () => {
2396
- const editable = await this.frame.isEditable(this.selector, this.options);
2944
+ const editable = await this.target.isEditable();
2397
2945
  if (editable == null) {
2398
2946
  return this.negate ? true : false;
2399
2947
  }
@@ -2403,7 +2951,7 @@ var ElementExpectation = class _ElementExpectation {
2403
2951
  async assert(predicate, message, details = {}) {
2404
2952
  const timeoutMs = this.options.timeoutMs ?? 3e4;
2405
2953
  const start = Date.now();
2406
- this.events.emit("assertion:start", { name: message, selector: this.selector, frameId: this.frame.id });
2954
+ this.events.emit("assertion:start", { name: message, selector: this.target.label, frameId: this.target.frameId });
2407
2955
  let lastState;
2408
2956
  try {
2409
2957
  await waitFor(async () => {
@@ -2413,13 +2961,51 @@ var ElementExpectation = class _ElementExpectation {
2413
2961
  }, { timeoutMs, description: message });
2414
2962
  } catch {
2415
2963
  const duration2 = Date.now() - start;
2416
- this.events.emit("assertion:end", { name: message, selector: this.selector, frameId: this.frame.id, durationMs: duration2, status: "failed" });
2417
- throw new AssertionError(message, { selector: this.selector, timeoutMs, lastState: { lastState, ...details } });
2964
+ this.events.emit("assertion:end", { name: message, selector: this.target.label, frameId: this.target.frameId, durationMs: duration2, status: "failed" });
2965
+ throw new AssertionError(message, { selector: this.target.label, timeoutMs, lastState: { lastState, ...details } });
2418
2966
  }
2419
2967
  const duration = Date.now() - start;
2420
- this.events.emit("assertion:end", { name: message, selector: this.selector, frameId: this.frame.id, durationMs: duration, status: "passed" });
2968
+ this.events.emit("assertion:end", { name: message, selector: this.target.label, frameId: this.target.frameId, durationMs: duration, status: "passed" });
2421
2969
  }
2422
2970
  };
2971
+ function frameTarget(frame, selector, options) {
2972
+ return {
2973
+ label: selector,
2974
+ frameId: frame.id,
2975
+ exists: () => frame.exists(selector, options),
2976
+ isVisible: () => frame.isVisible(selector, options),
2977
+ isEnabled: () => frame.isEnabled(selector, options),
2978
+ isChecked: () => frame.isChecked(selector, options),
2979
+ text: () => frame.text(selector, options),
2980
+ value: () => frame.value(selector, options),
2981
+ attribute: (name) => frame.attribute(selector, name, options),
2982
+ count: () => frame.count(selector, options),
2983
+ classes: () => frame.classes(selector, options),
2984
+ css: (property) => frame.css(selector, property, options),
2985
+ hasFocus: () => frame.hasFocus(selector, options),
2986
+ isInViewport: (fully = false) => frame.isInViewport(selector, options, fully),
2987
+ isEditable: () => frame.isEditable(selector, options)
2988
+ };
2989
+ }
2990
+ function locatorTarget(locator) {
2991
+ return {
2992
+ label: locator.describe(),
2993
+ frameId: locator.getFrameId(),
2994
+ exists: () => locator.exists(),
2995
+ isVisible: () => locator.isVisible(),
2996
+ isEnabled: () => locator.isEnabled(),
2997
+ isChecked: () => locator.isChecked(),
2998
+ text: () => locator.text(),
2999
+ value: () => locator.value(),
3000
+ attribute: (name) => locator.attribute(name),
3001
+ count: () => locator.count(),
3002
+ classes: () => locator.classes(),
3003
+ css: (property) => locator.css(property),
3004
+ hasFocus: () => locator.hasFocus(),
3005
+ isInViewport: (fully = false) => locator.isInViewport(fully),
3006
+ isEditable: () => locator.isEditable()
3007
+ };
3008
+ }
2423
3009
  var ExpectFrame = class {
2424
3010
  frame;
2425
3011
  events;
@@ -2428,25 +3014,31 @@ var ExpectFrame = class {
2428
3014
  this.events = events;
2429
3015
  }
2430
3016
  element(selector, options = {}) {
2431
- return new ElementExpectation(this.frame, selector, options, false, this.events);
3017
+ return new ElementExpectation(frameTarget(this.frame, selector, options), options, false, this.events);
2432
3018
  }
2433
3019
  };
2434
- function expect(page) {
3020
+ function expect(target) {
3021
+ if (target instanceof Locator) {
3022
+ return new ElementExpectation(locatorTarget(target), {}, false, target.getEvents());
3023
+ }
2435
3024
  return {
2436
- element: (selector, options = {}) => new ElementExpectation(page.mainFrame(), selector, options, false, page.getEvents()),
3025
+ element: (selector, options = {}) => new ElementExpectation(frameTarget(target.mainFrame(), selector, options), options, false, target.getEvents()),
2437
3026
  frame: (options) => {
2438
- const frame = page.frame(options);
3027
+ const frame = target.frame(options);
2439
3028
  if (!frame) {
2440
3029
  throw new AssertionError("Frame not found", { selector: JSON.stringify(options) });
2441
3030
  }
2442
- return new ExpectFrame(frame, page.getEvents());
3031
+ return new ExpectFrame(frame, target.getEvents());
2443
3032
  }
2444
3033
  };
2445
3034
  }
2446
- Page.prototype.expect = function(selector, options) {
3035
+ Page.prototype.expect = function(target, options) {
2447
3036
  const builder = expect(this);
2448
- if (selector) {
2449
- return builder.element(selector, options);
3037
+ if (target instanceof Locator) {
3038
+ return expect(target);
3039
+ }
3040
+ if (target) {
3041
+ return builder.element(target, options);
2450
3042
  }
2451
3043
  return builder;
2452
3044
  };
@@ -2463,38 +3055,97 @@ var automaton = {
2463
3055
  async launch(options = {}) {
2464
3056
  const manager = new ChromiumManager(options.logger);
2465
3057
  return manager.launch(options);
3058
+ },
3059
+ async connect(wsEndpoint, options = {}) {
3060
+ const logger = options.logger ?? new Logger(options.logLevel ?? "warn");
3061
+ const connection = new Connection(wsEndpoint, logger);
3062
+ await connection.waitForOpen();
3063
+ const events = new AutomationEvents();
3064
+ const logEvents = options.logEvents ?? true;
3065
+ const logActions = options.logActions ?? true;
3066
+ const logAssertions = options.logAssertions ?? true;
3067
+ if (logEvents && logActions) {
3068
+ events.on("action:end", (payload) => {
3069
+ const selector = payload.sensitive ? void 0 : payload.selector;
3070
+ const args = [];
3071
+ if (selector) args.push(selector);
3072
+ if (typeof payload.durationMs === "number") args.push(`${payload.durationMs}ms`);
3073
+ logger.info(`Action ${payload.name}`, ...args);
3074
+ });
3075
+ }
3076
+ if (logEvents && logAssertions) {
3077
+ events.on("assertion:end", (payload) => {
3078
+ const args = [];
3079
+ if (payload.selector) args.push(payload.selector);
3080
+ if (typeof payload.durationMs === "number") args.push(`${payload.durationMs}ms`);
3081
+ logger.info(`Assertion ${payload.name}`, ...args);
3082
+ });
3083
+ }
3084
+ return new Browser(connection, null, logger, events, [], wsEndpoint);
2466
3085
  }
2467
3086
  };
2468
3087
 
2469
3088
  // src/cli.ts
3089
+ function sessionFilePath() {
3090
+ const platform = detectPlatform();
3091
+ const cacheRoot = defaultCacheRoot(platform);
3092
+ return path5.join(cacheRoot, "session.json");
3093
+ }
3094
+ function writeSession(info) {
3095
+ const filePath = sessionFilePath();
3096
+ fs5.mkdirSync(path5.dirname(filePath), { recursive: true });
3097
+ fs5.writeFileSync(filePath, JSON.stringify(info));
3098
+ }
3099
+ function readSession() {
3100
+ const filePath = sessionFilePath();
3101
+ if (!fs5.existsSync(filePath)) return null;
3102
+ try {
3103
+ const data = JSON.parse(fs5.readFileSync(filePath, "utf-8"));
3104
+ if (!data.wsEndpoint || !data.pid) return null;
3105
+ try {
3106
+ process.kill(data.pid, 0);
3107
+ } catch {
3108
+ clearSession();
3109
+ return null;
3110
+ }
3111
+ return data;
3112
+ } catch {
3113
+ return null;
3114
+ }
3115
+ }
3116
+ function clearSession() {
3117
+ const filePath = sessionFilePath();
3118
+ try {
3119
+ fs5.unlinkSync(filePath);
3120
+ } catch {
3121
+ }
3122
+ }
2470
3123
  function printHelp() {
2471
3124
  console.log(`cdpwright (cpw) \u2014 Chromium automation CLI
2472
3125
 
2473
3126
  Commands:
3127
+ screenshot <url> -o f Take a screenshot (PNG/JPEG)
3128
+ pdf <url> -o file.pdf Generate visual PDF of page
3129
+ html <url> -o file.html Save the page HTML source
3130
+ eval <url> <script> Run JS in page, print result as JSON
2474
3131
  download [options] Download pinned Chromium snapshot
2475
3132
  install Alias for download
2476
- open <url> Launch headed browser and navigate to URL
2477
- screenshot <url> -o f Take a screenshot (PNG/JPEG)
2478
- pdf <url> -o file.pdf Generate PDF of page (headless only)
2479
- eval <url> <script> Run script in page, print result as JSON
2480
3133
  version Print cdpwright and Chromium versions
2481
3134
 
2482
- Download options:
2483
- --latest Download the latest Chromium revision
2484
- --mirror <url> Custom mirror base URL
2485
- --url <url> Exact zip URL override
2486
-
2487
- Common options:
3135
+ Options:
2488
3136
  --headless Run in headless mode (default for screenshot, pdf, eval)
2489
- --headed Run in headed mode (default for open)
2490
-
2491
- Screenshot options:
2492
- -o, --output <file> Output file path (required)
2493
- --full-page Capture full scrollable page
3137
+ --headed Run in headed mode
3138
+ -o, --output <file> Output file path (for screenshot, html, pdf)
3139
+ --full-page Capture full scrollable page (for screenshot)
3140
+ --latest Download the latest Chromium revision (for download)
3141
+ --mirror <url> Custom mirror base URL (for download)
3142
+ --url <url> Exact zip URL override (for download)
2494
3143
 
2495
- PDF options:
2496
- -o, --output <file> Output file path (required)
2497
- Note: pdf always runs headless (CDP limitation)`);
3144
+ Interactive session:
3145
+ open <url> Open browser and start a session
3146
+ close Close the running session
3147
+ When a session is running, commands accept no URL and operate on the
3148
+ live page instead: cpw screenshot -o shot.png | cpw eval "document.title"`);
2498
3149
  }
2499
3150
  function hasFlag(args, flag) {
2500
3151
  return args.includes(flag);
@@ -2521,14 +3172,46 @@ function positionalArgs(args) {
2521
3172
  }
2522
3173
  return result;
2523
3174
  }
2524
- async function withPage(url, options, fn) {
3175
+ function launchArgs() {
2525
3176
  const args = [];
2526
3177
  if (process.platform === "linux") {
2527
3178
  args.push("--no-sandbox", "--no-zygote", "--disable-dev-shm-usage");
2528
3179
  }
3180
+ return args;
3181
+ }
3182
+ function pngDimensions(buffer) {
3183
+ if (buffer.length < 24) {
3184
+ throw new Error("Invalid PNG screenshot");
3185
+ }
3186
+ if (buffer.readUInt32BE(0) !== 2303741511 || buffer.readUInt32BE(4) !== 218765834) {
3187
+ throw new Error("Invalid PNG screenshot");
3188
+ }
3189
+ return {
3190
+ width: buffer.readUInt32BE(16),
3191
+ height: buffer.readUInt32BE(20)
3192
+ };
3193
+ }
3194
+ async function connectToSession() {
3195
+ const session = readSession();
3196
+ if (!session) {
3197
+ console.error("No running session. Start one with: cpw open <url>");
3198
+ process.exit(1);
3199
+ }
3200
+ const browser = await automaton.connect(session.wsEndpoint);
3201
+ const targets = await browser.pages();
3202
+ if (targets.length === 0) {
3203
+ console.error("Session has no open pages.");
3204
+ await browser.disconnect();
3205
+ process.exit(1);
3206
+ }
3207
+ const page = await browser.attachPage(targets[0].targetId);
3208
+ return { browser, page };
3209
+ }
3210
+ async function withPage(url, options, fn) {
2529
3211
  const browser = await automaton.launch({
2530
3212
  headless: options.headless ?? true,
2531
- args,
3213
+ maximize: options.maximize ?? false,
3214
+ args: launchArgs(),
2532
3215
  logLevel: "warn"
2533
3216
  });
2534
3217
  try {
@@ -2555,19 +3238,24 @@ async function cmdOpen(rest) {
2555
3238
  process.exit(1);
2556
3239
  }
2557
3240
  const headless = resolveHeadless(rest, false);
2558
- const args = [];
2559
- if (process.platform === "linux") {
2560
- args.push("--no-sandbox", "--no-zygote", "--disable-dev-shm-usage");
2561
- }
2562
3241
  const browser = await automaton.launch({
2563
3242
  headless,
2564
- args,
3243
+ args: launchArgs(),
2565
3244
  logLevel: "warn"
2566
3245
  });
2567
3246
  const page = await browser.newPage();
2568
3247
  await page.goto(url, { waitUntil: "load" });
3248
+ writeSession({
3249
+ wsEndpoint: browser.wsEndpoint,
3250
+ pid: browser.pid || process.pid
3251
+ });
2569
3252
  console.log(`Browser open at ${url}`);
2570
- console.log("Press Ctrl+C to close.");
3253
+ console.log(`Session saved. Run commands in another terminal:`);
3254
+ console.log(` npx cpw screenshot -o shot.png`);
3255
+ console.log(` npx cpw eval "document.title"`);
3256
+ console.log(` npx cpw close`);
3257
+ console.log(`
3258
+ Press Ctrl+C to close.`);
2571
3259
  await new Promise((resolve) => {
2572
3260
  process.on("SIGINT", () => {
2573
3261
  console.log("\nClosing browser...");
@@ -2575,52 +3263,200 @@ async function cmdOpen(rest) {
2575
3263
  });
2576
3264
  process.on("SIGTERM", () => resolve());
2577
3265
  });
3266
+ clearSession();
3267
+ try {
3268
+ await browser.close();
3269
+ } catch {
3270
+ }
3271
+ }
3272
+ async function cmdClose() {
3273
+ const session = readSession();
3274
+ if (!session) {
3275
+ console.error("No running session.");
3276
+ process.exit(1);
3277
+ }
2578
3278
  try {
3279
+ const browser = await automaton.connect(session.wsEndpoint);
2579
3280
  await browser.close();
2580
3281
  } catch {
3282
+ try {
3283
+ process.kill(session.pid, "SIGTERM");
3284
+ } catch {
3285
+ }
2581
3286
  }
3287
+ clearSession();
3288
+ console.log("Session closed.");
2582
3289
  }
2583
3290
  async function cmdScreenshot(rest) {
2584
- const url = positionalArgs(rest)[0];
3291
+ const pos = positionalArgs(rest);
3292
+ const url = pos[0];
2585
3293
  const output = flagValue(rest, "-o") || flagValue(rest, "--output");
2586
- if (!url || !output) {
2587
- console.error("Usage: cpw screenshot <url> -o <file> [--full-page]");
3294
+ if (!output) {
3295
+ console.error("Usage: cpw screenshot [url] -o <file> [--full-page]");
2588
3296
  process.exit(1);
2589
3297
  }
2590
3298
  const fullPage = hasFlag(rest, "--full-page");
2591
- const headless = resolveHeadless(rest, true);
2592
3299
  const format = output.endsWith(".jpeg") || output.endsWith(".jpg") ? "jpeg" : "png";
2593
- await withPage(url, { headless }, async (page) => {
2594
- await page.screenshot({ path: output, format, fullPage });
2595
- });
3300
+ if (url) {
3301
+ const headless = resolveHeadless(rest, true);
3302
+ await withPage(url, { headless }, async (page) => {
3303
+ await page.screenshot({ path: output, format, fullPage });
3304
+ });
3305
+ } else {
3306
+ const { browser, page } = await connectToSession();
3307
+ try {
3308
+ await page.screenshot({ path: output, format, fullPage });
3309
+ } finally {
3310
+ await browser.disconnect();
3311
+ }
3312
+ }
2596
3313
  console.log(`Screenshot saved to ${output}`);
2597
3314
  }
3315
+ async function cmdHtml(rest) {
3316
+ const pos = positionalArgs(rest);
3317
+ const url = pos[0];
3318
+ const output = flagValue(rest, "-o") || flagValue(rest, "--output");
3319
+ if (!output) {
3320
+ console.error("Usage: cpw html [url] -o <file>");
3321
+ process.exit(1);
3322
+ }
3323
+ const writeHtml = async (page) => {
3324
+ const html = await page.content();
3325
+ fs5.writeFileSync(path5.resolve(output), html, "utf-8");
3326
+ };
3327
+ if (url) {
3328
+ const headless = resolveHeadless(rest, true);
3329
+ await withPage(url, { headless }, async (page) => {
3330
+ await writeHtml(page);
3331
+ });
3332
+ } else {
3333
+ const { browser, page } = await connectToSession();
3334
+ try {
3335
+ await writeHtml(page);
3336
+ } finally {
3337
+ await browser.disconnect();
3338
+ }
3339
+ }
3340
+ console.log(`HTML saved to ${output}`);
3341
+ }
2598
3342
  async function cmdPdf(rest) {
2599
- const url = positionalArgs(rest)[0];
3343
+ const pos = positionalArgs(rest);
3344
+ const url = pos[0];
2600
3345
  const output = flagValue(rest, "-o") || flagValue(rest, "--output");
2601
- if (!url || !output) {
2602
- console.error("Usage: cpw pdf <url> -o <file>");
3346
+ if (!output) {
3347
+ console.error("Usage: cpw pdf [url] -o <file>");
2603
3348
  process.exit(1);
2604
3349
  }
2605
- await withPage(url, { headless: true }, async (page) => {
2606
- await page.pdf({ path: output, printBackground: true });
2607
- });
3350
+ const renderVisualPdf = async (browser, page) => {
3351
+ const screenshot = await page.screenshotBase64({ format: "png" });
3352
+ const screenshotSize = pngDimensions(Buffer.from(screenshot, "base64"));
3353
+ const context = await browser.newContext();
3354
+ try {
3355
+ const helperPage = await context.newPage();
3356
+ const imageDataUrl = `data:image/png;base64,${screenshot}`;
3357
+ const screenshotDimensions = await helperPage.evaluate((dataUrl) => {
3358
+ document.open();
3359
+ document.write(`<!doctype html>
3360
+ <html>
3361
+ <head>
3362
+ <meta charset="utf-8">
3363
+ <style>
3364
+ html, body {
3365
+ margin: 0;
3366
+ padding: 0;
3367
+ overflow: hidden;
3368
+ background: #fff;
3369
+ }
3370
+ img {
3371
+ display: block;
3372
+ width: 100%;
3373
+ height: auto;
3374
+ }
3375
+ </style>
3376
+ </head>
3377
+ <body>
3378
+ <img id="shot" alt="page screenshot">
3379
+ </body>
3380
+ </html>`);
3381
+ document.close();
3382
+ const img = document.getElementById("shot");
3383
+ if (!(img instanceof HTMLImageElement)) {
3384
+ throw new Error("Failed to create PDF preview image");
3385
+ }
3386
+ return new Promise((resolve, reject) => {
3387
+ img.onload = () => {
3388
+ resolve({ width: img.naturalWidth || 0, height: img.naturalHeight || 0 });
3389
+ };
3390
+ img.onerror = () => reject(new Error("Failed to load screenshot image"));
3391
+ img.src = dataUrl;
3392
+ });
3393
+ }, imageDataUrl);
3394
+ await helperPage.pdf({
3395
+ path: output,
3396
+ printBackground: true,
3397
+ paperWidth: Math.max((screenshotDimensions?.width ?? screenshotSize.width) / 96, 1),
3398
+ paperHeight: Math.max((screenshotDimensions?.height ?? screenshotSize.height) / 96, 1),
3399
+ marginTop: 0,
3400
+ marginBottom: 0,
3401
+ marginLeft: 0,
3402
+ marginRight: 0,
3403
+ scale: 1,
3404
+ preferCSSPageSize: false
3405
+ });
3406
+ } finally {
3407
+ await context.close();
3408
+ }
3409
+ };
3410
+ if (url) {
3411
+ await withPage(url, { headless: true, maximize: true }, async (page, browser) => {
3412
+ await renderVisualPdf(browser, page);
3413
+ });
3414
+ } else {
3415
+ const { browser, page } = await connectToSession();
3416
+ try {
3417
+ await renderVisualPdf(browser, page);
3418
+ } finally {
3419
+ await browser.disconnect();
3420
+ }
3421
+ }
2608
3422
  console.log(`PDF saved to ${output}`);
2609
3423
  }
2610
3424
  async function cmdEval(rest) {
2611
3425
  const pos = positionalArgs(rest);
2612
- const url = pos[0];
2613
- const script = pos[1];
2614
- if (!url || !script) {
2615
- console.error("Usage: cpw eval <url> <script>");
3426
+ const session = readSession();
3427
+ let url;
3428
+ let script;
3429
+ if (pos.length >= 2) {
3430
+ url = pos[0];
3431
+ script = pos[1];
3432
+ } else if (pos.length === 1 && session) {
3433
+ script = pos[0];
3434
+ } else if (pos.length === 1) {
3435
+ console.error("Usage: cpw eval <url> <script>\n cpw eval <script> (when a session is running)");
3436
+ process.exit(1);
3437
+ return;
3438
+ } else {
3439
+ console.error("Usage: cpw eval [url] <script>");
2616
3440
  process.exit(1);
3441
+ return;
3442
+ }
3443
+ if (url) {
3444
+ const headless = resolveHeadless(rest, true);
3445
+ await withPage(url, { headless }, async (page) => {
3446
+ const result = await page.evaluate(script);
3447
+ const output = result === void 0 ? "undefined" : JSON.stringify(result, null, 2);
3448
+ console.log(output);
3449
+ });
3450
+ } else {
3451
+ const { browser, page } = await connectToSession();
3452
+ try {
3453
+ const result = await page.evaluate(script);
3454
+ const output = result === void 0 ? "undefined" : JSON.stringify(result, null, 2);
3455
+ console.log(output);
3456
+ } finally {
3457
+ await browser.disconnect();
3458
+ }
2617
3459
  }
2618
- const headless = resolveHeadless(rest, true);
2619
- await withPage(url, { headless }, async (page) => {
2620
- const result = await page.evaluate(script);
2621
- const output = result === void 0 ? "undefined" : JSON.stringify(result, null, 2);
2622
- console.log(output);
2623
- });
2624
3460
  }
2625
3461
  async function cmdVersion() {
2626
3462
  const pkgPath = path5.resolve(path5.dirname(fileURLToPath(import.meta.url)), "../package.json");
@@ -2646,6 +3482,10 @@ async function cmdVersion() {
2646
3482
  } else {
2647
3483
  console.log("Chromium: not installed (run 'cpw install')");
2648
3484
  }
3485
+ const session = readSession();
3486
+ if (session) {
3487
+ console.log(`Session: active (pid ${session.pid})`);
3488
+ }
2649
3489
  }
2650
3490
  async function main() {
2651
3491
  const [, , command, ...rest] = process.argv;
@@ -2659,8 +3499,12 @@ async function main() {
2659
3499
  return cmdDownload(rest);
2660
3500
  case "open":
2661
3501
  return cmdOpen(rest);
3502
+ case "close":
3503
+ return cmdClose();
2662
3504
  case "screenshot":
2663
3505
  return cmdScreenshot(rest);
3506
+ case "html":
3507
+ return cmdHtml(rest);
2664
3508
  case "pdf":
2665
3509
  return cmdPdf(rest);
2666
3510
  case "eval":