test: add mountWithVuexy helper, install axe-core, segment vitest configs

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) <noreply@anthropic.com>
This commit is contained in:
2026-05-09 03:27:31 +02:00
parent b7d814ad85
commit 5f135ec2b9
6 changed files with 728 additions and 31 deletions

View File

@@ -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"

378
apps/app/pnpm-lock.yaml generated
View File

@@ -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: {}

View File

@@ -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')
})
})

View File

@@ -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 */ }
}
}

View File

@@ -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<typeof vi.fn>
hide: ReturnType<typeof vi.fn>
}
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<string, string>
/** Initial Pinia store state (per-store map, see @pinia/testing docs). */
initialState?: Record<string, Record<string, unknown>>
/** 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<string, unknown>
/** Slots for mount(). */
slots?: Record<string, unknown>
/** Optional global stubs. */
stubs?: Record<string, Component | boolean>
}
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: '<div />' } }, { path: '/:pathMatch(.*)*', component: { template: '<div />' } }],
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<void> }).__routerReady = navigatePromise
return { wrapper, router, pinia, queryClient, notificationMock }
}

View File

@@ -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'],
},
},
},
},
],
},
})