@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/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 http from "http";
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
- constructor(connection, child, logger, events, cleanupTasks = []) {
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 = `${SNAPSHOT_BASE}/${folder}/LAST_CHANGE`;
1776
+ const url = `${base}/${folder}/LAST_CHANGE`;
1678
1777
  return new Promise((resolve, reject) => {
1679
- https.get(url, (res) => {
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 downloadUrl = `${SNAPSHOT_BASE}/${folder}/${revision}/${zipName}`;
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
- https.get(url, (res) => {
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
- http.get(versionUrl, (res) => {
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("cdpwright (cpw) download [--latest]");
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
- if (command !== "download") {
2149
- console.error(`Unknown command: ${command}`);
2150
- printHelp();
2151
- process.exit(1);
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));