From 9e24a66185016bdbd41caae348a4ba5e4e3eb210 Mon Sep 17 00:00:00 2001 From: 1natsu <1natsu172@users.noreply.github.com> Date: Sat, 3 Oct 2026 22:38:02 +0900 Subject: [PATCH 1/5] test: run the test suite on real browsers with Vitest browser mode jsdom only approximates how real engines deliver MutationObserver records and run microtask checkpoints, which this library depends on. Run every test file on Chromium, Firefox and WebKit through the Playwright provider instead, and drop jsdom. - Replace node:timers/promises with a local delay helper, since Vite externalizes node:* modules in the browser and the import fails. - Keep the per-test sandbox element: browser mode runs each test file in its own iframe, but the tests in a file still share one document. - Limit pnpm coverage to Chromium, because the v8 provider refuses to start with several browser instances. Co-Authored-By: Claude Opus 5.5 (1M context) --- package.json | 6 +- pnpm-lock.yaml | 203 +++++++++++++----- src/index.test.ts | 14 +- .../setupFiles/parepareCleanSandboxElement.ts | 6 +- vitest.config.ts | 12 +- 5 files changed, 178 insertions(+), 63 deletions(-) diff --git a/package.json b/package.json index 85fa577..4add0ae 100644 --- a/package.json +++ b/package.json @@ -58,10 +58,10 @@ }, "devDependencies": { "@biomejs/biome": "2.5.14", - "@types/jsdom": "30.0.0", + "@vitest/browser-playwright": "5.0.2", "@vitest/coverage-v8": "5.0.2", "@vitest/ui": "5.0.2", - "jsdom": "30.1.1", + "playwright": "1.63.0", "typescript": "6.0.3", "unbuild": "3.6.1", "vitest": "5.0.2" @@ -69,7 +69,7 @@ "scripts": { "test": "vitest", "test-ci": "vitest run", - "coverage": "vitest --coverage --ui", + "coverage": "vitest --coverage --ui --project chromium", "biome": "biome check --write ./src", "typecheck": "tsc --noEmit", "build": "unbuild", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index abf4cc5..8fe00f7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -176,18 +176,18 @@ importers: '@biomejs/biome': specifier: 2.5.14 version: 2.5.14 - '@types/jsdom': - specifier: 30.0.0 - version: 30.0.0 + '@vitest/browser-playwright': + specifier: 5.0.2 + version: 5.0.2(playwright@1.63.0)(vite@8.3.1(@types/node@22.15.17)(jiti@2.5.1))(vitest@5.0.2) '@vitest/coverage-v8': specifier: 5.0.2 - version: 5.0.2(vitest@5.0.2) + version: 5.0.2(@vitest/browser@5.0.2)(vitest@5.0.2) '@vitest/ui': specifier: 5.0.2 version: 5.0.2(vitest@5.0.2) - jsdom: - specifier: 30.1.1 - version: 30.1.1 + playwright: + specifier: 1.63.0 + version: 1.63.0 typescript: specifier: 6.0.3 version: 6.0.3 @@ -196,7 +196,7 @@ importers: version: 3.6.1(typescript@6.0.3) vitest: specifier: 5.0.2 - version: 5.0.2(@types/node@22.15.17)(@vitest/coverage-v8@5.0.2)(@vitest/ui@5.0.2)(jsdom@30.1.1)(vite@8.3.1(@types/node@22.15.17)(jiti@2.5.1)) + version: 5.0.2(@types/node@22.15.17)(@vitest/browser-playwright@5.0.2)(@vitest/coverage-v8@5.0.2)(@vitest/ui@5.0.2)(jsdom@30.1.1)(vite@8.3.1(@types/node@22.15.17)(jiti@2.5.1)) packages: @@ -294,6 +294,9 @@ packages: cpu: [x64] os: [win32] + '@blazediff/core@1.10.0': + resolution: {integrity: sha512-AOQff0zgR7cGsZL+4E7hVkmujoPUpm0J9xzWGWZj5wCjd3gmxESXAPfKyuzs93VdpQNFhHlBhfOjrcZ+XTERtQ==} + '@bramus/specificity@2.4.2': resolution: {integrity: sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==} hasBin: true @@ -831,17 +834,22 @@ packages: '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} - '@types/jsdom@30.0.0': - resolution: {integrity: sha512-uAHGxujGE0cDaKGdK28zgDotFtNA7MKq5DXl8LrfdxdCI8VHcg15oJz+amHTChPNI5JpgEPQWc2xFdrw3em/nQ==} - '@types/node@22.15.17': resolution: {integrity: sha512-wIX2aSZL5FE+MR0JlvF87BNVrtFWf6AE6rxSE9X7OwnVvoyCQjpzSRJ+M87se/4QCkCiebQAqrJ0y6fwIyi7nw==} '@types/resolve@1.20.2': resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==} - '@types/tough-cookie@4.0.5': - resolution: {integrity: sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==} + '@vitest/browser-playwright@5.0.2': + resolution: {integrity: sha512-wU0B1Zy9+a55fdUT7JBCcnnjpTx0HHvUlQT6yFnuwCSY244BkPPwabNX+9PAljc96gRELcfm5APtnsn/3qiaPw==} + peerDependencies: + playwright: '*' + vitest: 5.0.2 + + '@vitest/browser@5.0.2': + resolution: {integrity: sha512-nX9xXZ/j8YfU7KTeFa+tpSP331SbI81qBQTASRlcopYeaTgBerNmJryzHXHrFOW0NYldd3Dn5gw651zGnmaQzw==} + peerDependencies: + vitest: 5.0.2 '@vitest/coverage-v8@5.0.2': resolution: {integrity: sha512-3ffHBEi8DOOBLwIGBhOBZbRfYFYWjMUuxZicONdhuFEFEG5AVsMOSrVeuROskqnqpbOmEJwxM2eTVZ9N3a1t+A==} @@ -1333,6 +1341,20 @@ packages: pkg-types@2.2.0: resolution: {integrity: sha512-2SM/GZGAEkPp3KWORxQZns4M+WSeXbC2HEvmOIJe3Cmiv6ieAJvdVhDldtHqM5J1Y7MrR1XhkBT/rMlhh9FdqQ==} + playwright-core@1.63.0: + resolution: {integrity: sha512-rYCsBF/M5HjUch52bbtVONEFjv6Xu8sm8h72dNlR5bzIE1fvC/bxgspzkjSfU+MweEMmPM8KJebG6nnyxo5mCg==} + engines: {node: '>=20'} + hasBin: true + + playwright@1.63.0: + resolution: {integrity: sha512-+7ziBLidS4NaNCdt57SUDT+wYmmd5fmiQejUic/kb+YsYSCPyOOE9sebzMjNmQrsnNpDJqd4WHvV/8lfKfUDUg==} + engines: {node: '>=20'} + hasBin: true + + pngjs@7.0.0: + resolution: {integrity: sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==} + engines: {node: '>=14.19.0'} + postcss-calc@10.1.1: resolution: {integrity: sha512-NYEsLHh8DgG/PRH2+G9BTuUdtf9ViS+vdoQ0YA5OQdGsfN4ztiwtDWNtBl9EKeqNMFnIu8IKZ0cLxEQ5r5KVMw==} engines: {node: ^18.12 || ^20.9 || >=22.0} @@ -1647,9 +1669,6 @@ packages: undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} - undici-types@8.11.2: - resolution: {integrity: sha512-iMVNmWZ0leK/goS6eXMizSzmm9CDWtyphwbaCms3DNLqRxDL+mMoNVcZMTyyVgXP0N+Z8neAMzDoUOUJL8veKg==} - undici@8.11.2: resolution: {integrity: sha512-u4UB2/IrKdU6lFxumHmmo1a3fCQO5tzQllRorfoRS63txhrB7xTpSn1PftwC4qEHkOaqP95fCWW4lJzwErwzhQ==} engines: {node: '>=22.19.0'} @@ -1776,6 +1795,18 @@ packages: engines: {node: '>=20.11'} hasBin: true + ws@8.21.3: + resolution: {integrity: sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + xml-name-validator@5.0.0: resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} engines: {node: '>=18'} @@ -1792,6 +1823,7 @@ snapshots: '@csstools/css-parser-algorithms': 4.0.1(@csstools/css-tokenizer@4.0.2) '@csstools/css-tokenizer': 4.0.2 lru-cache: 11.5.3 + optional: true '@asamuzakjp/dom-selector@9.2.1': dependencies: @@ -1799,6 +1831,7 @@ snapshots: css-tree: 3.2.1 is-potential-custom-element-name: 1.0.1 lru-cache: 11.5.3 + optional: true '@babel/code-frame@7.27.1': dependencies: @@ -1860,16 +1893,21 @@ snapshots: '@biomejs/cli-win32-x64@2.5.14': optional: true + '@blazediff/core@1.10.0': {} + '@bramus/specificity@2.4.2': dependencies: css-tree: 3.2.1 + optional: true - '@csstools/color-helpers@6.1.2': {} + '@csstools/color-helpers@6.1.2': + optional: true '@csstools/css-calc@3.4.1(@csstools/css-parser-algorithms@4.0.1(@csstools/css-tokenizer@4.0.2))(@csstools/css-tokenizer@4.0.2)': dependencies: '@csstools/css-parser-algorithms': 4.0.1(@csstools/css-tokenizer@4.0.2) '@csstools/css-tokenizer': 4.0.2 + optional: true '@csstools/css-color-parser@4.2.4(@csstools/css-parser-algorithms@4.0.1(@csstools/css-tokenizer@4.0.2))(@csstools/css-tokenizer@4.0.2)': dependencies: @@ -1877,16 +1915,20 @@ snapshots: '@csstools/css-calc': 3.4.1(@csstools/css-parser-algorithms@4.0.1(@csstools/css-tokenizer@4.0.2))(@csstools/css-tokenizer@4.0.2) '@csstools/css-parser-algorithms': 4.0.1(@csstools/css-tokenizer@4.0.2) '@csstools/css-tokenizer': 4.0.2 + optional: true '@csstools/css-parser-algorithms@4.0.1(@csstools/css-tokenizer@4.0.2)': dependencies: '@csstools/css-tokenizer': 4.0.2 + optional: true '@csstools/css-syntax-patches-for-csstree@1.1.14(css-tree@3.2.1)': optionalDependencies: css-tree: 3.2.1 + optional: true - '@csstools/css-tokenizer@4.0.2': {} + '@csstools/css-tokenizer@4.0.2': + optional: true '@esbuild/aix-ppc64@0.25.12': optional: true @@ -1966,9 +2008,11 @@ snapshots: '@esbuild/win32-x64@0.25.12': optional: true - '@exodus/bytes@1.15.0': {} + '@exodus/bytes@1.15.0': + optional: true - '@exodus/bytes@1.16.0': {} + '@exodus/bytes@1.16.0': + optional: true '@jridgewell/resolve-uri@3.1.2': {} @@ -2164,22 +2208,45 @@ snapshots: '@types/estree@1.0.8': {} - '@types/jsdom@30.0.0': - dependencies: - '@types/node': 22.15.17 - '@types/tough-cookie': 4.0.5 - parse5: 8.0.1 - undici-types: 8.11.2 - '@types/node@22.15.17': dependencies: undici-types: 6.21.0 + optional: true '@types/resolve@1.20.2': {} - '@types/tough-cookie@4.0.5': {} + '@vitest/browser-playwright@5.0.2(playwright@1.63.0)(vite@8.3.1(@types/node@22.15.17)(jiti@2.5.1))(vitest@5.0.2)': + dependencies: + '@vitest/browser': 5.0.2(vite@8.3.1(@types/node@22.15.17)(jiti@2.5.1))(vitest@5.0.2) + '@vitest/mocker': 5.0.2(vite@8.3.1(@types/node@22.15.17)(jiti@2.5.1)) + playwright: 1.63.0 + tinyrainbow: 3.1.1 + vitest: 5.0.2(@types/node@22.15.17)(@vitest/browser-playwright@5.0.2)(@vitest/coverage-v8@5.0.2)(@vitest/ui@5.0.2)(jsdom@30.1.1)(vite@8.3.1(@types/node@22.15.17)(jiti@2.5.1)) + transitivePeerDependencies: + - bufferutil + - msw + - utf-8-validate + - vite + + '@vitest/browser@5.0.2(vite@8.3.1(@types/node@22.15.17)(jiti@2.5.1))(vitest@5.0.2)': + dependencies: + '@blazediff/core': 1.10.0 + '@vitest/mocker': 5.0.2(vite@8.3.1(@types/node@22.15.17)(jiti@2.5.1)) + '@vitest/ui': 5.0.2(vitest@5.0.2) + '@vitest/utils': 5.0.2 + magic-string: 1.4.2 + pngjs: 7.0.0 + sirv: 3.0.2 + tinyrainbow: 3.1.1 + vitest: 5.0.2(@types/node@22.15.17)(@vitest/browser-playwright@5.0.2)(@vitest/coverage-v8@5.0.2)(@vitest/ui@5.0.2)(jsdom@30.1.1)(vite@8.3.1(@types/node@22.15.17)(jiti@2.5.1)) + ws: 8.21.3 + transitivePeerDependencies: + - bufferutil + - msw + - utf-8-validate + - vite - '@vitest/coverage-v8@5.0.2(vitest@5.0.2)': + '@vitest/coverage-v8@5.0.2(@vitest/browser@5.0.2)(vitest@5.0.2)': dependencies: '@bcoe/v8-coverage': 1.0.2 '@vitest/istanbul-lib-coverage': 1.0.1 @@ -2189,7 +2256,9 @@ snapshots: obug: 2.2.1 std-env: 4.2.0 tinyrainbow: 3.1.1 - vitest: 5.0.2(@types/node@22.15.17)(@vitest/coverage-v8@5.0.2)(@vitest/ui@5.0.2)(jsdom@30.1.1)(vite@8.3.1(@types/node@22.15.17)(jiti@2.5.1)) + vitest: 5.0.2(@types/node@22.15.17)(@vitest/browser-playwright@5.0.2)(@vitest/coverage-v8@5.0.2)(@vitest/ui@5.0.2)(jsdom@30.1.1)(vite@8.3.1(@types/node@22.15.17)(jiti@2.5.1)) + optionalDependencies: + '@vitest/browser': 5.0.2(vite@8.3.1(@types/node@22.15.17)(jiti@2.5.1))(vitest@5.0.2) '@vitest/istanbul-lib-coverage@1.0.1': {} @@ -2220,7 +2289,7 @@ snapshots: pathe: 2.0.3 sirv: 3.0.2 tinyrainbow: 3.1.1 - vitest: 5.0.2(@types/node@22.15.17)(@vitest/coverage-v8@5.0.2)(@vitest/ui@5.0.2)(jsdom@30.1.1)(vite@8.3.1(@types/node@22.15.17)(jiti@2.5.1)) + vitest: 5.0.2(@types/node@22.15.17)(@vitest/browser-playwright@5.0.2)(@vitest/coverage-v8@5.0.2)(@vitest/ui@5.0.2)(jsdom@30.1.1)(vite@8.3.1(@types/node@22.15.17)(jiti@2.5.1)) '@vitest/utils@5.0.2': dependencies: @@ -2249,6 +2318,7 @@ snapshots: bidi-js@1.1.0: dependencies: require-from-string: 2.0.2 + optional: true boolbase@1.0.0: {} @@ -2314,6 +2384,7 @@ snapshots: dependencies: mdn-data: 2.27.1 source-map-js: 1.2.1 + optional: true css-what@6.1.0: {} @@ -2373,8 +2444,10 @@ snapshots: whatwg-url: 16.0.1 transitivePeerDependencies: - '@noble/hashes' + optional: true - decimal.js@10.6.0: {} + decimal.js@10.6.0: + optional: true deepmerge@4.3.1: {} @@ -2404,7 +2477,8 @@ snapshots: entities@4.5.0: {} - entities@8.0.0: {} + entities@8.0.0: + optional: true es-module-lexer@2.3.2: {} @@ -2485,6 +2559,7 @@ snapshots: '@exodus/bytes': 1.16.0 transitivePeerDependencies: - '@noble/hashes' + optional: true is-core-module@2.16.1: dependencies: @@ -2492,7 +2567,8 @@ snapshots: is-module@1.0.0: {} - is-potential-custom-element-name@1.0.1: {} + is-potential-custom-element-name@1.0.1: + optional: true is-reference@1.2.1: dependencies: @@ -2531,6 +2607,7 @@ snapshots: xml-name-validator: 5.0.0 transitivePeerDependencies: - '@noble/hashes' + optional: true knitwork@1.2.0: {} @@ -2589,7 +2666,8 @@ snapshots: lodash.uniq@4.5.0: {} - lru-cache@11.5.3: {} + lru-cache@11.5.3: + optional: true magic-string@0.30.21: dependencies: @@ -2611,7 +2689,8 @@ snapshots: mdn-data@2.0.30: {} - mdn-data@2.27.1: {} + mdn-data@2.27.1: + optional: true mkdist@2.3.0(typescript@6.0.3): dependencies: @@ -2657,6 +2736,7 @@ snapshots: parse5@8.0.1: dependencies: entities: 8.0.0 + optional: true path-parse@1.0.7: {} @@ -2680,6 +2760,14 @@ snapshots: exsolve: 1.0.7 pathe: 2.0.3 + playwright-core@1.63.0: {} + + playwright@1.63.0: + dependencies: + playwright-core: 1.63.0 + + pngjs@7.0.0: {} + postcss-calc@10.1.1(postcss@8.5.9): dependencies: postcss: 8.5.9 @@ -2855,9 +2943,11 @@ snapshots: pretty-bytes@7.0.1: {} - punycode@2.3.1: {} + punycode@2.3.1: + optional: true - require-from-string@2.0.2: {} + require-from-string@2.0.2: + optional: true resolve@1.22.10: dependencies: @@ -2928,6 +3018,7 @@ snapshots: saxes@6.0.0: dependencies: xmlchars: 2.2.0 + optional: true scule@1.3.0: {} @@ -2977,21 +3068,25 @@ snapshots: tinyrainbow@3.1.1: {} - tldts-core@7.0.27: {} + tldts-core@7.0.27: + optional: true tldts@7.0.27: dependencies: tldts-core: 7.0.27 + optional: true totalist@3.0.1: {} tough-cookie@6.0.2: dependencies: tldts: 7.0.27 + optional: true tr46@6.0.0: dependencies: punycode: 2.3.1 + optional: true typescript@6.0.3: {} @@ -3031,11 +3126,11 @@ snapshots: - vue-sfc-transformer - vue-tsc - undici-types@6.21.0: {} - - undici-types@8.11.2: {} + undici-types@6.21.0: + optional: true - undici@8.11.2: {} + undici@8.11.2: + optional: true untyped@2.0.0: dependencies: @@ -3065,7 +3160,7 @@ snapshots: fsevents: 2.3.3 jiti: 2.5.1 - vitest@5.0.2(@types/node@22.15.17)(@vitest/coverage-v8@5.0.2)(@vitest/ui@5.0.2)(jsdom@30.1.1)(vite@8.3.1(@types/node@22.15.17)(jiti@2.5.1)): + vitest@5.0.2(@types/node@22.15.17)(@vitest/browser-playwright@5.0.2)(@vitest/coverage-v8@5.0.2)(@vitest/ui@5.0.2)(jsdom@30.1.1)(vite@8.3.1(@types/node@22.15.17)(jiti@2.5.1)): dependencies: '@types/chai': 5.2.2 '@vitest/mocker': 5.0.2(vite@8.3.1(@types/node@22.15.17)(jiti@2.5.1)) @@ -3083,7 +3178,8 @@ snapshots: why-is-node-running: 3.2.2 optionalDependencies: '@types/node': 22.15.17 - '@vitest/coverage-v8': 5.0.2(vitest@5.0.2) + '@vitest/browser-playwright': 5.0.2(playwright@1.63.0)(vite@8.3.1(@types/node@22.15.17)(jiti@2.5.1))(vitest@5.0.2) + '@vitest/coverage-v8': 5.0.2(@vitest/browser@5.0.2)(vitest@5.0.2) '@vitest/ui': 5.0.2(vitest@5.0.2) jsdom: 30.1.1 transitivePeerDependencies: @@ -3092,10 +3188,13 @@ snapshots: w3c-xmlserializer@6.0.0: dependencies: xml-name-validator: 5.0.0 + optional: true - webidl-conversions@8.0.1: {} + webidl-conversions@8.0.1: + optional: true - whatwg-mimetype@5.0.0: {} + whatwg-mimetype@5.0.0: + optional: true whatwg-url@16.0.1: dependencies: @@ -3104,6 +3203,7 @@ snapshots: webidl-conversions: 8.0.1 transitivePeerDependencies: - '@noble/hashes' + optional: true whatwg-url@17.1.2: dependencies: @@ -3112,9 +3212,14 @@ snapshots: webidl-conversions: 8.0.1 transitivePeerDependencies: - '@noble/hashes' + optional: true why-is-node-running@3.2.2: {} - xml-name-validator@5.0.0: {} + ws@8.21.3: {} + + xml-name-validator@5.0.0: + optional: true - xmlchars@2.2.0: {} + xmlchars@2.2.0: + optional: true diff --git a/src/index.test.ts b/src/index.test.ts index a64ffcd..3db0e4d 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -1,15 +1,15 @@ -import { setTimeout as delay } from "node:timers/promises"; import { assert, beforeEach, describe, test, vi } from "vitest"; import { waitElement } from "./index"; +const delay = (ms: number) => + new Promise((resolve) => setTimeout(resolve, ms)); + const TEST_SANDBOX = "test-sandbox"; /** * @description - * Tests for DOM elements must always be tested against the `sandboxElement`. This is because the global DOM (JSDom) is common between tests, causing DOM elements to conflict. - * Cleaning the custom element each tests in `beforeEach` is a workaround to avoid this problem. - * - * refs: https://github.com/vitest-dev/vitest/issues/5919 + * Tests for DOM elements must always be tested against the `sandboxElement`. In browser mode, the tests in a file run on one shared document, so elements left by one test would be found by the selectors of another. + * Recreating the sandbox element in `beforeEach` gives each test an empty subtree to work in. */ describe.shuffle("waitElement", () => { let sandboxElement = document.createElement(TEST_SANDBOX); @@ -403,7 +403,9 @@ describe.shuffle("waitElement", () => { detector: async (element) => { return { isDetected: true, - result: await delay(100, `${element?.textContent} awaitable!`), + result: await delay(100).then( + () => `${element?.textContent} awaitable!`, + ), }; }, }); diff --git a/tests/setupFiles/parepareCleanSandboxElement.ts b/tests/setupFiles/parepareCleanSandboxElement.ts index 7506195..9dddacf 100644 --- a/tests/setupFiles/parepareCleanSandboxElement.ts +++ b/tests/setupFiles/parepareCleanSandboxElement.ts @@ -7,10 +7,8 @@ /** * @description - * Tests for DOM elements must always be tested against the `sandboxElement`. This is because the global DOM (JSDom) is common between tests, causing DOM elements to conflict. - * Cleaning the custom element each tests in `beforeEach` is a workaround to avoid this problem. - * - * refs: https://github.com/vitest-dev/vitest/issues/5919 + * Tests for DOM elements must always be tested against the `sandboxElement`. In browser mode, the tests in a file run on one shared document, so elements left by one test would be found by the selectors of another. + * Recreating the sandbox element in `beforeEach` gives each test an empty subtree to work in. */ // const prepareCleanSandbox: SuiteHooks["beforeEach"][number] = (context) => { // const TEST_SANDBOX = "test-sandbox"; diff --git a/vitest.config.ts b/vitest.config.ts index 33803fb..82dbe4a 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -1,3 +1,4 @@ +import { playwright } from "@vitest/browser-playwright"; import { defineConfig } from "vitest/config"; export default defineConfig({ @@ -8,7 +9,16 @@ export default defineConfig({ env: { RUN_ON_TESTING: "true", }, - environment: "jsdom", + browser: { + enabled: true, + provider: playwright(), + headless: true, + instances: [ + { browser: "chromium" }, + { browser: "firefox" }, + { browser: "webkit" }, + ], + }, coverage: { provider: "v8", }, From 938ff1727e6d2f84128813ef0991604ce6eafadf Mon Sep 17 00:00:00 2001 From: 1natsu <1natsu172@users.noreply.github.com> Date: Sat, 3 Oct 2026 22:38:02 +0900 Subject: [PATCH 2/5] test: check that customMatcher detects an element added later The XPath test appended the element before calling waitElement, because jsdom could not exercise dynamic detection, and the FIXME callback in Promise.all was never invoked. Start waiting first, append the element afterwards, and assert that the initial check finds nothing while a later observer-driven check returns the element. Co-Authored-By: Claude Opus 5.5 (1M context) --- src/index.test.ts | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/src/index.test.ts b/src/index.test.ts index 3db0e4d..9806f69 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -473,39 +473,38 @@ describe.shuffle("waitElement", () => { expect(customMatcher).toHaveBeenCalledTimes(2); }); - test("should get the element via customMatcher", async ({ expect }) => { + test("should detect the appearance of an element via customMatcher", async ({ + expect, + }) => { + const element = document.createElement("div"); + element.id = "late"; + const simulateMutation = () => delay(500).then(() => { - const element = document.createElement("div"); - element.id = "late"; sandboxElement.append(element); }); - await simulateMutation(); - - const customMatcher = vi.fn((selector) => { + const customMatcher = vi.fn((selector: string) => { return document.evaluate( selector, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null, - ).singleNodeValue as Element; + ).singleNodeValue as Element | null; }); const [, result] = await Promise.all([ - () => { - console.warn( - "FIXME: JSDOM is returning the same value as the XPath resolve return value, so dynamic detection cannot be tested. Want to change to browser mode.", - ); - }, + simulateMutation(), waitElement("//test-sandbox//div[@id='late']", { customMatcher, }), ]); - expect(result?.id).toEqual("late"); - expect(customMatcher).toHaveBeenCalled(); + expect(result).toBe(element); + // The initial check finds nothing; a later check run by the observer finds the element. + expect(customMatcher.mock.results[0]?.value).toBeNull(); + expect(customMatcher.mock.results.at(-1)?.value).toBe(element); }); }); }); From d6b43acd89cf67f9a84d3362e9057aff13eef929 Mon Sep 17 00:00:00 2001 From: 1natsu <1natsu172@users.noreply.github.com> Date: Sat, 3 Oct 2026 22:38:02 +0900 Subject: [PATCH 3/5] ci: install Playwright browsers for the browser mode tests Cache the browser binaries keyed by the Playwright version. The system packages the browsers need are not part of the cache, so install them on every run. Co-Authored-By: Claude Opus 5.5 (1M context) --- .github/workflows/node.js.yml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/.github/workflows/node.js.yml b/.github/workflows/node.js.yml index c71fadf..5e04286 100644 --- a/.github/workflows/node.js.yml +++ b/.github/workflows/node.js.yml @@ -28,5 +28,21 @@ jobs: run: pnpm run typecheck - name: Check to build passed run: pnpm run build + - name: Get Playwright version + id: playwright-version + run: echo "version=$(pnpm exec playwright --version | awk '{print $2}')" >> "$GITHUB_OUTPUT" + - name: Cache Playwright browsers + id: playwright-cache + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6 + with: + path: ~/.cache/ms-playwright + key: playwright-${{ runner.os }}-${{ steps.playwright-version.outputs.version }} + # The cache holds only the browser binaries; the system packages they need are installed on every run + - name: Install Playwright browsers + if: steps.playwright-cache.outputs.cache-hit != 'true' + run: pnpm exec playwright install --with-deps chromium firefox webkit + - name: Install Playwright system dependencies + if: steps.playwright-cache.outputs.cache-hit == 'true' + run: pnpm exec playwright install-deps chromium firefox webkit - name: Check to test passed run: pnpm run test From da6f3b7603d3e2f1abd2310f6e0d3e75a09199b2 Mon Sep 17 00:00:00 2001 From: 1natsu <1natsu172@users.noreply.github.com> Date: Sat, 3 Oct 2026 22:38:03 +0900 Subject: [PATCH 4/5] docs: describe the browser mode test environment in AGENTS.md Co-Authored-By: Claude Opus 5.5 (1M context) --- AGENTS.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index fd7f4a7..59336a1 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -37,9 +37,12 @@ This file provides guidance to AI coding agents when working with code in this r ## Testing -- **環境**: jsdom(vitest.config.tsで設定) +- **環境**: Vitest browser mode(Playwright provider)で Chromium / Firefox / WebKit の実ブラウザ上で全テストを実行する(vitest.config.tsで設定)。MutationObserverのレコード配信やマイクロタスクの実行タイミングはエンジンごとに差があり得るため、3エンジンすべてで通すことを前提にする +- **ブラウザの準備**: 初回は `pnpm exec playwright install chromium firefox webkit` でブラウザを取得する +- **特定エンジンだけ実行**: `pnpm vitest run --project chromium` +- **Node専用APIを使わない**: テストはブラウザで動くため、`node:*` モジュール(`node:timers/promises` 等)はimportできない。待機は `src/index.test.ts` のローカル `delay` を使う - **型テスト**: `*.test-d.ts`ファイルで`expectTypeOf`を使用(vitest typecheck有効) -- **DOMテストの注意**: jsdomはテスト間でグローバルDOMを共有するため、各テストでサンドボックス要素(`test-sandbox`)をbeforeEachでクリーン・再生成している(`describe.shuffle`で順序非依存を保証) +- **DOMテストの注意**: browser modeはテストファイルごとに別のiframeで実行するため、ファイルをまたいだDOMやグローバル変数の衝突は起きない。一方で同じファイル内のテストは1つのdocumentを共有するため、各テストでサンドボックス要素(`test-sandbox`)をbeforeEachでクリーン・再生成している(`describe.shuffle`で順序非依存を保証) - テストタイムアウトは50秒に設定 ## Code Style From 274a54d3219d21dea3ff3c8938437f54413a1735 Mon Sep 17 00:00:00 2001 From: 1natsu <1natsu172@users.noreply.github.com> Date: Sat, 3 Oct 2026 23:18:31 +0900 Subject: [PATCH 5/5] ci: stop caching the Playwright browsers The first run spent about 15s downloading the browsers and about 49s installing their system packages, which the cache cannot hold. A hit would save only the download, at the cost of restoring a 494MB entry that also counts against the repository cache quota. Co-Authored-By: Claude Opus 5.5 (1M context) --- .github/workflows/node.js.yml | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/.github/workflows/node.js.yml b/.github/workflows/node.js.yml index 5e04286..1e88a9c 100644 --- a/.github/workflows/node.js.yml +++ b/.github/workflows/node.js.yml @@ -28,21 +28,7 @@ jobs: run: pnpm run typecheck - name: Check to build passed run: pnpm run build - - name: Get Playwright version - id: playwright-version - run: echo "version=$(pnpm exec playwright --version | awk '{print $2}')" >> "$GITHUB_OUTPUT" - - name: Cache Playwright browsers - id: playwright-cache - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6 - with: - path: ~/.cache/ms-playwright - key: playwright-${{ runner.os }}-${{ steps.playwright-version.outputs.version }} - # The cache holds only the browser binaries; the system packages they need are installed on every run - name: Install Playwright browsers - if: steps.playwright-cache.outputs.cache-hit != 'true' run: pnpm exec playwright install --with-deps chromium firefox webkit - - name: Install Playwright system dependencies - if: steps.playwright-cache.outputs.cache-hit == 'true' - run: pnpm exec playwright install-deps chromium firefox webkit - name: Check to test passed run: pnpm run test