@toolstackhq/cdpwright 1.0.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 +62 -0
- package/dist/assert/expect.d.ts +1 -0
- package/dist/assert/expect.js +7 -0
- package/dist/assert/expect.js.map +1 -0
- package/dist/chunk-6BPF3IEU.js +1572 -0
- package/dist/chunk-6BPF3IEU.js.map +1 -0
- package/dist/cli.js +2161 -0
- package/dist/cli.js.map +1 -0
- package/dist/expect-CY70zJc0.d.ts +318 -0
- package/dist/index.d.ts +64 -0
- package/dist/index.js +878 -0
- package/dist/index.js.map +1 -0
- package/package.json +62 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,878 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AssertionError,
|
|
3
|
+
Frame,
|
|
4
|
+
Locator,
|
|
5
|
+
Page,
|
|
6
|
+
expect
|
|
7
|
+
} from "./chunk-6BPF3IEU.js";
|
|
8
|
+
|
|
9
|
+
// src/browser/ChromiumManager.ts
|
|
10
|
+
import fs2 from "fs";
|
|
11
|
+
import path2 from "path";
|
|
12
|
+
import os2 from "os";
|
|
13
|
+
import http from "http";
|
|
14
|
+
import { spawn as spawn2 } from "child_process";
|
|
15
|
+
|
|
16
|
+
// src/logging/Logger.ts
|
|
17
|
+
var LEVEL_ORDER = {
|
|
18
|
+
error: 0,
|
|
19
|
+
warn: 1,
|
|
20
|
+
info: 2,
|
|
21
|
+
debug: 3,
|
|
22
|
+
trace: 4
|
|
23
|
+
};
|
|
24
|
+
var REDACT_KEYS = ["password", "token", "secret", "authorization", "cookie"];
|
|
25
|
+
function redactValue(value) {
|
|
26
|
+
if (typeof value === "string") {
|
|
27
|
+
try {
|
|
28
|
+
const url = new URL(value);
|
|
29
|
+
if (url.search) {
|
|
30
|
+
url.search = "?redacted";
|
|
31
|
+
}
|
|
32
|
+
return url.toString();
|
|
33
|
+
} catch {
|
|
34
|
+
return value;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
if (value && typeof value === "object") {
|
|
38
|
+
return JSON.stringify(value, (key, val) => {
|
|
39
|
+
if (REDACT_KEYS.some((k) => key.toLowerCase().includes(k))) {
|
|
40
|
+
return "[redacted]";
|
|
41
|
+
}
|
|
42
|
+
return val;
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
return String(value);
|
|
46
|
+
}
|
|
47
|
+
var Logger = class {
|
|
48
|
+
level;
|
|
49
|
+
constructor(level = "info") {
|
|
50
|
+
this.level = level;
|
|
51
|
+
}
|
|
52
|
+
setLevel(level) {
|
|
53
|
+
this.level = level;
|
|
54
|
+
}
|
|
55
|
+
error(message, ...args) {
|
|
56
|
+
this.log("error", message, ...args);
|
|
57
|
+
}
|
|
58
|
+
warn(message, ...args) {
|
|
59
|
+
this.log("warn", message, ...args);
|
|
60
|
+
}
|
|
61
|
+
info(message, ...args) {
|
|
62
|
+
this.log("info", message, ...args);
|
|
63
|
+
}
|
|
64
|
+
debug(message, ...args) {
|
|
65
|
+
this.log("debug", message, ...args);
|
|
66
|
+
}
|
|
67
|
+
trace(message, ...args) {
|
|
68
|
+
this.log("trace", message, ...args);
|
|
69
|
+
}
|
|
70
|
+
log(level, message, ...args) {
|
|
71
|
+
if (LEVEL_ORDER[level] > LEVEL_ORDER[this.level]) {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
const time = (/* @__PURE__ */ new Date()).toISOString();
|
|
75
|
+
const suffix = args.length ? " " + args.map(redactValue).join(" ") : "";
|
|
76
|
+
const line = `[${time}] [${level}] ${message}${suffix}`;
|
|
77
|
+
if (level === "error") {
|
|
78
|
+
console.error(line);
|
|
79
|
+
} else if (level === "warn") {
|
|
80
|
+
console.warn(line);
|
|
81
|
+
} else {
|
|
82
|
+
console.log(line);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
// src/core/Events.ts
|
|
88
|
+
import { EventEmitter } from "events";
|
|
89
|
+
var AutomationEvents = class {
|
|
90
|
+
emitter = new EventEmitter();
|
|
91
|
+
on(event, handler) {
|
|
92
|
+
this.emitter.on(event, handler);
|
|
93
|
+
}
|
|
94
|
+
off(event, handler) {
|
|
95
|
+
this.emitter.off(event, handler);
|
|
96
|
+
}
|
|
97
|
+
emit(event, payload) {
|
|
98
|
+
this.emitter.emit(event, payload);
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
// src/cdp/Connection.ts
|
|
103
|
+
import WebSocket from "ws";
|
|
104
|
+
import { EventEmitter as EventEmitter3 } from "events";
|
|
105
|
+
|
|
106
|
+
// src/cdp/Session.ts
|
|
107
|
+
import { EventEmitter as EventEmitter2 } from "events";
|
|
108
|
+
var Session = class {
|
|
109
|
+
connection;
|
|
110
|
+
sessionId;
|
|
111
|
+
emitter = new EventEmitter2();
|
|
112
|
+
constructor(connection, sessionId) {
|
|
113
|
+
this.connection = connection;
|
|
114
|
+
this.sessionId = sessionId;
|
|
115
|
+
}
|
|
116
|
+
on(event, handler) {
|
|
117
|
+
this.emitter.on(event, handler);
|
|
118
|
+
}
|
|
119
|
+
once(event, handler) {
|
|
120
|
+
this.emitter.once(event, handler);
|
|
121
|
+
}
|
|
122
|
+
async send(method, params = {}) {
|
|
123
|
+
return this.connection.send(method, params, this.sessionId);
|
|
124
|
+
}
|
|
125
|
+
dispatch(method, params) {
|
|
126
|
+
this.emitter.emit(method, params);
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
// src/cdp/Connection.ts
|
|
131
|
+
var Connection = class {
|
|
132
|
+
ws;
|
|
133
|
+
id = 0;
|
|
134
|
+
callbacks = /* @__PURE__ */ new Map();
|
|
135
|
+
sessions = /* @__PURE__ */ new Map();
|
|
136
|
+
emitter = new EventEmitter3();
|
|
137
|
+
logger;
|
|
138
|
+
closed = false;
|
|
139
|
+
constructor(url, logger) {
|
|
140
|
+
this.logger = logger;
|
|
141
|
+
this.ws = new WebSocket(url);
|
|
142
|
+
this.ws.on("message", (data) => this.onMessage(data.toString()));
|
|
143
|
+
this.ws.on("error", (err) => this.onError(err));
|
|
144
|
+
this.ws.on("close", (code, reason) => this.onClose(code, reason));
|
|
145
|
+
}
|
|
146
|
+
async waitForOpen() {
|
|
147
|
+
if (this.ws.readyState === WebSocket.OPEN) {
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
if (this.ws.readyState === WebSocket.CLOSED) {
|
|
151
|
+
throw new Error("CDP socket is closed");
|
|
152
|
+
}
|
|
153
|
+
await new Promise((resolve, reject) => {
|
|
154
|
+
const onOpen = () => {
|
|
155
|
+
cleanup();
|
|
156
|
+
resolve();
|
|
157
|
+
};
|
|
158
|
+
const onError = (err) => {
|
|
159
|
+
cleanup();
|
|
160
|
+
reject(err);
|
|
161
|
+
};
|
|
162
|
+
const onClose = () => {
|
|
163
|
+
cleanup();
|
|
164
|
+
reject(new Error("CDP socket closed before opening"));
|
|
165
|
+
};
|
|
166
|
+
const cleanup = () => {
|
|
167
|
+
this.ws.off("open", onOpen);
|
|
168
|
+
this.ws.off("error", onError);
|
|
169
|
+
this.ws.off("close", onClose);
|
|
170
|
+
};
|
|
171
|
+
this.ws.on("open", onOpen);
|
|
172
|
+
this.ws.on("error", onError);
|
|
173
|
+
this.ws.on("close", onClose);
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
createSession(sessionId) {
|
|
177
|
+
const session = new Session(this, sessionId);
|
|
178
|
+
this.sessions.set(sessionId, session);
|
|
179
|
+
return session;
|
|
180
|
+
}
|
|
181
|
+
removeSession(sessionId) {
|
|
182
|
+
this.sessions.delete(sessionId);
|
|
183
|
+
}
|
|
184
|
+
on(event, handler) {
|
|
185
|
+
this.emitter.on(event, handler);
|
|
186
|
+
}
|
|
187
|
+
async send(method, params = {}, sessionId) {
|
|
188
|
+
await this.waitForOpen();
|
|
189
|
+
const id = ++this.id;
|
|
190
|
+
const payload = sessionId ? { id, method, params, sessionId } : { id, method, params };
|
|
191
|
+
const start = Date.now();
|
|
192
|
+
const promise = new Promise((resolve, reject) => {
|
|
193
|
+
this.callbacks.set(id, { resolve, reject, method, start });
|
|
194
|
+
});
|
|
195
|
+
if (this.logger) {
|
|
196
|
+
this.logger.trace("CDP send", method);
|
|
197
|
+
}
|
|
198
|
+
this.ws.send(JSON.stringify(payload));
|
|
199
|
+
return promise;
|
|
200
|
+
}
|
|
201
|
+
async close() {
|
|
202
|
+
if (this.ws.readyState === WebSocket.CLOSED) {
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
await new Promise((resolve) => {
|
|
206
|
+
this.ws.once("close", () => resolve());
|
|
207
|
+
if (this.ws.readyState === WebSocket.CLOSING) {
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
this.ws.close();
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
onError(err) {
|
|
214
|
+
this.logger.error("CDP socket error", err);
|
|
215
|
+
}
|
|
216
|
+
onClose(code, reason) {
|
|
217
|
+
if (this.closed) {
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
this.closed = true;
|
|
221
|
+
const reasonText = reason.toString() || "no reason";
|
|
222
|
+
this.failPending(new Error(`CDP socket closed (${code}): ${reasonText}`));
|
|
223
|
+
this.sessions.clear();
|
|
224
|
+
}
|
|
225
|
+
failPending(error) {
|
|
226
|
+
if (this.callbacks.size === 0) {
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
for (const [, callback] of this.callbacks) {
|
|
230
|
+
callback.reject(error);
|
|
231
|
+
}
|
|
232
|
+
this.callbacks.clear();
|
|
233
|
+
}
|
|
234
|
+
onMessage(message) {
|
|
235
|
+
let parsed;
|
|
236
|
+
try {
|
|
237
|
+
parsed = JSON.parse(message);
|
|
238
|
+
} catch (err) {
|
|
239
|
+
this.logger.warn("Failed to parse CDP message", err);
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
if (typeof parsed.id === "number") {
|
|
243
|
+
const callback = this.callbacks.get(parsed.id);
|
|
244
|
+
if (!callback) {
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
this.callbacks.delete(parsed.id);
|
|
248
|
+
const duration = Date.now() - callback.start;
|
|
249
|
+
this.logger.debug("CDP recv", callback.method, `${duration}ms`);
|
|
250
|
+
if (parsed.error) {
|
|
251
|
+
callback.reject(new Error(parsed.error.message));
|
|
252
|
+
} else {
|
|
253
|
+
callback.resolve(parsed.result);
|
|
254
|
+
}
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
if (parsed.sessionId) {
|
|
258
|
+
const session = this.sessions.get(parsed.sessionId);
|
|
259
|
+
if (session) {
|
|
260
|
+
session.dispatch(parsed.method, parsed.params);
|
|
261
|
+
}
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
if (parsed.method) {
|
|
265
|
+
this.emitter.emit(parsed.method, parsed.params);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
// src/core/Browser.ts
|
|
271
|
+
var BrowserContext = class {
|
|
272
|
+
browser;
|
|
273
|
+
id;
|
|
274
|
+
constructor(browser, id) {
|
|
275
|
+
this.browser = browser;
|
|
276
|
+
this.id = id;
|
|
277
|
+
}
|
|
278
|
+
getId() {
|
|
279
|
+
return this.id;
|
|
280
|
+
}
|
|
281
|
+
async newPage() {
|
|
282
|
+
return this.browser.newPage({ browserContextId: this.id });
|
|
283
|
+
}
|
|
284
|
+
async close() {
|
|
285
|
+
await this.browser.disposeContext(this.id);
|
|
286
|
+
}
|
|
287
|
+
};
|
|
288
|
+
var Browser = class {
|
|
289
|
+
connection;
|
|
290
|
+
process;
|
|
291
|
+
logger;
|
|
292
|
+
events;
|
|
293
|
+
cleanupTasks;
|
|
294
|
+
contexts = /* @__PURE__ */ new Set();
|
|
295
|
+
constructor(connection, child, logger, events, cleanupTasks = []) {
|
|
296
|
+
this.connection = connection;
|
|
297
|
+
this.process = child;
|
|
298
|
+
this.logger = logger;
|
|
299
|
+
this.events = events;
|
|
300
|
+
this.cleanupTasks = cleanupTasks;
|
|
301
|
+
}
|
|
302
|
+
on(event, handler) {
|
|
303
|
+
this.events.on(event, handler);
|
|
304
|
+
}
|
|
305
|
+
async newContext() {
|
|
306
|
+
const { browserContextId } = await this.connection.send("Target.createBrowserContext");
|
|
307
|
+
this.contexts.add(browserContextId);
|
|
308
|
+
return new BrowserContext(this, browserContextId);
|
|
309
|
+
}
|
|
310
|
+
async newPage(options = {}) {
|
|
311
|
+
const { browserContextId } = options;
|
|
312
|
+
const { targetId } = await this.connection.send("Target.createTarget", {
|
|
313
|
+
url: "about:blank",
|
|
314
|
+
browserContextId
|
|
315
|
+
});
|
|
316
|
+
const { sessionId } = await this.connection.send("Target.attachToTarget", { targetId, flatten: true });
|
|
317
|
+
const session = this.connection.createSession(sessionId);
|
|
318
|
+
const page = new Page(session, this.logger, this.events);
|
|
319
|
+
await page.initialize();
|
|
320
|
+
return page;
|
|
321
|
+
}
|
|
322
|
+
async disposeContext(contextId) {
|
|
323
|
+
if (!contextId) return;
|
|
324
|
+
try {
|
|
325
|
+
await this.connection.send("Target.disposeBrowserContext", { browserContextId: contextId });
|
|
326
|
+
} catch {
|
|
327
|
+
}
|
|
328
|
+
this.contexts.delete(contextId);
|
|
329
|
+
}
|
|
330
|
+
async close() {
|
|
331
|
+
if (this.contexts.size > 0) {
|
|
332
|
+
for (const contextId of Array.from(this.contexts)) {
|
|
333
|
+
await this.disposeContext(contextId);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
try {
|
|
337
|
+
await this.connection.send("Browser.close");
|
|
338
|
+
} catch {
|
|
339
|
+
}
|
|
340
|
+
await this.connection.close();
|
|
341
|
+
if (!this.process.killed) {
|
|
342
|
+
this.process.kill();
|
|
343
|
+
}
|
|
344
|
+
for (const task of this.cleanupTasks) {
|
|
345
|
+
try {
|
|
346
|
+
task();
|
|
347
|
+
} catch {
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
// src/browser/Downloader.ts
|
|
354
|
+
import fs from "fs";
|
|
355
|
+
import path from "path";
|
|
356
|
+
import os from "os";
|
|
357
|
+
import https from "https";
|
|
358
|
+
import { spawn } from "child_process";
|
|
359
|
+
import yauzl from "yauzl";
|
|
360
|
+
var SNAPSHOT_BASE = "https://commondatastorage.googleapis.com/chromium-browser-snapshots";
|
|
361
|
+
function detectPlatform(platform = process.platform) {
|
|
362
|
+
if (platform === "linux") return "linux";
|
|
363
|
+
if (platform === "darwin") return "mac";
|
|
364
|
+
if (platform === "win32") return "win";
|
|
365
|
+
throw new Error(`Unsupported platform: ${platform}`);
|
|
366
|
+
}
|
|
367
|
+
function platformFolder(platform) {
|
|
368
|
+
if (platform === "linux") return "Linux_x64";
|
|
369
|
+
if (platform === "mac") return "Mac";
|
|
370
|
+
return "Win";
|
|
371
|
+
}
|
|
372
|
+
function defaultCacheRoot(platform) {
|
|
373
|
+
if (platform === "win") {
|
|
374
|
+
const localAppData = process.env.LOCALAPPDATA || path.join(os.homedir(), "AppData", "Local");
|
|
375
|
+
return path.join(localAppData, "cdpwright");
|
|
376
|
+
}
|
|
377
|
+
return path.join(os.homedir(), ".cache", "cdpwright");
|
|
378
|
+
}
|
|
379
|
+
function ensureWithinRoot(root, target) {
|
|
380
|
+
const resolvedRoot = path.resolve(root);
|
|
381
|
+
const resolvedTarget = path.resolve(target);
|
|
382
|
+
if (resolvedTarget === resolvedRoot) {
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
if (!resolvedTarget.startsWith(resolvedRoot + path.sep)) {
|
|
386
|
+
throw new Error(`Path escapes cache root: ${resolvedTarget}`);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
function chromiumExecutableRelativePath(platform) {
|
|
390
|
+
if (platform === "linux") return path.join("chrome-linux", "chrome");
|
|
391
|
+
if (platform === "mac") return path.join("chrome-mac", "Chromium.app", "Contents", "MacOS", "Chromium");
|
|
392
|
+
return path.join("chrome-win", "chrome.exe");
|
|
393
|
+
}
|
|
394
|
+
async function fetchLatestRevision(platform) {
|
|
395
|
+
const folder = platformFolder(platform);
|
|
396
|
+
const url = `${SNAPSHOT_BASE}/${folder}/LAST_CHANGE`;
|
|
397
|
+
return new Promise((resolve, reject) => {
|
|
398
|
+
https.get(url, (res) => {
|
|
399
|
+
if (res.statusCode && res.statusCode >= 400) {
|
|
400
|
+
reject(new Error(`Failed to fetch LAST_CHANGE: ${res.statusCode}`));
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
let data = "";
|
|
404
|
+
res.on("data", (chunk) => data += chunk.toString());
|
|
405
|
+
res.on("end", () => resolve(data.trim()));
|
|
406
|
+
}).on("error", reject);
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
async function ensureDownloaded(options) {
|
|
410
|
+
const { cacheRoot, platform, revision, logger } = options;
|
|
411
|
+
ensureWithinRoot(cacheRoot, cacheRoot);
|
|
412
|
+
const platformDir = path.join(cacheRoot, platform);
|
|
413
|
+
const revisionDir = path.join(platformDir, revision);
|
|
414
|
+
ensureWithinRoot(cacheRoot, revisionDir);
|
|
415
|
+
const executablePath = path.join(revisionDir, chromiumExecutableRelativePath(platform));
|
|
416
|
+
const markerFile = path.join(revisionDir, "INSTALLATION_COMPLETE");
|
|
417
|
+
if (fs.existsSync(executablePath) && fs.existsSync(markerFile)) {
|
|
418
|
+
return { executablePath, revisionDir };
|
|
419
|
+
}
|
|
420
|
+
fs.mkdirSync(revisionDir, { recursive: true });
|
|
421
|
+
const folder = platformFolder(platform);
|
|
422
|
+
const zipName = platform === "win" ? "chrome-win.zip" : platform === "mac" ? "chrome-mac.zip" : "chrome-linux.zip";
|
|
423
|
+
const downloadUrl = `${SNAPSHOT_BASE}/${folder}/${revision}/${zipName}`;
|
|
424
|
+
const tempZipPath = path.join(os.tmpdir(), `cdpwright-${platform}-${revision}.zip`);
|
|
425
|
+
logger.info("Downloading Chromium snapshot", downloadUrl);
|
|
426
|
+
await downloadFile(downloadUrl, tempZipPath, logger);
|
|
427
|
+
logger.info("Extracting Chromium snapshot", tempZipPath);
|
|
428
|
+
await extractZipSafe(tempZipPath, revisionDir);
|
|
429
|
+
fs.writeFileSync(markerFile, (/* @__PURE__ */ new Date()).toISOString());
|
|
430
|
+
fs.unlinkSync(tempZipPath);
|
|
431
|
+
if (!fs.existsSync(executablePath)) {
|
|
432
|
+
throw new Error(`Executable not found after extraction: ${executablePath}`);
|
|
433
|
+
}
|
|
434
|
+
ensureExecutable(executablePath, platform);
|
|
435
|
+
return { executablePath, revisionDir };
|
|
436
|
+
}
|
|
437
|
+
function downloadFile(url, dest, logger) {
|
|
438
|
+
return new Promise((resolve, reject) => {
|
|
439
|
+
const file = fs.createWriteStream(dest);
|
|
440
|
+
https.get(url, (res) => {
|
|
441
|
+
if (res.statusCode && res.statusCode >= 400) {
|
|
442
|
+
reject(new Error(`Failed to download: ${res.statusCode}`));
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
const total = Number(res.headers["content-length"] || 0);
|
|
446
|
+
let downloaded = 0;
|
|
447
|
+
let lastLoggedPercent = -1;
|
|
448
|
+
let lastLoggedTime = Date.now();
|
|
449
|
+
res.pipe(file);
|
|
450
|
+
res.on("data", (chunk) => {
|
|
451
|
+
downloaded += chunk.length;
|
|
452
|
+
if (!total) {
|
|
453
|
+
const now = Date.now();
|
|
454
|
+
if (now - lastLoggedTime > 2e3) {
|
|
455
|
+
logger.info("Download progress", `${(downloaded / (1024 * 1024)).toFixed(1)} MB`);
|
|
456
|
+
lastLoggedTime = now;
|
|
457
|
+
}
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
const percent = Math.floor(downloaded / total * 100);
|
|
461
|
+
if (percent >= lastLoggedPercent + 5) {
|
|
462
|
+
logger.info("Download progress", `${percent}%`);
|
|
463
|
+
lastLoggedPercent = percent;
|
|
464
|
+
}
|
|
465
|
+
});
|
|
466
|
+
file.on("finish", () => file.close(() => resolve()));
|
|
467
|
+
}).on("error", (err) => {
|
|
468
|
+
fs.unlink(dest, () => reject(err));
|
|
469
|
+
});
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
async function extractZipSafe(zipPath, destDir) {
|
|
473
|
+
return new Promise((resolve, reject) => {
|
|
474
|
+
yauzl.open(zipPath, { lazyEntries: true }, (err, zipfile) => {
|
|
475
|
+
if (err || !zipfile) {
|
|
476
|
+
reject(err || new Error("Unable to open zip"));
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
zipfile.readEntry();
|
|
480
|
+
zipfile.on("entry", (entry) => {
|
|
481
|
+
const entryPath = entry.fileName.replace(/\\/g, "/");
|
|
482
|
+
const targetPath = path.join(destDir, entryPath);
|
|
483
|
+
try {
|
|
484
|
+
ensureWithinRoot(destDir, targetPath);
|
|
485
|
+
} catch (error) {
|
|
486
|
+
zipfile.close();
|
|
487
|
+
reject(error);
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
if (/\/$/.test(entry.fileName)) {
|
|
491
|
+
fs.mkdirSync(targetPath, { recursive: true });
|
|
492
|
+
zipfile.readEntry();
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
|
|
496
|
+
zipfile.openReadStream(entry, (streamErr, readStream) => {
|
|
497
|
+
if (streamErr || !readStream) {
|
|
498
|
+
zipfile.close();
|
|
499
|
+
reject(streamErr || new Error("Unable to read zip entry"));
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
const rawMode = entry.externalFileAttributes ? entry.externalFileAttributes >>> 16 & 65535 : 0;
|
|
503
|
+
const mode = rawMode > 0 ? rawMode : void 0;
|
|
504
|
+
const writeStream = fs.createWriteStream(targetPath);
|
|
505
|
+
readStream.pipe(writeStream);
|
|
506
|
+
writeStream.on("error", (writeErr) => {
|
|
507
|
+
zipfile.close();
|
|
508
|
+
reject(writeErr);
|
|
509
|
+
});
|
|
510
|
+
writeStream.on("close", () => {
|
|
511
|
+
if (mode && mode <= 511) {
|
|
512
|
+
try {
|
|
513
|
+
fs.chmodSync(targetPath, mode);
|
|
514
|
+
} catch {
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
zipfile.readEntry();
|
|
518
|
+
});
|
|
519
|
+
});
|
|
520
|
+
});
|
|
521
|
+
zipfile.on("end", () => {
|
|
522
|
+
zipfile.close();
|
|
523
|
+
resolve();
|
|
524
|
+
});
|
|
525
|
+
zipfile.on("error", (zipErr) => {
|
|
526
|
+
zipfile.close();
|
|
527
|
+
reject(zipErr);
|
|
528
|
+
});
|
|
529
|
+
});
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
async function chromiumVersion(executablePath) {
|
|
533
|
+
return new Promise((resolve, reject) => {
|
|
534
|
+
const child = spawn(executablePath, ["--version"], { stdio: ["ignore", "pipe", "pipe"] });
|
|
535
|
+
let output = "";
|
|
536
|
+
child.stdout.on("data", (chunk) => output += chunk.toString());
|
|
537
|
+
child.on("close", (code) => {
|
|
538
|
+
if (code === 0) {
|
|
539
|
+
resolve(output.trim());
|
|
540
|
+
} else {
|
|
541
|
+
reject(new Error(`Failed to get Chromium version: ${code}`));
|
|
542
|
+
}
|
|
543
|
+
});
|
|
544
|
+
child.on("error", reject);
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
function ensureExecutable(executablePath, platform) {
|
|
548
|
+
if (platform === "win") {
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
551
|
+
try {
|
|
552
|
+
const stat = fs.statSync(executablePath);
|
|
553
|
+
const isExecutable = (stat.mode & 73) !== 0;
|
|
554
|
+
if (!isExecutable) {
|
|
555
|
+
fs.chmodSync(executablePath, 493);
|
|
556
|
+
}
|
|
557
|
+
} catch {
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// src/browser/Revision.ts
|
|
562
|
+
var PINNED_REVISION = "1567454";
|
|
563
|
+
function resolveRevision(envRevision) {
|
|
564
|
+
if (envRevision && envRevision.trim()) {
|
|
565
|
+
return envRevision.trim();
|
|
566
|
+
}
|
|
567
|
+
return PINNED_REVISION;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// src/browser/ChromiumManager.ts
|
|
571
|
+
var ChromiumManager = class {
|
|
572
|
+
logger;
|
|
573
|
+
constructor(logger) {
|
|
574
|
+
const envLevel = process.env.CDPWRIGHT_LOG_LEVEL ?? "info";
|
|
575
|
+
this.logger = logger ?? new Logger(envLevel);
|
|
576
|
+
}
|
|
577
|
+
getLogger() {
|
|
578
|
+
return this.logger;
|
|
579
|
+
}
|
|
580
|
+
async download(options = {}) {
|
|
581
|
+
const platform = detectPlatform();
|
|
582
|
+
const cacheRoot = this.resolveCacheRoot(platform);
|
|
583
|
+
const overrideExecutable = process.env.CDPWRIGHT_EXECUTABLE_PATH;
|
|
584
|
+
let revision = options.latest ? await fetchLatestRevision(platform) : resolveRevision(process.env.CDPWRIGHT_REVISION);
|
|
585
|
+
let executablePath;
|
|
586
|
+
let revisionDir = "";
|
|
587
|
+
if (overrideExecutable) {
|
|
588
|
+
executablePath = path2.resolve(overrideExecutable);
|
|
589
|
+
} else {
|
|
590
|
+
const downloaded = await ensureDownloaded({
|
|
591
|
+
cacheRoot,
|
|
592
|
+
platform,
|
|
593
|
+
revision,
|
|
594
|
+
logger: this.logger
|
|
595
|
+
});
|
|
596
|
+
executablePath = downloaded.executablePath;
|
|
597
|
+
revisionDir = downloaded.revisionDir;
|
|
598
|
+
}
|
|
599
|
+
if (!fs2.existsSync(executablePath)) {
|
|
600
|
+
throw new Error(`Chromium executable not found: ${executablePath}`);
|
|
601
|
+
}
|
|
602
|
+
const version = await chromiumVersion(executablePath);
|
|
603
|
+
this.logger.info("Chromium cache root", cacheRoot);
|
|
604
|
+
this.logger.info("Platform", platform);
|
|
605
|
+
this.logger.info("Revision", revision);
|
|
606
|
+
this.logger.info("Chromium version", version);
|
|
607
|
+
return {
|
|
608
|
+
cacheRoot,
|
|
609
|
+
platform,
|
|
610
|
+
revision,
|
|
611
|
+
executablePath,
|
|
612
|
+
revisionDir,
|
|
613
|
+
chromiumVersion: version
|
|
614
|
+
};
|
|
615
|
+
}
|
|
616
|
+
async launch(options = {}) {
|
|
617
|
+
const logger = this.logger;
|
|
618
|
+
if (options.logLevel) {
|
|
619
|
+
logger.setLevel(options.logLevel);
|
|
620
|
+
}
|
|
621
|
+
const executablePath = options.executablePath || process.env.CDPWRIGHT_EXECUTABLE_PATH;
|
|
622
|
+
let resolvedExecutable = executablePath;
|
|
623
|
+
if (!resolvedExecutable) {
|
|
624
|
+
const platform = detectPlatform();
|
|
625
|
+
const cacheRoot = this.resolveCacheRoot(platform);
|
|
626
|
+
const revision = resolveRevision(process.env.CDPWRIGHT_REVISION);
|
|
627
|
+
const downloaded = await ensureDownloaded({
|
|
628
|
+
cacheRoot,
|
|
629
|
+
platform,
|
|
630
|
+
revision,
|
|
631
|
+
logger
|
|
632
|
+
});
|
|
633
|
+
resolvedExecutable = downloaded.executablePath;
|
|
634
|
+
}
|
|
635
|
+
if (!resolvedExecutable || !fs2.existsSync(resolvedExecutable)) {
|
|
636
|
+
throw new Error(`Chromium executable not found: ${resolvedExecutable}`);
|
|
637
|
+
}
|
|
638
|
+
const stats = fs2.statSync(resolvedExecutable);
|
|
639
|
+
if (!stats.isFile()) {
|
|
640
|
+
throw new Error(`Chromium executable is not a file: ${resolvedExecutable}`);
|
|
641
|
+
}
|
|
642
|
+
ensureExecutable2(resolvedExecutable);
|
|
643
|
+
const cleanupTasks = [];
|
|
644
|
+
let userDataDir = options.userDataDir ?? process.env.CDPWRIGHT_USER_DATA_DIR;
|
|
645
|
+
if (!userDataDir) {
|
|
646
|
+
userDataDir = fs2.mkdtempSync(path2.join(os2.tmpdir(), "cdpwright-"));
|
|
647
|
+
cleanupTasks.push(() => fs2.rmSync(userDataDir, { recursive: true, force: true }));
|
|
648
|
+
}
|
|
649
|
+
const args = [
|
|
650
|
+
"--remote-debugging-port=0",
|
|
651
|
+
"--no-first-run",
|
|
652
|
+
"--no-default-browser-check",
|
|
653
|
+
"--disable-background-networking",
|
|
654
|
+
"--disable-background-timer-throttling",
|
|
655
|
+
"--disable-backgrounding-occluded-windows",
|
|
656
|
+
"--disable-renderer-backgrounding"
|
|
657
|
+
];
|
|
658
|
+
if (userDataDir) {
|
|
659
|
+
args.push(`--user-data-dir=${userDataDir}`);
|
|
660
|
+
}
|
|
661
|
+
if (process.platform === "linux") {
|
|
662
|
+
args.push("--disable-crash-reporter", "--disable-crashpad");
|
|
663
|
+
}
|
|
664
|
+
if (options.headless ?? true) {
|
|
665
|
+
args.push("--headless=new");
|
|
666
|
+
}
|
|
667
|
+
if (options.maximize) {
|
|
668
|
+
args.push("--start-maximized");
|
|
669
|
+
}
|
|
670
|
+
if (options.args) {
|
|
671
|
+
args.push(...options.args);
|
|
672
|
+
}
|
|
673
|
+
logger.info("Launching Chromium", resolvedExecutable);
|
|
674
|
+
const child = spawn2(resolvedExecutable, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
675
|
+
const websocketUrl = await waitForWebSocketEndpoint(child, logger, options.timeoutMs ?? 3e4);
|
|
676
|
+
const httpUrl = toHttpVersionUrl(websocketUrl);
|
|
677
|
+
const wsEndpoint = await fetchWebSocketDebuggerUrl(httpUrl);
|
|
678
|
+
const connection = new Connection(wsEndpoint, logger);
|
|
679
|
+
await closeInitialPages(connection, logger);
|
|
680
|
+
const events = new AutomationEvents();
|
|
681
|
+
const logEvents = resolveLogFlag(options.logEvents, process.env.CDPWRIGHT_LOG, true);
|
|
682
|
+
const logActions = resolveLogFlag(options.logActions, process.env.CDPWRIGHT_LOG_ACTIONS, true);
|
|
683
|
+
const logAssertions = resolveLogFlag(options.logAssertions, process.env.CDPWRIGHT_LOG_ASSERTIONS, true);
|
|
684
|
+
if (logEvents && logActions) {
|
|
685
|
+
events.on("action:end", (payload) => {
|
|
686
|
+
const selector = payload.sensitive ? void 0 : payload.selector;
|
|
687
|
+
const args2 = buildLogArgs(selector, payload.durationMs);
|
|
688
|
+
logger.info(`Action ${payload.name}`, ...args2);
|
|
689
|
+
});
|
|
690
|
+
}
|
|
691
|
+
if (logEvents && logAssertions) {
|
|
692
|
+
events.on("assertion:end", (payload) => {
|
|
693
|
+
const args2 = buildLogArgs(payload.selector, payload.durationMs);
|
|
694
|
+
logger.info(`Assertion ${payload.name}`, ...args2);
|
|
695
|
+
});
|
|
696
|
+
}
|
|
697
|
+
const browser = new Browser(connection, child, logger, events, cleanupTasks);
|
|
698
|
+
return browser;
|
|
699
|
+
}
|
|
700
|
+
resolveCacheRoot(platform) {
|
|
701
|
+
const envRoot = process.env.CDPWRIGHT_CACHE_DIR;
|
|
702
|
+
if (envRoot && envRoot.trim()) {
|
|
703
|
+
return path2.resolve(envRoot.trim());
|
|
704
|
+
}
|
|
705
|
+
return defaultCacheRoot(platform);
|
|
706
|
+
}
|
|
707
|
+
};
|
|
708
|
+
async function closeInitialPages(connection, logger) {
|
|
709
|
+
try {
|
|
710
|
+
const targets = await connection.send("Target.getTargets");
|
|
711
|
+
for (const info of targets.targetInfos) {
|
|
712
|
+
if (info.type === "page") {
|
|
713
|
+
await connection.send("Target.closeTarget", { targetId: info.targetId });
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
} catch (err) {
|
|
717
|
+
logger.warn("Failed to close initial pages", err);
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
function resolveLogFlag(explicit, envValue, defaultValue) {
|
|
721
|
+
if (explicit !== void 0) {
|
|
722
|
+
return explicit;
|
|
723
|
+
}
|
|
724
|
+
if (envValue == null) {
|
|
725
|
+
return defaultValue;
|
|
726
|
+
}
|
|
727
|
+
const normalized = envValue.trim().toLowerCase();
|
|
728
|
+
return !["0", "false", "no", "off"].includes(normalized);
|
|
729
|
+
}
|
|
730
|
+
function buildLogArgs(selector, durationMs) {
|
|
731
|
+
const args = [];
|
|
732
|
+
if (selector) {
|
|
733
|
+
args.push(selector);
|
|
734
|
+
}
|
|
735
|
+
if (typeof durationMs === "number") {
|
|
736
|
+
args.push(`${durationMs}ms`);
|
|
737
|
+
}
|
|
738
|
+
return args;
|
|
739
|
+
}
|
|
740
|
+
function ensureExecutable2(executablePath) {
|
|
741
|
+
if (process.platform === "win32") {
|
|
742
|
+
return;
|
|
743
|
+
}
|
|
744
|
+
try {
|
|
745
|
+
const stat = fs2.statSync(executablePath);
|
|
746
|
+
const isExecutable = (stat.mode & 73) !== 0;
|
|
747
|
+
if (!isExecutable) {
|
|
748
|
+
fs2.chmodSync(executablePath, 493);
|
|
749
|
+
}
|
|
750
|
+
} catch {
|
|
751
|
+
}
|
|
752
|
+
const dir = path2.dirname(executablePath);
|
|
753
|
+
const helpers = [
|
|
754
|
+
"chrome_crashpad_handler",
|
|
755
|
+
"chrome_sandbox",
|
|
756
|
+
"chrome-wrapper",
|
|
757
|
+
"xdg-mime",
|
|
758
|
+
"xdg-settings"
|
|
759
|
+
];
|
|
760
|
+
for (const name of helpers) {
|
|
761
|
+
const helperPath = path2.join(dir, name);
|
|
762
|
+
if (!fs2.existsSync(helperPath)) {
|
|
763
|
+
continue;
|
|
764
|
+
}
|
|
765
|
+
try {
|
|
766
|
+
const stat = fs2.statSync(helperPath);
|
|
767
|
+
const isExecutable = (stat.mode & 73) !== 0;
|
|
768
|
+
if (!isExecutable) {
|
|
769
|
+
fs2.chmodSync(helperPath, 493);
|
|
770
|
+
}
|
|
771
|
+
} catch {
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
function waitForWebSocketEndpoint(child, logger, timeoutMs) {
|
|
776
|
+
return new Promise((resolve, reject) => {
|
|
777
|
+
const start = Date.now();
|
|
778
|
+
const timeout = setTimeout(() => {
|
|
779
|
+
reject(new Error("Timed out waiting for DevTools endpoint"));
|
|
780
|
+
}, timeoutMs);
|
|
781
|
+
const outputLines = [];
|
|
782
|
+
const pushOutput = (data) => {
|
|
783
|
+
const text = data.toString();
|
|
784
|
+
for (const line of text.split(/\r?\n/)) {
|
|
785
|
+
if (!line.trim()) continue;
|
|
786
|
+
outputLines.push(line);
|
|
787
|
+
if (outputLines.length > 50) {
|
|
788
|
+
outputLines.shift();
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
};
|
|
792
|
+
const onData = (data) => {
|
|
793
|
+
const text = data.toString();
|
|
794
|
+
const match = text.match(/DevTools listening on (ws:\/\/[^\s]+)/);
|
|
795
|
+
if (match) {
|
|
796
|
+
clearTimeout(timeout);
|
|
797
|
+
cleanup();
|
|
798
|
+
logger.info("DevTools endpoint", match[1]);
|
|
799
|
+
resolve(match[1]);
|
|
800
|
+
}
|
|
801
|
+
pushOutput(data);
|
|
802
|
+
};
|
|
803
|
+
const onExit = (code, signal) => {
|
|
804
|
+
cleanup();
|
|
805
|
+
const tail = outputLines.length ? `
|
|
806
|
+
Chromium output:
|
|
807
|
+
${outputLines.join("\n")}` : "";
|
|
808
|
+
reject(new Error(`Chromium exited early with code ${code ?? "null"} signal ${signal ?? "null"}${tail}`));
|
|
809
|
+
};
|
|
810
|
+
const cleanup = () => {
|
|
811
|
+
child.stdout?.off("data", onData);
|
|
812
|
+
child.stderr?.off("data", onData);
|
|
813
|
+
child.off("exit", onExit);
|
|
814
|
+
};
|
|
815
|
+
child.stdout?.on("data", onData);
|
|
816
|
+
child.stderr?.on("data", onData);
|
|
817
|
+
child.on("exit", onExit);
|
|
818
|
+
if (Date.now() - start > timeoutMs) {
|
|
819
|
+
cleanup();
|
|
820
|
+
}
|
|
821
|
+
});
|
|
822
|
+
}
|
|
823
|
+
function toHttpVersionUrl(wsUrl) {
|
|
824
|
+
try {
|
|
825
|
+
const url = new URL(wsUrl);
|
|
826
|
+
const port = url.port || "9222";
|
|
827
|
+
return `http://127.0.0.1:${port}/json/version`;
|
|
828
|
+
} catch {
|
|
829
|
+
throw new Error(`Invalid DevTools endpoint: ${wsUrl}`);
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
function fetchWebSocketDebuggerUrl(versionUrl) {
|
|
833
|
+
return new Promise((resolve, reject) => {
|
|
834
|
+
http.get(versionUrl, (res) => {
|
|
835
|
+
if (res.statusCode && res.statusCode >= 400) {
|
|
836
|
+
reject(new Error(`Failed to fetch /json/version: ${res.statusCode}`));
|
|
837
|
+
return;
|
|
838
|
+
}
|
|
839
|
+
let data = "";
|
|
840
|
+
res.on("data", (chunk) => data += chunk.toString());
|
|
841
|
+
res.on("end", () => {
|
|
842
|
+
try {
|
|
843
|
+
const parsed = JSON.parse(data);
|
|
844
|
+
if (!parsed.webSocketDebuggerUrl) {
|
|
845
|
+
reject(new Error("webSocketDebuggerUrl missing"));
|
|
846
|
+
return;
|
|
847
|
+
}
|
|
848
|
+
resolve(parsed.webSocketDebuggerUrl);
|
|
849
|
+
} catch (err) {
|
|
850
|
+
reject(err);
|
|
851
|
+
}
|
|
852
|
+
});
|
|
853
|
+
}).on("error", reject);
|
|
854
|
+
});
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
// src/index.ts
|
|
858
|
+
var automaton = {
|
|
859
|
+
async launch(options = {}) {
|
|
860
|
+
const manager = new ChromiumManager(options.logger);
|
|
861
|
+
return manager.launch(options);
|
|
862
|
+
}
|
|
863
|
+
};
|
|
864
|
+
var chromium = automaton;
|
|
865
|
+
export {
|
|
866
|
+
AssertionError,
|
|
867
|
+
AutomationEvents,
|
|
868
|
+
Browser,
|
|
869
|
+
BrowserContext,
|
|
870
|
+
Frame,
|
|
871
|
+
Locator,
|
|
872
|
+
Logger,
|
|
873
|
+
Page,
|
|
874
|
+
automaton,
|
|
875
|
+
chromium,
|
|
876
|
+
expect
|
|
877
|
+
};
|
|
878
|
+
//# sourceMappingURL=index.js.map
|