diff --git a/package-lock.json b/package-lock.json index 532061f..f45c440 100644 --- a/package-lock.json +++ b/package-lock.json @@ -590,8 +590,6 @@ "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", "dev": true, - "optional": true, - "peer": true, "dependencies": { "@jridgewell/trace-mapping": "0.3.9" }, @@ -604,8 +602,6 @@ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", "dev": true, - "optional": true, - "peer": true, "dependencies": { "@jridgewell/resolve-uri": "^3.0.3", "@jridgewell/sourcemap-codec": "^1.4.10" @@ -1959,33 +1955,25 @@ "version": "1.0.11", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", - "dev": true, - "optional": true, - "peer": true + "dev": true }, "node_modules/@tsconfig/node12": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", - "dev": true, - "optional": true, - "peer": true + "dev": true }, "node_modules/@tsconfig/node14": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", - "dev": true, - "optional": true, - "peer": true + "dev": true }, "node_modules/@tsconfig/node16": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", - "dev": true, - "optional": true, - "peer": true + "dev": true }, "node_modules/@types/babel__core": { "version": "7.20.5", @@ -2446,8 +2434,6 @@ "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", "dev": true, - "optional": true, - "peer": true, "dependencies": { "acorn": "^8.11.0" }, @@ -2553,9 +2539,7 @@ "version": "4.1.3", "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", - "dev": true, - "optional": true, - "peer": true + "dev": true }, "node_modules/argparse": { "version": "2.0.1", @@ -3309,9 +3293,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", - "dev": true, - "optional": true, - "peer": true + "dev": true }, "node_modules/cross-fetch": { "version": "4.0.0", @@ -3561,8 +3543,6 @@ "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", "dev": true, - "optional": true, - "peer": true, "engines": { "node": ">=0.3.1" } @@ -7856,8 +7836,7 @@ "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", "dev": true, - "optional": true, - "peer": true, + "license": "MIT", "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -8052,9 +8031,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", - "dev": true, - "optional": true, - "peer": true + "dev": true }, "node_modules/v8-to-istanbul": { "version": "9.3.0", @@ -8345,8 +8322,6 @@ "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", "dev": true, - "optional": true, - "peer": true, "engines": { "node": ">=6" } @@ -8418,6 +8393,7 @@ } }, "packages/drivers/web-utils": { + "name": "@wix-pilot/web-utils", "version": "1.0.0", "devDependencies": { "@playwright/test": "^1.50.0", @@ -8427,7 +8403,8 @@ "esbuild": "^0.24.2", "jest-image-snapshot": "^6.4.0", "jsdom": "^26.0.0", - "puppeteer": "^20.8.0" + "puppeteer": "^20.8.0", + "ts-node": "^10.9.2" } }, "packages/examples": { diff --git a/packages/drivers/playwright/examples/example.test.ts b/packages/drivers/playwright/examples/example.test.ts index 32cf9cb..b863d9a 100644 --- a/packages/drivers/playwright/examples/example.test.ts +++ b/packages/drivers/playwright/examples/example.test.ts @@ -33,7 +33,7 @@ describe("Example Test Suite", () => { it("perform test with pilot", async () => { await pilot.autopilot( - "Open https://www.wix.com/domains and search for the domain Shraga.com, is it available?", + "Open https://github.com/wix-incubator/pilot and tell me what was the last commit about and who have created it", ); }); }); diff --git a/packages/drivers/playwright/index.ts b/packages/drivers/playwright/index.ts index d8c218c..f176934 100644 --- a/packages/drivers/playwright/index.ts +++ b/packages/drivers/playwright/index.ts @@ -133,492 +133,18 @@ await page.waitForLoadState('load') ], }, { - title: "Navigation", + title: "Matchers", items: [ - { - signature: "await page.goto(url[, options])", - description: "Navigates to a URL.", - example: `const page = getCurrentPage(); -if (page) { - await page.goto('https://example.com'); - // Verify navigation success using assertions - await page.waitForLoadState('load') -}`, - guidelines: [ - "Use only await page.waitForLoadState('load') after page.goto(). never use expect()", - "Always verify navigation success with assertions.", - "Avoid using waitUntil options - use assertions instead.", - "Set proper timeouts at browser/context level.", - ], - }, - { - signature: "await page.reload()", - description: "Reloads the current page.", - example: `const page = getCurrentPage(); -if (page) { - await page.reload(); -}`, - guidelines: [ - "Avoid explicit waits - let assertions handle timing.", - "Good for refreshing stale content.", - ], - }, - ], - }, - { - title: "State Checks", - items: [ - { - signature: "await page.waitForLoadState('load')", - description: "Waits for the page to fully load.", - example: `const page = getCurrentPage(); - if (page) { - await page.goto('https://www.wix.com/domains'); - await page.waitForLoadState('load'); - }`, - guidelines: [ - "Waits until the 'load' event is fired.", - "Ensures the page is fully loaded before proceeding.", - "Useful when automatic waiting is insufficient.", - ], - }, - { - signature: "const currentURL = page.url()", - description: "Gets the current URL of the page.", - example: `const page = getCurrentPage(); - if (page) { - const currentURL = page.url(); - console.log('Current URL:', currentURL); - }`, - guidelines: [ - "Returns the current URL as a string synchronously.", - "Useful for logging or conditional logic.", - "No need to use `await` since it's a synchronous method.", - ], - }, - { - signature: "const title = await page.title()", - description: "Gets the page title.", - example: `const page = getCurrentPage(); - if (page) { - const title = await page.title(); - console.log('Page Title:', title); - }`, - guidelines: [ - "Returns the current page title.", - "Use `await` to get the title asynchronously.", - "Auto-waits for the title to be available.", - ], - }, - { - signature: "const isVisible = await locator.isVisible()", - description: "Checks if an element is visible.", - example: `const page = getCurrentPage(); - if (page) { - const isVisible = await page.getByText('Sign In').isVisible(); - if (isVisible) { - // Proceed with sign-in process - } - }`, - guidelines: [ - "Returns a boolean value immediately.", - "Good for conditional logic and flow control.", - "Does not auto-wait or retry", - ], - }, - { - signature: "const isEnabled = await locator.isEnabled()", - description: "Checks if an element is enabled.", - example: `const page = getCurrentPage(); - if (page) { - const isEnabled = await page.getByRole('button', { name: 'Submit' }).isEnabled(); - if (isEnabled) { - // Click the submit button - await page.getByRole('button', { name: 'Submit' }).click(); - } - }`, - guidelines: [ - "Returns a boolean indicating if the element is enabled.", - "Useful to check if a button or input is interactive.", - "Good for conditional actions based on element state.", - ], - }, - { - signature: "const isChecked = await locator.isChecked()", - description: "Checks if a checkbox or radio button is checked.", - example: `const page = getCurrentPage(); - if (page) { - const isChecked = await page.getByLabel('Accept Terms').isChecked(); - if (!isChecked) { - await page.getByLabel('Accept Terms').check(); - } - }`, - guidelines: [ - "Returns a boolean indicating if the element is checked.", - "Applicable to checkboxes and radio buttons.", - "Good for ensuring the desired state before proceeding.", - ], - }, - { - signature: "const isDisabled = await locator.isDisabled()", - description: "Checks if an element is disabled.", - example: `const page = getCurrentPage(); - if (page) { - const isDisabled = await page.getByRole('button', { name: 'Submit' }).isDisabled(); - if (isDisabled) { - console.log('Submit button is disabled'); - } - }`, - guidelines: [ - "Returns a boolean indicating if the element is disabled.", - "Useful for checking if an element is not interactive.", - "Can inform flow control based on element availability.", - ], - }, - { - signature: "const isEditable = await locator.isEditable()", - description: "Checks if an input element is editable.", - example: `const page = getCurrentPage(); - if (page) { - const isEditable = await page.getByPlaceholder('Enter your name').isEditable(); - if (isEditable) { - await page.fill('input[placeholder="Enter your name"]', 'John Doe'); - } - }`, - guidelines: [ - "Returns a boolean indicating if the element can be edited.", - "Applicable to input, textarea, and contenteditable elements.", - "Good for ensuring the field is ready for input.", - ], - }, - { - signature: "const isHidden = await locator.isHidden()", - description: "Checks if an element is hidden.", - example: `const page = getCurrentPage(); - if (page) { - const isHidden = await page.getByText('Loading...').isHidden(); - if (isHidden) { - // Proceed since the loading indicator is gone - } - }`, - guidelines: [ - "Returns a boolean value immediately.", - "Useful for conditional flow based on element invisibility.", - "Does not auto-wait or retry; use `expect()` for assertions.", - ], - }, - { - signature: "const isDetached = await locator.isDetached()", - description: "Checks if an element is detached from the DOM.", - example: `const page = getCurrentPage(); - if (page) { - const modal = page.getByRole('dialog'); - // Perform some action that closes the modal - await page.getByRole('button', { name: 'Close' }).click(); - const isDetached = await modal.isDetached(); - if (isDetached) { - // Modal has been removed from the DOM - } - }`, - guidelines: [ - "Returns true if the element is not attached to the DOM.", - "Useful to confirm that an element has been completely removed.", - "Can inform subsequent actions that depend on DOM structure.", - ], - }, - { - signature: "const textContent = await locator.textContent()", - description: "Retrieves the text content of an element.", - example: `const page = getCurrentPage(); - if (page) { - const message = await page.getByTestId('welcome-message').textContent(); - console.log('Welcome message:', message); - }`, - guidelines: [ - "Returns the text inside the element.", - "Useful for extracting text for further processing.", - "Consider using `expect().toHaveText()` for assertions.", - ], - }, - { - signature: "const value = await locator.inputValue()", - description: "Retrieves the current value of an input field.", - example: `const page = getCurrentPage(); - if (page) { - const email = await page.getByPlaceholder('Email').inputValue(); - console.log('Entered email:', email); - }`, - guidelines: [ - "Returns the value property of input and textarea elements.", - "Useful for validating or using the input value in logic.", - "Consider using `expect().toHaveValue()` for assertions.", - ], - }, { signature: - "const elementHandles = await locator.elementHandles()", - description: "Gets all matching element handles.", - example: `const page = getCurrentPage(); - if (page) { - const items = await page.getByRole('listitem').elementHandles(); - console.log('Number of items:', items.length); - }`, - guidelines: [ - "Retrieves an array of element handles matching the locator.", - "Useful for performing actions on multiple elements.", - "Remember to dispose of handles if necessary to prevent memory leaks.", - ], - }, - { - signature: "await page.waitForTimeout(timeout)", - description: "Waits for a specified amount of time.", - example: `const page = getCurrentPage(); - if (page) { - // Wait for 2 seconds - await page.waitForTimeout(2000); - }`, - guidelines: [ - "Pauses execution for the given time in milliseconds.", - "Useful for debugging or waiting in non-deterministic cases.", - "Prefer explicit waits for events or conditions when possible.", - ], - }, - ], - }, - { - title: "Assertions", - items: [ - { - signature: "await expect(locator).toBeVisible()", - description: "Asserts that the element is visible.", - example: `const page = getCurrentPage(); - if (page) { - await expect(page.getByText('Welcome')).toBeVisible(); - }`, - guidelines: [ - "Checks that the element is visible on the page.", - "Automatically waits for the condition to be met.", - "Throws an error if the element is not visible.", - ], - }, - { - signature: "await expect(locator).toHaveText(expectedText)", - description: - "Asserts that the element's text content matches the expected text.", - example: `const page = getCurrentPage(); - if (page) { - await expect(page.getByTestId('username')).toHaveText('John Doe'); - }`, - guidelines: [ - "Compares the element's text content with the expected text.", - "Supports partial matches and regular expressions.", - "Waits until the condition is met or times out.", - ], - }, - { - signature: "await expect(page).toHaveURL(expectedURL)", + 'document.querySelector(\'[aria-pilot-category="categoryName"][aria-pilot-index="index"]\')', description: - "Asserts that the page's URL matches the expected URL.", - example: `const page = getCurrentPage(); - if (page) { - await expect(page).toHaveURL('https://www.example.com/dashboard'); - }`, - guidelines: [ - "Waits for the URL to match the expected value.", - "Can use regular expressions or glob patterns for matching.", - "Useful for verifying navigation.", - ], - }, - { - signature: "await expect(locator).toHaveValue(expectedValue)", - description: - "Asserts that an input element has the expected value.", - example: `const page = getCurrentPage(); - if (page) { - await expect(page.getByPlaceholder('Email')).toHaveValue('user@example.com'); - }`, - guidelines: [ - "Checks the 'value' property of input elements.", - "Automatically waits for the condition.", - "Useful for validating user input.", - ], - }, - { - signature: "await expect(locator).toBeEnabled()", - description: "Asserts that the element is enabled.", - example: `const page = getCurrentPage(); - if (page) { - await expect(page.getByRole('button', { name: 'Submit' })).toBeEnabled(); - }`, - guidelines: [ - "Checks that the element can be interacted with.", - "Waits until the element is enabled.", - "Throws an error if the element remains disabled.", - ], - }, - { - signature: "await expect(locator).toBeDisabled()", - description: "Asserts that the element is disabled.", - example: `const page = getCurrentPage(); - if (page) { - await expect(page.getByRole('button', { name: 'Submit' })).toBeDisabled(); - }`, - guidelines: [ - "Checks that the element is not interactive.", - "Waits until the element is disabled.", - "Useful for ensuring correct UI state.", - ], - }, - { - signature: "await expect(locator).toBeChecked()", - description: - "Asserts that a checkbox or radio button is checked.", - example: `const page = getCurrentPage(); - if (page) { - await expect(page.getByLabel('Accept Terms')).toBeChecked(); - }`, - guidelines: [ - "Checks that the element is checked.", - "Waits for the condition to be met.", - "Throws an error if the element is not checked.", - ], - }, - { - signature: "await expect(locator).toBeEditable()", - description: - "Asserts that an input or textarea element is editable.", - example: `const page = getCurrentPage(); - if (page) { - await expect(page.getByPlaceholder('Enter your name')).toBeEditable(); - }`, - guidelines: [ - "Checks that the element can be edited.", - "Waits until the element is editable.", - "Useful before attempting to fill an input.", - ], - }, - { - signature: "await expect(locator).toHaveAttribute(name, value)", - description: - "Asserts that the element has the specified attribute value.", - example: `const page = getCurrentPage(); - if (page) { - await expect(page.getByTestId('profile-link')).toHaveAttribute('href', '/profile'); - }`, - guidelines: [ - "Checks the value of a specified attribute.", - "Supports regular expressions and partial matching.", - "Waits for the condition to be met.", - ], - }, - { - signature: "await expect(locator).toHaveClass(expectedClass)", - description: - "Asserts that the element has the specified CSS class.", - example: `const page = getCurrentPage(); - if (page) { - await expect(page.getByRole('button', { name: 'Submit' })).toHaveClass('btn-primary'); - }`, - guidelines: [ - "Checks the 'class' attribute of the element.", - "Supports multiple classes and partial matches.", - "Waits for the condition to be met.", - ], - }, - { - signature: "await expect(locator).toContainText(expectedText)", - description: - "Asserts that the element's text contains the expected text.", - example: `const page = getCurrentPage(); - if (page) { - await expect(page.getByTestId('notification')).toContainText('Success'); - }`, - guidelines: [ - "Checks that the element's text includes the expected substring.", - "Supports regular expressions and partial matches.", - "Waits for the condition to be met.", - ], - }, - { - signature: "await expect(locator).toHaveJSProperty(name, value)", - description: - "Asserts that the element has the specified JavaScript property.", - example: `const page = getCurrentPage(); - if (page) { - await expect(page.getByTestId('toggle')).toHaveJSProperty('checked', true); - }`, - guidelines: [ - "Checks the value of a JavaScript property on the element.", - "Useful for properties not exposed via attributes.", - "Waits for the condition to be met.", - ], - }, - { - signature: "await expect(locator).not.toBeVisible()", - description: "Asserts that the element is not visible.", - example: `const page = getCurrentPage(); - if (page) { - await expect(page.getByText('Loading...')).not.toBeVisible(); - }`, - guidelines: [ - "Checks that the element is hidden or does not exist.", - "Waits for the condition to be met.", - "Useful for ensuring elements are not present before proceeding.", - ], - }, - { - signature: "await expect(page).toHaveTitle(expectedTitle)", - description: - "Asserts that the page's title matches the expected title.", - example: `const page = getCurrentPage(); - if (page) { - await expect(page).toHaveTitle('Dashboard - MyApp'); - }`, - guidelines: [ - "Waits for the page's title to match the expected value.", - "Supports regular expressions and partial matching.", - "Useful for verifying page navigation.", - ], - }, - { - signature: "await expect(page).toHaveScreenshot([options])", - description: - "Asserts that the page's screenshot matches a stored reference image.", - example: `const page = getCurrentPage(); - if (page) { - await expect(page).toHaveScreenshot('dashboard.png'); - }`, - guidelines: [ - "Compares the current screenshot with a baseline image.", - "Can specify options like threshold and mask areas.", - "Useful for visual regression testing.", - ], - }, - { - signature: "await expect(locator).toHaveCount(expectedCount)", - description: - "Asserts that the locator resolves to a specific number of elements.", - example: `const page = getCurrentPage(); - if (page) { - await expect(page.locator('.todo-item')).toHaveCount(3); - }`, - guidelines: [ - "Checks the number of elements matching the locator.", - "Waits for the condition to be met.", - "Useful for validating dynamic lists.", - ], - }, - { - signature: "await expect(locator).toBeEmpty()", - description: "Asserts that the element is empty.", - example: `const page = getCurrentPage(); - if (page) { - await expect(page.getByTestId('search-results')).toBeEmpty(); - }`, + "Selects a specific element within a category based on its index.", + example: `const firstButton = await page.evaluate(() => document.querySelector('[aria-pilot-category="button"][aria-pilot-index="27"]');`, guidelines: [ - "Checks that the element has no text content.", - "Waits for the condition to be met.", - "Useful for asserting initial states.", + "Replace `categoryName` with the desired category and `index` with the specific index as a string.", + "Indexing is zero-based and increments per category as elements are found.", + "Use this to interact with or verify a specific instance of a category, ensuring the exact element is targeted.", ], }, ], diff --git a/packages/drivers/puppeteer/examples/example.test.ts b/packages/drivers/puppeteer/examples/example.test.ts index a4e2a7d..a46835f 100644 --- a/packages/drivers/puppeteer/examples/example.test.ts +++ b/packages/drivers/puppeteer/examples/example.test.ts @@ -32,7 +32,7 @@ describe("Example Test Suite", () => { it("perform test with pilot", async () => { await pilot.autopilot( - "Open https://www.wix.com/domains, and search for the domain Shraga.com, is it available?. if there is cookies message decline it", + "Open https://github.com/wix-incubator/pilot and click on the 'Code' tab, open with GUI browser", ); }); }); diff --git a/packages/drivers/web-utils/package.json b/packages/drivers/web-utils/package.json index f60c380..5768e21 100644 --- a/packages/drivers/web-utils/package.json +++ b/packages/drivers/web-utils/package.json @@ -15,7 +15,7 @@ "url": "git+https://github.com/wix-incubator/pilot.git" }, "scripts": { - "build": "tsc && tsc-alias && ts-node src/test/createBundleDriverUtilsFile.ts", + "build": "tsc && tsc-alias && ts-node src/scripts/createBundledFiles.ts", "test": "echo skipping tests", "test:web-utils": "jest", "bump-version:patch": "npm version patch && git commit -am 'chore: bump patch version' && git push", @@ -39,6 +39,7 @@ "esbuild": "^0.24.2", "jest-image-snapshot": "^6.4.0", "jsdom": "^26.0.0", - "puppeteer": "^20.8.0" - } + "puppeteer": "^20.8.0", + "ts-node": "^10.9.2" + } } diff --git a/packages/drivers/web-utils/src/index.ts b/packages/drivers/web-utils/src/index.ts index 5e1ccc6..bac2d06 100644 --- a/packages/drivers/web-utils/src/index.ts +++ b/packages/drivers/web-utils/src/index.ts @@ -2,83 +2,96 @@ import fs from "fs"; import path from "path"; import { Page } from "./types"; -declare global { - interface Window { - driverUtils: typeof import("./utils").default; - } -} - export default class WebTestingFrameworkDriverHelper { protected currentPage?: Page; constructor() {} /** - * Injects bundled code and marks important elements + * Executes a bundled script within the page context. + * @param page - The web page instance. + * @param bundleRelativePath - Relative path to the bundled script. + * @param returnResult - Whether to return the result of the script execution. */ - private async injectCodeAndMarkElements(page: Page): Promise { - const bundledCodePath = require.resolve("../dist/web-utils.browser.js"); - const isInjected = await page.evaluate( - () => typeof window.driverUtils?.markImportantElements === "function", - ); + private async executeBundledScript( + page: Page, + bundleRelativePath: string, + ): Promise { + const bundlePath = path.resolve(__dirname, bundleRelativePath); + const bundleString = fs.readFileSync(bundlePath, "utf8"); + await page.evaluate((code: string) => eval(code), bundleString); + } - if (!isInjected) { - await page.addScriptTag({ - content: fs.readFileSync(bundledCodePath, "utf8"), - }); - console.log("Bundled script injected into the page."); - } else { - console.log("Bundled script already injected. Skipping injection."); - } + /** + * Injects bundled code and marks important elements. + */ + async markImportantElements(page: Page): Promise { + await this.executeBundledScript( + page, + "../dist/markImportantElements.bundle.js", + ); + } - await page.evaluate(() => window.driverUtils.markImportantElements()); + /** + * Manipulates element styles. + */ + async highlightMarkedElements(page: Page): Promise { + await this.executeBundledScript( + page, + "../dist/highlightMarkedElements.bundle.js", + ); } /** - * Manipulates element styles + * Cleans up style changes. */ - private async manipulateStyles(page: Page): Promise { - await page.evaluate(() => { - window.driverUtils.manipulateElementStyles(); - }); + async removeMarkedElementsHighlights(page: Page): Promise { + await this.executeBundledScript( + page, + "../dist/removeMarkedElementsHighlights.bundle.js", + ); } /** - * Cleans up style changes + * Gets the clean view hierarchy as a string. */ - private async cleanUpStyleChanges(page: Page): Promise { - await page.evaluate(() => { - window.driverUtils.cleanupStyleChanges(); + async createMarkedViewHierarchy(page: Page): Promise { + return await page.evaluate(() => { + return window.createMarkedViewHierarchy(); }); } /** - * Captures a snapshot image + * Captures a snapshot image. */ async captureSnapshotImage(): Promise { if (!this.currentPage) { return undefined; } - const fileName = `temp/snapshot_${Date.now()}.png`; + const tempDir = "temp"; + const fileName = `snapshot_${Date.now()}.png`; + const filePath = path.resolve(tempDir, fileName); // Create temp directory if it doesn't exist - if (!fs.existsSync("temp")) { - fs.mkdirSync("temp"); + if (!fs.existsSync(tempDir)) { + fs.mkdirSync(tempDir); } - await this.injectCodeAndMarkElements(this.currentPage); - await this.manipulateStyles(this.currentPage); + await this.markImportantElements(this.currentPage); + await this.highlightMarkedElements(this.currentPage); + await this.currentPage.screenshot({ - path: fileName, + path: filePath, fullPage: true, }); - await this.cleanUpStyleChanges(this.currentPage); - return path.resolve(fileName); + + await this.removeMarkedElementsHighlights(this.currentPage); + return filePath; } /** - * Captures the view hierarchy as a string + * Captures the view hierarchy as a string. */ async captureViewHierarchyString(): Promise { if (!this.currentPage) { @@ -87,21 +100,20 @@ export default class WebTestingFrameworkDriverHelper { "START A NEW ONE BASED ON THE ACTION NEED OR RAISE AN ERROR" ); } - await this.injectCodeAndMarkElements(this.currentPage); - return await this.currentPage.evaluate(() => { - return window.driverUtils.extractCleanViewStructure(); - }); + + await this.markImportantElements(this.currentPage); + return await this.createMarkedViewHierarchy(this.currentPage); } /** - * Sets current working page + * Sets the current working page. */ - setCurrentPage(page: Page) { + setCurrentPage(page: Page): void { this.currentPage = page; } /** - * Gets the current page identifier + * Gets the current page. */ getCurrentPage(): Page | undefined { return this.currentPage; diff --git a/packages/drivers/web-utils/src/scripts/createBundledFiles.ts b/packages/drivers/web-utils/src/scripts/createBundledFiles.ts new file mode 100644 index 0000000..4b1e9de --- /dev/null +++ b/packages/drivers/web-utils/src/scripts/createBundledFiles.ts @@ -0,0 +1,68 @@ +import path from "path"; +import fs from "fs"; +import * as esbuild from "esbuild"; +import * as utils from "../utils"; + +async function bundleUtil( + methodName: string, + outputFilePath: string, +): Promise { + const tempDir = path.resolve(__dirname, "./temp"); + await fs.promises.mkdir(tempDir, { recursive: true }); + + const tempFilePath = path.join(tempDir, `${methodName}.ts`); + const fileContent = ` + import {${methodName}} from "../../utils"; + ${methodName}(); + `; + + await fs.promises.writeFile(tempFilePath, fileContent, "utf8"); + + try { + const { outputFiles } = await esbuild.build({ + entryPoints: [tempFilePath], + bundle: true, + write: false, + format: "iife", + target: ["chrome100"], + }); + + const [output] = outputFiles || []; + + if (!output) { + throw new Error("Bundle generation failed: No output produced"); + } + + await fs.promises.writeFile(outputFilePath, output.text, "utf8"); + return output.text; + } catch (error) { + console.error(`Bundling failed for ${tempFilePath}:`, error); + throw error; + } finally { + await fs.promises.unlink(tempFilePath); + } +} + +(async () => { + const methodNames = Object.keys(utils); + + for (const methodName of methodNames) { + const outputFilePath = path.resolve( + __dirname, + `../../dist/${methodName}.bundle.js`, + ); + + try { + await bundleUtil(methodName, outputFilePath); + } catch (error) { + console.error(`Failed to bundle method ${methodName}:`, error); + } + } + + const tempDir = path.resolve(__dirname, "./temp"); + try { + await fs.promises.rmdir(tempDir, { recursive: true }); + } catch (err) { + console.error(`Error deleting temporary directory: ${err}`); + } +})(); diff --git a/packages/drivers/web-utils/src/test-setup.ts b/packages/drivers/web-utils/src/test-setup.ts deleted file mode 100644 index ded957e..0000000 --- a/packages/drivers/web-utils/src/test-setup.ts +++ /dev/null @@ -1,38 +0,0 @@ -import * as path from "path"; -import { build } from "esbuild"; - -export async function bundleDriverUtils(): Promise { - try { - const result = await build({ - entryPoints: [path.resolve(__dirname, "./index.ts")], - bundle: true, - write: false, - format: "iife", - globalName: "driverUtils", - platform: "browser", - target: "es2015", - minify: false, - outfile: "out.js", - }); - - if (!("outputFiles" in result) || !result.outputFiles?.[0]?.text) { - throw new Error("No output generated from esbuild"); - } - - // Add a wrapper to handle default export - const bundled = ` - (function(global) { - ${result.outputFiles[0].text} - // Handle ES module default export - if (typeof driverUtils !== 'undefined' && driverUtils.default) { - global.driverUtils = driverUtils.default; - } - })(typeof window !== 'undefined' ? window : global); - `; - - return bundled; - } catch (error) { - console.error("Bundling failed:", error); - throw error; - } -} diff --git a/packages/drivers/web-utils/src/test/__image_snapshots__/wix-domains-puppeteer-desktop.png b/packages/drivers/web-utils/src/test/__image_snapshots__/wix-domains-puppeteer-desktop.png index 2ec7a36..3d2d898 100644 Binary files a/packages/drivers/web-utils/src/test/__image_snapshots__/wix-domains-puppeteer-desktop.png and b/packages/drivers/web-utils/src/test/__image_snapshots__/wix-domains-puppeteer-desktop.png differ diff --git a/packages/drivers/web-utils/src/test/bundle.ts b/packages/drivers/web-utils/src/test/bundle.ts deleted file mode 100644 index eeea8da..0000000 --- a/packages/drivers/web-utils/src/test/bundle.ts +++ /dev/null @@ -1,32 +0,0 @@ -import * as esbuild from "esbuild"; -import * as path from "path"; -import fs from "fs"; - -export async function bundleDriverUtils(): Promise { - try { - const result = await esbuild.build({ - entryPoints: [path.resolve(__dirname, "../utils.ts")], - bundle: true, - write: false, - format: "iife", - globalName: "driverUtils", - target: ["chrome100"], - footer: { - js: "window.driverUtils = driverUtils.default;", - }, - }); - - if (!result.outputFiles?.[0]) { - throw new Error("Bundle generation failed: No output produced"); - } - const outputPath = path.resolve( - __dirname, - "../../dist/web-utils.browser.js", - ); - fs.writeFileSync(outputPath, result.outputFiles[0].text, "utf8"); - return result.outputFiles[0].text; - } catch (error) { - console.error("Bundling failed:", error); - throw error; - } -} diff --git a/packages/drivers/web-utils/src/test/createBundleDriverUtilsFile.ts b/packages/drivers/web-utils/src/test/createBundleDriverUtilsFile.ts deleted file mode 100644 index c0e30f7..0000000 --- a/packages/drivers/web-utils/src/test/createBundleDriverUtilsFile.ts +++ /dev/null @@ -1,20 +0,0 @@ -import fs from "fs"; -import path from "path"; -import { bundleDriverUtils } from "./bundle"; - -async function createBundledDriverUtilsFile(): Promise { - try { - const bundleText = await bundleDriverUtils(); - - const outputPath = path.resolve( - __dirname, - "../../dist/web-utils.browser.js", - ); - fs.writeFileSync(outputPath, bundleText, "utf8"); - } catch (error) { - console.error("Failed to create bundled driver utils file:", error); - throw error; - } -} - -createBundledDriverUtilsFile(); diff --git a/packages/drivers/web-utils/src/test/setup.ts b/packages/drivers/web-utils/src/test/setup.ts index 066d36d..fab3f02 100644 --- a/packages/drivers/web-utils/src/test/setup.ts +++ b/packages/drivers/web-utils/src/test/setup.ts @@ -10,22 +10,14 @@ import type { Page as PuppeteerPage, } from "puppeteer"; import { toMatchImageSnapshot } from "jest-image-snapshot"; -import { bundleDriverUtils } from "./bundle"; type FrameworkDriver = "puppeteer" | "playwright"; expect.extend({ toMatchImageSnapshot }); -declare global { - interface Window { - driverUtils: typeof import("../utils").default; - } -} - export interface TestContext { browser: PuppeteerBrowser | PlaywrightBrowser; context?: PlaywrightContext; page: PuppeteerPage | PlaywrightPage; - bundledCode: string; } export async function setupTestEnvironment( @@ -33,8 +25,6 @@ export async function setupTestEnvironment( driver: FrameworkDriver, ): Promise { try { - const bundledCode = await bundleDriverUtils(); - let browser: PuppeteerBrowser | PlaywrightBrowser; let context: PlaywrightContext | undefined; let page: PuppeteerPage | PlaywrightPage; @@ -62,9 +52,8 @@ export async function setupTestEnvironment( page.setDefaultNavigationTimeout(10000); await page.goto(`file://${__dirname}/test-pages/${htmlFileName}`); - await page.addScriptTag({ content: bundledCode }); - return { browser, context, page, bundledCode }; + return { browser, context, page }; } catch (error) { console.error("Setup failed:", error); throw error; diff --git a/packages/drivers/web-utils/src/test/wix-domains-playwright.test.ts b/packages/drivers/web-utils/src/test/wix-domains-playwright.test.ts index 9e6e7c6..fe01b52 100644 --- a/packages/drivers/web-utils/src/test/wix-domains-playwright.test.ts +++ b/packages/drivers/web-utils/src/test/wix-domains-playwright.test.ts @@ -4,10 +4,13 @@ import { teardownTestEnvironment, } from "./setup"; import { Page as PlaywrightPage } from "playwright"; +import WebTestingFrameworkDriverHelper from "../index"; describe("Wix Domains Page Testing", () => { let testContext: TestContext; let page: PlaywrightPage; + const driverUtils: WebTestingFrameworkDriverHelper = + new WebTestingFrameworkDriverHelper(); beforeAll(async () => { testContext = await setupTestEnvironment("wix-domains.html", "playwright"); @@ -19,39 +22,33 @@ describe("Wix Domains Page Testing", () => { }); beforeEach(async () => { - await page.evaluate(() => { - window.driverUtils.cleanupStyleChanges(); - }); + await driverUtils.removeMarkedElementsHighlights(page); }); it("should match the screenshot against the baseline image", async () => { - await page.evaluate(() => { - window.driverUtils.markImportantElements(); - window.driverUtils.manipulateElementStyles(); - }); - + await driverUtils.markImportantElements(page); + await driverUtils.highlightMarkedElements(page); await page.setViewportSize({ width: 800, height: 600 }); await page.addStyleTag({ content: ` * { animation: none !important; transition: none !important; + will-change: auto !important; } `, }); const screenshot = await page.screenshot({ fullPage: true }); expect(screenshot).toMatchImageSnapshot({ customSnapshotIdentifier: "wix-domains-playwright-desktop", - failureThreshold: 0.05, + failureThreshold: 0.1, failureThresholdType: "percent", }); }); it("should generate the expected clean view structure", async () => { - const structure = await page.evaluate(() => { - window.driverUtils.markImportantElements(); - return window.driverUtils.extractCleanViewStructure(); - }); + await driverUtils.markImportantElements(page); + const structure = await driverUtils.createMarkedViewHierarchy(page); expect(structure).toMatchSnapshot("wix-domains-clean-view-structure"); }); }); diff --git a/packages/drivers/web-utils/src/test/wix-domains-puppeteer.test.ts b/packages/drivers/web-utils/src/test/wix-domains-puppeteer.test.ts index 1110315..d948fd3 100644 --- a/packages/drivers/web-utils/src/test/wix-domains-puppeteer.test.ts +++ b/packages/drivers/web-utils/src/test/wix-domains-puppeteer.test.ts @@ -4,10 +4,13 @@ import { setupTestEnvironment, teardownTestEnvironment, } from "./setup"; +import WebTestingFrameworkDriverHelper from "../index"; describe("Wix Domains Page Testing", () => { let testContext: TestContext; let page: PuppeteerPage; + const driverUtils: WebTestingFrameworkDriverHelper = + new WebTestingFrameworkDriverHelper(); beforeAll(async () => { testContext = await setupTestEnvironment("wix-domains.html", "puppeteer"); @@ -19,22 +22,19 @@ describe("Wix Domains Page Testing", () => { }); beforeEach(async () => { - await page.evaluate(() => { - window.driverUtils.cleanupStyleChanges(); - }); + await driverUtils.removeMarkedElementsHighlights(page); }); it("should match the screenshot against the baseline image", async () => { - await page.evaluate(() => { - window.driverUtils.markImportantElements(); - window.driverUtils.manipulateElementStyles(); - }); + await driverUtils.markImportantElements(page); + await driverUtils.highlightMarkedElements(page); await page.setViewport({ width: 800, height: 600 }); await page.addStyleTag({ content: ` * { animation: none !important; transition: none !important; + will-change: auto !important; } `, }); @@ -48,10 +48,8 @@ describe("Wix Domains Page Testing", () => { }); it("should generate the expected clean view structure", async () => { - const structure = await page.evaluate(() => { - window.driverUtils.markImportantElements(); - return window.driverUtils.extractCleanViewStructure(); - }); + await driverUtils.markImportantElements(page); + const structure = await driverUtils.createMarkedViewHierarchy(page); expect(structure).toMatchSnapshot("wix-domains-clean-view-structure"); }); }); diff --git a/packages/drivers/web-utils/src/utils.ts b/packages/drivers/web-utils/src/utils.ts index 0b21da9..2fa3d6f 100644 --- a/packages/drivers/web-utils/src/utils.ts +++ b/packages/drivers/web-utils/src/utils.ts @@ -2,6 +2,12 @@ import getElementCategory, { tags } from "./getElementCategory"; import isElementHidden from "./isElementHidden"; import { ElementCategory } from "./types"; +declare global { + interface Window { + createMarkedViewHierarchy: () => string; + } +} + const CATEGORY_COLORS: Record = { button: ["#ff0000", "#ffffff"], link: ["#0aff0a", "#000000"], @@ -24,132 +30,125 @@ const ATTRIBUTE_WHITELIST: Record = { const ESSENTIAL_ELEMENTS = ["HTML", "HEAD", "BODY"]; -export interface DriverUtils { - markImportantElements: (options?: { includeHidden?: boolean }) => void; - extractCleanViewStructure: () => string; - manipulateElementStyles: () => void; - cleanupStyleChanges: () => void; +export function markImportantElements(options?: { includeHidden?: boolean }) { + const selector = tags.join(","); + const elements = Array.from(document.querySelectorAll(selector)); + const categoryCounts = new Map(); + + elements.forEach((el) => { + if (!options?.includeHidden && isElementHidden(el)) return; + + const category = getElementCategory(el); + if (!category) return; + + const index = categoryCounts.get(category) || 0; + el.setAttribute("aria-pilot-category", category); + el.setAttribute("aria-pilot-index", index.toString()); + categoryCounts.set(category, index + 1); + }); } -const utils: DriverUtils = { - markImportantElements(options?: { includeHidden?: boolean }) { - const selector = tags.join(","); - const elements = Array.from(document.querySelectorAll(selector)); - const categoryCounts = new Map(); - - elements.forEach((el) => { - if (!options?.includeHidden && isElementHidden(el)) return; - - const category = getElementCategory(el); - if (!category) return; - - const index = categoryCounts.get(category) || 0; - el.setAttribute("aria-pilot-category", category); - el.setAttribute("aria-pilot-index", index.toString()); - categoryCounts.set(category, index + 1); - }); - }, - - extractCleanViewStructure() { - const clone = document.documentElement.cloneNode(true) as HTMLElement; - - function processElement(element: Element, depth = 0): string { - const children = Array.from(element.children); - let structure = ""; - - // Process all children - let childStructure = ""; - for (const child of children) { - const childStr = processElement(child, depth + 1); - if (childStr) { - childStructure += childStr; - } - } +export function createMarkedViewHierarchy() { + const clone = document.documentElement.cloneNode(true) as HTMLElement; - // Determine if current element is important or has important descendants - const isImportantElement = - element.hasAttribute("aria-pilot-category") || - ESSENTIAL_ELEMENTS.includes(element.tagName); + function processElement(element: Element, depth = 0): string { + const children = Array.from(element.children); + let structure = ""; - if (isImportantElement || childStructure) { - const category = element.getAttribute("aria-pilot-category"); - const index = element.getAttribute("aria-pilot-index"); - const indent = " ".repeat(depth); + // Process all children + let childStructure = ""; + for (const child of children) { + const childStr = processElement(child, depth + 1); + if (childStr) { + childStructure += childStr; + } + } - structure += `${indent}<${element.tagName.toLowerCase()}`; + // Determine if current element is important or has important descendants + const isImportantElement = + element.hasAttribute("aria-pilot-category") || + ESSENTIAL_ELEMENTS.includes(element.tagName); + + if (isImportantElement || childStructure) { + const category = element.getAttribute("aria-pilot-category"); + const index = element.getAttribute("aria-pilot-index"); + const indent = " ".repeat(depth); + + structure += `${indent}<${element.tagName.toLowerCase()}`; + + // Add relevant attributes + const tagName = element.tagName.toLowerCase(); + const allowedAttrs = [ + ...ATTRIBUTE_WHITELIST["*"], + ...(ATTRIBUTE_WHITELIST[tagName] || []), + ]; + + Array.from(element.attributes) + .filter((attr) => allowedAttrs.includes(attr.name.toLowerCase())) + .forEach((attr) => { + structure += ` ${attr.name}="${attr.value}"`; + }); + + if (category) { + structure += ` data-category="${category}" data-index="${index}"`; + } - // Add relevant attributes - const tagName = element.tagName.toLowerCase(); - const allowedAttrs = [ - ...ATTRIBUTE_WHITELIST["*"], - ...(ATTRIBUTE_WHITELIST[tagName] || []), - ]; + structure += ">\n"; - Array.from(element.attributes) - .filter((attr) => allowedAttrs.includes(attr.name.toLowerCase())) - .forEach((attr) => { - structure += ` ${attr.name}="${attr.value}"`; - }); + if (childStructure) { + structure += childStructure; + } - if (category) { - structure += ` data-category="${category}" data-index="${index}"`; - } + structure += `${indent}\n`; + } - structure += ">\n"; + return structure; + } - if (childStructure) { - structure += childStructure; - } + return processElement(clone); +} - structure += `${indent}\n`; +export function highlightMarkedElements() { + const styleId = "aria-pilot-styles"; + const oldStyle = document.getElementById(styleId); + if (oldStyle) oldStyle.remove(); + + const style = document.createElement("style"); + style.id = styleId; + style.textContent = Object.entries(CATEGORY_COLORS) + .map( + ([category, color]) => ` + [aria-pilot-category="${category}"] { + position: relative !important; + box-shadow: 0 0 0 2px ${color[0]} !important; + z-index: auto !important; } - return structure; - } + [aria-pilot-category="${category}"]::before { + content: "${category} #" attr(aria-pilot-index); + position: absolute !important; + top: -20px !important; + left: 0 !important; + background: ${color[0]}; + opacity: 0.5; + color: ${color[1]}; + font: 10px monospace; + padding: 2px 4px; + white-space: nowrap; + z-index: 2147483647 !important; + pointer-events: none !important; + } + `, + ) + .join("\n"); + document.head.appendChild(style); +} - return processElement(clone); - }, - - manipulateElementStyles() { - const styleId = "aria-pilot-styles"; - const oldStyle = document.getElementById(styleId); - if (oldStyle) oldStyle.remove(); - - const style = document.createElement("style"); - style.id = styleId; - style.textContent = Object.entries(CATEGORY_COLORS) - .map( - ([category, color]) => ` - [aria-pilot-category="${category}"] { - position: relative !important; - box-shadow: 0 0 0 2px ${color[0]} !important; - z-index: auto !important; - } - - [aria-pilot-category="${category}"]::before { - content: "${category} #" attr(aria-pilot-index); - position: absolute !important; - top: -20px !important; - left: 0 !important; - background: ${color[0]}; - opacity: 0.5; - color: ${color[1]}; - font: 10px monospace; - padding: 2px 4px; - white-space: nowrap; - z-index: 2147483647 !important; - pointer-events: none !important; - } - `, - ) - .join("\n"); - document.head.appendChild(style); - }, - - cleanupStyleChanges() { - const style = document.getElementById("aria-pilot-styles"); - style?.remove(); - }, -}; +export function removeMarkedElementsHighlights() { + const style = document.getElementById("aria-pilot-styles"); + style?.remove(); +} -export default utils; +if (typeof window !== "undefined") { + window.createMarkedViewHierarchy = createMarkedViewHierarchy; +}