Skill : Écrire et exécuter des tests pour Fidely
Ressources disponibles
templates/composable.test.ts— template complet pour tester un composable IndexedDBtemplates/component.test.ts— template complet pour tester un composant Vuereference/test-examples.md— exemples réels tirés des tests du projet
Règles obligatoires
Toujours lancer les tests après toute modification de code
pnpm vitest run # lancer tous les tests
pnpm vitest run app/composables/useDB.test.ts # lancer un fichier spécifique
Toute nouvelle feature doit être accompagnée de tests :
- Nouveau composable ou nouvelle fonction → fichier
.test.tsco-localisé dansapp/composables/ - Nouveau composant avec logique → fichier
.test.tsco-localisé dansapp/components/ - Nouvelle règle métier → test du cas nominal + cas limite
Quand lancer les tests :
- Après chaque modification d'un composable (
useDB.ts,useClient.ts) - Après chaque modification d'un composant (
CardItem.vue,CardForm.vue) - Après chaque ajout ou modification d'une fonction dans
utils/ - Avant de marquer une tâche comme ✅ dans
TASKS.md
Seuil d'acceptation :
- 0 test en échec — jamais valider une tâche avec des tests rouges
- Si un test échoue suite à un changement intentionnel, mettre à jour le test en même temps que le code
Setup (déjà configuré)
pnpm add -D @nuxt/test-utils vitest @vue/test-utils fake-indexeddb happy-dom
vitest.config.ts à la racine :
import { defineVitestConfig } from '@nuxt/test-utils/config'
export default defineVitestConfig({
test: {
environment: 'nuxt',
setupFiles: ['./app/setup.ts'] // ← app/setup.ts (pas setup.test.ts)
}
})
app/setup.ts :
import 'fake-indexeddb/auto'
Structure des fichiers de test
Les tests sont co-localisés avec les sources — même dossier que le fichier testé, suffixe .test.ts :
app/
├── composables/
│ ├── useDB.ts
│ ├── useDB.test.ts ← test co-localisé
│ ├── useClient.ts
│ └── useClient.test.ts
├── components/
│ ├── CardItem.vue
│ ├── CardItem.test.ts ← test co-localisé
│ ├── CardForm.vue
│ └── CardForm.test.ts
└── utils/
├── uuid.ts
└── uuid.test.ts
Ne jamais créer de dossier
tests/séparé — les tests vivent à côté du code qu'ils testent.
Isolation IndexedDB entre les tests
Obligatoire : créer une nouvelle instance IDBFactory dans beforeEach pour que chaque test parte d'une DB vide :
import { IDBFactory } from 'fake-indexeddb'
import { resetDB } from '~/composables/useDB'
beforeEach(() => {
globalThis.indexedDB = new IDBFactory()
resetDB() // reset le cache de connexion dans useDB.ts
})
Ne jamais appeler
indexedDB.deleteDatabase()— cette API est asynchrone et peut provoquer des timeouts si non attendue correctement.
Pattern pour tester les composables (useDB, useClient)
import { describe, it, expect, beforeEach } from 'vitest'
import { IDBFactory } from 'fake-indexeddb'
import { resetDB, useDB } from '~/composables/useDB'
beforeEach(() => {
globalThis.indexedDB = new IDBFactory()
resetDB()
})
describe('getAllCards', () => {
it('retourne un tableau vide par défaut', async () => {
const { getAllCards } = useDB()
const cards = await getAllCards()
expect(cards).toEqual([])
})
})
describe('updatePoints', () => {
it('ne descend jamais en dessous de 0', async () => {
const { addCard, updatePoints } = useDB()
const card = await addCard({ title: 'T', reward_points: 5, reward: 'R' })
const points = await updatePoints('client-1', card.id, -99)
expect(points).toBe(0)
})
})
Pattern pour tester les composants Vue
import { describe, it, expect } from 'vitest'
import { mountSuspended } from '@nuxt/test-utils/runtime'
import CardItem from '~/components/CardItem.vue'
import type { Card } from '~/composables/useDB'
const card: Card = {
id: 'card-1',
title: 'Carte café',
reward_points: 10,
reward: '1 café offert',
created_at: Date.now()
}
describe('CardItem — merchant mode', () => {
it('affiche le titre', async () => {
const wrapper = await mountSuspended(CardItem, {
props: { card, merchantMode: true }
})
expect(wrapper.text()).toContain('Carte café')
})
it('émet deleted avec le cardId au clic supprimer', async () => {
const wrapper = await mountSuspended(CardItem, {
props: { card, merchantMode: true }
})
await wrapper.find('[aria-label="Supprimer"]').trigger('click')
expect(wrapper.emitted('deleted')?.[0]).toEqual(['card-1'])
})
})
Structure describe / it standardisée
describe('NomDuSujet')
describe('nomDeLaFonction ou contexte')
it('fait X dans le cas normal')
it('gère le cas limite Y')
it('lance une erreur si Z')
Phrases it : toujours en anglais, décrivent le comportement attendu (pas l'implémentation).
Cas à toujours couvrir
Composables :
- Happy path (cas nominal)
- Valeurs limites (0 points, tableau vide, client inexistant)
- Comportement idempotent (appels multiples retournent le même résultat)
- Erreurs attendues (
rejects.toThrow(...))
Composants :
- Rendu avec les props minimales
- Chaque emit déclenché par l'action utilisateur correspondante
- États désactivés / conditionnels (
:disabled,v-if) - Props optionnelles non fournies (comportement par défaut)
Mocking des APIs Nuxt
// useToast et navigateTo sont auto-mockés par @nuxt/test-utils dans l'environment 'nuxt'
// Pas besoin de mock manuel dans la plupart des cas
// Si besoin de vérifier un appel toast ou navigateTo :
import { mockNuxtImport } from '@nuxt/test-utils/runtime'
const toastMock = vi.fn()
mockNuxtImport('useToast', () => () => ({ add: toastMock }))
Confirmation en fin de tâche
✅ testing-patterns appliqués
- Isolation : [beforeEach IDBFactory + resetDB en place]
- Couverture : [happy path + cas limite + erreurs attendues]
- Structure : [describe/it en anglais, comportemental]
- Résultat : [pnpm vitest run → 0 test en échec]