Ir al contenido principal

Pruebas E2E con Playwright y WordPress Playground

Las pruebas de extremo a extremo verifican que tu plugin o tema WordPress funciona correctamente desde la perspectiva del usuario: haciendo clic en botones, completando formularios y navegando por páginas en un navegador real. Esta guía muestra cómo combinar Playwright con la CLI de WordPress Playground para escribir pruebas E2E fiables sin Docker, bases de datos ni configuración manual.

información

Esta guía asume familiaridad con el desarrollo de plugins o temas WordPress. Para una introducción al uso de Playground en tu flujo de desarrollo, consulta WordPress Playground para desarrolladores de plugins. Para detalles de configuración de Blueprint, consulta Introducción a Blueprints.

Requisitos previos

  • Node.js 20+ y superior
  • Un plugin/tema WordPress o un sitio WordPress completo para probar
  • Recomendado: habilita la regla ESLint @typescript-eslint/no-floating-promises para detectar await faltante en llamadas asíncronas de Playwright

Configuración del proyecto

Instalar dependencias

Desde el directorio raíz de tu plugin o tema:

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

Esto instala Playwright como ejecutor de pruebas, la CLI de Playground para crear instancias de WordPress y el navegador Chromium para la ejecución de pruebas.

Configurar Playwright

Crea un archivo playwright.config.ts en la raíz de tu proyecto:

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 necesita más tiempo para iniciarse que una aplicación web típica. El timeout de prueba de 120 segundos y el timeout de aserción de 30 segundos tienen en cuenta el tiempo de arranque de WordPress y la carga de páginas. Configurar workers: 1 evita conflictos de puertos cuando varias pruebas comparten un servidor Playground.

Usar baseURL con puertos dinámicos

Por defecto, Playground usará el puerto 9400. Si quieres seleccionar un puerto diferente, pasa port: [NUEVO_NÚMERO_DE_PUERTO] en las opciones de runCLI para seleccionar un puerto diferente:

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

Luego añade baseURL: "http://localhost:9500" a la sección use anterior. Nota que testMatch tiene por defecto **/*.spec.ts — personalízalo si tus archivos de prueba usan un patrón de nombres diferente.

consejo

El proyecto WordPress Playground usa timeouts aún mayores (300s de prueba, 60s de aserción) para sus propias pruebas. Empieza con los valores anteriores y aumenta si tu entorno CI es más lento.

Primer archivo de prueba

Crea 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/);
});

Ejecuta la prueba:

npx playwright test

Elegir localizadores

Playwright ofrece varias formas de encontrar elementos en la página. Prefiere localizadores que reflejen cómo los usuarios ven la página, usando selectores CSS solo cuando sea necesario.

Prioridad de localizadores (de más a menos preferido):

  1. page.getByRole() — botones, encabezados, enlaces, controles de formulario
  2. page.getByLabel() — inputs de formulario con etiquetas asociadas
  3. page.getByText() — contenido de texto visible
  4. page.getByTestId() — elementos con atributos data-testid que añades a tu plugin
  5. page.locator() — selectores CSS o XPath como último recurso

Orientación específica para WordPress

En el admin de WordPress, algunos elementos principales (barra de administración, meta boxes) usan IDs y clases CSS en lugar de roles ARIA. Sin embargo, muchos elementos funcionan bien con localizadores semánticos. Esto significa:

  • Usa localizadores semánticos para botones, encabezados, enlaces, campos de formulario e ítems del menú admin — WordPress renderiza elementos estándar <button>, <input>, <a> y <h1> que getByRole y getByLabel pueden encontrar.
  • Usa data-testid para el markup de tu propio plugin — controlas el HTML, así que añade atributos testeables.
  • Usa selectores CSS para elementos de layout principales de WordPress como #wpadminbar o #wpbody-content — estos no tienen alternativas ARIA.

Mismo elemento, tres enfoques

// ✅ Preferido: localizador semántico (funciona porque WP renderiza un <button> real)
await page.getByRole("button", { name: "Guardar cambios" }).click();

// ⚠️ Aceptable: ID de prueba que añadiste al markup de tu plugin
await page.getByTestId("save-settings").click();

// ❌ Evitar: selector CSS frágil ligado al markup de WordPress
await page.locator("#submit").click();
Generar localizadores automáticamente

Ejecuta npx playwright codegen localhost:9400/wp-admin/ para abrir un navegador y grabar interacciones. Playwright genera código de localizador mientras haces clic, ayudándote a descubrir qué localizadores semánticos funcionan para cada elemento.

Autoespera y aserciones web-first

Los localizadores de Playwright esperan automáticamente a que los elementos aparezcan, sean visibles y accionables. En la mayoría de casos no necesitas llamadas manuales a waitForSelector.

Aserciones web-first

Las aserciones web-first reintentan automáticamente hasta que se cumpla la condición o expire el timeout. Siempre prefiérelas a las comprobaciones manuales:

// ✅ Aserción web-first (reintenta hasta visible o timeout)
await expect(page.getByText("Configuración guardada")).toBeVisible();

// ❌ Comprobación manual (sin retry — inestable si el elemento aparece con retraso)
expect(await page.getByText("Configuración guardada").isVisible()).toBe(true);

Aserciones suaves

Usa expect.soft() para comprobar varias cosas en una página sin parar en el primer fallo. Todos los fallos aparecen en el informe de pruebas:

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");

Escribir pruebas

Iniciar un servidor Playground

La función runCLI inicia un servidor Playground local y devuelve un objeto con serverUrl (la cadena URL) y server (la instancia del servidor HTTP). Pasa un Blueprint para configurar la instancia de 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",
},
},
],
},
});

Ciclo de vida del servidor: compartido vs. por prueba

Servidor compartido (beforeAll/afterAll) — una instancia Playground sirve todas las pruebas en un bloque describe. Más rápido, pero las pruebas pueden afectarse entre sí:

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
});

Servidor por prueba (beforeEach/afterEach) — cada prueba obtiene una instancia nueva. Más lento, pero totalmente aislado:

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

Usa servidores compartidos cuando las pruebas solo leen estado (comprobando renderizado de páginas). Usa servidores por prueba cuando las pruebas modifican estado (creando entradas, cambiando configuración).

Usar Blueprints como fixtures de prueba

Los Blueprints definen el estado de WordPress que cada escenario de prueba necesita. Aquí tienes patrones comunes:

Instalar un plugin desde wordpress.org

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

Instalar un plugin local

Monta el directorio de tu plugin local en la instancia 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",
},
],
},
});

Esto mapea tu directorio actual al path del plugin dentro de WordPress, luego activa el plugin. Los cambios en tus archivos locales se reflejan inmediatamente. El usuario puede configurar la propiedad autoMount para identificar plugins y temas, pero la propiedad mount proporcionará más control al usuario para establecer diferentes carpetas en el proyecto.

Configurar opciones y crear contenido

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',
]);
`,
},
],
};
consejo

Usa la Playground Step Library o Pootle Playground para prototipar tu configuración de Blueprint visualmente antes de añadirla a tu código de prueba.

Probar páginas del admin de WordPress

Navega a las páginas del admin e interactúa con la interfaz de 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: "Guardar cambios" }).click();

await expect(page.getByText("Configuración guardada")).toBeVisible();
await expect(page.getByLabel("API Key")).toHaveValue("test-key-123");
});

Manejar elementos comunes de la UI del admin

// Descartar avisos del admin de WordPress (WP añade aria-label a los botones descartar)
await page.getByRole("button", { name: "Dismiss this notice" }).first().click();

// Esperar carga de la barra admin — no hay rol ARIA disponible, usar locator
await page.locator("#wpadminbar").waitFor();

// Navegar por el menú admin
await page.getByRole("link", { name: "My Plugin" }).first().click();

Probar el 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();
});

Patrón Page Object Model

El Page Object Model (POM) encapsula las interacciones de página en clases reutilizables. Esto reduce duplicación y facilita el mantenimiento de las pruebas cuando cambia la UI de tu plugin.

// 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: "Guardar cambios" });
this.successNotice = page.getByText("Configuración guardada");
}

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();
}
}

Usa el POM en las pruebas:

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();
});

El proyecto Playground usa este patrón con la clase WebsitePage que proporciona métodos como goto(), wordpress() y getSiteTitle() — encapsulando navegación e interacciones específicas de WordPress.

Probar en diferentes versiones de PHP y WordPress

Las pruebas parametrizadas cubren múltiples combinaciones de versiones sin duplicar código de prueba:

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 propiedad preferredVersions en el Blueprint controla qué versiones de PHP y WordPress usa la instancia Playground. Rangos soportados: PHP 7.4–8.5, WordPress 6.3–6.8+, además de latest, nightly y beta. Para valores de versión PHP con seguridad de tipos, usa el tipo SupportedPHPVersion de @php-wasm/universal.

Ejecutar pruebas en CI/CD

GitHub Actions

Crea .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

Este workflow instala dependencias, descarga Chromium, ejecuta las pruebas y sube el informe HTML como artefacto. La opción --with-deps instala las bibliotecas del sistema que Chromium necesita en Ubuntu.

Fragmentación para CI más rápido

Divide las pruebas en múltiples jobs de CI con la fragmentación incorporada de Playwright:

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

Crea tres jobs paralelos en la matriz de tu workflow, cada uno ejecutando un fragmento diferente. Esto reduce el tiempo total de CI proporcionalmente.

información

Para pruebas manuales de PR junto con pruebas E2E automatizadas, consulta Añadir botones de vista previa de PR con GitHub Actions.

Solución de problemas

Errores de timeout — Aumenta el timeout en playwright.config.ts. El tiempo de arranque de WordPress varía según el entorno. Los runners de CI suelen necesitar 120–180 segundos.

Conflictos de puertos — Deja que Playground asigne puertos automáticamente. No codifiques números de puerto en tu configuración. La propiedad serverUrl devuelve la URL correcta.

Navegador no encontrado — Ejecuta npx playwright install chromium para descargar el binario del navegador. En CI, añade --with-deps para bibliotecas del sistema.

WordPress no carga — Comprueba la sintaxis de tu Blueprint con el esquema Blueprint. Los pasos inválidos fallan silenciosamente en algunos casos.

Las pruebas pasan localmente pero fallan en CI — Los runners de CI tienen menos memoria y CPU. Aumenta timeouts, reduce workers paralelos y asegúrate de workers: 1 en la config.

Depurar pruebas

Cuando una prueba falla, Playwright ofrece varias herramientas para investigar:

Playwright Inspector — recorre las pruebas interactivamente con un depurador incorporado:

npx playwright test --debug

Visor de traces — inspecciona una línea temporal de acciones, snapshots del DOM y solicitudes de red de una prueba fallida. La configuración trace: "on-first-retry" en el config anterior captura traces automáticamente:

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

Modo UI — ejecuta las pruebas en una interfaz visual donde puedes ver, filtrar y reejecutarlas:

npx playwright test --ui

Screenshot en fallo — la configuración screenshot: "only-on-failure" en el config guarda un screenshot cada vez que una prueba falla. Encuentra los screenshots en el directorio test-results/.

consejo

Combina --debug con un archivo de prueba específico para centrar tu investigación: npx playwright test tests/e2e/settings.spec.ts --debug

Próximos pasos