docs: fix plan's Task 7-9 test mocking approach

Task 3's review surfaced that the plan's original vi.importActual-based
mocking for Shop.test.tsx/Home.test.tsx/Book.test.tsx would execute the
real data-layer modules, which import the real Supabase client and
construct it at module-load time via createClient(VITE_SUPABASE_URL,
...) -- throwing "supabaseUrl is required" in a test env with no .env
file, before the test even runs.

Switched all three to mock ../lib/supabase directly instead, matching
the pattern Task 3's own products.test.ts already uses successfully.
Also fixed 3 occurrences of `err instanceof Error ? err.message :
String(err)` in the planned Shop.tsx/Home.tsx/Book.tsx error handling
-- fetchProducts/fetchServices throw the raw Postgrest error object
(not necessarily an Error instance), so this would have rendered
"[object Object]" instead of the real message in the mocked test
scenario.

Claude-Session: https://claude.ai/code/session_01NhxA4npAXAxmCDEJt6kxSH
This commit is contained in:
belviskhoremk
2026-08-12 20:09:10 +02:00
parent 79d05ca62d
commit 72fa3e0f90

View File

@@ -1031,32 +1031,37 @@ git commit -m "feat: upload product/service images to Supabase Storage during se
Replace `src/pages/Shop.test.tsx` entirely with:
```tsx
import { render, screen, waitFor } from '@testing-library/react'
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { describe, it, expect, vi, beforeEach } from 'vitest'
const fetchProductsMock = vi.fn()
const orderMock = vi.fn()
const selectMock = vi.fn(() => ({ order: orderMock }))
const fromMock = vi.fn<(...args: unknown[]) => { select: typeof selectMock }>(() => ({ select: selectMock }))
vi.mock('../data/products', async () => {
const actual = await vi.importActual('../data/products')
return { ...actual, fetchProducts: (...args: unknown[]) => fetchProductsMock(...args) }
})
vi.mock('../lib/supabase', () => ({
supabase: { from: (...args: unknown[]) => fromMock(...args) },
}))
vi.mock('../lib/resolveImageUrl', () => ({
resolveImageUrl: (path: string) => path,
}))
const PRODUCTS = [
{ id: 'edge-control', name: 'Gel Contour Soyeux', category: 'hair-care' as const, price: 12, image: '', description: '', inStock: true },
{ id: 'rat-tail-comb', name: 'Peigne à Tige en Acier', category: 'tools' as const, price: 6, image: '', description: '', inStock: true },
const PRODUCT_ROWS = [
{ id: 'edge-control', name: 'Gel Contour Soyeux', category: 'hair-care', price: 12, image: '', description: '', in_stock: true },
{ id: 'rat-tail-comb', name: 'Peigne à Tige en Acier', category: 'tools', price: 6, image: '', description: '', in_stock: true },
]
import Shop from './Shop'
describe('Shop page', () => {
beforeEach(() => {
fetchProductsMock.mockReset().mockResolvedValue(PRODUCTS)
orderMock.mockReset().mockResolvedValue({ data: PRODUCT_ROWS, error: null })
fromMock.mockClear()
})
it('renders all products by default', async () => {
render(<Shop />)
for (const p of PRODUCTS) {
for (const p of PRODUCT_ROWS) {
expect(await screen.findByText(p.name)).toBeInTheDocument()
}
})
@@ -1077,13 +1082,15 @@ describe('Shop page', () => {
})
it('shows an error message when the fetch fails', async () => {
fetchProductsMock.mockRejectedValue(new Error('network error'))
orderMock.mockResolvedValue({ data: null, error: { message: 'network error' } })
render(<Shop />)
expect(await screen.findByText(/network error/i)).toBeInTheDocument()
})
})
```
**Note on this mocking approach:** mocking `../lib/supabase` (rather than `../data/products` via `vi.importActual`) is deliberate — `vi.importActual` would execute the real `src/data/products.ts`, which imports the real `src/lib/supabase.ts`, which constructs a real Supabase client at module-load time via `createClient(import.meta.env.VITE_SUPABASE_URL, ...)`. In a test environment with no `.env` file (as this worktree has none), that throws `Error: supabaseUrl is required` before the test even runs. Mocking `../lib/supabase` directly avoids this entirely while still exercising the real `fetchProducts`/`getProductsByCategory` implementations — this is the same pattern Task 3's own `products.test.ts` already uses successfully.
- [ ] **Step 2: Run the test to verify it fails**
Run: `npx vitest run src/pages/Shop.test.tsx`
@@ -1124,7 +1131,7 @@ function Shop() {
try {
setProducts(await fetchProducts())
} catch (err) {
setError(err instanceof Error ? err.message : String(err))
setError(err && typeof err === 'object' && 'message' in err ? String((err as { message: unknown }).message) : String(err))
} finally {
setLoading(false)
}
@@ -1262,23 +1269,26 @@ import userEvent from '@testing-library/user-event'
import { MemoryRouter, Routes, Route } from 'react-router-dom'
import { describe, it, expect, vi, beforeEach } from 'vitest'
const fetchProductsMock = vi.fn()
const fetchServicesMock = vi.fn()
vi.mock('../data/products', async () => {
const actual = await vi.importActual('../data/products')
return { ...actual, fetchProducts: (...args: unknown[]) => fetchProductsMock(...args) }
})
vi.mock('../data/services', async () => {
const actual = await vi.importActual('../data/services')
return { ...actual, fetchServices: (...args: unknown[]) => fetchServicesMock(...args) }
const productsOrderMock = vi.fn()
const servicesOrderMock = vi.fn()
const fromMock = vi.fn<(table: string) => { select: () => { order: typeof productsOrderMock } }>((table: string) => {
if (table === 'products') return { select: () => ({ order: productsOrderMock }) }
if (table === 'services') return { select: () => ({ order: servicesOrderMock }) }
throw new Error(`unexpected table: ${table}`)
})
const PRODUCTS = [
{ id: 'edge-control', name: 'Gel Contour Soyeux', category: 'hair-care' as const, price: 12, image: '', description: '', inStock: true },
vi.mock('../lib/supabase', () => ({
supabase: { from: (table: string) => fromMock(table) },
}))
vi.mock('../lib/resolveImageUrl', () => ({
resolveImageUrl: (path: string) => path,
}))
const PRODUCT_ROWS = [
{ id: 'edge-control', name: 'Gel Contour Soyeux', category: 'hair-care', price: 12, image: '', description: '', in_stock: true },
]
const SERVICES = [
{ id: 'knotless', name: 'Tresses sans nœuds', durationMinutes: 240, priceFrom: 150, description: '', image: '' },
const SERVICE_ROWS = [
{ id: 'knotless', name: 'Tresses sans nœuds', duration_minutes: 240, price_from: 150, description: '', image: '' },
]
import Home from './Home'
@@ -1297,14 +1307,15 @@ function renderHome() {
describe('Home page', () => {
beforeEach(() => {
fetchProductsMock.mockReset().mockResolvedValue(PRODUCTS)
fetchServicesMock.mockReset().mockResolvedValue(SERVICES)
productsOrderMock.mockReset().mockResolvedValue({ data: PRODUCT_ROWS, error: null })
servicesOrderMock.mockReset().mockResolvedValue({ data: SERVICE_ROWS, error: null })
fromMock.mockClear()
})
it('renders the hero headline and all signature styles', async () => {
renderHome()
expect(screen.getByText(/vos cheveux, votre couronne/i)).toBeInTheDocument()
for (const service of SERVICES) {
for (const service of SERVICE_ROWS) {
expect(await screen.findByText(service.name)).toBeInTheDocument()
}
})
@@ -1318,7 +1329,7 @@ describe('Home page', () => {
it('navigates to /book with the chosen service preselected when a signature style is clicked', async () => {
renderHome()
const button = await screen.findByRole('button', { name: new RegExp(SERVICES[0].name) })
const button = await screen.findByRole('button', { name: new RegExp(SERVICE_ROWS[0].name) })
await userEvent.click(button)
expect(screen.getByLabelText(/choisissez une date/i)).toBeInTheDocument()
})
@@ -1330,6 +1341,8 @@ describe('Home page', () => {
})
```
**Note on this mocking approach and on rendering `<Book>` in this file:** as in Task 7, `../lib/supabase` is mocked directly rather than using `vi.importActual` on the data modules, for the same "supabaseUrl is required" reason. Separately: this test file also renders `<Book>` at `/book` (for the navigation test), and at this point in the plan `Book.tsx` has NOT yet been updated by Task 9 — it still has Task 2's breakage (`getSlotsForDate`/`getNextAvailableDate` no longer exported from `../data/availability`) and Task 4's breakage (`services` no longer exported from `../data/services`). This is safe for this specific test only because: (a) the navigation test passes a preselected `serviceId` via route state, which makes `useBooking` start at the `'date'` step directly — `Book.tsx`'s `'service'` step branch, which maps over the now-`undefined` `services` array, is never rendered; and (b) `getSlotsForDate` is only invoked when `dateInput` is truthy, and this test never types into the date field, so the broken import is never called as a function. Confirm this reasoning holds when you run the test — if `Book.tsx` throws anyway, it means the assumption above doesn't hold for the current state of the file, and you should stop and report rather than working around it, since that would indicate a real ordering problem in this plan.
- [ ] **Step 2: Run the test to verify it fails**
Run: `npx vitest run src/pages/Home.test.tsx`
@@ -1374,7 +1387,7 @@ function Home() {
setServices(servicesResult)
setProducts(productsResult)
} catch (err) {
setError(err instanceof Error ? err.message : String(err))
setError(err && typeof err === 'object' && 'message' in err ? String((err as { message: unknown }).message) : String(err))
}
}
void load()
@@ -1514,14 +1527,20 @@ import userEvent from '@testing-library/user-event'
import { MemoryRouter, Routes, Route } from 'react-router-dom'
import { describe, it, expect, vi, beforeEach } from 'vitest'
const fetchServicesMock = vi.fn()
const servicesOrderMock = vi.fn()
const fromMock = vi.fn<(table: string) => { select: () => { order: typeof servicesOrderMock } }>((table: string) => {
if (table === 'services') return { select: () => ({ order: servicesOrderMock }) }
throw new Error(`unexpected table: ${table}`)
})
const getSlotsForDateMock = vi.fn()
const findNextAvailableDateMock = vi.fn()
vi.mock('../data/services', async () => {
const actual = await vi.importActual('../data/services')
return { ...actual, fetchServices: (...args: unknown[]) => fetchServicesMock(...args) }
})
vi.mock('../lib/supabase', () => ({
supabase: { from: (table: string) => fromMock(table) },
}))
vi.mock('../lib/resolveImageUrl', () => ({
resolveImageUrl: (path: string) => path,
}))
vi.mock('../hooks/useAvailability', () => ({
default: () => ({
loading: false,
@@ -1531,9 +1550,9 @@ vi.mock('../hooks/useAvailability', () => ({
}),
}))
const SERVICES = [
{ id: 'knotless', name: 'Tresses sans nœuds', durationMinutes: 240, priceFrom: 150, description: '', image: '' },
{ id: 'cornrows', name: 'Nattes Collées', durationMinutes: 120, priceFrom: 90, description: '', image: '' },
const SERVICE_ROWS = [
{ id: 'knotless', name: 'Tresses sans nœuds', duration_minutes: 240, price_from: 150, description: '', image: '' },
{ id: 'cornrows', name: 'Nattes Collées', duration_minutes: 120, price_from: 90, description: '', image: '' },
]
import Book from './Book'
@@ -1550,7 +1569,8 @@ function renderBook(initialEntries: { pathname: string; state?: unknown }[]) {
describe('Book page', () => {
beforeEach(() => {
fetchServicesMock.mockReset().mockResolvedValue(SERVICES)
servicesOrderMock.mockReset().mockResolvedValue({ data: SERVICE_ROWS, error: null })
fromMock.mockClear()
getSlotsForDateMock.mockReset().mockResolvedValue([
{ date: '2026-08-11', time: '9:00 AM', available: true },
{ date: '2026-08-11', time: '11:00 AM', available: true },
@@ -1625,7 +1645,7 @@ function Book() {
try {
setServices(await fetchServices())
} catch (err) {
setServicesError(err instanceof Error ? err.message : String(err))
setServicesError(err && typeof err === 'object' && 'message' in err ? String((err as { message: unknown }).message) : String(err))
}
}
void load()