Assurer la qualité du blog avec des tests Playwright

- Read in english
Ressources: garabit

Alors que nous avons construit un blog utilisant VitePress et Vue.js, il est vital de confirmer qu'il fonctionne comme prévu. Les tests sont un élément essentiel du développement logiciel, garantissant que notre blog fonctionne correctement et efficacement.

Tester chaque fonctionnalité manuellement serait un processus laborieux et sujet aux erreurs. Alors que nous continuons à affiner notre blog pour répondre aux exigences en constante évolution, les tests automatisés deviennent indispensables. Ils nous permettent de vérifier rapidement que notre blog fonctionne comme prévu, même lorsque des modifications sont apportées. Cette pratique contribue à maintenir la qualité, à prévenir les régressions et à accélérer le flux de développement.

La nature directe de ce blog en fait un candidat idéal pour que les débutants mettent en œuvre des tests de bout en bout. Ce type de test valide la fonctionnalité du logiciel du point de vue de l'utilisateur, garantissant que tous les composants intégrés fonctionnent harmonieusement.

Les Tests ne Consistent Pas Seulement à Écrire des Tests

Dans cet article, nous explorerons les tests automatisés en utilisant Playwright.

Installation de Playwright

Playwright est une bibliothèque Node.js conçue pour automatiser les navigateurs, nous permettant de script les interactions avec les pages web telles que le clic sur des boutons, la complétion de formulaires et la navigation entre les pages. Playwright est compatible avec plusieurs navigateurs, notamment Chromium, Firefox et WebKit.

Pour installer Playwright, exécutez la commande suivante :

bash
pnpm create playwright

Cette commande invite à la création d'un dossier de tests dans le répertoire racine. Nous accepterons le nom de dossier par défaut tests. Elle demande également si nous avons besoin d'un fichier de workflow GitHub Action. Nous choisirons Oui. Enfin, elle demande si nous souhaitons installer les navigateurs Playwright. Nous opterons pour Oui.

Configuration de Playwright

Après l'installation de Playwright, plusieurs fichiers sont générés, dont playwright.config.ts, où nous devrons apporter les ajustements nécessaires pour répondre à nos besoins.

Dans un premier temps, nous modifierons la propriété timeout à 4_000 millisecondes. Étant donné la simplicité de nos tests, un délai d'attente plus court est suffisant, ce qui pourrait accélérer l'exécution des tests en cas d'échec. La propriété workers sera réglée à 100%, permettant à Playwright d'utiliser tous les cœurs CPU disponibles pour l'exécution parallèle des tests. Cette configuration fonctionne car nos tests sont indépendants ; nous créons un site statique. Dans la propriété use, définissez le baseURL sur http://localhost:4173. C'est le port par défaut de VitePress pour le serveur de prévisualisation, ce qui évite d'avoir à spécifier des URL redondantes dans chaque test. Dans la propriété projects, nous sélectionnerons uniquement le navigateur Google Chrome, bien que d'autres navigateurs puissent être inclus à votre discrétion.

Enfin, ajustez la propriété webServer pour construire et prévisualiser automatiquement notre blog avant l'exécution des tests, garantissant qu'il soit à jour. Comme VitePress fonctionne différemment en développement par rapport à la production, il est préférable de tester contre la version de production.

En fin de compte, notre fichier playwright.config.ts devrait ressembler à ceci :

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

export default defineConfig({
  testDir: './tests',
  fullyParallel: true,
  forbidOnly: !!process.env.CI,
  retries: process.env.CI ? 2 : 0,
  timeout: 4_000,
  workers: '100%',
  reporter: 'html',
  use: {
    baseURL: 'http://localhost:4173',
    trace: 'on-first-retry',
  },

  projects: [
    {
      name: 'Google Chrome',
      use: { ...devices['Desktop Chrome'], channel: 'chrome' },
    },
  ],

  webServer: {
    command: 'npm run build && npm run preview',
    url: 'http://localhost:4173',
    reuseExistingServer: false,
    timeout: 60_000,
  },
})

Dans notre fichier package.json, incluez les scripts suivants :

json
{
  "scripts": {
    "test": "npm run test:e2e",
    "test:e2e": "playwright test",
    "test:e2e:ui": "playwright test --ui"
  }
}

Le premier script exécute tous les tests, à la fois des tests unitaires et des tests de bout en bout. Le deuxième script exécute spécifiquement des tests de bout en bout. Le troisième script permet d'exécuter des tests de bout en bout avec une interface graphique, utile pendant le développement des tests car il nous permet de visualiser l'activité du navigateur.

Écriture des Tests

Avec Playwright maintenant configuré, nous pouvons procéder à l'écriture de nos tests. Commençons par un test simple avant de progresser vers des tests plus complexes.

Test de l'En-tête

Commencez par créer un fichier header.test.ts dans le dossier tests. Ces tests confirmeront l'affichage correct de l'en-tête de notre blog avec les liens attendus, garantissant la cohérence de l'en-tête après le refactoring.

ts
import { expect, test } from '@playwright/test'

test('doit avoir un lien vers la page d’accueil', async ({ page }) => {
  // Organiser

  // Agir
  await page.goto('/')

  // Affirmer
  const headerHomeLink = page
    .getByRole('banner')
    .getByRole('link', { name: 'Garabit', exact: true })

  await expect(headerHomeLink).toBeVisible()
  await expect(headerHomeLink).toHaveAttribute('href', '/')
})

Permettez-moi d'éclaircir ce test initial. Nous utilisons la fonction test de Playwright pour créer un test. Le premier argument décrit le test, tandis que le second est une fonction asynchrone recevant un objet contenant la propriété page. Cet objet représente l'objet page de Playwright, qui interagit avec le navigateur.

Dans notre test, nous naviguons d'abord vers la page d'accueil. Nous pouvons utiliser une URL relative en raison de la configuration baseURL dans playwright.config.ts. Ensuite, nous obtenons l'élément de l'en-tête par son rôle, en vérifiant sa visibilité et son lien correct. Utiliser la sélection d'éléments basée sur le rôle garantit que les tests restent résilients en cas de changements dans la structure HTML et garantit l'accessibilité des éléments.

Nous pouvons effectuer deux tests supplémentaires pour vérifier la présence des liens vers le blog et les projets GitHub, similaires au premier, mais ciblant différents éléments.

ts
test('doit avoir un lien vers le blog', async ({ page }) => {
  // Organiser

  // Agir
  await page.goto('/')

  // Affirmer
  const headerBlogLink = page
    .getByRole('banner')
    .getByRole('link', { name: 'Blog', exact: true })

  await expect(headerBlogLink).toBeVisible()
  await expect(headerBlogLink).toHaveAttribute('href', '/blog')
})

test('doit avoir un lien vers les projets', async ({ page }) => {
  // Organiser

  // Agir
  await page.goto('/')

  // Affirmer
  const headerProjectsLink = page
    .getByRole('banner')
    .getByRole('link', { name: 'Projects', exact: true })

  await expect(headerProjectsLink).toBeVisible()
  await expect(headerProjectsLink).toHaveAttribute('href', '/projects')
})

Exécutez les tests en utilisant la commande :

bash
pnpm run test:e2e:ui # ou pnpm run test:e2e

Cette commande ouvrira un navigateur et exécutera les tests. Un test réussi affichera une coche verte à côté de lui, tandis qu'un test échoué montrera une croix rouge. Cliquer sur un test fournira les détails du message d'erreur.

Projets

Ensuite, nous pouvons valider que les utilisateurs peuvent naviguer de la page des projets vers la page d'accueil en utilisant le bouton "Retour à l'accueil". Créez un fichier projects.test.ts dans le dossier tests avec le contenu suivant :

ts
import { expect, test } from '@playwright/test'

test('doit avoir un lien vers la page d’accueil', async ({ page }) => {
  // Organiser

  // Agir
  await page.goto('/projects')

  // Affirmer
  const headerHomeLink = page.getByRole('link', {
    name: 'Retour à l’accueil',
    exact: true,
  })

  await expect(headerHomeLink).toBeVisible()
  await expect(headerHomeLink).toHaveAttribute('href', '/')
})

Utiliser la commande pnpm run test:e2e:ui vous permettra d'observer les tests au fur et à mesure de leur création. Au fur et à mesure que vous les développez, vous pouvez relancer les tests en utilisant le bouton de lecture vert pour confirmer qu'ils passent.

Dans ce fichier, vous pourriez également envisager d'évaluer le titre de la page et de vérifier que la liste des projets reflète les projets prévus pour l'affichage. Dans un premier temps, privilégiez le test d'éléments non visibles comme les liens, comme cela a été fait précédemment, et intégrez des tests lorsque des problèmes surviennent ou que le code est refactorisé.

Blog

Suivant la même procédure pour la page des projets, créez un fichier blog.test.ts dans le dossier tests. Testons le bouton "Retour à l'accueil".

ts
import { expect, test } from '@playwright/test'

test.describe('index', () => {
  test('doit avoir un lien vers la page d’accueil', async ({ page }) => {
    // Organiser

    // Agir
    await page.goto('/blog')

    // Affirmer
    const headerHomeLink = page.getByRole('link', {
      name: 'Retour à l’accueil',
      exact: true,
    })

    await expect(headerHomeLink).toBeVisible()
    await expect(headerHomeLink).toHaveAttribute('href', '/')
  })
})

Nous utilisons un bloc test.describe pour regrouper des tests similaires, ce qui est avantageux lors de la gestion de plusieurs tests sur la même page. Dans ce fichier, nous allons écrire des tests pour la page d'index du blog ainsi que pour des articles de blog individuels.

Concernant la page d'article de blog, nous pourrions examiner si chaque article contient un bouton "Retour au blog". Devrions-nous essayer de tester un seul article, ou devrions-nous le faire pour tous ? Comment pouvons-nous naviguer vers l'article de blog ?

Une approche consiste à élaborer un test qui navigue vers /blog et clique sur le premier article, vérifiant ensuite la visibilité et l'exactitude du lien du bouton "Retour au blog". Ce test valide efficacement la navigation inter-pages mais n'est pas notre objectif actuel. Alternativement, nous pourrions sélectionner tous les articles et itérer pour eux, confirmant ainsi une fonctionnalité cohérente.

Cependant, comme VitePress génère l'URL en fonction du nom de fichier, nous pouvons exploiter cet aspect en utilisant un modèle glob pour récupérer tous les noms de fichiers d'articles et les itérer pour vérifier la fonctionnalité. Cette méthode efficace accélère les tests.

ts
import { expect, test } from '@playwright/test'
import { glob } from 'tinyglobby'

const posts = await glob('src/blog/*.md')

test.describe('index', () => {
  // ...
})

test.describe('show', () => {
  posts.forEach((post) => {
    const link = post.replace('src', '').replace('.md', '').replace('.md', '')

    test(`doit avoir un lien vers l'index du blog (${post})`, async ({ page }) => {
      // Organiser

      // Agir
      await page.goto(link)

      // Affirmer
      const headerBlogLink = page.getByRole('link', {
        name: 'Retour au blog',
        exact: true,
      })

      await expect(headerBlogLink).toBeVisible()
      await expect(headerBlogLink).toHaveAttribute('href', '/blog')
    })
  })
})

SEO

Utiliser des tests de bout en bout pour un blog statique s'avère très utile pour vérifier les métadonnées SEO. Nous pouvons vérifier l'exactitude du titre, de la description et de l'image de chaque page, garantissant une optimisation SEO optimale.

Vérifier manuellement le DOM de chaque page pour les balises correctes est laborieux, mais Playwright simplifie la manipulation du DOM, y compris des éléments cachés.

En appliquant les mêmes techniques que sur la page des articles de blog, nous pouvons utiliser un modèle glob pour itérer à travers toutes les pages, vérifiant les métadonnées SEO.

ts
import test, { expect } from '@playwright/test'
import { glob } from 'tinyglobby'
import { joinURL, withoutTrailingSlash } from 'ufo'

const pages = await glob('src/**/*.md')

pages.forEach((page) => {
  const link = page.replace('src', '').replace('.md', '').replace('index', '')

  test(`doit avoir des métadonnées (${page})`, async ({ page, request }) => {
    // Organiser

    // Agir
    await page.goto(link)

    // Affirmer
    const title = await page.title()
    expect(title).toBeTruthy()
    expect(title.endsWith(' | Garabit')).toBeTruthy()

    const ogTitle = await page
      .locator('meta[property="og:title"]')
      .getAttribute('content')
    expect(ogTitle).toBeTruthy()
    const twitterTitle = await page
      .locator('meta[name="twitter:title"]')
      .getAttribute('content')
    expect(twitterTitle).toBeTruthy()

    expect(title.startsWith(ogTitle!)).toBeTruthy()
    expect(ogTitle).toBe(twitterTitle)

    const description = await page
      .locator('meta[name="description"]')
      .getAttribute('content')
    expect(description).toBeTruthy()
    const ogDescription = await page
      .locator('meta[property="og:description"]')
      .getAttribute('content')
    expect(ogDescription).toBeTruthy()
    const twitterDescription = await page
      .locator('meta[name="twitter:description"]')
      .getAttribute('content')
    expect(twitterDescription).toBeTruthy()

    expect(description).toBe(ogDescription)
    expect(description).toBe(twitterDescription)

    const twitterSite = await page
      .locator('meta[name="twitter:site"]')
      .getAttribute('content')
    expect(twitterSite).toBe('@soubiran_')
    const twitterCard = await page
      .locator('meta[name="twitter:card"]')
      .getAttribute('content')
    expect(twitterCard).toBe('summary_large_image')

    const canonical = await page
      .locator('link[rel="canonical"]')
      .getAttribute('href')
    expect(canonical).toBe(
      withoutTrailingSlash(joinURL('https://garabit.barbapapazes.dev', link)),
    )

    const ogMetaTags = [
      { name: 'og:image:width', value: '1200' },
      { name: 'og:image:height', value: '630' },
      { name: 'og:image:type', value: 'image/png' },
      { name: 'og:site_name', value: 'Garabit' },
      { name: 'og:type', value: 'website' },
      { name: 'og:url', value: 'https://garabit.barbapapazes.dev' },
    ]

    for (const { name, value } of ogMetaTags) {
      const metaTag = await page
        .locator(`meta[property="${name}"]`)
        .getAttribute('content')
      expect(metaTag).toBe(value)
    }

    const url = await page
      .locator('meta[property="og:image"]')
      .getAttribute('content')
    expect(url).toBeTruthy()

    const image = await request.get(
      url!.replace('https://garabit.barbapapazes.dev', 'http://localhost:4173'),
    )
    expect(image.ok()).toBeTruthy()
    expect(image.headers()['content-type']).toBe('image/png')
  })
})

Ce test confirme même le bon fonctionnement de la génération automatique d'images Open Graph.

Intégration Continue

Pour automatiser l'exécution des tests lors des envois de code, nous employons GitHub Actions. Playwright fournit un fichier de workflow GitHub Action, que nous pouvons modifier selon nos spécifications.

yaml
name: Tests Playwright

on:
  push:
    branches:
      - main
  pull_request:
    branches:
      - main

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

jobs:
  test:
    timeout-minutes: 60
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
      - run: corepack enable
      - uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4
        with:
          node-version: 20
          cache: pnpm

      - name: Installer les dépendances
        run: pnpm install

      - name: Installer les navigateurs Playwright
        run: pnpm exec playwright install --with-deps chrome

      - name: Exécuter les tests Playwright
        run: pnpm run test:e2e

      - uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4
        if: always()
        with:
          name: rapport-playwright
          path: playwright-report/
          retention-days: 30

Nous installons uniquement le navigateur Chrome car il est le seul utilisé dans nos tests, ce qui accélère le workflow. La commande pnpm run test:e2e initie les tests, et les résultats échoués sont enregistrés en tant qu'artefacts pour l'analyse des erreurs.

Conclusion

Dans cet article, nous avons mis en œuvre des tests automatisés pour notre blog en utilisant Playwright. Nous avons configuré Playwright pour l'exécution parallèle des tests, mis en place les navigateurs et exécuté des tests contre des versions de production. Nous avons élaboré des tests pour les en-têtes, les projets, les blogs et les métadonnées SEO tout en établissant l'intégration continue avec GitHub Actions pour une exécution automatique des tests. Cette approche maintient la qualité du blog, prévient les régressions et rationalise le processus de développement.