mirror of
https://github.com/TracksApp/tracks.git
synced 2025-12-19 16:50:12 +01:00
278 lines
9.3 KiB
JavaScript
278 lines
9.3 KiB
JavaScript
|
|
"use strict";
|
||
|
|
var __defProp = Object.defineProperty;
|
||
|
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||
|
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||
|
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||
|
|
var __export = (target, all) => {
|
||
|
|
for (var name in all)
|
||
|
|
__defProp(target, name, { get: all[name], enumerable: true });
|
||
|
|
};
|
||
|
|
var __copyProps = (to, from, except, desc) => {
|
||
|
|
if (from && typeof from === "object" || typeof from === "function") {
|
||
|
|
for (let key of __getOwnPropNames(from))
|
||
|
|
if (!__hasOwnProp.call(to, key) && key !== except)
|
||
|
|
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||
|
|
}
|
||
|
|
return to;
|
||
|
|
};
|
||
|
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||
|
|
var tab_exports = {};
|
||
|
|
__export(tab_exports, {
|
||
|
|
Tab: () => Tab,
|
||
|
|
TabEvents: () => TabEvents,
|
||
|
|
renderModalStates: () => renderModalStates
|
||
|
|
});
|
||
|
|
module.exports = __toCommonJS(tab_exports);
|
||
|
|
var import_events = require("events");
|
||
|
|
var import_utils = require("playwright-core/lib/utils");
|
||
|
|
var import_utils2 = require("./tools/utils");
|
||
|
|
var import_log = require("../log");
|
||
|
|
var import_dialogs = require("./tools/dialogs");
|
||
|
|
var import_files = require("./tools/files");
|
||
|
|
const TabEvents = {
|
||
|
|
modalState: "modalState"
|
||
|
|
};
|
||
|
|
class Tab extends import_events.EventEmitter {
|
||
|
|
constructor(context, page, onPageClose) {
|
||
|
|
super();
|
||
|
|
this._lastTitle = "about:blank";
|
||
|
|
this._consoleMessages = [];
|
||
|
|
this._recentConsoleMessages = [];
|
||
|
|
this._requests = /* @__PURE__ */ new Set();
|
||
|
|
this._modalStates = [];
|
||
|
|
this._downloads = [];
|
||
|
|
this.context = context;
|
||
|
|
this.page = page;
|
||
|
|
this._onPageClose = onPageClose;
|
||
|
|
page.on("console", (event) => this._handleConsoleMessage(messageToConsoleMessage(event)));
|
||
|
|
page.on("pageerror", (error) => this._handleConsoleMessage(pageErrorToConsoleMessage(error)));
|
||
|
|
page.on("request", (request) => this._requests.add(request));
|
||
|
|
page.on("close", () => this._onClose());
|
||
|
|
page.on("filechooser", (chooser) => {
|
||
|
|
this.setModalState({
|
||
|
|
type: "fileChooser",
|
||
|
|
description: "File chooser",
|
||
|
|
fileChooser: chooser,
|
||
|
|
clearedBy: import_files.uploadFile.schema.name
|
||
|
|
});
|
||
|
|
});
|
||
|
|
page.on("dialog", (dialog) => this._dialogShown(dialog));
|
||
|
|
page.on("download", (download) => {
|
||
|
|
void this._downloadStarted(download);
|
||
|
|
});
|
||
|
|
page.setDefaultNavigationTimeout(this.context.config.timeouts.navigation);
|
||
|
|
page.setDefaultTimeout(this.context.config.timeouts.action);
|
||
|
|
page[tabSymbol] = this;
|
||
|
|
this._initializedPromise = this._initialize();
|
||
|
|
}
|
||
|
|
static forPage(page) {
|
||
|
|
return page[tabSymbol];
|
||
|
|
}
|
||
|
|
static async collectConsoleMessages(page) {
|
||
|
|
const result = [];
|
||
|
|
const messages = await page.consoleMessages().catch(() => []);
|
||
|
|
for (const message of messages)
|
||
|
|
result.push(messageToConsoleMessage(message));
|
||
|
|
const errors = await page.pageErrors().catch(() => []);
|
||
|
|
for (const error of errors)
|
||
|
|
result.push(pageErrorToConsoleMessage(error));
|
||
|
|
return result;
|
||
|
|
}
|
||
|
|
async _initialize() {
|
||
|
|
for (const message of await Tab.collectConsoleMessages(this.page))
|
||
|
|
this._handleConsoleMessage(message);
|
||
|
|
const requests = await this.page.requests().catch(() => []);
|
||
|
|
for (const request of requests)
|
||
|
|
this._requests.add(request);
|
||
|
|
}
|
||
|
|
modalStates() {
|
||
|
|
return this._modalStates;
|
||
|
|
}
|
||
|
|
setModalState(modalState) {
|
||
|
|
this._modalStates.push(modalState);
|
||
|
|
this.emit(TabEvents.modalState, modalState);
|
||
|
|
}
|
||
|
|
clearModalState(modalState) {
|
||
|
|
this._modalStates = this._modalStates.filter((state) => state !== modalState);
|
||
|
|
}
|
||
|
|
modalStatesMarkdown() {
|
||
|
|
return renderModalStates(this.context, this.modalStates());
|
||
|
|
}
|
||
|
|
_dialogShown(dialog) {
|
||
|
|
this.setModalState({
|
||
|
|
type: "dialog",
|
||
|
|
description: `"${dialog.type()}" dialog with message "${dialog.message()}"`,
|
||
|
|
dialog,
|
||
|
|
clearedBy: import_dialogs.handleDialog.schema.name
|
||
|
|
});
|
||
|
|
}
|
||
|
|
async _downloadStarted(download) {
|
||
|
|
const entry = {
|
||
|
|
download,
|
||
|
|
finished: false,
|
||
|
|
outputFile: await this.context.outputFile(download.suggestedFilename(), { origin: "web", reason: "Saving download" })
|
||
|
|
};
|
||
|
|
this._downloads.push(entry);
|
||
|
|
await download.saveAs(entry.outputFile);
|
||
|
|
entry.finished = true;
|
||
|
|
}
|
||
|
|
_clearCollectedArtifacts() {
|
||
|
|
this._consoleMessages.length = 0;
|
||
|
|
this._recentConsoleMessages.length = 0;
|
||
|
|
this._requests.clear();
|
||
|
|
}
|
||
|
|
_handleConsoleMessage(message) {
|
||
|
|
this._consoleMessages.push(message);
|
||
|
|
this._recentConsoleMessages.push(message);
|
||
|
|
}
|
||
|
|
_onClose() {
|
||
|
|
this._clearCollectedArtifacts();
|
||
|
|
this._onPageClose(this);
|
||
|
|
}
|
||
|
|
async updateTitle() {
|
||
|
|
await this._raceAgainstModalStates(async () => {
|
||
|
|
this._lastTitle = await (0, import_utils2.callOnPageNoTrace)(this.page, (page) => page.title());
|
||
|
|
});
|
||
|
|
}
|
||
|
|
lastTitle() {
|
||
|
|
return this._lastTitle;
|
||
|
|
}
|
||
|
|
isCurrentTab() {
|
||
|
|
return this === this.context.currentTab();
|
||
|
|
}
|
||
|
|
async waitForLoadState(state, options) {
|
||
|
|
await (0, import_utils2.callOnPageNoTrace)(this.page, (page) => page.waitForLoadState(state, options).catch(import_log.logUnhandledError));
|
||
|
|
}
|
||
|
|
async navigate(url) {
|
||
|
|
this._clearCollectedArtifacts();
|
||
|
|
const downloadEvent = (0, import_utils2.callOnPageNoTrace)(this.page, (page) => page.waitForEvent("download").catch(import_log.logUnhandledError));
|
||
|
|
try {
|
||
|
|
await this.page.goto(url, { waitUntil: "domcontentloaded" });
|
||
|
|
} catch (_e) {
|
||
|
|
const e = _e;
|
||
|
|
const mightBeDownload = e.message.includes("net::ERR_ABORTED") || e.message.includes("Download is starting");
|
||
|
|
if (!mightBeDownload)
|
||
|
|
throw e;
|
||
|
|
const download = await Promise.race([
|
||
|
|
downloadEvent,
|
||
|
|
new Promise((resolve) => setTimeout(resolve, 3e3))
|
||
|
|
]);
|
||
|
|
if (!download)
|
||
|
|
throw e;
|
||
|
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
await this.waitForLoadState("load", { timeout: 5e3 });
|
||
|
|
}
|
||
|
|
async consoleMessages(type) {
|
||
|
|
await this._initializedPromise;
|
||
|
|
return this._consoleMessages.filter((message) => type ? message.type === type : true);
|
||
|
|
}
|
||
|
|
async requests() {
|
||
|
|
await this._initializedPromise;
|
||
|
|
return this._requests;
|
||
|
|
}
|
||
|
|
async captureSnapshot() {
|
||
|
|
let tabSnapshot;
|
||
|
|
const modalStates = await this._raceAgainstModalStates(async () => {
|
||
|
|
const snapshot = await this.page._snapshotForAI();
|
||
|
|
tabSnapshot = {
|
||
|
|
url: this.page.url(),
|
||
|
|
title: await this.page.title(),
|
||
|
|
ariaSnapshot: snapshot,
|
||
|
|
modalStates: [],
|
||
|
|
consoleMessages: [],
|
||
|
|
downloads: this._downloads
|
||
|
|
};
|
||
|
|
});
|
||
|
|
if (tabSnapshot) {
|
||
|
|
tabSnapshot.consoleMessages = this._recentConsoleMessages;
|
||
|
|
this._recentConsoleMessages = [];
|
||
|
|
}
|
||
|
|
return tabSnapshot ?? {
|
||
|
|
url: this.page.url(),
|
||
|
|
title: "",
|
||
|
|
ariaSnapshot: "",
|
||
|
|
modalStates,
|
||
|
|
consoleMessages: [],
|
||
|
|
downloads: []
|
||
|
|
};
|
||
|
|
}
|
||
|
|
_javaScriptBlocked() {
|
||
|
|
return this._modalStates.some((state) => state.type === "dialog");
|
||
|
|
}
|
||
|
|
async _raceAgainstModalStates(action) {
|
||
|
|
if (this.modalStates().length)
|
||
|
|
return this.modalStates();
|
||
|
|
const promise = new import_utils.ManualPromise();
|
||
|
|
const listener = (modalState) => promise.resolve([modalState]);
|
||
|
|
this.once(TabEvents.modalState, listener);
|
||
|
|
return await Promise.race([
|
||
|
|
action().then(() => {
|
||
|
|
this.off(TabEvents.modalState, listener);
|
||
|
|
return [];
|
||
|
|
}),
|
||
|
|
promise
|
||
|
|
]);
|
||
|
|
}
|
||
|
|
async waitForCompletion(callback) {
|
||
|
|
await this._raceAgainstModalStates(() => (0, import_utils2.waitForCompletion)(this, callback));
|
||
|
|
}
|
||
|
|
async refLocator(params) {
|
||
|
|
return (await this.refLocators([params]))[0];
|
||
|
|
}
|
||
|
|
async refLocators(params) {
|
||
|
|
const snapshot = await this.page._snapshotForAI();
|
||
|
|
return params.map((param) => {
|
||
|
|
if (!snapshot.includes(`[ref=${param.ref}]`))
|
||
|
|
throw new Error(`Ref ${param.ref} not found in the current page snapshot. Try capturing new snapshot.`);
|
||
|
|
return this.page.locator(`aria-ref=${param.ref}`).describe(param.element);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
async waitForTimeout(time) {
|
||
|
|
if (this._javaScriptBlocked()) {
|
||
|
|
await new Promise((f) => setTimeout(f, time));
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
await (0, import_utils2.callOnPageNoTrace)(this.page, (page) => {
|
||
|
|
return page.evaluate(() => new Promise((f) => setTimeout(f, 1e3)));
|
||
|
|
});
|
||
|
|
}
|
||
|
|
}
|
||
|
|
function messageToConsoleMessage(message) {
|
||
|
|
return {
|
||
|
|
type: message.type(),
|
||
|
|
text: message.text(),
|
||
|
|
toString: () => `[${message.type().toUpperCase()}] ${message.text()} @ ${message.location().url}:${message.location().lineNumber}`
|
||
|
|
};
|
||
|
|
}
|
||
|
|
function pageErrorToConsoleMessage(errorOrValue) {
|
||
|
|
if (errorOrValue instanceof Error) {
|
||
|
|
return {
|
||
|
|
type: "error",
|
||
|
|
text: errorOrValue.message,
|
||
|
|
toString: () => errorOrValue.stack || errorOrValue.message
|
||
|
|
};
|
||
|
|
}
|
||
|
|
return {
|
||
|
|
type: "error",
|
||
|
|
text: String(errorOrValue),
|
||
|
|
toString: () => String(errorOrValue)
|
||
|
|
};
|
||
|
|
}
|
||
|
|
function renderModalStates(context, modalStates) {
|
||
|
|
const result = ["### Modal state"];
|
||
|
|
if (modalStates.length === 0)
|
||
|
|
result.push("- There is no modal state present");
|
||
|
|
for (const state of modalStates)
|
||
|
|
result.push(`- [${state.description}]: can be handled by the "${state.clearedBy}" tool`);
|
||
|
|
return result;
|
||
|
|
}
|
||
|
|
const tabSymbol = Symbol("tabSymbol");
|
||
|
|
// Annotate the CommonJS export names for ESM import in node:
|
||
|
|
0 && (module.exports = {
|
||
|
|
Tab,
|
||
|
|
TabEvents,
|
||
|
|
renderModalStates
|
||
|
|
});
|