Aller au contenu principal

Tests E2E avec Playwright et WordPress Playground

Les tests de bout en bout vérifient que votre extension ou thème WordPress fonctionne correctement du point de vue de l'utilisateur — cliquer sur des boutons, remplir des formulaires et naviguer sur les pages dans un véritable navigateur. Ce guide montre comment combiner Playwright avec la CLI WordPress Playground pour écrire des tests E2E fiables sans Docker, bases de données ou configuration manuelle.

info

Ce guide suppose une familiarité avec le développement d’extensions ou de thèmes WordPress. Pour une introduction à l'utilisation de Playground dans votre flux de développement, consultez WordPress Playground pour développeurs de plugins. Pour les détails de configuration des Blueprints, voir Démarrage avec Blueprints.

Prérequis

  • Node.js 20+ et supérieur
  • Une extension/thème WordPress ou un site WordPress complet à tester
  • Recommandé : activez la règle ESLint @typescript-eslint/no-floating-promises pour détecter les await manquants dans les appels asynchrones Playwright

Configuration du projet

Installer les dépendances

Depuis le répertoire racine de votre extension ou thème :

npm init -y
npm install --save-dev @playwright/test @wp-playground/cli
npx playwright install chromium

Cela installe Playwright comme exécuteur de tests, la CLI Playground pour créer des instances WordPress et le navigateur Chromium pour l'exécution des tests.

Configurer Playwright

Créez un fichier playwright.config.ts à la racine de votre projet :

import { defineConfig } from "@playwright/test";

export default defineConfig({
testDir: "./tests/e2e",
fullyParallel: false,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: 1,
reporter: "html",
timeout: 120_000,
expect: {
timeout: 30_000,
},
use: {
screenshot: "only-on-failure",
trace: "on-first-retry",
},
});

WordPress Playground a besoin de plus de temps pour démarrer qu'une application web typique. Le délai d'attente des tests de 120 secondes et le délai d'assertion de 30 secondes tiennent compte du temps de démarrage de WordPress et du chargement des pages. La valeur workers: 1 évite les conflits de ports lorsque plusieurs tests partagent un serveur Playground.

Utiliser baseURL avec des ports dynamiques

Par défaut, Playground utilisera le port 9400. Si vous souhaitez sélectionner un port différent, passez port: [NOUVEAU_NUMÉRO_DE_PORT] dans les options de runCLI pour sélectionner un port différent :

const cli = await runCLI({ command: "server", port: 9500, blueprint });

Puis ajoutez baseURL: "http://localhost:9500" à la section use ci-dessus. Notez que testMatch utilise par défaut **/*.spec.ts — personnalisez-le si vos fichiers de test utilisent un autre schéma de nommage.

astuce

Le projet WordPress Playground utilise des délais encore plus longs (300s pour les tests, 60s pour les assertions) pour ses propres tests. Commencez avec les valeurs ci-dessus et augmentez-les si votre environnement CI est plus lent.

Premier fichier de test

Créez tests/e2e/plugin.spec.ts :

import { test, expect } from "@playwright/test";
import { runCLI } from "@wp-playground/cli";

let cli: Awaited<ReturnType<typeof runCLI>>;

test.beforeAll(async () => {
cli = await runCLI({
command: "server",
blueprint: {
preferredVersions: { php: "8.3", wp: "latest" },
login: true,
},
});
});

test.afterAll(async () => {
await cli?.server?.close();
});

test("WordPress dashboard loads", async ({ page }) => {
await page.goto(`${cli.serverUrl}/wp-admin/`);
// WordPress core admin elements lack ARIA roles — CSS selectors are acceptable here
await expect(page.locator("#wpbody-content")).toBeVisible();
await expect(page).toHaveTitle(/Dashboard/);
});

Exécutez le test :

npx playwright test

Choisir les localisateurs

Playwright offre plusieurs façons de trouver les éléments sur la page. Préférez les localisateurs qui reflètent la façon dont les utilisateurs voient la page, en utilisant les sélecteurs CSS uniquement en dernier recours.

Priorité des localisateurs (du plus au moins recommandé) :

  1. page.getByRole() — boutons, titres, liens, contrôles de formulaire
  2. page.getByLabel() — champs de formulaire avec étiquettes associées
  3. page.getByText() — contenu texte visible
  4. page.getByTestId() — éléments avec attributs data-testid que vous ajoutez à votre plugin
  5. page.locator() — sélecteurs CSS ou XPath en dernier recours

Conseils spécifiques à WordPress

Dans l'admin WordPress, certains éléments principaux (barre d'administration, meta boxes) utilisent des ID et classes CSS plutôt que des rôles ARIA. Cependant, de nombreux éléments fonctionnent bien avec des localisateurs sémantiques. Cela signifie :

  • Utilisez des localisateurs sémantiques pour les boutons, titres, liens, champs de formulaire et éléments du menu admin — WordPress rend des éléments standards <button>, <input>, <a> et <h1> que getByRole et getByLabel peuvent trouver.
  • Utilisez data-testid pour le markup de votre propre plugin — vous contrôlez le HTML, ajoutez donc des attributs testables.
  • Utilisez des sélecteurs CSS pour les éléments de mise en page principaux de WordPress comme #wpadminbar ou #wpbody-content — ils n'ont pas d'alternatives ARIA.

Même élément, trois approches

// ✅ Préféré : localisateur sémantique (fonctionne car WP rend un vrai <button>)
await page.getByRole("button", { name: "Enregistrer les modifications" }).click();

// ⚠️ Acceptable : ID de test que vous avez ajouté au markup de votre plugin
await page.getByTestId("save-settings").click();

// ❌ À éviter : sélecteur CSS fragile lié au markup WordPress
await page.locator("#submit").click();
Générer les localisateurs automatiquement

Exécutez npx playwright codegen localhost:9400/wp-admin/ pour ouvrir un navigateur et enregistrer les interactions. Playwright génère le code des localisateurs pendant que vous cliquez, vous aidant à découvrir quels localisateurs sémantiques fonctionnent pour chaque élément.

Auto-attente et assertions orientée web

Les localisateurs Playwright attendent automatiquement que les éléments apparaissent, deviennent visibles et actionnables. Dans la plupart des cas, vous n'avez pas besoin d'appels manuels à waitForSelector.

Assertions orientée web

Les assertions orientée web réessaient automatiquement jusqu'à ce que la condition soit remplie ou que le délai expire. Préférez-les toujours aux vérifications manuelles :

// ✅ Assertion orientée web (réessaie jusqu'à visible ou timeout)
await expect(page.getByText("Paramètres enregistrés")).toBeVisible();

// ❌ Vérification manuelle (sans retry — instable si l'élément apparaît en retard)
expect(await page.getByText("Paramètres enregistrés").isVisible()).toBe(true);

Assertions souples

Utilisez expect.soft() pour vérifier plusieurs éléments sur une page sans s'arrêter au premier échec. Tous les échecs apparaissent dans le rapport de test :

await expect.soft(page.getByLabel("API Key")).toHaveValue("test-key-123");
await expect.soft(page.getByText("Settings saved")).toBeVisible();
await expect.soft(page.getByRole("heading", { level: 1 })).toContainText("Settings");

Écrire des tests

Démarrer un serveur Playground

La fonction runCLI démarre un serveur Playground local et retourne un objet avec serverUrl (la chaîne URL) et server (l'instance du serveur HTTP). Passez un Blueprint pour configurer l'instance WordPress :

const cli = await runCLI({
command: "server",
blueprint: {
preferredVersions: { php: "8.3", wp: "latest" },
login: true,
steps: [
{
step: "installPlugin",
pluginData: {
resource: "wordpress.org/plugins",
slug: "woocommerce",
},
},
],
},
});

Cycle de vie du serveur : partagé vs. par test

Serveur partagé (beforeAll/afterAll) — une instance Playground sert tous les tests d'un bloc describe. Plus rapide, mais les tests peuvent s'influencer mutuellement :

test.describe("Plugin settings", () => {
test.beforeAll(async () => {
cli = await runCLI({ command: "server", blueprint });
});
test.afterAll(async () => {
await cli?.server?.close();
});
// Tests share the same WordPress instance
});

Serveur par test (beforeEach/afterEach) — chaque test obtient une instance fraîche. Plus lent, mais entièrement isolé :

test.beforeEach(async () => {
cli = await runCLI({ command: "server", blueprint });
});
test.afterEach(async () => {
await cli?.server?.close();
});

Utilisez des serveurs partagés lorsque les tests ne font que lire l'état (vérification du rendu des pages). Utilisez des serveurs par test lorsque les tests modifient l'état (création d'articles, modification des paramètres).

Utiliser les Blueprints comme fixtures de test

Les Blueprints définissent l'état WordPress nécessaire à chaque scénario de test. Voici les modèles courants :

Installer un plugin depuis wordpress.org

const blueprint = {
preferredVersions: { php: "8.3", wp: "latest" },
login: true,
steps: [
{
step: "installPlugin",
pluginData: {
resource: "wordpress.org/plugins",
slug: "contact-form-7",
},
},
],
};

Installer un plugin local

Montez le répertoire de votre plugin local dans l'instance Playground :

const cli = await runCLI({
command: "server",
mount: {
"./": "/wordpress/wp-content/plugins/my-plugin",
},
blueprint: {
preferredVersions: { php: "8.3", wp: "latest" },
login: true,
steps: [
{
step: "activatePlugin",
pluginPath: "my-plugin/my-plugin.php",
},
],
},
});

Cela mappe votre répertoire actuel au chemin du plugin dans WordPress, puis active le plugin. Les modifications de vos fichiers locaux se reflètent immédiatement. L'utilisateur peut configurer la propriété autoMount pour identifier les plugins et thèmes, mais la propriété mount offrira plus de contrôle à l'utilisateur pour définir différents dossiers dans le projet.

Définir les options et créer du contenu

const blueprint = {
login: true,
steps: [
{
step: "setSiteOptions",
options: {
blogname: "Test Site",
permalink_structure: "/%postname%/",
},
},
{
step: "runPHP",
code: `<?php
require '/wordpress/wp-load.php';
wp_insert_post([
'post_title' => 'Test Post',
'post_content' => '<!-- wp:paragraph --><p>Hello World</p><!-- /wp:paragraph -->',
'post_status' => 'publish',
]);
`,
},
],
};
astuce

Utilisez la Playground Step Library ou Pootle Playground pour prototyper votre configuration Blueprint visuellement avant de l'ajouter à votre code de test.

Tester les pages d'administration WordPress

Naviguez vers les pages d'administration et interagissez avec l'interface WordPress :

test("plugin settings page saves options", async ({ page }) => {
await page.goto(`${cli.serverUrl}/wp-admin/options-general.php?page=my-plugin`);

await page.getByLabel("API Key").fill("test-key-123");
await page.getByRole("button", { name: "Enregistrer les modifications" }).click();

await expect(page.getByText("Paramètres enregistrés")).toBeVisible();
await expect(page.getByLabel("API Key")).toHaveValue("test-key-123");
});

Gérer les éléments courants de l'interface d'administration

// Fermer les notifications admin WordPress (WP ajoute aria-label aux boutons de fermeture)
await page.getByRole("button", { name: "Dismiss this notice" }).first().click();

// Attendre le chargement de la barre admin — pas de rôle ARIA disponible, utiliser locator
await page.locator("#wpadminbar").waitFor();

// Naviguer via le menu admin
await page.getByRole("link", { name: "My Plugin" }).first().click();

Tester le front-end

test("plugin shortcode renders on front end", async ({ page }) => {
// Navigate to a page with the shortcode
await page.goto(`${cli.serverUrl}/?p=2`);

// Recommend: add data-testid="my-plugin-widget" to your plugin markup
await expect(page.getByTestId("my-plugin-widget")).toBeVisible();
await expect(page.getByTestId("my-plugin-widget")).toContainText(
"Expected content"
);
// Or use CSS if you don't control the markup:
// await expect(page.locator(".my-plugin-widget")).toBeVisible();
});

test("theme displays post correctly", async ({ page }) => {
await page.goto(`${cli.serverUrl}/test-post/`);

await expect(page.getByRole("heading", { level: 1 })).toContainText("Test Post");
await expect(page.getByText("Hello World", { exact: true })).toBeVisible();
});

Modèle Page Object Model

Le Page Object Model (POM) encapsule les interactions avec les pages dans des classes réutilisables. Cela réduit la duplication et facilite la maintenance des tests lorsque l'interface de votre plugin change.

// tests/e2e/pages/plugin-settings.ts
import { type Page, type Locator, expect } from "@playwright/test";

export class PluginSettingsPage {
readonly page: Page;
readonly apiKeyInput: Locator;
readonly saveButton: Locator;
readonly successNotice: Locator;

constructor(page: Page) {
this.page = page;
this.apiKeyInput = page.getByLabel("API Key");
this.saveButton = page.getByRole("button", { name: "Enregistrer les modifications" });
this.successNotice = page.getByText("Paramètres enregistrés");
}

async goto(baseUrl: string) {
await this.page.goto(
`${baseUrl}/wp-admin/options-general.php?page=my-plugin`
);
}

async setApiKey(key: string) {
await this.apiKeyInput.fill(key);
await this.saveButton.click();
}

async expectSaved() {
await expect(this.successNotice).toBeVisible();
}
}

Utilisez le POM dans les tests :

import { PluginSettingsPage } from "./pages/plugin-settings";

test("save plugin settings", async ({ page }) => {
const settings = new PluginSettingsPage(page);
await settings.goto(cli.serverUrl);
await settings.setApiKey("test-key-123");
await settings.expectSaved();
});

Le projet Playground utilise ce modèle avec une classe WebsitePage qui fournit des méthodes comme goto(), wordpress() et getSiteTitle() — encapsulant la navigation et les interactions spécifiques à WordPress.

Tester sur différentes versions de PHP et WordPress

Les tests paramétrés couvrent plusieurs combinaisons de versions sans dupliquer le code de test :

const versionMatrix = [
{ php: "8.1", wp: "6.5" },
{ php: "8.2", wp: "6.7" },
{ php: "8.3", wp: "latest" },
];

for (const { php, wp } of versionMatrix) {
test.describe(`PHP ${php} + WP ${wp}`, () => {
let versionCli: Awaited<ReturnType<typeof runCLI>>;

test.beforeAll(async () => {
versionCli = await runCLI({
command: "server",
blueprint: {
preferredVersions: { php, wp },
login: true,
steps: [
{
step: "activatePlugin",
pluginPath: "my-plugin/my-plugin.php",
},
],
},
});
});

test("admin page loads without errors", async ({ page }) => {
await page.goto(
`${versionCli.serverUrl}/wp-admin/options-general.php?page=my-plugin`
);
// WordPress core elements use CSS selectors — no ARIA roles available
await expect(page.locator(".error")).not.toBeVisible();
await expect(page.locator("#wpbody-content")).toBeVisible();
});

test("front-end output renders", async ({ page }) => {
await page.goto(versionCli.serverUrl);
await expect(page.getByTestId("my-plugin-widget")).toBeVisible();
});

test.afterAll(async () => {
await versionCli?.server?.close();
});
});
}

La propriété preferredVersions dans le Blueprint contrôle les versions PHP et WordPress utilisées par l'instance Playground. Plages prises en charge : PHP 7.4–8.5, WordPress 6.3–6.8+, ainsi que latest, nightly et beta. Pour des valeurs de version PHP typées, utilisez le type SupportedPHPVersion de @php-wasm/universal.

Exécuter les tests en CI/CD

GitHub Actions

Créez .github/workflows/e2e-tests.yml :

name: E2E Tests

on:
push:
branches: [main]
pull_request:
branches: [main]

jobs:
e2e:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version: 20
cache: "npm"

- name: Install dependencies
run: npm ci

- name: Cache Playwright browsers
uses: actions/cache@v4
id: playwright-cache
with:
path: ~/.cache/ms-playwright
key: playwright-${{ hashFiles('package-lock.json') }}

- name: Install Playwright browsers
if: steps.playwright-cache.outputs.cache-hit != 'true'
run: npx playwright install chromium --with-deps

- name: Run E2E tests
run: npx playwright test

- name: Upload test report
uses: actions/upload-artifact@v4
if: ${{ !cancelled() }}
with:
name: playwright-report
path: playwright-report/
retention-days: 30

Ce workflow installe les dépendances, télécharge Chromium, exécute les tests et télécharge le rapport HTML comme artefact. L'option --with-deps installe les bibliothèques système nécessaires à Chromium sur Ubuntu.

Fragmenter pour un CI plus rapide

Répartissez les tests sur plusieurs jobs CI avec la fragmentation intégrée de Playwright :

npx playwright test --shard=1/3
npx playwright test --shard=2/3
npx playwright test --shard=3/3

Créez trois jobs parallèles dans la matrice de votre workflow, chacun exécutant un fragment différent. Cela réduit proportionnellement le temps total de CI.

info

Pour les tests manuels des PR en complément des tests E2E automatisés, voir Ajouter des boutons d'aperçu PR avec GitHub Actions.

Dépannage

Erreurs de timeout — Augmentez timeout dans playwright.config.ts. Le temps de démarrage de WordPress varie selon l'environnement. Les exécuteurs CI ont souvent besoin de 120–180 secondes.

Conflits de ports — Laissez Playground attribuer les ports automatiquement. Ne codez pas en dur les numéros de port dans votre configuration. La propriété serverUrl retourne la bonne URL.

Navigateur non trouvé — Exécutez npx playwright install chromium pour télécharger le binaire du navigateur. Sur CI, ajoutez --with-deps pour les bibliothèques système.

WordPress ne charge pas — Vérifiez la syntaxe de votre Blueprint par rapport au schéma Blueprint. Les étapes invalides échouent silencieusement dans certains cas.

Les tests passent en local mais échouent en CI — Les exécuteurs CI ont moins de mémoire et de CPU. Augmentez les délais d'expiration, réduisez les processus parallèles et assurez-vous que workers: 1 est dans la config.

Déboguer les tests

Lorsqu'un test échoue, Playwright fournit plusieurs outils pour investiguer :

Playwright Inspector — parcourez les tests de manière interactive avec un débogueur intégré :

npx playwright test --debug

Visualiseur de traces — examinez une chronologie des actions, des snapshots DOM et des requêtes réseau d'un test échoué. La configuration trace: "on-first-retry" ci-dessus capture automatiquement les traces :

npx playwright show-trace test-results/plugin-spec-ts/trace.zip

Mode UI — exécutez les tests dans une interface visuelle où vous pouvez regarder, filtrer et relancer les tests :

npx playwright test --ui

Capture d'écran en cas d'échec — la configuration screenshot: "only-on-failure" dans le configuration sauvegarde une capture d'écran à chaque échec de test. Trouvez les captures dans le répertoire test-results/.

astuce

Combinez --debug avec un fichier de test spécifique pour concentrer votre investigation : npx playwright test tests/e2e/settings.spec.ts --debug

Prochaines étapes