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.
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-promisespara detectarawaitausente 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.
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.
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):
page.getByRole()— botões, títulos, links, controles de formuláriopage.getByLabel()— inputs de formulário com rótulos associadospage.getByText()— conteúdo de texto visívelpage.getByTestId()— elementos com atributosdata-testidque você adiciona ao seu pluginpage.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>quegetByRoleegetByLabelpodem encontrar. - Use
data-testidpara 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
#wpadminbarou#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();
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',
]);
`,
},
],
};
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.
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.
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/.
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
- Documentação do WordPress Playground CLI — referência completa da CLI
- Documentação do Playwright — guia de escrita de testes e referência da API
- Referência de Blueprints — todos os passos de Blueprint disponíveis
- Adicionando botões de preview de PR — combine testes automatizados com previews manuais de PR