UNPKG

@auto-browse/auto-browse

Version:
217 lines (216 loc) 7.48 kB
"use strict"; /** * Copyright (c) Microsoft Corporation. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.run = run; exports.runAndWait = runAndWait; exports.runAndWaitWithSnapshot = runAndWaitWithSnapshot; exports.captureAriaSnapshot = captureAriaSnapshot; const yaml_1 = __importDefault(require("yaml")); async function waitForCompletion(page, callback) { const requests = new Set(); let frameNavigated = false; let waitCallback = () => { }; const waitBarrier = new Promise((f) => { waitCallback = f; }); const requestListener = (request) => requests.add(request); const requestFinishedListener = (request) => { requests.delete(request); if (!requests.size) waitCallback(); }; const frameNavigateListener = (frame) => { if (frame.parentFrame()) return; frameNavigated = true; dispose(); clearTimeout(timeout); void frame.waitForLoadState("load").then(() => { waitCallback(); }); }; const onTimeout = () => { dispose(); waitCallback(); }; page.on("request", requestListener); page.on("requestfinished", requestFinishedListener); page.on("framenavigated", frameNavigateListener); const timeout = setTimeout(onTimeout, 10000); const dispose = () => { page.off("request", requestListener); page.off("requestfinished", requestFinishedListener); page.off("framenavigated", frameNavigateListener); clearTimeout(timeout); }; try { const result = await callback(); if (!requests.size && !frameNavigated) waitCallback(); await waitBarrier; await page.evaluate(() => new Promise((f) => setTimeout(f, 1000))); return result; } finally { dispose(); } } async function run(context, options) { const page = context.existingPage(); const dismissFileChooser = !options.noClearFileChooser && context.hasFileChooser(); try { if (options.waitForCompletion) { await waitForCompletion(page, () => options.callback(page)); } else { await options.callback(page); } } finally { if (dismissFileChooser) context.clearFileChooser(); } const result = options.captureSnapshot ? await captureAriaSnapshot(context, options.status) : { content: [{ type: "text", text: options.status || "" }], }; return result; } async function runAndWait(context, status, callback, snapshot = false) { return run(context, { callback, status, captureSnapshot: snapshot, waitForCompletion: true, }); } async function runAndWaitWithSnapshot(context, options) { return run(context, { ...options, captureSnapshot: true, waitForCompletion: true, }); } class PageSnapshot { constructor() { this._frameLocators = []; } static async create(page) { const snapshot = new PageSnapshot(); await snapshot._build(page); return snapshot; } text(options) { const results = []; if (options?.status) { results.push(options.status); results.push(""); } if (options?.hasFileChooser) { results.push("- There is a file chooser visible that requires browser_choose_file to be called"); results.push(""); } results.push(this._text); return results.join("\n"); } async _build(page) { const yamlDocument = await this._snapshotFrame(page); const lines = []; lines.push(`- Page URL: ${page.url()}`, `- Page Title: ${await page.title()}`); lines.push(`- Page Snapshot`); yamlDocument .toString() .trim() .split("\n") .forEach((line) => { lines.push(` ${line}`); // 4-space indentation }); lines.push(""); this._text = lines.join("\n"); } async _snapshotFrame(frame) { const frameIndex = this._frameLocators.push(frame) - 1; const snapshotString = await frame .locator("body") .ariaSnapshot({ ref: true }); const snapshot = yaml_1.default.parseDocument(snapshotString); const visit = async (node) => { if (yaml_1.default.isPair(node)) { await Promise.all([ visit(node.key).then((k) => (node.key = k)), visit(node.value).then((v) => (node.value = v)), ]); } else if (yaml_1.default.isSeq(node) || yaml_1.default.isMap(node)) { node.items = await Promise.all(node.items.map(visit)); } else if (yaml_1.default.isScalar(node)) { if (typeof node.value === "string") { const value = node.value; if (frameIndex > 0) node.value = value.replace("[ref=", `[ref=f${frameIndex}`); if (value.startsWith("iframe ")) { const ref = value.match(/\[ref=(.*)\]/)?.[1]; if (ref) { try { const childSnapshot = await this._snapshotFrame(frame.frameLocator(`aria-ref=${ref}`)); return snapshot.createPair(node.value, childSnapshot); } catch (error) { return snapshot.createPair(node.value, "<could not take iframe snapshot>"); } } } } } return node; }; await visit(snapshot.contents); return snapshot; } refLocator(ref) { let frame = this._frameLocators[0]; const match = ref.match(/^f(\d+)(.*)/); if (match) { const frameIndex = parseInt(match[1], 10); frame = this._frameLocators[frameIndex]; ref = match[2]; } if (!frame) throw new Error(`Frame does not exist. Provide ref from the most current snapshot.`); return frame.locator(`aria-ref=${ref}`); } } async function captureAriaSnapshot(context, status = "") { const page = context.existingPage(); const snapshot = await PageSnapshot.create(page); return { content: [ { type: "text", text: snapshot.text({ status, hasFileChooser: context.hasFileChooser(), }), }, ], }; }