fix(ui): directives use client et correctifs qualité (lint, format, typecheck, tests)

This commit is contained in:
devcodetools committed 2026-09-22 08:39:04 +02:00
1 parent 9a62743e6f
commit 5f4596578f
11 files changed
+77 -38

No files matched your search

+1 -5
View File
@@ -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 {
+1 -5
View File
@@ -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">
+4 -2
View File
@@ -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()
+3 -1
View File
@@ -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(() => {
+16 -7
View File
@@ -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>
+4 -2
View File
@@ -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() },
}))
+3 -1
View File
@@ -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">
+20 -4
View File
@@ -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')
+7 -1
View File
@@ -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>
+11 -8
View File
@@ -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)
})
+7 -2
View File
@@ -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' }),