@toolstackhq/cdpwright 1.0.0 → 1.2.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 +34 -9
- package/dist/assert/expect.d.ts +1 -1
- package/dist/assert/expect.js +1 -1
- package/dist/{chunk-6BPF3IEU.js → chunk-PDUS6BDZ.js} +64 -3
- package/dist/chunk-PDUS6BDZ.js.map +1 -0
- package/dist/cli.js +856 -19
- package/dist/cli.js.map +1 -1
- package/dist/{expect-CY70zJc0.d.ts → expect-CLal_AQd.d.ts} +18 -0
- package/dist/index.d.ts +25 -4
- package/dist/index.js +73 -10
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-6BPF3IEU.js.map +0 -1
package/dist/cli.js
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
+
// src/cli.ts
|
|
4
|
+
import fs5 from "fs";
|
|
5
|
+
import path5 from "path";
|
|
6
|
+
import { fileURLToPath } from "url";
|
|
7
|
+
|
|
3
8
|
// src/browser/ChromiumManager.ts
|
|
4
9
|
import fs4 from "fs";
|
|
5
10
|
import path4 from "path";
|
|
6
11
|
import os2 from "os";
|
|
7
|
-
import
|
|
12
|
+
import http2 from "http";
|
|
8
13
|
import { spawn as spawn2 } from "child_process";
|
|
9
14
|
|
|
10
15
|
// src/logging/Logger.ts
|
|
@@ -1318,6 +1323,11 @@ function ensureAllowedUrl(url, options = {}) {
|
|
|
1318
1323
|
}
|
|
1319
1324
|
|
|
1320
1325
|
// src/core/Page.ts
|
|
1326
|
+
function assertPdfBuffer(buffer) {
|
|
1327
|
+
if (buffer.length < 5 || buffer.subarray(0, 5).toString("utf-8") !== "%PDF-") {
|
|
1328
|
+
throw new Error("PDF generation failed: Chromium did not return a valid PDF");
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1321
1331
|
var Page = class {
|
|
1322
1332
|
session;
|
|
1323
1333
|
logger;
|
|
@@ -1437,13 +1447,24 @@ var Page = class {
|
|
|
1437
1447
|
async findLocators(options = {}) {
|
|
1438
1448
|
return this.mainFrame().findLocators(options);
|
|
1439
1449
|
}
|
|
1450
|
+
async content() {
|
|
1451
|
+
await this.waitForLoad();
|
|
1452
|
+
return this.mainFrame().evaluate(() => {
|
|
1453
|
+
const doctype = document.doctype;
|
|
1454
|
+
const doctypeText = doctype ? `<!DOCTYPE ${doctype.name}${doctype.publicId ? ` PUBLIC "${doctype.publicId}"` : ""}${doctype.systemId ? ` "${doctype.systemId}"` : ""}>` : "<!doctype html>";
|
|
1455
|
+
return `${doctypeText}
|
|
1456
|
+
${document.documentElement.outerHTML}`;
|
|
1457
|
+
});
|
|
1458
|
+
}
|
|
1440
1459
|
async screenshot(options = {}) {
|
|
1441
1460
|
const start = Date.now();
|
|
1442
1461
|
this.events.emit("action:start", { name: "screenshot", frameId: this.mainFrameId });
|
|
1462
|
+
await this.waitForLoad();
|
|
1443
1463
|
const result = await this.session.send("Page.captureScreenshot", {
|
|
1444
1464
|
format: options.format ?? "png",
|
|
1445
1465
|
quality: options.quality,
|
|
1446
|
-
fromSurface: true
|
|
1466
|
+
fromSurface: true,
|
|
1467
|
+
captureBeyondViewport: options.fullPage ?? false
|
|
1447
1468
|
});
|
|
1448
1469
|
const buffer = Buffer.from(result.data, "base64");
|
|
1449
1470
|
if (options.path) {
|
|
@@ -1457,21 +1478,66 @@ var Page = class {
|
|
|
1457
1478
|
async screenshotBase64(options = {}) {
|
|
1458
1479
|
const start = Date.now();
|
|
1459
1480
|
this.events.emit("action:start", { name: "screenshotBase64", frameId: this.mainFrameId });
|
|
1481
|
+
await this.waitForLoad();
|
|
1460
1482
|
const result = await this.session.send("Page.captureScreenshot", {
|
|
1461
1483
|
format: options.format ?? "png",
|
|
1462
1484
|
quality: options.quality,
|
|
1463
|
-
fromSurface: true
|
|
1485
|
+
fromSurface: true,
|
|
1486
|
+
captureBeyondViewport: options.fullPage ?? false
|
|
1464
1487
|
});
|
|
1465
1488
|
const duration = Date.now() - start;
|
|
1466
1489
|
this.events.emit("action:end", { name: "screenshotBase64", frameId: this.mainFrameId, durationMs: duration });
|
|
1467
1490
|
return result.data;
|
|
1468
1491
|
}
|
|
1492
|
+
async pdf(options = {}) {
|
|
1493
|
+
const start = Date.now();
|
|
1494
|
+
this.events.emit("action:start", { name: "pdf", frameId: this.mainFrameId });
|
|
1495
|
+
await this.waitForLoad();
|
|
1496
|
+
await this.session.send("Emulation.setEmulatedMedia", { media: "screen" });
|
|
1497
|
+
let buffer;
|
|
1498
|
+
try {
|
|
1499
|
+
const result = await this.session.send("Page.printToPDF", {
|
|
1500
|
+
landscape: options.landscape ?? false,
|
|
1501
|
+
printBackground: options.printBackground ?? true,
|
|
1502
|
+
scale: options.scale,
|
|
1503
|
+
paperWidth: options.paperWidth,
|
|
1504
|
+
paperHeight: options.paperHeight,
|
|
1505
|
+
marginTop: options.marginTop,
|
|
1506
|
+
marginBottom: options.marginBottom,
|
|
1507
|
+
marginLeft: options.marginLeft,
|
|
1508
|
+
marginRight: options.marginRight,
|
|
1509
|
+
pageRanges: options.pageRanges,
|
|
1510
|
+
preferCSSPageSize: options.preferCSSPageSize
|
|
1511
|
+
});
|
|
1512
|
+
buffer = Buffer.from(result.data, "base64");
|
|
1513
|
+
assertPdfBuffer(buffer);
|
|
1514
|
+
if (options.path) {
|
|
1515
|
+
const resolved = path2.resolve(options.path);
|
|
1516
|
+
fs2.writeFileSync(resolved, buffer);
|
|
1517
|
+
}
|
|
1518
|
+
} finally {
|
|
1519
|
+
try {
|
|
1520
|
+
await this.session.send("Emulation.setEmulatedMedia", { media: "" });
|
|
1521
|
+
} catch {
|
|
1522
|
+
}
|
|
1523
|
+
const duration = Date.now() - start;
|
|
1524
|
+
this.events.emit("action:end", { name: "pdf", frameId: this.mainFrameId, durationMs: duration });
|
|
1525
|
+
}
|
|
1526
|
+
return buffer;
|
|
1527
|
+
}
|
|
1469
1528
|
getEvents() {
|
|
1470
1529
|
return this.events;
|
|
1471
1530
|
}
|
|
1472
1531
|
getDefaultTimeout() {
|
|
1473
1532
|
return this.defaultTimeout;
|
|
1474
1533
|
}
|
|
1534
|
+
async waitForLoad(timeoutMs = this.defaultTimeout) {
|
|
1535
|
+
const frame = this.mainFrame();
|
|
1536
|
+
await waitFor(async () => {
|
|
1537
|
+
const readyState = await frame.evaluate("document.readyState");
|
|
1538
|
+
return readyState === "complete";
|
|
1539
|
+
}, { timeoutMs, description: "page load" });
|
|
1540
|
+
}
|
|
1475
1541
|
buildFrameTree(tree) {
|
|
1476
1542
|
const frame = this.ensureFrame(tree.frame.id);
|
|
1477
1543
|
frame.setMeta({ name: tree.frame.name, url: tree.frame.url, parentId: tree.frame.parentId });
|
|
@@ -1573,12 +1639,16 @@ var Browser = class {
|
|
|
1573
1639
|
events;
|
|
1574
1640
|
cleanupTasks;
|
|
1575
1641
|
contexts = /* @__PURE__ */ new Set();
|
|
1576
|
-
|
|
1642
|
+
wsEndpoint;
|
|
1643
|
+
pid;
|
|
1644
|
+
constructor(connection, child, logger, events, cleanupTasks = [], wsEndpoint = "") {
|
|
1577
1645
|
this.connection = connection;
|
|
1578
1646
|
this.process = child;
|
|
1579
1647
|
this.logger = logger;
|
|
1580
1648
|
this.events = events;
|
|
1581
1649
|
this.cleanupTasks = cleanupTasks;
|
|
1650
|
+
this.wsEndpoint = wsEndpoint;
|
|
1651
|
+
this.pid = child?.pid ?? 0;
|
|
1582
1652
|
}
|
|
1583
1653
|
on(event, handler) {
|
|
1584
1654
|
this.events.on(event, handler);
|
|
@@ -1600,6 +1670,23 @@ var Browser = class {
|
|
|
1600
1670
|
await page.initialize();
|
|
1601
1671
|
return page;
|
|
1602
1672
|
}
|
|
1673
|
+
/** Attach to an existing page by target ID. */
|
|
1674
|
+
async attachPage(targetId) {
|
|
1675
|
+
const { sessionId } = await this.connection.send("Target.attachToTarget", { targetId, flatten: true });
|
|
1676
|
+
const session = this.connection.createSession(sessionId);
|
|
1677
|
+
const page = new Page(session, this.logger, this.events);
|
|
1678
|
+
await page.initialize();
|
|
1679
|
+
return page;
|
|
1680
|
+
}
|
|
1681
|
+
/** List open page targets. */
|
|
1682
|
+
async pages() {
|
|
1683
|
+
const result = await this.connection.send("Target.getTargets");
|
|
1684
|
+
return result.targetInfos.filter((t) => t.type === "page").map((t) => ({ targetId: t.targetId, url: t.url, title: t.title }));
|
|
1685
|
+
}
|
|
1686
|
+
/** Disconnect without killing the browser process. */
|
|
1687
|
+
async disconnect() {
|
|
1688
|
+
await this.connection.close();
|
|
1689
|
+
}
|
|
1603
1690
|
async disposeContext(contextId) {
|
|
1604
1691
|
if (!contextId) return;
|
|
1605
1692
|
try {
|
|
@@ -1619,7 +1706,7 @@ var Browser = class {
|
|
|
1619
1706
|
} catch {
|
|
1620
1707
|
}
|
|
1621
1708
|
await this.connection.close();
|
|
1622
|
-
if (!this.process.killed) {
|
|
1709
|
+
if (this.process && !this.process.killed) {
|
|
1623
1710
|
this.process.kill();
|
|
1624
1711
|
}
|
|
1625
1712
|
for (const task of this.cleanupTasks) {
|
|
@@ -1635,6 +1722,7 @@ var Browser = class {
|
|
|
1635
1722
|
import fs3 from "fs";
|
|
1636
1723
|
import path3 from "path";
|
|
1637
1724
|
import os from "os";
|
|
1725
|
+
import http from "http";
|
|
1638
1726
|
import https from "https";
|
|
1639
1727
|
import { spawn } from "child_process";
|
|
1640
1728
|
import yauzl from "yauzl";
|
|
@@ -1672,11 +1760,22 @@ function chromiumExecutableRelativePath(platform) {
|
|
|
1672
1760
|
if (platform === "mac") return path3.join("chrome-mac", "Chromium.app", "Contents", "MacOS", "Chromium");
|
|
1673
1761
|
return path3.join("chrome-win", "chrome.exe");
|
|
1674
1762
|
}
|
|
1763
|
+
function httpGet(url) {
|
|
1764
|
+
return url.startsWith("http://") ? http : https;
|
|
1765
|
+
}
|
|
1766
|
+
function resolveSnapshotBase() {
|
|
1767
|
+
const mirror = process.env.CDPWRIGHT_DOWNLOAD_MIRROR;
|
|
1768
|
+
if (mirror && mirror.trim()) {
|
|
1769
|
+
return mirror.trim().replace(/\/+$/, "");
|
|
1770
|
+
}
|
|
1771
|
+
return SNAPSHOT_BASE;
|
|
1772
|
+
}
|
|
1675
1773
|
async function fetchLatestRevision(platform) {
|
|
1774
|
+
const base = resolveSnapshotBase();
|
|
1676
1775
|
const folder = platformFolder(platform);
|
|
1677
|
-
const url = `${
|
|
1776
|
+
const url = `${base}/${folder}/LAST_CHANGE`;
|
|
1678
1777
|
return new Promise((resolve, reject) => {
|
|
1679
|
-
|
|
1778
|
+
httpGet(url).get(url, (res) => {
|
|
1680
1779
|
if (res.statusCode && res.statusCode >= 400) {
|
|
1681
1780
|
reject(new Error(`Failed to fetch LAST_CHANGE: ${res.statusCode}`));
|
|
1682
1781
|
return;
|
|
@@ -1701,7 +1800,9 @@ async function ensureDownloaded(options) {
|
|
|
1701
1800
|
fs3.mkdirSync(revisionDir, { recursive: true });
|
|
1702
1801
|
const folder = platformFolder(platform);
|
|
1703
1802
|
const zipName = platform === "win" ? "chrome-win.zip" : platform === "mac" ? "chrome-mac.zip" : "chrome-linux.zip";
|
|
1704
|
-
const
|
|
1803
|
+
const explicitUrl = process.env.CDPWRIGHT_DOWNLOAD_URL?.trim();
|
|
1804
|
+
const base = resolveSnapshotBase();
|
|
1805
|
+
const downloadUrl = explicitUrl || `${base}/${folder}/${revision}/${zipName}`;
|
|
1705
1806
|
const tempZipPath = path3.join(os.tmpdir(), `cdpwright-${platform}-${revision}.zip`);
|
|
1706
1807
|
logger.info("Downloading Chromium snapshot", downloadUrl);
|
|
1707
1808
|
await downloadFile(downloadUrl, tempZipPath, logger);
|
|
@@ -1718,7 +1819,7 @@ async function ensureDownloaded(options) {
|
|
|
1718
1819
|
function downloadFile(url, dest, logger) {
|
|
1719
1820
|
return new Promise((resolve, reject) => {
|
|
1720
1821
|
const file = fs3.createWriteStream(dest);
|
|
1721
|
-
|
|
1822
|
+
httpGet(url).get(url, (res) => {
|
|
1722
1823
|
if (res.statusCode && res.statusCode >= 400) {
|
|
1723
1824
|
reject(new Error(`Failed to download: ${res.statusCode}`));
|
|
1724
1825
|
return;
|
|
@@ -1947,6 +2048,7 @@ var ChromiumManager = class {
|
|
|
1947
2048
|
}
|
|
1948
2049
|
if (options.maximize) {
|
|
1949
2050
|
args.push("--start-maximized");
|
|
2051
|
+
args.push("--window-size=1920,1080");
|
|
1950
2052
|
}
|
|
1951
2053
|
if (options.args) {
|
|
1952
2054
|
args.push(...options.args);
|
|
@@ -1975,7 +2077,7 @@ var ChromiumManager = class {
|
|
|
1975
2077
|
logger.info(`Assertion ${payload.name}`, ...args2);
|
|
1976
2078
|
});
|
|
1977
2079
|
}
|
|
1978
|
-
const browser = new Browser(connection, child, logger, events, cleanupTasks);
|
|
2080
|
+
const browser = new Browser(connection, child, logger, events, cleanupTasks, wsEndpoint);
|
|
1979
2081
|
return browser;
|
|
1980
2082
|
}
|
|
1981
2083
|
resolveCacheRoot(platform) {
|
|
@@ -2112,7 +2214,7 @@ function toHttpVersionUrl(wsUrl) {
|
|
|
2112
2214
|
}
|
|
2113
2215
|
function fetchWebSocketDebuggerUrl(versionUrl) {
|
|
2114
2216
|
return new Promise((resolve, reject) => {
|
|
2115
|
-
|
|
2217
|
+
http2.get(versionUrl, (res) => {
|
|
2116
2218
|
if (res.statusCode && res.statusCode >= 400) {
|
|
2117
2219
|
reject(new Error(`Failed to fetch /json/version: ${res.statusCode}`));
|
|
2118
2220
|
return;
|
|
@@ -2135,9 +2237,727 @@ function fetchWebSocketDebuggerUrl(versionUrl) {
|
|
|
2135
2237
|
});
|
|
2136
2238
|
}
|
|
2137
2239
|
|
|
2240
|
+
// src/assert/AssertionError.ts
|
|
2241
|
+
var AssertionError = class extends Error {
|
|
2242
|
+
selector;
|
|
2243
|
+
timeoutMs;
|
|
2244
|
+
lastState;
|
|
2245
|
+
constructor(message, options = {}) {
|
|
2246
|
+
super(message);
|
|
2247
|
+
this.name = "AssertionError";
|
|
2248
|
+
this.selector = options.selector;
|
|
2249
|
+
this.timeoutMs = options.timeoutMs;
|
|
2250
|
+
this.lastState = options.lastState;
|
|
2251
|
+
}
|
|
2252
|
+
};
|
|
2253
|
+
|
|
2254
|
+
// src/assert/expect.ts
|
|
2255
|
+
var ElementExpectation = class _ElementExpectation {
|
|
2256
|
+
frame;
|
|
2257
|
+
selector;
|
|
2258
|
+
options;
|
|
2259
|
+
negate;
|
|
2260
|
+
events;
|
|
2261
|
+
constructor(frame, selector, options, negate, events) {
|
|
2262
|
+
this.frame = frame;
|
|
2263
|
+
this.selector = selector;
|
|
2264
|
+
this.options = options;
|
|
2265
|
+
this.negate = negate;
|
|
2266
|
+
this.events = events;
|
|
2267
|
+
}
|
|
2268
|
+
get not() {
|
|
2269
|
+
return new _ElementExpectation(this.frame, this.selector, this.options, !this.negate, this.events);
|
|
2270
|
+
}
|
|
2271
|
+
async toExist() {
|
|
2272
|
+
return this.assert(async () => {
|
|
2273
|
+
const exists = await this.frame.exists(this.selector, this.options);
|
|
2274
|
+
return this.negate ? !exists : exists;
|
|
2275
|
+
}, this.negate ? "Expected element not to exist" : "Expected element to exist");
|
|
2276
|
+
}
|
|
2277
|
+
async toBeVisible() {
|
|
2278
|
+
return this.assert(async () => {
|
|
2279
|
+
const visible = await this.frame.isVisible(this.selector, this.options);
|
|
2280
|
+
return this.negate ? !visible : visible;
|
|
2281
|
+
}, this.negate ? "Expected element not to be visible" : "Expected element to be visible");
|
|
2282
|
+
}
|
|
2283
|
+
async toBeHidden() {
|
|
2284
|
+
return this.assert(async () => {
|
|
2285
|
+
const visible = await this.frame.isVisible(this.selector, this.options);
|
|
2286
|
+
return this.negate ? visible : !visible;
|
|
2287
|
+
}, this.negate ? "Expected element not to be hidden" : "Expected element to be hidden");
|
|
2288
|
+
}
|
|
2289
|
+
async toBeEnabled() {
|
|
2290
|
+
return this.assert(async () => {
|
|
2291
|
+
const enabled = await this.frame.isEnabled(this.selector, this.options);
|
|
2292
|
+
if (enabled == null) {
|
|
2293
|
+
return this.negate ? true : false;
|
|
2294
|
+
}
|
|
2295
|
+
return this.negate ? !enabled : enabled;
|
|
2296
|
+
}, this.negate ? "Expected element not to be enabled" : "Expected element to be enabled");
|
|
2297
|
+
}
|
|
2298
|
+
async toBeDisabled() {
|
|
2299
|
+
return this.assert(async () => {
|
|
2300
|
+
const enabled = await this.frame.isEnabled(this.selector, this.options);
|
|
2301
|
+
if (enabled == null) {
|
|
2302
|
+
return this.negate ? true : false;
|
|
2303
|
+
}
|
|
2304
|
+
const disabled = !enabled;
|
|
2305
|
+
return this.negate ? !disabled : disabled;
|
|
2306
|
+
}, this.negate ? "Expected element not to be disabled" : "Expected element to be disabled");
|
|
2307
|
+
}
|
|
2308
|
+
async toBeChecked() {
|
|
2309
|
+
return this.assert(async () => {
|
|
2310
|
+
const checked = await this.frame.isChecked(this.selector, this.options);
|
|
2311
|
+
if (checked == null) {
|
|
2312
|
+
return this.negate ? true : false;
|
|
2313
|
+
}
|
|
2314
|
+
return this.negate ? !checked : checked;
|
|
2315
|
+
}, this.negate ? "Expected element not to be checked" : "Expected element to be checked");
|
|
2316
|
+
}
|
|
2317
|
+
async toBeUnchecked() {
|
|
2318
|
+
return this.assert(async () => {
|
|
2319
|
+
const checked = await this.frame.isChecked(this.selector, this.options);
|
|
2320
|
+
if (checked == null) {
|
|
2321
|
+
return this.negate ? true : false;
|
|
2322
|
+
}
|
|
2323
|
+
const unchecked = !checked;
|
|
2324
|
+
return this.negate ? !unchecked : unchecked;
|
|
2325
|
+
}, this.negate ? "Expected element not to be unchecked" : "Expected element to be unchecked");
|
|
2326
|
+
}
|
|
2327
|
+
async toHaveText(textOrRegex) {
|
|
2328
|
+
const expected = textOrRegex;
|
|
2329
|
+
return this.assert(async () => {
|
|
2330
|
+
const text = await this.frame.text(this.selector, this.options);
|
|
2331
|
+
if (text == null) {
|
|
2332
|
+
return this.negate ? true : false;
|
|
2333
|
+
}
|
|
2334
|
+
const matches = expected instanceof RegExp ? new RegExp(expected.source, expected.flags.replace("g", "")).test(text) : text.includes(expected);
|
|
2335
|
+
return this.negate ? !matches : matches;
|
|
2336
|
+
}, this.negate ? "Expected element text not to match" : "Expected element text to match", { expected });
|
|
2337
|
+
}
|
|
2338
|
+
async toHaveExactText(textOrRegex) {
|
|
2339
|
+
const expected = textOrRegex;
|
|
2340
|
+
return this.assert(async () => {
|
|
2341
|
+
const text = await this.frame.text(this.selector, this.options);
|
|
2342
|
+
if (text == null) {
|
|
2343
|
+
return this.negate ? true : false;
|
|
2344
|
+
}
|
|
2345
|
+
const matches = expected instanceof RegExp ? new RegExp(expected.source, expected.flags.replace("g", "")).test(text) : text === expected;
|
|
2346
|
+
return this.negate ? !matches : matches;
|
|
2347
|
+
}, this.negate ? "Expected element text not to match exactly" : "Expected element text to match exactly", { expected });
|
|
2348
|
+
}
|
|
2349
|
+
async toContainText(textOrRegex) {
|
|
2350
|
+
const expected = textOrRegex;
|
|
2351
|
+
return this.assert(async () => {
|
|
2352
|
+
const text = await this.frame.text(this.selector, this.options);
|
|
2353
|
+
if (text == null) {
|
|
2354
|
+
return this.negate ? true : false;
|
|
2355
|
+
}
|
|
2356
|
+
const matches = expected instanceof RegExp ? new RegExp(expected.source, expected.flags.replace("g", "")).test(text) : text.includes(expected);
|
|
2357
|
+
return this.negate ? !matches : matches;
|
|
2358
|
+
}, this.negate ? "Expected element text not to contain" : "Expected element text to contain", { expected });
|
|
2359
|
+
}
|
|
2360
|
+
async toHaveValue(valueOrRegex) {
|
|
2361
|
+
const expected = valueOrRegex;
|
|
2362
|
+
return this.assert(async () => {
|
|
2363
|
+
const value = await this.frame.value(this.selector, this.options);
|
|
2364
|
+
if (value == null) {
|
|
2365
|
+
return this.negate ? true : false;
|
|
2366
|
+
}
|
|
2367
|
+
const matches = expected instanceof RegExp ? new RegExp(expected.source, expected.flags.replace("g", "")).test(value) : value === expected;
|
|
2368
|
+
return this.negate ? !matches : matches;
|
|
2369
|
+
}, this.negate ? "Expected element value not to match" : "Expected element value to match", { expected });
|
|
2370
|
+
}
|
|
2371
|
+
async toHaveAttribute(name, valueOrRegex) {
|
|
2372
|
+
const expected = valueOrRegex;
|
|
2373
|
+
return this.assert(async () => {
|
|
2374
|
+
const value = await this.frame.attribute(this.selector, name, this.options);
|
|
2375
|
+
if (expected === void 0) {
|
|
2376
|
+
const exists = value != null;
|
|
2377
|
+
return this.negate ? !exists : exists;
|
|
2378
|
+
}
|
|
2379
|
+
if (value == null) {
|
|
2380
|
+
return this.negate ? true : false;
|
|
2381
|
+
}
|
|
2382
|
+
const matches = expected instanceof RegExp ? new RegExp(expected.source, expected.flags.replace("g", "")).test(value) : value === expected;
|
|
2383
|
+
return this.negate ? !matches : matches;
|
|
2384
|
+
}, this.negate ? "Expected element attribute not to match" : "Expected element attribute to match", { expected, name });
|
|
2385
|
+
}
|
|
2386
|
+
async toHaveId(idOrRegex) {
|
|
2387
|
+
return this.toHaveAttribute("id", idOrRegex);
|
|
2388
|
+
}
|
|
2389
|
+
async toHaveName(nameOrRegex) {
|
|
2390
|
+
return this.toHaveAttribute("name", nameOrRegex);
|
|
2391
|
+
}
|
|
2392
|
+
async toHaveCount(expected) {
|
|
2393
|
+
return this.assert(async () => {
|
|
2394
|
+
const count = await this.frame.count(this.selector, this.options);
|
|
2395
|
+
const matches = count === expected;
|
|
2396
|
+
return this.negate ? !matches : matches;
|
|
2397
|
+
}, this.negate ? "Expected element count not to match" : "Expected element count to match", { expected });
|
|
2398
|
+
}
|
|
2399
|
+
async toHaveClass(nameOrRegex) {
|
|
2400
|
+
const expected = nameOrRegex;
|
|
2401
|
+
return this.assert(async () => {
|
|
2402
|
+
const classes = await this.frame.classes(this.selector, this.options);
|
|
2403
|
+
if (classes == null) {
|
|
2404
|
+
return this.negate ? true : false;
|
|
2405
|
+
}
|
|
2406
|
+
const matches = expected instanceof RegExp ? classes.some((value) => new RegExp(expected.source, expected.flags.replace("g", "")).test(value)) : classes.includes(expected);
|
|
2407
|
+
return this.negate ? !matches : matches;
|
|
2408
|
+
}, this.negate ? "Expected element class not to match" : "Expected element class to match", { expected });
|
|
2409
|
+
}
|
|
2410
|
+
async toHaveClasses(expected) {
|
|
2411
|
+
return this.assert(async () => {
|
|
2412
|
+
const classes = await this.frame.classes(this.selector, this.options);
|
|
2413
|
+
if (classes == null) {
|
|
2414
|
+
return this.negate ? true : false;
|
|
2415
|
+
}
|
|
2416
|
+
const matches = expected.every((value) => classes.includes(value));
|
|
2417
|
+
return this.negate ? !matches : matches;
|
|
2418
|
+
}, this.negate ? "Expected element classes not to match" : "Expected element classes to match", { expected });
|
|
2419
|
+
}
|
|
2420
|
+
async toHaveCss(property, valueOrRegex) {
|
|
2421
|
+
const expected = valueOrRegex;
|
|
2422
|
+
return this.assert(async () => {
|
|
2423
|
+
const value = await this.frame.css(this.selector, property, this.options);
|
|
2424
|
+
if (value == null) {
|
|
2425
|
+
return this.negate ? true : false;
|
|
2426
|
+
}
|
|
2427
|
+
const actual = value.trim();
|
|
2428
|
+
const matches = expected instanceof RegExp ? new RegExp(expected.source, expected.flags.replace("g", "")).test(actual) : actual === expected;
|
|
2429
|
+
return this.negate ? !matches : matches;
|
|
2430
|
+
}, this.negate ? "Expected element css not to match" : "Expected element css to match", { expected, property });
|
|
2431
|
+
}
|
|
2432
|
+
async toHaveFocus() {
|
|
2433
|
+
return this.assert(async () => {
|
|
2434
|
+
const focused = await this.frame.hasFocus(this.selector, this.options);
|
|
2435
|
+
if (focused == null) {
|
|
2436
|
+
return this.negate ? true : false;
|
|
2437
|
+
}
|
|
2438
|
+
return this.negate ? !focused : focused;
|
|
2439
|
+
}, this.negate ? "Expected element not to have focus" : "Expected element to have focus");
|
|
2440
|
+
}
|
|
2441
|
+
async toBeInViewport(options = {}) {
|
|
2442
|
+
return this.assert(async () => {
|
|
2443
|
+
const inViewport = await this.frame.isInViewport(this.selector, this.options, Boolean(options.fully));
|
|
2444
|
+
if (inViewport == null) {
|
|
2445
|
+
return this.negate ? true : false;
|
|
2446
|
+
}
|
|
2447
|
+
return this.negate ? !inViewport : inViewport;
|
|
2448
|
+
}, this.negate ? "Expected element not to be in viewport" : "Expected element to be in viewport");
|
|
2449
|
+
}
|
|
2450
|
+
async toBeEditable() {
|
|
2451
|
+
return this.assert(async () => {
|
|
2452
|
+
const editable = await this.frame.isEditable(this.selector, this.options);
|
|
2453
|
+
if (editable == null) {
|
|
2454
|
+
return this.negate ? true : false;
|
|
2455
|
+
}
|
|
2456
|
+
return this.negate ? !editable : editable;
|
|
2457
|
+
}, this.negate ? "Expected element not to be editable" : "Expected element to be editable");
|
|
2458
|
+
}
|
|
2459
|
+
async assert(predicate, message, details = {}) {
|
|
2460
|
+
const timeoutMs = this.options.timeoutMs ?? 3e4;
|
|
2461
|
+
const start = Date.now();
|
|
2462
|
+
this.events.emit("assertion:start", { name: message, selector: this.selector, frameId: this.frame.id });
|
|
2463
|
+
let lastState;
|
|
2464
|
+
try {
|
|
2465
|
+
await waitFor(async () => {
|
|
2466
|
+
const result = await predicate();
|
|
2467
|
+
lastState = result;
|
|
2468
|
+
return result;
|
|
2469
|
+
}, { timeoutMs, description: message });
|
|
2470
|
+
} catch {
|
|
2471
|
+
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 } });
|
|
2474
|
+
}
|
|
2475
|
+
const duration = Date.now() - start;
|
|
2476
|
+
this.events.emit("assertion:end", { name: message, selector: this.selector, frameId: this.frame.id, durationMs: duration, status: "passed" });
|
|
2477
|
+
}
|
|
2478
|
+
};
|
|
2479
|
+
var ExpectFrame = class {
|
|
2480
|
+
frame;
|
|
2481
|
+
events;
|
|
2482
|
+
constructor(frame, events) {
|
|
2483
|
+
this.frame = frame;
|
|
2484
|
+
this.events = events;
|
|
2485
|
+
}
|
|
2486
|
+
element(selector, options = {}) {
|
|
2487
|
+
return new ElementExpectation(this.frame, selector, options, false, this.events);
|
|
2488
|
+
}
|
|
2489
|
+
};
|
|
2490
|
+
function expect(page) {
|
|
2491
|
+
return {
|
|
2492
|
+
element: (selector, options = {}) => new ElementExpectation(page.mainFrame(), selector, options, false, page.getEvents()),
|
|
2493
|
+
frame: (options) => {
|
|
2494
|
+
const frame = page.frame(options);
|
|
2495
|
+
if (!frame) {
|
|
2496
|
+
throw new AssertionError("Frame not found", { selector: JSON.stringify(options) });
|
|
2497
|
+
}
|
|
2498
|
+
return new ExpectFrame(frame, page.getEvents());
|
|
2499
|
+
}
|
|
2500
|
+
};
|
|
2501
|
+
}
|
|
2502
|
+
Page.prototype.expect = function(selector, options) {
|
|
2503
|
+
const builder = expect(this);
|
|
2504
|
+
if (selector) {
|
|
2505
|
+
return builder.element(selector, options);
|
|
2506
|
+
}
|
|
2507
|
+
return builder;
|
|
2508
|
+
};
|
|
2509
|
+
Object.defineProperty(Page.prototype, "find", {
|
|
2510
|
+
get: function() {
|
|
2511
|
+
return {
|
|
2512
|
+
locators: (options) => this.findLocators(options)
|
|
2513
|
+
};
|
|
2514
|
+
}
|
|
2515
|
+
});
|
|
2516
|
+
|
|
2517
|
+
// src/index.ts
|
|
2518
|
+
var automaton = {
|
|
2519
|
+
async launch(options = {}) {
|
|
2520
|
+
const manager = new ChromiumManager(options.logger);
|
|
2521
|
+
return manager.launch(options);
|
|
2522
|
+
},
|
|
2523
|
+
async connect(wsEndpoint, options = {}) {
|
|
2524
|
+
const logger = options.logger ?? new Logger(options.logLevel ?? "warn");
|
|
2525
|
+
const connection = new Connection(wsEndpoint, logger);
|
|
2526
|
+
await connection.waitForOpen();
|
|
2527
|
+
const events = new AutomationEvents();
|
|
2528
|
+
const logEvents = options.logEvents ?? true;
|
|
2529
|
+
const logActions = options.logActions ?? true;
|
|
2530
|
+
const logAssertions = options.logAssertions ?? true;
|
|
2531
|
+
if (logEvents && logActions) {
|
|
2532
|
+
events.on("action:end", (payload) => {
|
|
2533
|
+
const selector = payload.sensitive ? void 0 : payload.selector;
|
|
2534
|
+
const args = [];
|
|
2535
|
+
if (selector) args.push(selector);
|
|
2536
|
+
if (typeof payload.durationMs === "number") args.push(`${payload.durationMs}ms`);
|
|
2537
|
+
logger.info(`Action ${payload.name}`, ...args);
|
|
2538
|
+
});
|
|
2539
|
+
}
|
|
2540
|
+
if (logEvents && logAssertions) {
|
|
2541
|
+
events.on("assertion:end", (payload) => {
|
|
2542
|
+
const args = [];
|
|
2543
|
+
if (payload.selector) args.push(payload.selector);
|
|
2544
|
+
if (typeof payload.durationMs === "number") args.push(`${payload.durationMs}ms`);
|
|
2545
|
+
logger.info(`Assertion ${payload.name}`, ...args);
|
|
2546
|
+
});
|
|
2547
|
+
}
|
|
2548
|
+
return new Browser(connection, null, logger, events, [], wsEndpoint);
|
|
2549
|
+
}
|
|
2550
|
+
};
|
|
2551
|
+
|
|
2138
2552
|
// src/cli.ts
|
|
2553
|
+
function sessionFilePath() {
|
|
2554
|
+
const platform = detectPlatform();
|
|
2555
|
+
const cacheRoot = defaultCacheRoot(platform);
|
|
2556
|
+
return path5.join(cacheRoot, "session.json");
|
|
2557
|
+
}
|
|
2558
|
+
function writeSession(info) {
|
|
2559
|
+
const filePath = sessionFilePath();
|
|
2560
|
+
fs5.mkdirSync(path5.dirname(filePath), { recursive: true });
|
|
2561
|
+
fs5.writeFileSync(filePath, JSON.stringify(info));
|
|
2562
|
+
}
|
|
2563
|
+
function readSession() {
|
|
2564
|
+
const filePath = sessionFilePath();
|
|
2565
|
+
if (!fs5.existsSync(filePath)) return null;
|
|
2566
|
+
try {
|
|
2567
|
+
const data = JSON.parse(fs5.readFileSync(filePath, "utf-8"));
|
|
2568
|
+
if (!data.wsEndpoint || !data.pid) return null;
|
|
2569
|
+
try {
|
|
2570
|
+
process.kill(data.pid, 0);
|
|
2571
|
+
} catch {
|
|
2572
|
+
clearSession();
|
|
2573
|
+
return null;
|
|
2574
|
+
}
|
|
2575
|
+
return data;
|
|
2576
|
+
} catch {
|
|
2577
|
+
return null;
|
|
2578
|
+
}
|
|
2579
|
+
}
|
|
2580
|
+
function clearSession() {
|
|
2581
|
+
const filePath = sessionFilePath();
|
|
2582
|
+
try {
|
|
2583
|
+
fs5.unlinkSync(filePath);
|
|
2584
|
+
} catch {
|
|
2585
|
+
}
|
|
2586
|
+
}
|
|
2139
2587
|
function printHelp() {
|
|
2140
|
-
console.log(
|
|
2588
|
+
console.log(`cdpwright (cpw) \u2014 Chromium automation CLI
|
|
2589
|
+
|
|
2590
|
+
Commands:
|
|
2591
|
+
download [options] Download pinned Chromium snapshot
|
|
2592
|
+
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
|
+
version Print cdpwright and Chromium versions
|
|
2600
|
+
|
|
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:
|
|
2611
|
+
--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
|
|
2617
|
+
|
|
2618
|
+
PDF options:
|
|
2619
|
+
-o, --output <file> Output file path (required)
|
|
2620
|
+
Note: pdf always runs headless (CDP limitation)`);
|
|
2621
|
+
}
|
|
2622
|
+
function hasFlag(args, flag) {
|
|
2623
|
+
return args.includes(flag);
|
|
2624
|
+
}
|
|
2625
|
+
function flagValue(args, flag) {
|
|
2626
|
+
const index = args.indexOf(flag);
|
|
2627
|
+
if (index === -1 || index + 1 >= args.length) return void 0;
|
|
2628
|
+
return args[index + 1];
|
|
2629
|
+
}
|
|
2630
|
+
var VALUE_FLAGS = ["--mirror", "--url", "-o", "--output"];
|
|
2631
|
+
function resolveHeadless(args, defaultValue) {
|
|
2632
|
+
if (hasFlag(args, "--headed")) return false;
|
|
2633
|
+
if (hasFlag(args, "--headless")) return true;
|
|
2634
|
+
return defaultValue;
|
|
2635
|
+
}
|
|
2636
|
+
function positionalArgs(args) {
|
|
2637
|
+
const result = [];
|
|
2638
|
+
for (let i = 0; i < args.length; i++) {
|
|
2639
|
+
if (args[i].startsWith("-")) {
|
|
2640
|
+
if (VALUE_FLAGS.includes(args[i])) i++;
|
|
2641
|
+
continue;
|
|
2642
|
+
}
|
|
2643
|
+
result.push(args[i]);
|
|
2644
|
+
}
|
|
2645
|
+
return result;
|
|
2646
|
+
}
|
|
2647
|
+
function launchArgs() {
|
|
2648
|
+
const args = [];
|
|
2649
|
+
if (process.platform === "linux") {
|
|
2650
|
+
args.push("--no-sandbox", "--no-zygote", "--disable-dev-shm-usage");
|
|
2651
|
+
}
|
|
2652
|
+
return args;
|
|
2653
|
+
}
|
|
2654
|
+
function pngDimensions(buffer) {
|
|
2655
|
+
if (buffer.length < 24) {
|
|
2656
|
+
throw new Error("Invalid PNG screenshot");
|
|
2657
|
+
}
|
|
2658
|
+
if (buffer.readUInt32BE(0) !== 2303741511 || buffer.readUInt32BE(4) !== 218765834) {
|
|
2659
|
+
throw new Error("Invalid PNG screenshot");
|
|
2660
|
+
}
|
|
2661
|
+
return {
|
|
2662
|
+
width: buffer.readUInt32BE(16),
|
|
2663
|
+
height: buffer.readUInt32BE(20)
|
|
2664
|
+
};
|
|
2665
|
+
}
|
|
2666
|
+
async function connectToSession() {
|
|
2667
|
+
const session = readSession();
|
|
2668
|
+
if (!session) {
|
|
2669
|
+
console.error("No running session. Start one with: cpw open <url>");
|
|
2670
|
+
process.exit(1);
|
|
2671
|
+
}
|
|
2672
|
+
const browser = await automaton.connect(session.wsEndpoint);
|
|
2673
|
+
const targets = await browser.pages();
|
|
2674
|
+
if (targets.length === 0) {
|
|
2675
|
+
console.error("Session has no open pages.");
|
|
2676
|
+
await browser.disconnect();
|
|
2677
|
+
process.exit(1);
|
|
2678
|
+
}
|
|
2679
|
+
const page = await browser.attachPage(targets[0].targetId);
|
|
2680
|
+
return { browser, page };
|
|
2681
|
+
}
|
|
2682
|
+
async function withPage(url, options, fn) {
|
|
2683
|
+
const browser = await automaton.launch({
|
|
2684
|
+
headless: options.headless ?? true,
|
|
2685
|
+
maximize: options.maximize ?? false,
|
|
2686
|
+
args: launchArgs(),
|
|
2687
|
+
logLevel: "warn"
|
|
2688
|
+
});
|
|
2689
|
+
try {
|
|
2690
|
+
const page = await browser.newPage();
|
|
2691
|
+
await page.goto(url, { waitUntil: "load" });
|
|
2692
|
+
return await fn(page, browser);
|
|
2693
|
+
} finally {
|
|
2694
|
+
await browser.close();
|
|
2695
|
+
}
|
|
2696
|
+
}
|
|
2697
|
+
async function cmdDownload(rest) {
|
|
2698
|
+
const latest = hasFlag(rest, "--latest");
|
|
2699
|
+
const mirrorFlag = flagValue(rest, "--mirror");
|
|
2700
|
+
if (mirrorFlag) process.env.CDPWRIGHT_DOWNLOAD_MIRROR = mirrorFlag;
|
|
2701
|
+
const urlFlag = flagValue(rest, "--url");
|
|
2702
|
+
if (urlFlag) process.env.CDPWRIGHT_DOWNLOAD_URL = urlFlag;
|
|
2703
|
+
const manager = new ChromiumManager();
|
|
2704
|
+
await manager.download({ latest });
|
|
2705
|
+
}
|
|
2706
|
+
async function cmdOpen(rest) {
|
|
2707
|
+
const url = positionalArgs(rest)[0];
|
|
2708
|
+
if (!url) {
|
|
2709
|
+
console.error("Usage: cpw open <url>");
|
|
2710
|
+
process.exit(1);
|
|
2711
|
+
}
|
|
2712
|
+
const headless = resolveHeadless(rest, false);
|
|
2713
|
+
const browser = await automaton.launch({
|
|
2714
|
+
headless,
|
|
2715
|
+
args: launchArgs(),
|
|
2716
|
+
logLevel: "warn"
|
|
2717
|
+
});
|
|
2718
|
+
const page = await browser.newPage();
|
|
2719
|
+
await page.goto(url, { waitUntil: "load" });
|
|
2720
|
+
writeSession({
|
|
2721
|
+
wsEndpoint: browser.wsEndpoint,
|
|
2722
|
+
pid: browser.pid || process.pid
|
|
2723
|
+
});
|
|
2724
|
+
console.log(`Browser open at ${url}`);
|
|
2725
|
+
console.log(`Session saved. Run commands in another terminal:`);
|
|
2726
|
+
console.log(` npx cpw screenshot -o shot.png`);
|
|
2727
|
+
console.log(` npx cpw eval "document.title"`);
|
|
2728
|
+
console.log(` npx cpw close`);
|
|
2729
|
+
console.log(`
|
|
2730
|
+
Press Ctrl+C to close.`);
|
|
2731
|
+
await new Promise((resolve) => {
|
|
2732
|
+
process.on("SIGINT", () => {
|
|
2733
|
+
console.log("\nClosing browser...");
|
|
2734
|
+
resolve();
|
|
2735
|
+
});
|
|
2736
|
+
process.on("SIGTERM", () => resolve());
|
|
2737
|
+
});
|
|
2738
|
+
clearSession();
|
|
2739
|
+
try {
|
|
2740
|
+
await browser.close();
|
|
2741
|
+
} catch {
|
|
2742
|
+
}
|
|
2743
|
+
}
|
|
2744
|
+
async function cmdClose() {
|
|
2745
|
+
const session = readSession();
|
|
2746
|
+
if (!session) {
|
|
2747
|
+
console.error("No running session.");
|
|
2748
|
+
process.exit(1);
|
|
2749
|
+
}
|
|
2750
|
+
try {
|
|
2751
|
+
const browser = await automaton.connect(session.wsEndpoint);
|
|
2752
|
+
await browser.close();
|
|
2753
|
+
} catch {
|
|
2754
|
+
try {
|
|
2755
|
+
process.kill(session.pid, "SIGTERM");
|
|
2756
|
+
} catch {
|
|
2757
|
+
}
|
|
2758
|
+
}
|
|
2759
|
+
clearSession();
|
|
2760
|
+
console.log("Session closed.");
|
|
2761
|
+
}
|
|
2762
|
+
async function cmdScreenshot(rest) {
|
|
2763
|
+
const pos = positionalArgs(rest);
|
|
2764
|
+
const url = pos[0];
|
|
2765
|
+
const output = flagValue(rest, "-o") || flagValue(rest, "--output");
|
|
2766
|
+
if (!output) {
|
|
2767
|
+
console.error("Usage: cpw screenshot [url] -o <file> [--full-page]");
|
|
2768
|
+
process.exit(1);
|
|
2769
|
+
}
|
|
2770
|
+
const fullPage = hasFlag(rest, "--full-page");
|
|
2771
|
+
const format = output.endsWith(".jpeg") || output.endsWith(".jpg") ? "jpeg" : "png";
|
|
2772
|
+
if (url) {
|
|
2773
|
+
const headless = resolveHeadless(rest, true);
|
|
2774
|
+
await withPage(url, { headless }, async (page) => {
|
|
2775
|
+
await page.screenshot({ path: output, format, fullPage });
|
|
2776
|
+
});
|
|
2777
|
+
} else {
|
|
2778
|
+
const { browser, page } = await connectToSession();
|
|
2779
|
+
try {
|
|
2780
|
+
await page.screenshot({ path: output, format, fullPage });
|
|
2781
|
+
} finally {
|
|
2782
|
+
await browser.disconnect();
|
|
2783
|
+
}
|
|
2784
|
+
}
|
|
2785
|
+
console.log(`Screenshot saved to ${output}`);
|
|
2786
|
+
}
|
|
2787
|
+
async function cmdHtml(rest) {
|
|
2788
|
+
const pos = positionalArgs(rest);
|
|
2789
|
+
const url = pos[0];
|
|
2790
|
+
const output = flagValue(rest, "-o") || flagValue(rest, "--output");
|
|
2791
|
+
if (!output) {
|
|
2792
|
+
console.error("Usage: cpw html [url] -o <file>");
|
|
2793
|
+
process.exit(1);
|
|
2794
|
+
}
|
|
2795
|
+
const writeHtml = async (page) => {
|
|
2796
|
+
const html = await page.content();
|
|
2797
|
+
fs5.writeFileSync(path5.resolve(output), html, "utf-8");
|
|
2798
|
+
};
|
|
2799
|
+
if (url) {
|
|
2800
|
+
const headless = resolveHeadless(rest, true);
|
|
2801
|
+
await withPage(url, { headless }, async (page) => {
|
|
2802
|
+
await writeHtml(page);
|
|
2803
|
+
});
|
|
2804
|
+
} else {
|
|
2805
|
+
const { browser, page } = await connectToSession();
|
|
2806
|
+
try {
|
|
2807
|
+
await writeHtml(page);
|
|
2808
|
+
} finally {
|
|
2809
|
+
await browser.disconnect();
|
|
2810
|
+
}
|
|
2811
|
+
}
|
|
2812
|
+
console.log(`HTML saved to ${output}`);
|
|
2813
|
+
}
|
|
2814
|
+
async function cmdPdf(rest) {
|
|
2815
|
+
const pos = positionalArgs(rest);
|
|
2816
|
+
const url = pos[0];
|
|
2817
|
+
const output = flagValue(rest, "-o") || flagValue(rest, "--output");
|
|
2818
|
+
if (!output) {
|
|
2819
|
+
console.error("Usage: cpw pdf [url] -o <file>");
|
|
2820
|
+
process.exit(1);
|
|
2821
|
+
}
|
|
2822
|
+
const renderVisualPdf = async (browser, page) => {
|
|
2823
|
+
const screenshot = await page.screenshotBase64({ format: "png" });
|
|
2824
|
+
const screenshotSize = pngDimensions(Buffer.from(screenshot, "base64"));
|
|
2825
|
+
const context = await browser.newContext();
|
|
2826
|
+
try {
|
|
2827
|
+
const helperPage = await context.newPage();
|
|
2828
|
+
const imageDataUrl = `data:image/png;base64,${screenshot}`;
|
|
2829
|
+
const screenshotDimensions = await helperPage.evaluate((dataUrl) => {
|
|
2830
|
+
document.open();
|
|
2831
|
+
document.write(`<!doctype html>
|
|
2832
|
+
<html>
|
|
2833
|
+
<head>
|
|
2834
|
+
<meta charset="utf-8">
|
|
2835
|
+
<style>
|
|
2836
|
+
html, body {
|
|
2837
|
+
margin: 0;
|
|
2838
|
+
padding: 0;
|
|
2839
|
+
overflow: hidden;
|
|
2840
|
+
background: #fff;
|
|
2841
|
+
}
|
|
2842
|
+
img {
|
|
2843
|
+
display: block;
|
|
2844
|
+
width: 100%;
|
|
2845
|
+
height: auto;
|
|
2846
|
+
}
|
|
2847
|
+
</style>
|
|
2848
|
+
</head>
|
|
2849
|
+
<body>
|
|
2850
|
+
<img id="shot" alt="page screenshot">
|
|
2851
|
+
</body>
|
|
2852
|
+
</html>`);
|
|
2853
|
+
document.close();
|
|
2854
|
+
const img = document.getElementById("shot");
|
|
2855
|
+
if (!(img instanceof HTMLImageElement)) {
|
|
2856
|
+
throw new Error("Failed to create PDF preview image");
|
|
2857
|
+
}
|
|
2858
|
+
return new Promise((resolve, reject) => {
|
|
2859
|
+
img.onload = () => {
|
|
2860
|
+
resolve({ width: img.naturalWidth || 0, height: img.naturalHeight || 0 });
|
|
2861
|
+
};
|
|
2862
|
+
img.onerror = () => reject(new Error("Failed to load screenshot image"));
|
|
2863
|
+
img.src = dataUrl;
|
|
2864
|
+
});
|
|
2865
|
+
}, imageDataUrl);
|
|
2866
|
+
await helperPage.pdf({
|
|
2867
|
+
path: output,
|
|
2868
|
+
printBackground: true,
|
|
2869
|
+
paperWidth: Math.max((screenshotDimensions?.width ?? screenshotSize.width) / 96, 1),
|
|
2870
|
+
paperHeight: Math.max((screenshotDimensions?.height ?? screenshotSize.height) / 96, 1),
|
|
2871
|
+
marginTop: 0,
|
|
2872
|
+
marginBottom: 0,
|
|
2873
|
+
marginLeft: 0,
|
|
2874
|
+
marginRight: 0,
|
|
2875
|
+
scale: 1,
|
|
2876
|
+
preferCSSPageSize: false
|
|
2877
|
+
});
|
|
2878
|
+
} finally {
|
|
2879
|
+
await context.close();
|
|
2880
|
+
}
|
|
2881
|
+
};
|
|
2882
|
+
if (url) {
|
|
2883
|
+
await withPage(url, { headless: true, maximize: true }, async (page, browser) => {
|
|
2884
|
+
await renderVisualPdf(browser, page);
|
|
2885
|
+
});
|
|
2886
|
+
} else {
|
|
2887
|
+
const { browser, page } = await connectToSession();
|
|
2888
|
+
try {
|
|
2889
|
+
await renderVisualPdf(browser, page);
|
|
2890
|
+
} finally {
|
|
2891
|
+
await browser.disconnect();
|
|
2892
|
+
}
|
|
2893
|
+
}
|
|
2894
|
+
console.log(`PDF saved to ${output}`);
|
|
2895
|
+
}
|
|
2896
|
+
async function cmdEval(rest) {
|
|
2897
|
+
const pos = positionalArgs(rest);
|
|
2898
|
+
const session = readSession();
|
|
2899
|
+
let url;
|
|
2900
|
+
let script;
|
|
2901
|
+
if (pos.length >= 2) {
|
|
2902
|
+
url = pos[0];
|
|
2903
|
+
script = pos[1];
|
|
2904
|
+
} else if (pos.length === 1 && session) {
|
|
2905
|
+
script = pos[0];
|
|
2906
|
+
} else if (pos.length === 1) {
|
|
2907
|
+
console.error("Usage: cpw eval <url> <script>\n cpw eval <script> (when a session is running)");
|
|
2908
|
+
process.exit(1);
|
|
2909
|
+
return;
|
|
2910
|
+
} else {
|
|
2911
|
+
console.error("Usage: cpw eval [url] <script>");
|
|
2912
|
+
process.exit(1);
|
|
2913
|
+
return;
|
|
2914
|
+
}
|
|
2915
|
+
if (url) {
|
|
2916
|
+
const headless = resolveHeadless(rest, true);
|
|
2917
|
+
await withPage(url, { headless }, async (page) => {
|
|
2918
|
+
const result = await page.evaluate(script);
|
|
2919
|
+
const output = result === void 0 ? "undefined" : JSON.stringify(result, null, 2);
|
|
2920
|
+
console.log(output);
|
|
2921
|
+
});
|
|
2922
|
+
} else {
|
|
2923
|
+
const { browser, page } = await connectToSession();
|
|
2924
|
+
try {
|
|
2925
|
+
const result = await page.evaluate(script);
|
|
2926
|
+
const output = result === void 0 ? "undefined" : JSON.stringify(result, null, 2);
|
|
2927
|
+
console.log(output);
|
|
2928
|
+
} finally {
|
|
2929
|
+
await browser.disconnect();
|
|
2930
|
+
}
|
|
2931
|
+
}
|
|
2932
|
+
}
|
|
2933
|
+
async function cmdVersion() {
|
|
2934
|
+
const pkgPath = path5.resolve(path5.dirname(fileURLToPath(import.meta.url)), "../package.json");
|
|
2935
|
+
let version = "unknown";
|
|
2936
|
+
try {
|
|
2937
|
+
const pkg = JSON.parse(fs5.readFileSync(pkgPath, "utf-8"));
|
|
2938
|
+
version = pkg.version;
|
|
2939
|
+
} catch {
|
|
2940
|
+
}
|
|
2941
|
+
console.log(`cdpwright v${version}`);
|
|
2942
|
+
console.log(`Pinned revision: ${PINNED_REVISION}`);
|
|
2943
|
+
const platform = detectPlatform();
|
|
2944
|
+
const cacheRoot = defaultCacheRoot(platform);
|
|
2945
|
+
const revision = resolveRevision(process.env.CDPWRIGHT_REVISION);
|
|
2946
|
+
const execPath = path5.join(cacheRoot, platform, revision, chromiumExecutableRelativePath(platform));
|
|
2947
|
+
if (fs5.existsSync(execPath)) {
|
|
2948
|
+
try {
|
|
2949
|
+
const ver = await chromiumVersion(execPath);
|
|
2950
|
+
console.log(`Chromium: ${ver}`);
|
|
2951
|
+
} catch {
|
|
2952
|
+
console.log(`Chromium: installed at ${execPath}`);
|
|
2953
|
+
}
|
|
2954
|
+
} else {
|
|
2955
|
+
console.log("Chromium: not installed (run 'cpw install')");
|
|
2956
|
+
}
|
|
2957
|
+
const session = readSession();
|
|
2958
|
+
if (session) {
|
|
2959
|
+
console.log(`Session: active (pid ${session.pid})`);
|
|
2960
|
+
}
|
|
2141
2961
|
}
|
|
2142
2962
|
async function main() {
|
|
2143
2963
|
const [, , command, ...rest] = process.argv;
|
|
@@ -2145,14 +2965,31 @@ async function main() {
|
|
|
2145
2965
|
printHelp();
|
|
2146
2966
|
process.exit(0);
|
|
2147
2967
|
}
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2968
|
+
switch (command) {
|
|
2969
|
+
case "download":
|
|
2970
|
+
case "install":
|
|
2971
|
+
return cmdDownload(rest);
|
|
2972
|
+
case "open":
|
|
2973
|
+
return cmdOpen(rest);
|
|
2974
|
+
case "close":
|
|
2975
|
+
return cmdClose();
|
|
2976
|
+
case "screenshot":
|
|
2977
|
+
return cmdScreenshot(rest);
|
|
2978
|
+
case "html":
|
|
2979
|
+
return cmdHtml(rest);
|
|
2980
|
+
case "pdf":
|
|
2981
|
+
return cmdPdf(rest);
|
|
2982
|
+
case "eval":
|
|
2983
|
+
return cmdEval(rest);
|
|
2984
|
+
case "version":
|
|
2985
|
+
case "--version":
|
|
2986
|
+
case "-v":
|
|
2987
|
+
return cmdVersion();
|
|
2988
|
+
default:
|
|
2989
|
+
console.error(`Unknown command: ${command}`);
|
|
2990
|
+
printHelp();
|
|
2991
|
+
process.exit(1);
|
|
2152
2992
|
}
|
|
2153
|
-
const latest = rest.includes("--latest");
|
|
2154
|
-
const manager = new ChromiumManager();
|
|
2155
|
-
await manager.download({ latest });
|
|
2156
2993
|
}
|
|
2157
2994
|
main().catch((err) => {
|
|
2158
2995
|
console.error(err instanceof Error ? err.message : String(err));
|