diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml new file mode 100644 index 0000000..3eb1314 --- /dev/null +++ b/.github/workflows/playwright.yml @@ -0,0 +1,27 @@ +name: Playwright Tests +on: + push: + branches: [ main, master ] + pull_request: + branches: [ main, master ] +jobs: + test: + timeout-minutes: 60 + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: lts/* + - name: Install dependencies + run: npm ci + - name: Install Playwright Browsers + run: npx playwright install --with-deps + - name: Run Playwright tests + run: npx playwright test + - uses: actions/upload-artifact@v4 + if: ${{ !cancelled() }} + with: + name: playwright-report + path: playwright-report/ + retention-days: 30 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3e9277b --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +node_modules/ +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ +.env +.DS_Store +artifacts/ \ No newline at end of file diff --git a/README.md b/README.md index 3eee2d3..f0a2d7a 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,85 @@ -# TestAutomation-Hands-on -This repository is for doing hands-on practice for the Playwright Framework +# WordPress WooCommerce E2E Testing with Playwright + +This repository contains a set of end-to-end (E2E) test scripts designed to automate various processes in WordPress. The tests are written using the **wp-e2e-utils-playwright** framework functions, providing a powerful and flexible solution for testing WordPress websites. + +The test scripts in this repository cover the following areas: + +- **Product Management** + - Create a simple product. + - Add product categories and tags. + - Upload product images. + - Set pricing and inventory. + - Publish and verify product visibility. + +- **Coupon Management** + - Create different types of coupons (percentage, fixed amount). + - Apply coupons during checkout. + - Verify discount calculations. + +- **User Management** + - Create a customer user. + - Customer places an order. + - Admin/Store manager reviews the order. + +### Exclusive Features + +- **wp-e2e-utils-playwright Framework**: The tests are written using the **wp-e2e-utils-playwright** framework functions, which streamline the process of automating interactions with a WordPress site. This framework helps interact with the WordPress admin panel, as well as simulating user behavior on the frontend. + +- **Test Data Cleanup**: Each test script includes a **data cleanup** feature to ensure that the site storage is not overpopulated with test data. After each test execution, the relevant test data (e.g., products, users, coupons) is removed, maintaining a clean environment for subsequent tests. + +## Table of Contents + +- [Installation](#installation) +- [Prerequisites](#prerequisites) +- [Running the Tests](#running-the-tests) + +## Installation + +To get started, follow the steps below to set up the testing environment. + +1. **Clone the repository**: + +```bash +git clone https://github.com/rishavjeet/test-automation-hands-on.git +``` +2. **Install the dependencies**: + +This project uses npm to manage dependencies. Make sure you have Node.js and npm installed. Then, run the following command to install the required packages: + +```bash +npm install +``` +2. **Install the dependencies**: + +Create a .env file in the root directory of the project. You will need to add your WordPress admin credentials and other relevant environment configurations. + +```bash +WP_USERNAME= +WP_PASSWORD= +WP_BASE_URL= +``` + +## Prerequisites + +Before running the test scripts, ensure that you have the following: + +- `Node.js` (version 14 or later) +- `npm` (Node Package Manager) +- Playwright (installed automatically through npm) +- WordPress site setup for testing (local or staging environment) + +## Running the Tests + +Once the environment is set up, you can run the test scripts using Playwright’s test runner. + +1. Run all tests: + +```bash +npx playwright test +``` +2. Run a specific test script: + +```bash +npx playwright test path/to/test-script.spec.js +``` +*Replace `path/to/test-script.spec.js` with the path to the specific test script you want to run* \ No newline at end of file diff --git a/assets/product_test_image.png b/assets/product_test_image.png new file mode 100644 index 0000000..71d9f37 Binary files /dev/null and b/assets/product_test_image.png differ diff --git a/config/global-setup.js b/config/global-setup.js new file mode 100644 index 0000000..a8c26b3 --- /dev/null +++ b/config/global-setup.js @@ -0,0 +1,34 @@ +/** + * External dependencies + */ +import { request } from '@playwright/test'; +// import type { FullConfig } from '@playwright/test'; + +/** + * WordPress dependencies + */ +import { RequestUtils } from '@wordpress/e2e-test-utils-playwright'; + +async function globalSetup( config ) { + const { storageState, baseURL, userAgent } = config.projects[ 0 ].use; + + console.log('config :'+ config.projects[0].use.userAgent); + const storageStatePath = + typeof storageState === 'string' ? storageState : undefined; + + const requestContext = await request.newContext( { + baseURL, + userAgent, + } ); + + const requestUtils = new RequestUtils( requestContext, { + storageStatePath, + } ); + + // Authenticate and save the storageState to disk. + await requestUtils.setupRest(); + + await requestContext.dispose(); +} + +export default globalSetup; \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..d1bdf11 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,2343 @@ +{ + "name": "testautomation-hands-on", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "testautomation-hands-on", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "dotenv": "^16.4.7" + }, + "devDependencies": { + "@playwright/test": "^1.49.1", + "@types/node": "^22.10.3", + "@wordpress/e2e-test-utils-playwright": "^1.14.0" + } + }, + "node_modules/@formatjs/ecma402-abstract": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-2.3.1.tgz", + "integrity": "sha512-Ip9uV+/MpLXWRk03U/GzeJMuPeOXpJBSB5V1tjA6kJhvqssye5J5LoYLc7Z5IAHb7nR62sRoguzrFiVCP/hnzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@formatjs/fast-memoize": "2.2.5", + "@formatjs/intl-localematcher": "0.5.9", + "decimal.js": "10", + "tslib": "2" + } + }, + "node_modules/@formatjs/fast-memoize": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-2.2.5.tgz", + "integrity": "sha512-6PoewUMrrcqxSoBXAOJDiW1m+AmkrAj0RiXnOMD59GRaswjXhm3MDhgepXPBgonc09oSirAJTsAggzAGQf6A6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "2" + } + }, + "node_modules/@formatjs/icu-messageformat-parser": { + "version": "2.9.7", + "resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.9.7.tgz", + "integrity": "sha512-cuEHyRM5VqLQobANOjtjlgU7+qmk9Q3fDQuBiRRJ3+Wp3ZoZhpUPtUfuimZXsir6SaI2TaAJ+SLo9vLnV5QcbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@formatjs/ecma402-abstract": "2.3.1", + "@formatjs/icu-skeleton-parser": "1.8.11", + "tslib": "2" + } + }, + "node_modules/@formatjs/icu-skeleton-parser": { + "version": "1.8.11", + "resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.8.11.tgz", + "integrity": "sha512-8LlHHE/yL/zVJZHAX3pbKaCjZKmBIO6aJY1mkVh4RMSEu/2WRZ4Ysvv3kKXJ9M8RJLBHdnk1/dUQFdod1Dt7Dw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@formatjs/ecma402-abstract": "2.3.1", + "tslib": "2" + } + }, + "node_modules/@formatjs/intl-localematcher": { + "version": "0.5.9", + "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.5.9.tgz", + "integrity": "sha512-8zkGu/sv5euxbjfZ/xmklqLyDGQSxsLqg8XOq88JW3cmJtzhCP8EtSJXlaKZnVO4beEaoiT9wj4eIoCQ9smwxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "2" + } + }, + "node_modules/@paulirish/trace_engine": { + "version": "0.0.39", + "resolved": "https://registry.npmjs.org/@paulirish/trace_engine/-/trace_engine-0.0.39.tgz", + "integrity": "sha512-2Y/ejHX5DDi5bjfWY/0c/BLVSfQ61Jw1Hy60Hnh0hfEO632D3FVctkzT4Q/lVAdvIPR0bUaok9JDTr1pu/OziA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "third-party-web": "latest" + } + }, + "node_modules/@playwright/test": { + "version": "1.49.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.49.1.tgz", + "integrity": "sha512-Ky+BVzPz8pL6PQxHqNRW1k3mIyv933LML7HktS8uik0bUXNCdPhoS/kLihiO1tMf/egaJb4IutXd7UywvXEW+g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.49.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@puppeteer/browsers": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.6.1.tgz", + "integrity": "sha512-aBSREisdsGH890S2rQqK82qmQYU3uFpSH8wcZWHgHzl3LfzsxAKbLNiAG9mO8v1Y0UICBeClICxPJvyr0rcuxg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "debug": "^4.4.0", + "extract-zip": "^2.0.1", + "progress": "^2.0.3", + "proxy-agent": "^6.5.0", + "semver": "^7.6.3", + "tar-fs": "^3.0.6", + "unbzip2-stream": "^1.4.3", + "yargs": "^17.7.2" + }, + "bin": { + "browsers": "lib/cjs/main-cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@puppeteer/browsers/node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@puppeteer/browsers/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@puppeteer/browsers/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@sentry-internal/tracing": { + "version": "7.120.2", + "resolved": "https://registry.npmjs.org/@sentry-internal/tracing/-/tracing-7.120.2.tgz", + "integrity": "sha512-eo2F8cP6X+vr54Mp6vu+NoQEDz0M5O24Tz8jPY0T1CpiWdwCmHb7Sln+oLXeQ3/LlWdVQihBfKDBZfBdUfsBTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sentry/core": "7.120.2", + "@sentry/types": "7.120.2", + "@sentry/utils": "7.120.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@sentry/core": { + "version": "7.120.2", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.120.2.tgz", + "integrity": "sha512-eurLBFQJC7WWWYoEna25Z9I/GJjqAmH339tv52XP8sqXV7B5hRcHDcfrsT/UGHpU316M24p3lWhj0eimtCZ0SQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sentry/types": "7.120.2", + "@sentry/utils": "7.120.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@sentry/integrations": { + "version": "7.120.2", + "resolved": "https://registry.npmjs.org/@sentry/integrations/-/integrations-7.120.2.tgz", + "integrity": "sha512-bMvL2fD3TGLM5YAUoQ2Qz6bYeVU8f7YRFNSjKNxK4EbvFgAU9j1FD6EKg0V0RNOJYnJjGIZYMmcWTXBbVTJL6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sentry/core": "7.120.2", + "@sentry/types": "7.120.2", + "@sentry/utils": "7.120.2", + "localforage": "^1.8.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@sentry/node": { + "version": "7.120.2", + "resolved": "https://registry.npmjs.org/@sentry/node/-/node-7.120.2.tgz", + "integrity": "sha512-ZnW9gpIGaoU+vYZyVZca9dObfmWYiXEWIMUM/JXaFb8AhP1OXvYweNiU0Pe/gNrz4oGAogU8scJc70ar7Vj0ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sentry-internal/tracing": "7.120.2", + "@sentry/core": "7.120.2", + "@sentry/integrations": "7.120.2", + "@sentry/types": "7.120.2", + "@sentry/utils": "7.120.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@sentry/types": { + "version": "7.120.2", + "resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.120.2.tgz", + "integrity": "sha512-FWVoiblHQJ892GaOqdXx/5/n5XDLF28z81vJ0lCY49PMh8waz8LJ0b9RSmt9tasSDl0OQ7eUlPl1xu1jTrv1NA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@sentry/utils": { + "version": "7.120.2", + "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-7.120.2.tgz", + "integrity": "sha512-jgnQlw11mRfQrQRAXbq4zEd+tbYwHel5eqeS/oU6EImXRjmHNtS79nB8MHvJeQu1FMCpFs1Ymrrs5FICwS6VeQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sentry/types": "7.120.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@tootallnate/quickjs-emscripten": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", + "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.10.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.3.tgz", + "integrity": "sha512-DifAyw4BkrufCILvD3ucnuN8eydUfc/C1GlyrnI+LK6543w5/L3VeVgf05o3B4fqSXP1dKYLOZsKfutpxPzZrw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.20.0" + } + }, + "node_modules/@types/yauzl": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", + "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@wordpress/e2e-test-utils-playwright": { + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/@wordpress/e2e-test-utils-playwright/-/e2e-test-utils-playwright-1.14.0.tgz", + "integrity": "sha512-G9r3ZysgzAmUbR4bjGAEEP6P2RCIAG8uMU7yyzxOAHegINSbF3shEZKvVNBeKxNwHKAVa9koh/niGN3U4Kr6Rw==", + "dev": true, + "license": "GPL-2.0-or-later", + "dependencies": { + "change-case": "^4.1.2", + "form-data": "^4.0.0", + "get-port": "^5.1.1", + "lighthouse": "^12.2.2", + "mime": "^3.0.0", + "web-vitals": "^4.2.1" + }, + "engines": { + "node": ">=18.12.0", + "npm": ">=8.19.2" + }, + "peerDependencies": { + "@playwright/test": ">=1" + } + }, + "node_modules/agent-base": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", + "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/ast-types": { + "version": "0.13.4", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", + "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/axe-core": { + "version": "4.10.2", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.10.2.tgz", + "integrity": "sha512-RE3mdQ7P3FRSe7eqCWoeQ/Z9QXrtniSjp1wUjt5nRC3WIpz5rSCve6o3fsZ2aCpJtrZjSZgjwXAoTO5k4tEI0w==", + "dev": true, + "license": "MPL-2.0", + "engines": { + "node": ">=4" + } + }, + "node_modules/b4a": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.7.tgz", + "integrity": "sha512-OnAYlL5b7LEkALw87fUVafQw5rVR9RjwGd4KUwNQ6DrrNmaVaUCgLipfVlzrPQ4tWOR9P0IXGNOx50jYCCdSJg==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/bare-events": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.5.0.tgz", + "integrity": "sha512-/E8dDe9dsbLyh2qrZ64PEPadOQ0F4gbl1sUJOrmph7xOiIxfY8vwab/4bFLh4Y88/Hk/ujKcrQKc+ps0mv873A==", + "dev": true, + "license": "Apache-2.0", + "optional": true + }, + "node_modules/bare-fs": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-2.3.5.tgz", + "integrity": "sha512-SlE9eTxifPDJrT6YgemQ1WGFleevzwY+XAP1Xqgl56HtcrisC2CHCZ2tq6dBpcH2TnNxwUEUGhweo+lrQtYuiw==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "bare-events": "^2.0.0", + "bare-path": "^2.0.0", + "bare-stream": "^2.0.0" + } + }, + "node_modules/bare-os": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-2.4.4.tgz", + "integrity": "sha512-z3UiI2yi1mK0sXeRdc4O1Kk8aOa/e+FNWZcTiPB/dfTWyLypuE99LibgRaQki914Jq//yAWylcAt+mknKdixRQ==", + "dev": true, + "license": "Apache-2.0", + "optional": true + }, + "node_modules/bare-path": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-2.1.3.tgz", + "integrity": "sha512-lh/eITfU8hrj9Ru5quUp0Io1kJWIk1bTjzo7JH1P5dWmQ2EL4hFUlfI8FonAhSlgIfhn63p84CDY/x+PisgcXA==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "bare-os": "^2.1.0" + } + }, + "node_modules/bare-stream": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.6.1.tgz", + "integrity": "sha512-eVZbtKM+4uehzrsj49KtCy3Pbg7kO1pJ3SKZ1SFrIH/0pnj9scuGGgUlNDf/7qS8WKtGdiJY5Kyhs/ivYPTB/g==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "streamx": "^2.21.0" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/basic-ftp": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.0.5.tgz", + "integrity": "sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/camel-case": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-4.1.2.tgz", + "integrity": "sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "pascal-case": "^3.1.2", + "tslib": "^2.0.3" + } + }, + "node_modules/capital-case": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/capital-case/-/capital-case-1.0.4.tgz", + "integrity": "sha512-ds37W8CytHgwnhGGTi88pcPyR15qoNkOpYwmMMfnWqqWgESapLqvDx6huFjQ5vqWSn2Z06173XNA7LtMOeUh1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3", + "upper-case-first": "^2.0.2" + } + }, + "node_modules/change-case": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/change-case/-/change-case-4.1.2.tgz", + "integrity": "sha512-bSxY2ws9OtviILG1EiY5K7NNxkqg/JnRnFxLtKQ96JaviiIxi7djMrSd0ECT9AC+lttClmYwKw53BWpOMblo7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "camel-case": "^4.1.2", + "capital-case": "^1.0.4", + "constant-case": "^3.0.4", + "dot-case": "^3.0.4", + "header-case": "^2.0.4", + "no-case": "^3.0.4", + "param-case": "^3.0.4", + "pascal-case": "^3.1.2", + "path-case": "^3.0.4", + "sentence-case": "^3.0.4", + "snake-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/chrome-launcher": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/chrome-launcher/-/chrome-launcher-1.1.2.tgz", + "integrity": "sha512-YclTJey34KUm5jB1aEJCq807bSievi7Nb/TU4Gu504fUYi3jw3KCIaH6L7nFWQhdEgH3V+wCh+kKD1P5cXnfxw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/node": "*", + "escape-string-regexp": "^4.0.0", + "is-wsl": "^2.2.0", + "lighthouse-logger": "^2.0.1" + }, + "bin": { + "print-chrome-path": "bin/print-chrome-path.js" + }, + "engines": { + "node": ">=12.13.0" + } + }, + "node_modules/chromium-bidi": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-0.11.0.tgz", + "integrity": "sha512-6CJWHkNRoyZyjV9Rwv2lYONZf1Xm0IuDyNq97nwSsxxP3wf5Bwy15K5rOvVKMtJ127jJBmxFUanSAOjgFRxgrA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "mitt": "3.0.1", + "zod": "3.23.8" + }, + "peerDependencies": { + "devtools-protocol": "*" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/configstore": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/configstore/-/configstore-5.0.1.tgz", + "integrity": "sha512-aMKprgk5YhBNyH25hj8wGt2+D52Sw1DRRIzqBwLp2Ya9mFmY8KPvvtvmna8SxVR9JMZ4kzMD68N22vlaRpkeFA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "dot-prop": "^5.2.0", + "graceful-fs": "^4.1.2", + "make-dir": "^3.0.0", + "unique-string": "^2.0.0", + "write-file-atomic": "^3.0.0", + "xdg-basedir": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/constant-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/constant-case/-/constant-case-3.0.4.tgz", + "integrity": "sha512-I2hSBi7Vvs7BEuJDr5dDHfzb/Ruj3FyvFyh7KLilAjNQw3Be+xgqUBA2W6scVEcL0hL1dwPRtIqEPVUCKkSsyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3", + "upper-case": "^2.0.2" + } + }, + "node_modules/crypto-random-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz", + "integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/csp_evaluator": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/csp_evaluator/-/csp_evaluator-1.1.1.tgz", + "integrity": "sha512-N3ASg0C4kNPUaNxt1XAvzHIVuzdtr8KLgfk1O8WDyimp1GisPAHESupArO2ieHk9QWbrJ/WkQODyh21Ps/xhxw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/data-uri-to-buffer": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", + "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/decimal.js": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.3.tgz", + "integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==", + "dev": true, + "license": "MIT" + }, + "node_modules/define-lazy-prop": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", + "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/degenerator": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz", + "integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ast-types": "^0.13.4", + "escodegen": "^2.1.0", + "esprima": "^4.0.1" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/devtools-protocol": { + "version": "0.0.1312386", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1312386.tgz", + "integrity": "sha512-DPnhUXvmvKT2dFA/j7B+riVLUt9Q6RKJlcppojL5CoRywJJKLDYnRlw0gTFKfgDPHP5E04UoB71SxoJlVZy8FA==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/dot-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz", + "integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/dot-prop": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz", + "integrity": "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-obj": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/dotenv": { + "version": "16.4.7", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", + "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/enquirer": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.4.1.tgz", + "integrity": "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-colors": "^4.1.1", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/escodegen": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", + "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^5.2.0", + "esutils": "^2.0.2" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=6.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "bin": { + "extract-zip": "cli.js" + }, + "engines": { + "node": ">= 10.17.0" + }, + "optionalDependencies": { + "@types/yauzl": "^2.9.1" + } + }, + "node_modules/extract-zip/node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/extract-zip/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "pend": "~1.2.0" + } + }, + "node_modules/form-data": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz", + "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-port": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/get-port/-/get-port-5.1.1.tgz", + "integrity": "sha512-g/Q1aTSDOxFpchXC4i8ZWvxA1lnPqx/JHqcpIw0/LX9T8x/GBbi6YnlN5nhaKIFkT8oFsscUKgDJYxfwfS6QsQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-uri": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.4.tgz", + "integrity": "sha512-E1b1lFFLvLgak2whF2xDBcOy6NLVGZBqqjJjsIhvopKfWWEi64pLVTWWehV8KlLerZkfNTA95sTe2OdJKm1OzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "basic-ftp": "^5.0.2", + "data-uri-to-buffer": "^6.0.2", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/get-uri/node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/get-uri/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/header-case": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/header-case/-/header-case-2.0.4.tgz", + "integrity": "sha512-H/vuk5TEEVZwrR0lp2zed9OCo1uAILMlx0JEMgC26rzyJJ3N1v6XkwHHXJQdR2doSjcGPM6OKPYoJgf0plJ11Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "capital-case": "^1.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/http-link-header": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/http-link-header/-/http-link-header-1.1.3.tgz", + "integrity": "sha512-3cZ0SRL8fb9MUlU3mKM61FcQvPfXx2dBrZW3Vbg5CXa8jFlK8OaEpePenLe1oEXQduhz8b0QjsqfS59QP4AJDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/http-proxy-agent/node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/http-proxy-agent/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent/node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/https-proxy-agent/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/image-ssim": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/image-ssim/-/image-ssim-0.2.0.tgz", + "integrity": "sha512-W7+sO6/yhxy83L0G7xR8YAc5Z5QFtYEXXRV6EaE8tuYBZJnA3gVgp3q7X7muhLZVodeb9UfvjSbwt9VJwjIYAg==", + "dev": true, + "license": "MIT" + }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/intl-messageformat": { + "version": "10.7.10", + "resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-10.7.10.tgz", + "integrity": "sha512-hp7iejCBiJdW3zmOe18FdlJu8U/JsADSDiBPQhfdSeI8B9POtvPRvPh3nMlvhYayGMKLv6maldhR7y3Pf1vkpw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@formatjs/ecma402-abstract": "2.3.1", + "@formatjs/fast-memoize": "2.2.5", + "@formatjs/icu-messageformat-parser": "2.9.7", + "tslib": "2" + } + }, + "node_modules/ip-address": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", + "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "jsbn": "1.1.0", + "sprintf-js": "^1.1.3" + }, + "engines": { + "node": ">= 12" + } + }, + "node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "dev": true, + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-obj": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", + "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jpeg-js": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/jpeg-js/-/jpeg-js-0.4.4.tgz", + "integrity": "sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/js-library-detector": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/js-library-detector/-/js-library-detector-6.7.0.tgz", + "integrity": "sha512-c80Qupofp43y4cJ7+8TTDN/AsDwLi5oOm/plBrWI+iQt485vKXCco+yVmOwEgdo9VOdsYTuV0UlTeetVPTriXA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/jsbn": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", + "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==", + "dev": true, + "license": "MIT" + }, + "node_modules/lie": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.1.1.tgz", + "integrity": "sha512-RiNhHysUjhrDQntfYSfY4MU24coXXdEOgw9WGcKHNeEwffDYbF//u87M1EWaMGzuFoSbqW0C9C6lEEhDOAswfw==", + "dev": true, + "license": "MIT", + "dependencies": { + "immediate": "~3.0.5" + } + }, + "node_modules/lighthouse": { + "version": "12.3.0", + "resolved": "https://registry.npmjs.org/lighthouse/-/lighthouse-12.3.0.tgz", + "integrity": "sha512-OaLE8DasnwQkn2CBo2lKtD+IQv42mNP3T+Vaw29I++rAh0Zpgc6SM15usdIYyzhRMR5EWFxze5Fyb+HENJSh2A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@paulirish/trace_engine": "0.0.39", + "@sentry/node": "^7.0.0", + "axe-core": "^4.10.2", + "chrome-launcher": "^1.1.2", + "configstore": "^5.0.1", + "csp_evaluator": "1.1.1", + "devtools-protocol": "0.0.1312386", + "enquirer": "^2.3.6", + "http-link-header": "^1.1.1", + "intl-messageformat": "^10.5.3", + "jpeg-js": "^0.4.4", + "js-library-detector": "^6.7.0", + "lighthouse-logger": "^2.0.1", + "lighthouse-stack-packs": "1.12.2", + "lodash-es": "^4.17.21", + "lookup-closest-locale": "6.2.0", + "metaviewport-parser": "0.3.0", + "open": "^8.4.0", + "parse-cache-control": "1.0.1", + "puppeteer-core": "^23.10.4", + "robots-parser": "^3.0.1", + "semver": "^5.3.0", + "speedline-core": "^1.4.3", + "third-party-web": "^0.26.1", + "tldts-icann": "^6.1.16", + "ws": "^7.0.0", + "yargs": "^17.3.1", + "yargs-parser": "^21.0.0" + }, + "bin": { + "chrome-debug": "core/scripts/manual-chrome-launcher.js", + "lighthouse": "cli/index.js", + "smokehouse": "cli/test/smokehouse/frontends/smokehouse-bin.js" + }, + "engines": { + "node": ">=18.16" + } + }, + "node_modules/lighthouse-logger": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/lighthouse-logger/-/lighthouse-logger-2.0.1.tgz", + "integrity": "sha512-ioBrW3s2i97noEmnXxmUq7cjIcVRjT5HBpAYy8zE11CxU9HqlWHHeRxfeN1tn8F7OEMVPIC9x1f8t3Z7US9ehQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "debug": "^2.6.9", + "marky": "^1.2.2" + } + }, + "node_modules/lighthouse-stack-packs": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/lighthouse-stack-packs/-/lighthouse-stack-packs-1.12.2.tgz", + "integrity": "sha512-Ug8feS/A+92TMTCK6yHYLwaFMuelK/hAKRMdldYkMNwv+d9PtWxjXEg6rwKtsUXTADajhdrhXyuNCJ5/sfmPFw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/localforage": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/localforage/-/localforage-1.10.0.tgz", + "integrity": "sha512-14/H1aX7hzBBmmh7sGPd+AOMkkIrHM3Z1PAyGgZigA1H1p5O5ANnMyWzvpAETtG68/dC4pC0ncy3+PPGzXZHPg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "lie": "3.1.1" + } + }, + "node_modules/lodash-es": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", + "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==", + "dev": true, + "license": "MIT" + }, + "node_modules/lookup-closest-locale": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/lookup-closest-locale/-/lookup-closest-locale-6.2.0.tgz", + "integrity": "sha512-/c2kL+Vnp1jnV6K6RpDTHK3dgg0Tu2VVp+elEiJpjfS1UyY7AjOYHohRug6wT0OpoX2qFgNORndE9RqesfVxWQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lower-case": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", + "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.0.3" + } + }, + "node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/marky": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/marky/-/marky-1.2.5.tgz", + "integrity": "sha512-q9JtQJKjpsVxCRVgQ+WapguSbKC3SQ5HEzFGPAJMStgh3QjCawp00UKv3MTTAArTmGmmPUvllHZoNbZ3gs0I+Q==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/metaviewport-parser": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/metaviewport-parser/-/metaviewport-parser-0.3.0.tgz", + "integrity": "sha512-EoYJ8xfjQ6kpe9VbVHvZTZHiOl4HL1Z18CrZ+qahvLXT7ZO4YTC2JMyt5FaUp9JJp6J4Ybb/z7IsCXZt86/QkQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/mime": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", + "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", + "dev": true, + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "node_modules/netmask": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.0.2.tgz", + "integrity": "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/no-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", + "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "lower-case": "^2.0.2", + "tslib": "^2.0.3" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/open": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", + "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-lazy-prop": "^2.0.0", + "is-docker": "^2.1.1", + "is-wsl": "^2.2.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pac-proxy-agent": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.1.0.tgz", + "integrity": "sha512-Z5FnLVVZSnX7WjBg0mhDtydeRZ1xMcATZThjySQUHqr+0ksP8kqaw23fNKkaaN/Z8gwLUs/W7xdl0I75eP2Xyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tootallnate/quickjs-emscripten": "^0.23.0", + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "get-uri": "^6.0.1", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.6", + "pac-resolver": "^7.0.1", + "socks-proxy-agent": "^8.0.5" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/pac-proxy-agent/node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/pac-proxy-agent/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/pac-resolver": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz", + "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==", + "dev": true, + "license": "MIT", + "dependencies": { + "degenerator": "^5.0.0", + "netmask": "^2.0.2" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/param-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz", + "integrity": "sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==", + "dev": true, + "license": "MIT", + "dependencies": { + "dot-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/parse-cache-control": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parse-cache-control/-/parse-cache-control-1.0.1.tgz", + "integrity": "sha512-60zvsJReQPX5/QP0Kzfd/VrpjScIQ7SHBW6bFCYfEP+fp0Eppr1SHhIO5nd1PjZtvclzSzES9D/p5nFJurwfWg==", + "dev": true + }, + "node_modules/pascal-case": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz", + "integrity": "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/path-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/path-case/-/path-case-3.0.4.tgz", + "integrity": "sha512-qO4qCFjXqVTrcbPt/hQfhTQ+VhFsqNKOPtytgNKkKxSoEp3XPUQ8ObFuePylOIok5gjn69ry8XiULxCwot3Wfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "dot-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "dev": true, + "license": "MIT" + }, + "node_modules/playwright": { + "version": "1.49.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.49.1.tgz", + "integrity": "sha512-VYL8zLoNTBxVOrJBbDuRgDWa3i+mfQgDTrL8Ah9QXZ7ax4Dsj0MSq5bYgytRnDVVe+njoKnfsYkH3HzqVj5UZA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.49.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.49.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.49.1.tgz", + "integrity": "sha512-BzmpVcs4kE2CH15rWfzpjzVGhWERJfmnXmniSyKeRZUs9Ws65m+RGIi7mjJK/euCegfn3i7jvqWeWyHe9y3Vgg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/proxy-agent": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.5.0.tgz", + "integrity": "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "http-proxy-agent": "^7.0.1", + "https-proxy-agent": "^7.0.6", + "lru-cache": "^7.14.1", + "pac-proxy-agent": "^7.1.0", + "proxy-from-env": "^1.1.0", + "socks-proxy-agent": "^8.0.5" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/proxy-agent/node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/proxy-agent/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "dev": true, + "license": "MIT" + }, + "node_modules/pump": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz", + "integrity": "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==", + "dev": true, + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/puppeteer-core": { + "version": "23.11.1", + "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-23.11.1.tgz", + "integrity": "sha512-3HZ2/7hdDKZvZQ7dhhITOUg4/wOrDRjyK2ZBllRB0ZCOi9u0cwq1ACHDjBB+nX+7+kltHjQvBRdeY7+W0T+7Gg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@puppeteer/browsers": "2.6.1", + "chromium-bidi": "0.11.0", + "debug": "^4.4.0", + "devtools-protocol": "0.0.1367902", + "typed-query-selector": "^2.12.0", + "ws": "^8.18.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/puppeteer-core/node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/puppeteer-core/node_modules/devtools-protocol": { + "version": "0.0.1367902", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1367902.tgz", + "integrity": "sha512-XxtPuC3PGakY6PD7dG66/o8KwJ/LkH2/EKe19Dcw58w53dv4/vSQEkn/SzuyhHE2q4zPgCkxQBxus3VV4ql+Pg==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/puppeteer-core/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/puppeteer-core/node_modules/ws": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/queue-tick": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/queue-tick/-/queue-tick-1.0.1.tgz", + "integrity": "sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==", + "dev": true, + "license": "MIT" + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/robots-parser": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/robots-parser/-/robots-parser-3.0.1.tgz", + "integrity": "sha512-s+pyvQeIKIZ0dx5iJiQk1tPLJAWln39+MI5jtM8wnyws+G5azk+dMnMX0qfbqNetKKNgcWWOdi0sfm+FbQbgdQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/sentence-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/sentence-case/-/sentence-case-3.0.4.tgz", + "integrity": "sha512-8LS0JInaQMCRoQ7YUytAo/xUu5W2XnQxV2HI/6uM6U7CITS1RqPElr30V6uIqyMKM9lJGRVFy5/4CuzcixNYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3", + "upper-case-first": "^2.0.2" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/snake-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/snake-case/-/snake-case-3.0.4.tgz", + "integrity": "sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==", + "dev": true, + "license": "MIT", + "dependencies": { + "dot-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/socks": { + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.3.tgz", + "integrity": "sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "ip-address": "^9.0.5", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", + "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "socks": "^2.8.3" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/socks-proxy-agent/node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socks-proxy-agent/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/speedline-core": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/speedline-core/-/speedline-core-1.4.3.tgz", + "integrity": "sha512-DI7/OuAUD+GMpR6dmu8lliO2Wg5zfeh+/xsdyJZCzd8o5JgFUjCeLsBDuZjIQJdwXS3J0L/uZYrELKYqx+PXog==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "image-ssim": "^0.2.0", + "jpeg-js": "^0.4.1" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/streamx": { + "version": "2.21.1", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.21.1.tgz", + "integrity": "sha512-PhP9wUnFLa+91CPy3N6tiQsK+gnYyUNuk15S3YG/zjYE7RuPeCjJngqnzpC31ow0lzBHQ+QGO4cNJnd0djYUsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-fifo": "^1.3.2", + "queue-tick": "^1.0.1", + "text-decoder": "^1.1.0" + }, + "optionalDependencies": { + "bare-events": "^2.2.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tar-fs": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.6.tgz", + "integrity": "sha512-iokBDQQkUyeXhgPYaZxmczGPhnhXZ0CmrqI+MOb/WFGS9DW5wnfrLgtjUJBvz50vQ3qfRwJ62QVoCFu8mPVu5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "pump": "^3.0.0", + "tar-stream": "^3.1.5" + }, + "optionalDependencies": { + "bare-fs": "^2.1.1", + "bare-path": "^2.1.0" + } + }, + "node_modules/tar-stream": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz", + "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "b4a": "^1.6.4", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" + } + }, + "node_modules/text-decoder": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.3.tgz", + "integrity": "sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "b4a": "^1.6.4" + } + }, + "node_modules/third-party-web": { + "version": "0.26.2", + "resolved": "https://registry.npmjs.org/third-party-web/-/third-party-web-0.26.2.tgz", + "integrity": "sha512-taJ0Us0lKoYBqcbccMuDElSUPOxmBfwlHe1OkHQ3KFf+RwovvBHdXhbFk9XJVQE2vHzxbTwvwg5GFsT9hbDokQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tldts-core": { + "version": "6.1.70", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.70.tgz", + "integrity": "sha512-RNnIXDB1FD4T9cpQRErEqw6ZpjLlGdMOitdV+0xtbsnwr4YFka1zpc7D4KD+aAn8oSG5JyFrdasZTE04qDE9Yg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tldts-icann": { + "version": "6.1.70", + "resolved": "https://registry.npmjs.org/tldts-icann/-/tldts-icann-6.1.70.tgz", + "integrity": "sha512-sGnxNnxb/03iSROBEBiXGX49DMEktxWVUoTeHWekJOOrFfNRWfyAcOWphuRDau2jZrshvMhQPf3azYHyxV04/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^6.1.70" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/typed-query-selector": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.0.tgz", + "integrity": "sha512-SbklCd1F0EiZOyPiW192rrHZzZ5sBijB6xM+cpmrwDqObvdtunOHHIk9fCGsoK5JVIYXoyEp4iEdE3upFH3PAg==", + "dev": true, + "license": "MIT" + }, + "node_modules/typedarray-to-buffer": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", + "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-typedarray": "^1.0.0" + } + }, + "node_modules/unbzip2-stream": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz", + "integrity": "sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer": "^5.2.1", + "through": "^2.3.8" + } + }, + "node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true, + "license": "MIT" + }, + "node_modules/unique-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz", + "integrity": "sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "crypto-random-string": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/upper-case": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/upper-case/-/upper-case-2.0.2.tgz", + "integrity": "sha512-KgdgDGJt2TpuwBUIjgG6lzw2GWFRCW9Qkfkiv0DxqHHLYJHmtmdUIKcZd8rHgFSjopVTlw6ggzCm1b8MFQwikg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.0.3" + } + }, + "node_modules/upper-case-first": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/upper-case-first/-/upper-case-first-2.0.2.tgz", + "integrity": "sha512-514ppYHBaKwfJRK/pNC6c/OxfGa0obSnAl106u97Ed0I625Nin96KAjttZF6ZL3e1XLtphxnqrOi9iWgm+u+bg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.0.3" + } + }, + "node_modules/web-vitals": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-4.2.4.tgz", + "integrity": "sha512-r4DIlprAGwJ7YM11VZp4R884m0Vmgr6EAKe3P+kO0PPj3Unqyvv59rczf6UiGcb9Z8QxZVcqKNwv/g0WNdWwsw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/write-file-atomic": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", + "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "is-typedarray": "^1.0.0", + "signal-exit": "^3.0.2", + "typedarray-to-buffer": "^3.1.5" + } + }, + "node_modules/ws": { + "version": "7.5.10", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xdg-basedir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-4.0.0.tgz", + "integrity": "sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, + "node_modules/zod": { + "version": "3.23.8", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz", + "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..69e1053 --- /dev/null +++ b/package.json @@ -0,0 +1,18 @@ +{ + "name": "testautomation-hands-on", + "version": "1.0.0", + "description": "This repository is for doing hands-on practice for the Playwright Framework", + "main": "index.js", + "scripts": {}, + "keywords": [], + "author": "", + "license": "ISC", + "devDependencies": { + "@playwright/test": "^1.49.1", + "@types/node": "^22.10.3", + "@wordpress/e2e-test-utils-playwright": "^1.14.0" + }, + "dependencies": { + "dotenv": "^16.4.7" + } +} diff --git a/playwright.config.js b/playwright.config.js new file mode 100644 index 0000000..b26279a --- /dev/null +++ b/playwright.config.js @@ -0,0 +1,96 @@ +// @ts-check +const { defineConfig, devices } = require("@playwright/test"); +import path from "path"; + +import { fileURLToPath } from "url"; + +require("dotenv").config(); + +const STORAGE_STATE_PATH = + process.env.STORAGE_STATE_PATH || + path.join(process.cwd(), "artifacts/storage-states/admin.json"); + +/** + * Read environment variables from file. + * https://github.com/motdotla/dotenv + */ +// require('dotenv').config({ path: path.resolve(__dirname, '.env') }); + +/** + * @see https://playwright.dev/docs/test-configuration + */ +module.exports = defineConfig({ + testDir: fileURLToPath(new URL("./specs", "file:" + __filename).href), + globalSetup: fileURLToPath( + new URL("./config/global-setup.js", "file:" + __filename).href + ), + // testDir: './specs', + /* Run tests in files in parallel */ + fullyParallel: true, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + /* Retry on CI only */ + retries: process.env.CI ? 2 : 0, + /* Opt out of parallel tests on CI. */ + workers: process.env.CI ? 1 : undefined, + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: "html", + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + storageState: STORAGE_STATE_PATH, + baseURL: process.env.WP_BASE_URL, + /* Base URL to use in actions like `await page.goto('/')`. */ + // baseURL: 'http://127.0.0.1:3000', + + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: "on-first-retry", + globalSetup: fileURLToPath( + new URL("./config/global-setup.js", "file:" + __filename).href + ), + }, + + /* Configure projects for major browsers */ + projects: [ + { + name: "chromium", + use: { ...devices["Desktop Chrome"] }, + }, + + // { + // name: "firefox", + // use: { ...devices["Desktop Firefox"] }, + // }, + + // { + // name: "webkit", + // use: { ...devices["Desktop Safari"] }, + // }, + + /* Test against mobile viewports. */ + // { + // name: 'Mobile Chrome', + // use: { ...devices['Pixel 5'] }, + // }, + // { + // name: 'Mobile Safari', + // use: { ...devices['iPhone 12'] }, + // }, + + /* Test against branded browsers. */ + // { + // name: 'Microsoft Edge', + // use: { ...devices['Desktop Edge'], channel: 'msedge' }, + // }, + // { + // name: 'Google Chrome', + // use: { ...devices['Desktop Chrome'], channel: 'chrome' }, + // }, + ], + + /* Run your local dev server before starting the tests */ + // webServer: { + // command: 'npm run start', + // url: 'http://127.0.0.1:3000', + // reuseExistingServer: !process.env.CI, + // }, +}); diff --git a/sample.env b/sample.env new file mode 100644 index 0000000..7dbb2c4 --- /dev/null +++ b/sample.env @@ -0,0 +1,3 @@ +WP_USERNAME= +WP_PASSWORD= +WP_BASE_URL= \ No newline at end of file diff --git a/specs/add-category.spec.js b/specs/add-category.spec.js new file mode 100644 index 0000000..096ce17 --- /dev/null +++ b/specs/add-category.spec.js @@ -0,0 +1,76 @@ +/** + * E2E Test Suite: Product Category Features + * + * This test suite verifies the functionality of the Product Category feature in the WordPress application. + * It includes tests to add a new category and ensure proper cleanup after each test. + * + * Dependencies: + * - @wordpress/e2e-test-utils-playwright: Provides utility functions for Playwright-based WordPress E2E tests. + * - Custom utility functions: generateTestCode, createTaxonomy, deleteTestTaxonomy. + */ + +const { test, expect } = require("@wordpress/e2e-test-utils-playwright"); + +const {generateTestCode} = require('../utils/e2eUtils/randomTestCode'); + +const {createTaxonomy} = require('../utils/e2eUtils/addTaxonomyUtils'); + +const {deleteTestTaxonomy} = require('../utils/e2eUtils/removeTestTaxonomtyUtils'); + +/** + * Test Suite: Product Category Features + */ + +test.describe("It should test the Product Category features", () => { + + let testCode = 0; // Unique test code for each test + + let catName = ''; // Category name + let catDescription = ''; // Category description + + /** + * Hook: Before each test + * + * Generates unique test data for category name and description. + */ + test.beforeEach(()=>{ + + testCode = generateTestCode(); + + catName = `Test Tag ${testCode}`; + catDescription = `This is tag description for ${catName}`; + + }); + + /** + * Test Case: Add Category Feature + * + * Verifies that a new product category can be successfully created and displayed in the UI. + * + * @param {object} admin - Admin credentials for authentication. + * @param {object} page - Playwright page object for browser interaction. + */ + test("It should test the add category feature", async ({ admin, page }) => { + + // Create a new taxonomy (category) using the provided utility function + await createTaxonomy(admin, page, 'Category', catName, catDescription); + + // Verify that the created category is visible in the UI + await expect(page.locator(`//a[contains(@class, "row") and text()="${catName}"]`)).toBeVisible(); + }); + + /** + * Hook: After each test + * + * Cleans up by deleting the test category created during the test. + * + * @param {object} page - Playwright page object for browser interaction. + */ + test.afterEach(async({page})=>{ + + // Delete the test category using the provided utility function + await deleteTestTaxonomy(page, catName); + }); + +}); + diff --git a/specs/add-coupon.spec.js b/specs/add-coupon.spec.js new file mode 100644 index 0000000..016bd87 --- /dev/null +++ b/specs/add-coupon.spec.js @@ -0,0 +1,74 @@ +/** + * @fileoverview E2E test to validate coupon-related features in WordPress. + * + * This test suite focuses on creating and validating the addition of coupons. + * After the test execution, the created coupon is removed to maintain a clean state. + */ + +const {test, expect} = require('@wordpress/e2e-test-utils-playwright'); + +const {generateTestCode} = require('../utils/e2eUtils/randomTestCode'); + +const {removeTestCouponRecord} = require('../utils/e2eUtils/testCouponDeletion'); + +const {createCoupon} = require('../utils/e2eUtils/createCouponUtils'); + +/** + * Test suite to validate coupon creation functionality. + */ +test.describe('It should test the coupon features', ()=>{ + + /** + * Unique identifier for test data, ensuring uniqueness across tests. + * @type {number} + */ + let testCode = 0; + + /** + * Coupon code and description for the test coupon. + * @type {string} + */ + let couponCode = ''; + let couponDescription = ''; + + /** + * Hook that runs before each test in this suite. + * + * It generates a unique coupon code and description. + */ + test.beforeEach(()=>{ + testCode = generateTestCode(); + + couponCode = `Off-${testCode}`; + couponDescription = `Description for test coupon`; + }); + + /** + * Test case to create a new coupon and validate its addition. + * + * @param {object} context - Test context containing admin and page objects. + */ + test('It should test the add coupon feature', async ({admin, page})=>{ + + // Create a new coupon + await createCoupon(admin, page, couponCode, couponDescription); + + // Verify success message is visible + await expect(page.locator('//div[@id="message" and contains(@class,"notice-success")]')).toBeVisible(); + }); + + /** + * Hook that runs after each test in this suite. + * + * It removes the test coupon created during the test. + * + * @param {object} context - Test context containing admin and page objects. + */ + test.afterEach(async({admin, page})=>{ + + await removeTestCouponRecord(admin, page, couponCode); + + }) + + +}) \ No newline at end of file diff --git a/specs/add-pricing-inventory.spec.js b/specs/add-pricing-inventory.spec.js new file mode 100644 index 0000000..dfea737 --- /dev/null +++ b/specs/add-pricing-inventory.spec.js @@ -0,0 +1,80 @@ +/** + * @fileoverview E2E test to verify the Pricing and Inventory functionality in WordPress. + * + * This script creates a new product, adds pricing and inventory details, and validates the changes + * by checking the visibility of a success message. After the test, the product is deleted to maintain a clean state. + */ + +const { test, expect } = require("@wordpress/e2e-test-utils-playwright"); +const { addNewProduct } = require("../utils/e2eUtils/createProductUtils"); +const {addPricingInventory} = require('../utils/e2eUtils/productInventoryUtils'); + +const {generateTestCode} = require('../utils/e2eUtils/randomTestCode'); + +const {removeTestProductRecord} = require('../utils/e2eUtils/testProductDeletion'); + +/** + * Test suite to validate the Pricing and Inventory functionality. + */ +test.describe('Test should check the Pricing and Inventory feature', () => { + + /** + * Unique identifier for test data, ensuring uniqueness across tests. + * @type {number} + */ + let testCode = 0; + + /** + * Title and description of the test product. + * @type {string} + */ + let productTitle = ''; + let productDescription = ''; + + /** + * Hook that runs before each test in this suite. + * + * It generates a new product and assigns a unique title and description. + * + * @param {object} context - Test context containing admin and page objects. + */ + test.beforeEach(async ({admin, page})=>{ + + testCode = generateTestCode(); + + productTitle = `Product Demo Title ${testCode}`; + productDescription = `Demo Product Description ${testCode}`; + + // Create a new product + await addNewProduct(admin, page, productTitle, productDescription); + + }); + + /** + * Test case to add pricing and inventory to the product and validate success. + * + * @param {object} context - Test context containing admin and page objects. + */ + test('It should add the pricing and inventory', async({admin, page})=>{ + + // Add pricing and inventory details to the product + await addPricingInventory(admin, page); + + // Verify success message is visible + await expect(page.locator('//div[@id="message" and contains(@class,"notice-success")]')).toBeVisible(); + + }); + + /** + * Hook that runs after each test in this suite. + * + * It removes the test product created during the test. + * + * @param {object} context - Test context containing admin and page objects. + */ + test.afterEach(async ({admin, page})=>{ + + await removeTestProductRecord(admin, page, productTitle); + + }) +}) diff --git a/specs/add-product-image.spec.js b/specs/add-product-image.spec.js new file mode 100644 index 0000000..9ea9b23 --- /dev/null +++ b/specs/add-product-image.spec.js @@ -0,0 +1,89 @@ +const { test, expect } = require("@wordpress/e2e-test-utils-playwright"); +const { addNewProduct } = require("../utils/e2eUtils/createProductUtils"); + +const {generateTestCode} = require('../utils/e2eUtils/randomTestCode'); + +const {removeTestProductRecord} = require('../utils/e2eUtils/testProductDeletion'); + +const { extractPrdtSlug } = require('../utils/e2eUtils/getProductSlug'); + +const {addProductImage} = require('../utils/e2eUtils/addProductImageUtils'); + +const {addPricingInventory} = require('../utils/e2eUtils/productInventoryUtils'); + + +test.describe('Test the Product Image Feature', ()=>{ + + let testCode = 0; + let productTitle = ''; + let productDescription = ''; + + /** + * Generates a unique test code and sets up product details before all tests in this suite. + */ + test.beforeAll(({requestUtils})=>{ + + requestUtils.deleteAllMedia(); + + // Generate random test code for product details + testCode = generateTestCode(); + + /** + * Product Details + * @type {string} + */ + productTitle = `Product Demo Title ${testCode}`; + productDescription = `Demo Product Description ${testCode}`; + + }); + + /** + * Tests the ability to upload and set a product image. + * This test performs the following: + * 1. Uploads a test image to the media library. + * 2. Creates a new product with a unique title and description. + * 3. Sets the uploaded image as the product's featured image. + * 4. Verifies that the image is visible on the editor screen. + * + * @param {Object} admin - Admin object for managing WordPress admin panel actions. + * @param {Object} page - Page object for interacting with the browser. + * @param {Object} requestUtils - Utility object for handling requests (e.g., file uploads). + */ + test('It should be able to upload product image', async({admin,page, requestUtils})=>{ + + // Upload a test image to the media library + // await requestUtils.uploadMedia('assets/product_test_image.png'); + + // Add a new product with the generated title and description + await addNewProduct(admin, page, productTitle, productDescription); + + // Add pricing and inventory details to the product + await addPricingInventory(admin, page); + + // Add Product Image + await addProductImage(page, requestUtils); + + // Asserts the image is visible on editor screen + const thumbnailImage = page.locator('//a[@id="set-post-thumbnail"]//img'); + await expect(thumbnailImage).toBeVisible(); + + }); + + /** + * Hook that runs after each test in this suite. + * + * It removes the test product created during the test. + * + * @param {object} context - Test context containing admin and page objects. + */ + test.afterEach( async ({admin, page, requestUtils})=>{ + + // Remove the test product using the utility function + await removeTestProductRecord(admin, page, productTitle); + + // Remove Test Media + await requestUtils.deleteAllMedia(); + + + }) +}) \ No newline at end of file diff --git a/specs/add-tag-category-to-prdt.spec.js b/specs/add-tag-category-to-prdt.spec.js new file mode 100644 index 0000000..a3a1051 --- /dev/null +++ b/specs/add-tag-category-to-prdt.spec.js @@ -0,0 +1,122 @@ +const {test, expect} = require('@wordpress/e2e-test-utils-playwright'); + +const {addNewProduct} = require('../utils/e2eUtils/createProductUtils'); + +const {generateTestCode} = require('../utils/e2eUtils/randomTestCode'); + +const {removeTestProductRecord} = require('../utils/e2eUtils/testProductDeletion'); + +const {createTaxonomy} = require('../utils/e2eUtils/addTaxonomyUtils'); + +const {deleteTestTaxonomy} = require('../utils/e2eUtils/removeTestTaxonomtyUtils'); + +const {assignTagCategory} = require('../utils/e2eUtils/assignPrdtTagCategory'); + +require("dotenv").config(); + +test.describe('It should assign category and tag to the product', ()=>{ + + /** + * Unique identifier for the test product, used to ensure uniqueness across tests. + * @type {number} + */ + let testCode = 0; + + /** + * Title of the product to be created in the test. + * @type {string} + */ + let productTitle = ''; + + /** + * Description of the product to be created in the test. + * @type {string} + */ + let productDescription = ''; + + + let tagName = ''; + let tagDescription = ''; + + let catName = ''; // Category name + let catDescription = ''; // Category description + + /** + * Hook that runs before each test in this suite. + * + * It generates unique product details and creates a new product using the utility function. + * + * @param {object} context - Test context containing admin and page objects. + */ + test.beforeEach( async ({admin, page})=>{ + + // Generate a unique test code for the product + testCode = generateTestCode(); + + // Set the product title and description with the unique test code + productTitle = `Product Demo Title ${testCode}`; + productDescription = `Demo Product Description ${testCode}`; + + tagName = `Test Tag ${testCode}`; // Create unique tag name + tagDescription = `This is tag description for ${tagName}`; // Create unique tag description + + catName = `Test Tag ${testCode}`; + catDescription = `This is tag description for ${catName}`; + + // Create a new taxonomy (tag) using the provided utility function + await createTaxonomy(admin, page, 'Tag', tagName, tagDescription); + + // Create a new taxonomy (category) using the provided utility function + await createTaxonomy(admin, page, 'Category', catName, catDescription); + + }); + + /** + * Test case for testing the feature for assigning categories and tags to the product + */ + test('It should add tag and category to the product', async({admin, page})=>{ + + // Add a new product using the utility function + await addNewProduct(admin, page, productTitle, productDescription); + + // Locate the checkbox for the specific category + const categoryCheckBox = page.locator( + `//label[@class="selectit" and contains(text(),"${catName}")]//input[contains(@id,"in-product_cat-")]` + ); + + // Assign category and tag using the utility function + await assignTagCategory(page, tagName, categoryCheckBox); + + // Assert the specific category is assigned + await expect(categoryCheckBox).toBeChecked(); + + // Assert the specific tag is assigned + await expect(page.locator(`//ul[@class="tagchecklist"]//li`)).toBeVisible(); + + // Verify success message is visible + await expect(page.locator('//div[@id="message" and contains(@class,"notice-success")]')).toBeVisible(); + + }); + + /** + * Hook that runs after each of the test block + * + * 1. Removes the test product record + * 2. Removes the test category and tag created + */ + test.afterEach( async({admin, page})=>{ + + + // Remove the test product using the utility function + await removeTestProductRecord(admin, page, productTitle); + + // Delete the test tag using the provided utility function + await admin.visitAdminPage("edit-tags.php", `taxonomy=product_tag&post_type=product`); + await deleteTestTaxonomy(page, tagName); + + // Delete the test category using the provided utility function + await admin.visitAdminPage( "edit-tags.php", `taxonomy=product_cat&post_type=product`); + await deleteTestTaxonomy(page, catName); + + }) +}) \ No newline at end of file diff --git a/specs/add-tag.spec.js b/specs/add-tag.spec.js new file mode 100644 index 0000000..87ab935 --- /dev/null +++ b/specs/add-tag.spec.js @@ -0,0 +1,78 @@ +/** + * E2E Test Suite: Product Tag Features + * + * This test suite verifies the functionality of the Product Tag feature in the WordPress application. + * It includes tests to add a new tag and ensures proper cleanup after each test. + * + * Dependencies: + * - @wordpress/e2e-test-utils-playwright: Provides utility functions for Playwright-based WordPress E2E tests. + * - Custom utility functions: generateTestCode, createTaxonomy, deleteTestTaxonomy. + */ + +const { test, expect } = require("@wordpress/e2e-test-utils-playwright"); + +const {generateTestCode} = require('../utils/e2eUtils/randomTestCode'); + +const {createTaxonomy} = require('../utils/e2eUtils/addTaxonomyUtils'); + +const {deleteTestTaxonomy} = require('../utils/e2eUtils/removeTestTaxonomtyUtils'); + +/** + * Test Suite: Product Tag Features + */ +test.describe("It should test the Product Tag features", () => { + + let testCode = 0; + + let tagName = ''; + let tagDescription = ''; + + /** + * Hook: Before each test + * + * Generates unique test data for tag name and description. + */ + test.beforeEach(()=>{ + + testCode = generateTestCode(); // Generate unique test code + + tagName = `Test Tag ${testCode}`; // Create unique tag name + tagDescription = `This is tag description for ${tagName}`; // Create unique tag description + + }); + + + /** + * Test Case: Add Tag Feature + * + * Verifies that a new product tag can be successfully created and displayed in the UI. + * + * @param {object} admin - Admin credentials for authentication. + * @param {object} page - Playwright page object for browser interaction. + */ + test("It should test the add tag feature", async ({ admin, page }) => { + + // Create a new taxonomy (tag) using the provided utility function + await createTaxonomy(admin, page, 'Tag', tagName, tagDescription); + + // Verify that the created tag is visible in the UI + await expect(page.locator(`//a[contains(@class, "row-title") and text()="${tagName}"]`)).toBeVisible(); + }); + + + /** + * Hook: After each test + * + * Cleans up by deleting the test tag created during the test. + * Logs the name of the tag being deleted for debugging purposes. + * + * @param {object} page - Playwright page object for browser interaction. + */ + test.afterEach(async({page})=>{ + + // Delete the test tag using the provided utility function + await deleteTestTaxonomy(page, tagName); + }); + + +}); diff --git a/specs/add-user-customer.spec.js b/specs/add-user-customer.spec.js new file mode 100644 index 0000000..c535bdc --- /dev/null +++ b/specs/add-user-customer.spec.js @@ -0,0 +1,81 @@ +/** + * @fileoverview E2E test to verify the customer user creation feature in WordPress. + * + * This script creates a new customer user and validates its creation by checking the visibility + * of the user in the admin panel. After the test, the user is deleted to clean up test data. + */ + +const {test, expect} = require('@wordpress/e2e-test-utils-playwright'); + +const {generateTestCode} = require('../utils/e2eUtils/randomTestCode'); + +const {removeTestUserRecord} = require('../utils/e2eUtils/testUserDeletion'); + +const { addCustomerUser } = require('../utils/e2eUtils/createCustomerUtils'); + +/** + * Test suite to validate the customer user creation functionality. + */ +test.describe('It should customer user creation feature', () => { + + /** + * Unique identifier for test data, ensuring uniqueness across tests. + * @type {number} + */ + let testCode = 0; + + /** + * Details of the test user to be created. + * @type {string} + */ + let userName = ''; + let userEmail = ''; + let firstName = ''; + let lastName = ''; + let password = ''; + + /** + * Hook that runs before all tests in this suite. + * + * It generates unique test user details. + */ + test.beforeAll(()=>{ + testCode = generateTestCode(); + + // User details + userName = `TestUserName-${testCode}`; + userEmail = `test${testCode}@trial.com`; + firstName = `TestFirstName-${testCode}`; + lastName = `TestLastName-${testCode}`; + password = `TestPassword${testCode}*`; + + }) + + /** + * Test case to create a customer user and validate its presence in the admin panel. + * + * @param {object} context - Test context containing admin and page objects. + */ + test('It should create a customer user', async({admin, page})=>{ + + // Create a new customer user + await addCustomerUser(admin, page, userName, userEmail, firstName, lastName, password); + + // Verify the user is visible in the admin panel + await expect(page.getByRole('cell',{name: `${userName} Edit | Delete | View`})).toBeVisible(); + + }); + + /** + * Hook that runs after each test in this suite. + * + * It removes the test user created during the test. + * + * @param {object} context - Test context containing admin and page objects. + */ + test.afterEach(async ({admin, page})=>{ + + await removeTestUserRecord(admin, page, userName); + + }) +}) \ No newline at end of file diff --git a/specs/checkout-place-order.spec.js b/specs/checkout-place-order.spec.js new file mode 100644 index 0000000..b0609d6 --- /dev/null +++ b/specs/checkout-place-order.spec.js @@ -0,0 +1,170 @@ +/** + * @fileoverview E2E test to verify the checkout workflow in WooCommerce. + * + * This script covers the entire workflow of adding a product to the cart, performing checkout, and verifying the order. + * It also includes creation and cleanup of test data such as products and users. + */ + +const { test, expect, logout, login } = require("@wordpress/e2e-test-utils-playwright"); + +const { addNewProduct } = require("../utils/e2eUtils/createProductUtils"); + +const {addPricingInventory} = require('../utils/e2eUtils/productInventoryUtils'); + +const {generateTestCode} = require('../utils/e2eUtils/randomTestCode'); + +const {removeTestProductRecord} = require('../utils/e2eUtils/testProductDeletion'); + +const {removeTestUserRecord} = require('../utils/e2eUtils/testUserDeletion'); + +const { addCustomerUser } = require('../utils/e2eUtils/createCustomerUtils'); + +const {customerUserLogin} = require('../utils/e2eUtils/customerLoginUtils'); + +const {checkoutPlaceOrder} = require('../utils/e2eUtils/checkoutPlaceOrderUtils'); + +const {adminLogin} = require('../utils/e2eUtils/adminLoginUtils'); + +const { consumers } = require("stream"); + +require("dotenv").config(); + +/** + * Test suite to verify the checkout workflow in WooCommerce. + */ +test.describe('Test should verify the chekcout workflow', ()=>{ + + /** + * Unique identifier for test data, ensuring uniqueness across tests. + * @type {number} + */ + let testCode = 0; + + /** + * Title of the test product. + * @type {string} + */ + let productTitle = ''; + + /** + * Description of the test product. + * @type {string} + */ + let productDescription = ''; + + /** + * Customer user details. + * @type {string} + */ + let userName = ''; + let userEmail = ''; + let firstName = ''; + let lastName = ''; + let password = ''; + + /** + * ID of the created customer user. + * @type {number} + */ + let customerUserId = 0; + + /** + * Hook that runs before each test in this suite. + * + * It generates unique product and user details, creates a new product with pricing and inventory, + * and registers a new customer user via API. + * + * @param {object} context - Test context containing admin, page, and requestUtils objects. + */ + test.beforeEach( async ({admin, page, requestUtils})=>{ + + // Generate a unique test code for the product and user + testCode = generateTestCode(); + + // Set the product title and description + productTitle = `Product Demo Title ${testCode}`; + productDescription = `Demo Product Description ${testCode}`; + + // Add a new product with pricing and inventory + await addNewProduct(admin, page, productTitle, productDescription); + await addPricingInventory(admin, page); + + // Set customer user details + userName = `TestUserName-${testCode}`; + userEmail = `test${testCode}@trial.com`; + firstName = `TestFirstName-${testCode}`; + lastName = `TestLastName-${testCode}`; + password = `TestPassword${testCode}*`; + + // await addCustomerUser(admin, page, userName, userEmail, firstName, lastName, password); + + // Create a new customer user via API and capture the user ID + const customerUserData = await requestUtils.createUser({ + username: userName, + email: userEmail, + first_name: firstName, + last_name: lastName, + password, + roles: ['customer'] + }); + + customerUserId = customerUserData.id + + console.log(customerUserId); + + // Log out of the admin account + await page.goto(`${process.env.WP_BASE_URL}wp-login.php?action=logout`); + const logoutLink = page.locator('//div[contains(@class,"wp-die-message")]//p[2]//a'); + await logoutLink.click(); + + + }); + + /** + * Test case to verify the checkout workflow. + * + * It logs in as the customer, adds the product to the cart, performs checkout, + * and verifies the order confirmation. + * + * @param {object} context - Test context containing admin and page objects. + */ + test('It should be add product to cart, checkout and place order',async({admin, page})=>{ + + // Log in as the customer user + await customerUserLogin(page, userName, password); + + // Perform checkout and place the order + await checkoutPlaceOrder(page, firstName, lastName, productTitle) + + // Verify the order confirmation page title + await expect(page).toHaveTitle('Order Confirmation'); + + }); + + /** + * Hook that runs after each test in this suite. + * + * It removes all test users and the test product created during the test. + * + * @param {object} context - Test context containing admin, page, and requestUtils objects. + */ + test.afterEach(async ({admin, page, requestUtils})=>{ + + // Delete all test users via API + await requestUtils.deleteAllUsers(); + + // await requestUtils.login({ + // username: process.env.WP_USERNAME, + // password: process.env.WP_PASSWORD + // }); + + // Log in as the admin user + await adminLogin(page); + + // Remove the test product using the utility function + await removeTestProductRecord(admin, page, productTitle); + + // await removeTestUserRecord(admin, page, userName); + + }) +}) diff --git a/specs/checkout-verify-coupon.spec.js b/specs/checkout-verify-coupon.spec.js new file mode 100644 index 0000000..aae5342 --- /dev/null +++ b/specs/checkout-verify-coupon.spec.js @@ -0,0 +1,126 @@ +/** + * @fileoverview E2E test to verify coupon discount functionality in WooCommerce. + * + * This script creates a product, adds pricing and inventory, generates a coupon, + * and verifies that the coupon discount is applied correctly in the cart. + * It also includes cleanup of test data after each test. + */ + +const {test, expect} = require('@wordpress/e2e-test-utils-playwright'); + +const {addNewProduct} = require('../utils/e2eUtils/createProductUtils'); + +const {generateTestCode} = require('../utils/e2eUtils/randomTestCode'); + +const {removeTestProductRecord} = require('../utils/e2eUtils/testProductDeletion'); + +const {addPricingInventory} = require('../utils/e2eUtils/productInventoryUtils'); + +const {removeTestCouponRecord} = require('../utils/e2eUtils/testCouponDeletion'); + +const {createCoupon} = require('../utils/e2eUtils/createCouponUtils'); + +const {applyCouponDiscount} = require('../utils/e2eUtils/applyCouponUtils'); + +require("dotenv").config(); + +/** + * Test suite to verify WooCommerce coupon discounts. + */ +test.describe('It should test the coupon discounts', ()=> { + + /** + * Unique identifier for test data, ensuring uniqueness across tests. + * @type {number} + */ + let testCode = 0; + + /** + * Title and description of the test product. + * @type {string} + */ + let productTitle = ''; + + /** + * Code and description of the test coupon. + * @type {string} + */ + let productDescription = ''; + + /** + * Code and description of the test coupon. + * @type {string} + */ + let couponCode = ''; + let couponDescription = ''; + + /** + * Hook that runs before each test in this suite. + * + * It generates unique product and coupon details, creates a product with pricing, + * and generates a coupon for testing. + * + * @param {object} context - Test context containing admin and page objects. + */ + test.beforeEach( async ({admin, page})=>{ + + // Generate a unique test code for the product and coupon + testCode = generateTestCode(); + + // Set the product title and description + productTitle = `Product Demo Title ${testCode}`; + productDescription = `Demo Product Description ${testCode}`; + + // Set the coupon code and description + couponCode = `Off-${testCode}`; + couponDescription = `Description for test coupon`; + + // Add a new product with pricing and inventory + await addNewProduct(admin, page, productTitle, productDescription); + await addPricingInventory(admin, page); + + // Create a new coupon + await createCoupon(admin, page, couponCode, couponDescription); + + }) + + /** + * Test case to verify that the coupon discount is applied correctly. + * + * It applies the coupon to the cart and checks the discount value. + * + * @param {object} context - Test context containing admin and page objects. + */ + test('The coupons should apply proper discount', async({admin, page})=>{ + + // Apply the coupon to the product in the cart + await applyCouponDiscount(page, productTitle, couponCode); + + // Verify the discount value in the cart + const discountValue = page.locator('//div[contains(@class,"wc-block-components-totals-discount")]//span[contains(@class,"wc-block-components-totals-item__value")]'); + await expect(discountValue).toContainText('8.00'); + + }) + + /** + * Hook that runs after each test in this suite. + * + * It removes the test product and coupon created during the test. + * + * @param {object} context - Test context containing admin and page objects. + */ + test.afterEach(async ({admin, page}) => { + + // Navigate to the cart page + await page.goto(`${process.env.WP_BASE_URL}/cart`); + + // Remove the product from the cart + const removeItemBtn = page.locator('//button[@class="wc-block-cart-item__remove-link"]'); + await removeItemBtn.click(); + + // Remove the test product and coupon + await removeTestProductRecord(admin, page, productTitle); + await removeTestCouponRecord(admin, page, couponCode); + + }); +}) \ No newline at end of file diff --git a/specs/create-simple-product.spec.js b/specs/create-simple-product.spec.js new file mode 100644 index 0000000..7012f68 --- /dev/null +++ b/specs/create-simple-product.spec.js @@ -0,0 +1,90 @@ +/** + * @fileoverview E2E test for testing the Simple Product functionality + * + * This script tests the creation and deletion of a simple product in a WordPress environment + * using Playwright E2E testing utilities. + */ + +const {test, expect} = require('@wordpress/e2e-test-utils-playwright'); + +const {addNewProduct} = require('../utils/e2eUtils/createProductUtils'); + +const {generateTestCode} = require('../utils/e2eUtils/randomTestCode'); + +const {removeTestProductRecord} = require('../utils/e2eUtils/testProductDeletion'); + +require("dotenv").config(); + + +/** + * Test suite to verify the functionality of creating and managing a Simple Product in WooCommerce. + */ +test.describe('It should test the Simple Product Functionality', ()=>{ + + /** + * Unique identifier for the test product, used to ensure uniqueness across tests. + * @type {number} + */ + let testCode = 0; + + /** + * Title of the product to be created in the test. + * @type {string} + */ + let productTitle = ''; + + /** + * Description of the product to be created in the test. + * @type {string} + */ + let productDescription = ''; + + /** + * Hook that runs before each test in this suite. + * + * It generates unique product details and creates a new product using the utility function. + * + * @param {object} context - Test context containing admin and page objects. + */ + test.beforeEach( async ({admin, page})=>{ + + // Generate a unique test code for the product + testCode = generateTestCode(); + + // Set the product title and description with the unique test code + productTitle = `Product Demo Title ${testCode}`; + productDescription = `Demo Product Description ${testCode}`; + + // Add a new product using the utility function + await addNewProduct(admin, page, productTitle, productDescription); + + }) + + /** + * Test case to verify the creation of a simple product. + * + * It checks for the success message after the product is published. + * + * @param {object} context - Test context containing admin and page objects. + */ + test('It should test the creation of simple product', async({admin, page})=>{ + + // Verify that the success message is displayed on the page + await expect(page.locator('//div[@id="message" and contains(@class,"notice-success")]//p')).toContainText('Product published.'); + + }); + + /** + * Hook that runs after each test in this suite. + * + * It removes the test product created during the test. + * + * @param {object} context - Test context containing admin and page objects. + */ + test.afterEach(async ({admin, page})=>{ + + // Remove the test product using the utility function + await removeTestProductRecord(admin, page, productTitle); + + }) +}) \ No newline at end of file diff --git a/specs/example.spec.js b/specs/example.spec.js new file mode 100644 index 0000000..40eddb8 --- /dev/null +++ b/specs/example.spec.js @@ -0,0 +1,19 @@ +// @ts-check +const { test, expect } = require('@playwright/test'); + +test('has title', async ({ page }) => { + await page.goto('https://playwright.dev/'); + + // Expect a title "to contain" a substring. + await expect(page).toHaveTitle(/Playwright/); +}); + +test('get started link', async ({ page }) => { + await page.goto('https://playwright.dev/'); + + // Click the get started link. + await page.getByRole('link', { name: 'Get started' }).click(); + + // Expects page to have a heading with the name of Installation. + await expect(page.getByRole('heading', { name: 'Installation' })).toBeVisible(); +}); diff --git a/specs/publish-product.spec.js b/specs/publish-product.spec.js new file mode 100644 index 0000000..673fad9 --- /dev/null +++ b/specs/publish-product.spec.js @@ -0,0 +1,94 @@ +/** + * @fileoverview E2E test to verify the visibility of products in WooCommerce. + * + * This script ensures that a product can be created, published, and its visibility verified. + */ + +const { test, expect } = require("@wordpress/e2e-test-utils-playwright"); + +const { addNewProduct } = require("../utils/e2eUtils/createProductUtils"); + +const {generateTestCode} = require('../utils/e2eUtils/randomTestCode'); + +const {removeTestProductRecord} = require('../utils/e2eUtils/testProductDeletion'); + +require("dotenv").config(); + +/** + * Test suite to verify the visibility of products after publishing. + */ +test.describe('It should verify the verify the visibility of products', ()=>{ + + /** + * Unique identifier for the test product, ensuring uniqueness across tests. + * @type {number} + */ + let testCode = 0; + + /** + * Title of the product to be created in the test. + * @type {string} + */ + let productTitle = ''; + + /** + * Description of the product to be created in the test. + * @type {string} + */ + let productDescription = ''; + + /** + * Hook that runs before each test in this suite. + * + * It generates unique product details and creates a new product using the utility function. + * + * @param {object} context - Test context containing admin and page objects. + */ + test.beforeEach( async ({admin, page})=>{ + + // Generate a unique test code for the product + testCode = generateTestCode(); + + // Set the product title and description with the unique test code + productTitle = `Product Demo Title ${testCode}`; + productDescription = `Demo Product Description ${testCode}`; + + // Add a new product using the utility function + await addNewProduct(admin, page, productTitle, productDescription); + + }) + + /** + * Test case to verify the visibility of the product after publishing. + * + * It clicks the "View Product" link and checks the product page title. + * + * @param {object} context - Test context containing admin and page objects. + */ + test('It should publish and then verify the visibility of the products', async({admin, page})=>{ + + // Locate and click the "View Product" link in the success message + const viewProductLink = page.locator('//div[@id="message" and contains(@class,"notice-success")]//a'); + await viewProductLink.click(); + + // Store the product page title + const productPageTitle = await page.title(); + + // Verify the title of the product page + await expect(productPageTitle).toContain(productTitle); + + }) + + /** + * Hook that runs after each test in this suite. + * + * It removes the test product created during the test. + * + * @param {object} context - Test context containing admin and page objects. + */ + test.afterEach(async ({admin, page})=>{ + + // Remove the test product using the utility function + await removeTestProductRecord(admin, page, productTitle); + }); +}) diff --git a/specs/review-order.spec.js b/specs/review-order.spec.js new file mode 100644 index 0000000..9cb960b --- /dev/null +++ b/specs/review-order.spec.js @@ -0,0 +1,172 @@ +/** + * @fileoverview E2E test to verify the checkout workflow in WooCommerce. + * + * This script tests the end-to-end process of adding a product to the cart, performing checkout, and verifying the order. + */ + +const { test, expect, logout, login } = require("@wordpress/e2e-test-utils-playwright"); + +const { addNewProduct } = require("../utils/e2eUtils/createProductUtils"); + +const {addPricingInventory} = require('../utils/e2eUtils/productInventoryUtils'); + +const {generateTestCode} = require('../utils/e2eUtils/randomTestCode'); + +const {removeTestProductRecord} = require('../utils/e2eUtils/testProductDeletion'); + +const {removeTestUserRecord} = require('../utils/e2eUtils/testUserDeletion'); + +const { addCustomerUser } = require('../utils/e2eUtils/createCustomerUtils'); + +const {customerUserLogin} = require('../utils/e2eUtils/customerLoginUtils'); + +const {checkoutPlaceOrder} = require('../utils/e2eUtils/checkoutPlaceOrderUtils'); + +const {adminLogin} = require('../utils/e2eUtils/adminLoginUtils'); + +require("dotenv").config(); + +/** + * Test suite to verify the checkout workflow in WooCommerce. + */ +test.describe('Test should verify the chekcout workflow', ()=>{ + + /** + * Unique identifier for the test product and user, ensuring uniqueness across tests. + * @type {number} + */ + let testCode = 0; + + /** + * Title of the product to be created in the test. + * @type {string} + */ + let productTitle = ''; + + /** + * Description of the product to be created in the test. + * @type {string} + */ + let productDescription = ''; + + /** + * Customer user details. + * @type {string} + */ + let userName = ''; + let userEmail = ''; + let firstName = ''; + let lastName = ''; + let password = ''; + + /** + * Hook that runs before each test in this suite. + * + * It generates unique product and user details, creates a new product with pricing and inventory, + * and registers a new customer user. + * + * @param {object} context - Test context containing admin, page, and requestUtils objects. + */ + test.beforeEach( async ({admin, page, requestUtils})=>{ + + // Generate a unique test code for the product and user + testCode = generateTestCode(); + + // Set the product title and description + productTitle = `Product Demo Title ${testCode}`; + productDescription = `Demo Product Description ${testCode}`; + + // Add a new product with pricing and inventory + await addNewProduct(admin, page, productTitle, productDescription); + await addPricingInventory(admin, page); + + // Set customer user details + userName = `TestUserName-${testCode}`; + userEmail = `test${testCode}@trial.com`; + firstName = `TestFirstName-${testCode}`; + lastName = `TestLastName-${testCode}`; + password = `TestPassword${testCode}*`; + + // await addCustomerUser(admin, page, userName, userEmail, firstName, lastName, password); + + // Create a new customer user via API + await requestUtils.createUser({ + username: userName, + email: userEmail, + first_name: firstName, + last_name: lastName, + password, + roles: ['customer'] + }); + + // Log out of the admin account + await page.goto(`${process.env.WP_BASE_URL}wp-login.php?action=logout`); + const logoutLink = page.locator('//div[contains(@class,"wp-die-message")]//p[2]//a'); + await logoutLink.click(); + + + }); + + /** + * Test case to verify the checkout workflow. + * + * It logs in as the customer, adds the product to the cart, performs checkout, + * verifies the order, and checks the order in the admin panel. + * + * @param {object} context - Test context containing admin and page objects. + */ + test('It should be add product to cart, checkout and place order',async({admin, page})=>{ + + // Log in as the customer user + await customerUserLogin(page, userName, password); + + // Perform checkout and place the order + await checkoutPlaceOrder(page, firstName, lastName, productTitle); + + // Retrieve the order number from the order confirmation page + const orderNumberField = page.locator('//span[contains(text(),"Order #:")]/following-sibling::*[1]'); + const orderNumber = await orderNumberField.textContent(); + + // Log out from the customer account + await page.goto(`${process.env.WP_BASE_URL}my-account`); + const logoutLink = page.locator('//li//a[contains(text(),"Log out")]'); + await logoutLink.click(); + + // Log in as the admin user + await adminLogin(page); + + // Verify the order in the WooCommerce admin orders panel + await page.goto(`${process.env.WP_BASE_URL}wp-admin/admin.php?page=wc-orders`); + + const searchCouponField = page.locator('//input[@id="orders-search-input-search-input"]'); + await searchCouponField.fill(orderNumber); + + const searchBtn = page.locator('//input[@id="search-submit"]'); + + await page.keyboard.press('Enter'); + + const orderRecordData = page.locator('//td[contains(@class,"order_number")]//a[@class="order-view"]//strong'); + + await expect(orderRecordData).toContainText(orderNumber); + + + + }); + + /** + * Hook that runs after each test in this suite. + * + * It removes all test users and the test product created during the test. + * + * @param {object} context - Test context containing admin, page, and requestUtils objects. + */ + test.afterEach(async ({admin, page, requestUtils})=>{ + + // Delete all test users via API + await requestUtils.deleteAllUsers(); + + // Remove the test product using the utility function + await removeTestProductRecord(admin, page, productTitle); + + }) +}) diff --git a/utils/e2eUtils/addProductImageUtils.js b/utils/e2eUtils/addProductImageUtils.js new file mode 100644 index 0000000..5f4677c --- /dev/null +++ b/utils/e2eUtils/addProductImageUtils.js @@ -0,0 +1,48 @@ +/** + * Utility function for adding product image. + */ + +const addProductImage = async (page, requestUtils) => { + // Upload a test image to the media library + await requestUtils.uploadMedia("assets/product_test_image.png"); + + // Store the current product edit screen URL for later navigation + const prdtEditScreen = page.url(); + + await page.getByRole("link", { name: "Set product image" }).click(); + + // Switch to media Library + // const mediaLibraryTab = await page.locator('//a[text()="Media Library"]'); + // await mediaLibraryTab.click(); + + // Click the 'Use as Product Imagw' button + // const usePrdtImgBtn = await page.locator( + // '//a[text()="Use as product image"]' + // ); + // await usePrdtImgBtn.click(); + + // Switch to editor screen + // await page.goto(prdtEditScreen); + + // Select the image from the media library + const selectedImage = page.locator('//li[@aria-label="product_test_image"]'); + await selectedImage.click(); + + await selectedImage.click(); + + // Click on "Set Product image" button + const setPrdtImgBtn = page.locator('//button[text()="Set product image"]'); + await selectedImage.click(); + await setPrdtImgBtn.click(); + + // wait for 2 sec time interval. + await page.waitForTimeout(2000); + + // Locate the 'Update' button and click it to save the changes + const updateButton = page.locator('//input[@id="publish"]'); + await updateButton.click(); +}; + +module.exports = { + addProductImage, +}; diff --git a/utils/e2eUtils/addTaxonomyUtils.js b/utils/e2eUtils/addTaxonomyUtils.js new file mode 100644 index 0000000..faff91e --- /dev/null +++ b/utils/e2eUtils/addTaxonomyUtils.js @@ -0,0 +1,45 @@ +/** + * Creates a taxonomy (e.g., category or tag) in the WordPress admin interface. + * + * @param {object} admin - Admin utility object for navigating the WordPress admin area. + * @param {object} page - Playwright page object for browser interaction. + * @param {string} type - The type of taxonomy to create ('Category' or 'Tag'). + * @param {string} taxonomyName - The name of the taxonomy to create. + * @param {string} taxonomyDescription - The description of the taxonomy. + */ + +const createTaxonomy = async( admin, page, type, taxonomyName, taxonomyDescription )=>{ + + // Determine the taxonomy slug based on the type (Category or Tag) + const taxSlug = type === 'Category' ? 'cat' : 'tag'; + + // Navigate to the taxonomy creation page in the WordPress admin area + await admin.visitAdminPage( + "edit-tags.php", + `taxonomy=product_${taxSlug}&post_type=product` + ); + + // Locate and fill the taxonomy name field + const taxonomyNameField = page.locator('//input[@id="tag-name"]'); + await taxonomyNameField.fill(taxonomyName); + + // Locate and fill the taxonomy slug field (used for URL-friendly identifier) + const taxonomySlugField = page.locator('//input[@id="tag-slug"]'); + await taxonomySlugField.fill("test-cat"); + + // Locate and fill the taxonomy description field + const categoryDecriptionField = page.locator( + '//textarea[@id="tag-description"]' + ); + + // Locate and click the "Add New" button to create the taxonomy + await categoryDecriptionField.fill(taxonomyDescription); + const addNewCategoryButton = page.locator('//input[@id="submit"]'); + + await addNewCategoryButton.click(); + +} + +module.exports = { + createTaxonomy +} \ No newline at end of file diff --git a/utils/e2eUtils/adminLoginUtils.js b/utils/e2eUtils/adminLoginUtils.js new file mode 100644 index 0000000..0f524a7 --- /dev/null +++ b/utils/e2eUtils/adminLoginUtils.js @@ -0,0 +1,40 @@ +/** + * @fileoverview Utility function for logging in as an admin to a WordPress site. + * + * This module provides a reusable function for programmatically logging in + * as an administrator using Playwright, leveraging credentials stored in + * environment variables. + */ + +require("dotenv").config(); + +/** + * Logs in as an administrator to the WordPress admin dashboard. + * + * This function navigates to the WordPress login page, fills in the admin + * credentials (username and password), and submits the login form. + * + * @async + * @param {object} page - The Playwright `Page` object representing a browser tab. + * @throws Will throw an error if the login process fails due to incorrect credentials or page navigation issues. + */ +const adminLogin = async (page) => { + // Navigate to the WordPress login page + await page.goto(`${process.env.WP_BASE_URL}wp-login.php`); + + // Locate the username field and input the admin username from environment variables + const userNameField = page.locator('//input[@id="user_login"]'); + await userNameField.fill(`${process.env.WP_USERNAME}`); + + // Locate the password field and input the admin password from environment variables + const passwordField = page.locator('//input[@id="user_pass"]'); + await passwordField.fill(`${process.env.WP_PASSWORD}`); + + // Locate the login button and click it to submit the login form + const loginBtn = page.locator('//input[@id="wp-submit"]'); + await loginBtn.click(); +}; + +module.exports = { + adminLogin, +}; diff --git a/utils/e2eUtils/applyCouponUtils.js b/utils/e2eUtils/applyCouponUtils.js new file mode 100644 index 0000000..5d621c1 --- /dev/null +++ b/utils/e2eUtils/applyCouponUtils.js @@ -0,0 +1,58 @@ +/** + * @fileoverview Utility function to apply a coupon discount during the checkout process in a WordPress/WooCommerce environment. + * + * This module provides a function that navigates through a product's page, + * adds the product to the cart, and applies a coupon code during the checkout. + */ +const { extractPrdtSlug } = require("./getProductSlug"); + +/** + * Automates the process of applying a coupon discount during checkout. + * + * This function adds a specified product to the cart by navigating to its page, + * proceeds to the checkout page, and applies a given coupon code. + * + * @async + * @param {object} page - The Playwright `Page` object representing a browser tab. + * @param {string} productTitle - The title of the product to be added to the cart. + * @param {string} couponCode - The coupon code to apply during checkout. + * + * @throws Will throw an error if navigation or interactions with page elements fail. + */ +const applyCouponDiscount = async (page, productTitle, couponCode) => { + // Extract the product slug from the product title + const prdtSlug = extractPrdtSlug(productTitle); + + // Navigate to the product page using the extracted slug + await page.goto(`${process.env.WP_BASE_URL}product/${prdtSlug}`); + + // Locate and click the "Add to cart" button to add the product to the cart + const addToCartBtn = page.locator('//button[contains(text(),"Add to cart")]'); + await addToCartBtn.click(); + + // Navigate to the checkout page + await page.goto(`${process.env.WP_BASE_URL}checkout`); + + // Open the coupon code input field on the checkout page + const couponFieldBtn = page.locator( + '//div[contains(@class,"wc-block-components-totals-coupon")]//div[@role="button"]' + ); + await couponFieldBtn.click(); + + // Locate and focus on the coupon code input field + const couponCodeField = page.locator( + '//input[@id="wc-block-components-totals-coupon__input-coupon"]' + ); + await couponCodeField.click(); + + // Fill in the provided coupon code + await couponCodeField.fill(couponCode); + + // Locate and click the "Apply" button to apply the coupon + const couponApplyBtn = page.locator('//span[contains(text(),"Apply")]'); + await couponApplyBtn.click(); +}; + +module.exports = { + applyCouponDiscount, +}; diff --git a/utils/e2eUtils/assignPrdtTagCategory.js b/utils/e2eUtils/assignPrdtTagCategory.js new file mode 100644 index 0000000..fc2c8b9 --- /dev/null +++ b/utils/e2eUtils/assignPrdtTagCategory.js @@ -0,0 +1,31 @@ +/** + * Utility function for assigning category and tag to the product + * + * @param {object} page - Playwright's Page object representing the browser page. + * @param {string} catName - Category Name + * @param {string} tagName - Tag Name + */ + +const assignTagCategory = async (page, tagName, categoryCheckBox) => { + + await categoryCheckBox.check(); + + // Search for the category and add it + const tagOption = page.locator('//input[@id="new-tag-product_tag"]'); + await tagOption.fill(tagName); + + // Locate and click on the 'Add' button + const addTagBtn = page.locator('//input[@value="Add"]'); + await addTagBtn.click(); + + // wait for the tag to be added + await page.waitForTimeout(2000); + + // Locate the 'Update' button and click it to save the changes + const updateButton = page.locator('//input[@id="publish"]'); + await updateButton.click(); +}; + +module.exports = { + assignTagCategory +} diff --git a/utils/e2eUtils/checkoutPlaceOrderUtils.js b/utils/e2eUtils/checkoutPlaceOrderUtils.js new file mode 100644 index 0000000..fcb8286 --- /dev/null +++ b/utils/e2eUtils/checkoutPlaceOrderUtils.js @@ -0,0 +1,77 @@ +/** + * Function to simulate the checkout and order placement process for an e-commerce platform. + * + * @param {object} page - Playwright's Page object representing the browser page. + * @param {string} firstName - The first name of the customer for billing information. + * @param {string} lastName - The last name of the customer for billing information. + * @param {string} productTitle - The title of the product to add to the cart and checkout. + * + * This function performs the following steps: + * 1. Navigates to the product page using its slug. + * 2. Adds the product to the cart. + * 3. Navigates to the checkout page. + * 4. Fills in the billing details including name, address, city, state, and postal code. + * 5. Selects a billing option (e.g., Cash on Delivery). + * 6. Places the order by clicking the "Place Order" button. + */ + +const { extractPrdtSlug } = require("./getProductSlug"); + +const checkoutPlaceOrder = async (page, firstName, lastName, productTitle) => { + // Extract the product slug from its title + const prdtSlug = extractPrdtSlug(productTitle); + + // Navigate to the product's page using its slug + await page.goto(`${process.env.WP_BASE_URL}product/${prdtSlug}`); + + // Locate and click the "Add to cart" button + const addToCartBtn = page.locator('//button[contains(text(),"Add to cart")]'); + await addToCartBtn.click(); + + // Navigate to the checkout page + await page.goto(`${process.env.WP_BASE_URL}checkout`); + + // Fill in the billing details + + // Select the billing country (India in this case) + const billingCountryField = page.locator('//select[@id="billing-country"]'); + await billingCountryField.selectOption("India"); + + // Enter the customer's first name + const firstNameField = page.locator('//input[@id="billing-first_name"]'); + await firstNameField.fill(firstName); + + // Enter the customer's last name + const lastNameField = page.locator('//input[@id="billing-last_name"]'); + await lastNameField.fill(lastName); + + // Enter the billing address + const addressField1 = page.locator('//input[@id="billing-address_1"]'); + await addressField1.fill("ABC st."); + + // Enter the billing city + const cityField = page.locator('//input[@id="billing-city"]'); + await cityField.fill("Kolkata"); + + // Select the billing state (West Bengal in this case) + const stateField = page.locator('//select[@id="billing-state"]'); + await stateField.selectOption("West Bengal"); + + // Enter the postal code + const pincodeField = page.locator('//input[@id="billing-postcode"]'); + await pincodeField.fill("123456"); + + // Select the billing option (e.g., Cash on Delivery) + const billingOption = page.locator( + '//span[contains(text(),"Cash on delivery")]' + ); + await billingOption.click(); + + // Locate and click the "Place Order" button + const placeOrderBtn = page.locator('//div[contains(text(),"Place Order")]'); + await placeOrderBtn.click(); +}; + +module.exports = { + checkoutPlaceOrder, +}; diff --git a/utils/e2eUtils/createCouponUtils.js b/utils/e2eUtils/createCouponUtils.js new file mode 100644 index 0000000..e613ade --- /dev/null +++ b/utils/e2eUtils/createCouponUtils.js @@ -0,0 +1,53 @@ +/** + * createCoupon - Automates the process of creating a new coupon in WordPress admin. + * + * @param {object} admin - The admin object used to perform admin-specific operations. + * @param {object} page - The Playwright page object for interacting with the browser. + * @param {string} couponCode - The unique code for the coupon to be created. + * @param {string} couponDescription - A description of the coupon to describe its purpose or usage. + * + * @description + * This function automates the creation of a coupon in the WordPress admin panel by: + * - Navigating to the "Add New Coupon" page. + * - Filling in necessary fields such as coupon code, description, type, discount amount, and expiry date. + * - Publishing the coupon to make it available for use. + * + * @example + * const { createCoupon } = require('./createCouponUtils'); + * await createCoupon(admin, page, 'SAVE10', 'Save 10% on your next order'); + */ + +const createCoupon = async (admin, page, couponCode, couponDescription) => { + // Navigate to the "Add New Coupon" page in the WordPress admin dashboard + await admin.visitAdminPage("post-new.php", "post_type=shop_coupon"); + + // Fill in the coupon code field with the provided code + const couponCodeField = page.locator('//input[@id="title"]'); + await couponCodeField.fill(couponCode); + + // Fill in the coupon description field with the provided description + const couponDescriptionField = page.locator( + '//textarea[@id="woocommerce-coupon-description"]' + ); + await couponDescriptionField.fill(couponDescription); + + // Select "Percentage discount" as the coupon type + const couponTypeField = page.locator('//select[@id="discount_type"]'); + await couponTypeField.selectOption("Percentage discount"); + + // Enter the discount amount (e.g., 10%) + const couponAmountField = page.locator('//input[@id="coupon_amount"]'); + await couponAmountField.fill("10"); + + // Set an expiry date for the coupon (e.g., "2025-01-22") + const couponExpiryField = page.locator('//input[@id="expiry_date"]'); + await couponExpiryField.fill("2025-01-22"); + + // Click the "Publish" button to save and activate the coupon + const submitButton = page.locator('//input[@id="publish"]'); + await submitButton.click(); +}; + +module.exports = { + createCoupon, +}; diff --git a/utils/e2eUtils/createCustomerUtils.js b/utils/e2eUtils/createCustomerUtils.js new file mode 100644 index 0000000..7dc60da --- /dev/null +++ b/utils/e2eUtils/createCustomerUtils.js @@ -0,0 +1,69 @@ +/** + * addCustomerUser - Automates the creation of a new customer user in WordPress admin. + * + * @param {object} admin - The admin object used to perform admin-specific operations. + * @param {object} page - The Playwright page object for interacting with the browser. + * @param {string} userName - The username for the new customer user. + * @param {string} userEmail - The email address of the new customer user. + * @param {string} firstName - The first name of the new customer user. + * @param {string} lastName - The last name of the new customer user. + * @param {string} password - The password for the new customer user. + * + * @description + * This function automates the process of creating a new customer user in the WordPress admin dashboard by: + * - Navigating to the "Add New User" page. + * - Filling in the required fields such as username, email, first name, last name, and password. + * - Setting the user role as "Customer". + * - Clicking the "Add New User" button to create the user. + * + * This utility is particularly useful for testing and managing customer-related functionality in WordPress and WooCommerce environments. + * + * @example + * const { addCustomerUser } = require('./createCustomerUtils'); + * await addCustomerUser(admin, page, 'john_doe', 'john@example.com', 'John', 'Doe', 'securePassword123'); + */ + +const addCustomerUser = async ( + admin, + page, + userName, + userEmail, + firstName, + lastName, + password +) => { + // Navigate to the "Add New User" page in the WordPress admin dashboard + await admin.visitAdminPage("user-new.php"); + + // Fill in the username field with the provided value + const userNameField = page.locator('//input[@id="user_login"]'); + await userNameField.fill(userName); + + // Fill in the email field with the provided value + const emailField = page.locator('//input[@id="email"]'); + await emailField.fill(userEmail); + + // Fill in the first name field with the provided value + const firstNameField = page.locator('//input[@id="first_name"]'); + await firstNameField.fill(firstName); + + // Fill in the last name field with the provided value + const lastNameField = page.locator('//input[@id="last_name"]'); + await lastNameField.fill(lastName); + + // Fill in the password field with the provided value + const passwordField = page.locator('//input[@id="pass1"]'); + await passwordField.fill(password); + + // Set the user role to "Customer" in the dropdown + const userRoleField = page.locator('//select[@id="role"]'); + await userRoleField.selectOption("customer"); + + // Click the "Add New User" button to create the user + const addUserButton = page.locator('//input[@id="createusersub"]'); + await addUserButton.click(); +}; + +module.exports = { + addCustomerUser, +}; diff --git a/utils/e2eUtils/createProductUtils.js b/utils/e2eUtils/createProductUtils.js new file mode 100644 index 0000000..b420536 --- /dev/null +++ b/utils/e2eUtils/createProductUtils.js @@ -0,0 +1,47 @@ +/** + * addNewProduct - Automates the creation of a new product in the WordPress admin. + * + * @param {object} admin - The admin object used for navigating and interacting with the WordPress admin panel. + * @param {object} page - The Playwright page object used for interacting with the browser. + * @param {string} productTitle - The title of the new product to be created. + * @param {string} productDescription - The description of the new product to be added. + * + * @description + * This function automates the process of adding a new product in WooCommerce by: + * - Navigating to the "Add New Product" page in the WordPress admin dashboard. + * - Filling in the product title and description fields. + * - Publishing the product to make it available in the store. + * + * This utility is essential for testing WooCommerce features that involve product creation, such as inventory management, pricing updates, and user interactions with products. + * + * @example + * const { addNewProduct } = require('./createProductUtils'); + * await addNewProduct(admin, page, 'Test Product', 'This is a sample product description.'); + */ + +const addNewProduct = async (admin, page, productTitle, productDescription) => { + + // Navigate to the "Add New Product" page in the WordPress admin dashboard + await admin.visitAdminPage("/post-new.php", "post_type=product"); + + // Locate the product title field and fill it with the provided title + const productTitleField = page.locator('//input[@id="title"]'); + await productTitleField.fill(productTitle); + + // Locate the product description iframe and access its content + const productDescIframe = page.frameLocator("#content_ifr"); + const productDescField = await productDescIframe.locator("body#tinymce p"); + + // Click on the description field to activate it and then fill it with the provided description + await productDescField.click(); + await productDescField.fill(productDescription); + + // Locate the "Publish" button and click it to save and publish the new product + const submitButton = page.locator('//input[@id="publish"]'); + await submitButton.click(); + +}; + +module.exports = { + addNewProduct +} diff --git a/utils/e2eUtils/customerLoginUtils.js b/utils/e2eUtils/customerLoginUtils.js new file mode 100644 index 0000000..d529934 --- /dev/null +++ b/utils/e2eUtils/customerLoginUtils.js @@ -0,0 +1,40 @@ +/** + * customerUserLogin - Automates the login process for a customer user on a WooCommerce site. + * + * @param {object} page - The Playwright page object used for browser interactions. + * @param {string} userName - The username of the customer user to log in. + * @param {string} password - The password of the customer user. + * + * @description + * This function automates the login process for a customer user by: + * - Navigating to the "My Account" page of the WooCommerce site. + * - Filling in the username and password fields with the provided credentials. + * - Clicking the "Log in" button to submit the login form. + * + * It is designed for use in end-to-end tests where customer interactions with the WooCommerce site are tested, such as placing orders, viewing account details, and interacting with products. + * + * @example + * const { customerUserLogin } = require('./customerLoginUtils'); + * await customerUserLogin(page, 'testUser', 'testPassword123'); + */ + +const customerUserLogin = async (page, userName, password) => { + // Navigate to the WooCommerce "My Account" login page + await page.goto(`${process.env.WP_BASE_URL}my-account`); + + // Locate the username field and fill it with the provided username + const userNameField = page.locator('//input[@id="username"]'); + await userNameField.fill(userName); + + // Locate the password field and fill it with the provided password + const passwordField = page.locator('//input[@id="password"]'); + await passwordField.fill(password); + + // Locate the login button and click it to submit the login form + const loginBtn = page.locator('//button[@value="Log in"]'); + await loginBtn.click(); +}; + +module.exports = { + customerUserLogin, +}; diff --git a/utils/e2eUtils/getProductSlug.js b/utils/e2eUtils/getProductSlug.js new file mode 100644 index 0000000..f09cfb8 --- /dev/null +++ b/utils/e2eUtils/getProductSlug.js @@ -0,0 +1,31 @@ +/** + * extractPrdtSlug - Converts a product title into a URL-friendly slug. + * + * @param {string} productTitle - The title of the product to be converted into a slug. + * @returns {string} - The generated product slug, which is lowercase and spaces replaced by hyphens. + * + * @description + * This function takes a product title as input and converts it into a URL-friendly slug. + * It achieves this by: + * - Splitting the title into words using spaces as delimiters. + * - Joining the words with hyphens. + * - Converting all characters to lowercase. + * + * The resulting slug can be used in constructing URLs or for other purposes where a clean, + * URL-compatible string representation of the product title is required. + * + * @example + * const { extractPrdtSlug } = require('./getProductSlug'); + * const slug = extractPrdtSlug('Demo Product Title'); + * console.log(slug); // Output: "demo-product-title" + */ + +const extractPrdtSlug = (productTitle) => { + + // Split the product title into words, join them with hyphens, and convert to lowercase + return productTitle.split(" ").join("-").toLowerCase(); +} + +module.exports = { + extractPrdtSlug +} \ No newline at end of file diff --git a/utils/e2eUtils/productInventoryUtils.js b/utils/e2eUtils/productInventoryUtils.js new file mode 100644 index 0000000..24df418 --- /dev/null +++ b/utils/e2eUtils/productInventoryUtils.js @@ -0,0 +1,50 @@ +/** + * Function to add pricing and inventory details for a product on an admin page. + * It fills in fields for regular price, sale price, stock management options, and updates the product. + * + * @async + * @param {Object} admin - The admin user object (used for authentication, if needed). + * @param {Object} page - The page object from a browser automation library (e.g., Playwright or Puppeteer). + * + * This function interacts with the product page to: + * 1. Set a regular price of 100. + * 2. Set a sale price of 80. + * 3. Enable stock management and set the stock quantity to 2. + * 4. Click the "Update" button to save the changes. + */ + +const addPricingInventory = async (admin, page) => { + + // Navigate to the 'General Options' tab to update pricing details + const generalOptionsTab = page.locator('//li[contains(@class,"general_options")]//a'); + await generalOptionsTab.click(); + + // Locate the regular price field and fill in a value of 100 + const regularPriceField = page.locator('//input[@id="_regular_price"]'); + await regularPriceField.fill("100"); + + // Locate the sale price field and fill in a value of 80 + const salePriceField = page.locator('//input[@id="_sale_price"]'); + await salePriceField.fill('80'); + + // Navigate to the 'Inventory Options' tab to update inventory details + const inventoryOptionsTab = page.locator('//li[contains(@class,"inventory_options")]//a'); + await inventoryOptionsTab.click(); + + // Locate the checkbox for managing stock and check it + const manageStockCheckBox = page.locator('//input[@id="_manage_stock"]'); + await manageStockCheckBox.check(); + + // Locate the stock quantity field and set it to 2 + const stockQuantityField = page.locator('//input[@id="_stock"]'); + await stockQuantityField.fill('2'); + + // Locate the 'Update' button and click it to save the changes + const updateButton = page.locator('//input[@id="publish"]'); + await updateButton.click(); +} + +// Export the function for use in other modules +module.exports = { + addPricingInventory, +} diff --git a/utils/e2eUtils/randomTestCode.js b/utils/e2eUtils/randomTestCode.js new file mode 100644 index 0000000..f8c98ed --- /dev/null +++ b/utils/e2eUtils/randomTestCode.js @@ -0,0 +1,21 @@ +/** + * Generates a random 4-digit test code. + * This function produces a random integer between 1000 and 9999, inclusive. + * + * @returns {number} A random 4-digit integer. + * + * Example usage: + * const testCode = generateTestCode(); + * console.log(testCode); // Output: a random 4-digit number, e.g., 4532 + */ + +const generateTestCode = () => { + + // Generate a random number between 1000 and 9999 + return Math.floor(1000 + Math.random() * 9000); +} + +// Export the function for use in other modules +module.exports = { + generateTestCode +} \ No newline at end of file diff --git a/utils/e2eUtils/removeTestTaxonomtyUtils.js b/utils/e2eUtils/removeTestTaxonomtyUtils.js new file mode 100644 index 0000000..fd1f0cf --- /dev/null +++ b/utils/e2eUtils/removeTestTaxonomtyUtils.js @@ -0,0 +1,33 @@ +/** + * Deletes a taxonomy (e.g., tag or category) from the WordPress admin interface. + * + * @param {object} page - Playwright page object for browser interaction. + * @param {string} taxTitle - The title of the taxonomy to be deleted. + */ + +const deleteTestTaxonomy = async(page, taxTitle) =>{ + + // Locate the taxonomy search bar by its ID and enter the taxonomy title + const taxonomySearchBar = page.locator('//input[@id="tag-search-input"]'); + await taxonomySearchBar.fill(taxTitle); + + // Click the search button to find the taxonomy + const taxSearchBtn = page.locator('//input[@id="search-submit"]'); + await taxSearchBtn.click(); + + // Select the checkbox for the found taxonomy + const selectedTagCheckBox = page.locator('//input[contains(@id,"cb-select-") and contains(@name,"delete_tags")]'); + await selectedTagCheckBox.check(); + + // Select "Delete" from the bulk action dropdown menu + const bulkActionSelector = page.locator('//select[@id="bulk-action-selector-top"]'); + await bulkActionSelector.selectOption('Delete'); + + // Click the apply button to perform the delete action + const applyBtn = page.locator('//input[@id="doaction"]'); + await applyBtn.click(); +} + +module.exports = { + deleteTestTaxonomy +} \ No newline at end of file diff --git a/utils/e2eUtils/testCouponDeletion.js b/utils/e2eUtils/testCouponDeletion.js new file mode 100644 index 0000000..00e40ea --- /dev/null +++ b/utils/e2eUtils/testCouponDeletion.js @@ -0,0 +1,53 @@ +/** + * Removes a coupon record from the admin panel using its coupon code. + * This function navigates to the coupon management page, searches for the coupon by its code, + * selects the coupon, and moves it to the trash. + * + * @async + * @param {Object} admin - The admin user object (used for authentication, if needed). + * @param {Object} page - The page object from a browser automation library (e.g., Playwright or Puppeteer). + * @param {string} couponCode - The code of the coupon to be removed. + * + * This function performs the following steps: + * 1. Visits the coupon management page in the admin panel. + * 2. Searches for the coupon by its code. + * 3. Selects the coupon from the search results. + * 4. Moves the selected coupon to the trash. + */ + +const removeTestCouponRecord = async (admin, page, couponCode) => { + // Navigate to the coupon management page (admin panel) to manage coupons + await admin.visitAdminPage( + "edit.php", + "post_type=shop_coupon&legacy_coupon_menu=1" + ); + + // Locate the search field where the coupon code can be entered + const couponSearchField = page.locator('//input[@id="post-search-input"]'); + await couponSearchField.fill(couponCode); + + // Locate and click the search button to find the coupon + const couponSearchBtn = page.locator('//input[@id="search-submit"]'); + await couponSearchBtn.click(); + + // Locate the checkbox for selecting the coupon from search results + const selectedCouponCheckBox = page.locator( + '//input[contains(@id,"cb-select-") and contains(@name,"post")]' + ); + await selectedCouponCheckBox.check(); + + // Locate the dropdown selector for bulk actions and choose the "Move to Trash" option + const bulkActionSelector = page.locator( + '//select[@id="bulk-action-selector-top"]' + ); + await bulkActionSelector.selectOption("Move to Trash"); + + // Locate and click the "Apply" button to execute the bulk action + const applyBtn = page.locator('//input[@id="doaction"]'); + await applyBtn.click(); +}; + +// Export the function for use in other modules +module.exports = { + removeTestCouponRecord, +}; diff --git a/utils/e2eUtils/testProductDeletion.js b/utils/e2eUtils/testProductDeletion.js new file mode 100644 index 0000000..50be83b --- /dev/null +++ b/utils/e2eUtils/testProductDeletion.js @@ -0,0 +1,52 @@ +require("dotenv").config(); + +/** + * Removes a product record from the admin panel using its product title. + * This function navigates to the product management page, searches for the product by its title, + * selects the product, and moves it to the trash. + * + * @async + * @param {Object} admin - The admin user object (used for authentication, if needed). + * @param {Object} page - The page object from a browser automation library (e.g., Playwright or Puppeteer). + * @param {string} productTitle - The title of the product to be removed. + * + * This function performs the following steps: + * 1. Visits the product management page in the admin panel. + * 2. Searches for the product by its title. + * 3. Selects the product from the search results. + * 4. Moves the selected product to the trash. + */ + +const removeTestProductRecord = async (admin, page, productTitle) => { + // Navigate to the product management page in the admin panel + await admin.visitAdminPage("edit.php", "post_type=product"); + + // Locate the search field to find the product by its title + const searchProductField = page.locator('//input[@id="post-search-input"]'); + await searchProductField.fill(productTitle); + + // Locate and click the search submit button to perform the search + const searchSubmitBtn = page.locator('//input[@id="search-submit"]'); + await searchSubmitBtn.click(); + + // Locate the checkbox for selecting the product from search results + const selectedTestProduct = page.locator( + '//input[contains(@id,"cb-select-") and contains(@name,"post")]' + ); + await selectedTestProduct.check(); + + // Locate the dropdown menu for bulk actions and choose the "Move to Trash" option + const bulkActionSelector = page.locator( + '//select[@id="bulk-action-selector-top"]' + ); + await bulkActionSelector.selectOption("Move to Trash"); + + // Locate and click the "Apply" button to move the selected product to trash + const applyBtn = page.locator('//input[@id="doaction"]'); + await applyBtn.click(); +}; + +// Export the function for use in other modules +module.exports = { + removeTestProductRecord, +}; diff --git a/utils/e2eUtils/testUserDeletion.js b/utils/e2eUtils/testUserDeletion.js new file mode 100644 index 0000000..736630c --- /dev/null +++ b/utils/e2eUtils/testUserDeletion.js @@ -0,0 +1,53 @@ +/** + * Removes a user record from the admin panel based on the provided user name. + * This function navigates to the user management page, searches for the user by name, + * selects the user, and then deletes the user record. + * + * @async + * @param {Object} admin - The admin user object (used for authentication, if needed). + * @param {Object} page - The page object from a browser automation library (e.g., Playwright or Puppeteer). + * @param {string} userName - The name of the user to be removed. + * + * This function performs the following steps: + * 1. Navigates to the user management page (`users.php`). + * 2. Searches for the user by their name. + * 3. Selects the user from the search results. + * 4. Applies the "Delete" bulk action. + * 5. Confirms the deletion of the user. + */ + +const removeTestUserRecord = async (admin, page, userName) => { + // Navigate to the user management page within the admin panel + await admin.visitAdminPage("users.php"); + + // Locate the search input field and fill it with the provided user name + const userSearchField = page.locator('//input[@id="user-search-input"]'); + await userSearchField.fill(userName); + + // Locate and click the search submit button to initiate the search + const userSearchBtn = page.locator('//input[@id="search-submit"]'); + await userSearchBtn.click(); + + // Locate the checkbox for selecting the user from the search results + const selectUserCheckBox = page.locator('//input[contains(@name,"users")]'); + await selectUserCheckBox.check(); + + // Locate the bulk action dropdown and select the "Delete" option + const bulkActionSelector = page.locator( + '//select[@id="bulk-action-selector-top"]' + ); + await bulkActionSelector.selectOption("Delete"); + + // Locate and click the "Apply" button to apply the delete action + const applyBtn = page.locator('//input[@id="doaction"]'); + await applyBtn.click(); + + // Locate and click the confirmation button to finalize the user deletion + const confirmDeletionBtn = page.locator('//input[@id="submit"]'); + await confirmDeletionBtn.click(); +}; + +// Export the function for use in other modules +module.exports = { + removeTestUserRecord, +};