From 5f135ec2b902d85d3d595c817e95f789d2803c00 Mon Sep 17 00:00:00 2001 From: "bert.hausmans" Date: Sat, 9 May 2026 03:27:31 +0200 Subject: [PATCH] test: add mountWithVuexy helper, install axe-core, segment vitest configs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Foundation for the upcoming component / integration / a11y tests. vitest.config.ts now declares two projects: - "unit" — pure-logic tests under tests/unit/, src/**/__tests__/, and tests/*.spec.ts (the legacy sanity test). happy-dom, no Vuetify, fast path. - "component" — tests under tests/component/, tests/integration/, tests/a11y/. jsdom, Vuetify inlined via SSR noExternal, CSS imports processed (so :root token sheet loads), and no global vue-router mock so the real router can run. Both share the same alias map and AutoImport bag. tests/utils/mountWithVuexy.ts (new): - Real Vuetify with the Crewli theme tokens - createTestingPinia (actions execute by default; stubActions opt-in) - vue-router with memory history at the configured initialPath + ?query - Fresh QueryClient per call (zero cross-test cache leak) - Notification mock injected via Pinia plugin so any useNotificationStore() resolves to { show: vi.fn(), hide: vi.fn() } — matches the actual NotificationStore API surface (per Phase A finding A4) - Imports `@/styles/tokens/_timetable.css` at module load so JSDOM resolves var(--tt-…) when components call getComputedStyle() tests/setup.component.ts (new): - vitest-axe matcher registration - JSDOM polyfills: scrollIntoView, ResizeObserver, visualViewport, body bounding rect — Vuetify menus / overlays would crash without them - Deterministic crypto polyfill (mirrors tests/setup.ts so generateIdempotencyKey() is stable, but without the router mock) tests/component/_smoke.test.ts (new): - Mounts a trivial component → asserts wrapper, queryClient, pinia, router, notificationMock all populated - Calls getComputedStyle(documentElement).getPropertyValue('--tt-status-confirmed-bg') → asserts '#e8f8f0' (proves the CSS token sheet really loaded) devDependencies added: jsdom, axe-core, vitest-axe, @pinia/testing. Total: 319 → 321 tests; 42 → 43 files. Both projects green. Co-Authored-By: Claude Opus 4.7 (1M context) --- apps/app/package.json | 4 + apps/app/pnpm-lock.yaml | 378 +++++++++++++++++++++++- apps/app/tests/component/_smoke.test.ts | 38 +++ apps/app/tests/setup.component.ts | 55 ++++ apps/app/tests/utils/mountWithVuexy.ts | 180 +++++++++++ apps/app/vitest.config.ts | 104 +++++-- 6 files changed, 728 insertions(+), 31 deletions(-) create mode 100644 apps/app/tests/component/_smoke.test.ts create mode 100644 apps/app/tests/setup.component.ts create mode 100644 apps/app/tests/utils/mountWithVuexy.ts diff --git a/apps/app/package.json b/apps/app/package.json index 7ad3e112..dcef67f4 100644 --- a/apps/app/package.json +++ b/apps/app/package.json @@ -81,6 +81,7 @@ "@iconify/utils": "2.3.0", "@iconify/vue": "4.1.2", "@intlify/unplugin-vue-i18n": "11.0.1", + "@pinia/testing": "^1.0.3", "@stylistic/eslint-plugin-js": "0.0.4", "@stylistic/eslint-plugin-ts": "0.0.4", "@stylistic/stylelint-config": "1.0.1", @@ -101,6 +102,7 @@ "@vitejs/plugin-vue": "6.0.1", "@vitejs/plugin-vue-jsx": "5.1.1", "@vue/test-utils": "^2.4.9", + "axe-core": "^4.11.4", "baseline-browser-mapping": "^2.10.16", "eslint": "8.57.1", "eslint-config-airbnb-base": "15.0.0", @@ -127,6 +129,7 @@ "eslint-plugin-vue": "9.33.0", "eslint-plugin-yml": "1.19.0", "happy-dom": "^20.9.0", + "jsdom": "^29.1.1", "msw": "2.6.8", "postcss-html": "1.8.0", "postcss-scss": "4.0.9", @@ -148,6 +151,7 @@ "vite-plugin-vuetify": "2.1.2", "vite-svg-loader": "5.1.0", "vitest": "^4.1.5", + "vitest-axe": "^0.1.0", "vue-eslint-parser": "9.4.3", "vue-shepherd": "3.0.0", "vue-tsc": "3.1.2" diff --git a/apps/app/pnpm-lock.yaml b/apps/app/pnpm-lock.yaml index 60001748..1761002e 100644 --- a/apps/app/pnpm-lock.yaml +++ b/apps/app/pnpm-lock.yaml @@ -199,6 +199,9 @@ importers: '@intlify/unplugin-vue-i18n': specifier: 11.0.1 version: 11.0.1(@vue/compiler-dom@3.5.22)(eslint@8.57.1)(rollup@4.52.5)(typescript@5.9.3)(vue-i18n@11.1.12(vue@3.5.22(typescript@5.9.3)))(vue@3.5.22(typescript@5.9.3)) + '@pinia/testing': + specifier: ^1.0.3 + version: 1.0.3(pinia@3.0.3(typescript@5.9.3)(vue@3.5.22(typescript@5.9.3))) '@stylistic/eslint-plugin-js': specifier: 0.0.4 version: 0.0.4 @@ -259,6 +262,9 @@ importers: '@vue/test-utils': specifier: ^2.4.9 version: 2.4.9(@vue/compiler-dom@3.5.22)(@vue/server-renderer@3.5.22(vue@3.5.22(typescript@5.9.3)))(vue@3.5.22(typescript@5.9.3)) + axe-core: + specifier: ^4.11.4 + version: 4.11.4 baseline-browser-mapping: specifier: ^2.10.16 version: 2.10.16 @@ -337,6 +343,9 @@ importers: happy-dom: specifier: ^20.9.0 version: 20.9.0 + jsdom: + specifier: ^29.1.1 + version: 29.1.1 msw: specifier: 2.6.8 version: 2.6.8(@types/node@24.9.2)(typescript@5.9.3) @@ -399,7 +408,10 @@ importers: version: 5.1.0(vue@3.5.22(typescript@5.9.3)) vitest: specifier: ^4.1.5 - version: 4.1.5(@types/node@24.9.2)(happy-dom@20.9.0)(msw@2.6.8(@types/node@24.9.2)(typescript@5.9.3))(vite@7.1.12(@types/node@24.9.2)(sass@1.76.0)(tsx@4.20.6)(yaml@2.8.1)) + version: 4.1.5(@types/node@24.9.2)(happy-dom@20.9.0)(jsdom@29.1.1)(msw@2.6.8(@types/node@24.9.2)(typescript@5.9.3))(vite@7.1.12(@types/node@24.9.2)(sass@1.76.0)(tsx@4.20.6)(yaml@2.8.1)) + vitest-axe: + specifier: ^0.1.0 + version: 0.1.0(vitest@4.1.5(@types/node@24.9.2)(happy-dom@20.9.0)(jsdom@29.1.1)(msw@2.6.8(@types/node@24.9.2)(typescript@5.9.3))(vite@7.1.12(@types/node@24.9.2)(sass@1.76.0)(tsx@4.20.6)(yaml@2.8.1))) vue-eslint-parser: specifier: 9.4.3 version: 9.4.3(eslint@8.57.1) @@ -440,6 +452,21 @@ packages: '@antfu/utils@8.1.1': resolution: {integrity: sha512-Mex9nXf9vR6AhcXmMrlz/HVgYYZpVGJ6YlPgwl7UnaFpnshXs6EK/oa5Gpf3CzENMjkvEx2tQtntGnb7UtSTOQ==} + '@asamuzakjp/css-color@5.1.11': + resolution: {integrity: sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + + '@asamuzakjp/dom-selector@7.1.1': + resolution: {integrity: sha512-67RZDnYRc8H/8MLDgQCDE//zoqVFwajkepHZgmXrbwybzXOEwOWGPYGmALYl9J2DOLfFPPs6kKCqmbzV895hTQ==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + + '@asamuzakjp/generational-cache@1.0.1': + resolution: {integrity: sha512-wajfB8KqzMCN2KGNFdLkReeHncd0AslUSrvHVvvYWuU8ghncRJoA50kT3zP9MVL0+9g4/67H+cdvBskj9THPzg==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + + '@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'} @@ -588,6 +615,10 @@ packages: resolution: {integrity: sha512-sAWO3D8PFP6pBXdxxW93SQi/KQqqhE2AAHo3AgWfdtJXwO6bfK6/wUN81XnOZk0qRC6vHzUEKhjwVD9dtDWvxg==} engines: {node: '>=18.18'} + '@bramus/specificity@2.4.2': + resolution: {integrity: sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==} + hasBin: true + '@bundled-es-modules/cookie@2.0.1': resolution: {integrity: sha512-8o+5fRPLNbjbdGRRmJj3h6Hh1AQJf2dk3qQ/5ZFb+PXkRNiSoMGGUKlsgLfrxneb72axVJyIYji64E2+nNfYyw==} @@ -606,16 +637,52 @@ packages: '@casl/ability': ^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.1.0 || ^6.0.0 vue: ^3.0.0 + '@csstools/color-helpers@6.0.2': + resolution: {integrity: sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q==} + engines: {node: '>=20.19.0'} + + '@csstools/css-calc@3.2.0': + resolution: {integrity: sha512-bR9e6o2BDB12jzN/gIbjHa5wLJ4UjD1CB9pM7ehlc0ddk6EBz+yYS1EV2MF55/HUxrHcB/hehAyt5vhsA3hx7w==} + engines: {node: '>=20.19.0'} + peerDependencies: + '@csstools/css-parser-algorithms': ^4.0.0 + '@csstools/css-tokenizer': ^4.0.0 + + '@csstools/css-color-parser@4.1.0': + resolution: {integrity: sha512-U0KhLYmy2GVj6q4T3WaAe6NPuFYCPQoE3b0dRGxejWDgcPp8TP7S5rVdM5ZrFaqu4N67X8YaPBw14dQSYx3IyQ==} + engines: {node: '>=20.19.0'} + peerDependencies: + '@csstools/css-parser-algorithms': ^4.0.0 + '@csstools/css-tokenizer': ^4.0.0 + '@csstools/css-parser-algorithms@2.7.1': resolution: {integrity: sha512-2SJS42gxmACHgikc1WGesXLIT8d/q2l0UFM7TaEeIzdFCE/FPMtTiizcPGGJtlPo2xuQzY09OhrLTzRxqJqwGw==} engines: {node: ^14 || ^16 || >=18} peerDependencies: '@csstools/css-tokenizer': ^2.4.1 + '@csstools/css-parser-algorithms@4.0.0': + resolution: {integrity: sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==} + engines: {node: '>=20.19.0'} + peerDependencies: + '@csstools/css-tokenizer': ^4.0.0 + + '@csstools/css-syntax-patches-for-csstree@1.1.3': + resolution: {integrity: sha512-SH60bMfrRCJF3morcdk57WklujF4Jr/EsQUzqkarfHXEFcAR1gg7fS/chAE922Sehgzc1/+Tz5H3Ypa1HiEKrg==} + peerDependencies: + css-tree: ^3.2.1 + peerDependenciesMeta: + css-tree: + optional: true + '@csstools/css-tokenizer@2.4.1': resolution: {integrity: sha512-eQ9DIktFJBhGjioABJRtUucoWR2mwllurfnM8LuNGAqX3ViZXaUchqk+1s7jjtkFiT9ySdACsFEA3etErkALUg==} engines: {node: ^14 || ^16 || >=18} + '@csstools/css-tokenizer@4.0.0': + resolution: {integrity: sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==} + engines: {node: '>=20.19.0'} + '@csstools/media-query-list-parser@2.1.13': resolution: {integrity: sha512-XaHr+16KRU9Gf8XLi3q8kDlI18d5vzKSKCY510Vrtc9iNR0NJzbY9hhTmwhzYZj/ZwGL4VmB3TA9hJW0Um2qFA==} engines: {node: ^14 || ^16 || >=18} @@ -819,6 +886,15 @@ packages: resolution: {integrity: sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + '@exodus/bytes@1.15.0': + resolution: {integrity: sha512-UY0nlA+feH81UGSHv92sLEPLCeZFjXOuHhrIo0HQydScuQc8s0A7kL/UdgwgDq8g8ilksmuoF35YVTNphV2aBQ==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + peerDependencies: + '@noble/hashes': ^1.8.0 || ^2.0.0 + peerDependenciesMeta: + '@noble/hashes': + optional: true + '@floating-ui/core@1.7.3': resolution: {integrity: sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==} @@ -1064,6 +1140,11 @@ packages: '@open-draft/until@2.1.0': resolution: {integrity: sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==} + '@pinia/testing@1.0.3': + resolution: {integrity: sha512-g+qR49GNdI1Z8rZxKrQC3GN+LfnGTNf5Kk8Nz5Cz6mIGva5WRS+ffPXQfzhA0nu6TveWzPNYTjGl4nJqd3Cu9Q==} + peerDependencies: + pinia: '>=3.0.4' + '@pkgjs/parseargs@0.11.0': resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} @@ -2212,6 +2293,10 @@ packages: resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} engines: {node: '>= 0.4'} + axe-core@4.11.4: + resolution: {integrity: sha512-KunSNx+TVpkAw/6ULfhnx+HWRecjqZGTOyquAoWHYLRSdK1tB5Ihce1ZW+UY3fj33bYAFWPu7W/GRSmmrCGuxA==} + engines: {node: '>=4'} + axios@1.15.0: resolution: {integrity: sha512-wWyJDlAatxk30ZJer+GeCWS209sA42X+N5jU2jy6oHTp7ufw8uzUTVFBX9+wTfAlhiJXGS0Bq7X6efruWjuK9Q==} @@ -2226,6 +2311,9 @@ packages: engines: {node: '>=6.0.0'} hasBin: true + bidi-js@1.0.3: + resolution: {integrity: sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==} + binary-extensions@2.3.0: resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} engines: {node: '>=8'} @@ -2306,6 +2394,10 @@ packages: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} + chalk@5.6.2: + resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} + engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + character-entities-html4@2.1.0: resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==} @@ -2462,6 +2554,10 @@ packages: resolution: {integrity: sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==} engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} + css-tree@3.2.1: + resolution: {integrity: sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} + css-what@6.2.2: resolution: {integrity: sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==} engines: {node: '>= 6'} @@ -2481,6 +2577,10 @@ packages: csstype@3.1.3: resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} + data-urls@7.0.0: + resolution: {integrity: sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + data-view-buffer@1.0.2: resolution: {integrity: sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==} engines: {node: '>= 0.4'} @@ -2514,6 +2614,9 @@ packages: resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==} engines: {node: '>=0.10.0'} + decimal.js@10.6.0: + resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} + deep-equal@2.2.3: resolution: {integrity: sha512-ZIwpnevOurS8bpT4192sqAowWM76JDKSHYzMLty3BZGSswgq6pBaH3DhCSW5xVAZICZyKdOBPjwww5wfgT/6PA==} engines: {node: '>= 0.4'} @@ -2643,6 +2746,10 @@ packages: resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==} engines: {node: '>=0.12'} + entities@8.0.0: + resolution: {integrity: sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==} + engines: {node: '>=20.19.0'} + env-paths@2.2.1: resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} engines: {node: '>=6'} @@ -3280,6 +3387,10 @@ packages: hosted-git-info@2.8.9: resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==} + html-encoding-sniffer@6.0.0: + resolution: {integrity: sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + html-tags@3.3.1: resolution: {integrity: sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ==} engines: {node: '>=8'} @@ -3460,6 +3571,9 @@ packages: resolution: {integrity: sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==} engines: {node: '>=0.10.0'} + is-potential-custom-element-name@1.0.1: + resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + is-regex@1.2.1: resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} engines: {node: '>= 0.4'} @@ -3548,6 +3662,15 @@ packages: resolution: {integrity: sha512-iZ8Bdb84lWRuGHamRXFyML07r21pcwBrLkHEuHgEY5UbCouBwv7ECknDRKzsQIXMiqpPymqtIf8TC/shYKB5rw==} engines: {node: '>=12.0.0'} + jsdom@29.1.1: + resolution: {integrity: sha512-ECi4Fi2f7BdJtUKTflYRTiaMxIB0O6zfR1fX0GXpUrf6flp8QIYn1UT20YQqdSOfk2dfkCwS8LAFoJDEppNK5Q==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24.0.0} + peerDependencies: + canvas: ^3.0.0 + peerDependenciesMeta: + canvas: + optional: true + jsesc@0.5.0: resolution: {integrity: sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==} hasBin: true @@ -3641,6 +3764,9 @@ packages: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} + lodash-es@4.18.1: + resolution: {integrity: sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==} + lodash.clonedeep@4.5.0: resolution: {integrity: sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==} @@ -3656,6 +3782,10 @@ packages: lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + lru-cache@11.3.6: + resolution: {integrity: sha512-Gf/KoL3C/MlI7Bt0PGI9I+TeTC/I6r/csU58N4BSNc4lppLBeKsOdFYkK+dX0ABDUMJNfCHTyPpzwwO21Awd3A==} + engines: {node: 20 || >=22} + lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} @@ -3705,6 +3835,9 @@ packages: mdn-data@2.25.0: resolution: {integrity: sha512-T2LPsjgUE/tgMmRXREVmwsux89DwWfNjiynOeXuLd2mX6jphGQ2YE3Ukz7LQ2VOFKiVZU/Ee1GqzHiipZCjymw==} + mdn-data@2.27.1: + resolution: {integrity: sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==} + mdurl@2.0.0: resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==} @@ -3990,6 +4123,9 @@ packages: parse5@7.3.0: resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} + parse5@8.0.1: + resolution: {integrity: sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==} + path-browserify@1.0.1: resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} @@ -4270,6 +4406,10 @@ packages: resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} engines: {node: '>=8.10.0'} + redent@3.0.0: + resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==} + engines: {node: '>=8'} + refa@0.12.1: resolution: {integrity: sha512-J8rn6v4DBb2nnFqkqwy6/NnTYMcgLA+sLr0iIO41qpv0n+ngb7ksag2tMRl0inb1bbO/esUwzW1vbJi7K0sI0g==} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} @@ -4397,6 +4537,10 @@ packages: engines: {node: '>=14.0.0'} hasBin: true + saxes@6.0.0: + resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} + engines: {node: '>=v12.22.7'} + scslre@0.3.0: resolution: {integrity: sha512-3A6sD0WYP7+QrjbfNA2FN3FsOaGGFoekCVgTyypy53gPxhbkCIjtO6YWgdrfM+n/8sI8JeXZOIxsHjMTNxQ4nQ==} engines: {node: ^14.0.0 || >=16.0.0} @@ -4719,6 +4863,9 @@ packages: engines: {node: '>=14.0.0'} hasBin: true + symbol-tree@3.2.4: + resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} + synckit@0.11.11: resolution: {integrity: sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw==} engines: {node: ^14.18.0 || >=16.0.0} @@ -4770,6 +4917,13 @@ packages: tippy.js@6.3.7: resolution: {integrity: sha512-E1d3oP2emgJ9dRQZdf3Kkn0qJgI6ZLpyS5z6ZkY1DF3kaQaBsGZsndEpHwx+eC+tYM41HaSNvNtLx8tU57FzTQ==} + tldts-core@7.0.30: + resolution: {integrity: sha512-uiHN8PIB1VmWyS98eZYja4xzlYqeFZVjb4OuYlJQnZAuJhMw4PbKQOKgHKhBdJR3FE/t5mUQ1Kd80++B+qhD1Q==} + + tldts@7.0.30: + resolution: {integrity: sha512-ELrFxuqsDdHUwoh0XxDbxuLD3Wnz49Z57IFvTtvWy1hJdcMZjXLIuonjilCiWHlT2GbE4Wlv1wKVTzDFnXH1aw==} + hasBin: true + to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} @@ -4782,6 +4936,14 @@ packages: resolution: {integrity: sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==} engines: {node: '>=6'} + tough-cookie@6.0.1: + resolution: {integrity: sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==} + engines: {node: '>=16'} + + tr46@6.0.0: + resolution: {integrity: sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==} + engines: {node: '>=20'} + trim-lines@3.0.1: resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} @@ -4887,6 +5049,10 @@ packages: resolution: {integrity: sha512-hU/10obOIu62MGYjdskASR3CUAiYaFTtC9Pa6vHyf//mAipSvSQg6od2CnJswq7fvzNS3zJhxoRkgNVaHurWKw==} engines: {node: '>=18.17'} + undici@7.25.0: + resolution: {integrity: sha512-xXnp4kTyor2Zq+J1FfPI6Eq3ew5h6Vl0F/8d9XU5zZQf1tX9s2Su1/3PiMmUANFULpmksxkClamIZcaUqryHsQ==} + engines: {node: '>=20.18.1'} + unicorn-magic@0.3.0: resolution: {integrity: sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==} engines: {node: '>=18'} @@ -5086,6 +5252,11 @@ packages: yaml: optional: true + vitest-axe@0.1.0: + resolution: {integrity: sha512-jvtXxeQPg8R/2ANTY8QicA5pvvdRP4F0FsVUAHANJ46YCDASie/cuhlSzu0DGcLmZvGBSBNsNuK3HqfaeknyvA==} + peerDependencies: + vitest: '>=0.16.0' + vitest@4.1.5: resolution: {integrity: sha512-9Xx1v3/ih3m9hN+SbfkUyy0JAs72ap3r7joc87XL6jwF0jGg6mFBvQ1SrwaX+h8BlkX6Hz9shdd1uo6AF+ZGpg==} engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} @@ -5232,9 +5403,17 @@ packages: w3c-keyname@2.2.8: resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==} + w3c-xmlserializer@5.0.0: + resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} + engines: {node: '>=18'} + webfontloader@1.6.28: resolution: {integrity: sha512-Egb0oFEga6f+nSgasH3E0M405Pzn6y3/9tOVanv/DLfa1YBIgcv90L18YyWnvXkRbIM17v5Kv6IT2N6g1x5tvQ==} + webidl-conversions@8.0.1: + resolution: {integrity: sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==} + engines: {node: '>=20'} + webpack-virtual-modules@0.6.2: resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==} @@ -5251,6 +5430,14 @@ packages: resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} engines: {node: '>=18'} + whatwg-mimetype@5.0.0: + resolution: {integrity: sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==} + engines: {node: '>=20'} + + whatwg-url@16.0.1: + resolution: {integrity: sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + which-boxed-primitive@1.1.1: resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==} engines: {node: '>= 0.4'} @@ -5330,6 +5517,13 @@ packages: resolution: {integrity: sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==} engines: {node: '>=12'} + xml-name-validator@5.0.0: + resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} + engines: {node: '>=18'} + + xmlchars@2.2.0: + resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + y18n@4.0.3: resolution: {integrity: sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==} @@ -5489,6 +5683,26 @@ snapshots: '@antfu/utils@8.1.1': {} + '@asamuzakjp/css-color@5.1.11': + dependencies: + '@asamuzakjp/generational-cache': 1.0.1 + '@csstools/css-calc': 3.2.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-color-parser': 4.1.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-tokenizer': 4.0.0 + + '@asamuzakjp/dom-selector@7.1.1': + dependencies: + '@asamuzakjp/generational-cache': 1.0.1 + '@asamuzakjp/nwsapi': 2.3.9 + bidi-js: 1.0.3 + css-tree: 3.2.1 + is-potential-custom-element-name: 1.0.1 + + '@asamuzakjp/generational-cache@1.0.1': {} + + '@asamuzakjp/nwsapi@2.3.9': {} + '@babel/code-frame@7.27.1': dependencies: '@babel/helper-validator-identifier': 7.28.5 @@ -5696,6 +5910,10 @@ snapshots: - eslint-import-resolver-webpack - supports-color + '@bramus/specificity@2.4.2': + dependencies: + css-tree: 3.2.1 + '@bundled-es-modules/cookie@2.0.1': dependencies: cookie: 0.7.2 @@ -5718,12 +5936,36 @@ snapshots: '@casl/ability': 6.7.3 vue: 3.5.22(typescript@5.9.3) + '@csstools/color-helpers@6.0.2': {} + + '@csstools/css-calc@3.2.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)': + dependencies: + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-tokenizer': 4.0.0 + + '@csstools/css-color-parser@4.1.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)': + dependencies: + '@csstools/color-helpers': 6.0.2 + '@csstools/css-calc': 3.2.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-tokenizer': 4.0.0 + '@csstools/css-parser-algorithms@2.7.1(@csstools/css-tokenizer@2.4.1)': dependencies: '@csstools/css-tokenizer': 2.4.1 + '@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0)': + dependencies: + '@csstools/css-tokenizer': 4.0.0 + + '@csstools/css-syntax-patches-for-csstree@1.1.3(css-tree@3.2.1)': + optionalDependencies: + css-tree: 3.2.1 + '@csstools/css-tokenizer@2.4.1': {} + '@csstools/css-tokenizer@4.0.0': {} + '@csstools/media-query-list-parser@2.1.13(@csstools/css-parser-algorithms@2.7.1(@csstools/css-tokenizer@2.4.1))(@csstools/css-tokenizer@2.4.1)': dependencies: '@csstools/css-parser-algorithms': 2.7.1(@csstools/css-tokenizer@2.4.1) @@ -5858,6 +6100,8 @@ snapshots: '@eslint/js@8.57.1': {} + '@exodus/bytes@1.15.0': {} + '@floating-ui/core@1.7.3': dependencies: '@floating-ui/utils': 0.2.10 @@ -6130,6 +6374,10 @@ snapshots: '@open-draft/until@2.1.0': {} + '@pinia/testing@1.0.3(pinia@3.0.3(typescript@5.9.3)(vue@3.5.22(typescript@5.9.3)))': + dependencies: + pinia: 3.0.3(typescript@5.9.3)(vue@3.5.22(typescript@5.9.3)) + '@pkgjs/parseargs@0.11.0': optional: true @@ -7400,6 +7648,8 @@ snapshots: dependencies: possible-typed-array-names: 1.1.0 + axe-core@4.11.4: {} + axios@1.15.0: dependencies: follow-redirects: 1.15.11 @@ -7414,6 +7664,10 @@ snapshots: baseline-browser-mapping@2.10.16: {} + bidi-js@1.0.3: + dependencies: + require-from-string: 2.0.2 + binary-extensions@2.3.0: {} birpc@2.6.1: {} @@ -7492,6 +7746,8 @@ snapshots: ansi-styles: 4.3.0 supports-color: 7.2.0 + chalk@5.6.2: {} + character-entities-html4@2.1.0: {} character-entities-legacy@1.1.4: {} @@ -7656,6 +7912,11 @@ snapshots: mdn-data: 2.12.2 source-map-js: 1.2.1 + css-tree@3.2.1: + dependencies: + mdn-data: 2.27.1 + source-map-js: 1.2.1 + css-what@6.2.2: {} csscolorparser@1.0.3: {} @@ -7668,6 +7929,13 @@ snapshots: csstype@3.1.3: {} + data-urls@7.0.0: + dependencies: + whatwg-mimetype: 5.0.0 + whatwg-url: 16.0.1 + transitivePeerDependencies: + - '@noble/hashes' + data-view-buffer@1.0.2: dependencies: call-bound: 1.0.4 @@ -7696,6 +7964,8 @@ snapshots: decamelize@1.2.0: {} + decimal.js@10.6.0: {} + deep-equal@2.2.3: dependencies: array-buffer-byte-length: 1.0.2 @@ -7830,6 +8100,8 @@ snapshots: entities@7.0.1: {} + entities@8.0.0: {} + env-paths@2.2.1: {} error-ex@1.3.4: @@ -8781,6 +9053,12 @@ snapshots: hosted-git-info@2.8.9: {} + html-encoding-sniffer@6.0.0: + dependencies: + '@exodus/bytes': 1.15.0 + transitivePeerDependencies: + - '@noble/hashes' + html-tags@3.3.1: {} html-void-elements@3.0.0: {} @@ -8950,6 +9228,8 @@ snapshots: is-plain-object@5.0.0: {} + is-potential-custom-element-name@1.0.1: {} + is-regex@1.2.1: dependencies: call-bound: 1.0.4 @@ -9031,6 +9311,32 @@ snapshots: jsdoc-type-pratt-parser@4.8.0: {} + jsdom@29.1.1: + dependencies: + '@asamuzakjp/css-color': 5.1.11 + '@asamuzakjp/dom-selector': 7.1.1 + '@bramus/specificity': 2.4.2 + '@csstools/css-syntax-patches-for-csstree': 1.1.3(css-tree@3.2.1) + '@exodus/bytes': 1.15.0 + css-tree: 3.2.1 + data-urls: 7.0.0 + decimal.js: 10.6.0 + html-encoding-sniffer: 6.0.0 + is-potential-custom-element-name: 1.0.1 + lru-cache: 11.3.6 + parse5: 8.0.1 + saxes: 6.0.0 + symbol-tree: 3.2.4 + tough-cookie: 6.0.1 + undici: 7.25.0 + w3c-xmlserializer: 5.0.0 + webidl-conversions: 8.0.1 + whatwg-mimetype: 5.0.0 + whatwg-url: 16.0.1 + xml-name-validator: 5.0.0 + transitivePeerDependencies: + - '@noble/hashes' + jsesc@0.5.0: {} jsesc@3.1.0: {} @@ -9108,6 +9414,8 @@ snapshots: dependencies: p-locate: 5.0.0 + lodash-es@4.18.1: {} + lodash.clonedeep@4.5.0: {} lodash.merge@4.6.2: {} @@ -9118,6 +9426,8 @@ snapshots: lru-cache@10.4.3: {} + lru-cache@11.3.6: {} + lru-cache@5.1.1: dependencies: yallist: 3.1.1 @@ -9209,6 +9519,8 @@ snapshots: mdn-data@2.25.0: {} + mdn-data@2.27.1: {} + mdurl@2.0.0: {} meow@13.2.0: {} @@ -9522,6 +9834,10 @@ snapshots: dependencies: entities: 6.0.1 + parse5@8.0.1: + dependencies: + entities: 8.0.0 + path-browserify@1.0.1: {} path-exists@4.0.0: {} @@ -9811,6 +10127,11 @@ snapshots: dependencies: picomatch: 2.3.1 + redent@3.0.0: + dependencies: + indent-string: 4.0.0 + strip-indent: 3.0.0 + refa@0.12.1: dependencies: '@eslint-community/regexpp': 4.12.2 @@ -9968,6 +10289,10 @@ snapshots: immutable: 4.3.7 source-map-js: 1.2.1 + saxes@6.0.0: + dependencies: + xmlchars: 2.2.0 + scslre@0.3.0: dependencies: '@eslint-community/regexpp': 4.12.2 @@ -10352,6 +10677,8 @@ snapshots: csso: 5.0.5 picocolors: 1.1.1 + symbol-tree@3.2.4: {} + synckit@0.11.11: dependencies: '@pkgr/core': 0.2.9 @@ -10403,6 +10730,12 @@ snapshots: dependencies: '@popperjs/core': 2.11.8 + tldts-core@7.0.30: {} + + tldts@7.0.30: + dependencies: + tldts-core: 7.0.30 + to-regex-range@5.0.1: dependencies: is-number: 7.0.0 @@ -10416,6 +10749,14 @@ snapshots: universalify: 0.2.0 url-parse: 1.5.10 + tough-cookie@6.0.1: + dependencies: + tldts: 7.0.30 + + tr46@6.0.0: + dependencies: + punycode: 2.3.1 + trim-lines@3.0.1: {} ts-api-utils@1.4.3(typescript@5.9.3): @@ -10520,6 +10861,8 @@ snapshots: undici@6.22.0: {} + undici@7.25.0: {} + unicorn-magic@0.3.0: {} unimport@3.14.6(rollup@4.52.5): @@ -10800,7 +11143,17 @@ snapshots: tsx: 4.20.6 yaml: 2.8.1 - vitest@4.1.5(@types/node@24.9.2)(happy-dom@20.9.0)(msw@2.6.8(@types/node@24.9.2)(typescript@5.9.3))(vite@7.1.12(@types/node@24.9.2)(sass@1.76.0)(tsx@4.20.6)(yaml@2.8.1)): + vitest-axe@0.1.0(vitest@4.1.5(@types/node@24.9.2)(happy-dom@20.9.0)(jsdom@29.1.1)(msw@2.6.8(@types/node@24.9.2)(typescript@5.9.3))(vite@7.1.12(@types/node@24.9.2)(sass@1.76.0)(tsx@4.20.6)(yaml@2.8.1))): + dependencies: + aria-query: 5.1.3 + axe-core: 4.11.4 + chalk: 5.6.2 + dom-accessibility-api: 0.5.16 + lodash-es: 4.18.1 + redent: 3.0.0 + vitest: 4.1.5(@types/node@24.9.2)(happy-dom@20.9.0)(jsdom@29.1.1)(msw@2.6.8(@types/node@24.9.2)(typescript@5.9.3))(vite@7.1.12(@types/node@24.9.2)(sass@1.76.0)(tsx@4.20.6)(yaml@2.8.1)) + + vitest@4.1.5(@types/node@24.9.2)(happy-dom@20.9.0)(jsdom@29.1.1)(msw@2.6.8(@types/node@24.9.2)(typescript@5.9.3))(vite@7.1.12(@types/node@24.9.2)(sass@1.76.0)(tsx@4.20.6)(yaml@2.8.1)): dependencies: '@vitest/expect': 4.1.5 '@vitest/mocker': 4.1.5(msw@2.6.8(@types/node@24.9.2)(typescript@5.9.3))(vite@7.1.12(@types/node@24.9.2)(sass@1.76.0)(tsx@4.20.6)(yaml@2.8.1)) @@ -10825,6 +11178,7 @@ snapshots: optionalDependencies: '@types/node': 24.9.2 happy-dom: 20.9.0 + jsdom: 29.1.1 transitivePeerDependencies: - msw @@ -10924,8 +11278,14 @@ snapshots: w3c-keyname@2.2.8: {} + w3c-xmlserializer@5.0.0: + dependencies: + xml-name-validator: 5.0.0 + webfontloader@1.6.28: {} + webidl-conversions@8.0.1: {} + webpack-virtual-modules@0.6.2: {} whatwg-encoding@3.1.1: @@ -10936,6 +11296,16 @@ snapshots: whatwg-mimetype@4.0.0: {} + whatwg-mimetype@5.0.0: {} + + whatwg-url@16.0.1: + dependencies: + '@exodus/bytes': 1.15.0 + tr46: 6.0.0 + webidl-conversions: 8.0.1 + transitivePeerDependencies: + - '@noble/hashes' + which-boxed-primitive@1.1.1: dependencies: is-bigint: 1.1.0 @@ -11029,6 +11399,10 @@ snapshots: xml-name-validator@4.0.0: {} + xml-name-validator@5.0.0: {} + + xmlchars@2.2.0: {} + y18n@4.0.3: {} y18n@5.0.8: {} diff --git a/apps/app/tests/component/_smoke.test.ts b/apps/app/tests/component/_smoke.test.ts new file mode 100644 index 00000000..a3056ca1 --- /dev/null +++ b/apps/app/tests/component/_smoke.test.ts @@ -0,0 +1,38 @@ +import { describe, expect, it } from 'vitest' +import { defineComponent, h } from 'vue' +import { mountWithVuexy } from '../utils/mountWithVuexy' + +describe('mountWithVuexy harness', () => { + it('mounts a trivial component with the full Vuexy stack', () => { + const Trivial = defineComponent({ + setup() { + return () => h('div', { 'data-test': 'ok' }, 'hello') + }, + }) + + const { wrapper, queryClient, pinia, router, notificationMock } = mountWithVuexy(Trivial) + + expect(wrapper.find('[data-test="ok"]').text()).toBe('hello') + expect(queryClient).toBeDefined() + expect(pinia).toBeDefined() + expect(router).toBeDefined() + expect(notificationMock.show).toBeTypeOf('function') + }) + + it('loads the timetable CSS token sheet so var(--tt-…) resolves on :root', () => { + const Probe = defineComponent({ + setup() { + return () => h('div', { id: 'probe' }) + }, + }) + + mountWithVuexy(Probe) + + // The CSS file is imported at module load time inside mountWithVuexy. + // Resolving against documentElement (=:root) avoids ambiguity around + // jsdom's default style-cascade behaviour on arbitrary elements. + const value = getComputedStyle(document.documentElement).getPropertyValue('--tt-status-confirmed-bg').trim() + + expect(value).toBe('#e8f8f0') + }) +}) diff --git a/apps/app/tests/setup.component.ts b/apps/app/tests/setup.component.ts new file mode 100644 index 00000000..f68e9754 --- /dev/null +++ b/apps/app/tests/setup.component.ts @@ -0,0 +1,55 @@ +import 'vitest-axe/extend-expect' +import { expect } from 'vitest' +import * as matchers from 'vitest-axe/matchers' + +// Register vitest-axe's `toHaveNoViolations` matcher so a11y tests can call +// `expect(await axe(node)).toHaveNoViolations()`. +expect.extend(matchers) + +// Deterministic crypto polyfill (mirrors tests/setup.ts) so generateIdempotencyKey() +// returns a stable value across component-test runs without bringing in the +// router mock from tests/setup.ts. +if (!globalThis.crypto) { + ;(globalThis as { crypto: Crypto }).crypto = { + randomUUID: () => '00000000-0000-4000-8000-000000000000', + getRandomValues: (buf: Uint8Array) => { + for (let i = 0; i < buf.length; i++) buf[i] = 0 + + return buf + }, + } as unknown as Crypto +} + +// JSDOM's `Element.scrollIntoView` is not implemented by default; Vuetify's +// list/menu components call it during opening transitions. Stub it so the +// test environment doesn't throw. +if (typeof Element !== 'undefined' && !Element.prototype.scrollIntoView) + Element.prototype.scrollIntoView = () => undefined + +// JSDOM's `getBoundingClientRect` returns zeros, which is fine for most +// assertions but breaks Vuetify positioning math in some menus. Provide a +// minimal viewport size on document body so anchored components can render. +if (typeof document !== 'undefined') { + Object.defineProperty(document.body, 'getBoundingClientRect', { + configurable: true, + value: () => ({ top: 0, left: 0, right: 1024, bottom: 768, width: 1024, height: 768, x: 0, y: 0, toJSON: () => ({}) }), + }) +} + +// `window.visualViewport` is consulted by Vuetify; happy-dom has it but +// jsdom does not. Stub the minimum surface the lib reads. +if (typeof window !== 'undefined' && !window.visualViewport) { + Object.defineProperty(window, 'visualViewport', { + configurable: true, + value: { width: 1024, height: 768, offsetLeft: 0, offsetTop: 0, scale: 1, addEventListener: () => undefined, removeEventListener: () => undefined }, + }) +} + +// `ResizeObserver` is required by Vuetify VOverlay and friends; jsdom lacks it. +if (typeof globalThis.ResizeObserver === 'undefined') { + ;(globalThis as { ResizeObserver: unknown }).ResizeObserver = class ResizeObserver { + observe(): void { /* noop */ } + unobserve(): void { /* noop */ } + disconnect(): void { /* noop */ } + } +} diff --git a/apps/app/tests/utils/mountWithVuexy.ts b/apps/app/tests/utils/mountWithVuexy.ts new file mode 100644 index 00000000..718cda6e --- /dev/null +++ b/apps/app/tests/utils/mountWithVuexy.ts @@ -0,0 +1,180 @@ +import { type VueWrapper, mount } from '@vue/test-utils' +import { createTestingPinia } from '@pinia/testing' +import type { TestingPinia } from '@pinia/testing' +import { QueryClient, VueQueryPlugin } from '@tanstack/vue-query' +import { type RouteRecordRaw, type Router, createMemoryHistory, createRouter } from 'vue-router' +import { type ThemeDefinition, createVuetify } from 'vuetify' +import * as components from 'vuetify/components' +import * as directives from 'vuetify/directives' +import { vi } from 'vitest' +import type { Component } from 'vue' + +// Plain-CSS token sheet — JSDOM evaluates :root custom properties from this +// import so getComputedStyle(el).getPropertyValue('--tt-status-…') resolves +// during component tests. Path resolved by vitest.config alias `@`. +import '@/styles/tokens/_timetable.css' + +/** + * Notification mock matching the actual store API: + * useNotificationStore().show(message, type, duration) + * (See apps/app/src/stores/useNotificationStore.ts) + */ +export interface NotificationMock { + show: ReturnType + hide: ReturnType +} + +export function createNotificationMock(): NotificationMock { + return { + show: vi.fn(), + hide: vi.fn(), + } +} + +export interface MountWithVuexyOptions { + + /** Routes to register on the test router. Default: a single catch-all. */ + routes?: RouteRecordRaw[] + + /** Initial path the router opens at. Default: '/'. */ + initialPath?: string + + /** Initial query string params. */ + initialQuery?: Record + + /** Initial Pinia store state (per-store map, see @pinia/testing docs). */ + initialState?: Record> + + /** Override the default fresh QueryClient (useful for prefilled caches). */ + queryClient?: QueryClient + + /** Provide a custom notification mock; default `createNotificationMock()`. */ + notificationMock?: NotificationMock + + /** + * Set to `true` to use createTestingPinia's default action stubbing (every + * action becomes a vi.fn that does nothing). Default `false` — actions + * still execute so component tests exercise real store behaviour. + */ + stubActions?: boolean + + /** props forwarded to mount(). */ + props?: Record + + /** Slots for mount(). */ + slots?: Record + + /** Optional global stubs. */ + stubs?: Record +} + +export interface MountWithVuexyResult { + wrapper: VueWrapper + router: Router + pinia: TestingPinia + queryClient: QueryClient + notificationMock: NotificationMock +} + +const defaultTheme: ThemeDefinition = { + dark: false, + colors: { + primary: '#1f7ad1', + error: '#d63d4b', + success: '#2fa66a', + warning: '#e0992c', + info: '#1f7ad1', + }, +} + +/** + * Mounts a Vue component with the full Vuexy/Vuetify stack wired up: + * - Vuetify (real components + directives, default theme tokens) + * - Pinia (createTestingPinia — actions execute by default) + * - TanStack Vue Query (a fresh QueryClient per call — never shared) + * - Vue Router (memory history, opens at `initialPath` with `initialQuery`) + * - Notification store mocked at the Pinia layer + * + * Each call gets fresh instances of router, pinia, and queryClient — no + * cross-test leakage. The notification mock is exposed so tests can assert + * `expect(notificationMock.show).toHaveBeenCalledWith('…', 'error', …)`. + */ +export function mountWithVuexy(component: Component, options: MountWithVuexyOptions = {}): MountWithVuexyResult { + const { + routes = [{ path: '/', component: { template: '
' } }, { path: '/:pathMatch(.*)*', component: { template: '
' } }], + initialPath = '/', + initialQuery, + initialState = {}, + queryClient = new QueryClient({ defaultOptions: { queries: { retry: false }, mutations: { retry: false } } }), + notificationMock = createNotificationMock(), + stubActions = false, + props, + slots, + stubs, + } = options + + const router = createRouter({ history: createMemoryHistory(), routes }) + + // Patch the notification store via initialState so any useNotificationStore() + // call resolves to the mock fns. Pinia testing replaces actions when + // stubActions=true; here we override the action surface explicitly so the + // mock is consistent regardless of stubActions. + const pinia = createTestingPinia({ + stubActions, + initialState: { + ...initialState, + notification: { + visible: false, + message: '', + type: 'info', + timeout: 5000, + ...(initialState.notification ?? {}), + }, + }, + createSpy: vi.fn, + }) + + // Bind the notification action mocks into the store. We do this AFTER + // createTestingPinia so the store is registered. + pinia.use(({ store }) => { + if (store.$id === 'notification') { + store.show = notificationMock.show + store.hide = notificationMock.hide + } + }) + + const vuetify = createVuetify({ + components, + directives, + theme: { defaultTheme: 'crewliLight', themes: { crewliLight: defaultTheme } }, + }) + + const navigatePromise = (async () => { + if (initialQuery) + await router.push({ path: initialPath, query: initialQuery }) + else + await router.push(initialPath) + await router.isReady() + })() + + const wrapper = mount(component, { + props, + slots, + global: { + plugins: [ + vuetify, + pinia, + router, + [VueQueryPlugin, { queryClient }], + ], + stubs, + }, + }) + + // The router push above is fire-and-forget; consumers that need the route + // to be settled before the first assertion should `await wrapper.vm.$nextTick()` + // a couple of times after mount. We attach the promise so tests can await it. + ;(wrapper as unknown as { __routerReady: Promise }).__routerReady = navigatePromise + + return { wrapper, router, pinia, queryClient, notificationMock } +} diff --git a/apps/app/vitest.config.ts b/apps/app/vitest.config.ts index 0548d309..1ddc2537 100644 --- a/apps/app/vitest.config.ts +++ b/apps/app/vitest.config.ts @@ -3,36 +3,82 @@ import vue from '@vitejs/plugin-vue' import AutoImport from 'unplugin-auto-import/vite' import { defineConfig } from 'vitest/config' -// Dedicated Vitest config — intentionally trimmed down from vite.config.ts. -// Skip Vuetify / MetaLayouts / VueRouter plugins so unit tests run fast in -// happy-dom without loading the full Vuexy bundle. Mirrors apps/portal/vitest.config.ts. -export default defineConfig({ - plugins: [ - vue(), - AutoImport({ - imports: ['vue', '@vueuse/core'], - dirs: ['./src/@core/utils', './src/@core/composable/', './src/composables/', './src/utils/'], - vueTemplate: true, +// Two projects share one config: +// +// - "unit" — pure-logic tests under tests/unit/ + src/**/__tests__/. +// No Vuetify, no SCSS plugin, happy-dom only. Fast path. +// - "component" — component / integration / a11y tests under tests/component/, +// tests/integration/, tests/a11y/. Loads CSS imports so +// `import '@/styles/tokens/_timetable.css'` resolves and +// getComputedStyle() returns var(--tt-…) values in jsdom. +// +// Both share the same alias map and AutoImport bag so test paths and the +// auto-imported `ref/computed/watch` etc. work identically. +const sharedAliases = { + '@': fileURLToPath(new URL('./src', import.meta.url)), + '@core': fileURLToPath(new URL('./src/@core', import.meta.url)), + '@layouts': fileURLToPath(new URL('./src/@layouts', import.meta.url)), + '@images': fileURLToPath(new URL('./src/assets/images/', import.meta.url)), + '@styles': fileURLToPath(new URL('./src/assets/styles/', import.meta.url)), +} - // Don't write to auto-imports.d.ts — vite.config.ts owns that file - // with the full app's auto-import set. Trimmed test-only set must - // not clobber the IDE typings for the running dev server. - dts: false, - }), - ], - resolve: { - alias: { - '@': fileURLToPath(new URL('./src', import.meta.url)), - '@core': fileURLToPath(new URL('./src/@core', import.meta.url)), - '@layouts': fileURLToPath(new URL('./src/@layouts', import.meta.url)), - '@images': fileURLToPath(new URL('./src/assets/images/', import.meta.url)), - '@styles': fileURLToPath(new URL('./src/assets/styles/', import.meta.url)), - }, - }, +const sharedAutoImport = AutoImport({ + imports: ['vue', '@vueuse/core'], + dirs: ['./src/@core/utils', './src/@core/composable/', './src/composables/', './src/utils/'], + vueTemplate: true, + dts: false, +}) + +export default defineConfig({ test: { - environment: 'happy-dom', - globals: true, - include: ['tests/**/*.{test,spec}.ts', 'src/**/__tests__/**/*.{test,spec}.ts'], - setupFiles: ['./tests/setup.ts'], + projects: [ + { + plugins: [vue(), sharedAutoImport], + resolve: { alias: sharedAliases }, + test: { + name: 'unit', + environment: 'happy-dom', + globals: true, + include: [ + 'tests/unit/**/*.{test,spec}.ts', + 'tests/*.{test,spec}.ts', + 'src/**/__tests__/**/*.{test,spec}.ts', + ], + setupFiles: ['./tests/setup.ts'], + }, + }, + { + plugins: [vue(), sharedAutoImport], + resolve: { alias: sharedAliases }, + + // Inline Vuetify so its ESM bits are processed by Vite's transform. + ssr: { noExternal: ['vuetify'] }, + test: { + name: 'component', + environment: 'jsdom', + globals: true, + include: [ + 'tests/component/**/*.{test,spec}.ts', + 'tests/integration/**/*.{test,spec}.ts', + 'tests/a11y/**/*.{test,spec}.ts', + ], + + // Intentionally NOT including ./tests/setup.ts — it stubs `vue-router` + // globally for the unit project, which would defeat the real router + // wired by mountWithVuexy. setup.component.ts handles its own + // crypto/JSDOM stubs. + setupFiles: ['./tests/setup.component.ts'], + + // CSS @import statements (e.g. `@/styles/tokens/_timetable.css`) + // need to actually load so getComputedStyle resolves CSS variables. + css: true, + server: { + deps: { + inline: ['vuetify'], + }, + }, + }, + }, + ], }, })