diff --git a/examples/puppeteer/tests/example.test.ts b/examples/puppeteer/tests/example.test.ts index d5f803c..c04cea2 100644 --- a/examples/puppeteer/tests/example.test.ts +++ b/examples/puppeteer/tests/example.test.ts @@ -32,9 +32,9 @@ describe("Example Test Suite", () => { it("perform test with pilot", async () => { await copilot.pilot( - "Enter https://example.com/, press on more information, " + - "expect to be redirected to IANA site, summarize the findings. " + - "Open in non-headless mode.", + "On `https://github.com/wix-incubator/detox-copilot`, " + + "open the Commits page and summarize the latest commits. " + + "Open the browser with GUI.", ); }); }); diff --git a/src/drivers/puppeteer/getCleanDOM.ts b/src/drivers/puppeteer/getCleanDOM.ts new file mode 100644 index 0000000..29ae5a9 --- /dev/null +++ b/src/drivers/puppeteer/getCleanDOM.ts @@ -0,0 +1,73 @@ +import * as puppeteer from "puppeteer"; + +/** + * Get clean DOM from the page content + * - Removes hidden elements + * - Removes ads, analytics, tracking elements + * - Removes unnecessary attributes + * - Removes empty elements + * @param page + */ +export default async function getCleanDOM(page: puppeteer.Page) { + await page.waitForSelector("body"); + + return await page.evaluate(() => { + const copiedDocument = document.cloneNode(true) as Document; + + copiedDocument + .querySelectorAll('[hidden], [aria-hidden="true"]') + .forEach((el) => el.remove()); + + const removeSelectors = [ + "script", + "style", + "link", + "meta", + "noscript", + "iframe", + '[class*="ads"]', + '[id*="ads"]', + '[class*="analytics"]', + '[class*="tracking"]', + "footer", + "header", + "nav", + "path", + "aside", + ]; + + const allowedAttributes = [ + "src", + "href", + "alt", + "title", + "aria-label", + "aria-labelledby", + "aria-describedby", + "aria-hidden", + "role", + "class", + "id", + "data-*", + ]; + + copiedDocument.querySelectorAll("*").forEach((el) => { + Array.from(el.attributes).forEach((attr) => { + if (!allowedAttributes.includes(attr.name)) { + el.removeAttribute(attr.name); + } + }); + + if (!el.innerHTML.trim()) { + el.remove(); + } + }); + + removeSelectors.forEach((selector) => { + copiedDocument.querySelectorAll(selector).forEach((el) => el.remove()); + }); + + const mainContent = copiedDocument.body.innerHTML; + return mainContent.replace(/\s+/g, " ").trim(); + }); +} diff --git a/src/drivers/puppeteer/index.ts b/src/drivers/puppeteer/index.ts index 20823dd..89a978e 100644 --- a/src/drivers/puppeteer/index.ts +++ b/src/drivers/puppeteer/index.ts @@ -2,6 +2,7 @@ import { TestingFrameworkAPICatalog, TestingFrameworkDriver } from "@/types"; import * as puppeteer from "puppeteer"; import path from "path"; import fs from "fs"; +import getCleanDOM from "./getCleanDOM"; export class PuppeteerFrameworkDriver implements TestingFrameworkDriver { private currentPage?: puppeteer.Page; @@ -59,7 +60,7 @@ export class PuppeteerFrameworkDriver implements TestingFrameworkDriver { ); } - return await this.currentPage.content(); + return await getCleanDOM(this.currentPage); } /** @@ -87,7 +88,8 @@ export class PuppeteerFrameworkDriver implements TestingFrameworkDriver { guidelines: [ "Options can specify `headless`, `slowMo`, `args`, etc.", "Useful for running tests in a headless browser environment.", - 'Prefer passing `headless: "new"` to `puppeteer.launch() unless mentioned that it is required not to.', + 'Prefer passing `headless: "new"` to `puppeteer.launch() unless mentioned that ' + + "it is required not to (e.g. launching with GUI was mentioned).", ], }, { @@ -99,6 +101,15 @@ export class PuppeteerFrameworkDriver implements TestingFrameworkDriver { "Useful for cleaning up resources and freeing memory.", ], }, + { + signature: "await getCurrentPage().setUserAgent(userAgent)", + description: "Overrides the default user agent string.", + example: 'await getCurrentPage().setUserAgent("UA-TEST");', + guidelines: [ + "Affects the value of `navigator.userAgent`.", + "Useful for simulating different browsers or bots.", + ], + }, ], }, { @@ -184,6 +195,7 @@ export class PuppeteerFrameworkDriver implements TestingFrameworkDriver { example: 'await getCurrentPage().waitForFunction(() => document.querySelector("#status").innerText === "Loaded");', guidelines: [ + "Useful as an alternative for using click, type, etc. to wait for a condition. Preferred practice.", "Useful for waiting on dynamic content or conditions.", "Avoid tight polling intervals to prevent excessive CPU usage.", ], @@ -209,6 +221,7 @@ export class PuppeteerFrameworkDriver implements TestingFrameworkDriver { "Simulates a mouse click on the element matched by the selector.", example: 'await getCurrentPage().click("#submit-button");', guidelines: [ + "Prefer evaluated click instead of click when possible.", "Waits for the element to be visible and enabled.", "Throws an error if the element is not found or not interactable.", "Avoid clicking on elements that change page state without proper synchronization.", @@ -221,6 +234,7 @@ export class PuppeteerFrameworkDriver implements TestingFrameworkDriver { "Types text into an element matched by the selector.", example: 'await getCurrentPage().type("#username", "myUser123");', guidelines: [ + "Prefer evaluated type instead of type when possible.", "Suitable for input and textarea elements.", "Simulates individual key presses with optional delay.", "Ensure the element is focusable before typing.", @@ -231,6 +245,7 @@ export class PuppeteerFrameworkDriver implements TestingFrameworkDriver { description: "Focuses on the element matched by the selector.", example: 'await getCurrentPage().focus("#search-input");', guidelines: [ + "Prefer evaluated focus instead of focus when possible.", "Useful before performing keyboard actions.", "Waits for the element to be interactable.", ], @@ -241,6 +256,7 @@ export class PuppeteerFrameworkDriver implements TestingFrameworkDriver { example: 'await getCurrentPage().select("#country-select", "US");', guidelines: [ + "Prefer evaluated select instead of select when possible.", "Supports selecting multiple options if the `