UNPKG

@letsscrapedata/controller

Version:

Unified browser / HTML controller interfaces that support patchright, camoufox, playwright, puppeteer and cheerio

1,552 lines (1,535 loc) 268 kB
"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