Pular para o conteúdo principal

Testes E2E com Playwright e WordPress Playground

Testes de ponta a ponta verificam se seu plugin ou tema WordPress funciona corretamente da perspectiva do usuário — clicando em botões, preenchendo formulários e navegando em páginas em um navegador real. Este guia mostra como combinar Playwright com o WordPress Playground CLI para escrever testes E2E confiáveis sem Docker, bancos de dados ou configuração manual.

informação

Este guia pressupõe familiaridade com desenvolvimento de plugins ou temas WordPress. Para uma introdução ao uso do Playground no seu fluxo de desenvolvimento, consulte WordPress Playground para Desenvolvedores de Plugins. Para detalhes de configuração do Blueprint, consulte Introdução aos Blueprints.

Pré-requisitos

  • Node.js 20+ e superior
  • Um plugin/tema WordPress ou um site WordPress completo para testar
  • Recomendado: habilite a regra ESLint @typescript-eslint/no-floating-promises para detectar await ausente em chamadas assíncronas do Playwright

Configuração do projeto

Instalar dependências

A partir do diretório raiz do seu plugin ou tema:

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

Isso instala o Playwright como executor de testes, o Playground CLI para criar instâncias do WordPress e o navegador Chromium para execução dos testes.

Configurar o Playwright

Crie um arquivo playwright.config.ts na raiz do seu projeto:

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

O WordPress Playground precisa de mais tempo para iniciar do que uma aplicação web típica. O timeout de teste de 120 segundos e o timeout de asserção de 30 segundos consideram o tempo de inicialização do WordPress e o carregamento das páginas. Definir workers: 1 evita conflitos de porta quando vários testes compartilham um servidor Playground.

Usando baseURL com portas dinâmicas

Por padrão, o Playground usará a porta 9400. Se você quiser selecionar uma porta diferente, passe port: [NOVO_NÚMERO_DE_PORTA] nas opções do runCLI para selecionar uma porta diferente:

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

Em seguida, adicione baseURL: "http://localhost:9500" na seção use acima. Observe que testMatch tem como padrão **/*.spec.ts — personalize se seus arquivos de teste usarem um padrão de nomenclatura diferente.

dica

O projeto WordPress Playground usa timeouts ainda maiores (300s de teste, 60s de asserção) para seus próprios testes. Comece com os valores acima e aumente se seu ambiente de CI for mais lento.

Primeiro arquivo de teste

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

Execute o teste:

npx playwright test

Escolhendo localizadores

O Playwright oferece várias formas de encontrar elementos na página. Prefira localizadores que reflitam como os usuários veem a página, usando seletores CSS apenas quando necessário.

Prioridade de localizadores (do mais ao menos preferido):

  1. page.getByRole() — botões, títulos, links, controles de formulário
  2. page.getByLabel() — inputs de formulário com rótulos associados
  3. page.getByText() — conteúdo de texto visível
  4. page.getByTestId() — elementos com atributos data-testid que você adiciona ao seu plugin
  5. page.locator() — seletores CSS ou XPath como último recurso

Orientação específica para WordPress

No admin do WordPress, alguns elementos principais (barra de administração, meta boxes) usam IDs e classes CSS em vez de funções ARIA. Porém, muitos elementos funcionam bem com localizadores semânticos. Isso significa:

  • Use localizadores semânticos para botões, títulos, links, campos de formulário e itens do menu admin — o WordPress renderiza elementos padrão <button>, <input>, <a> e <h1> que getByRole e getByLabel podem encontrar.
  • Use data-testid para a marcação do seu próprio plugin — você controla o HTML, então adicione atributos testáveis.
  • Use seletores CSS para elementos de layout principais do WordPress como #wpadminbar ou #wpbody-content — estes não têm alternativas ARIA.

Mesmo elemento, três abordagens

Observação: o WordPress Playground usa a interface de administração em inglês por padrão, então os textos dos botões (como "Save Changes") aparecem em inglês.

// ✅ Preferido: localizador semântico (funciona porque o WP renderiza um <button> real)
await page.getByRole("button", { name: "Save Changes" }).click();

// ⚠️ Aceitável: ID de teste que você adicionou à marcação do seu plugin
await page.getByTestId("save-settings").click();

// ❌ Evite: seletor CSS frágil vinculado à marcação do WordPress
await page.locator("#submit").click();
Gerar localizadores automaticamente

Execute npx playwright codegen localhost:9400/wp-admin/ para abrir um navegador e gravar interações. O Playwright gera código de localizador conforme você clica, ajudando a descobrir quais localizadores semânticos funcionam para cada elemento.

Auto-espera e asserções web-first

Os localizadores do Playwright esperam automaticamente que os elementos apareçam, fiquem visíveis e acionáveis. Na maioria dos casos, você não precisa de chamadas manuais a waitForSelector.

Asserções web-first

Asserções web-first repetem automaticamente até a condição ser atendida ou o timeout expirar. Sempre prefira-as em vez de verificações manuais:

// ✅ Asserção web-first (repete automaticamente até visível ou timeout)
await expect(page.getByText("Configurações salvas")).toBeVisible();

// ❌ Verificação manual (sem retry — instável se o elemento aparecer com atraso)
expect(await page.getByText("Configurações salvas").isVisible()).toBe(true);

Asserções suaves

Use expect.soft() para verificar várias coisas em uma página sem parar no primeiro erro. Todas as falhas aparecem no relatório de teste:

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

Escrevendo testes

Iniciando um servidor Playground

A função runCLI inicia um servidor Playground local e retorna um objeto com serverUrl (a string da URL) e server (a instância do servidor HTTP). Passe um Blueprint para configurar a instância do 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 do servidor: compartilhado vs. por teste

Servidor compartilhado (beforeAll/afterAll) — uma instância Playground serve todos os testes em um bloco describe. Mais rápido, mas os testes podem afetar uns aos outros:

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 teste (beforeEach/afterEach) — cada teste recebe uma instância nova. Mais lento, mas totalmente isolado:

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

Use servidores compartilhados quando os testes apenas leem o estado (verificando renderização de páginas). Use servidores por teste quando os testes modificam o estado (criando posts, alterando configurações).

Usando Blueprints como fixtures de teste

Blueprints definem o estado do WordPress que cada cenário de teste precisa. Aqui estão padrões comuns:

Instalando um plugin do wordpress.org

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

Instalando um plugin local

Monte o diretório do seu plugin local na instância do 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",
},
],
},
});

Isso mapeia seu diretório atual para o caminho do plugin dentro do WordPress e, em seguida, ativa o plugin. As alterações nos seus arquivos locais refletem imediatamente. O usuário pode configurar a propriedade autoMount para identificar plugins e temas, mas a propriedade mount proporcionará mais controle ao usuário para definir diferentes pastas no projeto.

Definindo opções e criando conteúdo

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

Use a Playground Step Library ou o Pootle Playground para prototipar sua configuração de Blueprint visualmente antes de adicioná-la ao seu código de teste.

Testando páginas do admin do WordPress

Navegue até as páginas do admin e interaja com a interface do 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: "Save Changes" }).click();

await expect(page.getByText("Settings saved")).toBeVisible();
await expect(page.getByLabel("API Key")).toHaveValue("test-key-123");
});

Lidando com elementos comuns da UI do admin

// Dispensar avisos do admin do WordPress (o WP adiciona aria-label aos botões de dispensar)
await page.getByRole("button", { name: "Dismiss this notice" }).first().click();

// Aguardar o carregamento da barra admin — sem função ARIA disponível, use locator
await page.locator("#wpadminbar").waitFor();

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

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

Padrão Page Object Model

O Page Object Model (POM) encapsula as interações da página em classes reutilizáveis. Isso reduz duplicação e facilita a manutenção dos testes quando a UI do seu plugin muda.

// 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: "Save Changes" });
this.successNotice = page.getByText("Settings saved");
}

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

Use o POM nos testes:

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

O projeto Playground usa esse padrão com a classe WebsitePage que fornece métodos como goto(), wordpress() e getSiteTitle() — encapsulando navegação e interações específicas do WordPress.

Testando em diferentes versões de PHP e WordPress

Testes parametrizados cobrem múltiplas combinações de versões sem duplicar código de teste:

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

A propriedade preferredVersions no Blueprint controla quais versões de PHP e WordPress a instância Playground usa. Intervalos suportados: PHP 7.4–8.5, WordPress 6.3–6.8+, além de latest, nightly e beta. Para valores de versão PHP com segurança de tipos, use o tipo SupportedPHPVersion de @php-wasm/universal.

Executando testes em CI/CD

GitHub Actions

Crie .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 dependências, baixa o Chromium, executa os testes e faz upload do relatório HTML como artefato. A flag --with-deps instala as bibliotecas do sistema que o Chromium precisa no Ubuntu.

Fragmentação para CI mais rápido

Divida os testes em vários jobs de CI com a fragmentação integrada do Playwright:

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

Crie três jobs paralelos na matriz do seu workflow, cada um executando um fragmento diferente. Isso reduz o tempo total de CI proporcionalmente.

informação

Para testes manuais de PR junto com testes E2E automatizados, consulte Adicionando botões de preview de PR com GitHub Actions.

Solução de problemas

Erros de timeout — Aumente o timeout no playwright.config.ts. O tempo de inicialização do WordPress varia conforme o ambiente. Runners de CI frequentemente precisam de 120–180 segundos.

Conflitos de porta — Deixe o Playground atribuir portas automaticamente. Não codifique números de porta na sua configuração. A propriedade serverUrl retorna a URL correta.

Navegador não encontrado — Execute npx playwright install chromium para baixar o binário do navegador. No CI, adicione --with-deps para bibliotecas do sistema.

WordPress não carrega — Verifique a sintaxe do seu Blueprint com o esquema Blueprint. Passos inválidos podem falhar silenciosamente em alguns casos.

Testes passam localmente mas falham no CI — Runners de CI têm menos memória e CPU. Aumente os timeouts, reduza workers paralelos e garanta workers: 1 na config.

Depurando testes

Quando um teste falha, o Playwright oferece várias ferramentas para investigar:

Playwright Inspector — percorra os testes interativamente com um depurador integrado:

npx playwright test --debug

Visualizador de trace — inspecione uma linha do tempo de ações, capturas de DOM e requisições de rede de um teste que falhou. A configuração trace: "on-first-retry" no config acima captura traces automaticamente:

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

Modo UI — execute os testes em uma interface visual onde você pode assistir, filtrar e reexecutá-los:

npx playwright test --ui

Screenshot em caso de falha — a configuração screenshot: "only-on-failure" no config salva um screenshot sempre que um teste falha. Encontre os screenshots no diretório test-results/.

dica

Combine --debug com um arquivo de teste específico para focar sua investigação: npx playwright test tests/e2e/settings.spec.ts --debug

Próximos passos