@letsscrapedata/controller
Version:
Unified browser / HTML controller interfaces that support patchright, camoufox, playwright, puppeteer and cheerio
1,552 lines (1,535 loc) • 268 kB
JavaScript
"use strict";
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
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 __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/index.ts
var index_exports = {};
__export(index_exports, {
CheerioElement: () => CheerioElement,
CheerioPage: () => CheerioPage,
ControllerEvent: () => ControllerEvent,
LsdBrowserContextEvent: () => LsdBrowserContextEvent,
LsdBrowserEvent: () => LsdBrowserEvent,
LsdPageEvent: () => LsdPageEvent,
PlaywrightBrowser: () => PlaywrightBrowser,
PlaywrightBrowserContext: () => PlaywrightBrowserContext,
PlaywrightElement: () => PlaywrightElement,
PlaywrightPage: () => PlaywrightPage,
PuppeteerBrowser: () => PuppeteerBrowser,
PuppeteerBrowserContext: () => PuppeteerBrowserContext,
PuppeteerElement: () => PuppeteerElement,
PuppeteerPage: () => PuppeteerPage,
controller: () => controller,
setControllerLogFun: () => setControllerLogFun
});
module.exports = __toCommonJS(index_exports);
// src/types/types.ts
var ControllerEvent = /* @__PURE__ */ ((ControllerEvent2) => {
ControllerEvent2["BROWSERCONTEXT_CLOSE"] = "close";
ControllerEvent2["BROWSERCONTEXT_PAGE"] = "page";
ControllerEvent2["BROWSERCONTEXT_TARGETCREATED"] = "targetcreated";
ControllerEvent2["BROWSER_DISCONNECTED"] = "disconnected";
ControllerEvent2["PAGE_CLOSE"] = "close";
ControllerEvent2["PAGE_POUP"] = "popup";
ControllerEvent2["PAGE_REQUEST"] = "request";
ControllerEvent2["PAGE_RESPONSE"] = "response";
return ControllerEvent2;
})(ControllerEvent || {});
var LsdPageEvent = /* @__PURE__ */ ((LsdPageEvent2) => {
LsdPageEvent2["PAGE_CLOSE"] = "pageClose";
LsdPageEvent2["PAGE_POPUP"] = "pagePopup";
return LsdPageEvent2;
})(LsdPageEvent || {});
var LsdBrowserContextEvent = /* @__PURE__ */ ((LsdBrowserContextEvent2) => {
LsdBrowserContextEvent2["PAGE_CLOSE"] = "pageClose";
return LsdBrowserContextEvent2;
})(LsdBrowserContextEvent || {});
var LsdBrowserEvent = /* @__PURE__ */ ((LsdBrowserEvent2) => {
LsdBrowserEvent2["BROWSER_CONTEXT_CLOSE"] = "browserContextClose";
return LsdBrowserEvent2;
})(LsdBrowserEvent || {});
// src/utils/log.ts
var import_utils = require("@letsscrapedata/utils");
var pkgLog = import_utils.log;
function setControllerLogFun(logFun) {
if (typeof logFun === "function") {
pkgLog = logFun;
return true;
} else {
return false;
}
}
async function logdbg(...args) {
await pkgLog(import_utils.LogLevel.DBG, ...args);
}
async function loginfo(...args) {
await pkgLog(import_utils.LogLevel.INF, ...args);
}
async function logwarn(...args) {
await pkgLog(import_utils.LogLevel.WRN, ...args);
}
async function logerr(...args) {
await pkgLog(import_utils.LogLevel.ERR, ...args);
}
// src/playwright/browser.ts
var import_node_events3 = __toESM(require("events"), 1);
var import_utils5 = require("@letsscrapedata/utils");
// src/playwright/context.ts
var import_node_events2 = __toESM(require("events"), 1);
var import_utils4 = require("@letsscrapedata/utils");
// src/playwright/page.ts
var import_node_events = __toESM(require("events"), 1);
var import_utils3 = require("@letsscrapedata/utils");
// src/utils/common.ts
function convertDataAttributeName(attr) {
if (!attr.startsWith("data-")) {
return "";
}
const parts = attr.split("-");
let name = parts[1];
for (const part of parts.slice(2)) {
if (!part) {
continue;
}
name = `${name}${part[1].toUpperCase()}${part.slice(1).toLowerCase()}`;
}
return name;
}
function getIframeSelector(iframeOption) {
const { src = "", id = "", selector = "" } = iframeOption;
if (typeof src === "string" && src) {
return `iframe[src^="${src}"]`;
} else if (typeof id === "string" && id) {
return `iframe[id="${id}"]`;
} else {
return selector;
}
}
// src/playwright/element.ts
var import_utils2 = require("@letsscrapedata/utils");
var PlaywrightElement = class _PlaywrightElement {
#frame;
#locator;
constructor(locator, frame) {
if (!frame.locator || !locator.click) {
throw new Error("Invalid paras in new PlaywrightElement");
}
this.#frame = frame;
this.#locator = locator;
}
async attribute(attributeName) {
const attributeValue = await this.#locator.getAttribute(attributeName);
return attributeValue ? attributeValue : "";
}
async attributeNames() {
const names = await this.#locator.evaluate((node) => node.getAttributeNames());
return names;
}
async boundingBox() {
return await this.#locator.boundingBox();
}
async dataset() {
try {
const dataset = await this.#locator.evaluate((node) => node.dataset);
return dataset;
} catch (err) {
return {};
}
}
async evaluate(func, args) {
try {
const frame = this.#frame;
;
if (typeof frame.parentFrame === "function") {
return await frame.evaluate(func, args);
} else {
const locator = this.#frame.owner();
return await locator.evaluate(func, args);
}
} catch (err) {
logerr(err);
return "";
}
}
/*
async #getChildFrame(parentFrame: Frame, iframeOption: IframeOption): Promise<Frame | null> {
if (!parentFrame) {
throw new Error("Invalid parent frame");
}
let { src = "" } = iframeOption;
if (!src) {
throw new Error("Invalid src in IframeOption");
}
// src: use childFrames()
const childFrames = parentFrame.childFrames();
for (const childFrame of childFrames) {
const url = childFrame.url();
if (typeof src === "string") {
// src: string
if (url.startsWith(src)) {
return childFrame;
} else if (url.toLowerCase().startsWith(src)) {
return childFrame;
}
} else {
// src: RegExp
if (url.match(src)) {
return childFrame;
}
}
}
return null;
}
*/
async #getChildFrameLocator(parent, iframeOption) {
return parent.frameLocator(getIframeSelector(iframeOption));
}
async #getDescendantFrame(parent, iframeOptions) {
try {
if (iframeOptions.length <= 0) {
return null;
}
let frameLocator = parent.frameLocator(getIframeSelector(iframeOptions[0]));
for (const iframeOption of iframeOptions.slice(1)) {
if (!frameLocator) {
return null;
}
frameLocator = await this.#getChildFrameLocator(frameLocator, iframeOption);
}
return frameLocator;
} catch (err) {
throw new Error(`No child iframe: ${JSON.stringify(iframeOptions)}`);
}
}
async #findElementHandles(selector, absolute = false, iframeOptions = []) {
let parent = absolute ? this.#frame : this.#locator;
let frame = this.#frame;
const retObj = { frame, locators: [] };
if (iframeOptions.length > 0) {
const childFrame = await this.#getDescendantFrame(frame, iframeOptions);
if (!childFrame) {
return retObj;
}
retObj.frame = childFrame;
parent = childFrame;
}
try {
let locators = [];
if (selector.startsWith("./") || selector.startsWith("/") || selector.startsWith("..")) {
locators = await parent.locator(`xpath=${selector}`).all();
} else {
if (selector !== ".") {
locators = await parent.locator(selector).all();
} else {
locators = [this.#locator];
}
}
retObj.locators = locators;
return retObj;
} catch (err) {
loginfo(err);
return retObj;
}
}
async findElement(selectorOrXpath, iframeOptions = [], absolute = false) {
const selectors = typeof selectorOrXpath === "string" ? [selectorOrXpath] : selectorOrXpath;
if (!Array.isArray(selectors)) {
throw new Error(`Invalid selectorOrXpath ${selectorOrXpath} in findElement`);
}
for (const selector of selectors) {
const { frame, locators } = await this.#findElementHandles(selector, absolute, iframeOptions);
if (locators.length > 0) {
const playwrightElement = new _PlaywrightElement(locators[0], frame);
return playwrightElement;
}
}
return null;
}
async findElements(selectorOrXpath, iframeOptions = [], absolute = false) {
const selectors = typeof selectorOrXpath === "string" ? [selectorOrXpath] : selectorOrXpath;
if (!Array.isArray(selectors)) {
throw new Error(`Invalid selectorOrXpath ${selectorOrXpath} in findElements`);
}
for (const selector of selectors) {
const { frame, locators } = await this.#findElementHandles(selector, absolute, iframeOptions);
if (locators.length > 0) {
const playwrightElements = locators.map((locator) => new _PlaywrightElement(locator, frame));
return playwrightElements;
}
}
return [];
}
async hasAttribute(attributeName) {
const hasFlag = await this.#locator.evaluate((node, attr) => node.hasAttribute(attr), attributeName);
return hasFlag;
}
async innerHtml() {
const html = await this.#locator.innerHTML();
return html;
}
async innerText(onlyChild = false) {
let text = "";
if (onlyChild) {
text = await this.#locator.evaluate((node) => {
let child = node.firstChild;
let texts = [];
while (child) {
if (child.nodeType == 3) {
texts.push(child.data);
}
child = child.nextSibling;
}
return texts.join(" ");
});
} else {
text = await this.#locator.innerText();
}
return text;
}
async outerHtml() {
const html = await this.#locator.evaluate((node) => node.outerHTML);
return html;
}
async textContent() {
const text = await this.#locator.textContent();
return text ? text : "";
}
async click(options = {}) {
const { button, clickCount: count, delay, position: offset, clickType = "click" } = options;
const actOptions = { button, count, delay, offset };
if (clickType === "click") {
await this.#locator.click(actOptions);
} else if (clickType === "evaluate") {
await this.#locator.evaluate(async (ev) => await ev.click());
} else {
(0, import_utils2.unreachable)(clickType);
}
return true;
}
async focus() {
await this.#locator.focus();
return true;
}
async hover() {
await this.#locator.hover();
return true;
}
async input(value, options = {}) {
const { delay = 0, replace = false, enter = false } = options;
if (replace) {
await this.#locator.click({ button: "left", clickCount: 3 });
}
if (delay > 0) {
await this.#locator.fill(value);
} else {
await this.#locator.fill(value);
}
if (enter) {
await this.#locator.press("Enter");
}
return true;
}
async press(key, options = {}) {
await this.#locator.press(key, options);
return true;
}
async screenshot(options) {
return await this.#locator.screenshot(options);
}
async scrollIntoView() {
await this.#locator.scrollIntoViewIfNeeded();
return true;
}
async select(options) {
const { type, values = [], labels = [], indexes = [] } = options;
switch (type) {
case "value":
if (values.length > 0) {
await this.#locator.selectOption(values);
}
break;
case "label":
if (labels.length > 0) {
await this.#locator.selectOption(labels.map((label) => {
return { label };
}));
}
break;
case "index":
if (indexes.length > 0) {
const indexValues = await this.#locator.evaluate(
(node, indexes2) => {
const options2 = node.options;
const len = options2.length;
const vals = [];
for (const index of indexes2.filter((i) => i >= 0 && i < len)) {
vals.push(options2[index].value);
}
return vals;
},
indexes
);
if (indexValues.length > 0) {
await this.#locator.selectOption(indexValues);
}
}
break;
default:
(0, import_utils2.unreachable)(type);
}
return true;
}
async setAttribute(attributeName, newValue) {
await this.#locator.evaluate((node, argvs) => {
node.setAttribute(argvs[0], argvs[1]);
}, [attributeName, newValue]);
return true;
}
_origElement() {
return this.#locator;
}
};
// src/playwright/page.ts
var PlaywrightPage = class extends import_node_events.default {
#lsdBrowserContext;
#page;
#status;
#pageId;
#closeWhenFree;
#resquestInterceptionOptions;
#responseInterceptionOptions;
#client;
#responseCb;
#isDebugTask;
#hasValidUrl(page) {
const url = page.url();
return url.toLowerCase().startsWith("http");
}
async #clearCookies(page) {
if (!this.#hasValidUrl(page)) {
throw new Error("Please open related url before clearing cookies");
}
const browserContext = this.#lsdBrowserContext._origBrowserContext();
if (!browserContext) {
throw new Error(`Invalid LsdBrowserContext`);
}
const cookieItems = await this.#getCookies(page);
const domainSet = new Set(cookieItems.map((c) => c.domain));
if (domainSet.size !== 1) {
logwarn(`##browser LsdPage domains in clearCookies: ${Array.from(domainSet.values())}`);
}
for (const domain of domainSet.values()) {
await browserContext.clearCookies({ domain });
}
return true;
}
async #getCookies(page) {
if (!this.#hasValidUrl(page)) {
throw new Error("Please open related url before getting cookies");
}
const browserContext = this.#lsdBrowserContext._origBrowserContext();
if (!browserContext) {
throw new Error(`Invalid LsdBrowserContext`);
}
const url = page.url();
const origCookies = await browserContext.cookies(url);
const cookies = origCookies.map((origCookie) => {
const { name, value, domain, path, expires, httpOnly, secure, sameSite = "Lax" } = origCookie;
return { name, value, domain, path, expires, httpOnly, secure, sameSite };
});
return cookies;
}
async #setCookies(page, cookies) {
if (!page) {
throw new Error("No valid page");
}
if (Array.isArray(cookies) && cookies.length > 0 && cookies.every((c) => typeof c.name === "string")) {
const browserContext = this.#lsdBrowserContext._origBrowserContext();
if (!browserContext) {
throw new Error(`Invalid LsdBrowserContext`);
}
await browserContext.addCookies(cookies);
return true;
} else {
return false;
}
}
async #clearLocalStorage(page) {
if (!this.#hasValidUrl(page)) {
throw new Error("Please open related url before clearing localStorage");
}
await page.evaluate(() => window.localStorage.clear());
return true;
}
async #getLocalStorage(page) {
if (!this.#hasValidUrl(page)) {
throw new Error("Please open related url before getting localStorage");
}
const localStorageStr = await page.evaluate(() => JSON.stringify(window.localStorage));
const localStorageObj = JSON.parse(localStorageStr);
const localStorageItems = Object.keys(localStorageObj).map((name) => ({ name, value: localStorageObj[name] }));
if (localStorageItems.length === 0) {
return [];
}
const url = new URL(page.url());
return [{ origin: url.origin, localStorage: localStorageItems }];
}
async #setLocalStorage(page, localStorageItems) {
if (!this.#hasValidUrl(page)) {
throw new Error("Please open related url before setting localStorage");
}
await page.evaluate((items) => {
for (const item of items) {
window.localStorage.setItem(item.name, item.value);
}
}, localStorageItems);
return true;
}
async #clearIndexedDB(page) {
if (!this.#hasValidUrl(page)) {
throw new Error("Please open related url before clearing indexedDB");
}
await page.evaluate(async () => {
for (const db of await indexedDB.databases?.() || []) {
if (db.name)
indexedDB.deleteDatabase(db.name);
}
});
return true;
}
/*
async #getChildFrame(parentFrame: Frame, iframeOption: IframeOption): Promise<Frame | null> {
if (!parentFrame) {
throw new Error("Invalid parent frame");
}
let { src = "" } = iframeOption;
if (!src) {
throw new Error("Invalid src in IframeOption");
}
// src: use childFrames()
const childFrames = parentFrame.childFrames();
for (const childFrame of childFrames) {
const url = childFrame.url();
if (typeof src === "string") {
// src: string
if (url.startsWith(src)) {
return childFrame;
} else if (url.toLowerCase().startsWith(src)) {
return childFrame;
}
} else {
// src: RegExp
if (url.match(src)) {
return childFrame;
}
}
}
return null;
}
*/
async #findDescendantFrame(src, id) {
if (!this.#page) {
throw new Error("No valid page");
}
const frames = this.#page.frames();
for (const frame of frames) {
const url = frame.url();
if (typeof src === "string" && src) {
if (url.startsWith(src)) {
return frame;
} else if (url.toLowerCase().startsWith(src)) {
return frame;
}
} else if (src instanceof RegExp) {
if (url.match(src)) {
return frame;
}
} else if (id) {
const element = await frame.frameElement();
if (element) {
const frameId = await frame.evaluate(([ele, attr]) => ele.getAttribute(attr), [element, "id"]);
if (frameId === id) {
return frame;
}
}
}
}
return null;
}
async #getChildFrameLocator(parent, iframeOption) {
return parent.frameLocator(getIframeSelector(iframeOption));
}
async #getDescendantFrame(mainFrame, iframeOptions) {
try {
if (iframeOptions.length <= 0) {
return null;
}
if (iframeOptions.length === 1 && !iframeOptions[0].selector) {
const { src = "", id = "" } = iframeOptions[0];
const frame = await this.#findDescendantFrame(src, id);
return frame;
} else {
let frameLocator = mainFrame.frameLocator(getIframeSelector(iframeOptions[0]));
for (const iframeOption of iframeOptions.slice(1)) {
if (!frameLocator) {
return null;
}
frameLocator = await this.#getChildFrameLocator(frameLocator, iframeOption);
}
return frameLocator;
}
} catch (err) {
throw new Error(`No child iframe: ${JSON.stringify(iframeOptions)}`);
}
}
async #findElementHandles(selector, iframeOptions = []) {
if (!this.#page) {
throw new Error("No valid page");
}
let frame = this.#page.mainFrame();
const retObj = { frame, locators: [] };
if (iframeOptions.length > 0) {
frame = await this.#getDescendantFrame(frame, iframeOptions);
if (!frame) {
return retObj;
}
retObj.frame = frame;
}
try {
let locators = [];
if (selector.startsWith("./") || selector.startsWith("/") || selector.startsWith("..")) {
locators = await frame.locator(`xpath=${selector}`).all();
} else {
if (selector !== ".") {
locators = await frame.locator(selector).all();
} else {
throw new Error("Cannot use selector '.' on page");
}
}
retObj.locators = locators;
return retObj;
} catch (err) {
loginfo(err);
return retObj;
}
}
#addPageOn() {
if (!this.#page) {
throw new Error("No valid page");
}
const page = this.#page;
const pageId = this.#pageId;
page.on("close" /* PAGE_CLOSE */, async () => {
loginfo(`##browser page ${pageId} closed @LsdPage`);
if (!page.pageInfo) {
logerr(`##browser LsdPage logic error in page.on("close")`);
}
this.emit("pageClose" /* PAGE_CLOSE */);
this.#lsdBrowserContext.emit("pageClose" /* PAGE_CLOSE */, this);
});
page.on("popup" /* PAGE_POUP */, (p) => {
if (p) {
let evtData = null;
const pageInfo = p.pageInfo;
let popupPageId = "page";
if (pageInfo) {
const { browserIdx, browserContextIdx, pageIdx } = pageInfo;
popupPageId = `page-${browserIdx}-${browserContextIdx}-${pageIdx}`;
pageInfo.openType = "popup";
evtData = this.browserContext().page(pageIdx);
if (evtData && page.pageInfo?.taskId) {
pageInfo.relatedId = page.pageInfo.taskId;
}
} else {
logerr(`##browser page ${pageId} has popup without page.pageInfo @LsdPage`);
}
loginfo(`##browser page ${pageId} has popup ${popupPageId} @LsdPage`);
this.emit("pagePopup" /* PAGE_POPUP */, evtData);
} else {
logerr(`##browser page ${pageId} has popup page with null page @LsdPage`);
}
});
}
constructor(browserContext, page, pageInfo) {
if (!browserContext.pages || !page?.goto) {
throw new Error("Invalid paras in new LsdPage");
}
super();
this.#lsdBrowserContext = browserContext;
this.#page = page;
this.#status = "free";
const currentTime = (0, import_utils3.getCurrentUnixTime)();
const { browserIdx = 0, browserContextIdx = 0, pageIdx = 0, openType = "other", openTime = currentTime, lastStatusUpdateTime = currentTime, taskId = 0, relatedId = 0, misc = {} } = pageInfo ? pageInfo : {};
this.#page.pageInfo = { browserIdx, browserContextIdx, pageIdx, openType, openTime, lastStatusUpdateTime, taskId, relatedId, misc };
this.#pageId = `PlaywrightPage-${browserIdx}-${browserContextIdx}-${pageIdx}`;
this.#closeWhenFree = false;
this.#resquestInterceptionOptions = [];
this.#responseInterceptionOptions = [];
this.#client = null;
this.#responseCb = null;
this.#isDebugTask = false;
loginfo(`##browser LsdPage ${this.#pageId} ${openType}ed`);
this.#addPageOn();
}
async addPreloadScript(scriptOrFunc, arg) {
if (!this.#page) {
throw new Error("No valid page");
}
if (typeof scriptOrFunc === "string") {
await this.#page.addInitScript({ content: scriptOrFunc });
} else if (typeof scriptOrFunc === "function") {
await this.#page.addInitScript(scriptOrFunc, arg);
} else {
throw new Error(`Invalid type of scriptOrFunc ${typeof scriptOrFunc}`);
}
return true;
}
async addScriptTag(options) {
if (!this.#page) {
throw new Error("No valid page");
}
return this.#page.addScriptTag(options);
}
apiContext() {
return this.browserContext().apiContext();
}
async bringToFront() {
if (!this.#page) {
throw new Error("No valid page");
}
await this.#page.bringToFront();
return true;
}
browserContext() {
return this.#lsdBrowserContext;
}
async clearCookies() {
if (!this.#page) {
throw new Error("No valid page");
}
return await this.#clearCookies(this.#page);
}
async clearLocalStorage() {
if (!this.#page) {
throw new Error("No valid page");
}
return await this.#clearLocalStorage(this.#page);
}
async clearRequestInterceptions() {
if (!this.#page) {
throw new Error("No valid page");
}
await this.#page.unrouteAll();
return true;
}
async clearResponseInterceptions() {
if (!this.#page) {
throw new Error("No valid page");
}
try {
if (this.#responseInterceptionOptions.length > 0) {
if (this.#responseCb) {
this.#page.removeListener("response", this.#responseCb);
}
this.#responseInterceptionOptions = [];
}
return true;
} catch (err) {
logerr(err);
return false;
}
}
async clearStateData() {
if (!this.#page) {
throw new Error("No valid page");
}
await this.#clearCookies(this.#page);
await this.#clearIndexedDB(this.#page);
return await this.#clearLocalStorage(this.#page);
}
async close() {
if (this.#status === "closed") {
logwarn(`##browser LsdPage ${this.#pageId} is already closed.`);
return true;
} else if (this.#status === "busy") {
throw new Error(`Page ${this.#pageId} cannot be closed because it is busy.`);
}
if (!this.#page) {
throw new Error("No valid page");
}
await this.#page.close();
this.#page = null;
this.#status = "closed";
loginfo(`##browser LsdPage ${this.#pageId} is closed`);
return true;
}
closeWhenFree() {
return this.#closeWhenFree;
}
async content(iframeOptions = []) {
if (!this.#page) {
throw new Error("No valid page");
}
let content = "";
if (iframeOptions.length > 0) {
const frameLocator = await this.#getDescendantFrame(this.#page.mainFrame(), iframeOptions);
if (frameLocator) {
content = await frameLocator.locator(":root").evaluate(() => document.documentElement.outerHTML);
}
} else {
content = await this.#page.content();
}
return content;
}
async cookies() {
if (!this.#page) {
throw new Error("No valid page");
}
return this.#getCookies(this.#page);
}
async documentHeight() {
if (!this.#page) {
throw new Error("No valid page");
}
let height = await this.#page.evaluate(() => document?.documentElement?.scrollHeight);
if (typeof height === "undefined") {
height = 0;
}
return height;
}
async evaluate(func, args) {
if (!this.#page) {
throw new Error("No valid page");
}
return this.#page.evaluate(func, args);
}
async exposeFunction(name, callbackFunction) {
if (!this.#page) {
throw new Error("No valid page");
}
await this.#page.exposeFunction(name, callbackFunction);
return;
}
async findElement(selectorOrXpath, iframeOptions = []) {
if (!this.#page) {
throw new Error("No valid page");
}
const selectors = typeof selectorOrXpath === "string" ? [selectorOrXpath] : selectorOrXpath;
if (!Array.isArray(selectors)) {
throw new Error(`Invalid selectorOrXpath ${selectorOrXpath} in findElement`);
}
for (const selector of selectors) {
const { frame, locators } = await this.#findElementHandles(selector, iframeOptions);
if (locators.length > 0) {
const playwrightElement = new PlaywrightElement(locators[0], frame);
return playwrightElement;
}
}
return null;
}
async findElements(selectorOrXpath, iframeOptions = []) {
if (!this.#page) {
throw new Error("No valid page");
}
const selectors = typeof selectorOrXpath === "string" ? [selectorOrXpath] : selectorOrXpath;
if (!Array.isArray(selectors)) {
throw new Error(`Invalid selectorOrXpath ${selectorOrXpath} in findElements`);
}
for (const selector of selectors) {
const { frame, locators } = await this.#findElementHandles(selector, iframeOptions);
if (locators.length > 0) {
const playwrightElements = locators.map((locator) => new PlaywrightElement(locator, frame));
return playwrightElements;
}
}
return [];
}
async free() {
if (this.#status === "free" && this.pageInfo().openType !== "popup") {
logwarn(`##browser LsdPage ${this.#pageId} is already free.`);
}
this.#status = "free";
logdbg(`##browser LsdPage ${this.#pageId} is freed`);
await this.clearRequestInterceptions();
await this.clearResponseInterceptions();
return true;
}
#getWaitUntil(origWaitUntil) {
if (origWaitUntil === "networkidle0" || origWaitUntil === "networkidle2") {
return "networkidle";
} else {
return origWaitUntil;
}
}
async goto(url, options) {
if (!this.#page) {
throw new Error("No valid page");
}
if (options) {
const { referer, timeout, waitUntil = "load" } = options;
const newOptions = {};
if (referer) {
newOptions.referer = referer;
}
if (timeout) {
newOptions.timeout = timeout;
}
newOptions.waitUntil = this.#getWaitUntil(waitUntil);
await this.#page.goto(url, newOptions);
} else {
await this.#page.goto(url);
}
return true;
}
id() {
return this.#pageId;
}
isFree() {
return this.#status === "free";
}
async localStroage() {
if (!this.#page) {
throw new Error("No valid page");
}
return this.#getLocalStorage(this.#page);
}
load() {
throw new Error("Not supported in PlaywrightPage.");
}
mainFrame() {
if (!this.#page) {
throw new Error("No valid page");
}
return this.#page.mainFrame();
}
async maximizeViewport() {
const height = await this.pageHeight();
const width = await this.pageWidth();
return await this.setViewportSize({ height, width });
}
async mouseClick(x, y, options) {
if (!this.#page) {
throw new Error("No valid page");
}
await this.#page.mouse.click(x, y, options);
return true;
}
async mouseDown() {
if (!this.#page) {
throw new Error("No valid page");
}
await this.#page.mouse.down();
return true;
}
async mouseMove(x, y) {
if (!this.#page) {
throw new Error("No valid page");
}
await this.#page.mouse.move(x, y);
return true;
}
async mouseUp() {
if (!this.#page) {
throw new Error("No valid page");
}
await this.#page.mouse.up();
return true;
}
async mouseWheel(deltaX = 0, deltaY = 0) {
if (!this.#page) {
throw new Error("No valid page");
}
await this.#page.mouse.wheel(deltaX, deltaY);
return true;
}
async pageHeight() {
if (!this.#page) {
throw new Error("No valid page");
}
let bodyHeight = await this.#page.evaluate(() => document?.body?.scrollHeight);
if (typeof bodyHeight === "undefined") {
bodyHeight = 0;
}
let documentHeight = await this.#page.evaluate(() => document?.documentElement?.scrollHeight);
if (typeof documentHeight === "undefined") {
documentHeight = 0;
}
let windowHeight = await this.#page.evaluate(() => window.outerHeight);
if (typeof windowHeight === "undefined") {
windowHeight = 0;
}
const pageHeight = Math.max(bodyHeight, documentHeight, windowHeight);
return pageHeight;
}
pageInfo() {
if (!this.#page) {
throw new Error("No valid page");
}
return Object.assign({}, this.#page.pageInfo);
}
async pageWidth() {
if (!this.#page) {
throw new Error("No valid page");
}
let offsetWidth = await this.#page.evaluate(() => document?.documentElement?.offsetWidth);
if (typeof offsetWidth === "undefined") {
offsetWidth = 0;
}
let windowWidth = await this.#page.evaluate(() => window.outerWidth);
if (typeof windowWidth === "undefined") {
windowWidth = 0;
}
const pageWidth = Math.max(offsetWidth, windowWidth);
return pageWidth;
}
async pdf(options) {
if (!this.#page) {
throw new Error("No valid page");
}
const buffer = await this.#page.pdf(options);
return buffer;
}
async reload() {
if (!this.#page) {
throw new Error("No valid page");
}
try {
await this.#page.reload();
return true;
} catch (err) {
loginfo(err);
return false;
}
}
async screenshot(options) {
if (!this.#page) {
throw new Error("No valid page");
}
return await this.#page.screenshot(options);
}
async scrollBy(x, y) {
if (!this.#page) {
throw new Error("No valid page");
}
await this.#page.evaluate(
([x2, y2]) => {
window.scrollBy(x2, y2);
},
[x, y]
);
return true;
}
async scrollTo(x, y) {
if (!this.#page) {
throw new Error("No valid page");
}
await this.#page.evaluate(
([x2, y2]) => {
window.scrollTo(x2, y2);
},
[x, y]
);
return true;
}
async sendCDPMessage(method, params = null, detach = true) {
if (!this.#client) {
const origContext = this.browserContext()._origBrowserContext();
if (!origContext) {
throw new Error(`Invalid playwright browserContext`);
}
this.#client = await origContext.newCDPSession(this.#page);
}
if (!this.#client) {
throw new Error("No valid CDP session to send message");
}
const response = params ? await this.#client.send(method, params) : await this.#client.send(method);
if (detach) {
await this.#client.detach();
this.#client = null;
}
return response;
}
setCloseWhenFree(closeWhenFree) {
this.#closeWhenFree = closeWhenFree;
return true;
}
async setCookies(cookies) {
if (!this.#page) {
throw new Error("No valid page");
}
return await this.#setCookies(this.#page, cookies);
}
async setExtraHTTPHeaders(headers) {
if (!this.#page) {
throw new Error("No valid page");
}
await this.#page.setExtraHTTPHeaders(headers);
return true;
}
async setLocalStroage(localStorageItems) {
if (!this.#page) {
throw new Error("No valid page");
}
return await this.#setLocalStorage(this.#page, localStorageItems);
}
setPageInfo(pageInfo) {
if (!this.#page?.pageInfo) {
throw new Error("No valid page or pageInfo");
}
if (!pageInfo) {
throw new Error("Invalid paras in setPageInfo");
}
const actPageInfo = this.#page.pageInfo;
const { lastStatusUpdateTime, taskId, relatedId, misc } = pageInfo;
if (typeof lastStatusUpdateTime === "number") {
actPageInfo.lastStatusUpdateTime = lastStatusUpdateTime;
}
if (typeof taskId === "number") {
actPageInfo.taskId = taskId;
const debug = this.#page && this.#page.pageInfo && this.#page.pageInfo.taskId < 0;
this.#isDebugTask = !!debug;
}
if (typeof relatedId === "number") {
actPageInfo.relatedId = relatedId;
}
if (misc && typeof misc === "object") {
for (const key of Object.keys(misc)) {
actPageInfo.misc[key] = misc[key];
}
}
return true;
}
#checkRequestMatch(request, requestMatch) {
try {
if (!request) {
return false;
}
const { methods, postData, resourceTypes, url } = requestMatch;
if (methods && !methods.includes(request.method().toUpperCase())) {
return false;
}
if (resourceTypes && !resourceTypes.includes(request.resourceType())) {
return false;
}
if (url && !request.url().match(url)) {
return false;
}
const origData = request.postData();
const data = origData ? origData : "";
if (postData && !data.match(postData)) {
return false;
}
return true;
} catch (err) {
logerr(err);
return false;
}
}
async setRequestInterception(options) {
if (!this.#page) {
throw new Error("No valid page");
}
const actOptions = Array.isArray(options) ? options : [options];
if (actOptions.length <= 0) {
logwarn("##browser LsdPage invalid paras in setRequestInterception");
return false;
}
const firstRequestInterception = this.#resquestInterceptionOptions.length <= 0;
for (const option of actOptions) {
switch (option.action) {
case "abort":
case "fulfill":
this.#resquestInterceptionOptions.push(option);
break;
default:
(0, import_utils3.unreachable)(option.action);
}
}
if (firstRequestInterception && this.#resquestInterceptionOptions.length > 0) {
this.#page.route("**", async (route) => {
try {
for (const option of actOptions) {
const { requestMatch, action, fulfill } = option;
const request = route.request();
const matchedFlag = !requestMatch || this.#checkRequestMatch(request, requestMatch);
if (this.#isDebugTask) {
if (matchedFlag) {
loginfo(`##browser matched request ${request.method()} ${request.url()}`);
} else {
logdbg(`##browser unmatched request ${request.method()} ${request.url()}`);
}
}
if (matchedFlag) {
switch (action) {
case "abort":
await route.abort();
break;
case "fulfill":
const body = fulfill ? fulfill : `<html><body><h1>${request.url()}</h1></body></html>`;
route.fulfill({
status: 200,
// contentType: "text/html; charset=utf-8", // "text/plain",
body
});
break;
default:
(0, import_utils3.unreachable)(action);
}
return true;
} else {
}
}
await route.continue();
return true;
} catch (err) {
logerr(err);
return false;
}
});
}
return true;
}
async #responseListener(response) {
try {
const pageUrl = this.#page ? this.#page.url() : "";
if (!response.ok()) {
return;
}
const request = response.request();
if (!request) {
return;
}
for (const option of this.#responseInterceptionOptions) {
const { requestMatch, responseMatch, responseItems, handler, handlerOptions = {} } = option;
let matchedFlag = !requestMatch || this.#checkRequestMatch(request, requestMatch);
if (matchedFlag && responseMatch) {
const { minLength, maxLength } = responseMatch;
const text = await response.text();
const len = text.length;
if (minLength && minLength > 0 && len < minLength || maxLength && maxLength > 0 && len > maxLength) {
matchedFlag = false;
}
}
if (this.#isDebugTask) {
if (matchedFlag) {
loginfo(`##browser matched response ${request.method()} ${request.url()}`);
} else {
logdbg(`##browser unmatched response ${request.method()} ${request.url()}`);
}
}
if (!matchedFlag) {
continue;
}
if (Array.isArray(responseItems)) {
const requestMethod = request.method();
const requestUrl = request.url();
const reqData2 = request.postData();
const requestData = reqData2 ? reqData2 : "";
const responseData = await response.text();
responseItems.push({
pageUrl,
requestMethod,
requestUrl,
requestData,
responseData
});
loginfo(`##browser cache matched response: ${requestUrl}`);
}
if (typeof handler === "function") {
const pageData = { pageUrl, cookies: "" };
await handler(response, handlerOptions, pageData);
}
}
return;
} catch (err) {
logerr(err);
return;
}
}
async setResponseInterception(options) {
if (!this.#page) {
throw new Error("No valid page");
}
const actOptions = Array.isArray(options) ? options : [options];
if (actOptions.length <= 0) {
logwarn("##browser LsdPage invalid paras in setResponseInterception");
return false;
}
const firstResponseInterception = this.#responseInterceptionOptions.length <= 0;
for (const option of actOptions) {
if (option?.responseItems || option?.handler) {
this.#responseInterceptionOptions.push(option);
} else {
throw new Error(`Invalid ResponseInterceptionOption`);
}
}
if (firstResponseInterception && this.#responseInterceptionOptions.length > 0) {
this.#responseCb = this.#responseListener.bind(this);
this.#page.on("response" /* PAGE_RESPONSE */, this.#responseCb);
}
return true;
}
async setStateData(stateData) {
return await this.#lsdBrowserContext.setStateData(stateData);
}
async setUserAgent(userAgent) {
if (userAgent) {
throw new Error(`Playwright does not support page.setUserAgent by now`);
}
return false;
}
async setViewportSize(viewPortSize) {
if (!this.#page) {
throw new Error("No valid page");
}
await this.#page.setViewportSize(viewPortSize);
return true;
}
async stateData() {
if (!this.#page) {
throw new Error("No valid page");
}
const cookies = await this.#getCookies(this.#page);
const localStorage = await this.#getLocalStorage(this.#page);
return { cookies, localStorage };
}
status() {
return this.#status;
}
async title() {
if (!this.#page) {
throw new Error("No valid page");
}
return await this.#page.title();
}
url() {
if (!this.#page) {
throw new Error("No valid page");
}
return this.#page.url();
}
use() {
if (this.#status === "busy") {
throw new Error(`Page ${this.#pageId} is already busy!!!`);
}
this.#status = "busy";
logdbg(`##browser LsdPage ${this.#pageId} is allocated`);
return true;
}
async waitForElement(selector, options = {}) {
if (!this.#page) {
throw new Error("No valid page");
}
const locator = this.#page.locator(selector);
const { timeout = 3e4, state = "visible" } = options;
await locator.waitFor({ state, timeout });
return true;
}
async waitForNavigation(options) {
if (!this.#page) {
throw new Error("No valid page");
}
const { url = "", timeout = 3e4, waitUntil = "load" } = options;
const newWaitUntil = this.#getWaitUntil(waitUntil);
if (url) {
await this.#page.waitForURL(url, { timeout, waitUntil: newWaitUntil });
} else if (newWaitUntil === "commit") {
throw new Error("commit is not supported in PlaywrightPage.waitForNavigation");
} else {
await this.#page.waitForLoadState(newWaitUntil, { timeout });
}
return true;
}
async windowMember(keys) {
if (!this.#page) {
throw new Error("No valid page");
}
if (!this.#page || !Array.isArray(keys) || keys.length <= 0 || keys.length > 20) {
return "";
}
const content = await this.#page.evaluate(
(keys2) => {
let retObj = window;
for (const key of keys2) {
if (!key) {
break;
} else if (typeof retObj !== "object" || !retObj) {
return "";
} else {
retObj = retObj[key];
}
}
if (typeof retObj === "string") {
return retObj;
} else if (typeof retObj === "number") {
return String(retObj);
} else if (typeof retObj === "boolean") {
return String(Number(retObj));
} else if (!retObj) {
return "";
} else if (typeof retObj === "object") {
try {
return JSON.stringify(retObj);
} catch (err) {
return "";
}
} else if (typeof retObj === "bigint") {
return String(retObj);
} else {
return "";
}
},
keys
);
return content;
}
_origPage() {
return this.#page;
}
};
// src/playwright/api.ts
var PlaywrightApiContext = class {
#apiRequestContext;
#status;
constructor(apiRequestContext) {
this.#apiRequestContext = apiRequestContext;
this.#status = "normal";
}
async fetch(url, options = {}) {
if (this.#status !== "normal") {
throw new Error(`ApiContext has already been destroyed`);
}
const apiResponse = await this.#apiRequestContext.fetch(url, options);
const headers = apiResponse.headers();
const status = apiResponse.status();
const statusText = apiResponse.statusText();
const text = await apiResponse.text();
const responseUrl = apiResponse.url();
return { headers, status, statusText, text, url: responseUrl };
}
async stateData() {
if (this.#status !== "normal") {
throw new Error(`ApiContext has already been destroyed`);
}
const storageState = await this.#apiRequestContext.storageState();
const { cookies, origins: localStorage } = storageState;
return { cookies, localStorage };
}
async destroy() {
await this.#apiRequestContext.dispose();
this.#status = "destroyed";
return true;
}
};
// src/playwright/context.ts
var PlaywrightBrowserContext = class extends import_node_events2.default {
#lsdBrowser;
#browserIdx;
#browserContextIdx;
#browserContext;
#browserContextCreationMethod;
#apiContext;
#createTime;
#lastStatusUpdateTime;
#status;
#incognito;
#proxy;
#maxPagesPerBrowserContext;
#maxPageFreeSeconds;
#maxViewportOfNewPage;
#lsdPages;
#nextPageIdx;
#gettingPage;
async #initPages() {
if (!this.#browserContext) {
throw new Error("Invalid browserContext");
}
const pages = this.#browserContext.pages();
const openType = this.#lsdBrowser.browserCreationMethod();
const lastStatusUpdateTime = (0, import_utils4.getCurrentUnixTime)();
for (const page of pages) {
const pageInfo = { browserIdx: this.#browserIdx, browserContextIdx: this.#browserContextIdx, pageIdx: this.#nextPageIdx++, openType, openTime: this.#createTime, lastStatusUpdateTime, taskId: 0, relatedId: 0, misc: {} };
const lsdPage = new PlaywrightPage(this, page, pageInfo);
this.#lsdPages.push(lsdPage);
if (this.#maxViewportOfNewPage) {
await lsdPage.maximizeViewport();
}
}
}
constructor(lsdBrowser, browserContext, browserContextCreationMethod, incognito = false, proxy = null, browserIdx = 0, browserContextIdx = 0, maxPagesPerBrowserContext = 20, maxPageFreeSeconds = 0, maxViewportOfNewPage = true) {
if (!lsdBrowser || typeof lsdBrowser.browserContexts !== "function") {
throw new Error(`Invalid lsdBrowser parameter`);
}
if (!browserContext || typeof browserContext.setOffline !== "function") {
throw new Error(`Invalid playwright browserContext parameter`);
}
super();
this.#lsdBrowser = lsdBrowser;
this.#browserIdx = browserIdx;
this.#browserContextIdx = browserContextIdx;
this.#browserContext = browserContext;
this.#browserContextCreationMethod = browserContextCreationMethod;
const apiRequestContext = browserContext.request;
this.#apiContext = new PlaywrightApiContext(apiRequestContext);
const currentTime = (0, import_utils4.getCurrentUnixTime)();
this.#createTime = currentTime;
this.#lastStatusUpdateTime = currentTime;
this.#status = "free";
this.#incognito = incognito === false ? false : true;
this.#proxy = proxy?.proxyUrl ? proxy : null;
this.#maxPagesPerBrowserContext = maxPagesPerBrowserContext;
this.#maxPageFreeSeconds = maxPageFreeSeconds;
this.#maxViewportOfNewPage = maxViewportOfNewPage;
this.#lsdPages = [];
this.#nextPageIdx = 1;
this.#gettingPage = false;
loginfo(`##browser LsdBrowserContext ${this.id()} is created`);
this.#initPages();
browserContext.on("page" /* BROWSERCONTEXT_PAGE */, async (page) => {
const pageInfo = page.pageInfo;
if (pageInfo) {
const { browserIdx: browserIdx2, browserContextIdx: browserContextIdx2, pageIdx } = pageInfo;
logwarn(`##browser page-${browserIdx2}-${browserContextIdx2}-${pageIdx} has been already created`);
} else {
const currentTime2 = (0, import_utils4.getCurrentUnixTime)();
const pageInfo2 = { browserIdx: this.#browserIdx, browserContextIdx: this.#browserContextIdx, pageIdx: this.#nextPageIdx++, openType: "other", openTime: currentTime2, lastStatusUpdateTime: currentTime2, taskId: 0, relatedId: 0, misc: {} };
const lsdPage = new PlaywrightPage(this, page, pageInfo2);
this.#lsdPages.push(lsdPage);
if (this.#maxViewportOfNewPage) {
await lsdPage.maximizeViewport();
}
}
});
browserContext.on("close" /* BROWSERCONTEXT_CLOSE */, (bc) => {
if (browserContext !== bc) {
logerr(`##browser different browserContext in browserContext.on("close")`);
}
this.#lsdBrowser.emit("browserContextClose" /* BROWSER_CONTEXT_CLOSE */, this);
});
this.on("pageClose" /* PAGE_CLOSE */, (lsdPage) => {
if (!(lsdPage instanceof PlaywrightPage)) {
logerr(`##browser LsdBrowserContext invalid data in