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>
56 lines
2.3 KiB
TypeScript
56 lines
2.3 KiB
TypeScript
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 */ }
|
|
}
|
|
}
|