ScuttleBot

1
"use strict";
2
var __defProp = Object.defineProperty;
3
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
var __getOwnPropNames = Object.getOwnPropertyNames;
5
var __hasOwnProp = Object.prototype.hasOwnProperty;
6
var __export = (target, all) => {
7
for (var name in all)
8
__defProp(target, name, { get: all[name], enumerable: true });
9
};
10
var __copyProps = (to, from, except, desc) => {
11
if (from && typeof from === "object" || typeof from === "function") {
12
for (let key of __getOwnPropNames(from))
13
if (!__hasOwnProp.call(to, key) && key !== except)
14
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
}
16
return to;
17
};
18
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
var tab_exports = {};
20
__export(tab_exports, {
21
Tab: () => Tab,
22
TabEvents: () => TabEvents,
23
renderModalStates: () => renderModalStates,
24
shouldIncludeMessage: () => shouldIncludeMessage
25
});
26
module.exports = __toCommonJS(tab_exports);
27
var import_events = require("events");
28
var import_utils = require("playwright-core/lib/utils");
29
var import_utils2 = require("./tools/utils");
30
var import_log = require("../log");
31
var import_dialogs = require("./tools/dialogs");
32
var import_files = require("./tools/files");
33
var import_transform = require("../../transform/transform");
34
const TabEvents = {
35
modalState: "modalState"
36
};
37
class Tab extends import_events.EventEmitter {
38
constructor(context, page, onPageClose) {
39
super();
40
this._lastHeader = { title: "about:blank", url: "about:blank", current: false };
41
this._consoleMessages = [];
42
this._downloads = [];
43
this._requests = /* @__PURE__ */ new Set();
44
this._modalStates = [];
45
this._needsFullSnapshot = false;
46
this._eventEntries = [];
47
this._recentEventEntries = [];
48
this.context = context;
49
this.page = page;
50
this._onPageClose = onPageClose;
51
page.on("console", (event) => this._handleConsoleMessage(messageToConsoleMessage(event)));
52
page.on("pageerror", (error) => this._handleConsoleMessage(pageErrorToConsoleMessage(error)));
53
page.on("request", (request) => this._handleRequest(request));
54
page.on("close", () => this._onClose());
55
page.on("filechooser", (chooser) => {
56
this.setModalState({
57
type: "fileChooser",
58
description: "File chooser",
59
fileChooser: chooser,
60
clearedBy: import_files.uploadFile.schema.name
61
});
62
});
63
page.on("dialog", (dialog) => this._dialogShown(dialog));
64
page.on("download", (download) => {
65
void this._downloadStarted(download);
66
});
67
page.setDefaultNavigationTimeout(this.context.config.timeouts.navigation);
68
page.setDefaultTimeout(this.context.config.timeouts.action);
69
page[tabSymbol] = this;
70
this._initializedPromise = this._initialize();
71
}
72
static forPage(page) {
73
return page[tabSymbol];
74
}
75
static async collectConsoleMessages(page) {
76
const result = [];
77
const messages = await page.consoleMessages().catch(() => []);
78
for (const message of messages)
79
result.push(messageToConsoleMessage(message));
80
const errors = await page.pageErrors().catch(() => []);
81
for (const error of errors)
82
result.push(pageErrorToConsoleMessage(error));
83
return result;
84
}
85
async _initialize() {
86
for (const message of await Tab.collectConsoleMessages(this.page))
87
this._handleConsoleMessage(message);
88
const requests = await this.page.requests().catch(() => []);
89
for (const request of requests)
90
this._requests.add(request);
91
for (const initPage of this.context.config.browser.initPage || []) {
92
try {
93
const { default: func } = await (0, import_transform.requireOrImport)(initPage);
94
await func({ page: this.page });
95
} catch (e) {
96
(0, import_log.logUnhandledError)(e);
97
}
98
}
99
}
100
modalStates() {
101
return this._modalStates;
102
}
103
setModalState(modalState) {
104
this._modalStates.push(modalState);
105
this.emit(TabEvents.modalState, modalState);
106
}
107
clearModalState(modalState) {
108
this._modalStates = this._modalStates.filter((state) => state !== modalState);
109
}
110
_dialogShown(dialog) {
111
this.setModalState({
112
type: "dialog",
113
description: `"${dialog.type()}" dialog with message "${dialog.message()}"`,
114
dialog,
115
clearedBy: import_dialogs.handleDialog.schema.name
116
});
117
}
118
async _downloadStarted(download) {
119
const entry = {
120
download,
121
finished: false,
122
outputFile: await this.context.outputFile(download.suggestedFilename(), { origin: "web", title: "Saving download" })
123
};
124
this._downloads.push(entry);
125
this._addLogEntry({ type: "download-start", wallTime: Date.now(), download: entry });
126
await download.saveAs(entry.outputFile);
127
entry.finished = true;
128
this._addLogEntry({ type: "download-finish", wallTime: Date.now(), download: entry });
129
}
130
_clearCollectedArtifacts() {
131
this._consoleMessages.length = 0;
132
this._downloads.length = 0;
133
this._requests.clear();
134
this._eventEntries.length = 0;
135
this._recentEventEntries.length = 0;
136
}
137
_handleRequest(request) {
138
this._requests.add(request);
139
this._addLogEntry({ type: "request", wallTime: Date.now(), request });
140
}
141
_handleConsoleMessage(message) {
142
this._consoleMessages.push(message);
143
this._addLogEntry({ type: "console", wallTime: Date.now(), message });
144
}
145
_addLogEntry(entry) {
146
this._eventEntries.push(entry);
147
this._recentEventEntries.push(entry);
148
}
149
_onClose() {
150
this._clearCollectedArtifacts();
151
this._onPageClose(this);
152
}
153
async headerSnapshot() {
154
let title;
155
await this._raceAgainstModalStates(async () => {
156
title = await (0, import_utils2.callOnPageNoTrace)(this.page, (page) => page.title());
157
});
158
if (this._lastHeader.title !== title || this._lastHeader.url !== this.page.url() || this._lastHeader.current !== this.isCurrentTab()) {
159
this._lastHeader = { title: title ?? "", url: this.page.url(), current: this.isCurrentTab() };
160
return { ...this._lastHeader, changed: true };
161
}
162
return { ...this._lastHeader, changed: false };
163
}
164
isCurrentTab() {
165
return this === this.context.currentTab();
166
}
167
async waitForLoadState(state, options) {
168
await this._initializedPromise;
169
await (0, import_utils2.callOnPageNoTrace)(this.page, (page) => page.waitForLoadState(state, options).catch(import_log.logUnhandledError));
170
}
171
async navigate(url) {
172
await this._initializedPromise;
173
this._clearCollectedArtifacts();
174
const { promise: downloadEvent, abort: abortDownloadEvent } = (0, import_utils2.eventWaiter)(this.page, "download", 3e3);
175
try {
176
await this.page.goto(url, { waitUntil: "domcontentloaded" });
177
abortDownloadEvent();
178
} catch (_e) {
179
const e = _e;
180
const mightBeDownload = e.message.includes("net::ERR_ABORTED") || e.message.includes("Download is starting");
181
if (!mightBeDownload)
182
throw e;
183
const download = await downloadEvent;
184
if (!download)
185
throw e;
186
await new Promise((resolve) => setTimeout(resolve, 500));
187
return;
188
}
189
await this.waitForLoadState("load", { timeout: 5e3 });
190
}
191
async consoleMessages(level) {
192
await this._initializedPromise;
193
return this._consoleMessages.filter((message) => shouldIncludeMessage(level, message.type));
194
}
195
async requests() {
196
await this._initializedPromise;
197
return this._requests;
198
}
199
async captureSnapshot() {
200
await this._initializedPromise;
201
let tabSnapshot;
202
const modalStates = await this._raceAgainstModalStates(async () => {
203
const snapshot = await this.page._snapshotForAI({ track: "response" });
204
tabSnapshot = {
205
ariaSnapshot: snapshot.full,
206
ariaSnapshotDiff: this._needsFullSnapshot ? void 0 : snapshot.incremental,
207
modalStates: [],
208
events: []
209
};
210
});
211
if (tabSnapshot) {
212
tabSnapshot.events = this._recentEventEntries;
213
this._recentEventEntries = [];
214
}
215
this._needsFullSnapshot = !tabSnapshot;
216
return tabSnapshot ?? {
217
ariaSnapshot: "",
218
ariaSnapshotDiff: "",
219
modalStates,
220
events: []
221
};
222
}
223
_javaScriptBlocked() {
224
return this._modalStates.some((state) => state.type === "dialog");
225
}
226
async _raceAgainstModalStates(action) {
227
if (this.modalStates().length)
228
return this.modalStates();
229
const promise = new import_utils.ManualPromise();
230
const listener = (modalState) => promise.resolve([modalState]);
231
this.once(TabEvents.modalState, listener);
232
return await Promise.race([
233
action().then(() => {
234
this.off(TabEvents.modalState, listener);
235
return [];
236
}),
237
promise
238
]);
239
}
240
async waitForCompletion(callback) {
241
await this._initializedPromise;
242
await this._raceAgainstModalStates(() => (0, import_utils2.waitForCompletion)(this, callback));
243
}
244
async refLocator(params) {
245
await this._initializedPromise;
246
return (await this.refLocators([params]))[0];
247
}
248
async refLocators(params) {
249
await this._initializedPromise;
250
return Promise.all(params.map(async (param) => {
251
try {
252
let locator = this.page.locator(`aria-ref=${param.ref}`);
253
if (param.element)
254
locator = locator.describe(param.element);
255
const { resolvedSelector } = await locator._resolveSelector();
256
return { locator, resolved: (0, import_utils.asLocator)("javascript", resolvedSelector) };
257
} catch (e) {
258
throw new Error(`Ref ${param.ref} not found in the current page snapshot. Try capturing new snapshot.`);
259
}
260
}));
261
}
262
async waitForTimeout(time) {
263
if (this._javaScriptBlocked()) {
264
await new Promise((f) => setTimeout(f, time));
265
return;
266
}
267
await (0, import_utils2.callOnPageNoTrace)(this.page, (page) => {
268
return page.evaluate(() => new Promise((f) => setTimeout(f, 1e3))).catch(() => {
269
});
270
});
271
}
272
}
273
function messageToConsoleMessage(message) {
274
return {
275
type: message.type(),
276
text: message.text(),
277
toString: () => `[${message.type().toUpperCase()}] ${message.text()} @ ${message.location().url}:${message.location().lineNumber}`
278
};
279
}
280
function pageErrorToConsoleMessage(errorOrValue) {
281
if (errorOrValue instanceof Error) {
282
return {
283
type: "error",
284
text: errorOrValue.message,
285
toString: () => errorOrValue.stack || errorOrValue.message
286
};
287
}
288
return {
289
type: "error",
290
text: String(errorOrValue),
291
toString: () => String(errorOrValue)
292
};
293
}
294
function renderModalStates(modalStates) {
295
const result = [];
296
if (modalStates.length === 0)
297
result.push("- There is no modal state present");
298
for (const state of modalStates)
299
result.push(`- [${state.description}]: can be handled by the "${state.clearedBy}" tool`);
300
return result;
301
}
302
const consoleMessageLevels = ["error", "warning", "info", "debug"];
303
function shouldIncludeMessage(thresholdLevel, type) {
304
const messageLevel = consoleLevelForMessageType(type);
305
return consoleMessageLevels.indexOf(messageLevel) <= consoleMessageLevels.indexOf(thresholdLevel);
306
}
307
function consoleLevelForMessageType(type) {
308
switch (type) {
309
case "assert":
310
case "error":
311
return "error";
312
case "warning":
313
return "warning";
314
case "count":
315
case "dir":
316
case "dirxml":
317
case "info":
318
case "log":
319
case "table":
320
case "time":
321
case "timeEnd":
322
return "info";
323
case "clear":
324
case "debug":
325
case "endGroup":
326
case "profile":
327
case "profileEnd":
328
case "startGroup":
329
case "startGroupCollapsed":
330
case "trace":
331
return "debug";
332
default:
333
return "info";
334
}
335
}
336
const tabSymbol = Symbol("tabSymbol");
337
// Annotate the CommonJS export names for ESM import in node:
338
0 && (module.exports = {
339
Tab,
340
TabEvents,
341
renderModalStates,
342
shouldIncludeMessage
343
});
344

Keyboard Shortcuts

Open search /
Next entry (timeline) j
Previous entry (timeline) k
Open focused entry Enter
Show this help ?
Toggle theme Top nav button