Remove every as any from the test suite and enforce it

ESLint turned `no-explicit-any` off for test files, and 219 casts had
accumulated across 12 of them. Two shapes, very different risks: most
were `(useWorkspace as any).mockReturnValue(...)`, mere laziness that
`vi.mocked()` does properly; the rest cast partial fixtures onto API
types, which erases the contract exactly where a test should enforce it.

That second kind matters here specifically. This project's recurring bug
is a field silently dropped between backend and frontend -- the
hand-written mapping in models/plan.py has done it twice, and apiFetch
mishandling structured errors was the same shape. A test that casts an
incomplete fixture to `any` keeps passing through precisely that change.

A typed fixture layer already existed at tests/fixtures/mockData.ts and
was imported by exactly zero tests: the right tool was there and
invisible, so everyone reached for a cast. It is now a set of
`make*(overrides: Partial<T>) => T` factories returning complete, valid
objects, split by domain with a barrel. When a type gains a field, one
factory fails to compile instead of thirty tests silently passing.

With the count at zero the ESLint exemption is deleted, so this cannot
regrow. Removing it immediately caught two `}: any` prop stubs that a
grep for "as any" had missed; those mocks now use the real components'
exported prop types.

`as unknown as T` survives in five places, each a test deliberately
feeding invalid input (an undefined breakdown, missing tierData, a bogus
leave type). CLAUDE.md now documents that as the only accepted escape,
and points at the factories.

interface PollResponse in useScraperFetch.ts is exported so its shape can
be derived rather than duplicated -- pollScraperSession declares
Promise<unknown>, so there was no other honest way to type it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RXMhc9nT6sNz1SL4j8dznL
This commit is contained in:
2026-09-15 04:17:22 +02:00
parent da7ac84aca
commit fe120ee222
24 changed files with 1905 additions and 1137 deletions
+36 -51
View File
@@ -2,6 +2,7 @@ import { describe, it, expect, beforeEach, vi } from 'vitest'
import { renderHook, act } from '@testing-library/react'
import { usePlannedDays } from './usePlannedDays'
import * as api from '@/services/api'
import { makePlannedLeaveDay } from '../../tests/fixtures'
vi.mock('@/services/api')
@@ -18,7 +19,7 @@ describe('usePlannedDays', () => {
it('fetches planned days when childId provided', async () => {
const mockData = [
{
makePlannedLeaveDay({
id: '1',
workspaceId: 'ws1',
childId: 'child1',
@@ -26,12 +27,10 @@ describe('usePlannedDays', () => {
date: '2026-06-15',
dayType: 'vacation',
isDubbeldag: false,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
},
}),
]
vi.mocked(api.getPlannedDays).mockResolvedValue(mockData as any)
vi.mocked(api.getPlannedDays).mockResolvedValue(mockData)
renderHook(() => usePlannedDays('child1'))
@@ -51,19 +50,17 @@ describe('usePlannedDays', () => {
})
it('adds planned day and refreshes', async () => {
const mockAdded = {
const mockAdded = makePlannedLeaveDay({
id: '2',
workspaceId: 'ws1',
childId: 'child1',
parentPersonalDataUuid: 'parent1',
date: '2026-06-16',
dayType: 'vacation' as const,
dayType: 'vacation',
isDubbeldag: false,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
}
})
vi.mocked(api.createPlannedDay).mockResolvedValue(mockAdded as any)
vi.mocked(api.createPlannedDay).mockResolvedValue(mockAdded)
vi.mocked(api.getPlannedDays).mockResolvedValue([mockAdded])
renderHook(() => usePlannedDays('child1'))
@@ -118,19 +115,17 @@ describe('usePlannedDays', () => {
})
it('updates planned day and refreshes', async () => {
const mockUpdated = {
const mockUpdated = makePlannedLeaveDay({
id: '1',
workspaceId: 'ws1',
childId: 'child1',
parentPersonalDataUuid: 'parent1',
date: '2026-06-16',
dayType: 'lagsta' as const,
dayType: 'lagsta',
isDubbeldag: false,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
}
})
vi.mocked(api.updatePlannedDay).mockResolvedValue(mockUpdated as any)
vi.mocked(api.updatePlannedDay).mockResolvedValue(mockUpdated)
vi.mocked(api.getPlannedDays).mockResolvedValue([mockUpdated])
const { result } = renderHook(() => usePlannedDays('child1'))
@@ -145,34 +140,30 @@ describe('usePlannedDays', () => {
it('updates data after successful add', async () => {
const initialData = [
{
makePlannedLeaveDay({
id: '1',
workspaceId: 'ws1',
childId: 'child1',
parentPersonalDataUuid: 'parent1',
date: '2026-06-15',
dayType: 'vacation' as const,
dayType: 'vacation',
isDubbeldag: false,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
},
}),
]
const newData = {
const newData = makePlannedLeaveDay({
id: '2',
workspaceId: 'ws1',
childId: 'child1',
parentPersonalDataUuid: 'parent1',
date: '2026-06-16',
dayType: 'vacation' as const,
dayType: 'vacation',
isDubbeldag: false,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
}
})
vi.mocked(api.getPlannedDays).mockResolvedValueOnce(initialData as any)
vi.mocked(api.createPlannedDay).mockResolvedValue(newData as any)
vi.mocked(api.getPlannedDays).mockResolvedValueOnce([...initialData, newData] as any)
vi.mocked(api.getPlannedDays).mockResolvedValueOnce(initialData)
vi.mocked(api.createPlannedDay).mockResolvedValue(newData)
vi.mocked(api.getPlannedDays).mockResolvedValueOnce([...initialData, newData])
const { result } = renderHook(() => usePlannedDays('child1'))
@@ -191,33 +182,29 @@ describe('usePlannedDays', () => {
it('updates data after successful remove', async () => {
const initialData = [
{
makePlannedLeaveDay({
id: '1',
workspaceId: 'ws1',
childId: 'child1',
parentPersonalDataUuid: 'parent1',
date: '2026-06-15',
dayType: 'vacation' as const,
dayType: 'vacation',
isDubbeldag: false,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
},
{
}),
makePlannedLeaveDay({
id: '2',
workspaceId: 'ws1',
childId: 'child1',
parentPersonalDataUuid: 'parent1',
date: '2026-06-16',
dayType: 'vacation' as const,
dayType: 'vacation',
isDubbeldag: false,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
},
}),
]
vi.mocked(api.getPlannedDays).mockResolvedValueOnce(initialData as any)
vi.mocked(api.getPlannedDays).mockResolvedValueOnce(initialData)
vi.mocked(api.deletePlannedDay).mockResolvedValue(undefined)
vi.mocked(api.getPlannedDays).mockResolvedValueOnce([initialData[0]] as any)
vi.mocked(api.getPlannedDays).mockResolvedValueOnce([initialData[0]!])
const { result } = renderHook(() => usePlannedDays('child1'))
@@ -236,20 +223,18 @@ describe('usePlannedDays', () => {
it('refreshes data explicitly', async () => {
const mockData = [
{
makePlannedLeaveDay({
id: '1',
workspaceId: 'ws1',
childId: 'child1',
parentPersonalDataUuid: 'parent1',
date: '2026-06-15',
dayType: 'vacation' as const,
dayType: 'vacation',
isDubbeldag: false,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
},
}),
]
vi.mocked(api.getPlannedDays).mockResolvedValue(mockData as any)
vi.mocked(api.getPlannedDays).mockResolvedValue(mockData)
const { result } = renderHook(() => usePlannedDays('child1'))
@@ -268,12 +253,12 @@ describe('usePlannedDays', () => {
})
it('sets loading state during fetch', async () => {
let resolveGetPlannedDays: (value: any) => void
const getPlannedDaysPromise = new Promise(resolve => {
let resolveGetPlannedDays: (value: Awaited<ReturnType<typeof api.getPlannedDays>>) => void
const getPlannedDaysPromise: ReturnType<typeof api.getPlannedDays> = new Promise(resolve => {
resolveGetPlannedDays = resolve
})
vi.mocked(api.getPlannedDays).mockReturnValue(getPlannedDaysPromise as any)
vi.mocked(api.getPlannedDays).mockReturnValue(getPlannedDaysPromise)
const { result } = renderHook(() => usePlannedDays('child1'))