Tester dans un navigateur headless

"Gros plan d'une statue sans tête"

Gros plan d'une statue sans tête, Chris Lexow sur Flickr

Depuis plus de 12 ans, je développe des visualisations de données sur le web, et l’automatisation des tests dans un environnement navigateur a toujours été compliqué. C’est possible, mais souvent pénible et difficile à maintenir. De plus, l’automatisation des benchmarks dans un environnement navigateur a toujours été encore plus difficile à mettre en place.

Dans cet article, je vais démontrer à quel point il est facile aujourd’hui de configurer Vite, Vitest et Playwright pour tester et faire des benchmarks de code TypeScript dans un navigateur headless. Cette configuration permet d’exécuter les tests et les benchmarks depuis la ligne de commande et en CI, rendant la configuration beaucoup plus simple.

Pourquoi j’utilise Vitest

En travaillant récemment sur sigma.js v3, j’ai commencé à moderniser tous le tooling. L’état existant consistait en des configurations Webpack obsolètes, des scripts ad hoc et beaucoup trop de boilerplate. C’était difficile à maintenir et à faire évoluer, avec plusieurs problèmes non résolus, notamment l’exécution de tests unitaires dans un environnement navigateur.

J’ai passé des heures à chercher une solution de test moderne et facile à configurer, en vain. Divers articles de blog suggéraient des solutions basées sur Puppeteer, PhantomJS, voire CasperJS, mais elles semblaient toutes obsolètes et/ou difficiles à configurer (et probablement à maintenir).

Vitest pour les tests unitaires

Finalement, je suis tombé sur le mode navigateur de Vitest basé sur Playwright, qui correspondait parfaitement à mes besoins. À ce moment-là, sigma.js dépendait déjà de Playwright pour les tests end to end. J’aurais aimé que cette solution soit plus facile à trouver, et j’espère que cet article en accélérera la découverte pour d’autres.

Vitest pour les benchmarks

Au cours de mes recherches, j’ai également découvert la fonctionnalité de benchmarking de Vitest, basée sur TinyBench. Ça permet à Vitest d’exécuter TinyBench pour réaliser des benchmarks. Et ce n’est pas que du sucre : combinée avec le mode navigateur, ça permet de faire des benchmarks du code TypeScript depuis la ligne de commande dans un environnement navigateur.

Cependant, l’intégration de ces fonctionnalités n’était initialement pas possible en raison d’un problème dans Vitest. L’équipe Vitest a résolu ce ticket en quelques mois, avec une communication claire sur les progrès, ce qui était très agréable à observer.

"Un doodle réalisé avec le projet d'exemple"
Un doodle réalisé avec le projet d'exemple

Tester et benchmarker un projet d’exemple

Pour démontrer à quel point il est facile d’utiliser Vitest et Playwright pour tester et lancer des benchmarks de code TypeScript dans un navigateur headless, j’ai créé un projet d’exemple. Il vise à comparer deux méthodes de dessin de lignes épaisses sur un élément canvas : en utilisant CanvasRenderingContext2D.drawRect ou en utilisant CanvasRenderingContext2D.stroke (avec une lineWidth épaisse).

Le projet est disponible ici : github.com/jacomyal/canvas-benchmark

Le Code Principal

Essentiellement, les deux fonctions que j’ai testées et benchmarkées ressemblent à ceci :

function fillBasedThickLine(ctx, from, to, thickness, color) {
  const dx = to.x - from.x;
  const dy = to.y - from.y;
  const d = Math.sqrt(dx ** 2 + dy ** 2);
  const angle = Math.atan2(dy, dx);

  ctx.save();
  ctx.fillStyle = color;
  ctx.translate(from.x, from.y);
  ctx.rotate(angle);
  ctx.fillRect(0, 0 - thickness / 2, d, thickness);
  ctx.restore();
}

function strokeBasedThickLine(ctx, from, to, thickness, color) {
  ctx.beginPath();
  ctx.moveTo(from.x, from.y);
  ctx.lineTo(to.x, to.y);

  ctx.lineWidth = thickness;
  ctx.strokeStyle = color;
  ctx.stroke();
}

Le code exact est disponible dans le fichier src/index.ts. Pour vérifier qu’elles fonctionnent correctement, j’ai ajouté un petit exemple, où les utilisateurs peuvent cliquer sur un canvas pour dessiner des lignes épaisses. Vous pouvez jouer avec ici :

Tests Unitaires

J’ai commencé par écrire un test pour chaque fonction afin de vérifier que la fonction colorie au moins un pixel sur un canvas de 1x1. Ils ressemblent à ceci :

test("it should colorize pixels", () => {
  // Dessiner une ligne épaisse contenant l'unique pixel
  drawThickLine(ctx, { x: -1, y: -1 }, { x: 1, y: 1 }, 2, "#ff0000");

  // Récupérer la couleur du pixel :
  const {
    data: [r, g, b, a],
  } = ctx.getImageData(0, 0, 1, 1);

  expect([r, g, b, a]).toStrictEqual([255, 0, 0, 255]);
});

La version complète avec le boilerplate est disponible dans le fichier src/index.spec.ts.

Pour exécuter les tests unitaires, j’ai d’abord installé Vitest et Playwright :

npm install --save-dev vitest @vitest/browser playwright

Playwright doit également installer ses navigateurs headless localement :

npx playwright install

Ensuite, j’ai ajouté un fichier vitest.config.mts, indiquant à Vitest d’utiliser Playwright et le mode navigateur :

import { defineConfig } from "vitest/config";

export default defineConfig({
  test: {
    include: ["*.ts"],
    dir: "src",
    browser: {
      provider: "playwright",
      name: "chromium",
      enabled: true,
      headless: true,
    },
  },
});

À ce stade, lorsque j’ai exécuté npx vitest test src/index.spec.ts, cela a affiché ce qui suit :

DEV  v1.5.3 canvas-benchmark
Browser runner started at http://localhost:5173/

✓ src/index.spec.ts (2)
✓ fillBasedRectangle (1)
✓ it should colorize pixels
✓ strokeBasedRectangle (1)


✓ it should colorize pixels

Test Files  1 passed (1)
Tests  2 passed (2)
Start at  16:58:52
Duration  800ms (transform 0ms, setup 0ms, collect 20ms, tests 6ms, environment 0ms, prepare 0ms)

Les tests unitaires se sont exécutés avec succès depuis la ligne de commande dans un navigateur headless !

Benchmarks

Pour faire des benchmarks de ces fonctions, j’ai écrit quelque chose comme ceci :

bench(
  "Canvas methods to draw a thick line",
  () => {
    const angle = 2 * Math.PI * Math.random();
    drawThickLine(
      ctx,
      {
        x: 500 - 500 * Math.cos(angle),
        y: 500 - 500 * Math.sin(angle),
      },
      {
        x: 500 + 500 * Math.cos(angle),
        y: 500 + 500 * Math.sin(angle),
      },
      50,
      getRandomColor(),
    );
  },
  { iterations: 1000 },
);

Encore une fois, la version complète avec le boilerplate est disponible dans le fichier src/index.bench.ts.

Pour activer la fonctionnalité de benchmarking, j’ai ajouté une ligne dans la configuration de Vitest :

export default defineConfig({
  mode: "benchmark",
  test: {
    /* ... */
  },
});

Maintenant, exécuter npx vitest bench ./src/index.bench.ts affiche la sortie suivante :

DEV  v1.5.3 canvas-benchmark
Browser runner started at http://localhost:5173/

✓ src/index.bench.ts (2) 8624ms
✓ Canvas methods to draw a thick line (2) 8615ms
name                             hz     min       max    mean     p75     p99    p995    p999       rme  samples
· Using a filled rectangle  15,341.30  0.0000  1,785.50  0.0652  0.0000  0.1000  0.1000  0.1000  ±184.32%    29127   fastest
· Using a wide stroke       14,882.96  0.0000  2,427.90  0.0672  0.0000  0.1000  0.1000  0.1000  ±189.11%    37450

BENCH  Summary

Using a filled rectangle - src/index.bench.ts > Canvas methods to draw a thick line
1.03x faster than Using a wide stroke

Les deux méthodes affichent des performances très similaires. Et cette analyse est automatisable et reproductible depuis la ligne de commande.

Conclusion

Je développe des bibliothèques JavaScript ciblant le navigateur depuis plus de 10 ans, et les tests unitaires ont toujours été un défi - et c’était encore pire pour les benchmarks. Aujourd’hui, grâce à Vitest et Playwright, l’intégration de ces processus dans n’importe quel projet TypeScript est devenue remarquablement facile.