fix(ui): directives use client et correctifs qualité (lint, format, typecheck, tests)
This commit is contained in:
1 parent
9a62743e6f
commit
5f4596578f
11 files changed
+77
-38
No files matched your search
@@ -7,11 +7,7 @@ import { ApiClientError, cardsApi } from '@/lib/api-client'
|
||||
|
||||
export const metadata: Metadata = { title: 'Modifier la carte' }
|
||||
|
||||
export default async function EditCardPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ id: string }>
|
||||
}) {
|
||||
export default async function EditCardPage({ params }: { params: Promise<{ id: string }> }) {
|
||||
const { id } = await params
|
||||
let card: Card | null = null
|
||||
try {
|
||||
|
||||
@@ -5,11 +5,7 @@ export const metadata: Metadata = {
|
||||
title: 'Carte',
|
||||
}
|
||||
|
||||
export default async function CardDetailPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ id: string }>
|
||||
}) {
|
||||
export default async function CardDetailPage({ params }: { params: Promise<{ id: string }> }) {
|
||||
const { id } = await params
|
||||
return (
|
||||
<main className="mx-auto max-w-3xl px-4 py-8">
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { render, screen, within } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { beforeEach, expect, it, vi } from 'vitest'
|
||||
import { CardDeleteButton } from './card-delete'
|
||||
|
||||
vi.mock('@/lib/api-client', () => {
|
||||
@@ -52,7 +52,9 @@ it('supprime après confirmation puis retourne à la liste (F-07)', async () =>
|
||||
vi.mocked(cardsApi.remove).mockResolvedValue(undefined as never)
|
||||
render(<CardDeleteButton cardId="c1" question="question-x" />)
|
||||
await userEvent.click(screen.getByRole('button', { name: 'Supprimer' }))
|
||||
await userEvent.click(within(screen.getByRole('dialog')).getByRole('button', { name: 'Supprimer' }))
|
||||
await userEvent.click(
|
||||
within(screen.getByRole('dialog')).getByRole('button', { name: 'Supprimer' }),
|
||||
)
|
||||
expect(cardsApi.remove).toHaveBeenCalledWith('c1')
|
||||
expect(routerMock.push).toHaveBeenCalledWith('/cards')
|
||||
expect(routerMock.refresh).toHaveBeenCalled()
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { beforeEach, expect, it, vi } from 'vitest'
|
||||
import { CardDetail } from './card-detail'
|
||||
|
||||
vi.mock('@/lib/api-client', () => {
|
||||
@@ -27,6 +27,8 @@ const CARD = {
|
||||
response: 'Un snapshot du projet',
|
||||
theme: 'git',
|
||||
tags: ['snapshot'],
|
||||
createdAt: new Date('2026-01-01T08:00:00Z'),
|
||||
updatedAt: new Date('2026-01-01T08:00:00Z'),
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
'use client'
|
||||
|
||||
import Link from 'next/link'
|
||||
import { useEffect, useState } from 'react'
|
||||
import type { Card } from '@/domain/card'
|
||||
@@ -25,7 +27,11 @@ export function CardDetail({ cardId }: { cardId: string }) {
|
||||
}, [cardId])
|
||||
|
||||
if (error) {
|
||||
return <p role="alert" className="text-red-600">{error}</p>
|
||||
return (
|
||||
<p role="alert" className="text-red-600">
|
||||
{error}
|
||||
</p>
|
||||
)
|
||||
}
|
||||
if (card === null) {
|
||||
return <p aria-busy="true">Chargement…</p>
|
||||
@@ -46,21 +52,24 @@ export function CardDetail({ cardId }: { cardId: string }) {
|
||||
{card.tags.length > 0 && (
|
||||
<ul className="flex flex-wrap gap-1.5">
|
||||
{card.tags.map((tag) => (
|
||||
<li
|
||||
key={tag}
|
||||
className="rounded-full bg-slate-100 px-2 py-0.5 text-xs text-slate-600"
|
||||
>
|
||||
<li key={tag} className="rounded-full bg-slate-100 px-2 py-0.5 text-xs text-slate-600">
|
||||
#{tag}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
<div className="flex items-center gap-3">
|
||||
<Link href={`/cards/${card.id}/edit`} className="text-sm font-medium text-slate-700 underline-offset-4 hover:underline">
|
||||
<Link
|
||||
href={`/cards/${card.id}/edit`}
|
||||
className="text-sm font-medium text-slate-700 underline-offset-4 hover:underline"
|
||||
>
|
||||
Modifier
|
||||
</Link>
|
||||
<span className="text-slate-300">·</span>
|
||||
<Link href="/cards" className="text-sm font-medium text-slate-700 underline-offset-4 hover:underline">
|
||||
<Link
|
||||
href="/cards"
|
||||
className="text-sm font-medium text-slate-700 underline-offset-4 hover:underline"
|
||||
>
|
||||
Retour à la liste
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { beforeEach, expect, it, vi } from 'vitest'
|
||||
import { CardForm } from './card-form'
|
||||
|
||||
vi.mock('@/lib/api-client', () => ({
|
||||
ApiClientError: class extends Error { status = 0 },
|
||||
ApiClientError: class extends Error {
|
||||
status = 0
|
||||
},
|
||||
cardsApi: { create: vi.fn(), update: vi.fn() },
|
||||
}))
|
||||
|
||||
|
||||
@@ -43,7 +43,9 @@ export function CardForm({ mode, cardId, initial }: CardFormProps) {
|
||||
return (
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
{error && (
|
||||
<p role="alert" className="text-red-600">{error}</p>
|
||||
<p role="alert" className="text-red-600">
|
||||
{error}
|
||||
</p>
|
||||
)}
|
||||
<div>
|
||||
<label htmlFor="question" className="block text-sm font-medium text-slate-700">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { render, screen, within } from '@testing-library/react'
|
||||
import { describe, expect, it, vi, beforeEach } from 'vitest'
|
||||
import { expect, it, vi, beforeEach } from 'vitest'
|
||||
import { CardList } from './card-list'
|
||||
|
||||
vi.mock('@/lib/api-client', () => {
|
||||
@@ -22,8 +22,24 @@ vi.mock('@/lib/api-client', () => {
|
||||
import { cardsApi, ApiClientError } from '@/lib/api-client'
|
||||
|
||||
const CARDS = [
|
||||
{ id: '1', question: 'Cest quoi un commit ?', response: 'Un snapshot du projet', theme: 'git', tags: [], createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() },
|
||||
{ id: '2', question: 'Cest quoi une closure ?', response: 'Une fonction + son scope', theme: 'javascript', tags: [], createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() },
|
||||
{
|
||||
id: '1',
|
||||
question: 'Cest quoi un commit ?',
|
||||
response: 'Un snapshot du projet',
|
||||
theme: 'git',
|
||||
tags: [],
|
||||
createdAt: new Date('2026-01-01T08:00:00Z'),
|
||||
updatedAt: new Date('2026-01-01T08:00:00Z'),
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
question: 'Cest quoi une closure ?',
|
||||
response: 'Une fonction + son scope',
|
||||
theme: 'javascript',
|
||||
tags: [],
|
||||
createdAt: new Date('2026-01-01T08:00:00Z'),
|
||||
updatedAt: new Date('2026-01-01T08:00:00Z'),
|
||||
},
|
||||
]
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -45,7 +61,7 @@ it('affiche un lien vers le détail de chaque carte', async () => {
|
||||
expect(link).toHaveAttribute('href', '/cards/1')
|
||||
})
|
||||
|
||||
it("affiche le thème de chaque carte", async () => {
|
||||
it('affiche le thème de chaque carte', async () => {
|
||||
vi.mocked(cardsApi.list).mockResolvedValue(CARDS)
|
||||
render(<CardList />)
|
||||
const item = (await screen.findByText('Cest quoi un commit ?')).closest('li')
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
'use client'
|
||||
|
||||
import Link from 'next/link'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { ApiClientError, cardsApi } from '@/lib/api-client'
|
||||
@@ -39,7 +41,11 @@ export function CardList() {
|
||||
}, [])
|
||||
|
||||
if (error) {
|
||||
return <p role="alert" className="text-red-600">{error}</p>
|
||||
return (
|
||||
<p role="alert" className="text-red-600">
|
||||
{error}
|
||||
</p>
|
||||
)
|
||||
}
|
||||
if (cards === null) {
|
||||
return <p aria-busy="true">Chargement…</p>
|
||||
|
||||
@@ -22,10 +22,7 @@ beforeEach(() => {
|
||||
})
|
||||
|
||||
function expectRequest(method: string, path: string) {
|
||||
expect(fetchMock).toHaveBeenCalledWith(
|
||||
path,
|
||||
expect.objectContaining({ method }),
|
||||
)
|
||||
expect(fetchMock).toHaveBeenCalledWith(path, expect.objectContaining({ method }))
|
||||
}
|
||||
|
||||
describe('cardsApi', () => {
|
||||
@@ -43,8 +40,10 @@ describe('cardsApi', () => {
|
||||
|
||||
it('create() envoie POST avec un corps JSON', async () => {
|
||||
fetchMock.mockResolvedValue(jsonResponse(CARD, 201))
|
||||
await expect(cardsApi.create({ question: 'Q ?', response: 'R', theme: 'git' } as never)).resolves.toEqual(CARD)
|
||||
const [_, init] = fetchMock.mock.calls[0]
|
||||
await expect(
|
||||
cardsApi.create({ question: 'Q ?', response: 'R', theme: 'git' } as never),
|
||||
).resolves.toEqual(CARD)
|
||||
const [, init] = fetchMock.mock.calls[0]
|
||||
expect(init.method).toBe('POST')
|
||||
expect(JSON.parse(init.body)).toEqual({ question: 'Q ?', response: 'R', theme: 'git' })
|
||||
})
|
||||
@@ -76,7 +75,9 @@ describe('cardsApi', () => {
|
||||
})
|
||||
|
||||
it('lève ApiClientError avec le message de l API sur une erreur 404', async () => {
|
||||
fetchMock.mockResolvedValue(jsonResponse({ error: 'NotFoundError', message: 'Carte inconnue' }, 404))
|
||||
fetchMock.mockResolvedValue(
|
||||
jsonResponse({ error: 'NotFoundError', message: 'Carte inconnue' }, 404),
|
||||
)
|
||||
await expect(cardsApi.get('nope')).rejects.toMatchObject({
|
||||
name: 'NotFoundError',
|
||||
message: 'Carte inconnue',
|
||||
@@ -85,7 +86,9 @@ describe('cardsApi', () => {
|
||||
})
|
||||
|
||||
it('lève ApiClientError sur une réponse invalide', async () => {
|
||||
fetchMock.mockResolvedValue(jsonResponse({ error: 'ValidationError', message: 'Payload invalide' }, 400))
|
||||
fetchMock.mockResolvedValue(
|
||||
jsonResponse({ error: 'ValidationError', message: 'Payload invalide' }, 400),
|
||||
)
|
||||
await expect(cardsApi.create({} as never)).rejects.toBeInstanceOf(ApiClientError)
|
||||
})
|
||||
|
||||
|
||||
@@ -29,7 +29,11 @@ async function request<T>(path: string, init?: RequestInit): Promise<T> {
|
||||
const body: T | ApiErrorPayload = await res.json().catch(() => ({}))
|
||||
if (!res.ok) {
|
||||
const payload = body as ApiErrorPayload
|
||||
throw new ApiClientError(res.status, payload.error ?? 'HttpError', payload.message ?? `Erreur ${res.status}`)
|
||||
throw new ApiClientError(
|
||||
res.status,
|
||||
payload.error ?? 'HttpError',
|
||||
payload.message ?? `Erreur ${res.status}`,
|
||||
)
|
||||
}
|
||||
return body as T
|
||||
}
|
||||
@@ -37,7 +41,8 @@ async function request<T>(path: string, init?: RequestInit): Promise<T> {
|
||||
export const cardsApi = {
|
||||
list: () => request<Card[]>('/api/cards'),
|
||||
get: (id: string) => request<Card>(`/api/cards/${id}`),
|
||||
create: (input: CardInput) => request<Card>('/api/cards', { method: 'POST', body: JSON.stringify(input) }),
|
||||
create: (input: CardInput) =>
|
||||
request<Card>('/api/cards', { method: 'POST', body: JSON.stringify(input) }),
|
||||
update: (id: string, input: CardInput) =>
|
||||
request<Card>(`/api/cards/${id}`, { method: 'PUT', body: JSON.stringify(input) }),
|
||||
remove: (id: string) => request<void>(`/api/cards/${id}`, { method: 'DELETE' }),
|
||||
|
||||
Reference in new issue
Block a user