@toolstackhq/cdpwright 1.0.0 → 1.1.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 +10 -3
- package/dist/assert/expect.d.ts +1 -1
- package/dist/assert/expect.js +1 -1
- package/dist/{chunk-6BPF3IEU.js → chunk-M5G6EMAS.js} +30 -3
- package/dist/chunk-M5G6EMAS.js.map +1 -0
- package/dist/cli.js +537 -16
- package/dist/cli.js.map +1 -1
- package/dist/{expect-CY70zJc0.d.ts → expect-BY8vnFfi.d.ts} +16 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +21 -7
- 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
|
|
@@ -1443,7 +1448,8 @@ var Page = class {
|
|
|
1443
1448
|
const result = await this.session.send("Page.captureScreenshot", {
|
|
1444
1449
|
format: options.format ?? "png",
|
|
1445
1450
|
quality: options.quality,
|
|
1446
|
-
fromSurface: true
|
|
1451
|
+
fromSurface: true,
|
|
1452
|
+
captureBeyondViewport: options.fullPage ?? false
|
|
1447
1453
|
});
|
|
1448
1454
|
const buffer = Buffer.from(result.data, "base64");
|
|
1449
1455
|
if (options.path) {
|
|
@@ -1460,12 +1466,38 @@ var Page = class {
|
|
|
1460
1466
|
const result = await this.session.send("Page.captureScreenshot", {
|
|
1461
1467
|
format: options.format ?? "png",
|
|
1462
1468
|
quality: options.quality,
|
|
1463
|
-
fromSurface: true
|
|
1469
|
+
fromSurface: true,
|
|
1470
|
+
captureBeyondViewport: options.fullPage ?? false
|
|
1464
1471
|
});
|
|
1465
1472
|
const duration = Date.now() - start;
|
|
1466
1473
|
this.events.emit("action:end", { name: "screenshotBase64", frameId: this.mainFrameId, durationMs: duration });
|
|
1467
1474
|
return result.data;
|
|
1468
1475
|
}
|
|
1476
|
+
async pdf(options = {}) {
|
|
1477
|
+
const start = Date.now();
|
|
1478
|
+
this.events.emit("action:start", { name: "pdf", frameId: this.mainFrameId });
|
|
1479
|
+
const result = await this.session.send("Page.printToPDF", {
|
|
1480
|
+
landscape: options.landscape ?? false,
|
|
1481
|
+
printBackground: options.printBackground ?? true,
|
|
1482
|
+
scale: options.scale,
|
|
1483
|
+
paperWidth: options.paperWidth,
|
|
1484
|
+
paperHeight: options.paperHeight,
|
|
1485
|
+
marginTop: options.marginTop,
|
|
1486
|
+
marginBottom: options.marginBottom,
|
|
1487
|
+
marginLeft: options.marginLeft,
|
|
1488
|
+
marginRight: options.marginRight,
|
|
1489
|
+
pageRanges: options.pageRanges,
|
|
1490
|
+
preferCSSPageSize: options.preferCSSPageSize
|
|
1491
|
+
});
|
|
1492
|
+
const buffer = Buffer.from(result.data, "base64");
|
|
1493
|
+
if (options.path) {
|
|
1494
|
+
const resolved = path2.resolve(options.path);
|
|
1495
|
+
fs2.writeFileSync(resolved, buffer);
|
|
1496
|
+
}
|
|
1497
|
+
const duration = Date.now() - start;
|
|
1498
|
+
this.events.emit("action:end", { name: "pdf", frameId: this.mainFrameId, durationMs: duration });
|
|
1499
|
+
return buffer;
|
|
1500
|
+
}
|
|
1469
1501
|
getEvents() {
|
|
1470
1502
|
return this.events;
|
|
1471
1503
|
}
|
|
@@ -1635,6 +1667,7 @@ var Browser = class {
|
|
|
1635
1667
|
import fs3 from "fs";
|
|
1636
1668
|
import path3 from "path";
|
|
1637
1669
|
import os from "os";
|
|
1670
|
+
import http from "http";
|
|
1638
1671
|
import https from "https";
|
|
1639
1672
|
import { spawn } from "child_process";
|
|
1640
1673
|
import yauzl from "yauzl";
|
|
@@ -1672,11 +1705,22 @@ function chromiumExecutableRelativePath(platform) {
|
|
|
1672
1705
|
if (platform === "mac") return path3.join("chrome-mac", "Chromium.app", "Contents", "MacOS", "Chromium");
|
|
1673
1706
|
return path3.join("chrome-win", "chrome.exe");
|
|
1674
1707
|
}
|
|
1708
|
+
function httpGet(url) {
|
|
1709
|
+
return url.startsWith("http://") ? http : https;
|
|
1710
|
+
}
|
|
1711
|
+
function resolveSnapshotBase() {
|
|
1712
|
+
const mirror = process.env.CDPWRIGHT_DOWNLOAD_MIRROR;
|
|
1713
|
+
if (mirror && mirror.trim()) {
|
|
1714
|
+
return mirror.trim().replace(/\/+$/, "");
|
|
1715
|
+
}
|
|
1716
|
+
return SNAPSHOT_BASE;
|
|
1717
|
+
}
|
|
1675
1718
|
async function fetchLatestRevision(platform) {
|
|
1719
|
+
const base = resolveSnapshotBase();
|
|
1676
1720
|
const folder = platformFolder(platform);
|
|
1677
|
-
const url = `${
|
|
1721
|
+
const url = `${base}/${folder}/LAST_CHANGE`;
|
|
1678
1722
|
return new Promise((resolve, reject) => {
|
|
1679
|
-
|
|
1723
|
+
httpGet(url).get(url, (res) => {
|
|
1680
1724
|
if (res.statusCode && res.statusCode >= 400) {
|
|
1681
1725
|
reject(new Error(`Failed to fetch LAST_CHANGE: ${res.statusCode}`));
|
|
1682
1726
|
return;
|
|
@@ -1701,7 +1745,9 @@ async function ensureDownloaded(options) {
|
|
|
1701
1745
|
fs3.mkdirSync(revisionDir, { recursive: true });
|
|
1702
1746
|
const folder = platformFolder(platform);
|
|
1703
1747
|
const zipName = platform === "win" ? "chrome-win.zip" : platform === "mac" ? "chrome-mac.zip" : "chrome-linux.zip";
|
|
1704
|
-
const
|
|
1748
|
+
const explicitUrl = process.env.CDPWRIGHT_DOWNLOAD_URL?.trim();
|
|
1749
|
+
const base = resolveSnapshotBase();
|
|
1750
|
+
const downloadUrl = explicitUrl || `${base}/${folder}/${revision}/${zipName}`;
|
|
1705
1751
|
const tempZipPath = path3.join(os.tmpdir(), `cdpwright-${platform}-${revision}.zip`);
|
|
1706
1752
|
logger.info("Downloading Chromium snapshot", downloadUrl);
|
|
1707
1753
|
await downloadFile(downloadUrl, tempZipPath, logger);
|
|
@@ -1718,7 +1764,7 @@ async function ensureDownloaded(options) {
|
|
|
1718
1764
|
function downloadFile(url, dest, logger) {
|
|
1719
1765
|
return new Promise((resolve, reject) => {
|
|
1720
1766
|
const file = fs3.createWriteStream(dest);
|
|
1721
|
-
|
|
1767
|
+
httpGet(url).get(url, (res) => {
|
|
1722
1768
|
if (res.statusCode && res.statusCode >= 400) {
|
|
1723
1769
|
reject(new Error(`Failed to download: ${res.statusCode}`));
|
|
1724
1770
|
return;
|
|
@@ -2112,7 +2158,7 @@ function toHttpVersionUrl(wsUrl) {
|
|
|
2112
2158
|
}
|
|
2113
2159
|
function fetchWebSocketDebuggerUrl(versionUrl) {
|
|
2114
2160
|
return new Promise((resolve, reject) => {
|
|
2115
|
-
|
|
2161
|
+
http2.get(versionUrl, (res) => {
|
|
2116
2162
|
if (res.statusCode && res.statusCode >= 400) {
|
|
2117
2163
|
reject(new Error(`Failed to fetch /json/version: ${res.statusCode}`));
|
|
2118
2164
|
return;
|
|
@@ -2135,9 +2181,471 @@ function fetchWebSocketDebuggerUrl(versionUrl) {
|
|
|
2135
2181
|
});
|
|
2136
2182
|
}
|
|
2137
2183
|
|
|
2184
|
+
// src/assert/AssertionError.ts
|
|
2185
|
+
var AssertionError = class extends Error {
|
|
2186
|
+
selector;
|
|
2187
|
+
timeoutMs;
|
|
2188
|
+
lastState;
|
|
2189
|
+
constructor(message, options = {}) {
|
|
2190
|
+
super(message);
|
|
2191
|
+
this.name = "AssertionError";
|
|
2192
|
+
this.selector = options.selector;
|
|
2193
|
+
this.timeoutMs = options.timeoutMs;
|
|
2194
|
+
this.lastState = options.lastState;
|
|
2195
|
+
}
|
|
2196
|
+
};
|
|
2197
|
+
|
|
2198
|
+
// src/assert/expect.ts
|
|
2199
|
+
var ElementExpectation = class _ElementExpectation {
|
|
2200
|
+
frame;
|
|
2201
|
+
selector;
|
|
2202
|
+
options;
|
|
2203
|
+
negate;
|
|
2204
|
+
events;
|
|
2205
|
+
constructor(frame, selector, options, negate, events) {
|
|
2206
|
+
this.frame = frame;
|
|
2207
|
+
this.selector = selector;
|
|
2208
|
+
this.options = options;
|
|
2209
|
+
this.negate = negate;
|
|
2210
|
+
this.events = events;
|
|
2211
|
+
}
|
|
2212
|
+
get not() {
|
|
2213
|
+
return new _ElementExpectation(this.frame, this.selector, this.options, !this.negate, this.events);
|
|
2214
|
+
}
|
|
2215
|
+
async toExist() {
|
|
2216
|
+
return this.assert(async () => {
|
|
2217
|
+
const exists = await this.frame.exists(this.selector, this.options);
|
|
2218
|
+
return this.negate ? !exists : exists;
|
|
2219
|
+
}, this.negate ? "Expected element not to exist" : "Expected element to exist");
|
|
2220
|
+
}
|
|
2221
|
+
async toBeVisible() {
|
|
2222
|
+
return this.assert(async () => {
|
|
2223
|
+
const visible = await this.frame.isVisible(this.selector, this.options);
|
|
2224
|
+
return this.negate ? !visible : visible;
|
|
2225
|
+
}, this.negate ? "Expected element not to be visible" : "Expected element to be visible");
|
|
2226
|
+
}
|
|
2227
|
+
async toBeHidden() {
|
|
2228
|
+
return this.assert(async () => {
|
|
2229
|
+
const visible = await this.frame.isVisible(this.selector, this.options);
|
|
2230
|
+
return this.negate ? visible : !visible;
|
|
2231
|
+
}, this.negate ? "Expected element not to be hidden" : "Expected element to be hidden");
|
|
2232
|
+
}
|
|
2233
|
+
async toBeEnabled() {
|
|
2234
|
+
return this.assert(async () => {
|
|
2235
|
+
const enabled = await this.frame.isEnabled(this.selector, this.options);
|
|
2236
|
+
if (enabled == null) {
|
|
2237
|
+
return this.negate ? true : false;
|
|
2238
|
+
}
|
|
2239
|
+
return this.negate ? !enabled : enabled;
|
|
2240
|
+
}, this.negate ? "Expected element not to be enabled" : "Expected element to be enabled");
|
|
2241
|
+
}
|
|
2242
|
+
async toBeDisabled() {
|
|
2243
|
+
return this.assert(async () => {
|
|
2244
|
+
const enabled = await this.frame.isEnabled(this.selector, this.options);
|
|
2245
|
+
if (enabled == null) {
|
|
2246
|
+
return this.negate ? true : false;
|
|
2247
|
+
}
|
|
2248
|
+
const disabled = !enabled;
|
|
2249
|
+
return this.negate ? !disabled : disabled;
|
|
2250
|
+
}, this.negate ? "Expected element not to be disabled" : "Expected element to be disabled");
|
|
2251
|
+
}
|
|
2252
|
+
async toBeChecked() {
|
|
2253
|
+
return this.assert(async () => {
|
|
2254
|
+
const checked = await this.frame.isChecked(this.selector, this.options);
|
|
2255
|
+
if (checked == null) {
|
|
2256
|
+
return this.negate ? true : false;
|
|
2257
|
+
}
|
|
2258
|
+
return this.negate ? !checked : checked;
|
|
2259
|
+
}, this.negate ? "Expected element not to be checked" : "Expected element to be checked");
|
|
2260
|
+
}
|
|
2261
|
+
async toBeUnchecked() {
|
|
2262
|
+
return this.assert(async () => {
|
|
2263
|
+
const checked = await this.frame.isChecked(this.selector, this.options);
|
|
2264
|
+
if (checked == null) {
|
|
2265
|
+
return this.negate ? true : false;
|
|
2266
|
+
}
|
|
2267
|
+
const unchecked = !checked;
|
|
2268
|
+
return this.negate ? !unchecked : unchecked;
|
|
2269
|
+
}, this.negate ? "Expected element not to be unchecked" : "Expected element to be unchecked");
|
|
2270
|
+
}
|
|
2271
|
+
async toHaveText(textOrRegex) {
|
|
2272
|
+
const expected = textOrRegex;
|
|
2273
|
+
return this.assert(async () => {
|
|
2274
|
+
const text = await this.frame.text(this.selector, this.options);
|
|
2275
|
+
if (text == null) {
|
|
2276
|
+
return this.negate ? true : false;
|
|
2277
|
+
}
|
|
2278
|
+
const matches = expected instanceof RegExp ? new RegExp(expected.source, expected.flags.replace("g", "")).test(text) : text.includes(expected);
|
|
2279
|
+
return this.negate ? !matches : matches;
|
|
2280
|
+
}, this.negate ? "Expected element text not to match" : "Expected element text to match", { expected });
|
|
2281
|
+
}
|
|
2282
|
+
async toHaveExactText(textOrRegex) {
|
|
2283
|
+
const expected = textOrRegex;
|
|
2284
|
+
return this.assert(async () => {
|
|
2285
|
+
const text = await this.frame.text(this.selector, this.options);
|
|
2286
|
+
if (text == null) {
|
|
2287
|
+
return this.negate ? true : false;
|
|
2288
|
+
}
|
|
2289
|
+
const matches = expected instanceof RegExp ? new RegExp(expected.source, expected.flags.replace("g", "")).test(text) : text === expected;
|
|
2290
|
+
return this.negate ? !matches : matches;
|
|
2291
|
+
}, this.negate ? "Expected element text not to match exactly" : "Expected element text to match exactly", { expected });
|
|
2292
|
+
}
|
|
2293
|
+
async toContainText(textOrRegex) {
|
|
2294
|
+
const expected = textOrRegex;
|
|
2295
|
+
return this.assert(async () => {
|
|
2296
|
+
const text = await this.frame.text(this.selector, this.options);
|
|
2297
|
+
if (text == null) {
|
|
2298
|
+
return this.negate ? true : false;
|
|
2299
|
+
}
|
|
2300
|
+
const matches = expected instanceof RegExp ? new RegExp(expected.source, expected.flags.replace("g", "")).test(text) : text.includes(expected);
|
|
2301
|
+
return this.negate ? !matches : matches;
|
|
2302
|
+
}, this.negate ? "Expected element text not to contain" : "Expected element text to contain", { expected });
|
|
2303
|
+
}
|
|
2304
|
+
async toHaveValue(valueOrRegex) {
|
|
2305
|
+
const expected = valueOrRegex;
|
|
2306
|
+
return this.assert(async () => {
|
|
2307
|
+
const value = await this.frame.value(this.selector, this.options);
|
|
2308
|
+
if (value == null) {
|
|
2309
|
+
return this.negate ? true : false;
|
|
2310
|
+
}
|
|
2311
|
+
const matches = expected instanceof RegExp ? new RegExp(expected.source, expected.flags.replace("g", "")).test(value) : value === expected;
|
|
2312
|
+
return this.negate ? !matches : matches;
|
|
2313
|
+
}, this.negate ? "Expected element value not to match" : "Expected element value to match", { expected });
|
|
2314
|
+
}
|
|
2315
|
+
async toHaveAttribute(name, valueOrRegex) {
|
|
2316
|
+
const expected = valueOrRegex;
|
|
2317
|
+
return this.assert(async () => {
|
|
2318
|
+
const value = await this.frame.attribute(this.selector, name, this.options);
|
|
2319
|
+
if (expected === void 0) {
|
|
2320
|
+
const exists = value != null;
|
|
2321
|
+
return this.negate ? !exists : exists;
|
|
2322
|
+
}
|
|
2323
|
+
if (value == null) {
|
|
2324
|
+
return this.negate ? true : false;
|
|
2325
|
+
}
|
|
2326
|
+
const matches = expected instanceof RegExp ? new RegExp(expected.source, expected.flags.replace("g", "")).test(value) : value === expected;
|
|
2327
|
+
return this.negate ? !matches : matches;
|
|
2328
|
+
}, this.negate ? "Expected element attribute not to match" : "Expected element attribute to match", { expected, name });
|
|
2329
|
+
}
|
|
2330
|
+
async toHaveId(idOrRegex) {
|
|
2331
|
+
return this.toHaveAttribute("id", idOrRegex);
|
|
2332
|
+
}
|
|
2333
|
+
async toHaveName(nameOrRegex) {
|
|
2334
|
+
return this.toHaveAttribute("name", nameOrRegex);
|
|
2335
|
+
}
|
|
2336
|
+
async toHaveCount(expected) {
|
|
2337
|
+
return this.assert(async () => {
|
|
2338
|
+
const count = await this.frame.count(this.selector, this.options);
|
|
2339
|
+
const matches = count === expected;
|
|
2340
|
+
return this.negate ? !matches : matches;
|
|
2341
|
+
}, this.negate ? "Expected element count not to match" : "Expected element count to match", { expected });
|
|
2342
|
+
}
|
|
2343
|
+
async toHaveClass(nameOrRegex) {
|
|
2344
|
+
const expected = nameOrRegex;
|
|
2345
|
+
return this.assert(async () => {
|
|
2346
|
+
const classes = await this.frame.classes(this.selector, this.options);
|
|
2347
|
+
if (classes == null) {
|
|
2348
|
+
return this.negate ? true : false;
|
|
2349
|
+
}
|
|
2350
|
+
const matches = expected instanceof RegExp ? classes.some((value) => new RegExp(expected.source, expected.flags.replace("g", "")).test(value)) : classes.includes(expected);
|
|
2351
|
+
return this.negate ? !matches : matches;
|
|
2352
|
+
}, this.negate ? "Expected element class not to match" : "Expected element class to match", { expected });
|
|
2353
|
+
}
|
|
2354
|
+
async toHaveClasses(expected) {
|
|
2355
|
+
return this.assert(async () => {
|
|
2356
|
+
const classes = await this.frame.classes(this.selector, this.options);
|
|
2357
|
+
if (classes == null) {
|
|
2358
|
+
return this.negate ? true : false;
|
|
2359
|
+
}
|
|
2360
|
+
const matches = expected.every((value) => classes.includes(value));
|
|
2361
|
+
return this.negate ? !matches : matches;
|
|
2362
|
+
}, this.negate ? "Expected element classes not to match" : "Expected element classes to match", { expected });
|
|
2363
|
+
}
|
|
2364
|
+
async toHaveCss(property, valueOrRegex) {
|
|
2365
|
+
const expected = valueOrRegex;
|
|
2366
|
+
return this.assert(async () => {
|
|
2367
|
+
const value = await this.frame.css(this.selector, property, this.options);
|
|
2368
|
+
if (value == null) {
|
|
2369
|
+
return this.negate ? true : false;
|
|
2370
|
+
}
|
|
2371
|
+
const actual = value.trim();
|
|
2372
|
+
const matches = expected instanceof RegExp ? new RegExp(expected.source, expected.flags.replace("g", "")).test(actual) : actual === expected;
|
|
2373
|
+
return this.negate ? !matches : matches;
|
|
2374
|
+
}, this.negate ? "Expected element css not to match" : "Expected element css to match", { expected, property });
|
|
2375
|
+
}
|
|
2376
|
+
async toHaveFocus() {
|
|
2377
|
+
return this.assert(async () => {
|
|
2378
|
+
const focused = await this.frame.hasFocus(this.selector, this.options);
|
|
2379
|
+
if (focused == null) {
|
|
2380
|
+
return this.negate ? true : false;
|
|
2381
|
+
}
|
|
2382
|
+
return this.negate ? !focused : focused;
|
|
2383
|
+
}, this.negate ? "Expected element not to have focus" : "Expected element to have focus");
|
|
2384
|
+
}
|
|
2385
|
+
async toBeInViewport(options = {}) {
|
|
2386
|
+
return this.assert(async () => {
|
|
2387
|
+
const inViewport = await this.frame.isInViewport(this.selector, this.options, Boolean(options.fully));
|
|
2388
|
+
if (inViewport == null) {
|
|
2389
|
+
return this.negate ? true : false;
|
|
2390
|
+
}
|
|
2391
|
+
return this.negate ? !inViewport : inViewport;
|
|
2392
|
+
}, this.negate ? "Expected element not to be in viewport" : "Expected element to be in viewport");
|
|
2393
|
+
}
|
|
2394
|
+
async toBeEditable() {
|
|
2395
|
+
return this.assert(async () => {
|
|
2396
|
+
const editable = await this.frame.isEditable(this.selector, this.options);
|
|
2397
|
+
if (editable == null) {
|
|
2398
|
+
return this.negate ? true : false;
|
|
2399
|
+
}
|
|
2400
|
+
return this.negate ? !editable : editable;
|
|
2401
|
+
}, this.negate ? "Expected element not to be editable" : "Expected element to be editable");
|
|
2402
|
+
}
|
|
2403
|
+
async assert(predicate, message, details = {}) {
|
|
2404
|
+
const timeoutMs = this.options.timeoutMs ?? 3e4;
|
|
2405
|
+
const start = Date.now();
|
|
2406
|
+
this.events.emit("assertion:start", { name: message, selector: this.selector, frameId: this.frame.id });
|
|
2407
|
+
let lastState;
|
|
2408
|
+
try {
|
|
2409
|
+
await waitFor(async () => {
|
|
2410
|
+
const result = await predicate();
|
|
2411
|
+
lastState = result;
|
|
2412
|
+
return result;
|
|
2413
|
+
}, { timeoutMs, description: message });
|
|
2414
|
+
} catch {
|
|
2415
|
+
const duration2 = Date.now() - start;
|
|
2416
|
+
this.events.emit("assertion:end", { name: message, selector: this.selector, frameId: this.frame.id, durationMs: duration2, status: "failed" });
|
|
2417
|
+
throw new AssertionError(message, { selector: this.selector, timeoutMs, lastState: { lastState, ...details } });
|
|
2418
|
+
}
|
|
2419
|
+
const duration = Date.now() - start;
|
|
2420
|
+
this.events.emit("assertion:end", { name: message, selector: this.selector, frameId: this.frame.id, durationMs: duration, status: "passed" });
|
|
2421
|
+
}
|
|
2422
|
+
};
|
|
2423
|
+
var ExpectFrame = class {
|
|
2424
|
+
frame;
|
|
2425
|
+
events;
|
|
2426
|
+
constructor(frame, events) {
|
|
2427
|
+
this.frame = frame;
|
|
2428
|
+
this.events = events;
|
|
2429
|
+
}
|
|
2430
|
+
element(selector, options = {}) {
|
|
2431
|
+
return new ElementExpectation(this.frame, selector, options, false, this.events);
|
|
2432
|
+
}
|
|
2433
|
+
};
|
|
2434
|
+
function expect(page) {
|
|
2435
|
+
return {
|
|
2436
|
+
element: (selector, options = {}) => new ElementExpectation(page.mainFrame(), selector, options, false, page.getEvents()),
|
|
2437
|
+
frame: (options) => {
|
|
2438
|
+
const frame = page.frame(options);
|
|
2439
|
+
if (!frame) {
|
|
2440
|
+
throw new AssertionError("Frame not found", { selector: JSON.stringify(options) });
|
|
2441
|
+
}
|
|
2442
|
+
return new ExpectFrame(frame, page.getEvents());
|
|
2443
|
+
}
|
|
2444
|
+
};
|
|
2445
|
+
}
|
|
2446
|
+
Page.prototype.expect = function(selector, options) {
|
|
2447
|
+
const builder = expect(this);
|
|
2448
|
+
if (selector) {
|
|
2449
|
+
return builder.element(selector, options);
|
|
2450
|
+
}
|
|
2451
|
+
return builder;
|
|
2452
|
+
};
|
|
2453
|
+
Object.defineProperty(Page.prototype, "find", {
|
|
2454
|
+
get: function() {
|
|
2455
|
+
return {
|
|
2456
|
+
locators: (options) => this.findLocators(options)
|
|
2457
|
+
};
|
|
2458
|
+
}
|
|
2459
|
+
});
|
|
2460
|
+
|
|
2461
|
+
// src/index.ts
|
|
2462
|
+
var automaton = {
|
|
2463
|
+
async launch(options = {}) {
|
|
2464
|
+
const manager = new ChromiumManager(options.logger);
|
|
2465
|
+
return manager.launch(options);
|
|
2466
|
+
}
|
|
2467
|
+
};
|
|
2468
|
+
|
|
2138
2469
|
// src/cli.ts
|
|
2139
2470
|
function printHelp() {
|
|
2140
|
-
console.log(
|
|
2471
|
+
console.log(`cdpwright (cpw) \u2014 Chromium automation CLI
|
|
2472
|
+
|
|
2473
|
+
Commands:
|
|
2474
|
+
download [options] Download pinned Chromium snapshot
|
|
2475
|
+
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
|
+
version Print cdpwright and Chromium versions
|
|
2481
|
+
|
|
2482
|
+
Download options:
|
|
2483
|
+
--latest Download the latest Chromium revision
|
|
2484
|
+
--mirror <url> Custom mirror base URL
|
|
2485
|
+
--url <url> Exact zip URL override
|
|
2486
|
+
|
|
2487
|
+
Common options:
|
|
2488
|
+
--headless Run in headless mode (default for screenshot, pdf, eval)
|
|
2489
|
+
--headed Run in headed mode (default for open)
|
|
2490
|
+
|
|
2491
|
+
Screenshot options:
|
|
2492
|
+
-o, --output <file> Output file path (required)
|
|
2493
|
+
--full-page Capture full scrollable page
|
|
2494
|
+
|
|
2495
|
+
PDF options:
|
|
2496
|
+
-o, --output <file> Output file path (required)
|
|
2497
|
+
Note: pdf always runs headless (CDP limitation)`);
|
|
2498
|
+
}
|
|
2499
|
+
function hasFlag(args, flag) {
|
|
2500
|
+
return args.includes(flag);
|
|
2501
|
+
}
|
|
2502
|
+
function flagValue(args, flag) {
|
|
2503
|
+
const index = args.indexOf(flag);
|
|
2504
|
+
if (index === -1 || index + 1 >= args.length) return void 0;
|
|
2505
|
+
return args[index + 1];
|
|
2506
|
+
}
|
|
2507
|
+
var VALUE_FLAGS = ["--mirror", "--url", "-o", "--output"];
|
|
2508
|
+
function resolveHeadless(args, defaultValue) {
|
|
2509
|
+
if (hasFlag(args, "--headed")) return false;
|
|
2510
|
+
if (hasFlag(args, "--headless")) return true;
|
|
2511
|
+
return defaultValue;
|
|
2512
|
+
}
|
|
2513
|
+
function positionalArgs(args) {
|
|
2514
|
+
const result = [];
|
|
2515
|
+
for (let i = 0; i < args.length; i++) {
|
|
2516
|
+
if (args[i].startsWith("-")) {
|
|
2517
|
+
if (VALUE_FLAGS.includes(args[i])) i++;
|
|
2518
|
+
continue;
|
|
2519
|
+
}
|
|
2520
|
+
result.push(args[i]);
|
|
2521
|
+
}
|
|
2522
|
+
return result;
|
|
2523
|
+
}
|
|
2524
|
+
async function withPage(url, options, fn) {
|
|
2525
|
+
const args = [];
|
|
2526
|
+
if (process.platform === "linux") {
|
|
2527
|
+
args.push("--no-sandbox", "--no-zygote", "--disable-dev-shm-usage");
|
|
2528
|
+
}
|
|
2529
|
+
const browser = await automaton.launch({
|
|
2530
|
+
headless: options.headless ?? true,
|
|
2531
|
+
args,
|
|
2532
|
+
logLevel: "warn"
|
|
2533
|
+
});
|
|
2534
|
+
try {
|
|
2535
|
+
const page = await browser.newPage();
|
|
2536
|
+
await page.goto(url, { waitUntil: "load" });
|
|
2537
|
+
return await fn(page, browser);
|
|
2538
|
+
} finally {
|
|
2539
|
+
await browser.close();
|
|
2540
|
+
}
|
|
2541
|
+
}
|
|
2542
|
+
async function cmdDownload(rest) {
|
|
2543
|
+
const latest = hasFlag(rest, "--latest");
|
|
2544
|
+
const mirrorFlag = flagValue(rest, "--mirror");
|
|
2545
|
+
if (mirrorFlag) process.env.CDPWRIGHT_DOWNLOAD_MIRROR = mirrorFlag;
|
|
2546
|
+
const urlFlag = flagValue(rest, "--url");
|
|
2547
|
+
if (urlFlag) process.env.CDPWRIGHT_DOWNLOAD_URL = urlFlag;
|
|
2548
|
+
const manager = new ChromiumManager();
|
|
2549
|
+
await manager.download({ latest });
|
|
2550
|
+
}
|
|
2551
|
+
async function cmdOpen(rest) {
|
|
2552
|
+
const url = positionalArgs(rest)[0];
|
|
2553
|
+
if (!url) {
|
|
2554
|
+
console.error("Usage: cpw open <url>");
|
|
2555
|
+
process.exit(1);
|
|
2556
|
+
}
|
|
2557
|
+
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
|
+
const browser = await automaton.launch({
|
|
2563
|
+
headless,
|
|
2564
|
+
args,
|
|
2565
|
+
logLevel: "warn"
|
|
2566
|
+
});
|
|
2567
|
+
const page = await browser.newPage();
|
|
2568
|
+
await page.goto(url, { waitUntil: "load" });
|
|
2569
|
+
console.log(`Browser open at ${url}`);
|
|
2570
|
+
console.log("Press Ctrl+C to close.");
|
|
2571
|
+
await new Promise((resolve) => {
|
|
2572
|
+
process.on("SIGINT", () => {
|
|
2573
|
+
console.log("\nClosing browser...");
|
|
2574
|
+
resolve();
|
|
2575
|
+
});
|
|
2576
|
+
process.on("SIGTERM", () => resolve());
|
|
2577
|
+
});
|
|
2578
|
+
try {
|
|
2579
|
+
await browser.close();
|
|
2580
|
+
} catch {
|
|
2581
|
+
}
|
|
2582
|
+
}
|
|
2583
|
+
async function cmdScreenshot(rest) {
|
|
2584
|
+
const url = positionalArgs(rest)[0];
|
|
2585
|
+
const output = flagValue(rest, "-o") || flagValue(rest, "--output");
|
|
2586
|
+
if (!url || !output) {
|
|
2587
|
+
console.error("Usage: cpw screenshot <url> -o <file> [--full-page]");
|
|
2588
|
+
process.exit(1);
|
|
2589
|
+
}
|
|
2590
|
+
const fullPage = hasFlag(rest, "--full-page");
|
|
2591
|
+
const headless = resolveHeadless(rest, true);
|
|
2592
|
+
const format = output.endsWith(".jpeg") || output.endsWith(".jpg") ? "jpeg" : "png";
|
|
2593
|
+
await withPage(url, { headless }, async (page) => {
|
|
2594
|
+
await page.screenshot({ path: output, format, fullPage });
|
|
2595
|
+
});
|
|
2596
|
+
console.log(`Screenshot saved to ${output}`);
|
|
2597
|
+
}
|
|
2598
|
+
async function cmdPdf(rest) {
|
|
2599
|
+
const url = positionalArgs(rest)[0];
|
|
2600
|
+
const output = flagValue(rest, "-o") || flagValue(rest, "--output");
|
|
2601
|
+
if (!url || !output) {
|
|
2602
|
+
console.error("Usage: cpw pdf <url> -o <file>");
|
|
2603
|
+
process.exit(1);
|
|
2604
|
+
}
|
|
2605
|
+
await withPage(url, { headless: true }, async (page) => {
|
|
2606
|
+
await page.pdf({ path: output, printBackground: true });
|
|
2607
|
+
});
|
|
2608
|
+
console.log(`PDF saved to ${output}`);
|
|
2609
|
+
}
|
|
2610
|
+
async function cmdEval(rest) {
|
|
2611
|
+
const pos = positionalArgs(rest);
|
|
2612
|
+
const url = pos[0];
|
|
2613
|
+
const script = pos[1];
|
|
2614
|
+
if (!url || !script) {
|
|
2615
|
+
console.error("Usage: cpw eval <url> <script>");
|
|
2616
|
+
process.exit(1);
|
|
2617
|
+
}
|
|
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
|
+
}
|
|
2625
|
+
async function cmdVersion() {
|
|
2626
|
+
const pkgPath = path5.resolve(path5.dirname(fileURLToPath(import.meta.url)), "../package.json");
|
|
2627
|
+
let version = "unknown";
|
|
2628
|
+
try {
|
|
2629
|
+
const pkg = JSON.parse(fs5.readFileSync(pkgPath, "utf-8"));
|
|
2630
|
+
version = pkg.version;
|
|
2631
|
+
} catch {
|
|
2632
|
+
}
|
|
2633
|
+
console.log(`cdpwright v${version}`);
|
|
2634
|
+
console.log(`Pinned revision: ${PINNED_REVISION}`);
|
|
2635
|
+
const platform = detectPlatform();
|
|
2636
|
+
const cacheRoot = defaultCacheRoot(platform);
|
|
2637
|
+
const revision = resolveRevision(process.env.CDPWRIGHT_REVISION);
|
|
2638
|
+
const execPath = path5.join(cacheRoot, platform, revision, chromiumExecutableRelativePath(platform));
|
|
2639
|
+
if (fs5.existsSync(execPath)) {
|
|
2640
|
+
try {
|
|
2641
|
+
const ver = await chromiumVersion(execPath);
|
|
2642
|
+
console.log(`Chromium: ${ver}`);
|
|
2643
|
+
} catch {
|
|
2644
|
+
console.log(`Chromium: installed at ${execPath}`);
|
|
2645
|
+
}
|
|
2646
|
+
} else {
|
|
2647
|
+
console.log("Chromium: not installed (run 'cpw install')");
|
|
2648
|
+
}
|
|
2141
2649
|
}
|
|
2142
2650
|
async function main() {
|
|
2143
2651
|
const [, , command, ...rest] = process.argv;
|
|
@@ -2145,14 +2653,27 @@ async function main() {
|
|
|
2145
2653
|
printHelp();
|
|
2146
2654
|
process.exit(0);
|
|
2147
2655
|
}
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2656
|
+
switch (command) {
|
|
2657
|
+
case "download":
|
|
2658
|
+
case "install":
|
|
2659
|
+
return cmdDownload(rest);
|
|
2660
|
+
case "open":
|
|
2661
|
+
return cmdOpen(rest);
|
|
2662
|
+
case "screenshot":
|
|
2663
|
+
return cmdScreenshot(rest);
|
|
2664
|
+
case "pdf":
|
|
2665
|
+
return cmdPdf(rest);
|
|
2666
|
+
case "eval":
|
|
2667
|
+
return cmdEval(rest);
|
|
2668
|
+
case "version":
|
|
2669
|
+
case "--version":
|
|
2670
|
+
case "-v":
|
|
2671
|
+
return cmdVersion();
|
|
2672
|
+
default:
|
|
2673
|
+
console.error(`Unknown command: ${command}`);
|
|
2674
|
+
printHelp();
|
|
2675
|
+
process.exit(1);
|
|
2152
2676
|
}
|
|
2153
|
-
const latest = rest.includes("--latest");
|
|
2154
|
-
const manager = new ChromiumManager();
|
|
2155
|
-
await manager.download({ latest });
|
|
2156
2677
|
}
|
|
2157
2678
|
main().catch((err) => {
|
|
2158
2679
|
console.error(err instanceof Error ? err.message : String(err));
|