@auto-browse/auto-browse
Version:
AI-powered browser automation
217 lines (216 loc) • 7.48 kB
JavaScript
;
/**
* 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(),
}),
},
],
};
}