From 49a74ac6b1005ebded55d1e7d9edde7bd43e51c3 Mon Sep 17 00:00:00 2001 From: Rishav Dutta Date: Thu, 2 Jan 2025 18:38:47 +0530 Subject: [PATCH 01/27] add: initial test sripts --- .github/workflows/playwright.yml | 27 + .gitignore | 6 + artifacts/storage-states/admin.json | 1 + config/global-setup.js | 34 + package-lock.json | 2343 ++++++++++++++++++++++++++ package.json | 18 + playwright.config.js | 96 ++ sample.env | 3 + specs/addCategory.spec.js | 24 + specs/addCoupon.spec.js | 22 + specs/addProductImage.spec.js | 7 + specs/addTag.spec.js | 24 + specs/addUserCustomer.spec.js | 25 + specs/createSimpleProduct.spec.js | 21 + specs/example.spec.js | 19 + tests-examples/demo-todo-app.spec.js | 449 +++++ 16 files changed, 3119 insertions(+) create mode 100644 .github/workflows/playwright.yml create mode 100644 .gitignore create mode 100644 artifacts/storage-states/admin.json create mode 100644 config/global-setup.js create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 playwright.config.js create mode 100644 sample.env create mode 100644 specs/addCategory.spec.js create mode 100644 specs/addCoupon.spec.js create mode 100644 specs/addProductImage.spec.js create mode 100644 specs/addTag.spec.js create mode 100644 specs/addUserCustomer.spec.js create mode 100644 specs/createSimpleProduct.spec.js create mode 100644 specs/example.spec.js create mode 100644 tests-examples/demo-todo-app.spec.js 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..07919b8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +node_modules/ +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ +.env diff --git a/artifacts/storage-states/admin.json b/artifacts/storage-states/admin.json new file mode 100644 index 0000000..ccbc919 --- /dev/null +++ b/artifacts/storage-states/admin.json @@ -0,0 +1 @@ +{"cookies":[{"name":"wordpress_test_cookie","value":"WP%20Cookie%20check","domain":"rishav.rt.gw","path":"/","expires":-1,"httpOnly":false,"secure":true,"sameSite":"Lax"},{"name":"wordpress_sec_074e74b03d8a277981a80ce1db027a5e","value":"rishav%7C1735989750%7CubRA6Ye5updhqdmEOWAKgz84yKzEF8Q9TUPjgymACql%7C1046294a01822a5c0dac997ff1ddff4cdff18f0cfd388ddf56d1c62f9411e330","domain":"rishav.rt.gw","path":"/wp-content/plugins","expires":-1,"httpOnly":true,"secure":true,"sameSite":"Lax"},{"name":"wordpress_sec_074e74b03d8a277981a80ce1db027a5e","value":"rishav%7C1735989750%7CubRA6Ye5updhqdmEOWAKgz84yKzEF8Q9TUPjgymACql%7C1046294a01822a5c0dac997ff1ddff4cdff18f0cfd388ddf56d1c62f9411e330","domain":"rishav.rt.gw","path":"/wp-admin","expires":-1,"httpOnly":true,"secure":true,"sameSite":"Lax"},{"name":"wordpress_logged_in_074e74b03d8a277981a80ce1db027a5e","value":"rishav%7C1735989750%7CubRA6Ye5updhqdmEOWAKgz84yKzEF8Q9TUPjgymACql%7Cb51e60ea88c1d86658364525c9fabb01f3293d38ecabbf5b4a4c5d39fba2d1db","domain":"rishav.rt.gw","path":"/","expires":-1,"httpOnly":true,"secure":true,"sameSite":"Lax"},{"name":"tk_ai","value":"woo%3Att9OmINARUla7NfCYnKU5fJW","domain":"rishav.rt.gw","path":"/","expires":-1,"httpOnly":false,"secure":false,"sameSite":"Lax"},{"name":"wp-settings-time-2","value":"1735816950","domain":"rishav.rt.gw","path":"/","expires":1767352950.878,"httpOnly":false,"secure":true,"sameSite":"Lax"}],"nonce":"6bab227d76","rootURL":"https://rishav.rt.gw/wp-json/"} \ No newline at end of file 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..8177b5a --- /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/addCategory.spec.js b/specs/addCategory.spec.js new file mode 100644 index 0000000..74d3daa --- /dev/null +++ b/specs/addCategory.spec.js @@ -0,0 +1,24 @@ +const { test, expect } = require("@wordpress/e2e-test-utils-playwright"); + +test.describe("It should test the Product Category features", () => { + test("It should test the add category feature", async ({ admin, page }) => { + await admin.visitAdminPage( + "edit-tags.php", + "taxonomy=product_cat&post_type=product" + ); + + const categoryNameField = page.locator('//input[@id="tag-name"]'); + await categoryNameField.fill("TestCategory"); + const categorySlugField = page.locator('//input[@id="tag-slug"]'); + await categorySlugField.fill("test-cat"); + const categoryDecriptionField = page.locator( + '//textarea[@id="tag-description"]' + ); + await categoryDecriptionField.fill("This is a category for testing"); + const addNewCategoryButton = page.locator('//input[@id="submit"]'); + + await addNewCategoryButton.click(); + + await expect(page.locator('//a[contains(@class, "row") and text()="TestCategory"]')).toBeVisible(); + }); +}); diff --git a/specs/addCoupon.spec.js b/specs/addCoupon.spec.js new file mode 100644 index 0000000..9e7e70d --- /dev/null +++ b/specs/addCoupon.spec.js @@ -0,0 +1,22 @@ +const {test, expect} = require('@wordpress/e2e-test-utils-playwright'); + +test.describe('It should test the coupon features', ()=>{ + test('It should test the add coupon feature', async ({admin, page})=>{ + await admin.visitAdminPage('post-new.php','post_type=shop_coupon'); + const couponCodeField = page.locator('//input[@id="title"]'); + await couponCodeField.fill("offtest"); + const couponDescription = page.locator('//textarea[@id="woocommerce-coupon-description"]'); + await couponDescription.fill("This is for testing purpose"); + const couponTypeField = page.locator('//select[@id="discount_type"]'); + await couponTypeField.selectOption('Percentage discount'); + const couponAmountField = page.locator('//input[@id="coupon_amount"]'); + await couponAmountField.fill('10'); + const couponExpiryField = page.locator('//input[@id="expiry_date"]'); + await couponExpiryField.fill('2025-01-22'); + + const submitButton = page.locator('//input[@id="publish"]'); + await submitButton.click(); + + await expect(page.locator('//div[@id="message" and contains(@class,"notice-success")]')).toBeVisible(); + }) +}) \ No newline at end of file diff --git a/specs/addProductImage.spec.js b/specs/addProductImage.spec.js new file mode 100644 index 0000000..0f1a046 --- /dev/null +++ b/specs/addProductImage.spec.js @@ -0,0 +1,7 @@ +const { test, expect } = require("@wordpress/e2e-test-utils-playwright"); + +test.describe('Test the Product Image Feature', ()=>{ + test('It should be able to upload prodcut image', async({admin,page})=>{ + + }); +}) \ No newline at end of file diff --git a/specs/addTag.spec.js b/specs/addTag.spec.js new file mode 100644 index 0000000..08a7fa2 --- /dev/null +++ b/specs/addTag.spec.js @@ -0,0 +1,24 @@ +const { test, expect } = require("@wordpress/e2e-test-utils-playwright"); + +test.describe("It should test the Product Tag features", () => { + test("It should test the add tag feature", async ({ admin, page }) => { + await admin.visitAdminPage( + "edit-tags.php", + "taxonomy=product_tag&post_type=product" + ); + + const categoryNameField = page.locator('//input[@id="tag-name"]'); + await categoryNameField.fill("TestTag"); + const categorySlugField = page.locator('//input[@id="tag-slug"]'); + await categorySlugField.fill("test-tag"); + const categoryDecriptionField = page.locator( + '//textarea[@id="tag-description"]' + ); + await categoryDecriptionField.fill("This is a tag for testing"); + const addNewCategoryButton = page.locator('//input[@id="submit"]'); + + await addNewCategoryButton.click(); + + await expect(page.locator('//a[contains(@class, "row-title") and text()="TestTag"]')).toBeVisible(); + }); +}); diff --git a/specs/addUserCustomer.spec.js b/specs/addUserCustomer.spec.js new file mode 100644 index 0000000..c0ed9ff --- /dev/null +++ b/specs/addUserCustomer.spec.js @@ -0,0 +1,25 @@ +const {test, expect} = require('@wordpress/e2e-test-utils-playwright'); + +test.describe('It should customer user creation feature', () => { + test('It should create a customer user', async({admin, page})=>{ + await admin.visitAdminPage('user-new.php'); + const userNameField = page.locator('//input[@id="user_login"]'); + await userNameField.fill('TestUserName'); + const emailField = page.locator('//input[@id="email"]'); + await emailField.fill('test@trial.com'); + const firstNameField = page.locator('//input[@id="first_name"]'); + await firstNameField.fill('TestFirstName'); + const lastNameField = page.locator('//input[@id="last_name"]'); + await lastNameField.fill('TestLastNameField'); + const passwordField = page.locator('//input[@id="pass1"]'); + await passwordField.fill('TestPassword1234*'); + const userRoleField = page.locator('//select[@id="role"]'); + await userRoleField.selectOption('customer'); + const addUserButton = page.locator('//input[@id="createusersub"]'); + + await addUserButton.click(); + + await expect(page.getByRole('cell',{name: 'TestUserName Edit | Delete | View'})).toBeVisible(); + + }) +}) \ No newline at end of file diff --git a/specs/createSimpleProduct.spec.js b/specs/createSimpleProduct.spec.js new file mode 100644 index 0000000..c4a0fc4 --- /dev/null +++ b/specs/createSimpleProduct.spec.js @@ -0,0 +1,21 @@ +const {test, expect} = require('@wordpress/e2e-test-utils-playwright'); + +test.describe('It should test the Simple Product Functionality', ()=>{ + + test('It should test the creation of simple product', async({admin, page})=>{ + await admin.visitAdminPage( '/post-new.php','post_type=product' ); + + const productTitle = page.locator('//input[@id="title"]'); + await productTitle.fill('Product Demo Title'); + const productDescIframe = page.frameLocator('#content_ifr'); + const productDesc = await productDescIframe.locator('body#tinymce p'); + await productDesc.fill('New content for the paragraph.'); + const submitButton = page.locator('//input[@id="publish"]'); + + await submitButton.click(); + + await expect(page.locator('//div[@id="message" and contains(@class,"notice-success")]//p')).toContainText('Product published.'); + + + }); +}) \ 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/tests-examples/demo-todo-app.spec.js b/tests-examples/demo-todo-app.spec.js new file mode 100644 index 0000000..e2eb87c --- /dev/null +++ b/tests-examples/demo-todo-app.spec.js @@ -0,0 +1,449 @@ +// @ts-check +const { test, expect } = require('@playwright/test'); + +test.beforeEach(async ({ page }) => { + await page.goto('https://demo.playwright.dev/todomvc'); +}); + +const TODO_ITEMS = [ + 'buy some cheese', + 'feed the cat', + 'book a doctors appointment' +]; + +test.describe('New Todo', () => { + test('should allow me to add todo items', async ({ page }) => { + // create a new todo locator + const newTodo = page.getByPlaceholder('What needs to be done?'); + + // Create 1st todo. + await newTodo.fill(TODO_ITEMS[0]); + await newTodo.press('Enter'); + + // Make sure the list only has one todo item. + await expect(page.getByTestId('todo-title')).toHaveText([ + TODO_ITEMS[0] + ]); + + // Create 2nd todo. + await newTodo.fill(TODO_ITEMS[1]); + await newTodo.press('Enter'); + + // Make sure the list now has two todo items. + await expect(page.getByTestId('todo-title')).toHaveText([ + TODO_ITEMS[0], + TODO_ITEMS[1] + ]); + + await checkNumberOfTodosInLocalStorage(page, 2); + }); + + test('should clear text input field when an item is added', async ({ page }) => { + // create a new todo locator + const newTodo = page.getByPlaceholder('What needs to be done?'); + + // Create one todo item. + await newTodo.fill(TODO_ITEMS[0]); + await newTodo.press('Enter'); + + // Check that input is empty. + await expect(newTodo).toBeEmpty(); + await checkNumberOfTodosInLocalStorage(page, 1); + }); + + test('should append new items to the bottom of the list', async ({ page }) => { + // Create 3 items. + await createDefaultTodos(page); + + // create a todo count locator + const todoCount = page.getByTestId('todo-count') + + // Check test using different methods. + await expect(page.getByText('3 items left')).toBeVisible(); + await expect(todoCount).toHaveText('3 items left'); + await expect(todoCount).toContainText('3'); + await expect(todoCount).toHaveText(/3/); + + // Check all items in one call. + await expect(page.getByTestId('todo-title')).toHaveText(TODO_ITEMS); + await checkNumberOfTodosInLocalStorage(page, 3); + }); +}); + +test.describe('Mark all as completed', () => { + test.beforeEach(async ({ page }) => { + await createDefaultTodos(page); + await checkNumberOfTodosInLocalStorage(page, 3); + }); + + test.afterEach(async ({ page }) => { + await checkNumberOfTodosInLocalStorage(page, 3); + }); + + test('should allow me to mark all items as completed', async ({ page }) => { + // Complete all todos. + await page.getByLabel('Mark all as complete').check(); + + // Ensure all todos have 'completed' class. + await expect(page.getByTestId('todo-item')).toHaveClass(['completed', 'completed', 'completed']); + await checkNumberOfCompletedTodosInLocalStorage(page, 3); + }); + + test('should allow me to clear the complete state of all items', async ({ page }) => { + const toggleAll = page.getByLabel('Mark all as complete'); + // Check and then immediately uncheck. + await toggleAll.check(); + await toggleAll.uncheck(); + + // Should be no completed classes. + await expect(page.getByTestId('todo-item')).toHaveClass(['', '', '']); + }); + + test('complete all checkbox should update state when items are completed / cleared', async ({ page }) => { + const toggleAll = page.getByLabel('Mark all as complete'); + await toggleAll.check(); + await expect(toggleAll).toBeChecked(); + await checkNumberOfCompletedTodosInLocalStorage(page, 3); + + // Uncheck first todo. + const firstTodo = page.getByTestId('todo-item').nth(0); + await firstTodo.getByRole('checkbox').uncheck(); + + // Reuse toggleAll locator and make sure its not checked. + await expect(toggleAll).not.toBeChecked(); + + await firstTodo.getByRole('checkbox').check(); + await checkNumberOfCompletedTodosInLocalStorage(page, 3); + + // Assert the toggle all is checked again. + await expect(toggleAll).toBeChecked(); + }); +}); + +test.describe('Item', () => { + + test('should allow me to mark items as complete', async ({ page }) => { + // create a new todo locator + const newTodo = page.getByPlaceholder('What needs to be done?'); + + // Create two items. + for (const item of TODO_ITEMS.slice(0, 2)) { + await newTodo.fill(item); + await newTodo.press('Enter'); + } + + // Check first item. + const firstTodo = page.getByTestId('todo-item').nth(0); + await firstTodo.getByRole('checkbox').check(); + await expect(firstTodo).toHaveClass('completed'); + + // Check second item. + const secondTodo = page.getByTestId('todo-item').nth(1); + await expect(secondTodo).not.toHaveClass('completed'); + await secondTodo.getByRole('checkbox').check(); + + // Assert completed class. + await expect(firstTodo).toHaveClass('completed'); + await expect(secondTodo).toHaveClass('completed'); + }); + + test('should allow me to un-mark items as complete', async ({ page }) => { + // create a new todo locator + const newTodo = page.getByPlaceholder('What needs to be done?'); + + // Create two items. + for (const item of TODO_ITEMS.slice(0, 2)) { + await newTodo.fill(item); + await newTodo.press('Enter'); + } + + const firstTodo = page.getByTestId('todo-item').nth(0); + const secondTodo = page.getByTestId('todo-item').nth(1); + const firstTodoCheckbox = firstTodo.getByRole('checkbox'); + + await firstTodoCheckbox.check(); + await expect(firstTodo).toHaveClass('completed'); + await expect(secondTodo).not.toHaveClass('completed'); + await checkNumberOfCompletedTodosInLocalStorage(page, 1); + + await firstTodoCheckbox.uncheck(); + await expect(firstTodo).not.toHaveClass('completed'); + await expect(secondTodo).not.toHaveClass('completed'); + await checkNumberOfCompletedTodosInLocalStorage(page, 0); + }); + + test('should allow me to edit an item', async ({ page }) => { + await createDefaultTodos(page); + + const todoItems = page.getByTestId('todo-item'); + const secondTodo = todoItems.nth(1); + await secondTodo.dblclick(); + await expect(secondTodo.getByRole('textbox', { name: 'Edit' })).toHaveValue(TODO_ITEMS[1]); + await secondTodo.getByRole('textbox', { name: 'Edit' }).fill('buy some sausages'); + await secondTodo.getByRole('textbox', { name: 'Edit' }).press('Enter'); + + // Explicitly assert the new text value. + await expect(todoItems).toHaveText([ + TODO_ITEMS[0], + 'buy some sausages', + TODO_ITEMS[2] + ]); + await checkTodosInLocalStorage(page, 'buy some sausages'); + }); +}); + +test.describe('Editing', () => { + test.beforeEach(async ({ page }) => { + await createDefaultTodos(page); + await checkNumberOfTodosInLocalStorage(page, 3); + }); + + test('should hide other controls when editing', async ({ page }) => { + const todoItem = page.getByTestId('todo-item').nth(1); + await todoItem.dblclick(); + await expect(todoItem.getByRole('checkbox')).not.toBeVisible(); + await expect(todoItem.locator('label', { + hasText: TODO_ITEMS[1], + })).not.toBeVisible(); + await checkNumberOfTodosInLocalStorage(page, 3); + }); + + test('should save edits on blur', async ({ page }) => { + const todoItems = page.getByTestId('todo-item'); + await todoItems.nth(1).dblclick(); + await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill('buy some sausages'); + await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).dispatchEvent('blur'); + + await expect(todoItems).toHaveText([ + TODO_ITEMS[0], + 'buy some sausages', + TODO_ITEMS[2], + ]); + await checkTodosInLocalStorage(page, 'buy some sausages'); + }); + + test('should trim entered text', async ({ page }) => { + const todoItems = page.getByTestId('todo-item'); + await todoItems.nth(1).dblclick(); + await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill(' buy some sausages '); + await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).press('Enter'); + + await expect(todoItems).toHaveText([ + TODO_ITEMS[0], + 'buy some sausages', + TODO_ITEMS[2], + ]); + await checkTodosInLocalStorage(page, 'buy some sausages'); + }); + + test('should remove the item if an empty text string was entered', async ({ page }) => { + const todoItems = page.getByTestId('todo-item'); + await todoItems.nth(1).dblclick(); + await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill(''); + await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).press('Enter'); + + await expect(todoItems).toHaveText([ + TODO_ITEMS[0], + TODO_ITEMS[2], + ]); + }); + + test('should cancel edits on escape', async ({ page }) => { + const todoItems = page.getByTestId('todo-item'); + await todoItems.nth(1).dblclick(); + await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill('buy some sausages'); + await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).press('Escape'); + await expect(todoItems).toHaveText(TODO_ITEMS); + }); +}); + +test.describe('Counter', () => { + test('should display the current number of todo items', async ({ page }) => { + // create a new todo locator + const newTodo = page.getByPlaceholder('What needs to be done?'); + + // create a todo count locator + const todoCount = page.getByTestId('todo-count') + + await newTodo.fill(TODO_ITEMS[0]); + await newTodo.press('Enter'); + await expect(todoCount).toContainText('1'); + + await newTodo.fill(TODO_ITEMS[1]); + await newTodo.press('Enter'); + await expect(todoCount).toContainText('2'); + + await checkNumberOfTodosInLocalStorage(page, 2); + }); +}); + +test.describe('Clear completed button', () => { + test.beforeEach(async ({ page }) => { + await createDefaultTodos(page); + }); + + test('should display the correct text', async ({ page }) => { + await page.locator('.todo-list li .toggle').first().check(); + await expect(page.getByRole('button', { name: 'Clear completed' })).toBeVisible(); + }); + + test('should remove completed items when clicked', async ({ page }) => { + const todoItems = page.getByTestId('todo-item'); + await todoItems.nth(1).getByRole('checkbox').check(); + await page.getByRole('button', { name: 'Clear completed' }).click(); + await expect(todoItems).toHaveCount(2); + await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[2]]); + }); + + test('should be hidden when there are no items that are completed', async ({ page }) => { + await page.locator('.todo-list li .toggle').first().check(); + await page.getByRole('button', { name: 'Clear completed' }).click(); + await expect(page.getByRole('button', { name: 'Clear completed' })).toBeHidden(); + }); +}); + +test.describe('Persistence', () => { + test('should persist its data', async ({ page }) => { + // create a new todo locator + const newTodo = page.getByPlaceholder('What needs to be done?'); + + for (const item of TODO_ITEMS.slice(0, 2)) { + await newTodo.fill(item); + await newTodo.press('Enter'); + } + + const todoItems = page.getByTestId('todo-item'); + const firstTodoCheck = todoItems.nth(0).getByRole('checkbox'); + await firstTodoCheck.check(); + await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[1]]); + await expect(firstTodoCheck).toBeChecked(); + await expect(todoItems).toHaveClass(['completed', '']); + + // Ensure there is 1 completed item. + await checkNumberOfCompletedTodosInLocalStorage(page, 1); + + // Now reload. + await page.reload(); + await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[1]]); + await expect(firstTodoCheck).toBeChecked(); + await expect(todoItems).toHaveClass(['completed', '']); + }); +}); + +test.describe('Routing', () => { + test.beforeEach(async ({ page }) => { + await createDefaultTodos(page); + // make sure the app had a chance to save updated todos in storage + // before navigating to a new view, otherwise the items can get lost :( + // in some frameworks like Durandal + await checkTodosInLocalStorage(page, TODO_ITEMS[0]); + }); + + test('should allow me to display active items', async ({ page }) => { + const todoItem = page.getByTestId('todo-item'); + await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check(); + + await checkNumberOfCompletedTodosInLocalStorage(page, 1); + await page.getByRole('link', { name: 'Active' }).click(); + await expect(todoItem).toHaveCount(2); + await expect(todoItem).toHaveText([TODO_ITEMS[0], TODO_ITEMS[2]]); + }); + + test('should respect the back button', async ({ page }) => { + const todoItem = page.getByTestId('todo-item'); + await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check(); + + await checkNumberOfCompletedTodosInLocalStorage(page, 1); + + await test.step('Showing all items', async () => { + await page.getByRole('link', { name: 'All' }).click(); + await expect(todoItem).toHaveCount(3); + }); + + await test.step('Showing active items', async () => { + await page.getByRole('link', { name: 'Active' }).click(); + }); + + await test.step('Showing completed items', async () => { + await page.getByRole('link', { name: 'Completed' }).click(); + }); + + await expect(todoItem).toHaveCount(1); + await page.goBack(); + await expect(todoItem).toHaveCount(2); + await page.goBack(); + await expect(todoItem).toHaveCount(3); + }); + + test('should allow me to display completed items', async ({ page }) => { + await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check(); + await checkNumberOfCompletedTodosInLocalStorage(page, 1); + await page.getByRole('link', { name: 'Completed' }).click(); + await expect(page.getByTestId('todo-item')).toHaveCount(1); + }); + + test('should allow me to display all items', async ({ page }) => { + await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check(); + await checkNumberOfCompletedTodosInLocalStorage(page, 1); + await page.getByRole('link', { name: 'Active' }).click(); + await page.getByRole('link', { name: 'Completed' }).click(); + await page.getByRole('link', { name: 'All' }).click(); + await expect(page.getByTestId('todo-item')).toHaveCount(3); + }); + + test('should highlight the currently applied filter', async ({ page }) => { + await expect(page.getByRole('link', { name: 'All' })).toHaveClass('selected'); + + //create locators for active and completed links + const activeLink = page.getByRole('link', { name: 'Active' }); + const completedLink = page.getByRole('link', { name: 'Completed' }); + await activeLink.click(); + + // Page change - active items. + await expect(activeLink).toHaveClass('selected'); + await completedLink.click(); + + // Page change - completed items. + await expect(completedLink).toHaveClass('selected'); + }); +}); + +async function createDefaultTodos(page) { + // create a new todo locator + const newTodo = page.getByPlaceholder('What needs to be done?'); + + for (const item of TODO_ITEMS) { + await newTodo.fill(item); + await newTodo.press('Enter'); + } +} + +/** + * @param {import('@playwright/test').Page} page + * @param {number} expected + */ + async function checkNumberOfTodosInLocalStorage(page, expected) { + return await page.waitForFunction(e => { + return JSON.parse(localStorage['react-todos']).length === e; + }, expected); +} + +/** + * @param {import('@playwright/test').Page} page + * @param {number} expected + */ + async function checkNumberOfCompletedTodosInLocalStorage(page, expected) { + return await page.waitForFunction(e => { + return JSON.parse(localStorage['react-todos']).filter(i => i.completed).length === e; + }, expected); +} + +/** + * @param {import('@playwright/test').Page} page + * @param {string} title + */ +async function checkTodosInLocalStorage(page, title) { + return await page.waitForFunction(t => { + return JSON.parse(localStorage['react-todos']).map(i => i.title).includes(t); + }, title); +} From 58aee797cf071bd4b3e0723856bc9d5aba6f43bc Mon Sep 17 00:00:00 2001 From: Rishav Dutta Date: Fri, 3 Jan 2025 00:44:58 +0530 Subject: [PATCH 02/27] add: add inventory and product image test scripts --- .gitignore | 1 + artifacts/storage-states/admin.json | 2 +- assets/product_test_image.png | Bin 0 -> 204047 bytes specs/addPricingInventory.spec.js | 14 +++++++++++++ specs/addProductImage.spec.js | 12 ++++++++++- specs/createSimpleProduct.spec.js | 14 +++---------- utils/e2eUtils/createProductUtils.js | 16 +++++++++++++++ utils/e2eUtils/productInventoryUtils.js | 26 ++++++++++++++++++++++++ 8 files changed, 72 insertions(+), 13 deletions(-) create mode 100644 assets/product_test_image.png create mode 100644 specs/addPricingInventory.spec.js create mode 100644 utils/e2eUtils/createProductUtils.js create mode 100644 utils/e2eUtils/productInventoryUtils.js diff --git a/.gitignore b/.gitignore index 07919b8..bc9d287 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ node_modules/ /blob-report/ /playwright/.cache/ .env +.DS_Store \ No newline at end of file diff --git a/artifacts/storage-states/admin.json b/artifacts/storage-states/admin.json index ccbc919..d86c569 100644 --- a/artifacts/storage-states/admin.json +++ b/artifacts/storage-states/admin.json @@ -1 +1 @@ -{"cookies":[{"name":"wordpress_test_cookie","value":"WP%20Cookie%20check","domain":"rishav.rt.gw","path":"/","expires":-1,"httpOnly":false,"secure":true,"sameSite":"Lax"},{"name":"wordpress_sec_074e74b03d8a277981a80ce1db027a5e","value":"rishav%7C1735989750%7CubRA6Ye5updhqdmEOWAKgz84yKzEF8Q9TUPjgymACql%7C1046294a01822a5c0dac997ff1ddff4cdff18f0cfd388ddf56d1c62f9411e330","domain":"rishav.rt.gw","path":"/wp-content/plugins","expires":-1,"httpOnly":true,"secure":true,"sameSite":"Lax"},{"name":"wordpress_sec_074e74b03d8a277981a80ce1db027a5e","value":"rishav%7C1735989750%7CubRA6Ye5updhqdmEOWAKgz84yKzEF8Q9TUPjgymACql%7C1046294a01822a5c0dac997ff1ddff4cdff18f0cfd388ddf56d1c62f9411e330","domain":"rishav.rt.gw","path":"/wp-admin","expires":-1,"httpOnly":true,"secure":true,"sameSite":"Lax"},{"name":"wordpress_logged_in_074e74b03d8a277981a80ce1db027a5e","value":"rishav%7C1735989750%7CubRA6Ye5updhqdmEOWAKgz84yKzEF8Q9TUPjgymACql%7Cb51e60ea88c1d86658364525c9fabb01f3293d38ecabbf5b4a4c5d39fba2d1db","domain":"rishav.rt.gw","path":"/","expires":-1,"httpOnly":true,"secure":true,"sameSite":"Lax"},{"name":"tk_ai","value":"woo%3Att9OmINARUla7NfCYnKU5fJW","domain":"rishav.rt.gw","path":"/","expires":-1,"httpOnly":false,"secure":false,"sameSite":"Lax"},{"name":"wp-settings-time-2","value":"1735816950","domain":"rishav.rt.gw","path":"/","expires":1767352950.878,"httpOnly":false,"secure":true,"sameSite":"Lax"}],"nonce":"6bab227d76","rootURL":"https://rishav.rt.gw/wp-json/"} \ No newline at end of file +{"cookies":[{"name":"wordpress_test_cookie","value":"WP%20Cookie%20check","domain":"rishav.rt.gw","path":"/","expires":-1,"httpOnly":false,"secure":true,"sameSite":"Lax"},{"name":"wordpress_sec_074e74b03d8a277981a80ce1db027a5e","value":"rishav%7C1736017969%7CUCAsMpazQpoVLEWVfZS7bcwNfJpfzH4Lp6zy0CmChOZ%7C50d719663a932383383b80e4bd961c61b7a0ad1ba7df4640b02234db9b5a76c4","domain":"rishav.rt.gw","path":"/wp-content/plugins","expires":-1,"httpOnly":true,"secure":true,"sameSite":"Lax"},{"name":"wordpress_sec_074e74b03d8a277981a80ce1db027a5e","value":"rishav%7C1736017969%7CUCAsMpazQpoVLEWVfZS7bcwNfJpfzH4Lp6zy0CmChOZ%7C50d719663a932383383b80e4bd961c61b7a0ad1ba7df4640b02234db9b5a76c4","domain":"rishav.rt.gw","path":"/wp-admin","expires":-1,"httpOnly":true,"secure":true,"sameSite":"Lax"},{"name":"wordpress_logged_in_074e74b03d8a277981a80ce1db027a5e","value":"rishav%7C1736017969%7CUCAsMpazQpoVLEWVfZS7bcwNfJpfzH4Lp6zy0CmChOZ%7C71da721eb6f47d73eea810d9d4cdf88bd9f75ab021bc05eb41cee3ad424b6e12","domain":"rishav.rt.gw","path":"/","expires":-1,"httpOnly":true,"secure":true,"sameSite":"Lax"},{"name":"tk_ai","value":"woo%3Att9OmINARUla7NfCYnKU5fJW","domain":"rishav.rt.gw","path":"/","expires":-1,"httpOnly":false,"secure":false,"sameSite":"Lax"},{"name":"wp-settings-time-2","value":"1735845170","domain":"rishav.rt.gw","path":"/","expires":1767381170.72,"httpOnly":false,"secure":true,"sameSite":"Lax"}],"nonce":"b87765517a","rootURL":"https://rishav.rt.gw/wp-json/"} \ 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 0000000000000000000000000000000000000000..71d9f37919f23812af4454ccc715598710a92586 GIT binary patch literal 204047 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 { + test('It should add the pricing and inventory', async({admin, page})=>{ + await addNewProduct(admin, page); + + await addPricingInventory(admin, page); + + await expect(page.locator('//div[@id="message" and contains(@class,"notice-success")]')).toBeVisible(); + + }) +}) diff --git a/specs/addProductImage.spec.js b/specs/addProductImage.spec.js index 0f1a046..c953468 100644 --- a/specs/addProductImage.spec.js +++ b/specs/addProductImage.spec.js @@ -1,7 +1,17 @@ const { test, expect } = require("@wordpress/e2e-test-utils-playwright"); +const { addNewProduct } = require("../utils/e2eUtils/createProductUtils"); + test.describe('Test the Product Image Feature', ()=>{ - test('It should be able to upload prodcut image', async({admin,page})=>{ + test('It should be able to upload product image', async({admin,page})=>{ + await addNewProduct(admin, page); + + await page.getByRole('link', { name: 'Set product image' }).click(); + await page.getByLabel('Select Files').click(); + await page.getByLabel('Select Files').setInputFiles('/Users/rishavdutta/Documents/qa-idp/WP-e2e-utls/TestAutomation-Hands-on/assets/product_test_image.png'); + await page.getByRole('button', { name: 'Set product image' }).click(); + + }); }) \ No newline at end of file diff --git a/specs/createSimpleProduct.spec.js b/specs/createSimpleProduct.spec.js index c4a0fc4..558a479 100644 --- a/specs/createSimpleProduct.spec.js +++ b/specs/createSimpleProduct.spec.js @@ -1,21 +1,13 @@ const {test, expect} = require('@wordpress/e2e-test-utils-playwright'); +const {addNewProduct} = require('../utils/e2eUtils/createProductUtils'); + test.describe('It should test the Simple Product Functionality', ()=>{ test('It should test the creation of simple product', async({admin, page})=>{ - await admin.visitAdminPage( '/post-new.php','post_type=product' ); - - const productTitle = page.locator('//input[@id="title"]'); - await productTitle.fill('Product Demo Title'); - const productDescIframe = page.frameLocator('#content_ifr'); - const productDesc = await productDescIframe.locator('body#tinymce p'); - await productDesc.fill('New content for the paragraph.'); - const submitButton = page.locator('//input[@id="publish"]'); - - await submitButton.click(); + await addNewProduct(admin, page); await expect(page.locator('//div[@id="message" and contains(@class,"notice-success")]//p')).toContainText('Product published.'); - }); }) \ No newline at end of file diff --git a/utils/e2eUtils/createProductUtils.js b/utils/e2eUtils/createProductUtils.js new file mode 100644 index 0000000..3fae4ef --- /dev/null +++ b/utils/e2eUtils/createProductUtils.js @@ -0,0 +1,16 @@ +const addNewProduct = async (admin, page) => { + await admin.visitAdminPage("/post-new.php", "post_type=product"); + + const productTitle = page.locator('//input[@id="title"]'); + await productTitle.fill("Product Demo Title"); + const productDescIframe = page.frameLocator("#content_ifr"); + const productDesc = await productDescIframe.locator("body#tinymce p"); + await productDesc.fill("New content for the paragraph."); + const submitButton = page.locator('//input[@id="publish"]'); + + await submitButton.click(); +}; + +module.exports = { + addNewProduct +} diff --git a/utils/e2eUtils/productInventoryUtils.js b/utils/e2eUtils/productInventoryUtils.js new file mode 100644 index 0000000..6002228 --- /dev/null +++ b/utils/e2eUtils/productInventoryUtils.js @@ -0,0 +1,26 @@ +const addPricingInventory = async (admin, page) => { + const generalOptionsTab = page.locator('//li[contains(@class,"general_options")]//a'); + await generalOptionsTab.click(); + + const regularPriceField = page.locator('//input[@id="_regular_price"]'); + await regularPriceField.fill("100"); + + const salePriceField = page.locator('//input[@id="_sale_price"]'); + await salePriceField.fill('80'); + + const inventoryOptionsTab = page.locator('//li[contains(@class,"inventory_options")]//a'); + await inventoryOptionsTab.click(); + + const manageStockCheckBox = page.locator('//input[@id="_manage_stock"]'); + await manageStockCheckBox.check(); + + const stockQuantityField = page.locator('//input[@id="_stock"]'); + await stockQuantityField.fill('2'); + + const updateButton = page.locator('//input[@id="publish"]'); + await updateButton.click(); +} + +module.exports = { + addPricingInventory, +} From 2d26fc3ec87380e55c1417a49c9b64c3db494af9 Mon Sep 17 00:00:00 2001 From: Rishav Dutta Date: Fri, 3 Jan 2025 16:56:03 +0530 Subject: [PATCH 03/27] add: add test scripts for publish product and visibility of prdt --- .gitignore | 3 ++- artifacts/storage-states/admin.json | 2 +- specs/publishProduct.spec.js | 16 ++++++++++++++++ 3 files changed, 19 insertions(+), 2 deletions(-) create mode 100644 specs/publishProduct.spec.js diff --git a/.gitignore b/.gitignore index bc9d287..3e9277b 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,5 @@ node_modules/ /blob-report/ /playwright/.cache/ .env -.DS_Store \ No newline at end of file +.DS_Store +artifacts/ \ No newline at end of file diff --git a/artifacts/storage-states/admin.json b/artifacts/storage-states/admin.json index d86c569..89bd0e0 100644 --- a/artifacts/storage-states/admin.json +++ b/artifacts/storage-states/admin.json @@ -1 +1 @@ -{"cookies":[{"name":"wordpress_test_cookie","value":"WP%20Cookie%20check","domain":"rishav.rt.gw","path":"/","expires":-1,"httpOnly":false,"secure":true,"sameSite":"Lax"},{"name":"wordpress_sec_074e74b03d8a277981a80ce1db027a5e","value":"rishav%7C1736017969%7CUCAsMpazQpoVLEWVfZS7bcwNfJpfzH4Lp6zy0CmChOZ%7C50d719663a932383383b80e4bd961c61b7a0ad1ba7df4640b02234db9b5a76c4","domain":"rishav.rt.gw","path":"/wp-content/plugins","expires":-1,"httpOnly":true,"secure":true,"sameSite":"Lax"},{"name":"wordpress_sec_074e74b03d8a277981a80ce1db027a5e","value":"rishav%7C1736017969%7CUCAsMpazQpoVLEWVfZS7bcwNfJpfzH4Lp6zy0CmChOZ%7C50d719663a932383383b80e4bd961c61b7a0ad1ba7df4640b02234db9b5a76c4","domain":"rishav.rt.gw","path":"/wp-admin","expires":-1,"httpOnly":true,"secure":true,"sameSite":"Lax"},{"name":"wordpress_logged_in_074e74b03d8a277981a80ce1db027a5e","value":"rishav%7C1736017969%7CUCAsMpazQpoVLEWVfZS7bcwNfJpfzH4Lp6zy0CmChOZ%7C71da721eb6f47d73eea810d9d4cdf88bd9f75ab021bc05eb41cee3ad424b6e12","domain":"rishav.rt.gw","path":"/","expires":-1,"httpOnly":true,"secure":true,"sameSite":"Lax"},{"name":"tk_ai","value":"woo%3Att9OmINARUla7NfCYnKU5fJW","domain":"rishav.rt.gw","path":"/","expires":-1,"httpOnly":false,"secure":false,"sameSite":"Lax"},{"name":"wp-settings-time-2","value":"1735845170","domain":"rishav.rt.gw","path":"/","expires":1767381170.72,"httpOnly":false,"secure":true,"sameSite":"Lax"}],"nonce":"b87765517a","rootURL":"https://rishav.rt.gw/wp-json/"} \ No newline at end of file +{"cookies":[{"name":"wordpress_test_cookie","value":"WP%20Cookie%20check","domain":"rishav.rt.gw","path":"/","expires":-1,"httpOnly":false,"secure":true,"sameSite":"Lax"},{"name":"wordpress_sec_074e74b03d8a277981a80ce1db027a5e","value":"rishav%7C1736076275%7CINP9xKj1KocHkP9k8Kg1hiINp12ZRcr3L3Nr0LvPAPZ%7C9e244828ff929694ecce4f5510139a7d2b3fa988b84d9df837c7af37528dcbdc","domain":"rishav.rt.gw","path":"/wp-content/plugins","expires":-1,"httpOnly":true,"secure":true,"sameSite":"Lax"},{"name":"wordpress_sec_074e74b03d8a277981a80ce1db027a5e","value":"rishav%7C1736076275%7CINP9xKj1KocHkP9k8Kg1hiINp12ZRcr3L3Nr0LvPAPZ%7C9e244828ff929694ecce4f5510139a7d2b3fa988b84d9df837c7af37528dcbdc","domain":"rishav.rt.gw","path":"/wp-admin","expires":-1,"httpOnly":true,"secure":true,"sameSite":"Lax"},{"name":"wordpress_logged_in_074e74b03d8a277981a80ce1db027a5e","value":"rishav%7C1736076275%7CINP9xKj1KocHkP9k8Kg1hiINp12ZRcr3L3Nr0LvPAPZ%7Cc73a9449550722cbf09a7919983b3134eaa994d42ff36574a25b28c387047c1d","domain":"rishav.rt.gw","path":"/","expires":-1,"httpOnly":true,"secure":true,"sameSite":"Lax"},{"name":"tk_ai","value":"woo%3Att9OmINARUla7NfCYnKU5fJW","domain":"rishav.rt.gw","path":"/","expires":-1,"httpOnly":false,"secure":false,"sameSite":"Lax"},{"name":"wp-settings-time-2","value":"1735903476","domain":"rishav.rt.gw","path":"/","expires":1767439476.475,"httpOnly":false,"secure":true,"sameSite":"Lax"}],"nonce":"bee3caf576","rootURL":"https://rishav.rt.gw/wp-json/"} \ No newline at end of file diff --git a/specs/publishProduct.spec.js b/specs/publishProduct.spec.js new file mode 100644 index 0000000..0eac4d4 --- /dev/null +++ b/specs/publishProduct.spec.js @@ -0,0 +1,16 @@ +const { test, expect } = require("@wordpress/e2e-test-utils-playwright"); + +const { addNewProduct } = require("../utils/e2eUtils/createProductUtils"); + +test.describe('It should verify the verify the visibility of products', ()=>{ + test('It should publish and then verify the visibility of the products', async({admin, page})=>{ + + await addNewProduct(admin, page); + + const viewProductLink = page.locator('//div[@id="message" and contains(@class,"notice-success")]//a'); + await viewProductLink.click(); + + await expect(page).toHaveTitle(/Product Demo TitleNew content for the paragraph./); + + }) +}) From 597e4b78fb387eb7b406aefaf1992e361a87dc4b Mon Sep 17 00:00:00 2001 From: Rishav Dutta Date: Fri, 3 Jan 2025 18:56:52 +0530 Subject: [PATCH 04/27] add: checkout and place order test script --- specs/checkoutPlaceOrder.spec.js | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 specs/checkoutPlaceOrder.spec.js diff --git a/specs/checkoutPlaceOrder.spec.js b/specs/checkoutPlaceOrder.spec.js new file mode 100644 index 0000000..7db55ee --- /dev/null +++ b/specs/checkoutPlaceOrder.spec.js @@ -0,0 +1,17 @@ +const { test, expect } = require("@wordpress/e2e-test-utils-playwright"); + +const { addNewProduct } = require("../utils/e2eUtils/createProductUtils"); + +test.describe('Test should verify the chekcout workflow', ()=>{ + test('It should be add product to cart, checkout and place order',async({admin, page})=>{ + await addNewProduct(admin, page); + + const viewProductLink = page.locator('//div[@id="message" and contains(@class,"notice-success")]//a'); + await viewProductLink.click(); + + const addToCartButton = page.locator('//button[contains(@class,"single_add_to_cart_button") and @name="add-to-cart"]'); + await addToCartButton.click(); + + + }) +}) From b44f175607cee954f42ccb6d423711a795911992 Mon Sep 17 00:00:00 2001 From: Rishav Dutta Date: Sun, 5 Jan 2025 14:30:17 +0530 Subject: [PATCH 05/27] refactor: add some code refactor and utils function --- artifacts/storage-states/admin.json | 2 +- specs/addPricingInventory.spec.js | 18 +++++++++++++- specs/addProductImage.spec.js | 18 +++++++++++++- specs/addUserCustomer.spec.js | 36 ++++++++++++++++++++++------ specs/createSimpleProduct.spec.js | 17 ++++++++++++- utils/e2eUtils/createProductUtils.js | 10 ++++---- utils/e2eUtils/randomTestCode.js | 7 ++++++ 7 files changed, 92 insertions(+), 16 deletions(-) create mode 100644 utils/e2eUtils/randomTestCode.js diff --git a/artifacts/storage-states/admin.json b/artifacts/storage-states/admin.json index 89bd0e0..49ea668 100644 --- a/artifacts/storage-states/admin.json +++ b/artifacts/storage-states/admin.json @@ -1 +1 @@ -{"cookies":[{"name":"wordpress_test_cookie","value":"WP%20Cookie%20check","domain":"rishav.rt.gw","path":"/","expires":-1,"httpOnly":false,"secure":true,"sameSite":"Lax"},{"name":"wordpress_sec_074e74b03d8a277981a80ce1db027a5e","value":"rishav%7C1736076275%7CINP9xKj1KocHkP9k8Kg1hiINp12ZRcr3L3Nr0LvPAPZ%7C9e244828ff929694ecce4f5510139a7d2b3fa988b84d9df837c7af37528dcbdc","domain":"rishav.rt.gw","path":"/wp-content/plugins","expires":-1,"httpOnly":true,"secure":true,"sameSite":"Lax"},{"name":"wordpress_sec_074e74b03d8a277981a80ce1db027a5e","value":"rishav%7C1736076275%7CINP9xKj1KocHkP9k8Kg1hiINp12ZRcr3L3Nr0LvPAPZ%7C9e244828ff929694ecce4f5510139a7d2b3fa988b84d9df837c7af37528dcbdc","domain":"rishav.rt.gw","path":"/wp-admin","expires":-1,"httpOnly":true,"secure":true,"sameSite":"Lax"},{"name":"wordpress_logged_in_074e74b03d8a277981a80ce1db027a5e","value":"rishav%7C1736076275%7CINP9xKj1KocHkP9k8Kg1hiINp12ZRcr3L3Nr0LvPAPZ%7Cc73a9449550722cbf09a7919983b3134eaa994d42ff36574a25b28c387047c1d","domain":"rishav.rt.gw","path":"/","expires":-1,"httpOnly":true,"secure":true,"sameSite":"Lax"},{"name":"tk_ai","value":"woo%3Att9OmINARUla7NfCYnKU5fJW","domain":"rishav.rt.gw","path":"/","expires":-1,"httpOnly":false,"secure":false,"sameSite":"Lax"},{"name":"wp-settings-time-2","value":"1735903476","domain":"rishav.rt.gw","path":"/","expires":1767439476.475,"httpOnly":false,"secure":true,"sameSite":"Lax"}],"nonce":"bee3caf576","rootURL":"https://rishav.rt.gw/wp-json/"} \ No newline at end of file +{"cookies":[{"name":"wordpress_test_cookie","value":"WP%20Cookie%20check","domain":"rishav.rt.gw","path":"/","expires":-1,"httpOnly":false,"secure":true,"sameSite":"Lax"},{"name":"wordpress_sec_074e74b03d8a277981a80ce1db027a5e","value":"rishav%7C1736231066%7C10qBssEjKIOpnJvnZDtoM7tcHb4RG345uNaCkrwjyaE%7C4286337fe4c607b390433c84d49afba94bd0eb1f109b9954c73fd72b8362731d","domain":"rishav.rt.gw","path":"/wp-content/plugins","expires":-1,"httpOnly":true,"secure":true,"sameSite":"Lax"},{"name":"wordpress_sec_074e74b03d8a277981a80ce1db027a5e","value":"rishav%7C1736231066%7C10qBssEjKIOpnJvnZDtoM7tcHb4RG345uNaCkrwjyaE%7C4286337fe4c607b390433c84d49afba94bd0eb1f109b9954c73fd72b8362731d","domain":"rishav.rt.gw","path":"/wp-admin","expires":-1,"httpOnly":true,"secure":true,"sameSite":"Lax"},{"name":"wordpress_logged_in_074e74b03d8a277981a80ce1db027a5e","value":"rishav%7C1736231066%7C10qBssEjKIOpnJvnZDtoM7tcHb4RG345uNaCkrwjyaE%7Cfca7ae573ab59d1df07accfca68bd896ea453c9366cfca7420bc12da3a725c18","domain":"rishav.rt.gw","path":"/","expires":-1,"httpOnly":true,"secure":true,"sameSite":"Lax"},{"name":"tk_ai","value":"woo%3Att9OmINARUla7NfCYnKU5fJW","domain":"rishav.rt.gw","path":"/","expires":-1,"httpOnly":false,"secure":false,"sameSite":"Lax"},{"name":"wp-settings-time-2","value":"1736058266","domain":"rishav.rt.gw","path":"/","expires":1767594266.844,"httpOnly":false,"secure":true,"sameSite":"Lax"}],"nonce":"b2f7bf60f0","rootURL":"https://rishav.rt.gw/wp-json/"} \ No newline at end of file diff --git a/specs/addPricingInventory.spec.js b/specs/addPricingInventory.spec.js index 5b5f98a..c83d056 100644 --- a/specs/addPricingInventory.spec.js +++ b/specs/addPricingInventory.spec.js @@ -2,9 +2,25 @@ 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'); + test.describe('Test should check the Pricing and Inventory feature', () => { + + let testCode = 0; + let productTitle = ''; + let productDescription = ''; + + test.beforeAll(()=>{ + + testCode = generateTestCode(); + + productTitle = `Product Demo Title ${testCode}`; + productDescription = `Demo Product Description ${testCode}`; + + }); + test('It should add the pricing and inventory', async({admin, page})=>{ - await addNewProduct(admin, page); + await addNewProduct(admin, page, productTitle, productDescription); await addPricingInventory(admin, page); diff --git a/specs/addProductImage.spec.js b/specs/addProductImage.spec.js index c953468..1d87655 100644 --- a/specs/addProductImage.spec.js +++ b/specs/addProductImage.spec.js @@ -1,10 +1,26 @@ const { test, expect } = require("@wordpress/e2e-test-utils-playwright"); const { addNewProduct } = require("../utils/e2eUtils/createProductUtils"); +const {generateTestCode} = require('../utils/e2eUtils/randomTestCode'); + test.describe('Test the Product Image Feature', ()=>{ + + let testCode = 0; + let productTitle = ''; + let productDescription = ''; + + test.beforeAll(()=>{ + + testCode = generateTestCode(); + + productTitle = `Product Demo Title ${testCode}`; + productDescription = `Demo Product Description ${testCode}`; + + }); + test('It should be able to upload product image', async({admin,page})=>{ - await addNewProduct(admin, page); + await addNewProduct(admin, page, productTitle, productDescription); await page.getByRole('link', { name: 'Set product image' }).click(); await page.getByLabel('Select Files').click(); diff --git a/specs/addUserCustomer.spec.js b/specs/addUserCustomer.spec.js index c0ed9ff..06ad0e8 100644 --- a/specs/addUserCustomer.spec.js +++ b/specs/addUserCustomer.spec.js @@ -1,25 +1,47 @@ const {test, expect} = require('@wordpress/e2e-test-utils-playwright'); +const {generateTestCode} = require('../utils/e2eUtils/randomTestCode'); + test.describe('It should customer user creation feature', () => { + + let testCode = 0; + let userName = ''; + let userEmail = ''; + let firstName = ''; + let lastName = ''; + let password = ''; + + test.beforeAll(()=>{ + testCode = generateTestCode(); + + userName = `TestUserName-${testCode}`; + userEmail = `test${testCode}@trial.com`; + firstName = `TestFirstName-${testCode}`; + lastName = `TestLastName-${testCode}`; + password = `TestPassword${testCode}*`; + + }) + test('It should create a customer user', async({admin, page})=>{ + await admin.visitAdminPage('user-new.php'); const userNameField = page.locator('//input[@id="user_login"]'); - await userNameField.fill('TestUserName'); + await userNameField.fill(userName); const emailField = page.locator('//input[@id="email"]'); - await emailField.fill('test@trial.com'); + await emailField.fill(userEmail); const firstNameField = page.locator('//input[@id="first_name"]'); - await firstNameField.fill('TestFirstName'); + await firstNameField.fill(firstName); const lastNameField = page.locator('//input[@id="last_name"]'); - await lastNameField.fill('TestLastNameField'); + await lastNameField.fill(lastName); const passwordField = page.locator('//input[@id="pass1"]'); - await passwordField.fill('TestPassword1234*'); + await passwordField.fill(password); const userRoleField = page.locator('//select[@id="role"]'); await userRoleField.selectOption('customer'); const addUserButton = page.locator('//input[@id="createusersub"]'); await addUserButton.click(); - await expect(page.getByRole('cell',{name: 'TestUserName Edit | Delete | View'})).toBeVisible(); + await expect(page.getByRole('cell',{name: `${userName} Edit | Delete | View`})).toBeVisible(); - }) + }); }) \ No newline at end of file diff --git a/specs/createSimpleProduct.spec.js b/specs/createSimpleProduct.spec.js index 558a479..5961811 100644 --- a/specs/createSimpleProduct.spec.js +++ b/specs/createSimpleProduct.spec.js @@ -2,10 +2,25 @@ const {test, expect} = require('@wordpress/e2e-test-utils-playwright'); const {addNewProduct} = require('../utils/e2eUtils/createProductUtils'); +const {generateTestCode} = require('../utils/e2eUtils/randomTestCode'); + test.describe('It should test the Simple Product Functionality', ()=>{ + let testCode = 0; + let productTitle = ''; + let productDescription = ''; + + test.beforeAll(()=>{ + + testCode = generateTestCode(); + + productTitle = `Product Demo Title ${testCode}`; + productDescription = `Demo Product Description ${testCode}`; + + }) + test('It should test the creation of simple product', async({admin, page})=>{ - await addNewProduct(admin, page); + await addNewProduct(admin, page, productTitle, productDescription); await expect(page.locator('//div[@id="message" and contains(@class,"notice-success")]//p')).toContainText('Product published.'); diff --git a/utils/e2eUtils/createProductUtils.js b/utils/e2eUtils/createProductUtils.js index 3fae4ef..f63dc65 100644 --- a/utils/e2eUtils/createProductUtils.js +++ b/utils/e2eUtils/createProductUtils.js @@ -1,11 +1,11 @@ -const addNewProduct = async (admin, page) => { +const addNewProduct = async (admin, page, productTitle, productDescription) => { await admin.visitAdminPage("/post-new.php", "post_type=product"); - const productTitle = page.locator('//input[@id="title"]'); - await productTitle.fill("Product Demo Title"); + const productTitleField = page.locator('//input[@id="title"]'); + await productTitleField.fill(productTitle); const productDescIframe = page.frameLocator("#content_ifr"); - const productDesc = await productDescIframe.locator("body#tinymce p"); - await productDesc.fill("New content for the paragraph."); + const productDescField = await productDescIframe.locator("body#tinymce p"); + await productDescField.fill(productDescription); const submitButton = page.locator('//input[@id="publish"]'); await submitButton.click(); diff --git a/utils/e2eUtils/randomTestCode.js b/utils/e2eUtils/randomTestCode.js new file mode 100644 index 0000000..d81a027 --- /dev/null +++ b/utils/e2eUtils/randomTestCode.js @@ -0,0 +1,7 @@ +const generateTestCode = () => { + return Math.floor(1000 + Math.random() * 9000); +} + +module.exports = { + generateTestCode +} \ No newline at end of file From bf9af35304bb05988322e93f69fffa92654661ff Mon Sep 17 00:00:00 2001 From: Rishav Dutta Date: Sun, 5 Jan 2025 16:52:14 +0530 Subject: [PATCH 06/27] add: add test data clean up function --- artifacts/storage-states/admin.json | 2 +- specs/addPricingInventory.spec.js | 19 ++++++++++++----- specs/addUserCustomer.spec.js | 8 +++++++ specs/createSimpleProduct.spec.js | 16 ++++++++++++-- specs/publishProduct.spec.js | 30 ++++++++++++++++++++++++--- utils/e2eUtils/testProductDeletion.js | 28 +++++++++++++++++++++++++ utils/e2eUtils/testUserDeletion.js | 27 ++++++++++++++++++++++++ 7 files changed, 119 insertions(+), 11 deletions(-) create mode 100644 utils/e2eUtils/testProductDeletion.js create mode 100644 utils/e2eUtils/testUserDeletion.js diff --git a/artifacts/storage-states/admin.json b/artifacts/storage-states/admin.json index 49ea668..58203b4 100644 --- a/artifacts/storage-states/admin.json +++ b/artifacts/storage-states/admin.json @@ -1 +1 @@ -{"cookies":[{"name":"wordpress_test_cookie","value":"WP%20Cookie%20check","domain":"rishav.rt.gw","path":"/","expires":-1,"httpOnly":false,"secure":true,"sameSite":"Lax"},{"name":"wordpress_sec_074e74b03d8a277981a80ce1db027a5e","value":"rishav%7C1736231066%7C10qBssEjKIOpnJvnZDtoM7tcHb4RG345uNaCkrwjyaE%7C4286337fe4c607b390433c84d49afba94bd0eb1f109b9954c73fd72b8362731d","domain":"rishav.rt.gw","path":"/wp-content/plugins","expires":-1,"httpOnly":true,"secure":true,"sameSite":"Lax"},{"name":"wordpress_sec_074e74b03d8a277981a80ce1db027a5e","value":"rishav%7C1736231066%7C10qBssEjKIOpnJvnZDtoM7tcHb4RG345uNaCkrwjyaE%7C4286337fe4c607b390433c84d49afba94bd0eb1f109b9954c73fd72b8362731d","domain":"rishav.rt.gw","path":"/wp-admin","expires":-1,"httpOnly":true,"secure":true,"sameSite":"Lax"},{"name":"wordpress_logged_in_074e74b03d8a277981a80ce1db027a5e","value":"rishav%7C1736231066%7C10qBssEjKIOpnJvnZDtoM7tcHb4RG345uNaCkrwjyaE%7Cfca7ae573ab59d1df07accfca68bd896ea453c9366cfca7420bc12da3a725c18","domain":"rishav.rt.gw","path":"/","expires":-1,"httpOnly":true,"secure":true,"sameSite":"Lax"},{"name":"tk_ai","value":"woo%3Att9OmINARUla7NfCYnKU5fJW","domain":"rishav.rt.gw","path":"/","expires":-1,"httpOnly":false,"secure":false,"sameSite":"Lax"},{"name":"wp-settings-time-2","value":"1736058266","domain":"rishav.rt.gw","path":"/","expires":1767594266.844,"httpOnly":false,"secure":true,"sameSite":"Lax"}],"nonce":"b2f7bf60f0","rootURL":"https://rishav.rt.gw/wp-json/"} \ No newline at end of file +{"cookies":[{"name":"wordpress_test_cookie","value":"WP%20Cookie%20check","domain":"rishav.rt.gw","path":"/","expires":-1,"httpOnly":false,"secure":true,"sameSite":"Lax"},{"name":"wordpress_sec_074e74b03d8a277981a80ce1db027a5e","value":"rishav%7C1736248440%7C8ouZvTW3cgMMbQo3rVnn0mjWLoA2HL0DKZ6KOoR5Toh%7C7512667c6c8ae0f5aa3d0a0987ce05aca16dc1d00a7fa86dedd22bdc58f2c52b","domain":"rishav.rt.gw","path":"/wp-content/plugins","expires":-1,"httpOnly":true,"secure":true,"sameSite":"Lax"},{"name":"wordpress_sec_074e74b03d8a277981a80ce1db027a5e","value":"rishav%7C1736248440%7C8ouZvTW3cgMMbQo3rVnn0mjWLoA2HL0DKZ6KOoR5Toh%7C7512667c6c8ae0f5aa3d0a0987ce05aca16dc1d00a7fa86dedd22bdc58f2c52b","domain":"rishav.rt.gw","path":"/wp-admin","expires":-1,"httpOnly":true,"secure":true,"sameSite":"Lax"},{"name":"wordpress_logged_in_074e74b03d8a277981a80ce1db027a5e","value":"rishav%7C1736248440%7C8ouZvTW3cgMMbQo3rVnn0mjWLoA2HL0DKZ6KOoR5Toh%7Cc01a6764a8d1f29e46d83ef786f72c2284a9d6ee6066e4e478013a2aabb84e69","domain":"rishav.rt.gw","path":"/","expires":-1,"httpOnly":true,"secure":true,"sameSite":"Lax"},{"name":"tk_ai","value":"woo%3Att9OmINARUla7NfCYnKU5fJW","domain":"rishav.rt.gw","path":"/","expires":-1,"httpOnly":false,"secure":false,"sameSite":"Lax"},{"name":"wp-settings-time-2","value":"1736075640","domain":"rishav.rt.gw","path":"/","expires":1767611640.866,"httpOnly":false,"secure":true,"sameSite":"Lax"}],"nonce":"c3a46ddb85","rootURL":"https://rishav.rt.gw/wp-json/"} \ No newline at end of file diff --git a/specs/addPricingInventory.spec.js b/specs/addPricingInventory.spec.js index c83d056..a96bfb7 100644 --- a/specs/addPricingInventory.spec.js +++ b/specs/addPricingInventory.spec.js @@ -4,27 +4,36 @@ const {addPricingInventory} = require('../utils/e2eUtils/productInventoryUtils') const {generateTestCode} = require('../utils/e2eUtils/randomTestCode'); +const {removeTestProductRecord} = require('../utils/e2eUtils/testProductDeletion'); + test.describe('Test should check the Pricing and Inventory feature', () => { let testCode = 0; let productTitle = ''; let productDescription = ''; - test.beforeAll(()=>{ + test.beforeEach(async ({admin, page})=>{ testCode = generateTestCode(); productTitle = `Product Demo Title ${testCode}`; productDescription = `Demo Product Description ${testCode}`; - }); - - test('It should add the pricing and inventory', async({admin, page})=>{ await addNewProduct(admin, page, productTitle, productDescription); + }); + + test('It should add the pricing and inventory', async({admin, page})=>{ + await addPricingInventory(admin, page); - + await expect(page.locator('//div[@id="message" and contains(@class,"notice-success")]')).toBeVisible(); + }); + + test.afterEach(async ({page})=>{ + + await removeTestProductRecord(page, productTitle); + }) }) diff --git a/specs/addUserCustomer.spec.js b/specs/addUserCustomer.spec.js index 06ad0e8..3d22a8a 100644 --- a/specs/addUserCustomer.spec.js +++ b/specs/addUserCustomer.spec.js @@ -2,6 +2,8 @@ const {test, expect} = require('@wordpress/e2e-test-utils-playwright'); const {generateTestCode} = require('../utils/e2eUtils/randomTestCode'); +const {removeTestUserRecord} = require('../utils/e2eUtils/testUserDeletion'); + test.describe('It should customer user creation feature', () => { let testCode = 0; @@ -44,4 +46,10 @@ test.describe('It should customer user creation feature', () => { await expect(page.getByRole('cell',{name: `${userName} Edit | Delete | View`})).toBeVisible(); }); + + test.afterEach(async ({admin, page})=>{ + + await removeTestUserRecord(admin, page, userName); + + }) }) \ No newline at end of file diff --git a/specs/createSimpleProduct.spec.js b/specs/createSimpleProduct.spec.js index 5961811..1b4f41a 100644 --- a/specs/createSimpleProduct.spec.js +++ b/specs/createSimpleProduct.spec.js @@ -4,25 +4,37 @@ const {addNewProduct} = require('../utils/e2eUtils/createProductUtils'); const {generateTestCode} = require('../utils/e2eUtils/randomTestCode'); +const {removeTestProductRecord} = require('../utils/e2eUtils/testProductDeletion'); + +require("dotenv").config(); + test.describe('It should test the Simple Product Functionality', ()=>{ let testCode = 0; let productTitle = ''; let productDescription = ''; - test.beforeAll(()=>{ + test.beforeEach( async ({admin, page})=>{ testCode = generateTestCode(); productTitle = `Product Demo Title ${testCode}`; productDescription = `Demo Product Description ${testCode}`; + await addNewProduct(admin, page, productTitle, productDescription); + }) test('It should test the creation of simple product', async({admin, page})=>{ - await addNewProduct(admin, page, productTitle, productDescription); + // await addNewProduct(admin, page, productTitle, productDescription); await expect(page.locator('//div[@id="message" and contains(@class,"notice-success")]//p')).toContainText('Product published.'); }); + + test.afterEach(async ({admin, page})=>{ + + await removeTestProductRecord(admin, page, productTitle); + + }) }) \ No newline at end of file diff --git a/specs/publishProduct.spec.js b/specs/publishProduct.spec.js index 0eac4d4..c166ed6 100644 --- a/specs/publishProduct.spec.js +++ b/specs/publishProduct.spec.js @@ -2,15 +2,39 @@ 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.describe('It should verify the verify the visibility of products', ()=>{ + + let testCode = 0; + let productTitle = ''; + let productDescription = ''; + + test.beforeEach( async ({admin, page})=>{ + + testCode = generateTestCode(); + + productTitle = `Product Demo Title ${testCode}`; + productDescription = `Demo Product Description ${testCode}`; + + await addNewProduct(admin, page, productTitle, productDescription); + + }) + test('It should publish and then verify the visibility of the products', async({admin, page})=>{ - - await addNewProduct(admin, page); const viewProductLink = page.locator('//div[@id="message" and contains(@class,"notice-success")]//a'); await viewProductLink.click(); - await expect(page).toHaveTitle(/Product Demo TitleNew content for the paragraph./); + await expect(page).toHaveTitle(`${productTitle}${productDescription} – rishav.rt.gw`); }) + + test.afterEach(async ({page})=>{ + await removeTestProductRecord(page, productTitle); + }); }) diff --git a/utils/e2eUtils/testProductDeletion.js b/utils/e2eUtils/testProductDeletion.js new file mode 100644 index 0000000..6a2e17d --- /dev/null +++ b/utils/e2eUtils/testProductDeletion.js @@ -0,0 +1,28 @@ +require("dotenv").config(); + +const removeTestProductRecord = async (admin, page, productTitle) => { + await admin.visitAdminPage('edit.php','post_type=product'); + + const searchProductField = page.locator('//input[@id="post-search-input"]'); + await searchProductField.fill(productTitle); + + const searchSubmitBtn = page.locator('//input[@id="search-submit"]'); + await searchSubmitBtn.click(); + + const selectedTestProduct = page.locator( + '//input[contains(@id,"cb-select-") and contains(@name,"post")]' + ); + await selectedTestProduct.check(); + + const bulkActionSelector = page.locator( + '//select[@id="bulk-action-selector-top"]' + ); + await bulkActionSelector.selectOption("Move to Trash"); + + const applyBtn = page.locator('//input[@id="doaction"]'); + await applyBtn.click(); +}; + +module.exports = { + removeTestProductRecord +} diff --git a/utils/e2eUtils/testUserDeletion.js b/utils/e2eUtils/testUserDeletion.js new file mode 100644 index 0000000..9ee42ea --- /dev/null +++ b/utils/e2eUtils/testUserDeletion.js @@ -0,0 +1,27 @@ +const removeTestUserRecord = async (admin, page, userName) => { + await admin.visitAdminPage("users.php"); + + const userSearchField = page.locator('//input[@id="user-search-input"]'); + await userSearchField.fill(userName); + + const userSearchBtn = page.locator('//input[@id="search-submit"]'); + await userSearchBtn.click(); + + const selectUserCheckBox = page.locator('//input[contains(@name,"users")]'); + await selectUserCheckBox.check(); + + const bulkActionSelector = page.locator( + '//select[@id="bulk-action-selector-top"]' + ); + await bulkActionSelector.selectOption("Delete"); + + const applyBtn = page.locator('//input[@id="doaction"]'); + await applyBtn.click(); + + const confirmDeletionBtn = page.locator('//input[@id="submit"]'); + await confirmDeletionBtn.click(); +}; + +module.exports = { + removeTestUserRecord +} From a55882483db75861523f1584c0bd2d549dbb067e Mon Sep 17 00:00:00 2001 From: Rishav Dutta Date: Sun, 5 Jan 2025 17:27:57 +0530 Subject: [PATCH 07/27] add: add test coupon deletion function --- artifacts/storage-states/admin.json | 2 +- specs/addCoupon.spec.js | 32 +++++++++++++++++++++++++--- utils/e2eUtils/testCouponDeletion.js | 30 ++++++++++++++++++++++++++ 3 files changed, 60 insertions(+), 4 deletions(-) create mode 100644 utils/e2eUtils/testCouponDeletion.js diff --git a/artifacts/storage-states/admin.json b/artifacts/storage-states/admin.json index 58203b4..c1617ba 100644 --- a/artifacts/storage-states/admin.json +++ b/artifacts/storage-states/admin.json @@ -1 +1 @@ -{"cookies":[{"name":"wordpress_test_cookie","value":"WP%20Cookie%20check","domain":"rishav.rt.gw","path":"/","expires":-1,"httpOnly":false,"secure":true,"sameSite":"Lax"},{"name":"wordpress_sec_074e74b03d8a277981a80ce1db027a5e","value":"rishav%7C1736248440%7C8ouZvTW3cgMMbQo3rVnn0mjWLoA2HL0DKZ6KOoR5Toh%7C7512667c6c8ae0f5aa3d0a0987ce05aca16dc1d00a7fa86dedd22bdc58f2c52b","domain":"rishav.rt.gw","path":"/wp-content/plugins","expires":-1,"httpOnly":true,"secure":true,"sameSite":"Lax"},{"name":"wordpress_sec_074e74b03d8a277981a80ce1db027a5e","value":"rishav%7C1736248440%7C8ouZvTW3cgMMbQo3rVnn0mjWLoA2HL0DKZ6KOoR5Toh%7C7512667c6c8ae0f5aa3d0a0987ce05aca16dc1d00a7fa86dedd22bdc58f2c52b","domain":"rishav.rt.gw","path":"/wp-admin","expires":-1,"httpOnly":true,"secure":true,"sameSite":"Lax"},{"name":"wordpress_logged_in_074e74b03d8a277981a80ce1db027a5e","value":"rishav%7C1736248440%7C8ouZvTW3cgMMbQo3rVnn0mjWLoA2HL0DKZ6KOoR5Toh%7Cc01a6764a8d1f29e46d83ef786f72c2284a9d6ee6066e4e478013a2aabb84e69","domain":"rishav.rt.gw","path":"/","expires":-1,"httpOnly":true,"secure":true,"sameSite":"Lax"},{"name":"tk_ai","value":"woo%3Att9OmINARUla7NfCYnKU5fJW","domain":"rishav.rt.gw","path":"/","expires":-1,"httpOnly":false,"secure":false,"sameSite":"Lax"},{"name":"wp-settings-time-2","value":"1736075640","domain":"rishav.rt.gw","path":"/","expires":1767611640.866,"httpOnly":false,"secure":true,"sameSite":"Lax"}],"nonce":"c3a46ddb85","rootURL":"https://rishav.rt.gw/wp-json/"} \ No newline at end of file +{"cookies":[{"name":"wordpress_test_cookie","value":"WP%20Cookie%20check","domain":"rishav.rt.gw","path":"/","expires":-1,"httpOnly":false,"secure":true,"sameSite":"Lax"},{"name":"wordpress_sec_074e74b03d8a277981a80ce1db027a5e","value":"rishav%7C1736250760%7Cv5JlDt8Z81XezGB7jxigVhvQhU4Ci0Edn6WmdGBdQuQ%7C6cb87c8cb9da7863f1e46bda5c5240108f9f27cb5fe305b9c067ec5a8038a4b9","domain":"rishav.rt.gw","path":"/wp-content/plugins","expires":-1,"httpOnly":true,"secure":true,"sameSite":"Lax"},{"name":"wordpress_sec_074e74b03d8a277981a80ce1db027a5e","value":"rishav%7C1736250760%7Cv5JlDt8Z81XezGB7jxigVhvQhU4Ci0Edn6WmdGBdQuQ%7C6cb87c8cb9da7863f1e46bda5c5240108f9f27cb5fe305b9c067ec5a8038a4b9","domain":"rishav.rt.gw","path":"/wp-admin","expires":-1,"httpOnly":true,"secure":true,"sameSite":"Lax"},{"name":"wordpress_logged_in_074e74b03d8a277981a80ce1db027a5e","value":"rishav%7C1736250760%7Cv5JlDt8Z81XezGB7jxigVhvQhU4Ci0Edn6WmdGBdQuQ%7C2e92ee898dc86a17c01d11435b81ca946c5417e1ea408cca6914060da58caec4","domain":"rishav.rt.gw","path":"/","expires":-1,"httpOnly":true,"secure":true,"sameSite":"Lax"},{"name":"tk_ai","value":"woo%3Att9OmINARUla7NfCYnKU5fJW","domain":"rishav.rt.gw","path":"/","expires":-1,"httpOnly":false,"secure":false,"sameSite":"Lax"},{"name":"wp-settings-time-2","value":"1736077961","domain":"rishav.rt.gw","path":"/","expires":1767613961.508,"httpOnly":false,"secure":true,"sameSite":"Lax"}],"nonce":"f697fd9697","rootURL":"https://rishav.rt.gw/wp-json/"} \ No newline at end of file diff --git a/specs/addCoupon.spec.js b/specs/addCoupon.spec.js index 9e7e70d..32fd011 100644 --- a/specs/addCoupon.spec.js +++ b/specs/addCoupon.spec.js @@ -1,12 +1,30 @@ const {test, expect} = require('@wordpress/e2e-test-utils-playwright'); +const {generateTestCode} = require('../utils/e2eUtils/randomTestCode'); + +const {removeTestCouponRecord} = require('../utils/e2eUtils/testCouponDeletion'); + + test.describe('It should test the coupon features', ()=>{ + + let testCode = 0; + + let couponCode = ''; + let couponDescription = ''; + + test.beforeEach(()=>{ + testCode = generateTestCode(); + + couponCode = `Off-${testCode}`; + couponDescription = `Description for test coupon`; + }); + test('It should test the add coupon feature', async ({admin, page})=>{ await admin.visitAdminPage('post-new.php','post_type=shop_coupon'); const couponCodeField = page.locator('//input[@id="title"]'); - await couponCodeField.fill("offtest"); - const couponDescription = page.locator('//textarea[@id="woocommerce-coupon-description"]'); - await couponDescription.fill("This is for testing purpose"); + await couponCodeField.fill(couponCode); + const couponDescriptionField = page.locator('//textarea[@id="woocommerce-coupon-description"]'); + await couponDescriptionField.fill(couponDescription); const couponTypeField = page.locator('//select[@id="discount_type"]'); await couponTypeField.selectOption('Percentage discount'); const couponAmountField = page.locator('//input[@id="coupon_amount"]'); @@ -18,5 +36,13 @@ test.describe('It should test the coupon features', ()=>{ await submitButton.click(); await expect(page.locator('//div[@id="message" and contains(@class,"notice-success")]')).toBeVisible(); + }); + + test.afterEach(async({admin, page})=>{ + + await removeTestCouponRecord(admin, page, couponCode); + }) + + }) \ No newline at end of file diff --git a/utils/e2eUtils/testCouponDeletion.js b/utils/e2eUtils/testCouponDeletion.js new file mode 100644 index 0000000..7bf8605 --- /dev/null +++ b/utils/e2eUtils/testCouponDeletion.js @@ -0,0 +1,30 @@ +const removeTestCouponRecord = async (admin, page, couponCode) => { + await admin.visitAdminPage( + "edit.php", + "post_type=shop_coupon&legacy_coupon_menu=1" + ); + + const couponSearchField = page.locator('//input[@id="post-search-input"]'); + await couponSearchField.fill(couponCode); + + const couponSearchBtn = page.locator('//input[@id="search-submit"]'); + await couponSearchBtn.click(); + + const selectedCouponCheckBox = page.locator( + '//input[contains(@id,"cb-select-") and contains(@name,"post")]' + ); + await selectedCouponCheckBox.check(); + + const bulkActionSelector = page.locator( + '//select[@id="bulk-action-selector-top"]' + ); + await bulkActionSelector.selectOption("Move to Trash"); + + const applyBtn = page.locator('//input[@id="doaction"]'); + await applyBtn.click(); +}; + + +module.exports = { + removeTestCouponRecord +} \ No newline at end of file From 56d7682a8f0007d2d4f746e1ed1b13ccf534f7d2 Mon Sep 17 00:00:00 2001 From: Rishav Dutta Date: Mon, 6 Jan 2025 01:42:04 +0530 Subject: [PATCH 08/27] add: add checkout and place order test scripts --- artifacts/storage-states/admin.json | 2 +- specs/addUserCustomer.spec.js | 19 +----- specs/checkoutPlaceOrder.spec.js | 77 ++++++++++++++++++++--- specs/checkoutVerifyCoupon.spec.js | 0 specs/createSimpleProduct.spec.js | 1 - utils/e2eUtils/checkoutPlaceOrderUtils.js | 52 +++++++++++++++ utils/e2eUtils/createCustomerUtils.js | 30 +++++++++ utils/e2eUtils/customerLoginUtils.js | 16 +++++ utils/e2eUtils/getProductSlug.js | 8 +++ 9 files changed, 180 insertions(+), 25 deletions(-) create mode 100644 specs/checkoutVerifyCoupon.spec.js create mode 100644 utils/e2eUtils/checkoutPlaceOrderUtils.js create mode 100644 utils/e2eUtils/createCustomerUtils.js create mode 100644 utils/e2eUtils/customerLoginUtils.js create mode 100644 utils/e2eUtils/getProductSlug.js diff --git a/artifacts/storage-states/admin.json b/artifacts/storage-states/admin.json index c1617ba..fee0507 100644 --- a/artifacts/storage-states/admin.json +++ b/artifacts/storage-states/admin.json @@ -1 +1 @@ -{"cookies":[{"name":"wordpress_test_cookie","value":"WP%20Cookie%20check","domain":"rishav.rt.gw","path":"/","expires":-1,"httpOnly":false,"secure":true,"sameSite":"Lax"},{"name":"wordpress_sec_074e74b03d8a277981a80ce1db027a5e","value":"rishav%7C1736250760%7Cv5JlDt8Z81XezGB7jxigVhvQhU4Ci0Edn6WmdGBdQuQ%7C6cb87c8cb9da7863f1e46bda5c5240108f9f27cb5fe305b9c067ec5a8038a4b9","domain":"rishav.rt.gw","path":"/wp-content/plugins","expires":-1,"httpOnly":true,"secure":true,"sameSite":"Lax"},{"name":"wordpress_sec_074e74b03d8a277981a80ce1db027a5e","value":"rishav%7C1736250760%7Cv5JlDt8Z81XezGB7jxigVhvQhU4Ci0Edn6WmdGBdQuQ%7C6cb87c8cb9da7863f1e46bda5c5240108f9f27cb5fe305b9c067ec5a8038a4b9","domain":"rishav.rt.gw","path":"/wp-admin","expires":-1,"httpOnly":true,"secure":true,"sameSite":"Lax"},{"name":"wordpress_logged_in_074e74b03d8a277981a80ce1db027a5e","value":"rishav%7C1736250760%7Cv5JlDt8Z81XezGB7jxigVhvQhU4Ci0Edn6WmdGBdQuQ%7C2e92ee898dc86a17c01d11435b81ca946c5417e1ea408cca6914060da58caec4","domain":"rishav.rt.gw","path":"/","expires":-1,"httpOnly":true,"secure":true,"sameSite":"Lax"},{"name":"tk_ai","value":"woo%3Att9OmINARUla7NfCYnKU5fJW","domain":"rishav.rt.gw","path":"/","expires":-1,"httpOnly":false,"secure":false,"sameSite":"Lax"},{"name":"wp-settings-time-2","value":"1736077961","domain":"rishav.rt.gw","path":"/","expires":1767613961.508,"httpOnly":false,"secure":true,"sameSite":"Lax"}],"nonce":"f697fd9697","rootURL":"https://rishav.rt.gw/wp-json/"} \ No newline at end of file +{"cookies":[{"name":"wordpress_test_cookie","value":"WP%20Cookie%20check","domain":"rishav.rt.gw","path":"/","expires":-1,"httpOnly":false,"secure":true,"sameSite":"Lax"},{"name":"wordpress_sec_074e74b03d8a277981a80ce1db027a5e","value":"rishav%7C1736280620%7CNaUXZNDWVOMSwtPHGPv73H7O7J7Ywo4FPvEU7py0B1D%7C426eb49c7ac98fc5ffdc380499cabd262ea5979f8c439f78416172b496c7b0b1","domain":"rishav.rt.gw","path":"/wp-content/plugins","expires":-1,"httpOnly":true,"secure":true,"sameSite":"Lax"},{"name":"wordpress_sec_074e74b03d8a277981a80ce1db027a5e","value":"rishav%7C1736280620%7CNaUXZNDWVOMSwtPHGPv73H7O7J7Ywo4FPvEU7py0B1D%7C426eb49c7ac98fc5ffdc380499cabd262ea5979f8c439f78416172b496c7b0b1","domain":"rishav.rt.gw","path":"/wp-admin","expires":-1,"httpOnly":true,"secure":true,"sameSite":"Lax"},{"name":"wordpress_logged_in_074e74b03d8a277981a80ce1db027a5e","value":"rishav%7C1736280620%7CNaUXZNDWVOMSwtPHGPv73H7O7J7Ywo4FPvEU7py0B1D%7C9060487495c563cf6f65a6df0a926be77b58688fccbf4e7ec88284e7aa38fe76","domain":"rishav.rt.gw","path":"/","expires":-1,"httpOnly":true,"secure":true,"sameSite":"Lax"},{"name":"tk_ai","value":"woo%3Att9OmINARUla7NfCYnKU5fJW","domain":"rishav.rt.gw","path":"/","expires":-1,"httpOnly":false,"secure":false,"sameSite":"Lax"},{"name":"wp-settings-time-2","value":"1736107821","domain":"rishav.rt.gw","path":"/","expires":1767643821.602,"httpOnly":false,"secure":true,"sameSite":"Lax"}],"nonce":"b678fc0391","rootURL":"https://rishav.rt.gw/wp-json/"} \ No newline at end of file diff --git a/specs/addUserCustomer.spec.js b/specs/addUserCustomer.spec.js index 3d22a8a..60730dc 100644 --- a/specs/addUserCustomer.spec.js +++ b/specs/addUserCustomer.spec.js @@ -4,6 +4,8 @@ const {generateTestCode} = require('../utils/e2eUtils/randomTestCode'); const {removeTestUserRecord} = require('../utils/e2eUtils/testUserDeletion'); +const { addCustomerUser } = require('../utils/e2eUtils/createCustomerUtils'); + test.describe('It should customer user creation feature', () => { let testCode = 0; @@ -26,22 +28,7 @@ test.describe('It should customer user creation feature', () => { test('It should create a customer user', async({admin, page})=>{ - await admin.visitAdminPage('user-new.php'); - const userNameField = page.locator('//input[@id="user_login"]'); - await userNameField.fill(userName); - const emailField = page.locator('//input[@id="email"]'); - await emailField.fill(userEmail); - const firstNameField = page.locator('//input[@id="first_name"]'); - await firstNameField.fill(firstName); - const lastNameField = page.locator('//input[@id="last_name"]'); - await lastNameField.fill(lastName); - const passwordField = page.locator('//input[@id="pass1"]'); - await passwordField.fill(password); - const userRoleField = page.locator('//select[@id="role"]'); - await userRoleField.selectOption('customer'); - const addUserButton = page.locator('//input[@id="createusersub"]'); - - await addUserButton.click(); + await addCustomerUser(admin, page, userName, userEmail, firstName, lastName, password); await expect(page.getByRole('cell',{name: `${userName} Edit | Delete | View`})).toBeVisible(); diff --git a/specs/checkoutPlaceOrder.spec.js b/specs/checkoutPlaceOrder.spec.js index 7db55ee..b61a3c6 100644 --- a/specs/checkoutPlaceOrder.spec.js +++ b/specs/checkoutPlaceOrder.spec.js @@ -1,17 +1,80 @@ -const { test, expect } = require("@wordpress/e2e-test-utils-playwright"); +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'); + test.describe('Test should verify the chekcout workflow', ()=>{ - test('It should be add product to cart, checkout and place order',async({admin, page})=>{ - await addNewProduct(admin, page); - const viewProductLink = page.locator('//div[@id="message" and contains(@class,"notice-success")]//a'); - await viewProductLink.click(); + let testCode = 0; + let productTitle = ''; + let productDescription = ''; + + let userName = ''; + let userEmail = ''; + let firstName = ''; + let lastName = ''; + let password = ''; + + test.beforeEach( async ({admin, page})=>{ + + testCode = generateTestCode(); + + productTitle = `Product Demo Title ${testCode}`; + productDescription = `Demo Product Description ${testCode}`; + + await addNewProduct(admin, page, productTitle, productDescription); + + await addPricingInventory(admin, page); - const addToCartButton = page.locator('//button[contains(@class,"single_add_to_cart_button") and @name="add-to-cart"]'); - await addToCartButton.click(); + 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); + + 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('It should be add product to cart, checkout and place order',async({admin, page})=>{ + + await customerUserLogin(page, userName, password); + + await checkoutPlaceOrder(page, firstName, lastName, productTitle, productDescription) + + await expect(page).toHaveTitle('Order Confirmation'); + }); + + test.afterEach(async ({admin, page})=>{ + + await page.goto(`${process.env.WP_BASE_URL}my-account`); + + const logoutLink = page.locator('//li//a[contains(text(),"Log out")]'); + await logoutLink.click(); + + // await removeTestProductRecord(admin, page, productTitle); + + // await removeTestUserRecord(admin, page, userName); + }) }) diff --git a/specs/checkoutVerifyCoupon.spec.js b/specs/checkoutVerifyCoupon.spec.js new file mode 100644 index 0000000..e69de29 diff --git a/specs/createSimpleProduct.spec.js b/specs/createSimpleProduct.spec.js index 1b4f41a..9036a66 100644 --- a/specs/createSimpleProduct.spec.js +++ b/specs/createSimpleProduct.spec.js @@ -26,7 +26,6 @@ test.describe('It should test the Simple Product Functionality', ()=>{ }) test('It should test the creation of simple product', async({admin, page})=>{ - // await addNewProduct(admin, page, productTitle, productDescription); await expect(page.locator('//div[@id="message" and contains(@class,"notice-success")]//p')).toContainText('Product published.'); diff --git a/utils/e2eUtils/checkoutPlaceOrderUtils.js b/utils/e2eUtils/checkoutPlaceOrderUtils.js new file mode 100644 index 0000000..f90efc6 --- /dev/null +++ b/utils/e2eUtils/checkoutPlaceOrderUtils.js @@ -0,0 +1,52 @@ +const {extractPrdtSlug} = require('./getProductSlug'); + +const checkoutPlaceOrder = async ( + page, + firstName, + lastName, + productTitle, + productDescription +) => { + const prdtSlug = extractPrdtSlug(productTitle, productDescription); + console.log(prdtSlug); + + await page.goto(`${process.env.WP_BASE_URL}product/${prdtSlug}`); + + const addToCartBtn = page.locator('//button[contains(text(),"Add to cart")]'); + await addToCartBtn.click(); + + await page.goto(`${process.env.WP_BASE_URL}checkout`); + + const billingCountryField = page.locator('//select[@id="billing-country"]'); + await billingCountryField.selectOption("India"); + + const firstNameField = page.locator('//input[@id="billing-first_name"]'); + await firstNameField.fill(firstName); + + const lastNameField = page.locator('//input[@id="billing-last_name"]'); + await lastNameField.fill(lastName); + + const addressField1 = page.locator('//input[@id="billing-address_1"]'); + await addressField1.fill("ABC st."); + + const cityField = page.locator('//input[@id="billing-city"]'); + await cityField.fill("Kolkata"); + + const stateField = page.locator('//select[@id="billing-state"]'); + await stateField.selectOption("West Bengal"); + + const pincodeField = page.locator('//input[@id="billing-postcode"]'); + await pincodeField.fill("123456"); + + const billingOption = page.locator( + '//span[contains(text(),"Cash on delivery")]' + ); + await billingOption.click(); + + const placeOrderBtn = page.locator('//div[contains(text(),"Place Order")]'); + await placeOrderBtn.click(); +}; + +module.exports = { + checkoutPlaceOrder, +}; diff --git a/utils/e2eUtils/createCustomerUtils.js b/utils/e2eUtils/createCustomerUtils.js new file mode 100644 index 0000000..8c0761b --- /dev/null +++ b/utils/e2eUtils/createCustomerUtils.js @@ -0,0 +1,30 @@ +const addCustomerUser = async ( + admin, + page, + userName, + userEmail, + firstName, + lastName, + password, +) => { + await admin.visitAdminPage("user-new.php"); + const userNameField = page.locator('//input[@id="user_login"]'); + await userNameField.fill(userName); + const emailField = page.locator('//input[@id="email"]'); + await emailField.fill(userEmail); + const firstNameField = page.locator('//input[@id="first_name"]'); + await firstNameField.fill(firstName); + const lastNameField = page.locator('//input[@id="last_name"]'); + await lastNameField.fill(lastName); + const passwordField = page.locator('//input[@id="pass1"]'); + await passwordField.fill(password); + const userRoleField = page.locator('//select[@id="role"]'); + await userRoleField.selectOption("customer"); + const addUserButton = page.locator('//input[@id="createusersub"]'); + + await addUserButton.click(); +}; + +module.exports = { + addCustomerUser +} diff --git a/utils/e2eUtils/customerLoginUtils.js b/utils/e2eUtils/customerLoginUtils.js new file mode 100644 index 0000000..7870574 --- /dev/null +++ b/utils/e2eUtils/customerLoginUtils.js @@ -0,0 +1,16 @@ +const customerUserLogin = async (page, userName, password) => { + await page.goto(`${process.env.WP_BASE_URL}my-account`); + + const userNameField = page.locator('//input[@id="username"]'); + await userNameField.fill(userName); + + const passwordField = page.locator('//input[@id="password"]'); + await passwordField.fill(password); + + 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..1ae4647 --- /dev/null +++ b/utils/e2eUtils/getProductSlug.js @@ -0,0 +1,8 @@ +const extractPrdtSlug = (productTitle, productDescription) => { + + return productTitle.split(" ").join("-").toLowerCase(); + productDescription.split(" ").join("-").toLowerCase(); +} + +module.exports = { + extractPrdtSlug +} \ No newline at end of file From 85e341f87910c6575c6940c0df52efc9ffe968df Mon Sep 17 00:00:00 2001 From: Rishav Dutta Date: Mon, 6 Jan 2025 16:48:05 +0530 Subject: [PATCH 09/27] add: add coupon verification test script --- artifacts/storage-states/admin.json | 2 +- specs/addCoupon.spec.js | 18 ++--- specs/addPricingInventory.spec.js | 4 +- specs/checkoutPlaceOrder.spec.js | 11 ++- specs/checkoutVerifyCoupon.spec.js | 86 +++++++++++++++++++++++ specs/publishProduct.spec.js | 6 +- utils/e2eUtils/checkoutPlaceOrderUtils.js | 8 +-- utils/e2eUtils/createCouponUtils.js | 22 ++++++ utils/e2eUtils/createProductUtils.js | 4 ++ utils/e2eUtils/getProductSlug.js | 4 +- 10 files changed, 136 insertions(+), 29 deletions(-) create mode 100644 utils/e2eUtils/createCouponUtils.js diff --git a/artifacts/storage-states/admin.json b/artifacts/storage-states/admin.json index fee0507..bb47db7 100644 --- a/artifacts/storage-states/admin.json +++ b/artifacts/storage-states/admin.json @@ -1 +1 @@ -{"cookies":[{"name":"wordpress_test_cookie","value":"WP%20Cookie%20check","domain":"rishav.rt.gw","path":"/","expires":-1,"httpOnly":false,"secure":true,"sameSite":"Lax"},{"name":"wordpress_sec_074e74b03d8a277981a80ce1db027a5e","value":"rishav%7C1736280620%7CNaUXZNDWVOMSwtPHGPv73H7O7J7Ywo4FPvEU7py0B1D%7C426eb49c7ac98fc5ffdc380499cabd262ea5979f8c439f78416172b496c7b0b1","domain":"rishav.rt.gw","path":"/wp-content/plugins","expires":-1,"httpOnly":true,"secure":true,"sameSite":"Lax"},{"name":"wordpress_sec_074e74b03d8a277981a80ce1db027a5e","value":"rishav%7C1736280620%7CNaUXZNDWVOMSwtPHGPv73H7O7J7Ywo4FPvEU7py0B1D%7C426eb49c7ac98fc5ffdc380499cabd262ea5979f8c439f78416172b496c7b0b1","domain":"rishav.rt.gw","path":"/wp-admin","expires":-1,"httpOnly":true,"secure":true,"sameSite":"Lax"},{"name":"wordpress_logged_in_074e74b03d8a277981a80ce1db027a5e","value":"rishav%7C1736280620%7CNaUXZNDWVOMSwtPHGPv73H7O7J7Ywo4FPvEU7py0B1D%7C9060487495c563cf6f65a6df0a926be77b58688fccbf4e7ec88284e7aa38fe76","domain":"rishav.rt.gw","path":"/","expires":-1,"httpOnly":true,"secure":true,"sameSite":"Lax"},{"name":"tk_ai","value":"woo%3Att9OmINARUla7NfCYnKU5fJW","domain":"rishav.rt.gw","path":"/","expires":-1,"httpOnly":false,"secure":false,"sameSite":"Lax"},{"name":"wp-settings-time-2","value":"1736107821","domain":"rishav.rt.gw","path":"/","expires":1767643821.602,"httpOnly":false,"secure":true,"sameSite":"Lax"}],"nonce":"b678fc0391","rootURL":"https://rishav.rt.gw/wp-json/"} \ No newline at end of file +{"cookies":[{"name":"wordpress_test_cookie","value":"WP%20Cookie%20check","domain":"rishav.rt.gw","path":"/","expires":-1,"httpOnly":false,"secure":true,"sameSite":"Lax"},{"name":"wordpress_sec_074e74b03d8a277981a80ce1db027a5e","value":"rishav%7C1736334849%7CAJe0puWjjWOTOvz7A6n2ZVQcjuJ92q58SNpFcKQ6XWV%7C562551c36f185bbfd67001f62fdb1fd572a2dd44100b41af804b7f1e3b67138c","domain":"rishav.rt.gw","path":"/wp-content/plugins","expires":-1,"httpOnly":true,"secure":true,"sameSite":"Lax"},{"name":"wordpress_sec_074e74b03d8a277981a80ce1db027a5e","value":"rishav%7C1736334849%7CAJe0puWjjWOTOvz7A6n2ZVQcjuJ92q58SNpFcKQ6XWV%7C562551c36f185bbfd67001f62fdb1fd572a2dd44100b41af804b7f1e3b67138c","domain":"rishav.rt.gw","path":"/wp-admin","expires":-1,"httpOnly":true,"secure":true,"sameSite":"Lax"},{"name":"wordpress_logged_in_074e74b03d8a277981a80ce1db027a5e","value":"rishav%7C1736334849%7CAJe0puWjjWOTOvz7A6n2ZVQcjuJ92q58SNpFcKQ6XWV%7C691b823a7c23cdefac3931bb5c685fb5f127a151c84c5f4e6d9e8c509f183be2","domain":"rishav.rt.gw","path":"/","expires":-1,"httpOnly":true,"secure":true,"sameSite":"Lax"},{"name":"tk_ai","value":"woo%3Att9OmINARUla7NfCYnKU5fJW","domain":"rishav.rt.gw","path":"/","expires":-1,"httpOnly":false,"secure":false,"sameSite":"Lax"},{"name":"wp-settings-time-2","value":"1736162049","domain":"rishav.rt.gw","path":"/","expires":1767698049.93,"httpOnly":false,"secure":true,"sameSite":"Lax"}],"nonce":"b158f64645","rootURL":"https://rishav.rt.gw/wp-json/"} \ No newline at end of file diff --git a/specs/addCoupon.spec.js b/specs/addCoupon.spec.js index 32fd011..19dc77f 100644 --- a/specs/addCoupon.spec.js +++ b/specs/addCoupon.spec.js @@ -4,6 +4,8 @@ const {generateTestCode} = require('../utils/e2eUtils/randomTestCode'); const {removeTestCouponRecord} = require('../utils/e2eUtils/testCouponDeletion'); +const {createCoupon} = require('../utils/e2eUtils/createCouponUtils'); + test.describe('It should test the coupon features', ()=>{ @@ -20,20 +22,8 @@ test.describe('It should test the coupon features', ()=>{ }); test('It should test the add coupon feature', async ({admin, page})=>{ - await admin.visitAdminPage('post-new.php','post_type=shop_coupon'); - const couponCodeField = page.locator('//input[@id="title"]'); - await couponCodeField.fill(couponCode); - const couponDescriptionField = page.locator('//textarea[@id="woocommerce-coupon-description"]'); - await couponDescriptionField.fill(couponDescription); - const couponTypeField = page.locator('//select[@id="discount_type"]'); - await couponTypeField.selectOption('Percentage discount'); - const couponAmountField = page.locator('//input[@id="coupon_amount"]'); - await couponAmountField.fill('10'); - const couponExpiryField = page.locator('//input[@id="expiry_date"]'); - await couponExpiryField.fill('2025-01-22'); - - const submitButton = page.locator('//input[@id="publish"]'); - await submitButton.click(); + + await createCoupon(admin, page, couponCode, couponDescription); await expect(page.locator('//div[@id="message" and contains(@class,"notice-success")]')).toBeVisible(); }); diff --git a/specs/addPricingInventory.spec.js b/specs/addPricingInventory.spec.js index a96bfb7..2427e4f 100644 --- a/specs/addPricingInventory.spec.js +++ b/specs/addPricingInventory.spec.js @@ -31,9 +31,9 @@ test.describe('Test should check the Pricing and Inventory feature', () => { }); - test.afterEach(async ({page})=>{ + test.afterEach(async ({admin, page})=>{ - await removeTestProductRecord(page, productTitle); + await removeTestProductRecord(admin, page, productTitle); }) }) diff --git a/specs/checkoutPlaceOrder.spec.js b/specs/checkoutPlaceOrder.spec.js index b61a3c6..c708db4 100644 --- a/specs/checkoutPlaceOrder.spec.js +++ b/specs/checkoutPlaceOrder.spec.js @@ -16,6 +16,8 @@ const {customerUserLogin} = require('../utils/e2eUtils/customerLoginUtils'); const {checkoutPlaceOrder} = require('../utils/e2eUtils/checkoutPlaceOrderUtils'); +require("dotenv").config(); + test.describe('Test should verify the chekcout workflow', ()=>{ let testCode = 0; @@ -59,19 +61,24 @@ test.describe('Test should verify the chekcout workflow', ()=>{ await customerUserLogin(page, userName, password); - await checkoutPlaceOrder(page, firstName, lastName, productTitle, productDescription) + await checkoutPlaceOrder(page, firstName, lastName, productTitle) await expect(page).toHaveTitle('Order Confirmation'); }); - test.afterEach(async ({admin, page})=>{ + test.afterEach(async ({admin, page, requestUtils})=>{ await page.goto(`${process.env.WP_BASE_URL}my-account`); const logoutLink = page.locator('//li//a[contains(text(),"Log out")]'); await logoutLink.click(); + // await requestUtils.login({ + // username: process.env.WP_USERNAME, + // password: process.env.WP_PASSWORD + // }); + // await removeTestProductRecord(admin, page, productTitle); // await removeTestUserRecord(admin, page, userName); diff --git a/specs/checkoutVerifyCoupon.spec.js b/specs/checkoutVerifyCoupon.spec.js index e69de29..a31ea5c 100644 --- a/specs/checkoutVerifyCoupon.spec.js +++ b/specs/checkoutVerifyCoupon.spec.js @@ -0,0 +1,86 @@ +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 { extractPrdtSlug } = require("../utils/e2eUtils/getProductSlug"); + +const {removeTestCouponRecord} = require('../utils/e2eUtils/testCouponDeletion'); + +const {createCoupon} = require('../utils/e2eUtils/createCouponUtils'); + +require("dotenv").config(); + +test.describe('It should test the coupon discounts', ()=> { + + let testCode = 0; + let productTitle = ''; + let productDescription = ''; + + let couponCode = ''; + let couponDescription = ''; + + test.beforeEach( async ({admin, page})=>{ + + testCode = generateTestCode(); + + productTitle = `Product Demo Title ${testCode}`; + productDescription = `Demo Product Description ${testCode}`; + + couponCode = `Off-${testCode}`; + couponDescription = `Description for test coupon`; + + await addNewProduct(admin, page, productTitle, productDescription); + + await addPricingInventory(admin, page); + + await createCoupon(admin, page, couponCode, couponDescription); + + }) + + test('The coupons should apply proper discount', async({admin, page, requestUtils})=>{ + + const prdtSlug = extractPrdtSlug(productTitle); + + await page.goto(`${process.env.WP_BASE_URL}product/${prdtSlug}`); + + const addToCartBtn = page.locator('//button[contains(text(),"Add to cart")]'); + await addToCartBtn.click(); + + await page.goto(`${process.env.WP_BASE_URL}checkout`); + + const couponFieldBtn = page.locator('//div[contains(@class,"wc-block-components-totals-coupon")]//div[@role="button"]'); + await couponFieldBtn.click(); + + const couponCodeField = page.locator('//input[@id="wc-block-components-totals-coupon__input-coupon"]'); + await couponCodeField.click(); + + await couponCodeField.fill(couponCode); + + const couponApplyBtn = page.locator('//span[contains(text(),"Apply")]'); + await couponApplyBtn.click(); + + 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'); + + }) + + test.afterEach(async ({admin, page}) => { + + await page.goto(`${process.env.WP_BASE_URL}/cart`); + + const removeItemBtn = page.locator('//button[@class="wc-block-cart-item__remove-link"]'); + await removeItemBtn.click(); + + await removeTestProductRecord(admin, page, productTitle); + + await removeTestCouponRecord(admin, page, couponCode); + + }); +}) \ No newline at end of file diff --git a/specs/publishProduct.spec.js b/specs/publishProduct.spec.js index c166ed6..ac84770 100644 --- a/specs/publishProduct.spec.js +++ b/specs/publishProduct.spec.js @@ -30,11 +30,11 @@ test.describe('It should verify the verify the visibility of products', ()=>{ const viewProductLink = page.locator('//div[@id="message" and contains(@class,"notice-success")]//a'); await viewProductLink.click(); - await expect(page).toHaveTitle(`${productTitle}${productDescription} – rishav.rt.gw`); + await expect(page).toHaveTitle(`${productTitle} – rishav.rt.gw`); }) - test.afterEach(async ({page})=>{ - await removeTestProductRecord(page, productTitle); + test.afterEach(async ({admin, page})=>{ + await removeTestProductRecord(admin, page, productTitle); }); }) diff --git a/utils/e2eUtils/checkoutPlaceOrderUtils.js b/utils/e2eUtils/checkoutPlaceOrderUtils.js index f90efc6..7132243 100644 --- a/utils/e2eUtils/checkoutPlaceOrderUtils.js +++ b/utils/e2eUtils/checkoutPlaceOrderUtils.js @@ -1,14 +1,12 @@ -const {extractPrdtSlug} = require('./getProductSlug'); +const { extractPrdtSlug } = require("./getProductSlug"); const checkoutPlaceOrder = async ( page, firstName, lastName, - productTitle, - productDescription + productTitle ) => { - const prdtSlug = extractPrdtSlug(productTitle, productDescription); - console.log(prdtSlug); + const prdtSlug = extractPrdtSlug(productTitle); await page.goto(`${process.env.WP_BASE_URL}product/${prdtSlug}`); diff --git a/utils/e2eUtils/createCouponUtils.js b/utils/e2eUtils/createCouponUtils.js new file mode 100644 index 0000000..c01c54f --- /dev/null +++ b/utils/e2eUtils/createCouponUtils.js @@ -0,0 +1,22 @@ +const createCoupon = async (admin, page, couponCode, couponDescription) => { + await admin.visitAdminPage("post-new.php", "post_type=shop_coupon"); + const couponCodeField = page.locator('//input[@id="title"]'); + await couponCodeField.fill(couponCode); + const couponDescriptionField = page.locator( + '//textarea[@id="woocommerce-coupon-description"]' + ); + await couponDescriptionField.fill(couponDescription); + const couponTypeField = page.locator('//select[@id="discount_type"]'); + await couponTypeField.selectOption("Percentage discount"); + const couponAmountField = page.locator('//input[@id="coupon_amount"]'); + await couponAmountField.fill("10"); + const couponExpiryField = page.locator('//input[@id="expiry_date"]'); + await couponExpiryField.fill("2025-01-22"); + + const submitButton = page.locator('//input[@id="publish"]'); + await submitButton.click(); +}; + +module.exports = { + createCoupon +} diff --git a/utils/e2eUtils/createProductUtils.js b/utils/e2eUtils/createProductUtils.js index f63dc65..40a0451 100644 --- a/utils/e2eUtils/createProductUtils.js +++ b/utils/e2eUtils/createProductUtils.js @@ -5,7 +5,11 @@ const addNewProduct = async (admin, page, productTitle, productDescription) => { await productTitleField.fill(productTitle); const productDescIframe = page.frameLocator("#content_ifr"); const productDescField = await productDescIframe.locator("body#tinymce p"); + + await productDescField.click(); + await productDescField.fill(productDescription); + const submitButton = page.locator('//input[@id="publish"]'); await submitButton.click(); diff --git a/utils/e2eUtils/getProductSlug.js b/utils/e2eUtils/getProductSlug.js index 1ae4647..1266cb6 100644 --- a/utils/e2eUtils/getProductSlug.js +++ b/utils/e2eUtils/getProductSlug.js @@ -1,6 +1,6 @@ -const extractPrdtSlug = (productTitle, productDescription) => { +const extractPrdtSlug = (productTitle) => { - return productTitle.split(" ").join("-").toLowerCase(); + productDescription.split(" ").join("-").toLowerCase(); + return productTitle.split(" ").join("-").toLowerCase(); } module.exports = { From 2c6fac18d8d4dfc131307618727a6484f16ac802 Mon Sep 17 00:00:00 2001 From: Rishav Dutta Date: Mon, 6 Jan 2025 17:20:54 +0530 Subject: [PATCH 10/27] refactor: add utils for apply coupon --- artifacts/storage-states/admin.json | 2 +- specs/checkoutVerifyCoupon.spec.js | 26 ++++------------------- utils/e2eUtils/applyCouponUtils.js | 32 +++++++++++++++++++++++++++++ 3 files changed, 37 insertions(+), 23 deletions(-) create mode 100644 utils/e2eUtils/applyCouponUtils.js diff --git a/artifacts/storage-states/admin.json b/artifacts/storage-states/admin.json index bb47db7..ae770a6 100644 --- a/artifacts/storage-states/admin.json +++ b/artifacts/storage-states/admin.json @@ -1 +1 @@ -{"cookies":[{"name":"wordpress_test_cookie","value":"WP%20Cookie%20check","domain":"rishav.rt.gw","path":"/","expires":-1,"httpOnly":false,"secure":true,"sameSite":"Lax"},{"name":"wordpress_sec_074e74b03d8a277981a80ce1db027a5e","value":"rishav%7C1736334849%7CAJe0puWjjWOTOvz7A6n2ZVQcjuJ92q58SNpFcKQ6XWV%7C562551c36f185bbfd67001f62fdb1fd572a2dd44100b41af804b7f1e3b67138c","domain":"rishav.rt.gw","path":"/wp-content/plugins","expires":-1,"httpOnly":true,"secure":true,"sameSite":"Lax"},{"name":"wordpress_sec_074e74b03d8a277981a80ce1db027a5e","value":"rishav%7C1736334849%7CAJe0puWjjWOTOvz7A6n2ZVQcjuJ92q58SNpFcKQ6XWV%7C562551c36f185bbfd67001f62fdb1fd572a2dd44100b41af804b7f1e3b67138c","domain":"rishav.rt.gw","path":"/wp-admin","expires":-1,"httpOnly":true,"secure":true,"sameSite":"Lax"},{"name":"wordpress_logged_in_074e74b03d8a277981a80ce1db027a5e","value":"rishav%7C1736334849%7CAJe0puWjjWOTOvz7A6n2ZVQcjuJ92q58SNpFcKQ6XWV%7C691b823a7c23cdefac3931bb5c685fb5f127a151c84c5f4e6d9e8c509f183be2","domain":"rishav.rt.gw","path":"/","expires":-1,"httpOnly":true,"secure":true,"sameSite":"Lax"},{"name":"tk_ai","value":"woo%3Att9OmINARUla7NfCYnKU5fJW","domain":"rishav.rt.gw","path":"/","expires":-1,"httpOnly":false,"secure":false,"sameSite":"Lax"},{"name":"wp-settings-time-2","value":"1736162049","domain":"rishav.rt.gw","path":"/","expires":1767698049.93,"httpOnly":false,"secure":true,"sameSite":"Lax"}],"nonce":"b158f64645","rootURL":"https://rishav.rt.gw/wp-json/"} \ No newline at end of file +{"cookies":[{"name":"wordpress_test_cookie","value":"WP%20Cookie%20check","domain":"rishav.rt.gw","path":"/","expires":-1,"httpOnly":false,"secure":true,"sameSite":"Lax"},{"name":"wordpress_sec_074e74b03d8a277981a80ce1db027a5e","value":"rishav%7C1736335513%7ClheY9bUFj5BfQvQAL7kQGI2OBynZ4fNwXSOM8QntXuY%7C2d98a5cdbab81eb8712eb1bc3bac33bd1edea8b7be4e9b850c1c5a2b6f0361d1","domain":"rishav.rt.gw","path":"/wp-content/plugins","expires":-1,"httpOnly":true,"secure":true,"sameSite":"Lax"},{"name":"wordpress_sec_074e74b03d8a277981a80ce1db027a5e","value":"rishav%7C1736335513%7ClheY9bUFj5BfQvQAL7kQGI2OBynZ4fNwXSOM8QntXuY%7C2d98a5cdbab81eb8712eb1bc3bac33bd1edea8b7be4e9b850c1c5a2b6f0361d1","domain":"rishav.rt.gw","path":"/wp-admin","expires":-1,"httpOnly":true,"secure":true,"sameSite":"Lax"},{"name":"wordpress_logged_in_074e74b03d8a277981a80ce1db027a5e","value":"rishav%7C1736335513%7ClheY9bUFj5BfQvQAL7kQGI2OBynZ4fNwXSOM8QntXuY%7C5b67d934df4c6c5fb38bb0646fe6bf4b2d0c0a770e944d4d31da48c9631f4743","domain":"rishav.rt.gw","path":"/","expires":-1,"httpOnly":true,"secure":true,"sameSite":"Lax"},{"name":"tk_ai","value":"woo%3Att9OmINARUla7NfCYnKU5fJW","domain":"rishav.rt.gw","path":"/","expires":-1,"httpOnly":false,"secure":false,"sameSite":"Lax"},{"name":"wp-settings-time-2","value":"1736162714","domain":"rishav.rt.gw","path":"/","expires":1767698714.828,"httpOnly":false,"secure":true,"sameSite":"Lax"}],"nonce":"ec0aa9668d","rootURL":"https://rishav.rt.gw/wp-json/"} \ No newline at end of file diff --git a/specs/checkoutVerifyCoupon.spec.js b/specs/checkoutVerifyCoupon.spec.js index a31ea5c..c3862d1 100644 --- a/specs/checkoutVerifyCoupon.spec.js +++ b/specs/checkoutVerifyCoupon.spec.js @@ -8,12 +8,12 @@ const {removeTestProductRecord} = require('../utils/e2eUtils/testProductDeletion const {addPricingInventory} = require('../utils/e2eUtils/productInventoryUtils'); -const { extractPrdtSlug } = require("../utils/e2eUtils/getProductSlug"); - const {removeTestCouponRecord} = require('../utils/e2eUtils/testCouponDeletion'); const {createCoupon} = require('../utils/e2eUtils/createCouponUtils'); +const {applyCouponDiscount} = require('../utils/e2eUtils/applyCouponUtils'); + require("dotenv").config(); test.describe('It should test the coupon discounts', ()=> { @@ -43,27 +43,9 @@ test.describe('It should test the coupon discounts', ()=> { }) - test('The coupons should apply proper discount', async({admin, page, requestUtils})=>{ - - const prdtSlug = extractPrdtSlug(productTitle); - - await page.goto(`${process.env.WP_BASE_URL}product/${prdtSlug}`); - - const addToCartBtn = page.locator('//button[contains(text(),"Add to cart")]'); - await addToCartBtn.click(); - - await page.goto(`${process.env.WP_BASE_URL}checkout`); - - const couponFieldBtn = page.locator('//div[contains(@class,"wc-block-components-totals-coupon")]//div[@role="button"]'); - await couponFieldBtn.click(); - - const couponCodeField = page.locator('//input[@id="wc-block-components-totals-coupon__input-coupon"]'); - await couponCodeField.click(); - - await couponCodeField.fill(couponCode); + test('The coupons should apply proper discount', async({admin, page})=>{ - const couponApplyBtn = page.locator('//span[contains(text(),"Apply")]'); - await couponApplyBtn.click(); + await applyCouponDiscount(page, productTitle, couponCode); const discountValue = page.locator('//div[contains(@class,"wc-block-components-totals-discount")]//span[contains(@class,"wc-block-components-totals-item__value")]'); diff --git a/utils/e2eUtils/applyCouponUtils.js b/utils/e2eUtils/applyCouponUtils.js new file mode 100644 index 0000000..08e0348 --- /dev/null +++ b/utils/e2eUtils/applyCouponUtils.js @@ -0,0 +1,32 @@ + +const { extractPrdtSlug } = require("./getProductSlug"); + +const applyCouponDiscount = async (page, productTitle, couponCode) => { + const prdtSlug = extractPrdtSlug(productTitle); + + await page.goto(`${process.env.WP_BASE_URL}product/${prdtSlug}`); + + const addToCartBtn = page.locator('//button[contains(text(),"Add to cart")]'); + await addToCartBtn.click(); + + await page.goto(`${process.env.WP_BASE_URL}checkout`); + + const couponFieldBtn = page.locator( + '//div[contains(@class,"wc-block-components-totals-coupon")]//div[@role="button"]' + ); + await couponFieldBtn.click(); + + const couponCodeField = page.locator( + '//input[@id="wc-block-components-totals-coupon__input-coupon"]' + ); + await couponCodeField.click(); + + await couponCodeField.fill(couponCode); + + const couponApplyBtn = page.locator('//span[contains(text(),"Apply")]'); + await couponApplyBtn.click(); +}; + +module.exports = { + applyCouponDiscount +} From ec3dfab18b51b836711db2c507ca2142eae9d7a0 Mon Sep 17 00:00:00 2001 From: Rishav Dutta Date: Mon, 6 Jan 2025 21:47:18 +0530 Subject: [PATCH 11/27] add: add review orders test scripts --- artifacts/storage-states/admin.json | 2 +- specs/reviewOrder.spec.js | 120 ++++++++++++++++++++++++++++ 2 files changed, 121 insertions(+), 1 deletion(-) create mode 100644 specs/reviewOrder.spec.js diff --git a/artifacts/storage-states/admin.json b/artifacts/storage-states/admin.json index ae770a6..2dc1360 100644 --- a/artifacts/storage-states/admin.json +++ b/artifacts/storage-states/admin.json @@ -1 +1 @@ -{"cookies":[{"name":"wordpress_test_cookie","value":"WP%20Cookie%20check","domain":"rishav.rt.gw","path":"/","expires":-1,"httpOnly":false,"secure":true,"sameSite":"Lax"},{"name":"wordpress_sec_074e74b03d8a277981a80ce1db027a5e","value":"rishav%7C1736335513%7ClheY9bUFj5BfQvQAL7kQGI2OBynZ4fNwXSOM8QntXuY%7C2d98a5cdbab81eb8712eb1bc3bac33bd1edea8b7be4e9b850c1c5a2b6f0361d1","domain":"rishav.rt.gw","path":"/wp-content/plugins","expires":-1,"httpOnly":true,"secure":true,"sameSite":"Lax"},{"name":"wordpress_sec_074e74b03d8a277981a80ce1db027a5e","value":"rishav%7C1736335513%7ClheY9bUFj5BfQvQAL7kQGI2OBynZ4fNwXSOM8QntXuY%7C2d98a5cdbab81eb8712eb1bc3bac33bd1edea8b7be4e9b850c1c5a2b6f0361d1","domain":"rishav.rt.gw","path":"/wp-admin","expires":-1,"httpOnly":true,"secure":true,"sameSite":"Lax"},{"name":"wordpress_logged_in_074e74b03d8a277981a80ce1db027a5e","value":"rishav%7C1736335513%7ClheY9bUFj5BfQvQAL7kQGI2OBynZ4fNwXSOM8QntXuY%7C5b67d934df4c6c5fb38bb0646fe6bf4b2d0c0a770e944d4d31da48c9631f4743","domain":"rishav.rt.gw","path":"/","expires":-1,"httpOnly":true,"secure":true,"sameSite":"Lax"},{"name":"tk_ai","value":"woo%3Att9OmINARUla7NfCYnKU5fJW","domain":"rishav.rt.gw","path":"/","expires":-1,"httpOnly":false,"secure":false,"sameSite":"Lax"},{"name":"wp-settings-time-2","value":"1736162714","domain":"rishav.rt.gw","path":"/","expires":1767698714.828,"httpOnly":false,"secure":true,"sameSite":"Lax"}],"nonce":"ec0aa9668d","rootURL":"https://rishav.rt.gw/wp-json/"} \ No newline at end of file +{"cookies":[{"name":"wordpress_test_cookie","value":"WP%20Cookie%20check","domain":"rishav.rt.gw","path":"/","expires":-1,"httpOnly":false,"secure":true,"sameSite":"Lax"},{"name":"wordpress_sec_074e74b03d8a277981a80ce1db027a5e","value":"rishav%7C1736352947%7CsXKfThb9uCikYSqt1GuoqEuH2YDcH1CNqZNWNYzVFmq%7C93413219fa625c23ee4f178ae42a488eaa14c271cfc57f64572fc8a6efdfe7b9","domain":"rishav.rt.gw","path":"/wp-content/plugins","expires":-1,"httpOnly":true,"secure":true,"sameSite":"Lax"},{"name":"wordpress_sec_074e74b03d8a277981a80ce1db027a5e","value":"rishav%7C1736352947%7CsXKfThb9uCikYSqt1GuoqEuH2YDcH1CNqZNWNYzVFmq%7C93413219fa625c23ee4f178ae42a488eaa14c271cfc57f64572fc8a6efdfe7b9","domain":"rishav.rt.gw","path":"/wp-admin","expires":-1,"httpOnly":true,"secure":true,"sameSite":"Lax"},{"name":"wordpress_logged_in_074e74b03d8a277981a80ce1db027a5e","value":"rishav%7C1736352947%7CsXKfThb9uCikYSqt1GuoqEuH2YDcH1CNqZNWNYzVFmq%7C6c9412ac1c17c55a0041da3f10459c7e5f91a3f2dfa2abd242fde4248f441d9f","domain":"rishav.rt.gw","path":"/","expires":-1,"httpOnly":true,"secure":true,"sameSite":"Lax"},{"name":"tk_ai","value":"woo%3Att9OmINARUla7NfCYnKU5fJW","domain":"rishav.rt.gw","path":"/","expires":-1,"httpOnly":false,"secure":false,"sameSite":"Lax"},{"name":"wp-settings-time-2","value":"1736180147","domain":"rishav.rt.gw","path":"/","expires":1767716148.089,"httpOnly":false,"secure":true,"sameSite":"Lax"}],"nonce":"0c3d232207","rootURL":"https://rishav.rt.gw/wp-json/"} \ No newline at end of file diff --git a/specs/reviewOrder.spec.js b/specs/reviewOrder.spec.js new file mode 100644 index 0000000..8912973 --- /dev/null +++ b/specs/reviewOrder.spec.js @@ -0,0 +1,120 @@ +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'); + +require("dotenv").config(); + +test.describe('Test should verify the chekcout workflow', ()=>{ + + let testCode = 0; + let productTitle = ''; + let productDescription = ''; + + let userName = ''; + let userEmail = ''; + let firstName = ''; + let lastName = ''; + let password = ''; + + test.beforeEach( async ({admin, page})=>{ + + testCode = generateTestCode(); + + productTitle = `Product Demo Title ${testCode}`; + productDescription = `Demo Product Description ${testCode}`; + + await addNewProduct(admin, page, productTitle, productDescription); + + await addPricingInventory(admin, page); + + 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); + + 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('It should be add product to cart, checkout and place order',async({admin, page})=>{ + + await customerUserLogin(page, userName, password); + + await checkoutPlaceOrder(page, firstName, lastName, productTitle); + + const orderNumberField = page.locator('//span[contains(text(),"Order #:")]/following-sibling::*[1]'); + const orderNumber = await orderNumberField.textContent(); + + console.log(orderNumber); + + await page.goto(`${process.env.WP_BASE_URL}my-account`); + + const logoutLink = page.locator('//li//a[contains(text(),"Log out")]'); + await logoutLink.click(); + + await page.goto(`${process.env.WP_BASE_URL}login.php`); + + const userNameField = page.locator('//input[@id="user_login"]'); + await userNameField.fill(`${process.env.WP_USERNAME}`); + + const passwordField = page.locator('//input[@id="user_pass"]'); + await passwordField.fill(`${process.env.WP_PASSWORD}`); + + const loginBtn = page.locator(); + await loginBtn.click(); + + 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 searchBtn .click(); + + const orderRecordData = page.locator('//td[contains(@class,"order_number")]//a[@class="order-view"]//strong'); + + await expect(orderRecordData).toContainText(orderNumber); + + + + }); + + test.afterEach(async ({admin, page, requestUtils})=>{ + + // await page.goto(`${process.env.WP_BASE_URL}my-account`); + + // const logoutLink = page.locator('//li//a[contains(text(),"Log out")]'); + // await logoutLink.click(); + + // await requestUtils.login({ + // username: process.env.WP_USERNAME, + // password: process.env.WP_PASSWORD + // }); + + // await removeTestProductRecord(admin, page, productTitle); + + // await removeTestUserRecord(admin, page, userName); + + }) +}) From 3bab81ffab2d937fb8d124a55e7347d51fa95293 Mon Sep 17 00:00:00 2001 From: Rishav Dutta Date: Tue, 7 Jan 2025 12:18:03 +0530 Subject: [PATCH 12/27] fix: fix the review orders test script --- artifacts/storage-states/admin.json | 2 +- specs/reviewOrder.spec.js | 12 +++++++----- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/artifacts/storage-states/admin.json b/artifacts/storage-states/admin.json index 2dc1360..f70e79b 100644 --- a/artifacts/storage-states/admin.json +++ b/artifacts/storage-states/admin.json @@ -1 +1 @@ -{"cookies":[{"name":"wordpress_test_cookie","value":"WP%20Cookie%20check","domain":"rishav.rt.gw","path":"/","expires":-1,"httpOnly":false,"secure":true,"sameSite":"Lax"},{"name":"wordpress_sec_074e74b03d8a277981a80ce1db027a5e","value":"rishav%7C1736352947%7CsXKfThb9uCikYSqt1GuoqEuH2YDcH1CNqZNWNYzVFmq%7C93413219fa625c23ee4f178ae42a488eaa14c271cfc57f64572fc8a6efdfe7b9","domain":"rishav.rt.gw","path":"/wp-content/plugins","expires":-1,"httpOnly":true,"secure":true,"sameSite":"Lax"},{"name":"wordpress_sec_074e74b03d8a277981a80ce1db027a5e","value":"rishav%7C1736352947%7CsXKfThb9uCikYSqt1GuoqEuH2YDcH1CNqZNWNYzVFmq%7C93413219fa625c23ee4f178ae42a488eaa14c271cfc57f64572fc8a6efdfe7b9","domain":"rishav.rt.gw","path":"/wp-admin","expires":-1,"httpOnly":true,"secure":true,"sameSite":"Lax"},{"name":"wordpress_logged_in_074e74b03d8a277981a80ce1db027a5e","value":"rishav%7C1736352947%7CsXKfThb9uCikYSqt1GuoqEuH2YDcH1CNqZNWNYzVFmq%7C6c9412ac1c17c55a0041da3f10459c7e5f91a3f2dfa2abd242fde4248f441d9f","domain":"rishav.rt.gw","path":"/","expires":-1,"httpOnly":true,"secure":true,"sameSite":"Lax"},{"name":"tk_ai","value":"woo%3Att9OmINARUla7NfCYnKU5fJW","domain":"rishav.rt.gw","path":"/","expires":-1,"httpOnly":false,"secure":false,"sameSite":"Lax"},{"name":"wp-settings-time-2","value":"1736180147","domain":"rishav.rt.gw","path":"/","expires":1767716148.089,"httpOnly":false,"secure":true,"sameSite":"Lax"}],"nonce":"0c3d232207","rootURL":"https://rishav.rt.gw/wp-json/"} \ No newline at end of file +{"cookies":[{"name":"wordpress_test_cookie","value":"WP%20Cookie%20check","domain":"rishav.rt.gw","path":"/","expires":-1,"httpOnly":false,"secure":true,"sameSite":"Lax"},{"name":"wordpress_sec_074e74b03d8a277981a80ce1db027a5e","value":"rishav%7C1736405198%7CI9iqFyOqnmyIO8JhunowpgMR6jCftnaAEl0vTqfPkRA%7C6b7f68b3a569ee8b0f4a16dc086da12a1c227edd97997223260a5439a3cbe76d","domain":"rishav.rt.gw","path":"/wp-content/plugins","expires":-1,"httpOnly":true,"secure":true,"sameSite":"Lax"},{"name":"wordpress_sec_074e74b03d8a277981a80ce1db027a5e","value":"rishav%7C1736405198%7CI9iqFyOqnmyIO8JhunowpgMR6jCftnaAEl0vTqfPkRA%7C6b7f68b3a569ee8b0f4a16dc086da12a1c227edd97997223260a5439a3cbe76d","domain":"rishav.rt.gw","path":"/wp-admin","expires":-1,"httpOnly":true,"secure":true,"sameSite":"Lax"},{"name":"wordpress_logged_in_074e74b03d8a277981a80ce1db027a5e","value":"rishav%7C1736405198%7CI9iqFyOqnmyIO8JhunowpgMR6jCftnaAEl0vTqfPkRA%7C90927546630cdc16822f4f6cbc807e3fa139e0c11eb3ef07e5c18b2d5c173bf6","domain":"rishav.rt.gw","path":"/","expires":-1,"httpOnly":true,"secure":true,"sameSite":"Lax"},{"name":"tk_ai","value":"woo%3Att9OmINARUla7NfCYnKU5fJW","domain":"rishav.rt.gw","path":"/","expires":-1,"httpOnly":false,"secure":false,"sameSite":"Lax"},{"name":"wp-settings-time-2","value":"1736232399","domain":"rishav.rt.gw","path":"/","expires":1767768400.035,"httpOnly":false,"secure":true,"sameSite":"Lax"}],"nonce":"25133410df","rootURL":"https://rishav.rt.gw/wp-json/"} \ No newline at end of file diff --git a/specs/reviewOrder.spec.js b/specs/reviewOrder.spec.js index 8912973..79f8739 100644 --- a/specs/reviewOrder.spec.js +++ b/specs/reviewOrder.spec.js @@ -66,14 +66,12 @@ test.describe('Test should verify the chekcout workflow', ()=>{ const orderNumberField = page.locator('//span[contains(text(),"Order #:")]/following-sibling::*[1]'); const orderNumber = await orderNumberField.textContent(); - console.log(orderNumber); - await page.goto(`${process.env.WP_BASE_URL}my-account`); const logoutLink = page.locator('//li//a[contains(text(),"Log out")]'); await logoutLink.click(); - await page.goto(`${process.env.WP_BASE_URL}login.php`); + await page.goto(`${process.env.WP_BASE_URL}wp-login.php`); const userNameField = page.locator('//input[@id="user_login"]'); await userNameField.fill(`${process.env.WP_USERNAME}`); @@ -81,7 +79,7 @@ test.describe('Test should verify the chekcout workflow', ()=>{ const passwordField = page.locator('//input[@id="user_pass"]'); await passwordField.fill(`${process.env.WP_PASSWORD}`); - const loginBtn = page.locator(); + const loginBtn = page.locator('//input[@id="wp-submit"]'); await loginBtn.click(); await page.goto(`${process.env.WP_BASE_URL}wp-admin/admin.php?page=wc-orders`); @@ -90,7 +88,11 @@ test.describe('Test should verify the chekcout workflow', ()=>{ await searchCouponField.fill(orderNumber); const searchBtn = page.locator('//input[@id="search-submit"]'); - await searchBtn .click(); + + await page.keyboard.press('Enter'); + // await page.pause(); + + // await searchBtn.click(); const orderRecordData = page.locator('//td[contains(@class,"order_number")]//a[@class="order-view"]//strong'); From 957a03767a47307ef0ef00a6cf3e4ed52cda3854 Mon Sep 17 00:00:00 2001 From: Rishav Dutta Date: Tue, 7 Jan 2025 13:33:34 +0530 Subject: [PATCH 13/27] refactor: use in-built createUser utils function --- artifacts/storage-states/admin.json | 2 +- specs/checkoutPlaceOrder.spec.js | 22 ++++++++++++++++++++-- specs/reviewOrder.spec.js | 16 +++++++++++----- 3 files changed, 32 insertions(+), 8 deletions(-) diff --git a/artifacts/storage-states/admin.json b/artifacts/storage-states/admin.json index f70e79b..9bedc75 100644 --- a/artifacts/storage-states/admin.json +++ b/artifacts/storage-states/admin.json @@ -1 +1 @@ -{"cookies":[{"name":"wordpress_test_cookie","value":"WP%20Cookie%20check","domain":"rishav.rt.gw","path":"/","expires":-1,"httpOnly":false,"secure":true,"sameSite":"Lax"},{"name":"wordpress_sec_074e74b03d8a277981a80ce1db027a5e","value":"rishav%7C1736405198%7CI9iqFyOqnmyIO8JhunowpgMR6jCftnaAEl0vTqfPkRA%7C6b7f68b3a569ee8b0f4a16dc086da12a1c227edd97997223260a5439a3cbe76d","domain":"rishav.rt.gw","path":"/wp-content/plugins","expires":-1,"httpOnly":true,"secure":true,"sameSite":"Lax"},{"name":"wordpress_sec_074e74b03d8a277981a80ce1db027a5e","value":"rishav%7C1736405198%7CI9iqFyOqnmyIO8JhunowpgMR6jCftnaAEl0vTqfPkRA%7C6b7f68b3a569ee8b0f4a16dc086da12a1c227edd97997223260a5439a3cbe76d","domain":"rishav.rt.gw","path":"/wp-admin","expires":-1,"httpOnly":true,"secure":true,"sameSite":"Lax"},{"name":"wordpress_logged_in_074e74b03d8a277981a80ce1db027a5e","value":"rishav%7C1736405198%7CI9iqFyOqnmyIO8JhunowpgMR6jCftnaAEl0vTqfPkRA%7C90927546630cdc16822f4f6cbc807e3fa139e0c11eb3ef07e5c18b2d5c173bf6","domain":"rishav.rt.gw","path":"/","expires":-1,"httpOnly":true,"secure":true,"sameSite":"Lax"},{"name":"tk_ai","value":"woo%3Att9OmINARUla7NfCYnKU5fJW","domain":"rishav.rt.gw","path":"/","expires":-1,"httpOnly":false,"secure":false,"sameSite":"Lax"},{"name":"wp-settings-time-2","value":"1736232399","domain":"rishav.rt.gw","path":"/","expires":1767768400.035,"httpOnly":false,"secure":true,"sameSite":"Lax"}],"nonce":"25133410df","rootURL":"https://rishav.rt.gw/wp-json/"} \ No newline at end of file +{"cookies":[{"name":"wordpress_test_cookie","value":"WP%20Cookie%20check","domain":"rishav.rt.gw","path":"/","expires":-1,"httpOnly":false,"secure":true,"sameSite":"Lax"},{"name":"wordpress_sec_074e74b03d8a277981a80ce1db027a5e","value":"rishav%7C1736409626%7CUzCqzwfgtDURHYfnSBclUYZjxUo6sjnjPQvy5Cdkssa%7Cdd41066ac5d3b728639c2cb6470e21664ea557cf8c4040f62a3ca71f81e398a3","domain":"rishav.rt.gw","path":"/wp-content/plugins","expires":-1,"httpOnly":true,"secure":true,"sameSite":"Lax"},{"name":"wordpress_sec_074e74b03d8a277981a80ce1db027a5e","value":"rishav%7C1736409626%7CUzCqzwfgtDURHYfnSBclUYZjxUo6sjnjPQvy5Cdkssa%7Cdd41066ac5d3b728639c2cb6470e21664ea557cf8c4040f62a3ca71f81e398a3","domain":"rishav.rt.gw","path":"/wp-admin","expires":-1,"httpOnly":true,"secure":true,"sameSite":"Lax"},{"name":"wordpress_logged_in_074e74b03d8a277981a80ce1db027a5e","value":"rishav%7C1736409626%7CUzCqzwfgtDURHYfnSBclUYZjxUo6sjnjPQvy5Cdkssa%7C40565ee382133104cd97af69876a30b0b35479ad14502fe866d9bf51c0b45d5c","domain":"rishav.rt.gw","path":"/","expires":-1,"httpOnly":true,"secure":true,"sameSite":"Lax"},{"name":"tk_ai","value":"woo%3Att9OmINARUla7NfCYnKU5fJW","domain":"rishav.rt.gw","path":"/","expires":-1,"httpOnly":false,"secure":false,"sameSite":"Lax"},{"name":"wp-settings-time-2","value":"1736236827","domain":"rishav.rt.gw","path":"/","expires":1767772827.474,"httpOnly":false,"secure":true,"sameSite":"Lax"}],"nonce":"64e32e5876","rootURL":"https://rishav.rt.gw/wp-json/"} \ No newline at end of file diff --git a/specs/checkoutPlaceOrder.spec.js b/specs/checkoutPlaceOrder.spec.js index c708db4..8af7a23 100644 --- a/specs/checkoutPlaceOrder.spec.js +++ b/specs/checkoutPlaceOrder.spec.js @@ -15,6 +15,7 @@ const { addCustomerUser } = require('../utils/e2eUtils/createCustomerUtils'); const {customerUserLogin} = require('../utils/e2eUtils/customerLoginUtils'); const {checkoutPlaceOrder} = require('../utils/e2eUtils/checkoutPlaceOrderUtils'); +const { consumers } = require("stream"); require("dotenv").config(); @@ -30,7 +31,9 @@ test.describe('Test should verify the chekcout workflow', ()=>{ let lastName = ''; let password = ''; - test.beforeEach( async ({admin, page})=>{ + let customerUserId = 0; + + test.beforeEach( async ({admin, page, requestUtils})=>{ testCode = generateTestCode(); @@ -47,7 +50,20 @@ test.describe('Test should verify the chekcout workflow', ()=>{ lastName = `TestLastName-${testCode}`; password = `TestPassword${testCode}*`; - await addCustomerUser(admin, page, userName, userEmail, firstName, lastName, password); + // await addCustomerUser(admin, page, userName, userEmail, firstName, lastName, password); + + const customerUserData = await requestUtils.createUser({ + username: userName, + email: userEmail, + first_name: firstName, + last_name: lastName, + password, + roles: ['customer'] + }); + + customerUserId = customerUserData.id + + console.log(customerUserId); await page.goto(`${process.env.WP_BASE_URL}wp-login.php?action=logout`); @@ -74,6 +90,8 @@ test.describe('Test should verify the chekcout workflow', ()=>{ const logoutLink = page.locator('//li//a[contains(text(),"Log out")]'); await logoutLink.click(); + // await requestUtils.deleteUser( customerUserId ); + // await requestUtils.login({ // username: process.env.WP_USERNAME, // password: process.env.WP_PASSWORD diff --git a/specs/reviewOrder.spec.js b/specs/reviewOrder.spec.js index 79f8739..0d46da2 100644 --- a/specs/reviewOrder.spec.js +++ b/specs/reviewOrder.spec.js @@ -30,7 +30,7 @@ test.describe('Test should verify the chekcout workflow', ()=>{ let lastName = ''; let password = ''; - test.beforeEach( async ({admin, page})=>{ + test.beforeEach( async ({admin, page, requestUtils})=>{ testCode = generateTestCode(); @@ -47,7 +47,16 @@ test.describe('Test should verify the chekcout workflow', ()=>{ lastName = `TestLastName-${testCode}`; password = `TestPassword${testCode}*`; - await addCustomerUser(admin, page, userName, userEmail, firstName, lastName, password); + // await addCustomerUser(admin, page, userName, userEmail, firstName, lastName, password); + + await requestUtils.createUser({ + username: userName, + email: userEmail, + first_name: firstName, + last_name: lastName, + password, + roles: ['customer'] + }); await page.goto(`${process.env.WP_BASE_URL}wp-login.php?action=logout`); @@ -90,9 +99,6 @@ test.describe('Test should verify the chekcout workflow', ()=>{ const searchBtn = page.locator('//input[@id="search-submit"]'); await page.keyboard.press('Enter'); - // await page.pause(); - - // await searchBtn.click(); const orderRecordData = page.locator('//td[contains(@class,"order_number")]//a[@class="order-view"]//strong'); From 7c5898c09e7f415281f89cfa8502841441921084 Mon Sep 17 00:00:00 2001 From: Rishav Dutta Date: Tue, 7 Jan 2025 15:14:38 +0530 Subject: [PATCH 14/27] add: add adminLogin utils function --- artifacts/storage-states/admin.json | 2 +- specs/checkoutPlaceOrder.spec.js | 14 +++++++------- specs/reviewOrder.spec.js | 14 ++------------ utils/e2eUtils/adminLoginUtils.js | 19 +++++++++++++++++++ 4 files changed, 29 insertions(+), 20 deletions(-) create mode 100644 utils/e2eUtils/adminLoginUtils.js diff --git a/artifacts/storage-states/admin.json b/artifacts/storage-states/admin.json index 9bedc75..e1ac76d 100644 --- a/artifacts/storage-states/admin.json +++ b/artifacts/storage-states/admin.json @@ -1 +1 @@ -{"cookies":[{"name":"wordpress_test_cookie","value":"WP%20Cookie%20check","domain":"rishav.rt.gw","path":"/","expires":-1,"httpOnly":false,"secure":true,"sameSite":"Lax"},{"name":"wordpress_sec_074e74b03d8a277981a80ce1db027a5e","value":"rishav%7C1736409626%7CUzCqzwfgtDURHYfnSBclUYZjxUo6sjnjPQvy5Cdkssa%7Cdd41066ac5d3b728639c2cb6470e21664ea557cf8c4040f62a3ca71f81e398a3","domain":"rishav.rt.gw","path":"/wp-content/plugins","expires":-1,"httpOnly":true,"secure":true,"sameSite":"Lax"},{"name":"wordpress_sec_074e74b03d8a277981a80ce1db027a5e","value":"rishav%7C1736409626%7CUzCqzwfgtDURHYfnSBclUYZjxUo6sjnjPQvy5Cdkssa%7Cdd41066ac5d3b728639c2cb6470e21664ea557cf8c4040f62a3ca71f81e398a3","domain":"rishav.rt.gw","path":"/wp-admin","expires":-1,"httpOnly":true,"secure":true,"sameSite":"Lax"},{"name":"wordpress_logged_in_074e74b03d8a277981a80ce1db027a5e","value":"rishav%7C1736409626%7CUzCqzwfgtDURHYfnSBclUYZjxUo6sjnjPQvy5Cdkssa%7C40565ee382133104cd97af69876a30b0b35479ad14502fe866d9bf51c0b45d5c","domain":"rishav.rt.gw","path":"/","expires":-1,"httpOnly":true,"secure":true,"sameSite":"Lax"},{"name":"tk_ai","value":"woo%3Att9OmINARUla7NfCYnKU5fJW","domain":"rishav.rt.gw","path":"/","expires":-1,"httpOnly":false,"secure":false,"sameSite":"Lax"},{"name":"wp-settings-time-2","value":"1736236827","domain":"rishav.rt.gw","path":"/","expires":1767772827.474,"httpOnly":false,"secure":true,"sameSite":"Lax"}],"nonce":"64e32e5876","rootURL":"https://rishav.rt.gw/wp-json/"} \ No newline at end of file +{"cookies":[{"name":"wordpress_test_cookie","value":"WP%20Cookie%20check","domain":"rishav.rt.gw","path":"/","expires":-1,"httpOnly":false,"secure":true,"sameSite":"Lax"},{"name":"wordpress_sec_074e74b03d8a277981a80ce1db027a5e","value":"wp-user-4w8k6z%7C1736415807%7ChcTU2ppcSyEEzyvZmiYVZj1tGkAN7Zu1Qdo1JJm0G1k%7C881a248ebbc105104a70b988d5d4835b4aaa54fec7775029f02492ffad4479d6","domain":"rishav.rt.gw","path":"/wp-content/plugins","expires":-1,"httpOnly":true,"secure":true,"sameSite":"Lax"},{"name":"wordpress_sec_074e74b03d8a277981a80ce1db027a5e","value":"wp-user-4w8k6z%7C1736415807%7ChcTU2ppcSyEEzyvZmiYVZj1tGkAN7Zu1Qdo1JJm0G1k%7C881a248ebbc105104a70b988d5d4835b4aaa54fec7775029f02492ffad4479d6","domain":"rishav.rt.gw","path":"/wp-admin","expires":-1,"httpOnly":true,"secure":true,"sameSite":"Lax"},{"name":"wordpress_logged_in_074e74b03d8a277981a80ce1db027a5e","value":"wp-user-4w8k6z%7C1736415807%7ChcTU2ppcSyEEzyvZmiYVZj1tGkAN7Zu1Qdo1JJm0G1k%7C2947e4188d12bbd0f77ae9f3e6cbb1c2b726ceed20be709c0d56256192c83d24","domain":"rishav.rt.gw","path":"/","expires":-1,"httpOnly":true,"secure":true,"sameSite":"Lax"},{"name":"tk_ai","value":"woo%3Ar7yWZheRH%2BgrBhJayxfsytUU","domain":"rishav.rt.gw","path":"/","expires":-1,"httpOnly":false,"secure":false,"sameSite":"Lax"},{"name":"wp-settings-1","value":"libraryContent%3Dbrowse%26editor%3Dtinymce","domain":"rishav.rt.gw","path":"/","expires":1767778989.841,"httpOnly":false,"secure":true,"sameSite":"Lax"},{"name":"wp-settings-time-1","value":"1736242989","domain":"rishav.rt.gw","path":"/","expires":1767778989.841,"httpOnly":false,"secure":true,"sameSite":"Lax"}],"nonce":"98c5a695df","rootURL":"https://rishav.rt.gw/wp-json/"} \ No newline at end of file diff --git a/specs/checkoutPlaceOrder.spec.js b/specs/checkoutPlaceOrder.spec.js index 8af7a23..6090d00 100644 --- a/specs/checkoutPlaceOrder.spec.js +++ b/specs/checkoutPlaceOrder.spec.js @@ -15,6 +15,9 @@ 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(); @@ -85,19 +88,16 @@ test.describe('Test should verify the chekcout workflow', ()=>{ test.afterEach(async ({admin, page, requestUtils})=>{ - await page.goto(`${process.env.WP_BASE_URL}my-account`); - - const logoutLink = page.locator('//li//a[contains(text(),"Log out")]'); - await logoutLink.click(); - - // await requestUtils.deleteUser( customerUserId ); + await requestUtils.deleteAllUsers(); // await requestUtils.login({ // username: process.env.WP_USERNAME, // password: process.env.WP_PASSWORD // }); - // await removeTestProductRecord(admin, page, productTitle); + await adminLogin(page); + + await removeTestProductRecord(admin, page, productTitle); // await removeTestUserRecord(admin, page, userName); diff --git a/specs/reviewOrder.spec.js b/specs/reviewOrder.spec.js index 0d46da2..5ba237b 100644 --- a/specs/reviewOrder.spec.js +++ b/specs/reviewOrder.spec.js @@ -110,19 +110,9 @@ test.describe('Test should verify the chekcout workflow', ()=>{ test.afterEach(async ({admin, page, requestUtils})=>{ - // await page.goto(`${process.env.WP_BASE_URL}my-account`); + await requestUtils.deleteAllUsers(); - // const logoutLink = page.locator('//li//a[contains(text(),"Log out")]'); - // await logoutLink.click(); - - // await requestUtils.login({ - // username: process.env.WP_USERNAME, - // password: process.env.WP_PASSWORD - // }); - - // await removeTestProductRecord(admin, page, productTitle); - - // await removeTestUserRecord(admin, page, userName); + await removeTestProductRecord(admin, page, productTitle); }) }) diff --git a/utils/e2eUtils/adminLoginUtils.js b/utils/e2eUtils/adminLoginUtils.js new file mode 100644 index 0000000..2e8026a --- /dev/null +++ b/utils/e2eUtils/adminLoginUtils.js @@ -0,0 +1,19 @@ + +require("dotenv").config(); + +const adminLogin = async (page) => { + await page.goto(`${process.env.WP_BASE_URL}wp-login.php`); + + const userNameField = page.locator('//input[@id="user_login"]'); + await userNameField.fill(`${process.env.WP_USERNAME}`); + + const passwordField = page.locator('//input[@id="user_pass"]'); + await passwordField.fill(`${process.env.WP_PASSWORD}`); + + const loginBtn = page.locator('//input[@id="wp-submit"]'); + await loginBtn.click(); +}; + +module.exports = { + adminLogin +} From cb9668ff0596d5b7150f9f539a2e39cc58c1cc01 Mon Sep 17 00:00:00 2001 From: Rishav Dutta Date: Tue, 7 Jan 2025 15:53:27 +0530 Subject: [PATCH 15/27] add: add JS docs for test scripts --- artifacts/storage-states/admin.json | 2 +- specs/addCoupon.spec.js | 38 +++++++++++++- specs/addPricingInventory.spec.js | 41 +++++++++++++++ specs/addUserCustomer.spec.js | 38 ++++++++++++++ specs/checkoutPlaceOrder.spec.js | 69 ++++++++++++++++++++++++- specs/checkoutVerifyCoupon.spec.js | 64 +++++++++++++++++++++-- specs/createSimpleProduct.spec.js | 53 ++++++++++++++++++- specs/publishProduct.spec.js | 51 ++++++++++++++++++ specs/reviewOrder.spec.js | 80 ++++++++++++++++++++++++----- 9 files changed, 415 insertions(+), 21 deletions(-) diff --git a/artifacts/storage-states/admin.json b/artifacts/storage-states/admin.json index e1ac76d..e665b7e 100644 --- a/artifacts/storage-states/admin.json +++ b/artifacts/storage-states/admin.json @@ -1 +1 @@ -{"cookies":[{"name":"wordpress_test_cookie","value":"WP%20Cookie%20check","domain":"rishav.rt.gw","path":"/","expires":-1,"httpOnly":false,"secure":true,"sameSite":"Lax"},{"name":"wordpress_sec_074e74b03d8a277981a80ce1db027a5e","value":"wp-user-4w8k6z%7C1736415807%7ChcTU2ppcSyEEzyvZmiYVZj1tGkAN7Zu1Qdo1JJm0G1k%7C881a248ebbc105104a70b988d5d4835b4aaa54fec7775029f02492ffad4479d6","domain":"rishav.rt.gw","path":"/wp-content/plugins","expires":-1,"httpOnly":true,"secure":true,"sameSite":"Lax"},{"name":"wordpress_sec_074e74b03d8a277981a80ce1db027a5e","value":"wp-user-4w8k6z%7C1736415807%7ChcTU2ppcSyEEzyvZmiYVZj1tGkAN7Zu1Qdo1JJm0G1k%7C881a248ebbc105104a70b988d5d4835b4aaa54fec7775029f02492ffad4479d6","domain":"rishav.rt.gw","path":"/wp-admin","expires":-1,"httpOnly":true,"secure":true,"sameSite":"Lax"},{"name":"wordpress_logged_in_074e74b03d8a277981a80ce1db027a5e","value":"wp-user-4w8k6z%7C1736415807%7ChcTU2ppcSyEEzyvZmiYVZj1tGkAN7Zu1Qdo1JJm0G1k%7C2947e4188d12bbd0f77ae9f3e6cbb1c2b726ceed20be709c0d56256192c83d24","domain":"rishav.rt.gw","path":"/","expires":-1,"httpOnly":true,"secure":true,"sameSite":"Lax"},{"name":"tk_ai","value":"woo%3Ar7yWZheRH%2BgrBhJayxfsytUU","domain":"rishav.rt.gw","path":"/","expires":-1,"httpOnly":false,"secure":false,"sameSite":"Lax"},{"name":"wp-settings-1","value":"libraryContent%3Dbrowse%26editor%3Dtinymce","domain":"rishav.rt.gw","path":"/","expires":1767778989.841,"httpOnly":false,"secure":true,"sameSite":"Lax"},{"name":"wp-settings-time-1","value":"1736242989","domain":"rishav.rt.gw","path":"/","expires":1767778989.841,"httpOnly":false,"secure":true,"sameSite":"Lax"}],"nonce":"98c5a695df","rootURL":"https://rishav.rt.gw/wp-json/"} \ No newline at end of file +{"cookies":[{"name":"wordpress_test_cookie","value":"WP%20Cookie%20check","domain":"rishav.rt.gw","path":"/","expires":-1,"httpOnly":false,"secure":true,"sameSite":"Lax"},{"name":"wordpress_sec_074e74b03d8a277981a80ce1db027a5e","value":"wp-user-4w8k6z%7C1736416979%7CIbiaP7McEqZB0EGwubzLhlC5qt2JStUJNbGr2lgjsvW%7Cb57f016d79b1b9d7a365133d12734a9c46cf28c3f95f8b91418c983aabfc6597","domain":"rishav.rt.gw","path":"/wp-content/plugins","expires":-1,"httpOnly":true,"secure":true,"sameSite":"Lax"},{"name":"wordpress_sec_074e74b03d8a277981a80ce1db027a5e","value":"wp-user-4w8k6z%7C1736416979%7CIbiaP7McEqZB0EGwubzLhlC5qt2JStUJNbGr2lgjsvW%7Cb57f016d79b1b9d7a365133d12734a9c46cf28c3f95f8b91418c983aabfc6597","domain":"rishav.rt.gw","path":"/wp-admin","expires":-1,"httpOnly":true,"secure":true,"sameSite":"Lax"},{"name":"wordpress_logged_in_074e74b03d8a277981a80ce1db027a5e","value":"wp-user-4w8k6z%7C1736416979%7CIbiaP7McEqZB0EGwubzLhlC5qt2JStUJNbGr2lgjsvW%7C9431c529a33a4059518e18baa4b6ad101fb8614199151a3db4176d294d776d9a","domain":"rishav.rt.gw","path":"/","expires":-1,"httpOnly":true,"secure":true,"sameSite":"Lax"},{"name":"tk_ai","value":"woo%3Ar7yWZheRH%2BgrBhJayxfsytUU","domain":"rishav.rt.gw","path":"/","expires":-1,"httpOnly":false,"secure":false,"sameSite":"Lax"},{"name":"wp-settings-1","value":"libraryContent%3Dbrowse%26editor%3Dtinymce","domain":"rishav.rt.gw","path":"/","expires":1767780159.363,"httpOnly":false,"secure":true,"sameSite":"Lax"},{"name":"wp-settings-time-1","value":"1736244159","domain":"rishav.rt.gw","path":"/","expires":1767780159.363,"httpOnly":false,"secure":true,"sameSite":"Lax"}],"nonce":"c23ecf7384","rootURL":"https://rishav.rt.gw/wp-json/"} \ No newline at end of file diff --git a/specs/addCoupon.spec.js b/specs/addCoupon.spec.js index 19dc77f..016bd87 100644 --- a/specs/addCoupon.spec.js +++ b/specs/addCoupon.spec.js @@ -1,3 +1,10 @@ +/** + * @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'); @@ -6,14 +13,29 @@ 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(); @@ -21,13 +43,27 @@ test.describe('It should test the coupon features', ()=>{ 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); diff --git a/specs/addPricingInventory.spec.js b/specs/addPricingInventory.spec.js index 2427e4f..dfea737 100644 --- a/specs/addPricingInventory.spec.js +++ b/specs/addPricingInventory.spec.js @@ -1,3 +1,10 @@ +/** + * @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'); @@ -6,12 +13,31 @@ 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(); @@ -19,18 +45,33 @@ test.describe('Test should check the Pricing and Inventory feature', () => { 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/addUserCustomer.spec.js b/specs/addUserCustomer.spec.js index 60730dc..f142b59 100644 --- a/specs/addUserCustomer.spec.js +++ b/specs/addUserCustomer.spec.js @@ -1,3 +1,10 @@ +/** + * @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'); @@ -6,15 +13,32 @@ 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(); @@ -26,14 +50,28 @@ test.describe('It should customer user creation feature', () => { }) + /** + * 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); diff --git a/specs/checkoutPlaceOrder.spec.js b/specs/checkoutPlaceOrder.spec.js index 6090d00..b0609d6 100644 --- a/specs/checkoutPlaceOrder.spec.js +++ b/specs/checkoutPlaceOrder.spec.js @@ -1,3 +1,10 @@ +/** + * @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"); @@ -22,31 +29,67 @@ 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}`; @@ -55,6 +98,7 @@ test.describe('Test should verify the chekcout workflow', ()=>{ // 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, @@ -68,26 +112,45 @@ test.describe('Test should verify the chekcout workflow', ()=>{ 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({ @@ -95,8 +158,10 @@ test.describe('Test should verify the chekcout workflow', ()=>{ // 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/checkoutVerifyCoupon.spec.js b/specs/checkoutVerifyCoupon.spec.js index c3862d1..aae5342 100644 --- a/specs/checkoutVerifyCoupon.spec.js +++ b/specs/checkoutVerifyCoupon.spec.js @@ -1,3 +1,11 @@ +/** + * @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'); @@ -16,52 +24,102 @@ 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); }); diff --git a/specs/createSimpleProduct.spec.js b/specs/createSimpleProduct.spec.js index 9036a66..7012f68 100644 --- a/specs/createSimpleProduct.spec.js +++ b/specs/createSimpleProduct.spec.js @@ -1,3 +1,10 @@ +/** + * @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'); @@ -8,31 +15,75 @@ 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); }) diff --git a/specs/publishProduct.spec.js b/specs/publishProduct.spec.js index ac84770..34f2c3d 100644 --- a/specs/publishProduct.spec.js +++ b/specs/publishProduct.spec.js @@ -1,3 +1,9 @@ +/** + * @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"); @@ -8,33 +14,78 @@ 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(); + // Verify the title of the product page await expect(page).toHaveTitle(`${productTitle} – rishav.rt.gw`); }) + /** + * 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/reviewOrder.spec.js b/specs/reviewOrder.spec.js index 5ba237b..9cb960b 100644 --- a/specs/reviewOrder.spec.js +++ b/specs/reviewOrder.spec.js @@ -1,3 +1,9 @@ +/** + * @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"); @@ -16,31 +22,65 @@ 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}`; @@ -49,6 +89,7 @@ test.describe('Test should verify the chekcout workflow', ()=>{ // await addCustomerUser(admin, page, userName, userEmail, firstName, lastName, password); + // Create a new customer user via API await requestUtils.createUser({ username: userName, email: userEmail, @@ -58,39 +99,43 @@ test.describe('Test should verify the chekcout workflow', ()=>{ 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(); - await page.goto(`${process.env.WP_BASE_URL}wp-login.php`); - - const userNameField = page.locator('//input[@id="user_login"]'); - await userNameField.fill(`${process.env.WP_USERNAME}`); - - const passwordField = page.locator('//input[@id="user_pass"]'); - await passwordField.fill(`${process.env.WP_PASSWORD}`); - - const loginBtn = page.locator('//input[@id="wp-submit"]'); - await loginBtn.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"]'); @@ -108,10 +153,19 @@ test.describe('Test should verify the chekcout workflow', ()=>{ }); + /** + * 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); }) From e513e9dbc99633495845a94d3b0649a4fa1d1250 Mon Sep 17 00:00:00 2001 From: Rishav Dutta Date: Tue, 7 Jan 2025 16:34:39 +0530 Subject: [PATCH 16/27] add: add JS docs and comments in the utils function --- utils/e2eUtils/adminLoginUtils.js | 25 +++++++++++- utils/e2eUtils/applyCouponUtils.js | 32 +++++++++++++-- utils/e2eUtils/checkoutPlaceOrderUtils.js | 39 ++++++++++++++++--- utils/e2eUtils/createCouponUtils.js | 35 ++++++++++++++++- utils/e2eUtils/createCustomerUtils.js | 47 +++++++++++++++++++++-- utils/e2eUtils/createProductUtils.js | 31 ++++++++++++++- utils/e2eUtils/customerLoginUtils.js | 28 +++++++++++++- utils/e2eUtils/getProductSlug.js | 23 +++++++++++ utils/e2eUtils/productInventoryUtils.js | 24 ++++++++++++ utils/e2eUtils/randomTestCode.js | 14 +++++++ utils/e2eUtils/testCouponDeletion.js | 29 ++++++++++++-- utils/e2eUtils/testProductDeletion.js | 30 +++++++++++++-- utils/e2eUtils/testUserDeletion.js | 30 ++++++++++++++- 13 files changed, 358 insertions(+), 29 deletions(-) diff --git a/utils/e2eUtils/adminLoginUtils.js b/utils/e2eUtils/adminLoginUtils.js index 2e8026a..0f524a7 100644 --- a/utils/e2eUtils/adminLoginUtils.js +++ b/utils/e2eUtils/adminLoginUtils.js @@ -1,19 +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 -} + adminLogin, +}; diff --git a/utils/e2eUtils/applyCouponUtils.js b/utils/e2eUtils/applyCouponUtils.js index 08e0348..5d621c1 100644 --- a/utils/e2eUtils/applyCouponUtils.js +++ b/utils/e2eUtils/applyCouponUtils.js @@ -1,32 +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 -} + applyCouponDiscount, +}; diff --git a/utils/e2eUtils/checkoutPlaceOrderUtils.js b/utils/e2eUtils/checkoutPlaceOrderUtils.js index 7132243..fcb8286 100644 --- a/utils/e2eUtils/checkoutPlaceOrderUtils.js +++ b/utils/e2eUtils/checkoutPlaceOrderUtils.js @@ -1,46 +1,73 @@ +/** + * 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 -) => { +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(); }; diff --git a/utils/e2eUtils/createCouponUtils.js b/utils/e2eUtils/createCouponUtils.js index c01c54f..e613ade 100644 --- a/utils/e2eUtils/createCouponUtils.js +++ b/utils/e2eUtils/createCouponUtils.js @@ -1,22 +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 -} + createCoupon, +}; diff --git a/utils/e2eUtils/createCustomerUtils.js b/utils/e2eUtils/createCustomerUtils.js index 8c0761b..7dc60da 100644 --- a/utils/e2eUtils/createCustomerUtils.js +++ b/utils/e2eUtils/createCustomerUtils.js @@ -1,3 +1,28 @@ +/** + * 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, @@ -5,26 +30,40 @@ const addCustomerUser = async ( userEmail, firstName, lastName, - password, + 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"); - const addUserButton = page.locator('//input[@id="createusersub"]'); + // Click the "Add New User" button to create the user + const addUserButton = page.locator('//input[@id="createusersub"]'); await addUserButton.click(); }; module.exports = { - addCustomerUser -} + addCustomerUser, +}; diff --git a/utils/e2eUtils/createProductUtils.js b/utils/e2eUtils/createProductUtils.js index 40a0451..b420536 100644 --- a/utils/e2eUtils/createProductUtils.js +++ b/utils/e2eUtils/createProductUtils.js @@ -1,18 +1,45 @@ +/** + * 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 = { diff --git a/utils/e2eUtils/customerLoginUtils.js b/utils/e2eUtils/customerLoginUtils.js index 7870574..d529934 100644 --- a/utils/e2eUtils/customerLoginUtils.js +++ b/utils/e2eUtils/customerLoginUtils.js @@ -1,16 +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 -} + customerUserLogin, +}; diff --git a/utils/e2eUtils/getProductSlug.js b/utils/e2eUtils/getProductSlug.js index 1266cb6..f09cfb8 100644 --- a/utils/e2eUtils/getProductSlug.js +++ b/utils/e2eUtils/getProductSlug.js @@ -1,5 +1,28 @@ +/** + * 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(); } diff --git a/utils/e2eUtils/productInventoryUtils.js b/utils/e2eUtils/productInventoryUtils.js index 6002228..24df418 100644 --- a/utils/e2eUtils/productInventoryUtils.js +++ b/utils/e2eUtils/productInventoryUtils.js @@ -1,26 +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 index d81a027..f8c98ed 100644 --- a/utils/e2eUtils/randomTestCode.js +++ b/utils/e2eUtils/randomTestCode.js @@ -1,7 +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/testCouponDeletion.js b/utils/e2eUtils/testCouponDeletion.js index 7bf8605..00e40ea 100644 --- a/utils/e2eUtils/testCouponDeletion.js +++ b/utils/e2eUtils/testCouponDeletion.js @@ -1,30 +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 -} \ No newline at end of file + removeTestCouponRecord, +}; diff --git a/utils/e2eUtils/testProductDeletion.js b/utils/e2eUtils/testProductDeletion.js index 6a2e17d..50be83b 100644 --- a/utils/e2eUtils/testProductDeletion.js +++ b/utils/e2eUtils/testProductDeletion.js @@ -1,28 +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) => { - await admin.visitAdminPage('edit.php','post_type=product'); + // 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 -} + removeTestProductRecord, +}; diff --git a/utils/e2eUtils/testUserDeletion.js b/utils/e2eUtils/testUserDeletion.js index 9ee42ea..736630c 100644 --- a/utils/e2eUtils/testUserDeletion.js +++ b/utils/e2eUtils/testUserDeletion.js @@ -1,27 +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 -} + removeTestUserRecord, +}; From 8954ecd15527362c8922ca338878cab0cc138560 Mon Sep 17 00:00:00 2001 From: Rishav Dutta Date: Tue, 7 Jan 2025 16:50:58 +0530 Subject: [PATCH 17/27] update: update README.md file --- README.md | 87 +++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 85 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 3eee2d3..885b611 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 From 35674a88f325e69e13bb1f6fab7b67899e92b905 Mon Sep 17 00:00:00 2001 From: Rishav Dutta Date: Tue, 7 Jan 2025 17:51:43 +0530 Subject: [PATCH 18/27] update: update README Pre-requisites --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 885b611..f0a2d7a 100644 --- a/README.md +++ b/README.md @@ -63,10 +63,10 @@ WP_BASE_URL= 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) +- `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 From 682d2fab9266a5d3bee50ef154ea23166dea111c Mon Sep 17 00:00:00 2001 From: Rishav Dutta Date: Tue, 7 Jan 2025 20:23:23 +0530 Subject: [PATCH 19/27] fix: fix the product image test script --- artifacts/storage-states/admin.json | 2 +- specs/addProductImage.spec.js | 72 +++++++++++++++++++++++++++-- specs/addUserCustomer.spec.js | 1 + 3 files changed, 70 insertions(+), 5 deletions(-) diff --git a/artifacts/storage-states/admin.json b/artifacts/storage-states/admin.json index e665b7e..a9b2fb1 100644 --- a/artifacts/storage-states/admin.json +++ b/artifacts/storage-states/admin.json @@ -1 +1 @@ -{"cookies":[{"name":"wordpress_test_cookie","value":"WP%20Cookie%20check","domain":"rishav.rt.gw","path":"/","expires":-1,"httpOnly":false,"secure":true,"sameSite":"Lax"},{"name":"wordpress_sec_074e74b03d8a277981a80ce1db027a5e","value":"wp-user-4w8k6z%7C1736416979%7CIbiaP7McEqZB0EGwubzLhlC5qt2JStUJNbGr2lgjsvW%7Cb57f016d79b1b9d7a365133d12734a9c46cf28c3f95f8b91418c983aabfc6597","domain":"rishav.rt.gw","path":"/wp-content/plugins","expires":-1,"httpOnly":true,"secure":true,"sameSite":"Lax"},{"name":"wordpress_sec_074e74b03d8a277981a80ce1db027a5e","value":"wp-user-4w8k6z%7C1736416979%7CIbiaP7McEqZB0EGwubzLhlC5qt2JStUJNbGr2lgjsvW%7Cb57f016d79b1b9d7a365133d12734a9c46cf28c3f95f8b91418c983aabfc6597","domain":"rishav.rt.gw","path":"/wp-admin","expires":-1,"httpOnly":true,"secure":true,"sameSite":"Lax"},{"name":"wordpress_logged_in_074e74b03d8a277981a80ce1db027a5e","value":"wp-user-4w8k6z%7C1736416979%7CIbiaP7McEqZB0EGwubzLhlC5qt2JStUJNbGr2lgjsvW%7C9431c529a33a4059518e18baa4b6ad101fb8614199151a3db4176d294d776d9a","domain":"rishav.rt.gw","path":"/","expires":-1,"httpOnly":true,"secure":true,"sameSite":"Lax"},{"name":"tk_ai","value":"woo%3Ar7yWZheRH%2BgrBhJayxfsytUU","domain":"rishav.rt.gw","path":"/","expires":-1,"httpOnly":false,"secure":false,"sameSite":"Lax"},{"name":"wp-settings-1","value":"libraryContent%3Dbrowse%26editor%3Dtinymce","domain":"rishav.rt.gw","path":"/","expires":1767780159.363,"httpOnly":false,"secure":true,"sameSite":"Lax"},{"name":"wp-settings-time-1","value":"1736244159","domain":"rishav.rt.gw","path":"/","expires":1767780159.363,"httpOnly":false,"secure":true,"sameSite":"Lax"}],"nonce":"c23ecf7384","rootURL":"https://rishav.rt.gw/wp-json/"} \ No newline at end of file +{"cookies":[{"name":"wordpress_test_cookie","value":"WP%20Cookie%20check","domain":"rishav.rt.gw","path":"/","expires":-1,"httpOnly":false,"secure":true,"sameSite":"Lax"},{"name":"wordpress_sec_074e74b03d8a277981a80ce1db027a5e","value":"wp-user-4w8k6z%7C1736433896%7CCx1b0d2vMG3qrsjfeuR91qGl0AJNEasNLRdgtuUTlPj%7C777528138728b2d5323c24f09efc6d46d9286b966dcfdcdbe1c4aad1f7821264","domain":"rishav.rt.gw","path":"/wp-content/plugins","expires":-1,"httpOnly":true,"secure":true,"sameSite":"Lax"},{"name":"wordpress_sec_074e74b03d8a277981a80ce1db027a5e","value":"wp-user-4w8k6z%7C1736433896%7CCx1b0d2vMG3qrsjfeuR91qGl0AJNEasNLRdgtuUTlPj%7C777528138728b2d5323c24f09efc6d46d9286b966dcfdcdbe1c4aad1f7821264","domain":"rishav.rt.gw","path":"/wp-admin","expires":-1,"httpOnly":true,"secure":true,"sameSite":"Lax"},{"name":"wordpress_logged_in_074e74b03d8a277981a80ce1db027a5e","value":"wp-user-4w8k6z%7C1736433896%7CCx1b0d2vMG3qrsjfeuR91qGl0AJNEasNLRdgtuUTlPj%7Cb5c6ae5e8a4bb846516272009d0df85926416dc8cc1321567030995d956658c0","domain":"rishav.rt.gw","path":"/","expires":-1,"httpOnly":true,"secure":true,"sameSite":"Lax"},{"name":"tk_ai","value":"woo%3Ar7yWZheRH%2BgrBhJayxfsytUU","domain":"rishav.rt.gw","path":"/","expires":-1,"httpOnly":false,"secure":false,"sameSite":"Lax"},{"name":"wp-settings-1","value":"libraryContent%3Dbrowse%26editor%3Dtinymce","domain":"rishav.rt.gw","path":"/","expires":1767797097.388,"httpOnly":false,"secure":true,"sameSite":"Lax"},{"name":"wp-settings-time-1","value":"1736261097","domain":"rishav.rt.gw","path":"/","expires":1767797097.388,"httpOnly":false,"secure":true,"sameSite":"Lax"}],"nonce":"778bd34ce9","rootURL":"https://rishav.rt.gw/wp-json/"} \ No newline at end of file diff --git a/specs/addProductImage.spec.js b/specs/addProductImage.spec.js index 1d87655..3f3f865 100644 --- a/specs/addProductImage.spec.js +++ b/specs/addProductImage.spec.js @@ -3,6 +3,10 @@ const { addNewProduct } = require("../utils/e2eUtils/createProductUtils"); const {generateTestCode} = require('../utils/e2eUtils/randomTestCode'); +const {removeTestProductRecord} = require('../utils/e2eUtils/testProductDeletion'); + +const { extractPrdtSlug } = require('../utils/e2eUtils/getProductSlug'); + test.describe('Test the Product Image Feature', ()=>{ @@ -10,24 +14,84 @@ test.describe('Test the Product Image Feature', ()=>{ let productTitle = ''; let productDescription = ''; + /** + * Generates a unique test code and sets up product details before all tests in this suite. + */ test.beforeAll(()=>{ + // Generate random test code for product details testCode = generateTestCode(); + /** + * Product Details + * @type {string} + */ productTitle = `Product Demo Title ${testCode}`; productDescription = `Demo Product Description ${testCode}`; }); - test('It should be able to upload product image', async({admin,page})=>{ + /** + * 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); + // Store the current product edit screen URL for later navigation + const prdtEditScreen = page.url(); + await page.getByRole('link', { name: 'Set product image' }).click(); - await page.getByLabel('Select Files').click(); - await page.getByLabel('Select Files').setInputFiles('/Users/rishavdutta/Documents/qa-idp/WP-e2e-utls/TestAutomation-Hands-on/assets/product_test_image.png'); - await page.getByRole('button', { 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); + + // Locate the 'Update' button and click it to save the changes + const updateButton = page.locator('//input[@id="publish"]'); + await updateButton.click(); + + // 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/addUserCustomer.spec.js b/specs/addUserCustomer.spec.js index f142b59..c535bdc 100644 --- a/specs/addUserCustomer.spec.js +++ b/specs/addUserCustomer.spec.js @@ -42,6 +42,7 @@ test.describe('It should customer user creation feature', () => { test.beforeAll(()=>{ testCode = generateTestCode(); + // User details userName = `TestUserName-${testCode}`; userEmail = `test${testCode}@trial.com`; firstName = `TestFirstName-${testCode}`; From 47e7146a99ab9ba01491d4529f0eaa6f970d80ce Mon Sep 17 00:00:00 2001 From: Rishav Dutta Date: Tue, 7 Jan 2025 21:39:23 +0530 Subject: [PATCH 20/27] refcator: refactor tag and category test scripts --- artifacts/storage-states/admin.json | 2 +- specs/addCategory.spec.js | 92 ++++++++++++++++----- specs/addTag.spec.js | 94 +++++++++++++++++----- utils/e2eUtils/addTaxonomyUtils.js | 45 +++++++++++ utils/e2eUtils/removeTestTaxonomtyUtils.js | 33 ++++++++ 5 files changed, 225 insertions(+), 41 deletions(-) create mode 100644 utils/e2eUtils/addTaxonomyUtils.js create mode 100644 utils/e2eUtils/removeTestTaxonomtyUtils.js diff --git a/artifacts/storage-states/admin.json b/artifacts/storage-states/admin.json index a9b2fb1..0550825 100644 --- a/artifacts/storage-states/admin.json +++ b/artifacts/storage-states/admin.json @@ -1 +1 @@ -{"cookies":[{"name":"wordpress_test_cookie","value":"WP%20Cookie%20check","domain":"rishav.rt.gw","path":"/","expires":-1,"httpOnly":false,"secure":true,"sameSite":"Lax"},{"name":"wordpress_sec_074e74b03d8a277981a80ce1db027a5e","value":"wp-user-4w8k6z%7C1736433896%7CCx1b0d2vMG3qrsjfeuR91qGl0AJNEasNLRdgtuUTlPj%7C777528138728b2d5323c24f09efc6d46d9286b966dcfdcdbe1c4aad1f7821264","domain":"rishav.rt.gw","path":"/wp-content/plugins","expires":-1,"httpOnly":true,"secure":true,"sameSite":"Lax"},{"name":"wordpress_sec_074e74b03d8a277981a80ce1db027a5e","value":"wp-user-4w8k6z%7C1736433896%7CCx1b0d2vMG3qrsjfeuR91qGl0AJNEasNLRdgtuUTlPj%7C777528138728b2d5323c24f09efc6d46d9286b966dcfdcdbe1c4aad1f7821264","domain":"rishav.rt.gw","path":"/wp-admin","expires":-1,"httpOnly":true,"secure":true,"sameSite":"Lax"},{"name":"wordpress_logged_in_074e74b03d8a277981a80ce1db027a5e","value":"wp-user-4w8k6z%7C1736433896%7CCx1b0d2vMG3qrsjfeuR91qGl0AJNEasNLRdgtuUTlPj%7Cb5c6ae5e8a4bb846516272009d0df85926416dc8cc1321567030995d956658c0","domain":"rishav.rt.gw","path":"/","expires":-1,"httpOnly":true,"secure":true,"sameSite":"Lax"},{"name":"tk_ai","value":"woo%3Ar7yWZheRH%2BgrBhJayxfsytUU","domain":"rishav.rt.gw","path":"/","expires":-1,"httpOnly":false,"secure":false,"sameSite":"Lax"},{"name":"wp-settings-1","value":"libraryContent%3Dbrowse%26editor%3Dtinymce","domain":"rishav.rt.gw","path":"/","expires":1767797097.388,"httpOnly":false,"secure":true,"sameSite":"Lax"},{"name":"wp-settings-time-1","value":"1736261097","domain":"rishav.rt.gw","path":"/","expires":1767797097.388,"httpOnly":false,"secure":true,"sameSite":"Lax"}],"nonce":"778bd34ce9","rootURL":"https://rishav.rt.gw/wp-json/"} \ No newline at end of file +{"cookies":[{"name":"wordpress_test_cookie","value":"WP%20Cookie%20check","domain":"rishav.rt.gw","path":"/","expires":-1,"httpOnly":false,"secure":true,"sameSite":"Lax"},{"name":"wordpress_sec_074e74b03d8a277981a80ce1db027a5e","value":"wp-user-4w8k6z%7C1736438212%7C1FzXriqWTWsG4zIcxEcwVYMDFuUw8dT8ZrbDD6Zw6QV%7C3a62a84b2c09a7dbc77609aa9d5f21f9b764f8dcf72dc8c1b508adf486acf19d","domain":"rishav.rt.gw","path":"/wp-content/plugins","expires":-1,"httpOnly":true,"secure":true,"sameSite":"Lax"},{"name":"wordpress_sec_074e74b03d8a277981a80ce1db027a5e","value":"wp-user-4w8k6z%7C1736438212%7C1FzXriqWTWsG4zIcxEcwVYMDFuUw8dT8ZrbDD6Zw6QV%7C3a62a84b2c09a7dbc77609aa9d5f21f9b764f8dcf72dc8c1b508adf486acf19d","domain":"rishav.rt.gw","path":"/wp-admin","expires":-1,"httpOnly":true,"secure":true,"sameSite":"Lax"},{"name":"wordpress_logged_in_074e74b03d8a277981a80ce1db027a5e","value":"wp-user-4w8k6z%7C1736438212%7C1FzXriqWTWsG4zIcxEcwVYMDFuUw8dT8ZrbDD6Zw6QV%7C0f7ded8de9ea9e8382ecea63404ac0ce9cef51add38257b08e50720b42a81fcd","domain":"rishav.rt.gw","path":"/","expires":-1,"httpOnly":true,"secure":true,"sameSite":"Lax"},{"name":"tk_ai","value":"woo%3Ar7yWZheRH%2BgrBhJayxfsytUU","domain":"rishav.rt.gw","path":"/","expires":-1,"httpOnly":false,"secure":false,"sameSite":"Lax"},{"name":"wp-settings-1","value":"libraryContent%3Dbrowse%26editor%3Dtinymce","domain":"rishav.rt.gw","path":"/","expires":1767801413.821,"httpOnly":false,"secure":true,"sameSite":"Lax"},{"name":"wp-settings-time-1","value":"1736265413","domain":"rishav.rt.gw","path":"/","expires":1767801413.821,"httpOnly":false,"secure":true,"sameSite":"Lax"}],"nonce":"d4a9ea1170","rootURL":"https://rishav.rt.gw/wp-json/"} \ No newline at end of file diff --git a/specs/addCategory.spec.js b/specs/addCategory.spec.js index 74d3daa..096ce17 100644 --- a/specs/addCategory.spec.js +++ b/specs/addCategory.spec.js @@ -1,24 +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", () => { - test("It should test the add category feature", async ({ admin, page }) => { - await admin.visitAdminPage( - "edit-tags.php", - "taxonomy=product_cat&post_type=product" - ); - - const categoryNameField = page.locator('//input[@id="tag-name"]'); - await categoryNameField.fill("TestCategory"); - const categorySlugField = page.locator('//input[@id="tag-slug"]'); - await categorySlugField.fill("test-cat"); - const categoryDecriptionField = page.locator( - '//textarea[@id="tag-description"]' - ); - await categoryDecriptionField.fill("This is a category for testing"); - const addNewCategoryButton = page.locator('//input[@id="submit"]'); - - await addNewCategoryButton.click(); - - await expect(page.locator('//a[contains(@class, "row") and text()="TestCategory"]')).toBeVisible(); - }); + + 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/addTag.spec.js b/specs/addTag.spec.js index 08a7fa2..87ab935 100644 --- a/specs/addTag.spec.js +++ b/specs/addTag.spec.js @@ -1,24 +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", () => { - test("It should test the add tag feature", async ({ admin, page }) => { - await admin.visitAdminPage( - "edit-tags.php", - "taxonomy=product_tag&post_type=product" - ); - - const categoryNameField = page.locator('//input[@id="tag-name"]'); - await categoryNameField.fill("TestTag"); - const categorySlugField = page.locator('//input[@id="tag-slug"]'); - await categorySlugField.fill("test-tag"); - const categoryDecriptionField = page.locator( - '//textarea[@id="tag-description"]' - ); - await categoryDecriptionField.fill("This is a tag for testing"); - const addNewCategoryButton = page.locator('//input[@id="submit"]'); - - await addNewCategoryButton.click(); - - await expect(page.locator('//a[contains(@class, "row-title") and text()="TestTag"]')).toBeVisible(); - }); + + 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/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/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 From cc91d5ead2a384018319f855b9167faa6f7e2dbd Mon Sep 17 00:00:00 2001 From: Rishav Dutta Date: Tue, 7 Jan 2025 23:37:05 +0530 Subject: [PATCH 21/27] refactor: refactor add product image test script --- artifacts/storage-states/admin.json | 2 +- specs/addProductImage.spec.js | 32 +++++++---------- specs/addTagCategoryToPrdt.spec.js | 0 utils/e2eUtils/addProductImageUtils.js | 48 ++++++++++++++++++++++++++ 4 files changed, 61 insertions(+), 21 deletions(-) create mode 100644 specs/addTagCategoryToPrdt.spec.js create mode 100644 utils/e2eUtils/addProductImageUtils.js diff --git a/artifacts/storage-states/admin.json b/artifacts/storage-states/admin.json index 0550825..d2fd7f1 100644 --- a/artifacts/storage-states/admin.json +++ b/artifacts/storage-states/admin.json @@ -1 +1 @@ -{"cookies":[{"name":"wordpress_test_cookie","value":"WP%20Cookie%20check","domain":"rishav.rt.gw","path":"/","expires":-1,"httpOnly":false,"secure":true,"sameSite":"Lax"},{"name":"wordpress_sec_074e74b03d8a277981a80ce1db027a5e","value":"wp-user-4w8k6z%7C1736438212%7C1FzXriqWTWsG4zIcxEcwVYMDFuUw8dT8ZrbDD6Zw6QV%7C3a62a84b2c09a7dbc77609aa9d5f21f9b764f8dcf72dc8c1b508adf486acf19d","domain":"rishav.rt.gw","path":"/wp-content/plugins","expires":-1,"httpOnly":true,"secure":true,"sameSite":"Lax"},{"name":"wordpress_sec_074e74b03d8a277981a80ce1db027a5e","value":"wp-user-4w8k6z%7C1736438212%7C1FzXriqWTWsG4zIcxEcwVYMDFuUw8dT8ZrbDD6Zw6QV%7C3a62a84b2c09a7dbc77609aa9d5f21f9b764f8dcf72dc8c1b508adf486acf19d","domain":"rishav.rt.gw","path":"/wp-admin","expires":-1,"httpOnly":true,"secure":true,"sameSite":"Lax"},{"name":"wordpress_logged_in_074e74b03d8a277981a80ce1db027a5e","value":"wp-user-4w8k6z%7C1736438212%7C1FzXriqWTWsG4zIcxEcwVYMDFuUw8dT8ZrbDD6Zw6QV%7C0f7ded8de9ea9e8382ecea63404ac0ce9cef51add38257b08e50720b42a81fcd","domain":"rishav.rt.gw","path":"/","expires":-1,"httpOnly":true,"secure":true,"sameSite":"Lax"},{"name":"tk_ai","value":"woo%3Ar7yWZheRH%2BgrBhJayxfsytUU","domain":"rishav.rt.gw","path":"/","expires":-1,"httpOnly":false,"secure":false,"sameSite":"Lax"},{"name":"wp-settings-1","value":"libraryContent%3Dbrowse%26editor%3Dtinymce","domain":"rishav.rt.gw","path":"/","expires":1767801413.821,"httpOnly":false,"secure":true,"sameSite":"Lax"},{"name":"wp-settings-time-1","value":"1736265413","domain":"rishav.rt.gw","path":"/","expires":1767801413.821,"httpOnly":false,"secure":true,"sameSite":"Lax"}],"nonce":"d4a9ea1170","rootURL":"https://rishav.rt.gw/wp-json/"} \ No newline at end of file +{"cookies":[{"name":"wordpress_test_cookie","value":"WP%20Cookie%20check","domain":"rishav.rt.gw","path":"/","expires":-1,"httpOnly":false,"secure":true,"sameSite":"Lax"},{"name":"wordpress_sec_074e74b03d8a277981a80ce1db027a5e","value":"wp-user-4w8k6z%7C1736445879%7C0da6XbJDjFMlhahrmpf2Z4MeRVDkhDsXJSUD8fCa1mD%7C1f32fb4088cea0a23ceed14a6d864076f92a37c5c961614247c1b4d7c3d8a93c","domain":"rishav.rt.gw","path":"/wp-content/plugins","expires":-1,"httpOnly":true,"secure":true,"sameSite":"Lax"},{"name":"wordpress_sec_074e74b03d8a277981a80ce1db027a5e","value":"wp-user-4w8k6z%7C1736445879%7C0da6XbJDjFMlhahrmpf2Z4MeRVDkhDsXJSUD8fCa1mD%7C1f32fb4088cea0a23ceed14a6d864076f92a37c5c961614247c1b4d7c3d8a93c","domain":"rishav.rt.gw","path":"/wp-admin","expires":-1,"httpOnly":true,"secure":true,"sameSite":"Lax"},{"name":"wordpress_logged_in_074e74b03d8a277981a80ce1db027a5e","value":"wp-user-4w8k6z%7C1736445879%7C0da6XbJDjFMlhahrmpf2Z4MeRVDkhDsXJSUD8fCa1mD%7Cc69f42c571644bae11d4ed9a59265a743ffefb4f3d34158c75c1c77f05be7dba","domain":"rishav.rt.gw","path":"/","expires":-1,"httpOnly":true,"secure":true,"sameSite":"Lax"},{"name":"tk_ai","value":"woo%3Ar7yWZheRH%2BgrBhJayxfsytUU","domain":"rishav.rt.gw","path":"/","expires":-1,"httpOnly":false,"secure":false,"sameSite":"Lax"},{"name":"wp-settings-1","value":"libraryContent%3Dbrowse%26editor%3Dtinymce","domain":"rishav.rt.gw","path":"/","expires":1767808894.443,"httpOnly":false,"secure":true,"sameSite":"Lax"},{"name":"wp-settings-time-1","value":"1736272894","domain":"rishav.rt.gw","path":"/","expires":1767808894.443,"httpOnly":false,"secure":true,"sameSite":"Lax"}],"nonce":"81c71392d4","rootURL":"https://rishav.rt.gw/wp-json/"} \ No newline at end of file diff --git a/specs/addProductImage.spec.js b/specs/addProductImage.spec.js index 3f3f865..9ea9b23 100644 --- a/specs/addProductImage.spec.js +++ b/specs/addProductImage.spec.js @@ -7,6 +7,10 @@ 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', ()=>{ @@ -17,7 +21,9 @@ test.describe('Test the Product Image Feature', ()=>{ /** * Generates a unique test code and sets up product details before all tests in this suite. */ - test.beforeAll(()=>{ + test.beforeAll(({requestUtils})=>{ + + requestUtils.deleteAllMedia(); // Generate random test code for product details testCode = generateTestCode(); @@ -46,30 +52,16 @@ test.describe('Test the Product Image Feature', ()=>{ 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'); + // await requestUtils.uploadMedia('assets/product_test_image.png'); // Add a new product with the generated title and description await addNewProduct(admin, page, productTitle, productDescription); - // 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); + // Add pricing and inventory details to the product + await addPricingInventory(admin, page); - // Locate the 'Update' button and click it to save the changes - const updateButton = page.locator('//input[@id="publish"]'); - await updateButton.click(); + // 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'); diff --git a/specs/addTagCategoryToPrdt.spec.js b/specs/addTagCategoryToPrdt.spec.js new file mode 100644 index 0000000..e69de29 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, +}; From e035ae9cfc8ede07db8802b96727b4c125e9e172 Mon Sep 17 00:00:00 2001 From: Rishav Dutta Date: Wed, 8 Jan 2025 00:55:25 +0530 Subject: [PATCH 22/27] refactor: refactor assign product tag and category test script and add utility function --- artifacts/storage-states/admin.json | 2 +- playwright.config.js | 16 ++-- specs/addTagCategoryToPrdt.spec.js | 114 ++++++++++++++++++++++++ utils/e2eUtils/assignPrdtTagCategory.js | 28 ++++++ 4 files changed, 151 insertions(+), 9 deletions(-) create mode 100644 utils/e2eUtils/assignPrdtTagCategory.js diff --git a/artifacts/storage-states/admin.json b/artifacts/storage-states/admin.json index d2fd7f1..e19e169 100644 --- a/artifacts/storage-states/admin.json +++ b/artifacts/storage-states/admin.json @@ -1 +1 @@ -{"cookies":[{"name":"wordpress_test_cookie","value":"WP%20Cookie%20check","domain":"rishav.rt.gw","path":"/","expires":-1,"httpOnly":false,"secure":true,"sameSite":"Lax"},{"name":"wordpress_sec_074e74b03d8a277981a80ce1db027a5e","value":"wp-user-4w8k6z%7C1736445879%7C0da6XbJDjFMlhahrmpf2Z4MeRVDkhDsXJSUD8fCa1mD%7C1f32fb4088cea0a23ceed14a6d864076f92a37c5c961614247c1b4d7c3d8a93c","domain":"rishav.rt.gw","path":"/wp-content/plugins","expires":-1,"httpOnly":true,"secure":true,"sameSite":"Lax"},{"name":"wordpress_sec_074e74b03d8a277981a80ce1db027a5e","value":"wp-user-4w8k6z%7C1736445879%7C0da6XbJDjFMlhahrmpf2Z4MeRVDkhDsXJSUD8fCa1mD%7C1f32fb4088cea0a23ceed14a6d864076f92a37c5c961614247c1b4d7c3d8a93c","domain":"rishav.rt.gw","path":"/wp-admin","expires":-1,"httpOnly":true,"secure":true,"sameSite":"Lax"},{"name":"wordpress_logged_in_074e74b03d8a277981a80ce1db027a5e","value":"wp-user-4w8k6z%7C1736445879%7C0da6XbJDjFMlhahrmpf2Z4MeRVDkhDsXJSUD8fCa1mD%7Cc69f42c571644bae11d4ed9a59265a743ffefb4f3d34158c75c1c77f05be7dba","domain":"rishav.rt.gw","path":"/","expires":-1,"httpOnly":true,"secure":true,"sameSite":"Lax"},{"name":"tk_ai","value":"woo%3Ar7yWZheRH%2BgrBhJayxfsytUU","domain":"rishav.rt.gw","path":"/","expires":-1,"httpOnly":false,"secure":false,"sameSite":"Lax"},{"name":"wp-settings-1","value":"libraryContent%3Dbrowse%26editor%3Dtinymce","domain":"rishav.rt.gw","path":"/","expires":1767808894.443,"httpOnly":false,"secure":true,"sameSite":"Lax"},{"name":"wp-settings-time-1","value":"1736272894","domain":"rishav.rt.gw","path":"/","expires":1767808894.443,"httpOnly":false,"secure":true,"sameSite":"Lax"}],"nonce":"81c71392d4","rootURL":"https://rishav.rt.gw/wp-json/"} \ No newline at end of file +{"cookies":[{"name":"wordpress_test_cookie","value":"WP%20Cookie%20check","domain":"rishav.rt.gw","path":"/","expires":-1,"httpOnly":false,"secure":true,"sameSite":"Lax"},{"name":"wordpress_sec_074e74b03d8a277981a80ce1db027a5e","value":"wp-user-4w8k6z%7C1736450613%7CIaAhbRf34oCILTw99kMDEGaQH56CtQaPUgU017I6eqD%7C79e249e73914fe0cd6f954711286e6a830112bc80e1ddc5a60bba16bbd019aca","domain":"rishav.rt.gw","path":"/wp-content/plugins","expires":-1,"httpOnly":true,"secure":true,"sameSite":"Lax"},{"name":"wordpress_sec_074e74b03d8a277981a80ce1db027a5e","value":"wp-user-4w8k6z%7C1736450613%7CIaAhbRf34oCILTw99kMDEGaQH56CtQaPUgU017I6eqD%7C79e249e73914fe0cd6f954711286e6a830112bc80e1ddc5a60bba16bbd019aca","domain":"rishav.rt.gw","path":"/wp-admin","expires":-1,"httpOnly":true,"secure":true,"sameSite":"Lax"},{"name":"wordpress_logged_in_074e74b03d8a277981a80ce1db027a5e","value":"wp-user-4w8k6z%7C1736450613%7CIaAhbRf34oCILTw99kMDEGaQH56CtQaPUgU017I6eqD%7C39a976dae6a75ccae8e86e59e6da19ff332aae943da435a9d40542cc155e2c5c","domain":"rishav.rt.gw","path":"/","expires":-1,"httpOnly":true,"secure":true,"sameSite":"Lax"},{"name":"tk_ai","value":"woo%3Ar7yWZheRH%2BgrBhJayxfsytUU","domain":"rishav.rt.gw","path":"/","expires":-1,"httpOnly":false,"secure":false,"sameSite":"Lax"},{"name":"wp-settings-1","value":"libraryContent%3Dbrowse%26editor%3Dtinymce","domain":"rishav.rt.gw","path":"/","expires":1767813814.799,"httpOnly":false,"secure":true,"sameSite":"Lax"},{"name":"wp-settings-time-1","value":"1736277814","domain":"rishav.rt.gw","path":"/","expires":1767813814.799,"httpOnly":false,"secure":true,"sameSite":"Lax"},{"name":"woocommerce_items_in_cart","value":"1","domain":"rishav.rt.gw","path":"/","expires":-1,"httpOnly":false,"secure":false,"sameSite":"Lax"},{"name":"woocommerce_cart_hash","value":"ca1e57ca31bf215971405ce1bbc9b8f5","domain":"rishav.rt.gw","path":"/","expires":-1,"httpOnly":false,"secure":false,"sameSite":"Lax"},{"name":"wp_woocommerce_session_074e74b03d8a277981a80ce1db027a5e","value":"1%7C%7C1736450614%7C%7C1736447014%7C%7C7c59c08b6d7b7563919ffae1ab8cd75a","domain":"rishav.rt.gw","path":"/","expires":1736450614.108,"httpOnly":true,"secure":true,"sameSite":"Lax"}],"nonce":"056471df64","rootURL":"https://rishav.rt.gw/wp-json/"} \ No newline at end of file diff --git a/playwright.config.js b/playwright.config.js index 8177b5a..b26279a 100644 --- a/playwright.config.js +++ b/playwright.config.js @@ -56,15 +56,15 @@ module.exports = defineConfig({ use: { ...devices["Desktop Chrome"] }, }, - { - name: "firefox", - use: { ...devices["Desktop Firefox"] }, - }, + // { + // name: "firefox", + // use: { ...devices["Desktop Firefox"] }, + // }, - { - name: "webkit", - use: { ...devices["Desktop Safari"] }, - }, + // { + // name: "webkit", + // use: { ...devices["Desktop Safari"] }, + // }, /* Test against mobile viewports. */ // { diff --git a/specs/addTagCategoryToPrdt.spec.js b/specs/addTagCategoryToPrdt.spec.js index e69de29..5d5e8ce 100644 --- a/specs/addTagCategoryToPrdt.spec.js +++ b/specs/addTagCategoryToPrdt.spec.js @@ -0,0 +1,114 @@ +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); + + await assignTagCategory(page, catName, tagName); + + // await expect(categoryCheckBox).toBeChecked(); + + // 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/utils/e2eUtils/assignPrdtTagCategory.js b/utils/e2eUtils/assignPrdtTagCategory.js new file mode 100644 index 0000000..a4d86b9 --- /dev/null +++ b/utils/e2eUtils/assignPrdtTagCategory.js @@ -0,0 +1,28 @@ +/** + * 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, catName, tagName) => { + const categoryCheckBox = page.locator( + `//label[@class="selectit" and contains(text(),"${catName}")]//input[contains(@id,"in-product_cat-")]` + ); + await categoryCheckBox.check(); + + const tagOption = page.locator('//input[@id="new-tag-product_tag"]'); + await tagOption.fill(tagName); + + const addTagBtn = page.locator('//input[@value="Add"]'); + await addTagBtn.click(); + + // Locate the 'Update' button and click it to save the changes + const updateButton = page.locator('//input[@id="publish"]'); + await updateButton.click(); +}; + +module.exports = { + assignTagCategory +} From ba359c20eb7f0f92406e44069bc6c4c05d984f50 Mon Sep 17 00:00:00 2001 From: Rishav Dutta Date: Wed, 8 Jan 2025 12:04:45 +0530 Subject: [PATCH 23/27] update: update the test assertion for product visibility --- artifacts/storage-states/admin.json | 2 +- specs/publishProduct.spec.js | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/artifacts/storage-states/admin.json b/artifacts/storage-states/admin.json index e19e169..9bb2808 100644 --- a/artifacts/storage-states/admin.json +++ b/artifacts/storage-states/admin.json @@ -1 +1 @@ -{"cookies":[{"name":"wordpress_test_cookie","value":"WP%20Cookie%20check","domain":"rishav.rt.gw","path":"/","expires":-1,"httpOnly":false,"secure":true,"sameSite":"Lax"},{"name":"wordpress_sec_074e74b03d8a277981a80ce1db027a5e","value":"wp-user-4w8k6z%7C1736450613%7CIaAhbRf34oCILTw99kMDEGaQH56CtQaPUgU017I6eqD%7C79e249e73914fe0cd6f954711286e6a830112bc80e1ddc5a60bba16bbd019aca","domain":"rishav.rt.gw","path":"/wp-content/plugins","expires":-1,"httpOnly":true,"secure":true,"sameSite":"Lax"},{"name":"wordpress_sec_074e74b03d8a277981a80ce1db027a5e","value":"wp-user-4w8k6z%7C1736450613%7CIaAhbRf34oCILTw99kMDEGaQH56CtQaPUgU017I6eqD%7C79e249e73914fe0cd6f954711286e6a830112bc80e1ddc5a60bba16bbd019aca","domain":"rishav.rt.gw","path":"/wp-admin","expires":-1,"httpOnly":true,"secure":true,"sameSite":"Lax"},{"name":"wordpress_logged_in_074e74b03d8a277981a80ce1db027a5e","value":"wp-user-4w8k6z%7C1736450613%7CIaAhbRf34oCILTw99kMDEGaQH56CtQaPUgU017I6eqD%7C39a976dae6a75ccae8e86e59e6da19ff332aae943da435a9d40542cc155e2c5c","domain":"rishav.rt.gw","path":"/","expires":-1,"httpOnly":true,"secure":true,"sameSite":"Lax"},{"name":"tk_ai","value":"woo%3Ar7yWZheRH%2BgrBhJayxfsytUU","domain":"rishav.rt.gw","path":"/","expires":-1,"httpOnly":false,"secure":false,"sameSite":"Lax"},{"name":"wp-settings-1","value":"libraryContent%3Dbrowse%26editor%3Dtinymce","domain":"rishav.rt.gw","path":"/","expires":1767813814.799,"httpOnly":false,"secure":true,"sameSite":"Lax"},{"name":"wp-settings-time-1","value":"1736277814","domain":"rishav.rt.gw","path":"/","expires":1767813814.799,"httpOnly":false,"secure":true,"sameSite":"Lax"},{"name":"woocommerce_items_in_cart","value":"1","domain":"rishav.rt.gw","path":"/","expires":-1,"httpOnly":false,"secure":false,"sameSite":"Lax"},{"name":"woocommerce_cart_hash","value":"ca1e57ca31bf215971405ce1bbc9b8f5","domain":"rishav.rt.gw","path":"/","expires":-1,"httpOnly":false,"secure":false,"sameSite":"Lax"},{"name":"wp_woocommerce_session_074e74b03d8a277981a80ce1db027a5e","value":"1%7C%7C1736450614%7C%7C1736447014%7C%7C7c59c08b6d7b7563919ffae1ab8cd75a","domain":"rishav.rt.gw","path":"/","expires":1736450614.108,"httpOnly":true,"secure":true,"sameSite":"Lax"}],"nonce":"056471df64","rootURL":"https://rishav.rt.gw/wp-json/"} \ No newline at end of file +{"cookies":[{"name":"wordpress_test_cookie","value":"WP%20Cookie%20check","domain":"rishav.rt.gw","path":"/","expires":-1,"httpOnly":false,"secure":true,"sameSite":"Lax"},{"name":"wordpress_sec_074e74b03d8a277981a80ce1db027a5e","value":"wp-user-4w8k6z%7C1736490826%7CzMSPsdLu6CICEuXtNqFvdFkF4W3lHk9zwYLWRfc2laJ%7Ca69bc14cc5a0ddb0df328b82160ddceaa12eeebde438f51a3d7a79de1e007e0e","domain":"rishav.rt.gw","path":"/wp-content/plugins","expires":-1,"httpOnly":true,"secure":true,"sameSite":"Lax"},{"name":"wordpress_sec_074e74b03d8a277981a80ce1db027a5e","value":"wp-user-4w8k6z%7C1736490826%7CzMSPsdLu6CICEuXtNqFvdFkF4W3lHk9zwYLWRfc2laJ%7Ca69bc14cc5a0ddb0df328b82160ddceaa12eeebde438f51a3d7a79de1e007e0e","domain":"rishav.rt.gw","path":"/wp-admin","expires":-1,"httpOnly":true,"secure":true,"sameSite":"Lax"},{"name":"wordpress_logged_in_074e74b03d8a277981a80ce1db027a5e","value":"wp-user-4w8k6z%7C1736490826%7CzMSPsdLu6CICEuXtNqFvdFkF4W3lHk9zwYLWRfc2laJ%7C145a37ecd790382e0d7f541e17656bf9dae1ea0e6d518c2e56e20d66a9735549","domain":"rishav.rt.gw","path":"/","expires":-1,"httpOnly":true,"secure":true,"sameSite":"Lax"},{"name":"tk_ai","value":"woo%3Ar7yWZheRH%2BgrBhJayxfsytUU","domain":"rishav.rt.gw","path":"/","expires":-1,"httpOnly":false,"secure":false,"sameSite":"Lax"},{"name":"wp-settings-1","value":"libraryContent%3Dbrowse%26editor%3Dtinymce","domain":"rishav.rt.gw","path":"/","expires":1767854027.053,"httpOnly":false,"secure":true,"sameSite":"Lax"},{"name":"wp-settings-time-1","value":"1736318026","domain":"rishav.rt.gw","path":"/","expires":1767854027.053,"httpOnly":false,"secure":true,"sameSite":"Lax"}],"nonce":"88eb29d688","rootURL":"https://rishav.rt.gw/wp-json/"} \ No newline at end of file diff --git a/specs/publishProduct.spec.js b/specs/publishProduct.spec.js index 34f2c3d..673fad9 100644 --- a/specs/publishProduct.spec.js +++ b/specs/publishProduct.spec.js @@ -71,8 +71,11 @@ test.describe('It should verify the verify the visibility of products', ()=>{ 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(page).toHaveTitle(`${productTitle} – rishav.rt.gw`); + await expect(productPageTitle).toContain(productTitle); }) From c4da181c80833fd02eaf5eee90931beab5c5e6d7 Mon Sep 17 00:00:00 2001 From: Rishav Dutta Date: Wed, 8 Jan 2025 13:23:55 +0530 Subject: [PATCH 24/27] update: update the test assertions for assign tag & category test scripts --- artifacts/storage-states/admin.json | 2 +- specs/addTagCategoryToPrdt.spec.js | 14 +++++++++++--- utils/e2eUtils/assignPrdtTagCategory.js | 11 +++++++---- 3 files changed, 19 insertions(+), 8 deletions(-) diff --git a/artifacts/storage-states/admin.json b/artifacts/storage-states/admin.json index 9bb2808..3d4ef3a 100644 --- a/artifacts/storage-states/admin.json +++ b/artifacts/storage-states/admin.json @@ -1 +1 @@ -{"cookies":[{"name":"wordpress_test_cookie","value":"WP%20Cookie%20check","domain":"rishav.rt.gw","path":"/","expires":-1,"httpOnly":false,"secure":true,"sameSite":"Lax"},{"name":"wordpress_sec_074e74b03d8a277981a80ce1db027a5e","value":"wp-user-4w8k6z%7C1736490826%7CzMSPsdLu6CICEuXtNqFvdFkF4W3lHk9zwYLWRfc2laJ%7Ca69bc14cc5a0ddb0df328b82160ddceaa12eeebde438f51a3d7a79de1e007e0e","domain":"rishav.rt.gw","path":"/wp-content/plugins","expires":-1,"httpOnly":true,"secure":true,"sameSite":"Lax"},{"name":"wordpress_sec_074e74b03d8a277981a80ce1db027a5e","value":"wp-user-4w8k6z%7C1736490826%7CzMSPsdLu6CICEuXtNqFvdFkF4W3lHk9zwYLWRfc2laJ%7Ca69bc14cc5a0ddb0df328b82160ddceaa12eeebde438f51a3d7a79de1e007e0e","domain":"rishav.rt.gw","path":"/wp-admin","expires":-1,"httpOnly":true,"secure":true,"sameSite":"Lax"},{"name":"wordpress_logged_in_074e74b03d8a277981a80ce1db027a5e","value":"wp-user-4w8k6z%7C1736490826%7CzMSPsdLu6CICEuXtNqFvdFkF4W3lHk9zwYLWRfc2laJ%7C145a37ecd790382e0d7f541e17656bf9dae1ea0e6d518c2e56e20d66a9735549","domain":"rishav.rt.gw","path":"/","expires":-1,"httpOnly":true,"secure":true,"sameSite":"Lax"},{"name":"tk_ai","value":"woo%3Ar7yWZheRH%2BgrBhJayxfsytUU","domain":"rishav.rt.gw","path":"/","expires":-1,"httpOnly":false,"secure":false,"sameSite":"Lax"},{"name":"wp-settings-1","value":"libraryContent%3Dbrowse%26editor%3Dtinymce","domain":"rishav.rt.gw","path":"/","expires":1767854027.053,"httpOnly":false,"secure":true,"sameSite":"Lax"},{"name":"wp-settings-time-1","value":"1736318026","domain":"rishav.rt.gw","path":"/","expires":1767854027.053,"httpOnly":false,"secure":true,"sameSite":"Lax"}],"nonce":"88eb29d688","rootURL":"https://rishav.rt.gw/wp-json/"} \ No newline at end of file +{"cookies":[{"name":"wordpress_test_cookie","value":"WP%20Cookie%20check","domain":"rishav.rt.gw","path":"/","expires":-1,"httpOnly":false,"secure":true,"sameSite":"Lax"},{"name":"wordpress_sec_074e74b03d8a277981a80ce1db027a5e","value":"wp-user-4w8k6z%7C1736495557%7CcOKQXt2kRuHMd8mG1aTDI3aEm1zp8bcV2aXEZo7fDNd%7C3509a609d877a3896bad3915cda8c5ac5579ac52574ace5306a276dba61df8cf","domain":"rishav.rt.gw","path":"/wp-content/plugins","expires":-1,"httpOnly":true,"secure":true,"sameSite":"Lax"},{"name":"wordpress_sec_074e74b03d8a277981a80ce1db027a5e","value":"wp-user-4w8k6z%7C1736495557%7CcOKQXt2kRuHMd8mG1aTDI3aEm1zp8bcV2aXEZo7fDNd%7C3509a609d877a3896bad3915cda8c5ac5579ac52574ace5306a276dba61df8cf","domain":"rishav.rt.gw","path":"/wp-admin","expires":-1,"httpOnly":true,"secure":true,"sameSite":"Lax"},{"name":"wordpress_logged_in_074e74b03d8a277981a80ce1db027a5e","value":"wp-user-4w8k6z%7C1736495557%7CcOKQXt2kRuHMd8mG1aTDI3aEm1zp8bcV2aXEZo7fDNd%7C6dba02e9915cc88ecee16e5847d8de48835bde801da2fef17412984cb6272f61","domain":"rishav.rt.gw","path":"/","expires":-1,"httpOnly":true,"secure":true,"sameSite":"Lax"},{"name":"tk_ai","value":"woo%3Ar7yWZheRH%2BgrBhJayxfsytUU","domain":"rishav.rt.gw","path":"/","expires":-1,"httpOnly":false,"secure":false,"sameSite":"Lax"},{"name":"wp-settings-1","value":"libraryContent%3Dbrowse%26editor%3Dtinymce","domain":"rishav.rt.gw","path":"/","expires":1767858758.343,"httpOnly":false,"secure":true,"sameSite":"Lax"},{"name":"wp-settings-time-1","value":"1736322758","domain":"rishav.rt.gw","path":"/","expires":1767858758.343,"httpOnly":false,"secure":true,"sameSite":"Lax"}],"nonce":"0e7fec4623","rootURL":"https://rishav.rt.gw/wp-json/"} \ No newline at end of file diff --git a/specs/addTagCategoryToPrdt.spec.js b/specs/addTagCategoryToPrdt.spec.js index 5d5e8ce..a3a1051 100644 --- a/specs/addTagCategoryToPrdt.spec.js +++ b/specs/addTagCategoryToPrdt.spec.js @@ -79,11 +79,19 @@ test.describe('It should assign category and tag to the product', ()=>{ // Add a new product using the utility function await addNewProduct(admin, page, productTitle, productDescription); - await assignTagCategory(page, catName, tagName); + // 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); - // await expect(categoryCheckBox).toBeChecked(); + // Assert the specific category is assigned + await expect(categoryCheckBox).toBeChecked(); - // await expect(page.locator(`//ul[@class="tagchecklist"]//li`)).toBeVisible(); + // 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(); diff --git a/utils/e2eUtils/assignPrdtTagCategory.js b/utils/e2eUtils/assignPrdtTagCategory.js index a4d86b9..fc2c8b9 100644 --- a/utils/e2eUtils/assignPrdtTagCategory.js +++ b/utils/e2eUtils/assignPrdtTagCategory.js @@ -6,18 +6,21 @@ * @param {string} tagName - Tag Name */ -const assignTagCategory = async (page, catName, tagName) => { - const categoryCheckBox = page.locator( - `//label[@class="selectit" and contains(text(),"${catName}")]//input[contains(@id,"in-product_cat-")]` - ); +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(); From 20cfed997df6cc6cb5277a842f5769e61091cdf8 Mon Sep 17 00:00:00 2001 From: Rishav Dutta Date: Mon, 13 Jan 2025 10:01:44 +0530 Subject: [PATCH 25/27] refcator: add artifacts in .gitignore --- artifacts/storage-states/admin.json | 1 - tests-examples/demo-todo-app.spec.js | 449 --------------------------- 2 files changed, 450 deletions(-) delete mode 100644 artifacts/storage-states/admin.json delete mode 100644 tests-examples/demo-todo-app.spec.js diff --git a/artifacts/storage-states/admin.json b/artifacts/storage-states/admin.json deleted file mode 100644 index 3d4ef3a..0000000 --- a/artifacts/storage-states/admin.json +++ /dev/null @@ -1 +0,0 @@ -{"cookies":[{"name":"wordpress_test_cookie","value":"WP%20Cookie%20check","domain":"rishav.rt.gw","path":"/","expires":-1,"httpOnly":false,"secure":true,"sameSite":"Lax"},{"name":"wordpress_sec_074e74b03d8a277981a80ce1db027a5e","value":"wp-user-4w8k6z%7C1736495557%7CcOKQXt2kRuHMd8mG1aTDI3aEm1zp8bcV2aXEZo7fDNd%7C3509a609d877a3896bad3915cda8c5ac5579ac52574ace5306a276dba61df8cf","domain":"rishav.rt.gw","path":"/wp-content/plugins","expires":-1,"httpOnly":true,"secure":true,"sameSite":"Lax"},{"name":"wordpress_sec_074e74b03d8a277981a80ce1db027a5e","value":"wp-user-4w8k6z%7C1736495557%7CcOKQXt2kRuHMd8mG1aTDI3aEm1zp8bcV2aXEZo7fDNd%7C3509a609d877a3896bad3915cda8c5ac5579ac52574ace5306a276dba61df8cf","domain":"rishav.rt.gw","path":"/wp-admin","expires":-1,"httpOnly":true,"secure":true,"sameSite":"Lax"},{"name":"wordpress_logged_in_074e74b03d8a277981a80ce1db027a5e","value":"wp-user-4w8k6z%7C1736495557%7CcOKQXt2kRuHMd8mG1aTDI3aEm1zp8bcV2aXEZo7fDNd%7C6dba02e9915cc88ecee16e5847d8de48835bde801da2fef17412984cb6272f61","domain":"rishav.rt.gw","path":"/","expires":-1,"httpOnly":true,"secure":true,"sameSite":"Lax"},{"name":"tk_ai","value":"woo%3Ar7yWZheRH%2BgrBhJayxfsytUU","domain":"rishav.rt.gw","path":"/","expires":-1,"httpOnly":false,"secure":false,"sameSite":"Lax"},{"name":"wp-settings-1","value":"libraryContent%3Dbrowse%26editor%3Dtinymce","domain":"rishav.rt.gw","path":"/","expires":1767858758.343,"httpOnly":false,"secure":true,"sameSite":"Lax"},{"name":"wp-settings-time-1","value":"1736322758","domain":"rishav.rt.gw","path":"/","expires":1767858758.343,"httpOnly":false,"secure":true,"sameSite":"Lax"}],"nonce":"0e7fec4623","rootURL":"https://rishav.rt.gw/wp-json/"} \ No newline at end of file diff --git a/tests-examples/demo-todo-app.spec.js b/tests-examples/demo-todo-app.spec.js deleted file mode 100644 index e2eb87c..0000000 --- a/tests-examples/demo-todo-app.spec.js +++ /dev/null @@ -1,449 +0,0 @@ -// @ts-check -const { test, expect } = require('@playwright/test'); - -test.beforeEach(async ({ page }) => { - await page.goto('https://demo.playwright.dev/todomvc'); -}); - -const TODO_ITEMS = [ - 'buy some cheese', - 'feed the cat', - 'book a doctors appointment' -]; - -test.describe('New Todo', () => { - test('should allow me to add todo items', async ({ page }) => { - // create a new todo locator - const newTodo = page.getByPlaceholder('What needs to be done?'); - - // Create 1st todo. - await newTodo.fill(TODO_ITEMS[0]); - await newTodo.press('Enter'); - - // Make sure the list only has one todo item. - await expect(page.getByTestId('todo-title')).toHaveText([ - TODO_ITEMS[0] - ]); - - // Create 2nd todo. - await newTodo.fill(TODO_ITEMS[1]); - await newTodo.press('Enter'); - - // Make sure the list now has two todo items. - await expect(page.getByTestId('todo-title')).toHaveText([ - TODO_ITEMS[0], - TODO_ITEMS[1] - ]); - - await checkNumberOfTodosInLocalStorage(page, 2); - }); - - test('should clear text input field when an item is added', async ({ page }) => { - // create a new todo locator - const newTodo = page.getByPlaceholder('What needs to be done?'); - - // Create one todo item. - await newTodo.fill(TODO_ITEMS[0]); - await newTodo.press('Enter'); - - // Check that input is empty. - await expect(newTodo).toBeEmpty(); - await checkNumberOfTodosInLocalStorage(page, 1); - }); - - test('should append new items to the bottom of the list', async ({ page }) => { - // Create 3 items. - await createDefaultTodos(page); - - // create a todo count locator - const todoCount = page.getByTestId('todo-count') - - // Check test using different methods. - await expect(page.getByText('3 items left')).toBeVisible(); - await expect(todoCount).toHaveText('3 items left'); - await expect(todoCount).toContainText('3'); - await expect(todoCount).toHaveText(/3/); - - // Check all items in one call. - await expect(page.getByTestId('todo-title')).toHaveText(TODO_ITEMS); - await checkNumberOfTodosInLocalStorage(page, 3); - }); -}); - -test.describe('Mark all as completed', () => { - test.beforeEach(async ({ page }) => { - await createDefaultTodos(page); - await checkNumberOfTodosInLocalStorage(page, 3); - }); - - test.afterEach(async ({ page }) => { - await checkNumberOfTodosInLocalStorage(page, 3); - }); - - test('should allow me to mark all items as completed', async ({ page }) => { - // Complete all todos. - await page.getByLabel('Mark all as complete').check(); - - // Ensure all todos have 'completed' class. - await expect(page.getByTestId('todo-item')).toHaveClass(['completed', 'completed', 'completed']); - await checkNumberOfCompletedTodosInLocalStorage(page, 3); - }); - - test('should allow me to clear the complete state of all items', async ({ page }) => { - const toggleAll = page.getByLabel('Mark all as complete'); - // Check and then immediately uncheck. - await toggleAll.check(); - await toggleAll.uncheck(); - - // Should be no completed classes. - await expect(page.getByTestId('todo-item')).toHaveClass(['', '', '']); - }); - - test('complete all checkbox should update state when items are completed / cleared', async ({ page }) => { - const toggleAll = page.getByLabel('Mark all as complete'); - await toggleAll.check(); - await expect(toggleAll).toBeChecked(); - await checkNumberOfCompletedTodosInLocalStorage(page, 3); - - // Uncheck first todo. - const firstTodo = page.getByTestId('todo-item').nth(0); - await firstTodo.getByRole('checkbox').uncheck(); - - // Reuse toggleAll locator and make sure its not checked. - await expect(toggleAll).not.toBeChecked(); - - await firstTodo.getByRole('checkbox').check(); - await checkNumberOfCompletedTodosInLocalStorage(page, 3); - - // Assert the toggle all is checked again. - await expect(toggleAll).toBeChecked(); - }); -}); - -test.describe('Item', () => { - - test('should allow me to mark items as complete', async ({ page }) => { - // create a new todo locator - const newTodo = page.getByPlaceholder('What needs to be done?'); - - // Create two items. - for (const item of TODO_ITEMS.slice(0, 2)) { - await newTodo.fill(item); - await newTodo.press('Enter'); - } - - // Check first item. - const firstTodo = page.getByTestId('todo-item').nth(0); - await firstTodo.getByRole('checkbox').check(); - await expect(firstTodo).toHaveClass('completed'); - - // Check second item. - const secondTodo = page.getByTestId('todo-item').nth(1); - await expect(secondTodo).not.toHaveClass('completed'); - await secondTodo.getByRole('checkbox').check(); - - // Assert completed class. - await expect(firstTodo).toHaveClass('completed'); - await expect(secondTodo).toHaveClass('completed'); - }); - - test('should allow me to un-mark items as complete', async ({ page }) => { - // create a new todo locator - const newTodo = page.getByPlaceholder('What needs to be done?'); - - // Create two items. - for (const item of TODO_ITEMS.slice(0, 2)) { - await newTodo.fill(item); - await newTodo.press('Enter'); - } - - const firstTodo = page.getByTestId('todo-item').nth(0); - const secondTodo = page.getByTestId('todo-item').nth(1); - const firstTodoCheckbox = firstTodo.getByRole('checkbox'); - - await firstTodoCheckbox.check(); - await expect(firstTodo).toHaveClass('completed'); - await expect(secondTodo).not.toHaveClass('completed'); - await checkNumberOfCompletedTodosInLocalStorage(page, 1); - - await firstTodoCheckbox.uncheck(); - await expect(firstTodo).not.toHaveClass('completed'); - await expect(secondTodo).not.toHaveClass('completed'); - await checkNumberOfCompletedTodosInLocalStorage(page, 0); - }); - - test('should allow me to edit an item', async ({ page }) => { - await createDefaultTodos(page); - - const todoItems = page.getByTestId('todo-item'); - const secondTodo = todoItems.nth(1); - await secondTodo.dblclick(); - await expect(secondTodo.getByRole('textbox', { name: 'Edit' })).toHaveValue(TODO_ITEMS[1]); - await secondTodo.getByRole('textbox', { name: 'Edit' }).fill('buy some sausages'); - await secondTodo.getByRole('textbox', { name: 'Edit' }).press('Enter'); - - // Explicitly assert the new text value. - await expect(todoItems).toHaveText([ - TODO_ITEMS[0], - 'buy some sausages', - TODO_ITEMS[2] - ]); - await checkTodosInLocalStorage(page, 'buy some sausages'); - }); -}); - -test.describe('Editing', () => { - test.beforeEach(async ({ page }) => { - await createDefaultTodos(page); - await checkNumberOfTodosInLocalStorage(page, 3); - }); - - test('should hide other controls when editing', async ({ page }) => { - const todoItem = page.getByTestId('todo-item').nth(1); - await todoItem.dblclick(); - await expect(todoItem.getByRole('checkbox')).not.toBeVisible(); - await expect(todoItem.locator('label', { - hasText: TODO_ITEMS[1], - })).not.toBeVisible(); - await checkNumberOfTodosInLocalStorage(page, 3); - }); - - test('should save edits on blur', async ({ page }) => { - const todoItems = page.getByTestId('todo-item'); - await todoItems.nth(1).dblclick(); - await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill('buy some sausages'); - await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).dispatchEvent('blur'); - - await expect(todoItems).toHaveText([ - TODO_ITEMS[0], - 'buy some sausages', - TODO_ITEMS[2], - ]); - await checkTodosInLocalStorage(page, 'buy some sausages'); - }); - - test('should trim entered text', async ({ page }) => { - const todoItems = page.getByTestId('todo-item'); - await todoItems.nth(1).dblclick(); - await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill(' buy some sausages '); - await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).press('Enter'); - - await expect(todoItems).toHaveText([ - TODO_ITEMS[0], - 'buy some sausages', - TODO_ITEMS[2], - ]); - await checkTodosInLocalStorage(page, 'buy some sausages'); - }); - - test('should remove the item if an empty text string was entered', async ({ page }) => { - const todoItems = page.getByTestId('todo-item'); - await todoItems.nth(1).dblclick(); - await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill(''); - await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).press('Enter'); - - await expect(todoItems).toHaveText([ - TODO_ITEMS[0], - TODO_ITEMS[2], - ]); - }); - - test('should cancel edits on escape', async ({ page }) => { - const todoItems = page.getByTestId('todo-item'); - await todoItems.nth(1).dblclick(); - await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill('buy some sausages'); - await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).press('Escape'); - await expect(todoItems).toHaveText(TODO_ITEMS); - }); -}); - -test.describe('Counter', () => { - test('should display the current number of todo items', async ({ page }) => { - // create a new todo locator - const newTodo = page.getByPlaceholder('What needs to be done?'); - - // create a todo count locator - const todoCount = page.getByTestId('todo-count') - - await newTodo.fill(TODO_ITEMS[0]); - await newTodo.press('Enter'); - await expect(todoCount).toContainText('1'); - - await newTodo.fill(TODO_ITEMS[1]); - await newTodo.press('Enter'); - await expect(todoCount).toContainText('2'); - - await checkNumberOfTodosInLocalStorage(page, 2); - }); -}); - -test.describe('Clear completed button', () => { - test.beforeEach(async ({ page }) => { - await createDefaultTodos(page); - }); - - test('should display the correct text', async ({ page }) => { - await page.locator('.todo-list li .toggle').first().check(); - await expect(page.getByRole('button', { name: 'Clear completed' })).toBeVisible(); - }); - - test('should remove completed items when clicked', async ({ page }) => { - const todoItems = page.getByTestId('todo-item'); - await todoItems.nth(1).getByRole('checkbox').check(); - await page.getByRole('button', { name: 'Clear completed' }).click(); - await expect(todoItems).toHaveCount(2); - await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[2]]); - }); - - test('should be hidden when there are no items that are completed', async ({ page }) => { - await page.locator('.todo-list li .toggle').first().check(); - await page.getByRole('button', { name: 'Clear completed' }).click(); - await expect(page.getByRole('button', { name: 'Clear completed' })).toBeHidden(); - }); -}); - -test.describe('Persistence', () => { - test('should persist its data', async ({ page }) => { - // create a new todo locator - const newTodo = page.getByPlaceholder('What needs to be done?'); - - for (const item of TODO_ITEMS.slice(0, 2)) { - await newTodo.fill(item); - await newTodo.press('Enter'); - } - - const todoItems = page.getByTestId('todo-item'); - const firstTodoCheck = todoItems.nth(0).getByRole('checkbox'); - await firstTodoCheck.check(); - await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[1]]); - await expect(firstTodoCheck).toBeChecked(); - await expect(todoItems).toHaveClass(['completed', '']); - - // Ensure there is 1 completed item. - await checkNumberOfCompletedTodosInLocalStorage(page, 1); - - // Now reload. - await page.reload(); - await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[1]]); - await expect(firstTodoCheck).toBeChecked(); - await expect(todoItems).toHaveClass(['completed', '']); - }); -}); - -test.describe('Routing', () => { - test.beforeEach(async ({ page }) => { - await createDefaultTodos(page); - // make sure the app had a chance to save updated todos in storage - // before navigating to a new view, otherwise the items can get lost :( - // in some frameworks like Durandal - await checkTodosInLocalStorage(page, TODO_ITEMS[0]); - }); - - test('should allow me to display active items', async ({ page }) => { - const todoItem = page.getByTestId('todo-item'); - await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check(); - - await checkNumberOfCompletedTodosInLocalStorage(page, 1); - await page.getByRole('link', { name: 'Active' }).click(); - await expect(todoItem).toHaveCount(2); - await expect(todoItem).toHaveText([TODO_ITEMS[0], TODO_ITEMS[2]]); - }); - - test('should respect the back button', async ({ page }) => { - const todoItem = page.getByTestId('todo-item'); - await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check(); - - await checkNumberOfCompletedTodosInLocalStorage(page, 1); - - await test.step('Showing all items', async () => { - await page.getByRole('link', { name: 'All' }).click(); - await expect(todoItem).toHaveCount(3); - }); - - await test.step('Showing active items', async () => { - await page.getByRole('link', { name: 'Active' }).click(); - }); - - await test.step('Showing completed items', async () => { - await page.getByRole('link', { name: 'Completed' }).click(); - }); - - await expect(todoItem).toHaveCount(1); - await page.goBack(); - await expect(todoItem).toHaveCount(2); - await page.goBack(); - await expect(todoItem).toHaveCount(3); - }); - - test('should allow me to display completed items', async ({ page }) => { - await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check(); - await checkNumberOfCompletedTodosInLocalStorage(page, 1); - await page.getByRole('link', { name: 'Completed' }).click(); - await expect(page.getByTestId('todo-item')).toHaveCount(1); - }); - - test('should allow me to display all items', async ({ page }) => { - await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check(); - await checkNumberOfCompletedTodosInLocalStorage(page, 1); - await page.getByRole('link', { name: 'Active' }).click(); - await page.getByRole('link', { name: 'Completed' }).click(); - await page.getByRole('link', { name: 'All' }).click(); - await expect(page.getByTestId('todo-item')).toHaveCount(3); - }); - - test('should highlight the currently applied filter', async ({ page }) => { - await expect(page.getByRole('link', { name: 'All' })).toHaveClass('selected'); - - //create locators for active and completed links - const activeLink = page.getByRole('link', { name: 'Active' }); - const completedLink = page.getByRole('link', { name: 'Completed' }); - await activeLink.click(); - - // Page change - active items. - await expect(activeLink).toHaveClass('selected'); - await completedLink.click(); - - // Page change - completed items. - await expect(completedLink).toHaveClass('selected'); - }); -}); - -async function createDefaultTodos(page) { - // create a new todo locator - const newTodo = page.getByPlaceholder('What needs to be done?'); - - for (const item of TODO_ITEMS) { - await newTodo.fill(item); - await newTodo.press('Enter'); - } -} - -/** - * @param {import('@playwright/test').Page} page - * @param {number} expected - */ - async function checkNumberOfTodosInLocalStorage(page, expected) { - return await page.waitForFunction(e => { - return JSON.parse(localStorage['react-todos']).length === e; - }, expected); -} - -/** - * @param {import('@playwright/test').Page} page - * @param {number} expected - */ - async function checkNumberOfCompletedTodosInLocalStorage(page, expected) { - return await page.waitForFunction(e => { - return JSON.parse(localStorage['react-todos']).filter(i => i.completed).length === e; - }, expected); -} - -/** - * @param {import('@playwright/test').Page} page - * @param {string} title - */ -async function checkTodosInLocalStorage(page, title) { - return await page.waitForFunction(t => { - return JSON.parse(localStorage['react-todos']).map(i => i.title).includes(t); - }, title); -} From 0f4c0790fc6edd5ef394ca173fdc01037977bafe Mon Sep 17 00:00:00 2001 From: Rishav Dutta Date: Mon, 13 Jan 2025 10:13:35 +0530 Subject: [PATCH 26/27] update: rename the test files --- specs/{addCategory.spec.js => add-category.spec.js} | 0 specs/{addCoupon.spec.js => add-coupon.spec.js} | 0 ...{addPricingInventory.spec.js => add-pricing-inventory.spec.js} | 0 specs/{addProductImage.spec.js => add-product-image.spec.js} | 0 ...TagCategoryToPrdt.spec.js => add-tag-category-to-prdt.spec.js} | 0 specs/{addTag.spec.js => add-tag-spec.js} | 0 specs/{addUserCustomer.spec.js => add-user-customer.spec.js} | 0 .../{checkoutPlaceOrder.spec.js => checkout-place-order.spec.js} | 0 ...heckoutVerifyCoupon.spec.js => checkout-verify-coupon.spec.js} | 0 ...{createSimpleProduct.spec.js => create-simple-product.spec.js} | 0 specs/{publishProduct.spec.js => publish-product.spec.js} | 0 specs/{reviewOrder.spec.js => review-order.spec.js} | 0 12 files changed, 0 insertions(+), 0 deletions(-) rename specs/{addCategory.spec.js => add-category.spec.js} (100%) rename specs/{addCoupon.spec.js => add-coupon.spec.js} (100%) rename specs/{addPricingInventory.spec.js => add-pricing-inventory.spec.js} (100%) rename specs/{addProductImage.spec.js => add-product-image.spec.js} (100%) rename specs/{addTagCategoryToPrdt.spec.js => add-tag-category-to-prdt.spec.js} (100%) rename specs/{addTag.spec.js => add-tag-spec.js} (100%) rename specs/{addUserCustomer.spec.js => add-user-customer.spec.js} (100%) rename specs/{checkoutPlaceOrder.spec.js => checkout-place-order.spec.js} (100%) rename specs/{checkoutVerifyCoupon.spec.js => checkout-verify-coupon.spec.js} (100%) rename specs/{createSimpleProduct.spec.js => create-simple-product.spec.js} (100%) rename specs/{publishProduct.spec.js => publish-product.spec.js} (100%) rename specs/{reviewOrder.spec.js => review-order.spec.js} (100%) diff --git a/specs/addCategory.spec.js b/specs/add-category.spec.js similarity index 100% rename from specs/addCategory.spec.js rename to specs/add-category.spec.js diff --git a/specs/addCoupon.spec.js b/specs/add-coupon.spec.js similarity index 100% rename from specs/addCoupon.spec.js rename to specs/add-coupon.spec.js diff --git a/specs/addPricingInventory.spec.js b/specs/add-pricing-inventory.spec.js similarity index 100% rename from specs/addPricingInventory.spec.js rename to specs/add-pricing-inventory.spec.js diff --git a/specs/addProductImage.spec.js b/specs/add-product-image.spec.js similarity index 100% rename from specs/addProductImage.spec.js rename to specs/add-product-image.spec.js diff --git a/specs/addTagCategoryToPrdt.spec.js b/specs/add-tag-category-to-prdt.spec.js similarity index 100% rename from specs/addTagCategoryToPrdt.spec.js rename to specs/add-tag-category-to-prdt.spec.js diff --git a/specs/addTag.spec.js b/specs/add-tag-spec.js similarity index 100% rename from specs/addTag.spec.js rename to specs/add-tag-spec.js diff --git a/specs/addUserCustomer.spec.js b/specs/add-user-customer.spec.js similarity index 100% rename from specs/addUserCustomer.spec.js rename to specs/add-user-customer.spec.js diff --git a/specs/checkoutPlaceOrder.spec.js b/specs/checkout-place-order.spec.js similarity index 100% rename from specs/checkoutPlaceOrder.spec.js rename to specs/checkout-place-order.spec.js diff --git a/specs/checkoutVerifyCoupon.spec.js b/specs/checkout-verify-coupon.spec.js similarity index 100% rename from specs/checkoutVerifyCoupon.spec.js rename to specs/checkout-verify-coupon.spec.js diff --git a/specs/createSimpleProduct.spec.js b/specs/create-simple-product.spec.js similarity index 100% rename from specs/createSimpleProduct.spec.js rename to specs/create-simple-product.spec.js diff --git a/specs/publishProduct.spec.js b/specs/publish-product.spec.js similarity index 100% rename from specs/publishProduct.spec.js rename to specs/publish-product.spec.js diff --git a/specs/reviewOrder.spec.js b/specs/review-order.spec.js similarity index 100% rename from specs/reviewOrder.spec.js rename to specs/review-order.spec.js From 71cf4309074eddfcdeb2ec78812dbfc69c9367fa Mon Sep 17 00:00:00 2001 From: Rishav Dutta Date: Mon, 13 Jan 2025 10:24:03 +0530 Subject: [PATCH 27/27] fix: fix the file name of add-tag test script file --- specs/{add-tag-spec.js => add-tag.spec.js} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename specs/{add-tag-spec.js => add-tag.spec.js} (100%) diff --git a/specs/add-tag-spec.js b/specs/add-tag.spec.js similarity index 100% rename from specs/add-tag-spec.js rename to specs/add-tag.spec.js