# Testing Patterns

> Patterns de tests Fidely : Vitest + @nuxt/test-utils + fake-indexeddb + happy-dom. Déclencher quand : écriture de test, ajout composable (useDB, useClient), nouveau composant Vue, modification d'une fonction dans utils/, isolation IndexedDB, mock Nuxt (useToast, navigateTo). Mots-clés : describe, it, expect, beforeEach, IDBFactory, resetDB, mountSuspended, mockNuxtImport, fake-indexeddb, useDB.test.ts, CardItem.test.ts, CardForm.test.ts, updatePoints, consumeReward, addCard, getOrCreateClientId.

- Skill: `chawkitariq/testing-patterns` (Agent Skill, multi-file: 4 files)
- Install (CLI): `npx skillmds@latest add chawkitariq/testing-patterns`
- Raw SKILL.md: https://api.skillmd.com/api/skills/chawkitariq/testing-patterns/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Web & Frontend
- Author: chawkitariq (https://skillmd.com/u/chawkitariq)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/chawkitariq/testing-patterns

---


# Skill : Écrire et exécuter des tests pour Fidely

## Ressources disponibles

- [`templates/composable.test.ts`](templates/composable.test.ts) — template complet pour tester un composable IndexedDB
- [`templates/component.test.ts`](templates/component.test.ts) — template complet pour tester un composant Vue
- [`reference/test-examples.md`](reference/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

```bash
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.ts` co-localisé dans `app/composables/`
- Nouveau composant avec logique → fichier `.test.ts` co-localisé dans `app/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é)

```bash
pnpm add -D @nuxt/test-utils vitest @vue/test-utils fake-indexeddb happy-dom
```

`vitest.config.ts` à la racine :
```ts
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` :
```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 :

```ts
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)

```ts
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

```ts
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

```ts
// 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]

