ID: 0x

|

DATE:

Testes E2E com Playwright e WordPress Playground

AUTHOR:

|

READ_TIME: ~5 MIN

Testes end-to-end (E2E) verificam se o seu plugin ou tema WordPress funciona corretamente do ponto de vista do usuário: clicando em botões, preenchendo formulários e navegando em páginas em um navegador real.

Este guia mostra como combinar o Playwright com a CLI do WordPress Playground para escrever testes E2E confiáveis sem precisar de Docker, bancos de dados ou configuração manual complexa.

Nota: Este guia assume que você já tem familiaridade com o desenvolvimento de plugins ou temas WordPress. Para uma introdução ao uso do Playground no seu fluxo de trabalho, veja WordPress Playground para Desenvolvedores de Plugins. Para detalhes de configuração de Blueprints, veja Primeiros Passos com Blueprints.

Pré-requisitos

  • Node.js 20+ e npm.
  • Um plugin/tema do WordPress ou o próprio WordPress para testar.
  • Recomendado: ativar a regra de ESLint @typescript-eslint/no-floating-promises para capturar chamadas assíncronas do Playwright sem o await.

Configuração do Projeto

Instalação de dependências

No diretório raiz do seu plugin ou tema, execute:

npm init -y
npm install --save-dev @playwright/test @wp-playground/cli
npx playwright install chromiumCode language: CSS (css)

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

Configurando 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",
  },
});Code language: JavaScript (javascript)

O WordPress Playground leva um pouco mais de tempo para iniciar do que uma aplicação web comum. O timeout de 120 segundos para testes e 30 segundos para asserções (assertions) compensa o tempo de boot do WordPress e o carregamento das páginas. Definir workers: 1 evita conflitos de porta quando múltiplos testes compartilham um servidor do Playground.

Criando o 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/`);
  // Elementos do core admin do WordPress carecem de ARIA roles — seletores CSS são aceitáveis aqui
  await expect(page.locator("#wpbody-content")).toBeVisible();
  await expect(page).toHaveTitle(/Dashboard/);
});Code language: JavaScript (javascript)

Execute o teste:

npx playwright test

Escolhendo seletores (Locators)

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

Prioridade de Locators (do mais para o menos recomendado):

  1. page.getByRole() — botões, cabeçalhos, links, controles de formulário.
  2. page.getByLabel() — Campos de formulário com labels associadas.
  3. page.getByText() — conteúdo de texto visível.
  4. page.getByTestId() — Elementos com o atributo data-testid que você adiciona ao seu plugin.
  5. page.locator() — seletores CSS ou XPath como último recurso.

Orientação específica para WordPress

O HTML do admin do WordPress não oferece suporte total ao ARIA. Alguns elementos principais (barra de admin, meta boxes) dependem de IDs e de classes CSS, em vez de ARIA roles. No entanto, muitos elementos funcionam bem com seletores semânticos. Isso significa:

  • Use seletores semânticos para botões, cabeçalhos, links, campos de formulário e itens do menu de admin — o WordPress renderiza elementos padrão <button>, <input>, <a> e <h1> que o getByRole e getByLabel conseguem 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 do core do WordPress como #wpadminbar ou #wpbody-content.

O mesmo elemento, três abordagens:

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

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

// ❌ Evite: seletor CSS frágil atrelado à marcação do WordPress
await page.locator("#submit").click();Code language: JavaScript (javascript)

Dica (Gere locators automaticamente): Execute npx playwright codegen localhost:9400/wp-admin/ para abrir um navegador e gravar suas interações. O Playwright gera o código do seletor conforme você clica, ajudando a descobrir quais seletores semânticos funcionam para cada elemento.

Auto-waiting e asserções Web-first

Os locators do Playwright aguardam automaticamente que os elementos apareçam, se tornem visíveis e acionáveis. Você não precisa de chamadas manuais para waitForSelector Na maioria dos casos.

Web-first assertions

As asserções web-first tentam novamente de forma automática até que a condição seja atendida ou o timeout expire. Sempre prefira elas em vez de verificações manuais:

// ✅ Web-first assertion (tenta novamente até ficar visível ou dar timeout)
await expect(page.getByText("Settings saved")).toBeVisible();

// ❌ Verificação manual (não tenta novamente — falha se o elemento aparecer após um pequeno atraso)
expect(await page.getByText("Settings saved").isVisible()).toBe(true);Code language: JavaScript (javascript)

Aviso: Evite page.waitForTimeout() — isso adiciona atrasos fixos que tornam os testes lentos e instáveis. Use uma asserção web-first ou locator.waitFor() para aguardar uma condição específica.

Soft assertions

Use expect.soft() para verificar várias coisas em uma página sem interromper o teste na primeira falha. Todas as falhas aparecerão no relatório:

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");Code language: CSS (css)

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",
        },
      },
    ],
  },
});Code language: JavaScript (javascript)

Ciclo de vida do servidor: compartilhado vs. por teste

Servidor compartilhado (beforeAll/afterAll) — uma única instância do Playground atende a todos os testes em um bloco describe. É mais rápido, mas os testes podem interferir uns nos outros:

test.describe("Plugin settings", () => {
  test.beforeAll(async () => {
    cli = await runCLI({ command: "server", blueprint });
  });
  test.afterAll(async () => {
    await cli?.server?.close();
  });
  // Os testes compartilham a mesma instância do WordPress
});Code language: JavaScript (javascript)

Servidor por teste (beforeEach/afterEach): cada teste recebe uma instância limpa. É mais lento, mas totalmente isolado:

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

Use servidores compartilhados quando os testes apenas leem o estado (ex.: verificando se as páginas carregam). Use servidores para teste quando os testes modificam o estado (ex.: criando posts, alterando configurações).

Usando Blueprints como fixtures de teste

Blueprints definem o estado do WordPress de 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",
      },
    },
  ],
};Code language: JavaScript (javascript)

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",
      },
    ],
  },
});Code language: JavaScript (javascript)

Isso mapeia seu diretório atual para o caminho do plugin no WordPress e o ativa. Mudanças nos seus arquivos locais são refletidas imediatamente.

Instalando do GitHub

const blueprint = {
  login: true,
  steps: [
    {
      step: "installPlugin",
      pluginData: {
        resource: "git:directory",
        url: "https://github.com/seu-usuario/seu-plugin",
        ref: "HEAD",
        refType: "refname",
      },
    },
  ],
};Code language: JavaScript (javascript)

Definindo opções e criando conteúdo

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

Dica: Use o Blueprints builder para prototipar sua configuração de Blueprint visualmente antes de adicioná-la ao código de teste.

Testando páginas do admin do WordPress

Navegue até as páginas administrativas e interaja com a UI 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");
});Code language: JavaScript (javascript)

Lidando com elementos comuns da UI do admin

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

// Esperar carregar a admin bar — sem ARIA role disponível, use o locator por ID
await page.locator("#wpadminbar").waitFor();

// Navegar via menu lateral
await page.getByRole("link", { name: "My Plugin" }).first().click();Code language: JavaScript (javascript)

Testando o front-end

test("plugin shortcode renders on front end", async ({ page }) => {
  // Navega para uma página que contém o shortcode
  await page.goto(`${cli.serverUrl}/?p=2`);

  // Recomendado: adicionar data-testid="my-plugin-widget" na marcação do seu plugin
  await expect(page.getByTestId("my-plugin-widget")).toBeVisible();
  await expect(page.getByTestId("my-plugin-widget")).toContainText(
    "Conteúdo esperado"
  );
});

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

  await expect(page.getByRole("heading", { level: 1 })).toContainText("Post de Teste");
  await expect(page.getByText("Olá Mundo", { exact: true })).toBeVisible();
});Code language: JavaScript (javascript)

Padrão Page Object Model (POM)

O Page Object Model encapsula as interações da página em classes reutilizáveis. Isso reduz a 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();
  }
}Code language: JavaScript (javascript)

Usando 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();
});Code language: JavaScript (javascript)

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

Testando entre versões de PHP e WordPress

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

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`
      );
      await expect(page.locator(".error")).not.toBeVisible();
      await expect(page.locator("#wpbody-content")).toBeVisible();
    });

    test.afterAll(async () => {
      await versionCli?.server?.close();
    });
  });
}Code language: JavaScript (javascript)

A propriedade preferredVersions no Blueprint, controlam-se quais versões de PHP e WordPress a instância do Playground utiliza. Faixas suportadas: PHP 7.0–8.4, WordPress 6.3–6.8+, além de latest, nightly e beta.

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: Install Playwright browsers
        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: 30Code language: JavaScript (javascript)

Sharding para um CI mais rápido

Divida os testes em múltiplos jobs com o sharding nativo do Playwright:

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

Isso reduz drasticamente o tempo total do CI ao rodar os testes em paralelo.

Complementar: Testes server-side com Vitest

Testes E2E verificam o comportamento visual no navegador. Para validação de REST API, endpoints customizados e comportamento puramente no servidor.

  • Use Playwright para: “A página de configurações salva corretamente?”
  • Use Vitest para: “A REST API retorna os dados corretos?”

Ambas as ferramentas compartilham a mesma configuração com runCLI e se complementam.

Resolução de Problemas (Troubleshooting)

  • Erros de Timeout — Aumente o timeout no playwright.config.ts. O tempo de boot do WordPress varia conforme o ambiente. Runners de CI costumam precisar de 120–180 segundos.
  • Conflitos de Porta — Deixe o Playground atribuir as portas automaticamente. Não use portas fixas no código; a propriedade serverUrl retornará sempre a URL correta.
  • Navegador não encontrado — Execute npx playwright install chromium para baixar o binário do navegador. No CI, use o sufixo --with-deps.
  • WordPress não carrega — Verifique a sintaxe do seu Blueprint no Blueprint schema. Alguns erros de passo falham silenciosamente.

Depurando Testes (Debugging)

Quando um teste falha, o Playwright oferece ferramentas poderosas:

  • Playwright Inspector — Execute os testes passo a passo: npx playwright test --debug.
  • Trace viewer — Inspecione a linha do tempo das ações, snapshots do DOM e requisições de rede: npx playwright show-trace test-results/arquivo-do-teste/trace.zip.
  • UI mode — Execute testes em uma interface visual completa: npx playwright test --ui.

ENCODING: UTF-8

|

CHMOD: 644

// RELATED_ENTRIES

NEXT_READS

> cat ./comments.log

LOADING_ENTRIES…


> write ./comments.log –append

Deixe um comentário

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *