feat: wire Shop page to live Supabase product data
This commit is contained in:
@@ -1,16 +1,27 @@
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { describe, it, expect, vi } from 'vitest'
|
||||
import App from './App'
|
||||
|
||||
vi.mock('./lib/supabase', () => ({
|
||||
supabase: {
|
||||
from: () => ({
|
||||
select: () => ({
|
||||
order: () => Promise.resolve({ data: [], error: null }),
|
||||
eq: () => ({ single: () => Promise.resolve({ data: null, error: null }) }),
|
||||
}),
|
||||
}),
|
||||
},
|
||||
}))
|
||||
|
||||
describe('App routing', () => {
|
||||
it('renders the Home page at /', async () => {
|
||||
render(<App initialEntries={['/']} />)
|
||||
expect(await screen.findByText(/vos cheveux, votre couronne/i)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders the Shop page at /shop', () => {
|
||||
it('renders the Shop page at /shop', async () => {
|
||||
render(<App initialEntries={['/shop']} />)
|
||||
expect(screen.getByRole('heading', { name: /boutique/i })).toBeInTheDocument()
|
||||
expect(await screen.findByRole('heading', { name: /boutique/i })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders the Book page at /book', () => {
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
padding: var(--space-xl) var(--space-lg);
|
||||
}
|
||||
|
||||
.error {
|
||||
color: #a33;
|
||||
}
|
||||
|
||||
.filters {
|
||||
display: flex;
|
||||
gap: var(--space-sm);
|
||||
|
||||
@@ -1,19 +1,41 @@
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||
|
||||
const orderMock = vi.fn()
|
||||
const selectMock = vi.fn(() => ({ order: orderMock }))
|
||||
const fromMock = vi.fn<(...args: unknown[]) => { select: typeof selectMock }>(() => ({ select: selectMock }))
|
||||
|
||||
vi.mock('../lib/supabase', () => ({
|
||||
supabase: { from: (...args: unknown[]) => fromMock(...args) },
|
||||
}))
|
||||
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 },
|
||||
{ id: 'rat-tail-comb', name: 'Peigne à Tige en Acier', category: 'tools', price: 6, image: '', description: '', in_stock: true },
|
||||
]
|
||||
|
||||
import Shop from './Shop'
|
||||
import { products } from '../data/products'
|
||||
|
||||
describe('Shop page', () => {
|
||||
it('renders all products by default', () => {
|
||||
beforeEach(() => {
|
||||
orderMock.mockReset().mockResolvedValue({ data: PRODUCT_ROWS, error: null })
|
||||
fromMock.mockClear()
|
||||
})
|
||||
|
||||
it('renders all products by default', async () => {
|
||||
render(<Shop />)
|
||||
for (const p of products) {
|
||||
expect(screen.getByText(p.name)).toBeInTheDocument()
|
||||
for (const p of PRODUCT_ROWS) {
|
||||
expect(await screen.findByText(p.name)).toBeInTheDocument()
|
||||
}
|
||||
})
|
||||
|
||||
it('filters products by category', async () => {
|
||||
render(<Shop />)
|
||||
await screen.findByText('Gel Contour Soyeux')
|
||||
await userEvent.click(screen.getByRole('button', { name: /^outils$/i }))
|
||||
expect(screen.getByText('Peigne à Tige en Acier')).toBeInTheDocument()
|
||||
expect(screen.queryByText('Gel Contour Soyeux')).not.toBeInTheDocument()
|
||||
@@ -21,8 +43,14 @@ describe('Shop page', () => {
|
||||
|
||||
it('opens the inquiry modal when a product is inquired about', async () => {
|
||||
render(<Shop />)
|
||||
const buttons = screen.getAllByRole('button', { name: /poser une question/i })
|
||||
const buttons = await screen.findAllByRole('button', { name: /poser une question/i })
|
||||
await userEvent.click(buttons[0])
|
||||
expect(screen.getByRole('button', { name: /envoyer la demande/i })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('shows an error message when the fetch fails', async () => {
|
||||
orderMock.mockResolvedValue({ data: null, error: { message: 'network error' } })
|
||||
render(<Shop />)
|
||||
expect(await screen.findByText(/network error/i)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { useState } from 'react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { motion, useReducedMotion } from 'framer-motion'
|
||||
import ProductCard from '../components/ProductCard'
|
||||
import InquiryModal from '../components/InquiryModal'
|
||||
import { getProductsByCategory, type Product } from '../data/products'
|
||||
import { fetchProducts, getProductsByCategory, type Product } from '../data/products'
|
||||
import styles from './Shop.module.css'
|
||||
|
||||
const CATEGORIES: (Product['category'] | 'all')[] = ['all', 'hair-care', 'extensions', 'tools', 'accessories']
|
||||
@@ -16,13 +16,31 @@ const CATEGORY_LABELS: Record<Product['category'] | 'all', string> = {
|
||||
}
|
||||
|
||||
function Shop() {
|
||||
const [products, setProducts] = useState<Product[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState('')
|
||||
const [category, setCategory] = useState<Product['category'] | 'all'>('all')
|
||||
const [inquiring, setInquiring] = useState<Product | null>(null)
|
||||
const prefersReducedMotion = useReducedMotion()
|
||||
|
||||
useEffect(() => {
|
||||
async function load() {
|
||||
try {
|
||||
setProducts(await fetchProducts())
|
||||
} catch (err) {
|
||||
setError(err && typeof err === 'object' && 'message' in err ? String((err as { message: unknown }).message) : String(err))
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
void load()
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className={styles.page}>
|
||||
<h1>Boutique</h1>
|
||||
{loading && <p>Chargement…</p>}
|
||||
{error && <p className={styles.error}>{error}</p>}
|
||||
<div className={styles.filters}>
|
||||
{CATEGORIES.map((c) => (
|
||||
<button
|
||||
@@ -43,7 +61,7 @@ function Shop() {
|
||||
))}
|
||||
</div>
|
||||
<div className={styles.grid}>
|
||||
{getProductsByCategory(category).map((product) => (
|
||||
{getProductsByCategory(products, category).map((product) => (
|
||||
<ProductCard key={product.id} product={product} onInquire={setInquiring} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user