Tests e2e avec Playwright sur un site Astro : mon setup CI/CD complet


TL;DR : Playwright + Astro + GitLab CI = tests e2e automatis�s qui tournent � chaque push. Mais il y a des pi�ges - notamment les race conditions au d�marrage du serveur. Voici comment j’ai tout mis en place, et ce que j’ai d� corriger.

Pourquoi des tests e2e sur un site statique ?

Mon site sevan.zone est construit avec Astro 4 et d�ploy� en statique sur nginx via Unraid. On pourrait se dire : “c’est statique, pas besoin de tests”. Sauf que :

  • Le build Astro peut casser du jour au lendemain (d�pendances, configs, composants)
  • Les liens de navigation peuvent �tre cass�s silencieusement
  • Les pages blog, projets, apps ont des logiques dynamiques (filtres, FAQ, navigation)
  • Le deploy se fait automatiquement - mieux vaut tester avant de pousser en prod

J’ai donc mis en place une suite de 25 tests e2e avec Playwright qui v�rifient l’int�gralit� du site � chaque push sur master.

L’architecture du pipeline

Voici ce que mon .gitlab-ci.yml fait � chaque commit :

test ? security ? build ? e2e ? deploy
StageJobCe qu’il fait
testunit-testsVitest sur les composants
securitynpm-auditV�rifie les vuln�rabilit�s npm
buildbuildastro build ? produit dist/
e2ee2e-testsPlaywright sur le build??
deploydeployrsync dist/ vers nginx (Unraid)

Le point cl� : les tests e2e tournent sur le build?? (dist/), pas sur le serveur dev. �a reproduit exactement ce qui sera d�ploy�.

La config Playwright

Voici mon playwright.config.ts :

import { defineConfig, devices } from '@playwright/test';

const isCI = !!process.env.CI;

export default defineConfig({
  testDir: './src/tests/e2e',
  outputDir: './test-results',
  retries: isCI ? 1 : 0,
  use: {
    baseURL: 'http://localhost:4321',
    trace: 'on-first-retry',
  },
  projects: [
    {
      name: 'chromium',
      use: { ...devices['Desktop Chrome'] },
    },
  ],
  webServer: isCI
    ? {
        command: 'python3 -m http.server 4321 --directory dist',
        url: 'http://localhost:4321',
        timeout: 30000,
      }
    : {
        command: 'npm run dev',
        url: 'http://localhost:4321',
        reuseExistingServer: true,
        timeout: 60000,
      },
});

Les choix importants :

  • retries: 1 en CI - si un test est flaky (race condition), il est relanc� une fois
  • webServer diff�rent selon l’environnement - en CI on sert le dist/ avec python, en local on lance le serveur dev Astro
  • timeout: 30000 pour le webServer en CI - augment� apr�s un bug
  • trace: 'on-first-retry' - ne capture les traces que si le test �choue (�conomie d’espace)

Le pi�ge des race conditions

Le probl�me que j’ai rencontr� : le premier test �chouait syst�matiquement avec un timeout de 30 secondes sur page.goto('/blog/'). Les 24 autres tests passaient parfaitement.

Sympt�me :

page.goto: Test timeout of 30000ms exceeded.
navigating to "http://localhost:4321/blog/", waiting until "load"

Cause : Le serveur python (python3 -m http.server 4321 --directory dist) mettait trop de temps � d�marrer. Le webServer de Playwright attendait que le port soit ouvert (timeout 15s), mais le serveur n’�tait pas encore pr�t � servir les fichiers.

La solution :

  1. Augmenter le timeout du webServer de 15s � 30s
  2. Ajouter retries: 1 pour tol�rer les �checs transitoires
webServer: isCI
  ? {
      command: 'python3 -m http.server 4321 --directory dist',
      url: 'http://localhost:4321',
      timeout: 30000,  // ? augment� de 15000
    }
  : { /* ... */ },
retries: isCI ? 1 : 0,  // ? ajout�

Depuis, le pipeline passe � chaque fois.

Structure des tests

Mes 25 tests couvrent toutes les pages du site :

// src/tests/e2e/blog.spec.ts
import { test, expect } from '@playwright/test';

test.describe('Page Blog', () => {
  test('se charge avec le bon titre', async ({ page }) => {
    await page.goto('/blog/');
    await expect(page).toHaveTitle(/[Bb]log/);
    await expect(page.locator('h1')).toContainText('Blog');
  });

  test('liste des articles', async ({ page }) => {
    await page.goto('/blog/');
    const cards = page.locator('.post-card');
    await expect(cards.first()).toBeVisible();
    await expect(await cards.count()).toBeGreaterThan(0);
  });

  test('les filtres de tags sont pr�sents', async ({ page }) => {
    await page.goto('/blog/');
    await expect(page.locator('.tag-btn').first()).toBeVisible();
  });

  test('filtrer par tag cache les articles sans ce tag', async ({ page }) => {
    await page.goto('/blog/');
    const tagBtn = page.locator('.tag-btn').nth(1);
    await tagBtn.click();
    await expect(tagBtn).toHaveClass(/active/);
  });
});

Les tests v�rifient :

  • Le chargement de chaque page (titre, contenu)
  • La navigation entre les pages
  • Les filtres par tag
  • Les liens externes
  • La FAQ interactive
  • Les stats hero

Le job e2e dans GitLab CI

e2e-tests:
  stage: e2e
  image: node:22-bookworm
  needs: [build]
  script:
    - npm ci --no-fund
    - npx playwright install --with-deps chromium
    - npx playwright test --reporter=line
  artifacts:
    when: on_failure
    paths:
      - test-results/
    expire_in: 3 days
  rules:
    - if: $CI_COMMIT_BRANCH == "master"

Points importants :

  • needs: [build] - t�l�charge le dist/ g�n�r� par le stage build
  • --with-deps chromium - installe les d�pendances syst�me de Chrome (fonts, libs graphiques)
  • artifacts: when: on_failure - ne garde les screenshots/traces qu’en cas d’�chec
  • --reporter=line - output minimal en CI (pas de HTML report)

Le deploy automatique

Si tous les tests passent, le deploy se fait en automatique :

deploy:
  stage: deploy
  image: alpine:latest
  needs: [build, e2e-tests]
  before_script:
    - apk add --no-cache openssh-client rsync
    - mkdir -p ~/.ssh
    - echo "$SSH_DEPLOY_KEY" > ~/.ssh/id_ed25519
    - chmod 600 ~/.ssh/id_ed25519
  script:
    - rsync -avz --delete
        --exclude='projects/'
        --exclude='fring/'
        -e "ssh -p 2222 -o StrictHostKeyChecking=no"
        dist/
        root@$DEPLOY_HOST:$DEPLOY_PATH/

Le deploy utilise rsync over SSH pour pousser le dist/ sur le serveur Unraid. La cl� SSH est stock�e dans les variables CI/CD de GitLab.

Les le�ons apprises

Pi�geSolution
Serveur pas pr�t au premier testAugmenter timeout du webServer
Tests flaky en CIretries: 1
chromium ne d�marre pas--with-deps chromium pour les libs syst�me
Artifacts trop grosartifacts: when: on_failure
Deploy sans testneeds: [build, e2e-tests]

Conclusion

Mettre en place des tests e2e sur un site statique peut sembler overkill, mais �a a chang� ma fa�on de travailler. Avant : je pushais, je v�rifiais manuellement, je corrigeais les bugs en prod. Maintenant : je push, le pipeline tourne, et si �a passe c’est bon.

Le vrai gain n’est pas tant la couverture de tests que la confiance. Quand je modifie un composant, un fichier de config, ou un article de blog, je sais que le pipeline v�rifiera que tout fonctionne encore.

Et avec l’IA (OpenCode dans mon cas), �crire des tests devient quasi gratuit - je lui demande de tester une fonctionnalit�, elle �crit le test, je review, et c’est push�.

Mon workflow est maintenant : �crire ? tester ? push ? deploy. Sans passer par la case “bug en prod � 23h”.


Cet article a �t� �crit avec l’aide d’OpenCode, un assistant IA de d�veloppement. Le site sevan.zone est construit avec Astro 4, test� avec Playwright, et d�ploy� via GitLab CI sur un serveur Unraid self-host�.