@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/README.md +25 -7
- package/dist/assert/expect.d.ts +1 -1
- package/dist/assert/expect.js +1 -1
- package/dist/{chunk-M5G6EMAS.js → chunk-EIAXMGD5.js} +639 -69
- package/dist/chunk-EIAXMGD5.js.map +1 -0
- package/dist/cli.js +964 -120
- package/dist/cli.js.map +1 -1
- package/dist/{expect-BY8vnFfi.d.ts → expect-Cd5SNFuF.d.ts} +113 -7
- package/dist/index.d.ts +25 -4
- package/dist/index.js +53 -4
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-M5G6EMAS.js.map +0 -1
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
|
-
|
|
423
|
-
|
|
424
|
-
constructor(frame, selector, options = {}) {
|
|
422
|
+
query;
|
|
423
|
+
constructor(frame, query) {
|
|
425
424
|
this.frame = frame;
|
|
426
|
-
this.
|
|
427
|
-
|
|
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.
|
|
444
|
+
return this.frame.clickLocator(this.query, { ...this.queryTimeoutOptions(), ...options });
|
|
431
445
|
}
|
|
432
446
|
async dblclick(options = {}) {
|
|
433
|
-
return this.frame.
|
|
447
|
+
return this.frame.dblclickLocator(this.query, { ...this.queryTimeoutOptions(), ...options });
|
|
434
448
|
}
|
|
435
449
|
async type(text, options = {}) {
|
|
436
|
-
return this.frame.
|
|
450
|
+
return this.frame.typeLocator(this.query, text, { ...this.queryTimeoutOptions(), ...options });
|
|
437
451
|
}
|
|
438
452
|
async exists() {
|
|
439
|
-
return this.frame.
|
|
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.
|
|
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
|
|
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
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2201
|
-
selector;
|
|
2748
|
+
target;
|
|
2202
2749
|
options;
|
|
2203
2750
|
negate;
|
|
2204
2751
|
events;
|
|
2205
|
-
constructor(
|
|
2206
|
-
this.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
2417
|
-
throw new AssertionError(message, { selector: this.
|
|
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.
|
|
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(
|
|
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(
|
|
3025
|
+
element: (selector, options = {}) => new ElementExpectation(frameTarget(target.mainFrame(), selector, options), options, false, target.getEvents()),
|
|
2437
3026
|
frame: (options) => {
|
|
2438
|
-
const frame =
|
|
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,
|
|
3031
|
+
return new ExpectFrame(frame, target.getEvents());
|
|
2443
3032
|
}
|
|
2444
3033
|
};
|
|
2445
3034
|
}
|
|
2446
|
-
Page.prototype.expect = function(
|
|
3035
|
+
Page.prototype.expect = function(target, options) {
|
|
2447
3036
|
const builder = expect(this);
|
|
2448
|
-
if (
|
|
2449
|
-
return
|
|
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
|
-
|
|
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
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
--
|
|
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
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
3291
|
+
const pos = positionalArgs(rest);
|
|
3292
|
+
const url = pos[0];
|
|
2585
3293
|
const output = flagValue(rest, "-o") || flagValue(rest, "--output");
|
|
2586
|
-
if (!
|
|
2587
|
-
console.error("Usage: cpw screenshot
|
|
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
|
-
|
|
2594
|
-
|
|
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
|
|
3343
|
+
const pos = positionalArgs(rest);
|
|
3344
|
+
const url = pos[0];
|
|
2600
3345
|
const output = flagValue(rest, "-o") || flagValue(rest, "--output");
|
|
2601
|
-
if (!
|
|
2602
|
-
console.error("Usage: cpw pdf
|
|
3346
|
+
if (!output) {
|
|
3347
|
+
console.error("Usage: cpw pdf [url] -o <file>");
|
|
2603
3348
|
process.exit(1);
|
|
2604
3349
|
}
|
|
2605
|
-
|
|
2606
|
-
await page.
|
|
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
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
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":
|