diff --git a/.claude/rules/testing.md b/.claude/rules/testing.md index 75577c16d..ee415a835 100644 --- a/.claude/rules/testing.md +++ b/.claude/rules/testing.md @@ -28,6 +28,52 @@ E2E files: **must** use `.spec.ts` extension (`.test.ts` not detected). Route unit tests: `src/routes/**/*.test.ts` is included by `vite.config.ts`. **Never use `+` as a filename prefix** — SvelteKit reserves it and `pnpm check` will error. Name route test files `page_server.test.ts`, not `+page.server.test.ts`. +## Test Environment + +Default is `node` (set in `vite.config.ts`). Only files touching the real DOM (`window` / `document` / `localStorage`) opt in with a top-of-file `// @vitest-environment jsdom`. **Never set jsdom globally** — most tests are pure units and per-file jsdom construction is ~5.5x slower. + +### Toggling `browser` per describe + +**Never register `vi.mock('$app/environment')` twice in one file.** Every `vi.mock` is hoisted above the imports and runs once before any test, so a `{ browser: false }` mock written inside an SSR `describe`/`beforeEach` silently overwrites the `{ browser: true }` one at the top — the whole file ends up pinned to `browser = false`, the localStorage branches never execute, and tests that "verify" them become false-positives while still passing. + +Use **one dynamic mock** driven by a `vi.hoisted` flag, and toggle the flag in `beforeEach`. Default the flag to `false` so singletons constructed at import time stay SSR-safe: + +```typescript +// @vitest-environment jsdom +const browserState = vi.hoisted(() => ({ value: false })); + +vi.mock('$app/environment', () => ({ + get browser() { + return browserState.value; + }, +})); + +describe('MyStore', () => { + let store: MyStore; + + beforeEach(() => { + browserState.value = true; + localStorage.clear(); + store = new MyStore(); // constructed after the flag flips — reads localStorage + }); + // … +}); + +describe('MyStore in SSR', () => { + beforeEach(() => { + browserState.value = false; + }); + // … +}); +``` + +**Never assert a browser branch on the import-time singleton.** The flag is `false` while the module graph is evaluated, so the exported singleton is always built in SSR mode and never touches localStorage — asserting on it under `browser = true` is the same false-positive as the double-`vi.mock` above. Construct a fresh instance inside the browser `describe` (which is why the store class, not just the singleton, needs a named export). The singleton is still worth one assertion: that import-time construction is SSR-safe. + +In jsdom files, do **not** stub localStorage with `vi.stubGlobal` — use jsdom's real `Storage` and assert on state (`localStorage.getItem(key)`), not on spy calls. Cover both SSR guards with separate cases, since one assertion cannot carry both: + +- **read guard**: pre-seed localStorage, construct a fresh store, expect the _default_ (the pre-seeded value stays in storage, so do not expect `getItem(key)` to be null here) +- **write guard**: start from empty localStorage, call the setter, expect `getItem(key)` to still be null + ## Unit Testing Patterns ### Assertions diff --git a/compose.yaml b/compose.yaml index ca78cccf6..28e479c63 100644 --- a/compose.yaml +++ b/compose.yaml @@ -6,8 +6,8 @@ services: - '5555:5555' tty: true volumes: - - .:/usr/src/app:cached - - ./node_modules:/usr/src/app/node_modules:cached + - .:/usr/src/app + - ./node_modules:/usr/src/app/node_modules environment: - NODE_ENV=development - DATABASE_URL=postgresql://db_user:db_password@db:5432/test_db?pgbouncer=true&connection_limit=10&connect_timeout=60&statement_timeout=60000 # Note: Local server cannot start if port is set to db:6543. diff --git a/package.json b/package.json index 177d5e52b..120918a26 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,6 @@ "@sveltejs/vite-plugin-svelte": "7.2.0", "@tailwindcss/forms": "0.5.11", "@tailwindcss/vite": "4.3.2", - "@testing-library/jest-dom": "6.9.1", "@types/gtag.js": "0.0.20", "@types/jsdom": "28.0.3", "@typescript-eslint/eslint-plugin": "8.63.0", @@ -77,8 +76,6 @@ "@lucia-auth/adapter-prisma": "3.0.2", "@lucide/svelte": "1.24.0", "@prisma/client": "5.22.0", - "@testing-library/svelte": "5.4.2", - "@types/jest": "30.0.0", "@types/node": "25.9.5", "debug": "4.4.3", "lucia": "2.7.7", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index da0968fb3..9a2de3637 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -42,12 +42,6 @@ importers: '@prisma/client': specifier: 5.22.0 version: 5.22.0(prisma@5.22.0) - '@testing-library/svelte': - specifier: 5.4.2 - version: 5.4.2(svelte@5.56.4(@typescript-eslint/types@8.63.0))(vite@8.1.4(@types/node@25.9.5)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.1))(vitest@4.1.10) - '@types/jest': - specifier: 30.0.0 - version: 30.0.0 '@types/node': specifier: 25.9.5 version: 25.9.5 @@ -103,9 +97,6 @@ importers: '@tailwindcss/vite': specifier: 4.3.2 version: 4.3.2(vite@8.1.4(@types/node@25.9.5)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.1)) - '@testing-library/jest-dom': - specifier: 6.9.1 - version: 6.9.1 '@types/gtag.js': specifier: 0.0.20 version: 0.0.20 @@ -214,9 +205,6 @@ importers: packages: - '@adobe/css-tools@4.4.4': - resolution: {integrity: sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==} - '@ark/schema@0.56.1': resolution: {integrity: sha512-1Cf2g9nKD8K/3JGRu+gCCfYw5d4qR8YLLjDs5W5kpmaButCYWAPFUJqSXyBATPjglzCd4tIkp398iPYVs8MjRA==} @@ -238,10 +226,6 @@ packages: '@asamuzakjp/nwsapi@2.3.9': resolution: {integrity: sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==} - '@babel/code-frame@7.27.1': - resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} - engines: {node: '>=6.9.0'} - '@babel/helper-string-parser@7.29.7': resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==} engines: {node: '>=6.9.0'} @@ -763,30 +747,6 @@ packages: resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==} engines: {node: '>=18.0.0'} - '@jest/diff-sequences@30.0.1': - resolution: {integrity: sha512-n5H8QLDJ47QqbCNn5SuFjCRDrOLEZ0h8vAHCK5RL9Ls7Xa8AQLa/YxAc9UjFqoEDM48muwtBGjtMY5cr0PLDCw==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - - '@jest/expect-utils@30.2.0': - resolution: {integrity: sha512-1JnRfhqpD8HGpOmQp180Fo9Zt69zNtC+9lR+kT7NVL05tNXIi+QC8Csz7lfidMoVLPD3FnOtcmp0CEFnxExGEA==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - - '@jest/get-type@30.1.0': - resolution: {integrity: sha512-eMbZE2hUnx1WV0pmURZY9XoXPkUYjpc55mb0CrhtdWLtzMQPFvu/rZkTLZFTsdaVQa+Tr4eWAteqcUzoawq/uA==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - - '@jest/pattern@30.0.1': - resolution: {integrity: sha512-gWp7NfQW27LaBQz3TITS8L7ZCQ0TLvtmI//4OwlQRx4rnWxcPNIYjxZpDcN4+UlGxgm3jS5QPz8IPTCkb59wZA==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - - '@jest/schemas@30.0.5': - resolution: {integrity: sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - - '@jest/types@30.2.0': - resolution: {integrity: sha512-H9xg1/sfVvyfU7o3zMfBEjQ1gcsdeTMgqHoYdN79tuLqfTtuu7WckRA1R5whDwOzxaZAeMKTYWqP+WCAi0CHsg==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - '@jridgewell/gen-mapping@0.3.13': resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} @@ -1501,9 +1461,6 @@ packages: '@sinclair/typebox@0.25.24': resolution: {integrity: sha512-XJfwUVUKDHF5ugKwIcxEgc9k8b7HbznCp6eUfWgu710hMPNIO4aw4/zB5RogDQz8nd6gyCDpU9O/m6qYEWY6yQ==} - '@sinclair/typebox@0.34.41': - resolution: {integrity: sha512-6gS8pZzSXdyRHTIqoqSVknxolr1kzfy4/CeDnrzsVz8TTIWUbOBr6gnzOmTYJ3eXQNh4IYHIGi5aIL7sOZ2G/g==} - '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} @@ -1644,33 +1601,6 @@ packages: peerDependencies: vite: ^5.2.0 || ^6 || ^7 || ^8 - '@testing-library/dom@10.4.1': - resolution: {integrity: sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==} - engines: {node: '>=18'} - - '@testing-library/jest-dom@6.9.1': - resolution: {integrity: sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==} - engines: {node: '>=14', npm: '>=6', yarn: '>=1'} - - '@testing-library/svelte-core@1.1.3': - resolution: {integrity: sha512-KkMAvXeWorxN2Yn0kdC1lfoAItxpoj4uOWzxK5leDrNxonLvS5nwBFvztrroyTszQ0Wf/EU6iLT8JhY5qcn22g==} - engines: {node: '>=16'} - peerDependencies: - svelte: ^3 || ^4 || ^5 || ^5.0.0-next.0 - - '@testing-library/svelte@5.4.2': - resolution: {integrity: sha512-4o31E4HGo5BU5KwPkulNRocEden+7Tt9JYm9uhln5ajF7DULeyFA46BBWVfKJ8Ms9B3JmOFPTIiVamH7n3KpuQ==} - engines: {node: '>= 10'} - peerDependencies: - svelte: ^3 || ^4 || ^5 || ^5.0.0-next.0 - vite: '*' - vitest: '*' - peerDependenciesMeta: - vite: - optional: true - vitest: - optional: true - '@tootallnate/once@2.0.1': resolution: {integrity: sha512-HqmEUIGRJ5fSXchkVgR5F7qn48bDBzv0kWj/Kfu5e6uci4UlEeng4331LnBkWffb++Ei3FOVLxo8JJWMFBDMeQ==} engines: {node: '>= 10'} @@ -1684,9 +1614,6 @@ packages: '@tybys/wasm-util@0.10.3': resolution: {integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==} - '@types/aria-query@5.0.4': - resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==} - '@types/chai@5.2.3': resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} @@ -1705,18 +1632,6 @@ packages: '@types/gtag.js@0.0.20': resolution: {integrity: sha512-wwAbk3SA2QeU67unN7zPxjEHmPmlXwZXZvQEpbEUQuMCRGgKyE1m6XDuTUA9b6pCGb/GqJmdfMOY5LuDjJSbbg==} - '@types/istanbul-lib-coverage@2.0.6': - resolution: {integrity: sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==} - - '@types/istanbul-lib-report@3.0.3': - resolution: {integrity: sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==} - - '@types/istanbul-reports@3.0.4': - resolution: {integrity: sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==} - - '@types/jest@30.0.0': - resolution: {integrity: sha512-XTYugzhuwqWjws0CVz8QpM36+T+Dz5mTEBKhNs/esGLnCIlGdRy+Dq78NRjd7ls7r8BC8ZRMOrKlkO1hU0JOwA==} - '@types/jsdom@28.0.3': resolution: {integrity: sha512-/HQ2uFoetFTXuye8vzIcHw2z6Fwi7Hi/qcgC+RoS9NCyewiqxhVGqlG+ViGB6lkax481R6dmhf1I7lIGlzJStQ==} @@ -1732,9 +1647,6 @@ packages: '@types/resolve@1.20.2': resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==} - '@types/stack-utils@2.0.3': - resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==} - '@types/tough-cookie@4.0.5': resolution: {integrity: sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==} @@ -1744,12 +1656,6 @@ packages: '@types/validator@13.15.10': resolution: {integrity: sha512-T8L6i7wCuyoK8A/ZeLYt1+q0ty3Zb9+qbSSvrIVitzT3YjZqkTZ40IbRsPanlB4h1QB3JVL1SYCdR6ngtFYcuA==} - '@types/yargs-parser@21.0.3': - resolution: {integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==} - - '@types/yargs@17.0.34': - resolution: {integrity: sha512-KExbHVa92aJpw9WDQvzBaGVE2/Pz+pLZQloT2hjL8IqsZnV62rlPOYvNnLmf/L2dyllfVUOVBj64M0z/46eR2A==} - '@typeschema/class-validator@0.3.0': resolution: {integrity: sha512-OJSFeZDIQ8EK1HTljKLT5CItM2wsbgczLN8tMEfz3I1Lmhc5TBfkZ0eikFzUC16tI3d1Nag7um6TfCgp2I2Bww==} peerDependencies: @@ -2030,18 +1936,6 @@ packages: ajv@8.20.0: resolution: {integrity: sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==} - ansi-regex@5.0.1: - resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} - engines: {node: '>=8'} - - ansi-styles@4.3.0: - resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} - engines: {node: '>=8'} - - ansi-styles@5.2.0: - resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} - engines: {node: '>=10'} - any-base@1.1.0: resolution: {integrity: sha512-uMgjozySS8adZZYePpaWs8cxB9/kdzmpX6SgJZ+wbz1K5eYk5QMYDVJaZKhxyIHUdnnJkfR7SVgStgH7LkGUyg==} @@ -2060,17 +1954,10 @@ packages: argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} - aria-query@5.3.0: - resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==} - aria-query@5.3.1: resolution: {integrity: sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g==} engines: {node: '>= 0.4'} - aria-query@5.3.2: - resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==} - engines: {node: '>= 0.4'} - arkregex@0.0.7: resolution: {integrity: sha512-O/Ltrn9EUSn3ui0KVzfyrWGDUsHlzKxDVBtpQxL/6JmLRMAZAebfSNf/A/J5Ny5S6QIwrXX+RfXsu888HMs35A==} @@ -2180,10 +2067,6 @@ packages: resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} engines: {node: '>=18'} - chalk@4.1.2: - resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} - engines: {node: '>=10'} - chokidar@4.0.0: resolution: {integrity: sha512-mxIojEAQcuEvT/lyXq+jf/3cO/KoA6z4CeNDGGevTybECPOMFCnQy3OPahluUkbqgPNGw5Bi78UC7Po6Lhy+NA==} engines: {node: '>= 14.16.0'} @@ -2196,10 +2079,6 @@ packages: resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==} engines: {node: '>=18'} - ci-info@4.3.1: - resolution: {integrity: sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA==} - engines: {node: '>=8'} - citty@0.1.6: resolution: {integrity: sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==} @@ -2216,13 +2095,6 @@ packages: code-block-writer@10.1.1: resolution: {integrity: sha512-67ueh2IRGst/51p0n6FvPrnRjAGHY5F8xdjkgrYE7DDzpJe6qA07RYQ9VcoUeo5ATOjSOiWpSL3SWBRRbempMw==} - color-convert@2.0.1: - resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} - engines: {node: '>=7.0.0'} - - color-name@1.1.4: - resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} - commander@2.20.3: resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} @@ -2259,9 +2131,6 @@ packages: resolution: {integrity: sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==} engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} - css.escape@1.5.1: - resolution: {integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==} - cssesc@3.0.0: resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} engines: {node: '>=4'} @@ -2331,10 +2200,6 @@ packages: resolution: {integrity: sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==} engines: {node: '>= 0.6'} - dequal@2.0.3: - resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} - engines: {node: '>=6'} - destr@2.0.5: resolution: {integrity: sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==} @@ -2348,12 +2213,6 @@ packages: dlv@1.1.3: resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==} - dom-accessibility-api@0.5.16: - resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==} - - dom-accessibility-api@0.6.3: - resolution: {integrity: sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==} - dotenv@16.6.1: resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==} engines: {node: '>=12'} @@ -2403,10 +2262,6 @@ packages: engines: {node: '>=18'} hasBin: true - escape-string-regexp@2.0.0: - resolution: {integrity: sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==} - engines: {node: '>=8'} - escape-string-regexp@4.0.0: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} @@ -2533,10 +2388,6 @@ packages: resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} engines: {node: '>=12.0.0'} - expect@30.2.0: - resolution: {integrity: sha512-u/feCi0GPsI+988gU2FLcsHyAHTU0MX1Wg68NhAnN7z/+C5wqG+CY8J53N9ioe8RXgaoz0nBR/TYMf3AycUuPw==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - exsolve@1.0.8: resolution: {integrity: sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==} @@ -2752,10 +2603,6 @@ packages: resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} engines: {node: '>=0.8.19'} - indent-string@4.0.0: - resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} - engines: {node: '>=8'} - inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} @@ -2823,30 +2670,6 @@ packages: resolution: {integrity: sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==} engines: {node: '>=8'} - jest-diff@30.2.0: - resolution: {integrity: sha512-dQHFo3Pt4/NLlG5z4PxZ/3yZTZ1C7s9hveiOj+GCN+uT109NC2QgsoVZsVOAvbJ3RgKkvyLGXZV9+piDpWbm6A==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - - jest-matcher-utils@30.2.0: - resolution: {integrity: sha512-dQ94Nq4dbzmUWkQ0ANAWS9tBRfqCrn0bV9AMYdOi/MHW726xn7eQmMeRTpX2ViC00bpNaWXq+7o4lIQ3AX13Hg==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - - jest-message-util@30.2.0: - resolution: {integrity: sha512-y4DKFLZ2y6DxTWD4cDe07RglV88ZiNEdlRfGtqahfbIjfsw1nMCPx49Uev4IA/hWn3sDKyAnSPwoYSsAEdcimw==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - - jest-mock@30.2.0: - resolution: {integrity: sha512-JNNNl2rj4b5ICpmAcq+WbLH83XswjPbjH4T7yvGzfAGCPh1rw+xVNbtk+FnRslvt9lkCcdn9i1oAoKUuFsOxRw==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - - jest-regex-util@30.0.1: - resolution: {integrity: sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - - jest-util@30.2.0: - resolution: {integrity: sha512-QKNsM0o3Xe6ISQU869e+DhG+4CK/48aHYdJZGlFQVTjnbvgpcKyxpzk29fGiO7i/J8VENZ+d2iGnSsvmuHywlA==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - jiti@2.7.0: resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==} hasBin: true @@ -2863,9 +2686,6 @@ packages: js-tokens@10.0.0: resolution: {integrity: sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==} - js-tokens@4.0.0: - resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} - js-yaml@4.3.0: resolution: {integrity: sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==} hasBin: true @@ -3091,10 +2911,6 @@ packages: resolution: {integrity: sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew==} engines: {node: '>=12'} - lz-string@1.5.0: - resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} - hasBin: true - magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} @@ -3139,10 +2955,6 @@ packages: resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} engines: {node: '>=6'} - min-indent@1.0.1: - resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} - engines: {node: '>=4'} - mini-svg-data-uri@1.4.4: resolution: {integrity: sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg==} hasBin: true @@ -3516,14 +3328,6 @@ packages: engines: {node: '>=14'} hasBin: true - pretty-format@27.5.1: - resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} - engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} - - pretty-format@30.2.0: - resolution: {integrity: sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - pretty-ms@7.0.1: resolution: {integrity: sha512-973driJZvxiGOQ5ONsFhOF/DtzPMOMtgC11kCpUrPGMTgqp2q/1gwzCquocrN33is0VZ5GFHXZYMM9l6h67v2Q==} engines: {node: '>=10'} @@ -3581,20 +3385,10 @@ packages: rc9@2.1.2: resolution: {integrity: sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==} - react-is@17.0.2: - resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} - - react-is@18.3.1: - resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} - readdirp@4.1.2: resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} engines: {node: '>= 14.18.0'} - redent@3.0.0: - resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==} - engines: {node: '>=8'} - require-from-string@2.0.2: resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} engines: {node: '>=0.10.0'} @@ -3718,10 +3512,6 @@ packages: sisteransi@1.0.5: resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} - slash@3.0.0: - resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} - engines: {node: '>=8'} - smart-buffer@4.2.0: resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==} engines: {node: '>= 6.0.0', npm: '>= 3.0.0'} @@ -3751,10 +3541,6 @@ packages: engines: {node: '>=20.16.0'} hasBin: true - stack-utils@2.0.6: - resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==} - engines: {node: '>=10'} - stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} @@ -3785,10 +3571,6 @@ packages: resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} engines: {node: '>=6'} - strip-indent@3.0.0: - resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} - engines: {node: '>=8'} - strip-json-comments@3.1.1: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} @@ -4281,8 +4063,6 @@ packages: snapshots: - '@adobe/css-tools@4.4.4': {} - '@ark/schema@0.56.1': dependencies: '@ark/util': 0.56.1 @@ -4311,12 +4091,6 @@ snapshots: '@asamuzakjp/nwsapi@2.3.9': {} - '@babel/code-frame@7.27.1': - dependencies: - '@babel/helper-validator-identifier': 7.29.7 - js-tokens: 4.0.0 - picocolors: 1.1.1 - '@babel/helper-string-parser@7.29.7': {} '@babel/helper-validator-identifier@7.29.7': {} @@ -4325,7 +4099,8 @@ snapshots: dependencies: '@babel/types': 7.29.7 - '@babel/runtime@7.29.2': {} + '@babel/runtime@7.29.2': + optional: true '@babel/types@7.29.7': dependencies: @@ -4681,33 +4456,6 @@ snapshots: dependencies: minipass: 7.1.3 - '@jest/diff-sequences@30.0.1': {} - - '@jest/expect-utils@30.2.0': - dependencies: - '@jest/get-type': 30.1.0 - - '@jest/get-type@30.1.0': {} - - '@jest/pattern@30.0.1': - dependencies: - '@types/node': 25.9.5 - jest-regex-util: 30.0.1 - - '@jest/schemas@30.0.5': - dependencies: - '@sinclair/typebox': 0.34.41 - - '@jest/types@30.2.0': - dependencies: - '@jest/pattern': 30.0.1 - '@jest/schemas': 30.0.5 - '@types/istanbul-lib-coverage': 2.0.6 - '@types/istanbul-reports': 3.0.4 - '@types/node': 25.9.5 - '@types/yargs': 17.0.34 - chalk: 4.1.2 - '@jridgewell/gen-mapping@0.3.13': dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -5216,8 +4964,6 @@ snapshots: '@sinclair/typebox@0.25.24': {} - '@sinclair/typebox@0.34.41': {} - '@standard-schema/spec@1.1.0': {} '@sveltejs/acorn-typescript@1.0.10(acorn@8.17.0)': @@ -5338,39 +5084,6 @@ snapshots: tailwindcss: 4.3.2 vite: 8.1.4(@types/node@25.9.5)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.1) - '@testing-library/dom@10.4.1': - dependencies: - '@babel/code-frame': 7.27.1 - '@babel/runtime': 7.29.2 - '@types/aria-query': 5.0.4 - aria-query: 5.3.0 - dom-accessibility-api: 0.5.16 - lz-string: 1.5.0 - picocolors: 1.1.1 - pretty-format: 27.5.1 - - '@testing-library/jest-dom@6.9.1': - dependencies: - '@adobe/css-tools': 4.4.4 - aria-query: 5.3.2 - css.escape: 1.5.1 - dom-accessibility-api: 0.6.3 - picocolors: 1.1.1 - redent: 3.0.0 - - '@testing-library/svelte-core@1.1.3(svelte@5.56.4(@typescript-eslint/types@8.63.0))': - dependencies: - svelte: 5.56.4(@typescript-eslint/types@8.63.0) - - '@testing-library/svelte@5.4.2(svelte@5.56.4(@typescript-eslint/types@8.63.0))(vite@8.1.4(@types/node@25.9.5)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.1))(vitest@4.1.10)': - dependencies: - '@testing-library/dom': 10.4.1 - '@testing-library/svelte-core': 1.1.3(svelte@5.56.4(@typescript-eslint/types@8.63.0)) - svelte: 5.56.4(@typescript-eslint/types@8.63.0) - optionalDependencies: - vite: 8.1.4(@types/node@25.9.5)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.1) - vitest: 4.1.10(@edge-runtime/vm@3.2.0)(@types/node@25.9.5)(@vitest/coverage-v8@4.1.10)(@vitest/ui@4.1.10)(jsdom@29.1.1)(vite@8.1.4(@types/node@25.9.5)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.1)) - '@tootallnate/once@2.0.1': {} '@tootallnate/quickjs-emscripten@0.23.0': {} @@ -5387,8 +5100,6 @@ snapshots: tslib: 2.8.1 optional: true - '@types/aria-query@5.0.4': {} - '@types/chai@5.2.3': dependencies: '@types/deep-eql': 4.0.2 @@ -5404,21 +5115,6 @@ snapshots: '@types/gtag.js@0.0.20': {} - '@types/istanbul-lib-coverage@2.0.6': {} - - '@types/istanbul-lib-report@3.0.3': - dependencies: - '@types/istanbul-lib-coverage': 2.0.6 - - '@types/istanbul-reports@3.0.4': - dependencies: - '@types/istanbul-lib-report': 3.0.3 - - '@types/jest@30.0.0': - dependencies: - expect: 30.2.0 - pretty-format: 30.2.0 - '@types/jsdom@28.0.3': dependencies: '@types/node': 25.9.5 @@ -5438,8 +5134,6 @@ snapshots: '@types/resolve@1.20.2': {} - '@types/stack-utils@2.0.3': {} - '@types/tough-cookie@4.0.5': {} '@types/trusted-types@2.0.7': {} @@ -5447,12 +5141,6 @@ snapshots: '@types/validator@13.15.10': optional: true - '@types/yargs-parser@21.0.3': {} - - '@types/yargs@17.0.34': - dependencies: - '@types/yargs-parser': 21.0.3 - '@typeschema/class-validator@0.3.0(@types/json-schema@7.0.15)(class-validator@0.14.4)': dependencies: '@typeschema/core': 0.14.0(@types/json-schema@7.0.15) @@ -6014,14 +5702,6 @@ snapshots: json-schema-traverse: 1.0.0 require-from-string: 2.0.2 - ansi-regex@5.0.1: {} - - ansi-styles@4.3.0: - dependencies: - color-convert: 2.0.1 - - ansi-styles@5.2.0: {} - any-base@1.1.0: {} any-promise@1.3.0: {} @@ -6034,14 +5714,8 @@ snapshots: argparse@2.0.1: {} - aria-query@5.3.0: - dependencies: - dequal: 2.0.3 - aria-query@5.3.1: {} - aria-query@5.3.2: {} - arkregex@0.0.7: dependencies: '@ark/util': 0.56.1 @@ -6137,11 +5811,6 @@ snapshots: chai@6.2.2: {} - chalk@4.1.2: - dependencies: - ansi-styles: 4.3.0 - supports-color: 7.2.0 - chokidar@4.0.0: dependencies: readdirp: 4.1.2 @@ -6152,8 +5821,6 @@ snapshots: chownr@3.0.0: {} - ci-info@4.3.1: {} - citty@0.1.6: dependencies: consola: 3.4.2 @@ -6171,12 +5838,6 @@ snapshots: code-block-writer@10.1.1: {} - color-convert@2.0.1: - dependencies: - color-name: 1.1.4 - - color-name@1.1.4: {} - commander@2.20.3: {} concat-map@0.0.1: {} @@ -6204,8 +5865,6 @@ snapshots: mdn-data: 2.27.1 source-map-js: 1.2.1 - css.escape@1.5.1: {} - cssesc@3.0.0: {} cssfilter@0.0.10: {} @@ -6252,8 +5911,6 @@ snapshots: depd@1.1.2: {} - dequal@2.0.3: {} - destr@2.0.5: {} detect-libc@2.1.2: {} @@ -6263,10 +5920,6 @@ snapshots: dlv@1.1.3: optional: true - dom-accessibility-api@0.5.16: {} - - dom-accessibility-api@0.6.3: {} - dotenv@16.6.1: {} edge-runtime@2.5.9: @@ -6367,8 +6020,6 @@ snapshots: '@esbuild/win32-ia32': 0.28.1 '@esbuild/win32-x64': 0.28.1 - escape-string-regexp@2.0.0: {} - escape-string-regexp@4.0.0: {} escodegen@2.1.0: @@ -6535,15 +6186,6 @@ snapshots: expect-type@1.3.0: {} - expect@30.2.0: - dependencies: - '@jest/expect-utils': 30.2.0 - '@jest/get-type': 30.1.0 - jest-matcher-utils: 30.2.0 - jest-message-util: 30.2.0 - jest-mock: 30.2.0 - jest-util: 30.2.0 - exsolve@1.0.8: {} fast-check@3.23.2: @@ -6778,8 +6420,6 @@ snapshots: imurmurhash@0.1.4: {} - indent-string@4.0.0: {} - inherits@2.0.4: {} ip-address@10.2.0: {} @@ -6831,49 +6471,6 @@ snapshots: html-escaper: 2.0.2 istanbul-lib-report: 3.0.1 - jest-diff@30.2.0: - dependencies: - '@jest/diff-sequences': 30.0.1 - '@jest/get-type': 30.1.0 - chalk: 4.1.2 - pretty-format: 30.2.0 - - jest-matcher-utils@30.2.0: - dependencies: - '@jest/get-type': 30.1.0 - chalk: 4.1.2 - jest-diff: 30.2.0 - pretty-format: 30.2.0 - - jest-message-util@30.2.0: - dependencies: - '@babel/code-frame': 7.27.1 - '@jest/types': 30.2.0 - '@types/stack-utils': 2.0.3 - chalk: 4.1.2 - graceful-fs: 4.2.11 - micromatch: 4.0.8 - pretty-format: 30.2.0 - slash: 3.0.0 - stack-utils: 2.0.6 - - jest-mock@30.2.0: - dependencies: - '@jest/types': 30.2.0 - '@types/node': 25.9.5 - jest-util: 30.2.0 - - jest-regex-util@30.0.1: {} - - jest-util@30.2.0: - dependencies: - '@jest/types': 30.2.0 - '@types/node': 25.9.5 - chalk: 4.1.2 - ci-info: 4.3.1 - graceful-fs: 4.2.11 - picomatch: 4.0.4 - jiti@2.7.0: {} joi@17.13.4: @@ -6891,8 +6488,6 @@ snapshots: js-tokens@10.0.0: {} - js-tokens@4.0.0: {} - js-yaml@4.3.0: dependencies: argparse: 2.0.1 @@ -7084,8 +6679,6 @@ snapshots: luxon@3.7.2: {} - lz-string@1.5.0: {} - magic-string@0.30.21: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -7127,8 +6720,6 @@ snapshots: mimic-fn@2.1.0: {} - min-indent@1.0.1: {} - mini-svg-data-uri@1.4.4: {} minimatch@10.2.5: @@ -7428,18 +7019,6 @@ snapshots: prettier@3.9.5: {} - pretty-format@27.5.1: - dependencies: - ansi-regex: 5.0.1 - ansi-styles: 5.2.0 - react-is: 17.0.2 - - pretty-format@30.2.0: - dependencies: - '@jest/schemas': 30.0.5 - ansi-styles: 5.2.0 - react-is: 18.3.1 - pretty-ms@7.0.1: dependencies: parse-ms: 2.1.0 @@ -7506,17 +7085,8 @@ snapshots: defu: 6.1.7 destr: 2.0.5 - react-is@17.0.2: {} - - react-is@18.3.1: {} - readdirp@4.1.2: {} - redent@3.0.0: - dependencies: - indent-string: 4.0.0 - strip-indent: 3.0.0 - require-from-string@2.0.2: {} resolve-from@4.0.0: {} @@ -7657,8 +7227,6 @@ snapshots: sisteransi@1.0.5: {} - slash@3.0.0: {} - smart-buffer@4.2.0: {} smol-toml@1.7.0: {} @@ -7683,10 +7251,6 @@ snapshots: srvx@0.11.16: {} - stack-utils@2.0.6: - dependencies: - escape-string-regexp: 2.0.0 - stackback@0.0.2: {} stat-mode@0.3.0: {} @@ -7718,10 +7282,6 @@ snapshots: strip-final-newline@2.0.0: {} - strip-indent@3.0.0: - dependencies: - min-indent: 1.0.1 - strip-json-comments@3.1.1: {} super-sitemap@1.0.12(svelte@5.56.4(@typescript-eslint/types@8.63.0)): diff --git a/src/features/tasks/stores/active_contest_type.svelte.test.ts b/src/features/tasks/stores/active_contest_type.svelte.test.ts index c1dc22941..90967b826 100644 --- a/src/features/tasks/stores/active_contest_type.svelte.test.ts +++ b/src/features/tasks/stores/active_contest_type.svelte.test.ts @@ -1,3 +1,5 @@ +// @vitest-environment jsdom + import { describe, test, expect, vi, beforeEach, afterEach } from 'vitest'; import type { ContestTableProviderGroups } from '$features/tasks/utils/contest-table/contest_table_provider'; @@ -6,40 +8,35 @@ import { ActiveContestTypeStore, } from '$features/tasks/stores/active_contest_type.svelte'; +// WHY: a single dynamic mock is the only way to toggle `browser` per describe. Every vi.mock is +// hoisted and the last registration for a module wins, so a second vi.mock of `$app/environment` +// would silently clamp the whole file to one branch. +// Defaults to false so the singleton constructed at import time stays SSR-safe (no localStorage). +const browserState = vi.hoisted(() => ({ value: false })); + vi.mock('$app/environment', () => ({ - browser: true, + get browser() { + return browserState.value; + }, })); +const localStorageKey = 'contest_table_providers'; + +afterEach(() => { + localStorage.clear(); +}); + describe('ActiveContestTypeStore', () => { let store: ActiveContestTypeStore; - const mockLocalStorage: Storage = { - getItem: vi.fn((key) => mockStorage[key] || null), - setItem: vi.fn((key, value) => { - mockStorage[key] = value; - }), - removeItem: vi.fn(), - clear: vi.fn(), - length: 0, - key: vi.fn(), - }; - const mockStorage: Record = {}; - beforeEach(() => { - vi.clearAllMocks(); - // Clear mockStorage before each test - Object.keys(mockStorage).forEach((key) => delete mockStorage[key]); - // Setup mock for localStorage - vi.stubGlobal('localStorage', mockLocalStorage); + browserState.value = true; + localStorage.clear(); store = new ActiveContestTypeStore(); store.reset(); }); - afterEach(() => { - vi.unstubAllGlobals(); - }); - test('expects to initialize with default value', () => { expect(store.get()).toBe('abs'); }); @@ -60,6 +57,17 @@ describe('ActiveContestTypeStore', () => { expect(store.get()).toBe('abc319Onwards'); }); + test('expects to persist the value in localStorage when calling set()', () => { + store.set('abc319Onwards' as ContestTableProviderGroups); + expect(localStorage.getItem(localStorageKey)).toBe(JSON.stringify('abc319Onwards')); + }); + + test('expects to restore the value persisted in localStorage', () => { + localStorage.setItem(localStorageKey, JSON.stringify('fromAbc212ToAbc318')); + + expect(new ActiveContestTypeStore().get()).toBe('fromAbc212ToAbc318'); + }); + test('expects to correctly determine if contest type is the same with isSame()', () => { expect(store.isSame('abs' as ContestTableProviderGroups)).toBe(true); expect(store.isSame('abc319Onwards' as ContestTableProviderGroups)).toBe(false); @@ -94,15 +102,21 @@ describe('ActiveContestTypeStore', () => { }); test('expects to reset to default when initialized with invalid localStorage key', () => { - // Simulate invalid key in localStorage - mockStorage['contest_table_providers'] = JSON.stringify('invalidContestType'); + localStorage.setItem(localStorageKey, JSON.stringify('invalidContestType')); + + const newStore = new ActiveContestTypeStore(); + expect(newStore.get()).toBe('abs'); + }); + + test('expects to reset to default when localStorage holds invalid JSON', () => { + localStorage.setItem(localStorageKey, 'invalid-json'); const newStore = new ActiveContestTypeStore(); expect(newStore.get()).toBe('abs'); }); test('expects to reset to default when initialized with null', () => { - mockStorage['contest_table_providers'] = JSON.stringify(null); + localStorage.setItem(localStorageKey, JSON.stringify(null)); const newStore = new ActiveContestTypeStore(); expect(newStore.get()).toBe('abs'); @@ -124,17 +138,24 @@ describe('ActiveContestTypeStore', () => { describe('Active contest type store in SSR', () => { beforeEach(() => { - vi.restoreAllMocks(); - vi.mock('$app/environment', () => ({ - browser: false, - })); + browserState.value = false; }); - afterEach(() => { - vi.restoreAllMocks(); + test('expects to ignore localStorage and initialize with default value', () => { + localStorage.setItem(localStorageKey, JSON.stringify('abc319Onwards')); + + expect(new ActiveContestTypeStore().get()).toBe('abs'); + }); + + test('expects not to persist the value in localStorage when calling set()', () => { + const store = new ActiveContestTypeStore(); + store.set('abc319Onwards' as ContestTableProviderGroups); + + expect(store.get()).toBe('abc319Onwards'); + expect(localStorage.getItem(localStorageKey)).toBeNull(); }); - test('handles SSR gracefully', () => { + test('expects the singleton constructed at import time to hold the default value', () => { expect(activeContestTypeStore.get()).toBe('abs'); }); }); diff --git a/src/features/workbooks/stores/replenishment_workbook.svelte.ts b/src/features/workbooks/stores/replenishment_workbook.svelte.ts index 9cf790e25..6eb7fa62c 100644 --- a/src/features/workbooks/stores/replenishment_workbook.svelte.ts +++ b/src/features/workbooks/stores/replenishment_workbook.svelte.ts @@ -7,7 +7,7 @@ import { browser } from '$app/environment'; // https://svelte.dev/docs/svelte/$state const IS_SHOWN_REPLENISHMENT_WORKBOOKS = 'is_shown_replenishment_workbooks'; -class ReplenishmentWorkBooksStore { +export class ReplenishmentWorkBooksStore { private isShown = $state(this.loadInitialState()); // Note: diff --git a/src/features/workbooks/stores/replenishment_workbook.test.ts b/src/features/workbooks/stores/replenishment_workbook.test.ts index 8dcbff159..cbba5d863 100644 --- a/src/features/workbooks/stores/replenishment_workbook.test.ts +++ b/src/features/workbooks/stores/replenishment_workbook.test.ts @@ -1,36 +1,40 @@ -import { expect, test, vi, type Mock } from 'vitest'; +// @vitest-environment jsdom -import { replenishmentWorkBooksStore } from '$features/workbooks/stores/replenishment_workbook.svelte'; +import { describe, test, expect, vi, beforeEach, afterEach } from 'vitest'; + +import { + replenishmentWorkBooksStore, + ReplenishmentWorkBooksStore, +} from '$features/workbooks/stores/replenishment_workbook.svelte'; + +// WHY: a single dynamic mock is the only way to toggle `browser` per describe. Every vi.mock is +// hoisted and the last registration for a module wins, so a second vi.mock of `$app/environment` +// would silently clamp the whole file to one branch. +// Defaults to false so the singleton constructed at import time stays SSR-safe (no localStorage). +const browserState = vi.hoisted(() => ({ value: false })); vi.mock('$app/environment', () => ({ - browser: true, + get browser() { + return browserState.value; + }, })); -describe('Replenishment workbooks store', () => { - const localStorageKey = 'is_shown_replenishment_workbooks'; - const mockLocalStorage: Storage = { - getItem: vi.fn(), - setItem: vi.fn(), - removeItem: vi.fn(), - clear: vi.fn(), - length: 0, - key: vi.fn(), - }; +const localStorageKey = 'is_shown_replenishment_workbooks'; + +afterEach(() => { + localStorage.clear(); +}); +describe('Replenishment workbooks store', () => { beforeEach(() => { - vi.clearAllMocks(); - // Setup mock for localStorage - vi.stubGlobal('localStorage', mockLocalStorage); + browserState.value = true; + localStorage.clear(); replenishmentWorkBooksStore.reset(); }); - afterEach(() => { - vi.unstubAllGlobals(); - }); - test('expects to be invisible before toggling', () => { - expect(replenishmentWorkBooksStore.canView()).toBeFalsy(); + expect(replenishmentWorkBooksStore.canView()).toBe(false); }); test.each([ @@ -45,30 +49,47 @@ describe('Replenishment workbooks store', () => { expect(replenishmentWorkBooksStore.canView()).toBe(expected); }); - // Note: This test is skipped because it is not possible to mock localStorage in JSDOM. - test.skip('persists state in localStorage', () => { - (mockLocalStorage.getItem as Mock).mockReturnValue(JSON.stringify(false)); + test('expects to persist state in localStorage', () => { + replenishmentWorkBooksStore.toggleView(); + expect(localStorage.getItem(localStorageKey)).toBe(JSON.stringify(true)); replenishmentWorkBooksStore.toggleView(); + expect(localStorage.getItem(localStorageKey)).toBe(JSON.stringify(false)); + }); - expect(mockLocalStorage.setItem).toHaveBeenCalledTimes(1); - expect(mockLocalStorage.setItem).toHaveBeenCalledWith(localStorageKey, JSON.stringify(true)); + test('expects to be invisible when localStorage holds no state', () => { + expect(new ReplenishmentWorkBooksStore().canView()).toBe(false); }); - test('handles invalid localStorage data', () => { + test('expects to restore the state persisted in localStorage', () => { + localStorage.setItem(localStorageKey, JSON.stringify(true)); + + expect(new ReplenishmentWorkBooksStore().canView()).toBe(true); + }); + + test('expects to be invisible when localStorage data is invalid', () => { localStorage.setItem(localStorageKey, 'invalid-json'); - expect(replenishmentWorkBooksStore.canView()).toBeFalsy(); + + expect(new ReplenishmentWorkBooksStore().canView()).toBe(false); }); }); describe('Replenishment workbooks store in SSR', () => { beforeEach(() => { - vi.mock('$app/environment', () => ({ - browser: false, - })); + browserState.value = false; }); - test('handles SSR gracefully', () => { - expect(replenishmentWorkBooksStore.canView()).toBeFalsy(); + test('expects to ignore localStorage and be invisible', () => { + localStorage.setItem(localStorageKey, JSON.stringify(true)); + + expect(new ReplenishmentWorkBooksStore().canView()).toBe(false); + }); + + test('expects not to persist state in localStorage when toggling', () => { + const store = new ReplenishmentWorkBooksStore(); + store.toggleView(); + + expect(store.canView()).toBe(true); + expect(localStorage.getItem(localStorageKey)).toBeNull(); }); }); diff --git a/src/test/lib/stores/active_problem_list_tab.svelte.test.ts b/src/test/lib/stores/active_problem_list_tab.svelte.test.ts index 6af9f0f4a..288edc478 100644 --- a/src/test/lib/stores/active_problem_list_tab.svelte.test.ts +++ b/src/test/lib/stores/active_problem_list_tab.svelte.test.ts @@ -1,42 +1,41 @@ -import { describe, test, expect, vi, beforeEach } from 'vitest'; +// @vitest-environment jsdom + +import { describe, test, expect, vi, beforeEach, afterEach } from 'vitest'; import { activeProblemListTabStore, ActiveProblemListTabStore, } from '$lib/stores/active_problem_list_tab.svelte'; +// WHY: a single dynamic mock is the only way to toggle `browser` per describe. Every vi.mock is +// hoisted and the last registration for a module wins, so a second vi.mock of `$app/environment` +// would silently clamp the whole file to one branch. +// Defaults to false so the singleton constructed at import time stays SSR-safe (no localStorage). +const browserState = vi.hoisted(() => ({ value: false })); + vi.mock('$app/environment', () => ({ - browser: true, + get browser() { + return browserState.value; + }, })); +const localStorageKey = 'active_problem_list_tab'; + +afterEach(() => { + localStorage.clear(); +}); + describe('ActiveProblemListTabStore', () => { let store: ActiveProblemListTabStore; - const mockLocalStorage: Storage = { - getItem: vi.fn((key) => mockStorage[key] || null), - setItem: vi.fn((key, value) => { - mockStorage[key] = value; - }), - removeItem: vi.fn(), - clear: vi.fn(), - length: 0, - key: vi.fn(), - }; - const mockStorage: Record = {}; - beforeEach(() => { - vi.clearAllMocks(); - // Setup mock for localStorage - vi.stubGlobal('localStorage', mockLocalStorage); + browserState.value = true; + localStorage.clear(); store = new ActiveProblemListTabStore(); store.reset(); }); - afterEach(() => { - vi.unstubAllGlobals(); - }); - describe('constructor', () => { test('expects to initialize with default value', () => { expect(store.get()).toBe('contestTable'); @@ -46,6 +45,12 @@ describe('ActiveProblemListTabStore', () => { const customStore = new ActiveProblemListTabStore('listByGrade'); expect(customStore.get()).toBe('listByGrade'); }); + + test('expects to restore the value persisted in localStorage', () => { + localStorage.setItem(localStorageKey, JSON.stringify('gradeGuidelineTable')); + + expect(new ActiveProblemListTabStore().get()).toBe('gradeGuidelineTable'); + }); }); describe('get', () => { @@ -62,6 +67,11 @@ describe('ActiveProblemListTabStore', () => { store.set('gradeGuidelineTable'); expect(store.get()).toBe('gradeGuidelineTable'); }); + + test('expects to persist the active tab in localStorage', () => { + store.set('listByGrade'); + expect(localStorage.getItem(localStorageKey)).toBe(JSON.stringify('listByGrade')); + }); }); describe('isSame', () => { @@ -90,17 +100,16 @@ describe('ActiveProblemListTabStore', () => { describe('Active problem list tab store in SSR', () => { beforeEach(() => { - vi.restoreAllMocks(); - vi.mock('$app/environment', () => ({ - browser: false, - })); + browserState.value = false; }); - afterEach(() => { - vi.restoreAllMocks(); + test('expects to ignore localStorage and initialize with default value', () => { + localStorage.setItem(localStorageKey, JSON.stringify('listByGrade')); + + expect(new ActiveProblemListTabStore().get()).toBe('contestTable'); }); - test('handles SSR gracefully', () => { + test('expects the singleton constructed at import time to hold the default value', () => { expect(activeProblemListTabStore.get()).toBe('contestTable'); }); }); diff --git a/vite.config.ts b/vite.config.ts index 65227f17c..f1c220a99 100755 --- a/vite.config.ts +++ b/vite.config.ts @@ -22,7 +22,8 @@ export default defineConfig({ 'dist/**', 'node_modules/**', ], - environment: 'jsdom', + // Default to node; DOM-dependent tests opt in via `// @vitest-environment jsdom`. + environment: 'node', globals: true, coverage: { provider: 'v8',