import type { ElementHandle, Page } from "puppeteer"; import fs from "node:fs"; import { InteractiveElementHandler } from "./interactive-element-handler.js"; import type { IAgentDriver } from "../agent/index.js"; export type SelectorOpts = { debug?: boolean; }; export interface IElementSelector { getElements(page: Page, selector?: string): Promise[]>; } export class ElementSelector implements IElementSelector { page?: Page; interactiveElementHandler: InteractiveElementHandler; skipped: ElementHandle[] = []; selected: ElementHandle[] = []; debug: boolean; driver: IAgentDriver; constructor(driver: IAgentDriver, opts: SelectorOpts = {}) { this.driver = driver; this.debug = Boolean(opts.debug); this.interactiveElementHandler = this.createInteractiveElementHandler(); } public async getElements(page: Page, selector = "*") { let id = 0; const elements: ElementHandle[] = await page .mainFrame() .$$(this.elementSelector); let limit = 400; for (const element of elements) { if (--limit < 0) { this.skipElement(element); break; } const nextElem = await this.getNext(element, ++id, selector); if (nextElem) { this.selectElement(element); } } this.logElements(); return elements; } protected createInteractiveElementHandler() { return new InteractiveElementHandler(this.driver); } protected get elementSelector() { return this.selectors.join(","); } get selectors() { return [ "input:not([type=hidden]):not([disabled])", "textarea:not([disabled])", "button:not([disabled])", '[tabindex]:not([tabindex="-1"])', "select:not([disabled])", 'a[href]:not([href="javascript:void(0)"]):not([href="#"])', ]; } protected async getNext( element: ElementHandle, id: number, selector: string, ) { return await this.interactiveElementHandler.nextInteractiveElement( element, id, selector, ); } protected skipElement(element: ElementHandle) { this.skipped.push(element); } protected selectElement(element: ElementHandle) { this.selected.push(element); } protected writeElements( fileName: string, elements: ElementHandle[], ) { if (!this.debug) { return; } fs.writeFileSync(fileName, JSON.stringify(elements, null, 2)); } protected logElements() { this.writeElements("skipped.json", this.skipped); this.writeElements("selected.json", this.selected); } }