Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
152 changes: 152 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
name: Tests & Coverage

on:
pull_request:
branches: [main, dev]
push:
branches: [main, dev, "feat/**", "fix/**", "refactor/**"]

jobs:
unit-tests:
name: Unit Tests
runs-on: ubuntu-latest
permissions:
contents: read

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "20"

- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 10
run_install: false

- name: Cache pnpm store
uses: actions/cache@v4
with:
path: ~/.pnpm-store
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-

- name: Install dependencies
run: pnpm install --frozen-lockfile

- name: Run unit tests
run: pnpm vitest run
env:
CI: true

e2e-tests:
name: E2E Tests
runs-on: ubuntu-latest
permissions:
contents: read

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "20"

- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 10
run_install: false

- name: Cache pnpm store
uses: actions/cache@v4
with:
path: ~/.pnpm-store
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-

- name: Install dependencies
run: pnpm install --frozen-lockfile

- name: Install Playwright browsers
run: npx playwright install --with-deps chromium

- name: Build application
run: pnpm build
env:
CI: true

- name: Run E2E tests
run: pnpm exec playwright test
env:
CI: true

- name: Upload Playwright report
if: always()
uses: actions/upload-artifact@v4
with:
name: playwright-report
path: playwright-report/
retention-days: 14

coverage-report:
name: Coverage Report
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "20"

- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 10
run_install: false

- name: Cache pnpm store
uses: actions/cache@v4
with:
path: ~/.pnpm-store
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-

- name: Install dependencies
run: pnpm install --frozen-lockfile

- name: Run unit tests with coverage
run: pnpm test:ci
env:
CI: true

- name: Coverage report on PR
if: always()
uses: davelosert/vitest-coverage-report-action@v2
with:
json-summary-path: coverage/coverage-summary.json
json-final-path: coverage/coverage-final.json

- name: Upload coverage artifacts
if: always()
uses: actions/upload-artifact@v4
with:
name: coverage-report
path: coverage/
retention-days: 14
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,17 @@ Le projet Edukai vise à simplifier la création de matériel de révision perso

L'application est lancée sur le port 3000 de votre machine (`http://localhost:3000`).<br>

## 🧪 Tests

```bash
pnpm test # Unit tests (watch mode)
pnpm test:ci # Unit tests + coverage
pnpm test:e2e # Playwright E2E tests
pnpm test:e2e:headed # E2E with visible browser
```

See [docs/TESTING.md](docs/TESTING.md) for details on test structure and patterns.

## 👥 Équipe

- **Tristan Hourtoulle** - Développeur Frontend
Expand Down
4 changes: 2 additions & 2 deletions components/auth/signin-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ export function SigninForm({
render={({ field }) => (
<FormItem>
<FormLabel className="text-sm font-medium text-gray-700 flex items-center gap-2">
<Mail className="w-4 h-4" />
<Mail className="w-4 h-4" aria-hidden="true" />
Adresse email
</FormLabel>
<FormControl>
Expand All @@ -124,7 +124,7 @@ export function SigninForm({
render={({ field }) => (
<FormItem>
<FormLabel className="text-sm font-medium text-gray-700 flex items-center gap-2">
<Lock className="w-4 h-4" />
<Lock className="w-4 h-4" aria-hidden="true" />
Mot de passe
</FormLabel>
<FormControl>
Expand Down
10 changes: 5 additions & 5 deletions components/auth/signup-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ export function SignupForm({ onSuccess, onError }: SignupFormProps) {
htmlFor="firstName"
className="text-sm font-medium text-gray-700 flex items-center gap-2"
>
<User className="w-4 h-4" />
<User className="w-4 h-4" aria-hidden="true" />
Prénom
</label>
<div className="relative">
Expand Down Expand Up @@ -126,7 +126,7 @@ export function SignupForm({ onSuccess, onError }: SignupFormProps) {
htmlFor="lastName"
className="text-sm font-medium text-gray-700 flex items-center gap-2"
>
<User className="w-4 h-4" />
<User className="w-4 h-4" aria-hidden="true" />
Nom
</label>
<div className="relative">
Expand Down Expand Up @@ -158,7 +158,7 @@ export function SignupForm({ onSuccess, onError }: SignupFormProps) {
htmlFor="email"
className="text-sm font-medium text-gray-700 flex items-center gap-2"
>
<Mail className="w-4 h-4" />
<Mail className="w-4 h-4" aria-hidden="true" />
Adresse email
</label>
<div className="relative">
Expand Down Expand Up @@ -189,7 +189,7 @@ export function SignupForm({ onSuccess, onError }: SignupFormProps) {
htmlFor="password"
className="text-sm font-medium text-gray-700 flex items-center gap-2"
>
<Lock className="w-4 h-4" />
<Lock className="w-4 h-4" aria-hidden="true" />
Mot de passe
</label>
<div className="relative">
Expand Down Expand Up @@ -270,7 +270,7 @@ export function SignupForm({ onSuccess, onError }: SignupFormProps) {
htmlFor="confirmPassword"
className="text-sm font-medium text-gray-700 flex items-center gap-2"
>
<Lock className="w-4 h-4" />
<Lock className="w-4 h-4" aria-hidden="true" />
Confirmer le mot de passe
</label>
<div className="relative">
Expand Down
111 changes: 111 additions & 0 deletions docs/TESTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
# Testing

## Architecture

```
tests/
├── setup.ts # Global mocks (router, EventSource, ResizeObserver)
├── factories/ # Test data builders (buildUser, buildQuiz, ...)
├── mocks/ # Shared mock helpers (session, router, axios)
├── unit/
│ ├── lib/ # Pure functions (no React)
│ ├── hooks/ # Hooks via renderHook()
│ └── components/ # Components via render() + userEvent
└── e2e/ # Playwright browser tests
```

## Unit Tests (Vitest)

### Pure functions

Test direct input/output, no React needed.

```ts
import { shuffleQuiz } from "@/lib/utils/quiz";

it("preserves the correct answer after shuffle", () => {
const [shuffled] = shuffleQuiz([question]);
expect(shuffled.answer).toContain("Paris");
});
```

Reference: `tests/unit/lib/quiz.test.ts`

### Hooks

Use `renderHook` from `@testing-library/react`.

```ts
import { renderHook, act } from "@testing-library/react";

const { result } = renderHook(() => useQuizPlayer({ quizData }));

act(() => {
result.current.setSelectedAnswer("B) 4");
});

expect(result.current.score).toBe(1);
```

Reference: `tests/unit/hooks/useQuizPlayer.test.ts`

### Components (RTL)

Use `render`, `screen`, `userEvent.setup()`, and `waitFor` for async updates.

```tsx
import { render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";

const user = userEvent.setup();

render(<SigninForm onSuccess={onSuccess} />);

await user.type(screen.getByPlaceholderText("ton.email@exemple.com"), "test@example.com");
await user.click(screen.getByRole("button", { name: /se connecter/i }));

await waitFor(() => {
expect(onSuccess).toHaveBeenCalled();
});
```

Mock external hooks at the top of the file:

```ts
vi.mock("@/hooks/useSession", () => ({
useSession: () => ({ login: mockLogin, user: null, loading: false }),
}));
```

Reference: `tests/unit/components/auth/SigninForm.test.tsx`

## E2E Tests (Playwright)

Browser tests against the running app. Use accessible selectors (`getByRole`, `getByPlaceholder`).

```ts
import { test, expect } from "@playwright/test";

test("should redirect unauthenticated users", async ({ page }) => {
await page.goto("/library");
await expect(page).toHaveURL(/auth/);
});
```

Reference: `tests/e2e/auth.spec.ts`

## Commands

| Command | Description |
|---|---|
| `pnpm test` | Run unit tests (watch mode) |
| `pnpm test:ci` | Run unit tests once + coverage |
| `pnpm test:e2e` | Run Playwright E2E tests |
| `pnpm test:e2e:headed` | Run E2E tests with visible browser |

## CI/CD

Configured in `.github/workflows/test.yml`:

- **Every push**: unit tests + E2E tests
- **PRs only**: coverage report posted as comment
15 changes: 15 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"reset": "pkill -f 'next dev' 2>/dev/null || true && rm -rf .next && pnpm dev",
"test": "vitest",
"test:watch": "vitest --watch",
"test:ci": "vitest run --coverage",
"test:coverage": "vitest --coverage",
"test:ui": "vitest --ui",
"test:e2e": "playwright test",
Expand Down Expand Up @@ -73,13 +74,15 @@
"@tailwindcss/typography": "^0.5.19",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.0",
"@testing-library/user-event": "^14.6.1",
"@types/node": "^20.17.6",
"@types/react": "^18",
"@types/react-dom": "^18",
"@typescript-eslint/eslint-plugin": "^8.34.1",
"@typescript-eslint/parser": "^8.34.1",
"@vitejs/plugin-react": "^5.1.1",
"@vitest/coverage-v8": "4.0.15",
"@vitest/ui": "4.0.15",
"eslint": "^8",
"eslint-config-next": "14.2.16",
"eslint-config-prettier": "^10.1.5",
Expand Down
Loading