From 372903b306c50c7f3959c5508e09e364681ac713 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 26 Mar 2026 07:16:09 +0000 Subject: [PATCH 1/2] Add Playwright E2E tests for frontend components Sets up a complete E2E test suite using Playwright to cover the plugin's frontend JavaScript behaviour without requiring a WordPress install. Tests run against standalone HTML fixtures that load the actual plugin JS files. - Install @playwright/test and serve as dev dependencies - Add playwright.config.ts with a static file server (serve) - Add tsconfig.json for TypeScript test support - Add HTML fixtures: accordion, reading-progress, counter, sticky-column, back-button - Add test specs (25 tests across 5 files) covering: * Accordion open/close, aria-expanded, multiple items * Reading progress bar initialisation, scroll update, 100% cap * Counter animation target value, prefix/suffix, non-zero start * Sticky column activation/deactivation on scroll, top-offset * Back button visibility logic based on navigation history and scroll - Add npm scripts: test:e2e, test:e2e:ui, test:e2e:headed - Add .github/workflows/e2e.yml CI workflow (runs on PRs to trunk) - Update .gitignore to exclude Playwright report/result artefacts Closes #125 https://claude.ai/code/session_01Nyp3kpBW1eYMQj3mjEubjx --- .github/workflows/e2e.yml | 44 ++ .gitignore | 5 + package-lock.json | 907 ++++++++++++++++++++++- package.json | 10 +- playwright.config.ts | 47 ++ tests/e2e/accordion.spec.ts | 76 ++ tests/e2e/back-button.spec.ts | 64 ++ tests/e2e/counter.spec.ts | 69 ++ tests/e2e/fixtures/accordion.html | 40 + tests/e2e/fixtures/back-button.html | 35 + tests/e2e/fixtures/counter.html | 37 + tests/e2e/fixtures/reading-progress.html | 41 + tests/e2e/fixtures/sticky-column.html | 49 ++ tests/e2e/reading-progress.spec.ts | 57 ++ tests/e2e/sticky-column.spec.ts | 51 ++ tsconfig.json | 20 + 16 files changed, 1537 insertions(+), 15 deletions(-) create mode 100644 .github/workflows/e2e.yml create mode 100644 playwright.config.ts create mode 100644 tests/e2e/accordion.spec.ts create mode 100644 tests/e2e/back-button.spec.ts create mode 100644 tests/e2e/counter.spec.ts create mode 100644 tests/e2e/fixtures/accordion.html create mode 100644 tests/e2e/fixtures/back-button.html create mode 100644 tests/e2e/fixtures/counter.html create mode 100644 tests/e2e/fixtures/reading-progress.html create mode 100644 tests/e2e/fixtures/sticky-column.html create mode 100644 tests/e2e/reading-progress.spec.ts create mode 100644 tests/e2e/sticky-column.spec.ts create mode 100644 tsconfig.json diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml new file mode 100644 index 0000000..c0ccd6b --- /dev/null +++ b/.github/workflows/e2e.yml @@ -0,0 +1,44 @@ +name: E2E Tests + +on: + pull_request: + branches: + - trunk + - 'release/**' + - 'feature/**' + push: + branches: + - trunk + +jobs: + e2e: + name: Playwright E2E + runs-on: ubuntu-latest + timeout-minutes: 30 + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Install Playwright browsers + run: npx playwright install --with-deps chromium firefox + + - name: Run E2E tests + run: npm run test:e2e + + - name: Upload Playwright report + if: always() + uses: actions/upload-artifact@v4 + with: + name: playwright-report + path: tests/e2e/playwright-report/ + retention-days: 14 diff --git a/.gitignore b/.gitignore index 5640c2a..afe5f7d 100644 --- a/.gitignore +++ b/.gitignore @@ -40,3 +40,8 @@ node_modules/ # dotenv environment variables file .env + +# Playwright +tests/e2e/playwright-report/ +tests/e2e/test-results/ +tests/e2e/dist/ diff --git a/package-lock.json b/package-lock.json index 24002f4..7e3b8bd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "@babel/core": "^7.27.4", "@babel/preset-env": "^7.27.2", "@babel/preset-react": "^7.27.1", + "@playwright/test": "^1.58.2", "autoprefixer": "^10.4.20", "gulp": "^5.0.0", "gulp-clean-css": "^4.3.0", @@ -20,6 +21,7 @@ "merge-stream": "^2.0.0", "postcss": "^8.4.49", "postcss-cli": "^11.0.0", + "serve": "^14.2.6", "tailwindcss": "^3.4.15" } }, @@ -1943,6 +1945,29 @@ "node": ">=14" } }, + "node_modules/@playwright/test": { + "version": "1.58.2", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.58.2.tgz", + "integrity": "sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.58.2" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@zeit/schemas": { + "version": "2.36.0", + "resolved": "https://registry.npmjs.org/@zeit/schemas/-/schemas-2.36.0.tgz", + "integrity": "sha512-7kjMwcChYEzMKjeex9ZFXkt1AyNov9R5HZtjBKVsmVpw7pa7ZtlCGvCBC2vnnXctaYN+aRI61HjIqeetZW5ROg==", + "dev": true, + "license": "MIT" + }, "node_modules/acorn": { "version": "8.15.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", @@ -1956,6 +1981,33 @@ "node": ">=0.4.0" } }, + "node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-align": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.1.tgz", + "integrity": "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.1.0" + } + }, "node_modules/ansi-colors": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-1.1.0.tgz", @@ -2026,6 +2078,27 @@ "node": ">= 8" } }, + "node_modules/arch": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/arch/-/arch-2.2.0.tgz", + "integrity": "sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ==", + "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/arg": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", @@ -2274,6 +2347,127 @@ "readable-stream": "^3.4.0" } }, + "node_modules/boxen": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/boxen/-/boxen-7.0.0.tgz", + "integrity": "sha512-j//dBVuyacJbvW+tvZ9HuH03fZ46QcaKvvhZickZqtB271DxJ7SNRSNxrV/dZX0085m7hISRZWbzWlJvx/rHSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-align": "^3.0.1", + "camelcase": "^7.0.0", + "chalk": "^5.0.1", + "cli-boxes": "^3.0.0", + "string-width": "^5.1.2", + "type-fest": "^2.13.0", + "widest-line": "^4.0.1", + "wrap-ansi": "^8.0.1" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/boxen/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/boxen/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/boxen/node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/boxen/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/boxen/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/boxen/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/boxen/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -2363,6 +2557,29 @@ "dev": true, "license": "MIT" }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/camelcase": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-7.0.1.tgz", + "integrity": "sha512-xlx1yCK2Oc1APsPXDL2LdlNP6+uu8OCDdhOBSVT279M/S+y75O30C2VuD8T2ogdePBBl7PfPF4504tnLgX3zfw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/camelcase-css": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", @@ -2411,6 +2628,22 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/chalk-template": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/chalk-template/-/chalk-template-0.4.0.tgz", + "integrity": "sha512-/ghrgmhfY8RaSdeo43hNXxpoHAtxdbskUHjPpfqUWGttFgycUhYPGx3YZBCnUCvOa7Doivn1IZec3DEGFoMgLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/chalk-template?sponsor=1" + } + }, "node_modules/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", @@ -2449,6 +2682,37 @@ "node": ">= 4.0" } }, + "node_modules/cli-boxes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-3.0.0.tgz", + "integrity": "sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/clipboardy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/clipboardy/-/clipboardy-3.0.0.tgz", + "integrity": "sha512-Su+uU5sr1jkUy1sGRpLKjKrvEOVXgSgiSInwa/qeID6aJ07yh+5NWc3h2QfjHjBnfX4LhtFcuAWKUsJ3r+fjbg==", + "dev": true, + "license": "MIT", + "dependencies": { + "arch": "^2.2.0", + "execa": "^5.1.1", + "is-wsl": "^2.2.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/cliui": { "version": "7.0.4", "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", @@ -2505,6 +2769,55 @@ "dev": true, "license": "MIT" }, + "node_modules/compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": ">= 1.43.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/compression": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz", + "integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "compressible": "~2.0.18", + "debug": "2.6.9", + "negotiator": "~0.6.4", + "on-headers": "~1.1.0", + "safe-buffer": "5.2.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/compression/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/compression/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/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -2512,6 +2825,16 @@ "dev": true, "license": "MIT" }, + "node_modules/content-disposition": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz", + "integrity": "sha512-kRGRZw3bLlFISDBgwTSA1TMBFN6J6GWDeubmDE3AF+3+yXL8hTWv8r5rkLbqYXY4RjPk/EzHnClI3zQf1cFmHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", @@ -2609,6 +2932,16 @@ } } }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/dependency-graph": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/dependency-graph/-/dependency-graph-1.0.0.tgz", @@ -2708,6 +3041,37 @@ "node": ">=0.10.0" } }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/execa/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/expand-tilde": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-2.0.2.tgz", @@ -2742,6 +3106,13 @@ "node": ">=0.10.0" } }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "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", @@ -2776,6 +3147,23 @@ "fastest-levenshtein": "^1.0.7" } }, + "node_modules/fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/fastest-levenshtein": { "version": "1.0.16", "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", @@ -2994,6 +3382,19 @@ "node": "6.* || 8.* || >= 10.*" } }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -3262,6 +3663,16 @@ "node": ">=0.10.0" } }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, "node_modules/iconv-lite": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", @@ -3375,6 +3786,22 @@ "url": "https://github.com/sponsors/ljharb" } }, + "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-extendable": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", @@ -3464,6 +3891,19 @@ "node": ">=0.10.0" } }, + "node_modules/is-port-reachable": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-port-reachable/-/is-port-reachable-4.0.0.tgz", + "integrity": "sha512-9UoipoxYmSk6Xy7QFgRv2HDyaysmgSG75TFQs6S+3pDM7ZhKTF/bskZV+0UlABHzKjNVhPjYCLfeZUEg1wXxig==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-relative": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-relative/-/is-relative-1.0.0.tgz", @@ -3477,6 +3917,19 @@ "node": ">=0.10.0" } }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-unc-path": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-unc-path/-/is-unc-path-1.0.0.tgz", @@ -3510,6 +3963,19 @@ "node": ">=0.10.0" } }, + "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/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -3573,6 +4039,13 @@ "node": ">=6" } }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, "node_modules/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", @@ -3723,27 +4196,70 @@ "dev": true, "license": "MIT", "engines": { - "node": ">= 8" + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" } }, - "node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "node_modules/mime-types": { + "version": "2.1.18", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.18.tgz", + "integrity": "sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==", "dev": true, "license": "MIT", "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" + "mime-db": "~1.33.0" }, "engines": { - "node": ">=8.6" + "node": ">= 0.6" + } + }, + "node_modules/mime-types/node_modules/mime-db": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.33.0.tgz", + "integrity": "sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" } }, "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -3753,6 +4269,16 @@ "node": "*" } }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/minipass": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", @@ -3811,6 +4337,16 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/negotiator": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", + "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/node-releases": { "version": "2.0.19", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", @@ -3851,6 +4387,19 @@ "node": ">= 10.13.0" } }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -3900,6 +4449,16 @@ "node": ">=0.10.0" } }, + "node_modules/on-headers": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", + "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -3910,6 +4469,22 @@ "wrappy": "1" } }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/package-json-from-dist": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", @@ -3952,6 +4527,13 @@ "node": ">=0.10.0" } }, + "node_modules/path-is-inside": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", + "integrity": "sha512-DUWJr3+ULp4zXmol/SZkFf3JGsS9/SIv+Y3Rt93/UjPpDpklB5f1er4O3POIbUuUJ3FXgqte2Q7SrU6zAqwk8w==", + "dev": true, + "license": "(WTFPL OR MIT)" + }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -4016,6 +4598,13 @@ "dev": true, "license": "ISC" }, + "node_modules/path-to-regexp": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-3.3.0.tgz", + "integrity": "sha512-qyCH421YQPS2WFDxDjftfc1ZR5WKQzVzqsp4n9M2kQhVOo/ByahFoUNJfl58kOcEGfQ//7weFTDhm+ss8Ecxgw==", + "dev": true, + "license": "MIT" + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -4056,6 +4645,53 @@ "node": ">= 6" } }, + "node_modules/playwright": { + "version": "1.58.2", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.2.tgz", + "integrity": "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.58.2" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.58.2", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.2.tgz", + "integrity": "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/playwright/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/plugin-error": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/plugin-error/-/plugin-error-1.0.1.tgz", @@ -4383,6 +5019,32 @@ "dev": true, "license": "MIT" }, + "node_modules/range-parser": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz", + "integrity": "sha512-kA5WQoNVo4t9lNx2kQNFCxKeBl5IbbSNBl1M/tLkw9WCn+hxNBAW5Qh8gdhs63CJnhjJ2zQWFoqPJP2sK1AV5A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "dev": true, + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, "node_modules/react": { "version": "19.1.0", "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", @@ -4503,6 +5165,30 @@ "node": ">=4" } }, + "node_modules/registry-auth-token": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-3.3.2.tgz", + "integrity": "sha512-JL39c60XlzCVgNrO+qq68FoNb56w/m7JYvGR2jT5iR1xBrUA3Mfx5Twk5rqTThPmQKMWydGmq8oFtDlxfrmxnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "rc": "^1.1.6", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/registry-url": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/registry-url/-/registry-url-3.1.0.tgz", + "integrity": "sha512-ZbgR5aZEdf4UKZVBPYIgaglBmSF2Hi94s2PcIHhRGFjKYu+chjJdYfHn4rt3hB6eCKLJ8giVIIfgMa1ehDfZKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "rc": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/regjsgen": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.8.0.tgz", @@ -4573,6 +5259,16 @@ "node": ">=0.10.0" } }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/resolve": { "version": "1.22.10", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", @@ -4713,6 +5409,71 @@ "node": ">= 10.13.0" } }, + "node_modules/serve": { + "version": "14.2.6", + "resolved": "https://registry.npmjs.org/serve/-/serve-14.2.6.tgz", + "integrity": "sha512-QEjUSA+sD4Rotm1znR8s50YqA3kYpRGPmtd5GlFxbaL9n/FdUNbqMhxClqdditSk0LlZyA/dhud6XNRTOC9x2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@zeit/schemas": "2.36.0", + "ajv": "8.18.0", + "arg": "5.0.2", + "boxen": "7.0.0", + "chalk": "5.0.1", + "chalk-template": "0.4.0", + "clipboardy": "3.0.0", + "compression": "1.8.1", + "is-port-reachable": "4.0.0", + "serve-handler": "6.1.7", + "update-check": "1.5.4" + }, + "bin": { + "serve": "build/main.js" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/serve-handler": { + "version": "6.1.7", + "resolved": "https://registry.npmjs.org/serve-handler/-/serve-handler-6.1.7.tgz", + "integrity": "sha512-CinAq1xWb0vR3twAv9evEU8cNWkXCb9kd5ePAHUKJBkOsUpR1wt/CvGdeca7vqumL1U5cSaeVQ6zZMxiJ3yWsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "3.0.0", + "content-disposition": "0.5.2", + "mime-types": "2.1.18", + "minimatch": "3.1.5", + "path-is-inside": "1.0.2", + "path-to-regexp": "3.3.0", + "range-parser": "1.2.0" + } + }, + "node_modules/serve-handler/node_modules/bytes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", + "integrity": "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/serve/node_modules/chalk": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.0.1.tgz", + "integrity": "sha512-Fo07WOYGqMfCWHOzSXOt2CxDbC6skS/jO9ynEcmpANMoPrD+W1r1K6Vx7iNm+AQmETU1Xr2t+n8nzkV9t6xh3w==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -4900,6 +5661,26 @@ "node": ">=8" } }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/sucrase": { "version": "3.35.0", "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", @@ -5227,6 +6008,19 @@ "dev": true, "license": "Apache-2.0" }, + "node_modules/type-fest": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", + "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/unc-path-regex": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/unc-path-regex/-/unc-path-regex-0.1.2.tgz", @@ -5348,6 +6142,17 @@ "browserslist": ">= 4.21.0" } }, + "node_modules/update-check": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/update-check/-/update-check-1.5.4.tgz", + "integrity": "sha512-5YHsflzHP4t1G+8WGPlvKbJEbAJGCgw+Em+dGR1KmBUbr1J36SJBqlHLjR7oob7sco5hWHGQVcr9B2poIVDDTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "registry-auth-token": "3.3.2", + "registry-url": "3.1.0" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -5375,6 +6180,16 @@ "node": ">= 10.13.0" } }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/vinyl": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-3.0.0.tgz", @@ -5483,6 +6298,76 @@ "which": "bin/which" } }, + "node_modules/widest-line": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-4.0.1.tgz", + "integrity": "sha512-o0cyEG0e8GPzT4iGHphIOh0cJOV8fivsXxddQasHPHfoZf1ZexrfeA21w2NaEN1RHE+fXlfISmOE8R9N3u3Qig==", + "dev": true, + "license": "MIT", + "dependencies": { + "string-width": "^5.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/widest-line/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/widest-line/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/widest-line/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/widest-line/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, "node_modules/wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", diff --git a/package.json b/package.json index a9c432d..cf643d0 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,7 @@ "@babel/core": "^7.27.4", "@babel/preset-env": "^7.27.2", "@babel/preset-react": "^7.27.1", + "@playwright/test": "^1.58.2", "autoprefixer": "^10.4.20", "gulp": "^5.0.0", "gulp-clean-css": "^4.3.0", @@ -11,6 +12,7 @@ "merge-stream": "^2.0.0", "postcss": "^8.4.49", "postcss-cli": "^11.0.0", + "serve": "^14.2.6", "tailwindcss": "^3.4.15" }, "babel": { @@ -44,15 +46,15 @@ "build:product-categories": "babel assets/product-categories/frontblocks-product-categories-option.jsx --out-file assets/product-categories/frontblocks-product-categories.js", "build:reading-time": "babel assets/reading-time/frontblocks-reading-time-option.jsx --out-file assets/reading-time/frontblocks-reading-time.js", "build:stacked-images": "babel assets/stacked-images/frontblocks-stacked-images-option.jsx --out-file assets/stacked-images/frontblocks-stacked-images.js", - "build:edge-alignment": "babel assets/container-edge-alignment/frontblocks-edge-alignment-option.jsx --out-file assets/container-edge-alignment/frontblocks-edge-alignment.js", "build:shape-animations": "babel assets/shape-animations/frontblocks-shape-animation-option.jsx --out-file assets/shape-animations/frontblocks-shape-animation-option.js", "build:gf-inline": "babel assets/gravityforms-inline/frontblocks-gf-inline-option.jsx --out-file assets/gravityforms-inline/frontblocks-gf-inline-option.js", - "build:css": "postcss assets/admin/settings-src.css -o assets/admin/settings.css", "watch:css": "postcss assets/admin/settings-src.css -o assets/admin/settings.css --watch", - - "build": "npm run build:carousel && npm run build:animations && npm run build:sticky && npm run build:gallery && npm run build:post && npm run build:headline && npm run build:product-categories && npm run build:reading-time && npm run build:stacked-images && npm run build:edge-alignment && npm run build:shape-animations && npm run build:gf-inline && npm run build:css" + "build": "npm run build:carousel && npm run build:animations && npm run build:sticky && npm run build:gallery && npm run build:post && npm run build:headline && npm run build:product-categories && npm run build:reading-time && npm run build:stacked-images && npm run build:edge-alignment && npm run build:shape-animations && npm run build:gf-inline && npm run build:css", + "test:e2e": "playwright test", + "test:e2e:ui": "playwright test --ui", + "test:e2e:headed": "playwright test --headed" }, "dependencies": { "react": "^19.1.0", diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 0000000..b7e18c3 --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,47 @@ +import { defineConfig, devices } from '@playwright/test'; +import path from 'path'; + +/** + * Playwright E2E configuration for FrontBlocks plugin. + * + * Tests run against standalone HTML fixtures that load the plugin's actual + * JavaScript files directly — no WordPress install required. + */ +export default defineConfig({ + testDir: './tests/e2e', + fullyParallel: true, + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 2 : 0, + workers: process.env.CI ? 2 : undefined, + reporter: [ + ['html', { outputFolder: 'tests/e2e/playwright-report', open: 'never' }], + ['list'], + ], + + use: { + // Serve fixtures from the repo root so tests can reference assets with + // relative paths like /assets/accordion/frontblocks-accordion.js + baseURL: 'http://localhost:3737', + trace: 'on-first-retry', + screenshot: 'only-on-failure', + }, + + // Static file server — serves the whole repo (fixtures + assets). + webServer: { + command: 'npx serve . --listen 3737 --no-clipboard', + url: 'http://localhost:3737', + reuseExistingServer: !process.env.CI, + timeout: 30_000, + }, + + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + { + name: 'firefox', + use: { ...devices['Desktop Firefox'] }, + }, + ], +}); diff --git a/tests/e2e/accordion.spec.ts b/tests/e2e/accordion.spec.ts new file mode 100644 index 0000000..ab872ee --- /dev/null +++ b/tests/e2e/accordion.spec.ts @@ -0,0 +1,76 @@ +import { test, expect } from '@playwright/test'; + +test.describe( 'Accordion', () => { + test.beforeEach( async ( { page } ) => { + await page.goto( '/tests/e2e/fixtures/accordion.html' ); + } ); + + test( 'closed item starts hidden', async ( { page } ) => { + const content = page.locator( '#item-1 .gb-accordion__content' ); + await expect( content ).not.toBeVisible(); + } ); + + test( 'open item starts visible', async ( { page } ) => { + const content = page.locator( '#item-2 .gb-accordion__content' ); + await expect( content ).toBeVisible(); + } ); + + test( 'clicking toggle opens a closed item', async ( { page } ) => { + const toggle = page.locator( '#item-1 .gb-accordion__toggle' ); + const content = page.locator( '#item-1 .gb-accordion__content' ); + + await toggle.click(); + await expect( content ).toBeVisible(); + } ); + + test( 'clicking toggle closes an open item', async ( { page } ) => { + const toggle = page.locator( '#item-2 .gb-accordion__toggle' ); + const content = page.locator( '#item-2 .gb-accordion__content' ); + + await toggle.click(); + await expect( content ).not.toBeVisible(); + } ); + + test( 'toggle sets aria-expanded correctly when opening', async ( { page } ) => { + const toggle = page.locator( '#item-1 .gb-accordion__toggle' ); + + await toggle.click(); + await expect( toggle ).toHaveAttribute( 'aria-expanded', 'true' ); + } ); + + test( 'toggle sets aria-expanded correctly when closing', async ( { page } ) => { + const toggle = page.locator( '#item-2 .gb-accordion__toggle' ); + + await toggle.click(); + await expect( toggle ).toHaveAttribute( 'aria-expanded', 'false' ); + } ); + + test( 'toggle can open and close the same item multiple times', async ( { page } ) => { + const toggle = page.locator( '#item-3 .gb-accordion__toggle' ); + const content = page.locator( '#item-3 .gb-accordion__content' ); + + // Open. + await toggle.click(); + await expect( content ).toBeVisible(); + + // Close. + await toggle.click(); + await expect( content ).not.toBeVisible(); + + // Open again. + await toggle.click(); + await expect( content ).toBeVisible(); + } ); + + test( 'multiple items can be open simultaneously', async ( { page } ) => { + const toggle1 = page.locator( '#item-1 .gb-accordion__toggle' ); + const toggle3 = page.locator( '#item-3 .gb-accordion__toggle' ); + + await toggle1.click(); + await toggle3.click(); + + await expect( page.locator( '#item-1 .gb-accordion__content' ) ).toBeVisible(); + await expect( page.locator( '#item-2 .gb-accordion__content' ) ).toBeVisible(); + await expect( page.locator( '#item-3 .gb-accordion__content' ) ).toBeVisible(); + } ); +} ); diff --git a/tests/e2e/back-button.spec.ts b/tests/e2e/back-button.spec.ts new file mode 100644 index 0000000..1656d32 --- /dev/null +++ b/tests/e2e/back-button.spec.ts @@ -0,0 +1,64 @@ +import { test, expect } from '@playwright/test'; + +test.describe( 'Back Button', () => { + test.beforeEach( async ( { page } ) => { + await page.goto( '/tests/e2e/fixtures/back-button.html' ); + } ); + + test( 'back button element is present in DOM', async ( { page } ) => { + await expect( page.locator( '#frbl-back-button' ) ).toBeAttached(); + } ); + + test( 'back button is hidden on first page visit', async ( { page } ) => { + // navCount is 1 on first visit → shouldShowButton() returns false. + const button = page.locator( '#frbl-back-button' ); + await expect( button ).not.toHaveClass( /frbl-show/ ); + } ); + + test( 'back button has no frbl-show class before scrolling (first visit)', async ( { page } ) => { + const button = page.locator( '#frbl-back-button' ); + await page.evaluate( () => window.scrollTo( 0, 200 ) ); + await page.waitForTimeout( 200 ); + + // Still hidden because it is the first page visited (navCount ≤ 1). + await expect( button ).not.toHaveClass( /frbl-show/ ); + } ); + + test( 'back button shows frbl-show class after navigation and scroll', async ( { page } ) => { + // Simulate a second navigation by manually setting sessionStorage so + // the script thinks the user already visited another page. + await page.evaluate( () => { + sessionStorage.setItem( 'frbl_nav_count', '2' ); + sessionStorage.setItem( 'frbl_entry_url', 'http://localhost:3737/other-page.html' ); + } ); + + // Reload so the script reads the updated sessionStorage. + await page.reload(); + + // Scroll past the 100px threshold. + await page.evaluate( () => window.scrollTo( 0, 200 ) ); + await page.waitForTimeout( 600 ); // wait for the 500ms show delay + rAF. + + const button = page.locator( '#frbl-back-button' ); + await expect( button ).toHaveClass( /frbl-show/ ); + } ); + + test( 'back button hides when scrolling back to top', async ( { page } ) => { + // Simulate second visit. + await page.evaluate( () => { + sessionStorage.setItem( 'frbl_nav_count', '2' ); + sessionStorage.setItem( 'frbl_entry_url', 'http://localhost:3737/other-page.html' ); + } ); + await page.reload(); + + await page.evaluate( () => window.scrollTo( 0, 200 ) ); + await page.waitForTimeout( 600 ); + + // Now scroll back to top. + await page.evaluate( () => window.scrollTo( 0, 0 ) ); + await page.waitForTimeout( 200 ); + + const button = page.locator( '#frbl-back-button' ); + await expect( button ).not.toHaveClass( /frbl-show/ ); + } ); +} ); diff --git a/tests/e2e/counter.spec.ts b/tests/e2e/counter.spec.ts new file mode 100644 index 0000000..34b6e10 --- /dev/null +++ b/tests/e2e/counter.spec.ts @@ -0,0 +1,69 @@ +import { test, expect } from '@playwright/test'; + +test.describe( 'Counter Animation', () => { + test.beforeEach( async ( { page } ) => { + await page.goto( '/tests/e2e/fixtures/counter.html' ); + } ); + + test( 'counter initialises with start value (0) before animation', async ( { page } ) => { + // On DOMContentLoaded the script sets the text to the start value before + // the IntersectionObserver fires. The counters are visible in the viewport + // so animation starts immediately; we check a snapshot before it ends by + // using a very short evaluation window — but we can at least assert the + // element exists and has the expected class. + const counter = page.locator( '#counter-1' ); + await expect( counter ).toBeAttached(); + await expect( counter ).toHaveClass( /frontblocks-counter-active/ ); + } ); + + test( 'counter reaches target value after animation', async ( { page } ) => { + const counter = page.locator( '#counter-1' ); + + // Wait until the count-up-animated class is added (animation complete). + await expect( counter ).toHaveClass( /count-up-animated/, { timeout: 3000 } ); + + const text = await counter.textContent(); + expect( text?.replace( /,/g, '' ) ).toBe( '500' ); + } ); + + test( 'counter with prefix and suffix renders correctly after animation', async ( { page } ) => { + const counter = page.locator( '#counter-2' ); + + await expect( counter ).toHaveClass( /count-up-animated/, { timeout: 3000 } ); + + const text = await counter.textContent(); + expect( text ).toContain( '$' ); + expect( text ).toContain( 'K' ); + expect( text ).toContain( '100' ); + } ); + + test( 'counter with non-zero start value reaches correct target', async ( { page } ) => { + const counter = page.locator( '#counter-3' ); + + await expect( counter ).toHaveClass( /count-up-animated/, { timeout: 3000 } ); + + const text = await counter.textContent(); + expect( text?.replace( /,/g, '' ) ).toBe( '200' ); + } ); + + test( 'animation does not run twice on the same element', async ( { page } ) => { + const counter = page.locator( '#counter-1' ); + + // Wait for first animation to complete. + await expect( counter ).toHaveClass( /count-up-animated/, { timeout: 3000 } ); + + const valueBefore = await counter.textContent(); + + // Trigger an artificial IntersectionObserver-like call via evaluate. + await page.evaluate( () => { + const el = document.getElementById( 'counter-1' )!; + // Simulate a second DOMContentLoaded — should be a no-op. + el.dispatchEvent( new Event( 'DOMContentLoaded' ) ); + } ); + + await page.waitForTimeout( 600 ); + const valueAfter = await counter.textContent(); + + expect( valueAfter ).toBe( valueBefore ); + } ); +} ); diff --git a/tests/e2e/fixtures/accordion.html b/tests/e2e/fixtures/accordion.html new file mode 100644 index 0000000..0e01974 --- /dev/null +++ b/tests/e2e/fixtures/accordion.html @@ -0,0 +1,40 @@ + + + + + Accordion fixture – FrontBlocks + + + +
+ +
+ +
+

Content for item 1

+
+
+ + +
+ +
+

Content for item 2

+
+
+ + +
+ +
+

Content for item 3

+
+
+
+ + + + diff --git a/tests/e2e/fixtures/back-button.html b/tests/e2e/fixtures/back-button.html new file mode 100644 index 0000000..f7fda78 --- /dev/null +++ b/tests/e2e/fixtures/back-button.html @@ -0,0 +1,35 @@ + + + + + Back Button fixture – FrontBlocks + + + + Back + +
+

Back Button Test Page

+

Scroll down to see the back button appear.

+
+ + + + diff --git a/tests/e2e/fixtures/counter.html b/tests/e2e/fixtures/counter.html new file mode 100644 index 0000000..9deedae --- /dev/null +++ b/tests/e2e/fixtures/counter.html @@ -0,0 +1,37 @@ + + + + + Counter fixture – FrontBlocks + + + + 0 + + + $0K + + + 100 + + + + diff --git a/tests/e2e/fixtures/reading-progress.html b/tests/e2e/fixtures/reading-progress.html new file mode 100644 index 0000000..fd09d68 --- /dev/null +++ b/tests/e2e/fixtures/reading-progress.html @@ -0,0 +1,41 @@ + + + + + Reading Progress fixture – FrontBlocks + + + +
+
+
+ +
+

Long article

+

Scroll down to see progress update.

+
+ + + + diff --git a/tests/e2e/fixtures/sticky-column.html b/tests/e2e/fixtures/sticky-column.html new file mode 100644 index 0000000..bc7f1b6 --- /dev/null +++ b/tests/e2e/fixtures/sticky-column.html @@ -0,0 +1,49 @@ + + + + + Sticky Column fixture – FrontBlocks + + + +
Scroll down to reach the sticky wrapper
+ +
+
+
+ Sticky column content +
+
+
+

Normal column content

+
+
+ + + + diff --git a/tests/e2e/reading-progress.spec.ts b/tests/e2e/reading-progress.spec.ts new file mode 100644 index 0000000..dd5f831 --- /dev/null +++ b/tests/e2e/reading-progress.spec.ts @@ -0,0 +1,57 @@ +import { test, expect } from '@playwright/test'; + +test.describe( 'Reading Progress Bar', () => { + test.beforeEach( async ( { page } ) => { + await page.goto( '/tests/e2e/fixtures/reading-progress.html' ); + } ); + + test( 'progress bar is present in the DOM', async ( { page } ) => { + await expect( page.locator( '.frbl-reading-progress-bar' ) ).toBeAttached(); + } ); + + test( 'progress fill starts at 0%', async ( { page } ) => { + const fill = page.locator( '.frbl-reading-progress-fill' ); + const height = await fill.evaluate( ( el ) => ( el as HTMLElement ).style.height ); + expect( height ).toBe( '0%' ); + } ); + + test( 'body gets frbl-reading-progress-active class on init', async ( { page } ) => { + await expect( page.locator( 'body' ) ).toHaveClass( /frbl-reading-progress-active/ ); + } ); + + test( 'progress bar aria-valuenow starts at 0', async ( { page } ) => { + const bar = page.locator( '.frbl-reading-progress-bar' ); + await expect( bar ).toHaveAttribute( 'aria-valuenow', '0' ); + } ); + + test( 'progress increases after scrolling down', async ( { page } ) => { + // Scroll halfway down the tall article. + await page.evaluate( () => window.scrollTo( 0, 1500 ) ); + + // Wait for rAF throttle to fire. + await page.waitForTimeout( 100 ); + + const fill = page.locator( '.frbl-reading-progress-fill' ); + const height = await fill.evaluate( ( el ) => parseFloat( ( el as HTMLElement ).style.height ) ); + + expect( height ).toBeGreaterThan( 0 ); + } ); + + test( 'progress aria-valuenow updates on scroll', async ( { page } ) => { + await page.evaluate( () => window.scrollTo( 0, 1500 ) ); + await page.waitForTimeout( 100 ); + + const bar = page.locator( '.frbl-reading-progress-bar' ); + const ariaValue = await bar.getAttribute( 'aria-valuenow' ); + expect( Number( ariaValue ) ).toBeGreaterThan( 0 ); + } ); + + test( 'progress does not exceed 100% at page bottom', async ( { page } ) => { + await page.evaluate( () => window.scrollTo( 0, document.body.scrollHeight ) ); + await page.waitForTimeout( 100 ); + + const fill = page.locator( '.frbl-reading-progress-fill' ); + const height = await fill.evaluate( ( el ) => parseFloat( ( el as HTMLElement ).style.height ) ); + expect( height ).toBeLessThanOrEqual( 100 ); + } ); +} ); diff --git a/tests/e2e/sticky-column.spec.ts b/tests/e2e/sticky-column.spec.ts new file mode 100644 index 0000000..afd2f6b --- /dev/null +++ b/tests/e2e/sticky-column.spec.ts @@ -0,0 +1,51 @@ +import { test, expect } from '@playwright/test'; + +test.describe( 'Sticky Column', () => { + test.beforeEach( async ( { page } ) => { + // Use a small viewport so we can scroll into the wrapper area. + await page.setViewportSize( { width: 1024, height: 600 } ); + await page.goto( '/tests/e2e/fixtures/sticky-column.html' ); + } ); + + test( 'sticky column wrapper is present in DOM', async ( { page } ) => { + await expect( page.locator( '#sticky-wrapper' ) ).toBeAttached(); + } ); + + test( 'sticky column does not have sticky-active class on page load', async ( { page } ) => { + const col = page.locator( '#sticky-col' ); + await expect( col ).not.toHaveClass( /sticky-active/ ); + } ); + + test( 'sticky column gets sticky-active class after scrolling into view', async ( { page } ) => { + // Scroll past the wrapper top (it starts at y=800px). + await page.evaluate( () => window.scrollTo( 0, 900 ) ); + await page.waitForTimeout( 100 ); + + const col = page.locator( '#sticky-col' ); + await expect( col ).toHaveClass( /sticky-active/ ); + } ); + + test( 'sticky column loses sticky-active class after scrolling back to top', async ( { page } ) => { + // Scroll down to activate sticky. + await page.evaluate( () => window.scrollTo( 0, 900 ) ); + await page.waitForTimeout( 100 ); + + // Scroll back up above the wrapper. + await page.evaluate( () => window.scrollTo( 0, 0 ) ); + await page.waitForTimeout( 100 ); + + const col = page.locator( '#sticky-col' ); + await expect( col ).not.toHaveClass( /sticky-active/ ); + } ); + + test( 'sticky column applies top offset to inner container', async ( { page } ) => { + await page.evaluate( () => window.scrollTo( 0, 900 ) ); + await page.waitForTimeout( 100 ); + + const container = page.locator( '#sticky-content' ); + const topStyle = await container.evaluate( ( el ) => ( el as HTMLElement ).style.top ); + + // data-sticky-offset="20" → top should be "20px". + expect( topStyle ).toBe( '20px' ); + } ); +} ); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..892ca49 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ES2019", + "module": "commonjs", + "lib": ["ES2019", "DOM"], + "strict": true, + "esModuleInterop": true, + "resolveJsonModule": true, + "outDir": "tests/e2e/dist", + "rootDir": ".", + "types": ["node"] + }, + "include": [ + "playwright.config.ts", + "tests/e2e/**/*.ts" + ], + "exclude": [ + "node_modules" + ] +} From d8bad38cbd97d4af17b47256113f0dc157583811 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 29 Mar 2026 18:04:05 +0000 Subject: [PATCH 2/2] Add Gutenberg-rendered block fixtures and expand E2E test suite MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New HTML fixtures mirror the exact HTML that each FrontBlocks PHP class outputs on a real WordPress/GeneratePress frontend page, enabling E2E tests without a WordPress installation. New fixtures: - tests/e2e/fixtures/carousel.html – three carousel variants (arrows, bullets, slider) - tests/e2e/fixtures/marquee.html – three marquee speed presets - tests/e2e/fixtures/wordpress-frontend-page.html – full realistic WP page combining all components: reading progress, back button, carousel, counters, sticky column, accordion, and marquee in one document New test specs (49 new tests): - tests/e2e/carousel.spec.ts – Glide.js mount, DOM structure, arrow/bullet navigation, aria-labels, slider type - tests/e2e/marquee.spec.ts – initialisation, copy count, CSS animation, speed presets, MutationObserver dynamic init - tests/e2e/wordpress-frontend-page.spec.ts – full-page integration: all components on one page, accessibility checks Updated: - tests/e2e/fixtures/back-button.html – corrected to use

Back Button Test Page

-

Scroll down to see the back button appear.

+

Scroll down to see the back button appear (requires prior navigation).

diff --git a/tests/e2e/fixtures/carousel.html b/tests/e2e/fixtures/carousel.html new file mode 100644 index 0000000..4ff1fec --- /dev/null +++ b/tests/e2e/fixtures/carousel.html @@ -0,0 +1,124 @@ + + + + + Carousel fixture – FrontBlocks + + + + + + + +
+ + + + +
+ + + + + + + + + diff --git a/tests/e2e/fixtures/marquee.html b/tests/e2e/fixtures/marquee.html new file mode 100644 index 0000000..c1c25a2 --- /dev/null +++ b/tests/e2e/fixtures/marquee.html @@ -0,0 +1,67 @@ + + + + + Marquee fixture – FrontBlocks + + + + + + + +
+ FrontBlocks — Gutenberg enhancements for GeneratePress ★ +
+ +
+ + +
+ Fast scrolling text — FrontBlocks ★ Fast scrolling text — FrontBlocks ★ +
+ +
+ + +
+ Slow marquee text here ★ +
+ + + + diff --git a/tests/e2e/fixtures/wordpress-frontend-page.html b/tests/e2e/fixtures/wordpress-frontend-page.html new file mode 100644 index 0000000..adf0d1d --- /dev/null +++ b/tests/e2e/fixtures/wordpress-frontend-page.html @@ -0,0 +1,357 @@ + + + + + + + Sample Page – My WordPress Site + + + + + + + + +
+
+ + +
+
+ FrontBlocks · Gutenberg Blocks for GeneratePress · FrontBlocks · Gutenberg Blocks for GeneratePress · +
+
+ + +
+

Our Numbers

+ +
+
+

0+

+

Happy clients

+
+
+

0 projects

+

Completed

+
+
+

0 yrs

+

In business

+
+
+
+ + + + + +
+

Long Content with Sticky Sidebar

+ +
+ +
+

This is the main article content. It is very long so the sidebar can stick next to it as you scroll.

+

You are now in the middle of the article…

+

Almost at the end of the long content section.

+
+ + +
+
+ + +
+

Frequently Asked Questions

+ +
+
+ +
+

FrontBlocks is a WordPress plugin that extends Gutenberg and GeneratePress with additional frontend features and block enhancements.

+
+
+
+ +
+

FrontBlocks is optimised for GeneratePress and GenerateBlocks, but many features work with any block-based theme.

+
+
+
+ +
+

Yes! The core plugin is free and available on WordPress.org. A PRO version with advanced WooCommerce features is also available.

+
+
+
+
+ +
+
+ + +
+

© 2025 My WordPress Site · Powered by WordPress & FrontBlocks

+
+ + +
+
+
+ + + + + + + + + + + + + + + + diff --git a/tests/e2e/marquee.spec.ts b/tests/e2e/marquee.spec.ts new file mode 100644 index 0000000..685c865 --- /dev/null +++ b/tests/e2e/marquee.spec.ts @@ -0,0 +1,120 @@ +import { test, expect } from '@playwright/test'; + +test.describe( 'Marquee (infinite scroll headline)', () => { + test.beforeEach( async ( { page } ) => { + await page.goto( '/tests/e2e/fixtures/marquee.html' ); + // Wait for rAF-based init to complete. + await page.waitForTimeout( 300 ); + } ); + + // ── Initialisation ──────────────────────────────────────────── + + test( 'marquee element is marked as initialized', async ( { page } ) => { + const el = page.locator( '#marquee-medium' ); + await expect( el ).toHaveAttribute( 'data-marquee-initialized', 'true' ); + } ); + + test( 'marquee wrapper is created inside the element', async ( { page } ) => { + const wrapper = page.locator( '#marquee-medium .gb-marquee-wrapper' ); + await expect( wrapper ).toBeAttached(); + } ); + + test( 'at least two content copies are created for seamless loop', async ( { page } ) => { + const copies = page.locator( '#marquee-medium .gb-marquee-copy' ); + const count = await copies.count(); + expect( count ).toBeGreaterThanOrEqual( 2 ); + } ); + + test( 'each copy contains the original text', async ( { page } ) => { + const firstCopy = page.locator( '#marquee-medium .gb-marquee-copy' ).first(); + const text = await firstCopy.textContent(); + expect( text ).toContain( 'FrontBlocks' ); + } ); + + // ── CSS animation ───────────────────────────────────────────── + + test( 'wrapper has an animation applied after init', async ( { page } ) => { + const wrapper = page.locator( '#marquee-medium .gb-marquee-wrapper' ); + const animationName = await wrapper.evaluate( + ( el ) => window.getComputedStyle( el ).animationName + ); + // The script generates a unique keyframe name starting with 'marquee-scroll-'. + expect( animationName ).toMatch( /marquee-scroll-/ ); + } ); + + test( 'wrapper animation iteration count is infinite', async ( { page } ) => { + const wrapper = page.locator( '#marquee-medium .gb-marquee-wrapper' ); + const iterationCount = await wrapper.evaluate( + ( el ) => window.getComputedStyle( el ).animationIterationCount + ); + expect( iterationCount ).toBe( 'infinite' ); + } ); + + test( 'animation is running (not paused) by default', async ( { page } ) => { + const wrapper = page.locator( '#marquee-medium .gb-marquee-wrapper' ); + const playState = await wrapper.evaluate( + ( el ) => window.getComputedStyle( el ).animationPlayState + ); + expect( playState ).toBe( 'running' ); + } ); + + // ── Speed presets ───────────────────────────────────────────── + + test( 'medium speed uses 20s animation duration', async ( { page } ) => { + const wrapper = page.locator( '#marquee-medium .gb-marquee-wrapper' ); + const speed = await wrapper.getAttribute( 'data-marquee-speed' ); + expect( Number( speed ) ).toBe( 20 ); + } ); + + test( 'fast speed uses 10s animation duration', async ( { page } ) => { + const wrapper = page.locator( '#marquee-fast .gb-marquee-wrapper' ); + await expect( page.locator( '#marquee-fast' ) ).toHaveAttribute( 'data-marquee-initialized', 'true' ); + const speed = await wrapper.getAttribute( 'data-marquee-speed' ); + expect( Number( speed ) ).toBe( 10 ); + } ); + + test( 'slow speed uses 40s animation duration', async ( { page } ) => { + const wrapper = page.locator( '#marquee-slow .gb-marquee-wrapper' ); + await expect( page.locator( '#marquee-slow' ) ).toHaveAttribute( 'data-marquee-initialized', 'true' ); + const speed = await wrapper.getAttribute( 'data-marquee-speed' ); + expect( Number( speed ) ).toBe( 40 ); + } ); + + // ── Dynamic initialisation ──────────────────────────────────── + + test( 'dynamically added marquee element is initialised via MutationObserver', async ( { page } ) => { + await page.evaluate( () => { + const el = document.createElement( 'div' ); + el.id = 'marquee-dynamic'; + el.className = 'gb-element gb-marquee-infinite-scroll'; + el.setAttribute( 'data-marquee-speed', 'medium' ); + el.style.overflow = 'hidden'; + el.style.width = '400px'; + el.innerHTML = 'Dynamic marquee content ★'; + document.body.appendChild( el ); + } ); + + // Wait for MutationObserver + rAF callbacks. + await page.waitForTimeout( 500 ); + + const el = page.locator( '#marquee-dynamic' ); + await expect( el ).toHaveAttribute( 'data-marquee-initialized', 'true' ); + } ); +} ); + +// ── WordPress frontend page – marquee integration ────────────────── +test.describe( 'Marquee on WordPress frontend page', () => { + test.beforeEach( async ( { page } ) => { + await page.goto( '/tests/e2e/fixtures/wordpress-frontend-page.html' ); + await page.waitForTimeout( 300 ); + } ); + + test( 'hero marquee is initialized on the frontend page', async ( { page } ) => { + await expect( page.locator( '#marquee-hero' ) ).toHaveAttribute( 'data-marquee-initialized', 'true' ); + } ); + + test( 'hero marquee has scroll copies', async ( { page } ) => { + const copies = page.locator( '#marquee-hero .gb-marquee-copy' ); + await expect( copies ).not.toHaveCount( 0 ); + } ); +} ); diff --git a/tests/e2e/wordpress-frontend-page.spec.ts b/tests/e2e/wordpress-frontend-page.spec.ts new file mode 100644 index 0000000..d9de5d3 --- /dev/null +++ b/tests/e2e/wordpress-frontend-page.spec.ts @@ -0,0 +1,171 @@ +/** + * End-to-end tests for the realistic WordPress frontend page fixture. + * + * The fixture at tests/e2e/fixtures/wordpress-frontend-page.html combines all + * FrontBlocks components on a single page, mirroring what a GeneratePress site + * with FrontBlocks active would render on the frontend. + */ + +import { test, expect } from '@playwright/test'; + +test.describe( 'WordPress frontend page – full integration', () => { + test.beforeEach( async ( { page } ) => { + await page.setViewportSize( { width: 1280, height: 800 } ); + await page.goto( '/tests/e2e/fixtures/wordpress-frontend-page.html' ); + await page.waitForLoadState( 'load' ); + // Allow rAF-based initialisations (marquee, carousel) to settle. + await page.waitForTimeout( 400 ); + } ); + + // ── Page structure ──────────────────────────────────────────── + + test( 'page title is correct', async ( { page } ) => { + await expect( page ).toHaveTitle( /Sample Page/ ); + } ); + + test( 'site header is visible', async ( { page } ) => { + await expect( page.locator( '.site-header' ) ).toBeVisible(); + } ); + + test( 'article content is present', async ( { page } ) => { + await expect( page.locator( '#post-1' ) ).toBeAttached(); + } ); + + // ── Reading progress bar ────────────────────────────────────── + + test( 'reading progress bar is present', async ( { page } ) => { + await expect( page.locator( '.frbl-reading-progress-bar' ) ).toBeAttached(); + } ); + + test( 'reading progress body class is added', async ( { page } ) => { + await expect( page.locator( 'body' ) ).toHaveClass( /frbl-reading-progress-active/ ); + } ); + + test( 'reading progress updates on scroll', async ( { page } ) => { + await page.evaluate( () => window.scrollTo( 0, 800 ) ); + await page.waitForTimeout( 150 ); + + const fill = page.locator( '.frbl-reading-progress-fill' ); + const height = await fill.evaluate( ( el ) => parseFloat( ( el as HTMLElement ).style.height ) ); + expect( height ).toBeGreaterThan( 0 ); + } ); + + // ── Animated counters ───────────────────────────────────────── + + test( 'all three counters are present', async ( { page } ) => { + await expect( page.locator( '#counter-clients' ) ).toBeAttached(); + await expect( page.locator( '#counter-projects' ) ).toBeAttached(); + await expect( page.locator( '#counter-years' ) ).toBeAttached(); + } ); + + test( 'counters reach their target values', async ( { page } ) => { + await expect( page.locator( '#counter-clients' ) ).toHaveClass( /count-up-animated/, { timeout: 4000 } ); + await expect( page.locator( '#counter-projects' ) ).toHaveClass( /count-up-animated/, { timeout: 4000 } ); + await expect( page.locator( '#counter-years' ) ).toHaveClass( /count-up-animated/, { timeout: 4000 } ); + } ); + + test( 'counter-clients ends with "+" suffix', async ( { page } ) => { + await expect( page.locator( '#counter-clients' ) ).toHaveClass( /count-up-animated/, { timeout: 4000 } ); + const text = await page.locator( '#counter-clients' ).textContent(); + expect( text ).toContain( '+' ); + expect( text?.replace( /,|\+/g, '' ) ).toBe( '1200' ); + } ); + + test( 'counter-years ends with " yrs" suffix', async ( { page } ) => { + await expect( page.locator( '#counter-years' ) ).toHaveClass( /count-up-animated/, { timeout: 4000 } ); + const text = await page.locator( '#counter-years' ).textContent(); + expect( text ).toContain( 'yrs' ); + } ); + + // ── Carousel ────────────────────────────────────────────────── + + test( 'featured articles carousel is mounted', async ( { page } ) => { + const wrapper = page.locator( '#carousel-featured' ).locator( 'xpath=../..' ); + await expect( wrapper ).toHaveClass( /glide/ ); + } ); + + test( 'featured articles carousel has 5 slides', async ( { page } ) => { + await expect( page.locator( '#carousel-featured .glide__slide' ) ).toHaveCount( 5 ); + } ); + + // ── Sticky column ───────────────────────────────────────────── + + test( 'sticky layout wrapper is present', async ( { page } ) => { + await expect( page.locator( '#sticky-layout' ) ).toBeAttached(); + } ); + + test( 'sidebar becomes sticky after scrolling to it', async ( { page } ) => { + // Scroll to the sticky section (it starts after a long counters + carousel section). + await page.locator( '#section-sticky' ).scrollIntoViewIfNeeded(); + await page.evaluate( () => window.scrollBy( 0, 200 ) ); + await page.waitForTimeout( 150 ); + + const sidebar = page.locator( '#sidebar-col' ); + await expect( sidebar ).toHaveClass( /sticky-active/ ); + } ); + + // ── Accordion (FAQ) ─────────────────────────────────────────── + + test( 'closed FAQ items start hidden', async ( { page } ) => { + await expect( page.locator( '#faq-1 .gb-accordion__content' ) ).not.toBeVisible(); + await expect( page.locator( '#faq-2 .gb-accordion__content' ) ).not.toBeVisible(); + } ); + + test( 'pre-opened FAQ item is visible', async ( { page } ) => { + await expect( page.locator( '#faq-3 .gb-accordion__content' ) ).toBeVisible(); + } ); + + test( 'clicking FAQ toggle opens it', async ( { page } ) => { + await page.locator( '#faq-1 .gb-accordion__toggle' ).click(); + await expect( page.locator( '#faq-1 .gb-accordion__content' ) ).toBeVisible(); + } ); + + test( 'clicking FAQ toggle sets aria-expanded to true', async ( { page } ) => { + await page.locator( '#faq-2 .gb-accordion__toggle' ).click(); + await expect( page.locator( '#faq-2 .gb-accordion__toggle' ) ).toHaveAttribute( 'aria-expanded', 'true' ); + } ); + + // ── Marquee ─────────────────────────────────────────────────── + + test( 'hero marquee is initialised', async ( { page } ) => { + await expect( page.locator( '#marquee-hero' ) ).toHaveAttribute( 'data-marquee-initialized', 'true' ); + } ); + + test( 'hero marquee has animation running', async ( { page } ) => { + const wrapper = page.locator( '#marquee-hero .gb-marquee-wrapper' ); + const playState = await wrapper.evaluate( + ( el ) => window.getComputedStyle( el ).animationPlayState + ); + expect( playState ).toBe( 'running' ); + } ); + + // ── Back button ─────────────────────────────────────────────── + + test( 'back button is present in DOM', async ( { page } ) => { + await expect( page.locator( '#frbl-back-button' ) ).toBeAttached(); + } ); + + test( 'back button has correct aria-label', async ( { page } ) => { + await expect( page.locator( '#frbl-back-button' ) ).toHaveAttribute( 'aria-label', 'Go back to previous page' ); + } ); + + test( 'back button is hidden on first visit (no prior navigation)', async ( { page } ) => { + await expect( page.locator( '#frbl-back-button' ) ).not.toHaveClass( /frbl-show/ ); + } ); + + // ── Accessibility ───────────────────────────────────────────── + + test( 'reading progress bar has proper ARIA role', async ( { page } ) => { + await expect( page.locator( '.frbl-reading-progress-bar' ) ).toHaveAttribute( 'role', 'progressbar' ); + } ); + + test( 'reading progress bar has accessible aria-label', async ( { page } ) => { + await expect( page.locator( '.frbl-reading-progress-bar' ) ).toHaveAttribute( 'aria-label', 'Reading progress' ); + } ); + + test( 'carousel arrows have aria-labels', async ( { page } ) => { + const wrapper = page.locator( '#carousel-featured' ).locator( 'xpath=../..' ); + await expect( wrapper.locator( '.glide__arrow--left' ) ).toHaveAttribute( 'aria-label', 'Previous slide' ); + await expect( wrapper.locator( '.glide__arrow--right' ) ).toHaveAttribute( 'aria-label', 'Next slide' ); + } ); +} );