diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..9631516 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,67 @@ +name: Tests + +on: + push: + branches: [main] + pull_request: + branches: [main] + +# Cancel superseded runs on the same ref (e.g. new pushes to a PR). +concurrency: + group: tests-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + unit: + name: Unit (happy-dom) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: pnpm/action-setup@v4 + with: + version: 11 + - uses: actions/setup-node@v6 + with: + node-version-file: .nvmrc + cache: pnpm + - run: pnpm install --frozen-lockfile + - run: pnpm test + + e2e: + name: E2E (Chromium + Firefox + WebKit) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: pnpm/action-setup@v4 + with: + version: 11 + - uses: actions/setup-node@v6 + with: + node-version-file: .nvmrc + cache: pnpm + - run: pnpm install --frozen-lockfile + # Downloads the browser binaries and their OS libraries (Firefox/WebKit + # need system deps that `--with-deps` installs via apt on the runner). + - name: Install Playwright browsers + run: pnpm exec playwright install --with-deps chromium firefox webkit + - run: pnpm test:e2e:all + + size: + name: Size limit + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: pnpm/action-setup@v4 + with: + version: 11 + - uses: actions/setup-node@v6 + with: + node-version-file: .nvmrc + cache: pnpm + - run: pnpm install --frozen-lockfile + # `pnpm size-limit` builds `dist/` (tsc + esbuild) and checks each entry + # against the byte budgets declared in package.json `size-limit`. + - run: pnpm size-limit diff --git a/.husky/pre-commit b/.husky/pre-commit index 336c5e0..d34f720 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,4 +1,5 @@ -npm run lint --cache -npm run test:all -npm run build -npx size-limit +# Lean: lint + format staged files, then the fast unit suite. +# Heavier checks (browser e2e, size-limit) run on pre-push; the full +# cross-browser matrix runs in CI. +pnpm exec lint-staged +pnpm test diff --git a/.husky/pre-push b/.husky/pre-push new file mode 100755 index 0000000..07bbad3 --- /dev/null +++ b/.husky/pre-push @@ -0,0 +1,4 @@ +# Heavier gate before code leaves the machine: browser e2e (Chromium only — +# Firefox/WebKit + their system deps are exercised in CI) and the size budget. +pnpm test:e2e +pnpm size-limit diff --git a/AGENTS.md b/AGENTS.md index d6998cd..422a6f3 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -17,7 +17,7 @@ This file provides guidance to AI coding agents when working with code in this r - `pnpm size-limit` — enforces the byte budgets declared in `package.json` `size-limit` (e.g. `WebComponent.js` ≤ 1.2 KB). Keep additions tiny; this gate is a core project value. - `pnpm docs` — run the Astro docs site (`docs/` workspace) locally. - `pnpm demo` — run the Vite examples showcase (`demo/` workspace) locally; `pnpm demo:build` builds it. -- `pnpm test:e2e` — run the browser e2e specs (`test/e2e/`) in Chromium via Vitest browser mode; `pnpm test:all` runs unit + e2e. +- `pnpm test:e2e` — run the browser e2e specs (`test/e2e/`) via Vitest browser mode (Playwright). Defaults to Chromium; `pnpm test:e2e:firefox` / `:webkit` / `:all` target the other engines (the `E2E_BROWSERS` env var, comma-separated, selects instances). `pnpm test:all` runs unit + default e2e. pnpm is mandatory (a `preinstall` `only-allow pnpm` guard enforces it). This is a pnpm workspace with two sub-packages: `docs/` (Astro docs site) and `demo/` (Vite examples showcase, which consumes the root `web-component-base` package as a `workspace:*` dependency). The runnable example sources live in `demo/examples/` and import the package by name (`web-component-base`), not via relative `src/` paths. diff --git a/eslint.config.mjs b/eslint.config.mjs index 083b0dd..5fb812b 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -12,6 +12,12 @@ export default [ 'no-unused-vars': 'warn', }, }, + // Config, build, and script files run in Node — give them Node globals + // (e.g. `process`) on top of the browser defaults. + { + files: ['**/*.config.{js,mjs,cjs}', 'scripts/**/*.{js,mjs}'], + languageOptions: { globals: globals.node }, + }, { ignores: ['site/*', '**/dist/*'], }, diff --git a/package.json b/package.json index c53b34b..a4d0721 100644 --- a/package.json +++ b/package.json @@ -30,8 +30,12 @@ "dev": "pnpm -F demo dev", "test": "vitest --run", "test:watch": "vitest", - "test:e2e": "vitest --run --config vitest.e2e.config.mjs", - "test:all": "pnpm test && pnpm test:e2e", + "test:e2e": "E2E_BROWSERS=chromium vitest --run --config vitest.e2e.config.mjs", + "test:e2e:chromium": "E2E_BROWSERS=chromium vitest --run --config vitest.e2e.config.mjs", + "test:e2e:firefox": "E2E_BROWSERS=firefox vitest --run --config vitest.e2e.config.mjs", + "test:e2e:webkit": "E2E_BROWSERS=webkit vitest --run --config vitest.e2e.config.mjs", + "test:e2e:all": "vitest --run --config vitest.e2e.config.mjs", + "test:all": "pnpm test && pnpm test:e2e:all", "demo": "pnpm -F demo dev", "demo:build": "pnpm -F demo build", "docs": "pnpm -F docs start", @@ -44,6 +48,13 @@ "lint": "eslint . --config eslint.config.mjs", "prepare": "husky" }, + "lint-staged": { + "*.{js,mjs,cjs}": [ + "eslint --config eslint.config.mjs --fix", + "prettier --write" + ], + "*.{json,css,html,md,yml,yaml}": "prettier --write" + }, "repository": { "type": "git", "url": "git+https://github.com/ayo-run/web-component-base.git" @@ -73,6 +84,7 @@ "globals": "^17.1.0", "happy-dom": "^20.3.7", "husky": "^9.1.7", + "lint-staged": "^17.0.8", "netlify-cli": "^23.13.5", "playwright": "^1.61.1", "prettier": "^3.8.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5efa677..9f59d9e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -16,13 +16,13 @@ importers: version: 12.0.0(size-limit@12.0.0(jiti@2.6.1)) '@vitest/browser': specifier: 4.0.18 - version: 4.0.18(vite@7.3.1(@types/node@25.0.10)(jiti@2.6.1)(terser@5.39.0)(yaml@2.8.2))(vitest@4.0.18) + version: 4.0.18(vite@7.3.1(@types/node@25.0.10)(jiti@2.6.1)(terser@5.39.0)(yaml@2.9.0))(vitest@4.0.18) '@vitest/browser-playwright': specifier: 4.0.18 - version: 4.0.18(playwright@1.61.1)(vite@7.3.1(@types/node@25.0.10)(jiti@2.6.1)(terser@5.39.0)(yaml@2.8.2))(vitest@4.0.18) + version: 4.0.18(playwright@1.61.1)(vite@7.3.1(@types/node@25.0.10)(jiti@2.6.1)(terser@5.39.0)(yaml@2.9.0))(vitest@4.0.18) '@vitest/coverage-v8': specifier: 4.0.18 - version: 4.0.18(@vitest/browser@4.0.18(vite@7.3.1(@types/node@25.0.10)(jiti@2.6.1)(terser@5.39.0)(yaml@2.8.2))(vitest@4.0.18))(vitest@4.0.18) + version: 4.0.18(@vitest/browser@4.0.18(vite@7.3.1(@types/node@25.0.10)(jiti@2.6.1)(terser@5.39.0)(yaml@2.9.0))(vitest@4.0.18))(vitest@4.0.18) bumpp: specifier: ^11.1.0 version: 11.1.0 @@ -44,6 +44,9 @@ importers: husky: specifier: ^9.1.7 version: 9.1.7 + lint-staged: + specifier: ^17.0.8 + version: 17.0.8 netlify-cli: specifier: ^23.13.5 version: 23.13.5(@types/node@25.0.10)(db0@0.3.1)(ioredis@5.6.0)(picomatch@4.0.3)(rollup@4.60.3)(supports-color@10.2.2) @@ -70,7 +73,7 @@ importers: version: 5.9.3 vitest: specifier: ^4.0.18 - version: 4.0.18(@opentelemetry/api@1.8.0)(@types/node@25.0.10)(@vitest/browser-playwright@4.0.18)(happy-dom@20.3.7)(jiti@2.6.1)(terser@5.39.0)(yaml@2.8.2) + version: 4.0.18(@opentelemetry/api@1.8.0)(@types/node@25.0.10)(@vitest/browser-playwright@4.0.18)(happy-dom@20.3.7)(jiti@2.6.1)(terser@5.39.0)(yaml@2.9.0) demo: dependencies: @@ -89,10 +92,10 @@ importers: dependencies: '@astrojs/starlight': specifier: ^0.39.1 - version: 0.39.1(astro@6.3.1(@netlify/blobs@10.5.0)(@types/node@25.0.10)(db0@0.3.1)(ioredis@5.6.0)(jiti@2.6.1)(rollup@4.60.3)(terser@5.39.0)(yaml@2.8.2))(typescript@5.9.3) + version: 0.39.1(astro@6.3.1(@netlify/blobs@10.5.0)(@types/node@25.0.10)(db0@0.3.1)(ioredis@5.6.0)(jiti@2.6.1)(rollup@4.60.3)(terser@5.39.0)(yaml@2.9.0))(typescript@5.9.3) astro: specifier: ^6.3.1 - version: 6.3.1(@netlify/blobs@10.5.0)(@types/node@25.0.10)(db0@0.3.1)(ioredis@5.6.0)(jiti@2.6.1)(rollup@4.60.3)(terser@5.39.0)(yaml@2.8.2) + version: 6.3.1(@netlify/blobs@10.5.0)(@types/node@25.0.10)(db0@0.3.1)(ioredis@5.6.0)(jiti@2.6.1)(rollup@4.60.3)(terser@5.39.0)(yaml@2.9.0) sharp: specifier: ^0.34.5 version: 0.34.5 @@ -2680,6 +2683,10 @@ packages: resolution: {integrity: sha512-bXfOC4QcT1tKXGorxL3wbJm6XJPDqEnij2gQ2m7ESQuE+/z9YFIWnl/5RpTiKWbMq3EVKR4fRLJGn6DVfu0mpw==} engines: {node: '>=18.20'} + cli-truncate@5.2.0: + resolution: {integrity: sha512-xRwvIOMGrfOAnM1JYtqQImuaNtDEv9v6oIYAs4LIHwTiKee8uwvIi363igssOC0O5U04i4AlENs79LQLu9tEMw==} + engines: {node: '>=20'} + cli-width@3.0.0: resolution: {integrity: sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==} engines: {node: '>= 10'} @@ -3690,6 +3697,10 @@ packages: resolution: {integrity: sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==} engines: {node: '>=18'} + get-east-asian-width@1.6.0: + resolution: {integrity: sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==} + engines: {node: '>=18'} + get-intrinsic@1.3.0: resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} engines: {node: '>= 0.4'} @@ -4432,10 +4443,19 @@ packages: resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} engines: {node: '>=14'} + lint-staged@17.0.8: + resolution: {integrity: sha512-B2P/d+jVW0UXOQ0MVMLrB/9ydA1P+zz6jYfdrbbEd9ur3S2rcbduFWKiUCC02Sm5hbC8nrm7y24WuYMG54HfxA==} + engines: {node: '>=22.22.1'} + hasBin: true + listhen@1.9.0: resolution: {integrity: sha512-I8oW2+QL5KJo8zXNWX046M134WchxsXC7SawLPvRQpogCbkyQIaFxPE89A2HiwR7vAK2Dm2ERBAmyjTYGYEpBg==} hasBin: true + listr2@10.2.2: + resolution: {integrity: sha512-JtNtbZj8q5BnDMR7trpwvwk3RIrANtIVzEUm8w7amp6xelLgyuq+4WZoTH913XaQAoH/cNdYhaNzBPA2U3xbDw==} + engines: {node: '>=22.13.0'} + locate-path@6.0.0: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} @@ -6027,6 +6047,10 @@ packages: resolution: {integrity: sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==} engines: {node: '>=18'} + slice-ansi@8.0.0: + resolution: {integrity: sha512-stxByr12oeeOyY2BlviTNQlYV5xOj47GirPr4yA1hE9JCtxfQN0+tVbkxwCtYDQWhEKWFHsEK48ORg5jrouCAg==} + engines: {node: '>=20'} + smart-buffer@4.2.0: resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==} engines: {node: '>= 6.0.0', npm: '>= 3.0.0'} @@ -6146,6 +6170,10 @@ packages: streamx@2.22.0: resolution: {integrity: sha512-sLh1evHOzBy/iWRiR6d1zRcLao4gGZr3C1kzNz4fopCOKJb6xD9ub8Mpi9Mr1R6id5o43S+d93fI48UC5uM9aw==} + string-argv@0.3.2: + resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==} + engines: {node: '>=0.6.19'} + string-width@2.1.1: resolution: {integrity: sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==} engines: {node: '>=4'} @@ -6166,6 +6194,10 @@ packages: resolution: {integrity: sha512-Kxl3KJGb/gxkaUMOjRsQ8IrXiGW75O4E3RPjFIINOVH8AMl2SQ/yWdTzWwF3FevIX9LcMAjJW+GRwAlAbTSXdg==} engines: {node: '>=20'} + string-width@8.2.1: + resolution: {integrity: sha512-IIaP0g3iy9Cyy18w3M9YcaDudujEAVHKt3a3QJg1+sr/oX96TbaGUubG0hJyCjCBThFH+tFpcIyoUHUn1ogaLA==} + engines: {node: '>=20'} + string_decoder@1.1.1: resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} @@ -6318,6 +6350,10 @@ packages: resolution: {integrity: sha512-dAqSqE/RabpBKI8+h26GfLq6Vb3JVXs30XYQjdMjaj/c2tS8IYYMbIzP599KtRj7c57/wYApb3QjgRgXmrCukA==} engines: {node: '>=18'} + tinyexec@1.2.4: + resolution: {integrity: sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==} + engines: {node: '>=18'} + tinyglobby@0.2.15: resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} engines: {node: '>=12.0.0'} @@ -6937,6 +6973,10 @@ packages: resolution: {integrity: sha512-1tMA907+V4QmxV7dbRvb4/8MaRALK6q9Abid3ndMYnbyo8piisCmeONVqVSXqQA3KaP4SLt5b7ud6E2sqP8TFw==} engines: {node: '>=0.4.0'} + wrap-ansi@10.0.0: + resolution: {integrity: sha512-SGcvg80f0wUy2/fXES19feHMz8E0JoXv2uNgHOu4Dgi2OrCy1lqwFYEJz1BLbDI0exjPMe/ZdzZ/YpGECBG/aQ==} + engines: {node: '>=20'} + wrap-ansi@6.2.0: resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} engines: {node: '>=8'} @@ -7015,11 +7055,6 @@ packages: resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==} engines: {node: '>=18'} - yaml@2.8.2: - resolution: {integrity: sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==} - engines: {node: '>= 14.6'} - hasBin: true - yaml@2.9.0: resolution: {integrity: sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==} engines: {node: '>= 14.6'} @@ -7113,12 +7148,12 @@ snapshots: transitivePeerDependencies: - supports-color - '@astrojs/mdx@5.0.4(astro@6.3.1(@netlify/blobs@10.5.0)(@types/node@25.0.10)(db0@0.3.1)(ioredis@5.6.0)(jiti@2.6.1)(rollup@4.60.3)(terser@5.39.0)(yaml@2.8.2))': + '@astrojs/mdx@5.0.4(astro@6.3.1(@netlify/blobs@10.5.0)(@types/node@25.0.10)(db0@0.3.1)(ioredis@5.6.0)(jiti@2.6.1)(rollup@4.60.3)(terser@5.39.0)(yaml@2.9.0))': dependencies: '@astrojs/markdown-remark': 7.1.1 '@mdx-js/mdx': 3.1.1 acorn: 8.16.0 - astro: 6.3.1(@netlify/blobs@10.5.0)(@types/node@25.0.10)(db0@0.3.1)(ioredis@5.6.0)(jiti@2.6.1)(rollup@4.60.3)(terser@5.39.0)(yaml@2.8.2) + astro: 6.3.1(@netlify/blobs@10.5.0)(@types/node@25.0.10)(db0@0.3.1)(ioredis@5.6.0)(jiti@2.6.1)(rollup@4.60.3)(terser@5.39.0)(yaml@2.9.0) es-module-lexer: 2.1.0 estree-util-visit: 2.0.0 hast-util-to-html: 9.0.5 @@ -7142,17 +7177,17 @@ snapshots: stream-replace-string: 2.0.0 zod: 4.4.3 - '@astrojs/starlight@0.39.1(astro@6.3.1(@netlify/blobs@10.5.0)(@types/node@25.0.10)(db0@0.3.1)(ioredis@5.6.0)(jiti@2.6.1)(rollup@4.60.3)(terser@5.39.0)(yaml@2.8.2))(typescript@5.9.3)': + '@astrojs/starlight@0.39.1(astro@6.3.1(@netlify/blobs@10.5.0)(@types/node@25.0.10)(db0@0.3.1)(ioredis@5.6.0)(jiti@2.6.1)(rollup@4.60.3)(terser@5.39.0)(yaml@2.9.0))(typescript@5.9.3)': dependencies: '@astrojs/markdown-remark': 7.1.1 - '@astrojs/mdx': 5.0.4(astro@6.3.1(@netlify/blobs@10.5.0)(@types/node@25.0.10)(db0@0.3.1)(ioredis@5.6.0)(jiti@2.6.1)(rollup@4.60.3)(terser@5.39.0)(yaml@2.8.2)) + '@astrojs/mdx': 5.0.4(astro@6.3.1(@netlify/blobs@10.5.0)(@types/node@25.0.10)(db0@0.3.1)(ioredis@5.6.0)(jiti@2.6.1)(rollup@4.60.3)(terser@5.39.0)(yaml@2.9.0)) '@astrojs/sitemap': 3.7.2 '@pagefind/default-ui': 1.5.2 '@types/hast': 3.0.4 '@types/js-yaml': 4.0.9 '@types/mdast': 4.0.4 - astro: 6.3.1(@netlify/blobs@10.5.0)(@types/node@25.0.10)(db0@0.3.1)(ioredis@5.6.0)(jiti@2.6.1)(rollup@4.60.3)(terser@5.39.0)(yaml@2.8.2) - astro-expressive-code: 0.42.0(astro@6.3.1(@netlify/blobs@10.5.0)(@types/node@25.0.10)(db0@0.3.1)(ioredis@5.6.0)(jiti@2.6.1)(rollup@4.60.3)(terser@5.39.0)(yaml@2.8.2)) + astro: 6.3.1(@netlify/blobs@10.5.0)(@types/node@25.0.10)(db0@0.3.1)(ioredis@5.6.0)(jiti@2.6.1)(rollup@4.60.3)(terser@5.39.0)(yaml@2.9.0) + astro-expressive-code: 0.42.0(astro@6.3.1(@netlify/blobs@10.5.0)(@types/node@25.0.10)(db0@0.3.1)(ioredis@5.6.0)(jiti@2.6.1)(rollup@4.60.3)(terser@5.39.0)(yaml@2.9.0)) bcp-47: 2.1.0 hast-util-from-html: 2.0.3 hast-util-select: 6.0.4 @@ -7934,7 +7969,7 @@ snapshots: minimatch: 9.0.5 read-pkg: 9.0.1 semver: 7.7.2 - yaml: 2.8.2 + yaml: 2.9.0 yargs: 17.7.2 '@netlify/build@35.5.10(@opentelemetry/api@1.8.0)(@types/node@25.0.10)(picomatch@4.0.3)(rollup@4.60.3)': @@ -7988,7 +8023,7 @@ snapshots: ts-node: 10.9.2(@types/node@25.0.10)(typescript@5.9.3) typescript: 5.9.3 uuid: 11.1.0 - yaml: 2.8.2 + yaml: 2.9.0 yargs: 17.7.2 zod: 3.25.76 transitivePeerDependencies: @@ -8033,7 +8068,7 @@ snapshots: read-package-up: 11.0.0 tomlify-j0.4: 3.0.0 validate-npm-package-name: 5.0.1 - yaml: 2.8.2 + yaml: 2.9.0 yargs: 17.7.2 zod: 4.3.6 @@ -8602,7 +8637,7 @@ snapshots: dependencies: '@types/estree': 1.0.8 estree-walker: 2.0.2 - picomatch: 4.0.3 + picomatch: 4.0.4 optionalDependencies: rollup: 4.60.3 @@ -9034,29 +9069,29 @@ snapshots: - rollup - supports-color - '@vitest/browser-playwright@4.0.18(playwright@1.61.1)(vite@7.3.1(@types/node@25.0.10)(jiti@2.6.1)(terser@5.39.0)(yaml@2.8.2))(vitest@4.0.18)': + '@vitest/browser-playwright@4.0.18(playwright@1.61.1)(vite@7.3.1(@types/node@25.0.10)(jiti@2.6.1)(terser@5.39.0)(yaml@2.9.0))(vitest@4.0.18)': dependencies: - '@vitest/browser': 4.0.18(vite@7.3.1(@types/node@25.0.10)(jiti@2.6.1)(terser@5.39.0)(yaml@2.8.2))(vitest@4.0.18) - '@vitest/mocker': 4.0.18(vite@7.3.1(@types/node@25.0.10)(jiti@2.6.1)(terser@5.39.0)(yaml@2.8.2)) + '@vitest/browser': 4.0.18(vite@7.3.1(@types/node@25.0.10)(jiti@2.6.1)(terser@5.39.0)(yaml@2.9.0))(vitest@4.0.18) + '@vitest/mocker': 4.0.18(vite@7.3.1(@types/node@25.0.10)(jiti@2.6.1)(terser@5.39.0)(yaml@2.9.0)) playwright: 1.61.1 tinyrainbow: 3.0.3 - vitest: 4.0.18(@opentelemetry/api@1.8.0)(@types/node@25.0.10)(@vitest/browser-playwright@4.0.18)(happy-dom@20.3.7)(jiti@2.6.1)(terser@5.39.0)(yaml@2.8.2) + vitest: 4.0.18(@opentelemetry/api@1.8.0)(@types/node@25.0.10)(@vitest/browser-playwright@4.0.18)(happy-dom@20.3.7)(jiti@2.6.1)(terser@5.39.0)(yaml@2.9.0) transitivePeerDependencies: - bufferutil - msw - utf-8-validate - vite - '@vitest/browser@4.0.18(vite@7.3.1(@types/node@25.0.10)(jiti@2.6.1)(terser@5.39.0)(yaml@2.8.2))(vitest@4.0.18)': + '@vitest/browser@4.0.18(vite@7.3.1(@types/node@25.0.10)(jiti@2.6.1)(terser@5.39.0)(yaml@2.9.0))(vitest@4.0.18)': dependencies: - '@vitest/mocker': 4.0.18(vite@7.3.1(@types/node@25.0.10)(jiti@2.6.1)(terser@5.39.0)(yaml@2.8.2)) + '@vitest/mocker': 4.0.18(vite@7.3.1(@types/node@25.0.10)(jiti@2.6.1)(terser@5.39.0)(yaml@2.9.0)) '@vitest/utils': 4.0.18 magic-string: 0.30.21 pixelmatch: 7.1.0 pngjs: 7.0.0 sirv: 3.0.2 tinyrainbow: 3.0.3 - vitest: 4.0.18(@opentelemetry/api@1.8.0)(@types/node@25.0.10)(@vitest/browser-playwright@4.0.18)(happy-dom@20.3.7)(jiti@2.6.1)(terser@5.39.0)(yaml@2.8.2) + vitest: 4.0.18(@opentelemetry/api@1.8.0)(@types/node@25.0.10)(@vitest/browser-playwright@4.0.18)(happy-dom@20.3.7)(jiti@2.6.1)(terser@5.39.0)(yaml@2.9.0) ws: 8.19.0 transitivePeerDependencies: - bufferutil @@ -9064,7 +9099,7 @@ snapshots: - utf-8-validate - vite - '@vitest/coverage-v8@4.0.18(@vitest/browser@4.0.18(vite@7.3.1(@types/node@25.0.10)(jiti@2.6.1)(terser@5.39.0)(yaml@2.8.2))(vitest@4.0.18))(vitest@4.0.18)': + '@vitest/coverage-v8@4.0.18(@vitest/browser@4.0.18(vite@7.3.1(@types/node@25.0.10)(jiti@2.6.1)(terser@5.39.0)(yaml@2.9.0))(vitest@4.0.18))(vitest@4.0.18)': dependencies: '@bcoe/v8-coverage': 1.0.2 '@vitest/utils': 4.0.18 @@ -9076,9 +9111,9 @@ snapshots: obug: 2.1.1 std-env: 3.10.0 tinyrainbow: 3.0.3 - vitest: 4.0.18(@opentelemetry/api@1.8.0)(@types/node@25.0.10)(@vitest/browser-playwright@4.0.18)(happy-dom@20.3.7)(jiti@2.6.1)(terser@5.39.0)(yaml@2.8.2) + vitest: 4.0.18(@opentelemetry/api@1.8.0)(@types/node@25.0.10)(@vitest/browser-playwright@4.0.18)(happy-dom@20.3.7)(jiti@2.6.1)(terser@5.39.0)(yaml@2.9.0) optionalDependencies: - '@vitest/browser': 4.0.18(vite@7.3.1(@types/node@25.0.10)(jiti@2.6.1)(terser@5.39.0)(yaml@2.8.2))(vitest@4.0.18) + '@vitest/browser': 4.0.18(vite@7.3.1(@types/node@25.0.10)(jiti@2.6.1)(terser@5.39.0)(yaml@2.9.0))(vitest@4.0.18) '@vitest/expect@4.0.18': dependencies: @@ -9089,13 +9124,13 @@ snapshots: chai: 6.2.2 tinyrainbow: 3.0.3 - '@vitest/mocker@4.0.18(vite@7.3.1(@types/node@25.0.10)(jiti@2.6.1)(terser@5.39.0)(yaml@2.8.2))': + '@vitest/mocker@4.0.18(vite@7.3.1(@types/node@25.0.10)(jiti@2.6.1)(terser@5.39.0)(yaml@2.9.0))': dependencies: '@vitest/spy': 4.0.18 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 7.3.1(@types/node@25.0.10)(jiti@2.6.1)(terser@5.39.0)(yaml@2.8.2) + vite: 7.3.1(@types/node@25.0.10)(jiti@2.6.1)(terser@5.39.0)(yaml@2.9.0) '@vitest/pretty-format@4.0.18': dependencies: @@ -9421,12 +9456,12 @@ snapshots: astring@1.9.0: {} - astro-expressive-code@0.42.0(astro@6.3.1(@netlify/blobs@10.5.0)(@types/node@25.0.10)(db0@0.3.1)(ioredis@5.6.0)(jiti@2.6.1)(rollup@4.60.3)(terser@5.39.0)(yaml@2.8.2)): + astro-expressive-code@0.42.0(astro@6.3.1(@netlify/blobs@10.5.0)(@types/node@25.0.10)(db0@0.3.1)(ioredis@5.6.0)(jiti@2.6.1)(rollup@4.60.3)(terser@5.39.0)(yaml@2.9.0)): dependencies: - astro: 6.3.1(@netlify/blobs@10.5.0)(@types/node@25.0.10)(db0@0.3.1)(ioredis@5.6.0)(jiti@2.6.1)(rollup@4.60.3)(terser@5.39.0)(yaml@2.8.2) + astro: 6.3.1(@netlify/blobs@10.5.0)(@types/node@25.0.10)(db0@0.3.1)(ioredis@5.6.0)(jiti@2.6.1)(rollup@4.60.3)(terser@5.39.0)(yaml@2.9.0) rehype-expressive-code: 0.42.0 - astro@6.3.1(@netlify/blobs@10.5.0)(@types/node@25.0.10)(db0@0.3.1)(ioredis@5.6.0)(jiti@2.6.1)(rollup@4.60.3)(terser@5.39.0)(yaml@2.8.2): + astro@6.3.1(@netlify/blobs@10.5.0)(@types/node@25.0.10)(db0@0.3.1)(ioredis@5.6.0)(jiti@2.6.1)(rollup@4.60.3)(terser@5.39.0)(yaml@2.9.0): dependencies: '@astrojs/compiler': 4.0.0 '@astrojs/internal-helpers': 0.9.0 @@ -9478,8 +9513,8 @@ snapshots: unist-util-visit: 5.1.0 unstorage: 1.17.5(@netlify/blobs@10.5.0)(db0@0.3.1)(ioredis@5.6.0) vfile: 6.0.3 - vite: 7.3.3(@types/node@25.0.10)(jiti@2.6.1)(terser@5.39.0)(yaml@2.8.2) - vitefu: 1.1.3(vite@7.3.3(@types/node@25.0.10)(jiti@2.6.1)(terser@5.39.0)(yaml@2.8.2)) + vite: 7.3.3(@types/node@25.0.10)(jiti@2.6.1)(terser@5.39.0)(yaml@2.9.0) + vitefu: 1.1.3(vite@7.3.3(@types/node@25.0.10)(jiti@2.6.1)(terser@5.39.0)(yaml@2.9.0)) xxhash-wasm: 1.1.0 yargs-parser: 22.0.0 zod: 4.4.3 @@ -9839,6 +9874,11 @@ snapshots: cli-spinners@3.4.0: {} + cli-truncate@5.2.0: + dependencies: + slice-ansi: 8.0.0 + string-width: 8.2.1 + cli-width@3.0.0: {} cli-width@4.1.0: {} @@ -10923,6 +10963,8 @@ snapshots: get-east-asian-width@1.4.0: {} + get-east-asian-width@1.6.0: {} + get-intrinsic@1.3.0: dependencies: call-bind-apply-helpers: 1.0.2 @@ -11836,6 +11878,15 @@ snapshots: lilconfig@3.1.3: {} + lint-staged@17.0.8: + dependencies: + listr2: 10.2.2 + picomatch: 4.0.4 + string-argv: 0.3.2 + tinyexec: 1.2.4 + optionalDependencies: + yaml: 2.9.0 + listhen@1.9.0: dependencies: '@parcel/watcher': 2.5.6 @@ -11857,6 +11908,14 @@ snapshots: untun: 0.1.3 uqr: 0.1.2 + listr2@10.2.2: + dependencies: + cli-truncate: 5.2.0 + eventemitter3: 5.0.4 + log-update: 6.1.0 + rfdc: 1.4.1 + wrap-ansi: 10.0.0 + locate-path@6.0.0: dependencies: p-locate: 5.0.0 @@ -14073,6 +14132,11 @@ snapshots: ansi-styles: 6.2.3 is-fullwidth-code-point: 5.1.0 + slice-ansi@8.0.0: + dependencies: + ansi-styles: 6.2.3 + is-fullwidth-code-point: 5.1.0 + smart-buffer@4.2.0: {} smol-toml@1.6.1: {} @@ -14181,6 +14245,8 @@ snapshots: optionalDependencies: bare-events: 2.5.4 + string-argv@0.3.2: {} + string-width@2.1.1: dependencies: is-fullwidth-code-point: 2.0.0 @@ -14209,6 +14275,11 @@ snapshots: get-east-asian-width: 1.4.0 strip-ansi: 7.1.2 + string-width@8.2.1: + dependencies: + get-east-asian-width: 1.6.0 + strip-ansi: 7.1.2 + string_decoder@1.1.1: dependencies: safe-buffer: 5.1.2 @@ -14367,6 +14438,8 @@ snapshots: tinyexec@1.1.2: {} + tinyexec@1.2.4: {} + tinyglobby@0.2.15: dependencies: fdir: 6.5.0(picomatch@4.0.3) @@ -14695,11 +14768,11 @@ snapshots: '@types/unist': 3.0.3 vfile-message: 4.0.3 - vite@7.3.1(@types/node@25.0.10)(jiti@2.6.1)(terser@5.39.0)(yaml@2.8.2): + vite@7.3.1(@types/node@25.0.10)(jiti@2.6.1)(terser@5.39.0)(yaml@2.9.0): dependencies: esbuild: 0.27.2 - fdir: 6.5.0(picomatch@4.0.3) - picomatch: 4.0.3 + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 postcss: 8.5.6 rollup: 4.56.0 tinyglobby: 0.2.15 @@ -14708,22 +14781,7 @@ snapshots: fsevents: 2.3.3 jiti: 2.6.1 terser: 5.39.0 - yaml: 2.8.2 - - vite@7.3.3(@types/node@25.0.10)(jiti@2.6.1)(terser@5.39.0)(yaml@2.8.2): - dependencies: - esbuild: 0.27.7 - fdir: 6.5.0(picomatch@4.0.4) - picomatch: 4.0.4 - postcss: 8.5.14 - rollup: 4.60.3 - tinyglobby: 0.2.16 - optionalDependencies: - '@types/node': 25.0.10 - fsevents: 2.3.3 - jiti: 2.6.1 - terser: 5.39.0 - yaml: 2.8.2 + yaml: 2.9.0 vite@7.3.3(@types/node@25.0.10)(jiti@2.6.1)(terser@5.39.0)(yaml@2.9.0): dependencies: @@ -14740,14 +14798,14 @@ snapshots: terser: 5.39.0 yaml: 2.9.0 - vitefu@1.1.3(vite@7.3.3(@types/node@25.0.10)(jiti@2.6.1)(terser@5.39.0)(yaml@2.8.2)): + vitefu@1.1.3(vite@7.3.3(@types/node@25.0.10)(jiti@2.6.1)(terser@5.39.0)(yaml@2.9.0)): optionalDependencies: - vite: 7.3.3(@types/node@25.0.10)(jiti@2.6.1)(terser@5.39.0)(yaml@2.8.2) + vite: 7.3.3(@types/node@25.0.10)(jiti@2.6.1)(terser@5.39.0)(yaml@2.9.0) - vitest@4.0.18(@opentelemetry/api@1.8.0)(@types/node@25.0.10)(@vitest/browser-playwright@4.0.18)(happy-dom@20.3.7)(jiti@2.6.1)(terser@5.39.0)(yaml@2.8.2): + vitest@4.0.18(@opentelemetry/api@1.8.0)(@types/node@25.0.10)(@vitest/browser-playwright@4.0.18)(happy-dom@20.3.7)(jiti@2.6.1)(terser@5.39.0)(yaml@2.9.0): dependencies: '@vitest/expect': 4.0.18 - '@vitest/mocker': 4.0.18(vite@7.3.1(@types/node@25.0.10)(jiti@2.6.1)(terser@5.39.0)(yaml@2.8.2)) + '@vitest/mocker': 4.0.18(vite@7.3.1(@types/node@25.0.10)(jiti@2.6.1)(terser@5.39.0)(yaml@2.9.0)) '@vitest/pretty-format': 4.0.18 '@vitest/runner': 4.0.18 '@vitest/snapshot': 4.0.18 @@ -14764,12 +14822,12 @@ snapshots: tinyexec: 1.0.2 tinyglobby: 0.2.15 tinyrainbow: 3.0.3 - vite: 7.3.1(@types/node@25.0.10)(jiti@2.6.1)(terser@5.39.0)(yaml@2.8.2) + vite: 7.3.1(@types/node@25.0.10)(jiti@2.6.1)(terser@5.39.0)(yaml@2.9.0) why-is-node-running: 2.3.0 optionalDependencies: '@opentelemetry/api': 1.8.0 '@types/node': 25.0.10 - '@vitest/browser-playwright': 4.0.18(playwright@1.61.1)(vite@7.3.1(@types/node@25.0.10)(jiti@2.6.1)(terser@5.39.0)(yaml@2.8.2))(vitest@4.0.18) + '@vitest/browser-playwright': 4.0.18(playwright@1.61.1)(vite@7.3.1(@types/node@25.0.10)(jiti@2.6.1)(terser@5.39.0)(yaml@2.9.0))(vitest@4.0.18) happy-dom: 20.3.7 transitivePeerDependencies: - jiti @@ -14862,6 +14920,12 @@ snapshots: wordwrap@0.0.3: {} + wrap-ansi@10.0.0: + dependencies: + ansi-styles: 6.2.3 + string-width: 8.2.1 + strip-ansi: 7.1.2 + wrap-ansi@6.2.0: dependencies: ansi-styles: 4.3.0 @@ -14918,8 +14982,6 @@ snapshots: yallist@5.0.0: {} - yaml@2.8.2: {} - yaml@2.9.0: {} yargs-parser@21.1.1: {} diff --git a/test/e2e/README.md b/test/e2e/README.md index 2bf1e95..1a39010 100644 --- a/test/e2e/README.md +++ b/test/e2e/README.md @@ -1,24 +1,45 @@ # End-to-end example tests -These specs load each `demo/examples/*` component into a **real browser** (Chromium -via Playwright) and drive it the way a user would. They exist because the v5 -correctness work targets behaviors that happy-dom cannot reproduce — custom -element upgrade ordering, the constructor-attribute rule, empty-string/removed -attributes, and adopted stylesheets — so the unit suite alone can pass while a -real browser misbehaves. +These specs load each `demo/examples/*` component into a **real browser** +(Chromium, Firefox, and WebKit via Playwright) and drive it the way a user +would. They exist because the v5 correctness work targets behaviors that +happy-dom cannot reproduce — custom element upgrade ordering, the +constructor-attribute rule, empty-string/removed attributes, and adopted +stylesheets — so the unit suite alone can pass while a real browser misbehaves. +Running across engines also catches cross-browser differences. ## Running ```sh -pnpm test:e2e # browser specs only (this folder) -pnpm test # fast unit suite (happy-dom), unchanged -pnpm test:all # both +pnpm test:e2e # default: Chromium only (fast, universal) +pnpm test:e2e:chromium # Chromium +pnpm test:e2e:firefox # Firefox +pnpm test:e2e:webkit # WebKit (the engine behind Safari) +pnpm test:e2e:all # Chromium + Firefox + WebKit + +pnpm test # fast unit suite (happy-dom), unchanged +pnpm test:all # unit + default e2e ``` +`pnpm test:e2e` stays on Chromium so it works everywhere with minimal setup; use +`test:e2e:all` (or a CI matrix) for the full cross-browser run. Each script just +sets the `E2E_BROWSERS` env var (comma-separated), which the config reads. + Config lives in `vitest.e2e.config.mjs` (Vitest browser mode + the -`@vitest/browser-playwright` provider). It is kept separate from -`vitest.config.mjs` so `pnpm test` stays fast and needs no browser. The -Chromium binary is installed with `npx playwright install chromium`. +`@vitest/browser-playwright` provider), kept separate from `vitest.config.mjs` +so `pnpm test` stays fast and needs no browser. + +### Browser setup + +```sh +npx playwright install chromium firefox webkit # download the browsers +npx playwright install-deps # Linux only: system libraries +``` + +Firefox and especially WebKit need extra OS libraries on Linux (`libwoff1`, +etc.); `playwright install-deps` requires root. Without them those engines fail +to launch — which is why the default target is Chromium and CI installs the deps +before running `test:e2e:all`. ## Coverage diff --git a/vitest.e2e.config.mjs b/vitest.e2e.config.mjs index 79c5f91..74f62c3 100644 --- a/vitest.e2e.config.mjs +++ b/vitest.e2e.config.mjs @@ -4,11 +4,21 @@ import { fileURLToPath } from 'node:url' const src = (p) => fileURLToPath(new URL(`./src/${p}`, import.meta.url)) -// End-to-end specs run against the real `src/` in a real browser (Chromium via +// Which engines to run the e2e suite against. Defaults to all three Playwright +// engines (Chromium, Firefox, and WebKit — the engine behind Safari). Override +// with e.g. `E2E_BROWSERS=chromium pnpm test:e2e` in environments that only +// have some browsers installed (Firefox/WebKit also need system libs — see +// `npx playwright install-deps`). +const BROWSERS = (process.env.E2E_BROWSERS || 'chromium,firefox,webkit') + .split(',') + .map((name) => name.trim()) + .filter(Boolean) + +// End-to-end specs run against the real `src/` in a real browser (via // Playwright), so behaviors that happy-dom cannot reproduce — custom-element // upgrade ordering, constructor attribute rules, adopted stylesheets — are -// actually exercised. Kept separate from the unit config so `pnpm test` stays -// fast and headless-browser-free. +// actually exercised across engines. Kept separate from the unit config so +// `pnpm test` stays fast and headless-browser-free. export default defineConfig({ // The demo examples import the `web-component-base` package; resolve it to // local source (mirrors demo/vite.config.js) so e2e tests exercise src. @@ -29,7 +39,7 @@ export default defineConfig({ provider: playwright(), headless: true, screenshotFailures: false, - instances: [{ browser: 'chromium' }], + instances: BROWSERS.map((browser) => ({ browser })), }, }, })