@toolstackhq/cdpwright 1.2.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";
@@ -1380,7 +1866,13 @@ var Page = class {
1380
1866
  return null;
1381
1867
  }
1382
1868
  locator(selector) {
1383
- 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);
1384
1876
  }
1385
1877
  async goto(url, options = {}) {
1386
1878
  ensureAllowedUrl(url, { allowFileUrl: options.allowFileUrl });
@@ -2253,42 +2745,42 @@ var AssertionError = class extends Error {
2253
2745
 
2254
2746
  // src/assert/expect.ts
2255
2747
  var ElementExpectation = class _ElementExpectation {
2256
- frame;
2257
- selector;
2748
+ target;
2258
2749
  options;
2259
2750
  negate;
2260
2751
  events;
2261
- constructor(frame, selector, options, negate, events) {
2262
- this.frame = frame;
2263
- this.selector = selector;
2752
+ constructor(target, options, negate, events) {
2753
+ this.target = target;
2264
2754
  this.options = options;
2265
2755
  this.negate = negate;
2266
2756
  this.events = events;
2267
2757
  }
2268
2758
  get not() {
2269
- 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);
2270
2760
  }
2271
2761
  async toExist() {
2272
2762
  return this.assert(async () => {
2273
- const exists = await this.frame.exists(this.selector, this.options);
2763
+ const exists = await this.target.exists();
2274
2764
  return this.negate ? !exists : exists;
2275
2765
  }, this.negate ? "Expected element not to exist" : "Expected element to exist");
2276
2766
  }
2277
2767
  async toBeVisible() {
2278
2768
  return this.assert(async () => {
2279
- 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;
2280
2771
  return this.negate ? !visible : visible;
2281
2772
  }, this.negate ? "Expected element not to be visible" : "Expected element to be visible");
2282
2773
  }
2283
2774
  async toBeHidden() {
2284
2775
  return this.assert(async () => {
2285
- 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;
2286
2778
  return this.negate ? visible : !visible;
2287
2779
  }, this.negate ? "Expected element not to be hidden" : "Expected element to be hidden");
2288
2780
  }
2289
2781
  async toBeEnabled() {
2290
2782
  return this.assert(async () => {
2291
- const enabled = await this.frame.isEnabled(this.selector, this.options);
2783
+ const enabled = await this.target.isEnabled();
2292
2784
  if (enabled == null) {
2293
2785
  return this.negate ? true : false;
2294
2786
  }
@@ -2297,7 +2789,7 @@ var ElementExpectation = class _ElementExpectation {
2297
2789
  }
2298
2790
  async toBeDisabled() {
2299
2791
  return this.assert(async () => {
2300
- const enabled = await this.frame.isEnabled(this.selector, this.options);
2792
+ const enabled = await this.target.isEnabled();
2301
2793
  if (enabled == null) {
2302
2794
  return this.negate ? true : false;
2303
2795
  }
@@ -2307,7 +2799,7 @@ var ElementExpectation = class _ElementExpectation {
2307
2799
  }
2308
2800
  async toBeChecked() {
2309
2801
  return this.assert(async () => {
2310
- const checked = await this.frame.isChecked(this.selector, this.options);
2802
+ const checked = await this.target.isChecked();
2311
2803
  if (checked == null) {
2312
2804
  return this.negate ? true : false;
2313
2805
  }
@@ -2316,7 +2808,7 @@ var ElementExpectation = class _ElementExpectation {
2316
2808
  }
2317
2809
  async toBeUnchecked() {
2318
2810
  return this.assert(async () => {
2319
- const checked = await this.frame.isChecked(this.selector, this.options);
2811
+ const checked = await this.target.isChecked();
2320
2812
  if (checked == null) {
2321
2813
  return this.negate ? true : false;
2322
2814
  }
@@ -2327,7 +2819,7 @@ var ElementExpectation = class _ElementExpectation {
2327
2819
  async toHaveText(textOrRegex) {
2328
2820
  const expected = textOrRegex;
2329
2821
  return this.assert(async () => {
2330
- const text = await this.frame.text(this.selector, this.options);
2822
+ const text = await this.target.text();
2331
2823
  if (text == null) {
2332
2824
  return this.negate ? true : false;
2333
2825
  }
@@ -2338,7 +2830,7 @@ var ElementExpectation = class _ElementExpectation {
2338
2830
  async toHaveExactText(textOrRegex) {
2339
2831
  const expected = textOrRegex;
2340
2832
  return this.assert(async () => {
2341
- const text = await this.frame.text(this.selector, this.options);
2833
+ const text = await this.target.text();
2342
2834
  if (text == null) {
2343
2835
  return this.negate ? true : false;
2344
2836
  }
@@ -2349,7 +2841,7 @@ var ElementExpectation = class _ElementExpectation {
2349
2841
  async toContainText(textOrRegex) {
2350
2842
  const expected = textOrRegex;
2351
2843
  return this.assert(async () => {
2352
- const text = await this.frame.text(this.selector, this.options);
2844
+ const text = await this.target.text();
2353
2845
  if (text == null) {
2354
2846
  return this.negate ? true : false;
2355
2847
  }
@@ -2360,7 +2852,7 @@ var ElementExpectation = class _ElementExpectation {
2360
2852
  async toHaveValue(valueOrRegex) {
2361
2853
  const expected = valueOrRegex;
2362
2854
  return this.assert(async () => {
2363
- const value = await this.frame.value(this.selector, this.options);
2855
+ const value = await this.target.value();
2364
2856
  if (value == null) {
2365
2857
  return this.negate ? true : false;
2366
2858
  }
@@ -2371,7 +2863,7 @@ var ElementExpectation = class _ElementExpectation {
2371
2863
  async toHaveAttribute(name, valueOrRegex) {
2372
2864
  const expected = valueOrRegex;
2373
2865
  return this.assert(async () => {
2374
- const value = await this.frame.attribute(this.selector, name, this.options);
2866
+ const value = await this.target.attribute(name);
2375
2867
  if (expected === void 0) {
2376
2868
  const exists = value != null;
2377
2869
  return this.negate ? !exists : exists;
@@ -2391,7 +2883,7 @@ var ElementExpectation = class _ElementExpectation {
2391
2883
  }
2392
2884
  async toHaveCount(expected) {
2393
2885
  return this.assert(async () => {
2394
- const count = await this.frame.count(this.selector, this.options);
2886
+ const count = await this.target.count();
2395
2887
  const matches = count === expected;
2396
2888
  return this.negate ? !matches : matches;
2397
2889
  }, this.negate ? "Expected element count not to match" : "Expected element count to match", { expected });
@@ -2399,7 +2891,7 @@ var ElementExpectation = class _ElementExpectation {
2399
2891
  async toHaveClass(nameOrRegex) {
2400
2892
  const expected = nameOrRegex;
2401
2893
  return this.assert(async () => {
2402
- const classes = await this.frame.classes(this.selector, this.options);
2894
+ const classes = await this.target.classes();
2403
2895
  if (classes == null) {
2404
2896
  return this.negate ? true : false;
2405
2897
  }
@@ -2409,7 +2901,7 @@ var ElementExpectation = class _ElementExpectation {
2409
2901
  }
2410
2902
  async toHaveClasses(expected) {
2411
2903
  return this.assert(async () => {
2412
- const classes = await this.frame.classes(this.selector, this.options);
2904
+ const classes = await this.target.classes();
2413
2905
  if (classes == null) {
2414
2906
  return this.negate ? true : false;
2415
2907
  }
@@ -2420,7 +2912,7 @@ var ElementExpectation = class _ElementExpectation {
2420
2912
  async toHaveCss(property, valueOrRegex) {
2421
2913
  const expected = valueOrRegex;
2422
2914
  return this.assert(async () => {
2423
- const value = await this.frame.css(this.selector, property, this.options);
2915
+ const value = await this.target.css(property);
2424
2916
  if (value == null) {
2425
2917
  return this.negate ? true : false;
2426
2918
  }
@@ -2431,7 +2923,7 @@ var ElementExpectation = class _ElementExpectation {
2431
2923
  }
2432
2924
  async toHaveFocus() {
2433
2925
  return this.assert(async () => {
2434
- const focused = await this.frame.hasFocus(this.selector, this.options);
2926
+ const focused = await this.target.hasFocus();
2435
2927
  if (focused == null) {
2436
2928
  return this.negate ? true : false;
2437
2929
  }
@@ -2440,7 +2932,7 @@ var ElementExpectation = class _ElementExpectation {
2440
2932
  }
2441
2933
  async toBeInViewport(options = {}) {
2442
2934
  return this.assert(async () => {
2443
- const inViewport = await this.frame.isInViewport(this.selector, this.options, Boolean(options.fully));
2935
+ const inViewport = await this.target.isInViewport(Boolean(options.fully));
2444
2936
  if (inViewport == null) {
2445
2937
  return this.negate ? true : false;
2446
2938
  }
@@ -2449,7 +2941,7 @@ var ElementExpectation = class _ElementExpectation {
2449
2941
  }
2450
2942
  async toBeEditable() {
2451
2943
  return this.assert(async () => {
2452
- const editable = await this.frame.isEditable(this.selector, this.options);
2944
+ const editable = await this.target.isEditable();
2453
2945
  if (editable == null) {
2454
2946
  return this.negate ? true : false;
2455
2947
  }
@@ -2459,7 +2951,7 @@ var ElementExpectation = class _ElementExpectation {
2459
2951
  async assert(predicate, message, details = {}) {
2460
2952
  const timeoutMs = this.options.timeoutMs ?? 3e4;
2461
2953
  const start = Date.now();
2462
- 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 });
2463
2955
  let lastState;
2464
2956
  try {
2465
2957
  await waitFor(async () => {
@@ -2469,13 +2961,51 @@ var ElementExpectation = class _ElementExpectation {
2469
2961
  }, { timeoutMs, description: message });
2470
2962
  } catch {
2471
2963
  const duration2 = Date.now() - start;
2472
- this.events.emit("assertion:end", { name: message, selector: this.selector, frameId: this.frame.id, durationMs: duration2, status: "failed" });
2473
- 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 } });
2474
2966
  }
2475
2967
  const duration = Date.now() - start;
2476
- 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" });
2477
2969
  }
2478
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
+ }
2479
3009
  var ExpectFrame = class {
2480
3010
  frame;
2481
3011
  events;
@@ -2484,25 +3014,31 @@ var ExpectFrame = class {
2484
3014
  this.events = events;
2485
3015
  }
2486
3016
  element(selector, options = {}) {
2487
- return new ElementExpectation(this.frame, selector, options, false, this.events);
3017
+ return new ElementExpectation(frameTarget(this.frame, selector, options), options, false, this.events);
2488
3018
  }
2489
3019
  };
2490
- function expect(page) {
3020
+ function expect(target) {
3021
+ if (target instanceof Locator) {
3022
+ return new ElementExpectation(locatorTarget(target), {}, false, target.getEvents());
3023
+ }
2491
3024
  return {
2492
- 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()),
2493
3026
  frame: (options) => {
2494
- const frame = page.frame(options);
3027
+ const frame = target.frame(options);
2495
3028
  if (!frame) {
2496
3029
  throw new AssertionError("Frame not found", { selector: JSON.stringify(options) });
2497
3030
  }
2498
- return new ExpectFrame(frame, page.getEvents());
3031
+ return new ExpectFrame(frame, target.getEvents());
2499
3032
  }
2500
3033
  };
2501
3034
  }
2502
- Page.prototype.expect = function(selector, options) {
3035
+ Page.prototype.expect = function(target, options) {
2503
3036
  const builder = expect(this);
2504
- if (selector) {
2505
- return builder.element(selector, options);
3037
+ if (target instanceof Locator) {
3038
+ return expect(target);
3039
+ }
3040
+ if (target) {
3041
+ return builder.element(target, options);
2506
3042
  }
2507
3043
  return builder;
2508
3044
  };
@@ -2588,36 +3124,28 @@ function printHelp() {
2588
3124
  console.log(`cdpwright (cpw) \u2014 Chromium automation CLI
2589
3125
 
2590
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
2591
3131
  download [options] Download pinned Chromium snapshot
2592
3132
  install Alias for download
2593
- open <url> Launch browser session and navigate to URL
2594
- close Close the running browser session
2595
- screenshot [url] -o f Take a screenshot (PNG/JPEG)
2596
- html [url] -o file.html Save the page HTML source
2597
- pdf [url] -o file.pdf Generate PDF of page (headless only)
2598
- eval [url] <script> Run script in page, print result as JSON
2599
3133
  version Print cdpwright and Chromium versions
2600
3134
 
2601
- Session:
2602
- 'open' starts a browser and saves a session. Other commands auto-connect
2603
- to the running session when no URL is given. 'close' shuts it down.
2604
-
2605
- Download options:
2606
- --latest Download the latest Chromium revision
2607
- --mirror <url> Custom mirror base URL
2608
- --url <url> Exact zip URL override
2609
-
2610
- Common options:
3135
+ Options:
2611
3136
  --headless Run in headless mode (default for screenshot, pdf, eval)
2612
- --headed Run in headed mode (default for open)
2613
-
2614
- Screenshot options:
2615
- -o, --output <file> Output file path (required)
2616
- --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)
2617
3143
 
2618
- PDF options:
2619
- -o, --output <file> Output file path (required)
2620
- 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"`);
2621
3149
  }
2622
3150
  function hasFlag(args, flag) {
2623
3151
  return args.includes(flag);