Receber por email
← Voltar para o catálogo

e2e-playwright

Battle-tested Playwright E2E testing patterns for Next.js/React apps. Use when writing, running, debugging, or fixing Playwright tests. Also triggers on 'e2e', 'end-to-end', 'playwright', 'browser test', 'UI test', 'integration test with browser', 'flaky test', 'test keeps failing'. Covers locators, assertions, fixtures, auth, network mocking, flaky test diagnosis, Next.js-specific patterns, and d

11estrelas
Atualizado há 6 meses

Ver no GitHub ↗

Como adicionar

/plugin marketplace add burhankhatri/e2e-testing

O comando exato pode variar conforme o repositório. Confira o README no GitHub.

Para o autor da skill

Cole no README do seu repo

Mostra que sua skill está catalogada na Skillteca, gera backlink e tráfego rastreável.

[![Skillteca](https://www.skillteca.com.br/api/badge/e2e-playwright/svg)](https://www.skillteca.com.br/skills/e2e-playwright)

Skills relacionadas

Ver todas de Desenvolvimento →

Alerta por categoria

Receba novas skills de Desenvolvimento toda segunda

1 email curto, só com as skills novas de Desenvolvimento. 4 minutos de leitura, sem spam, cancela com 1 clique.

Você confirma o email no primeiro envio. Sem spam. Cancela com 1 clique.

Ler descrição completa

Playwright E2E Testing

Production-tested patterns from the TestDino Playwright Skill. Every pattern includes when (and when not) to use it.

Golden Rules

  1. getByRole() over CSS/XPath — resilient to markup changes, mirrors how users see the page
  2. Never page.waitForTimeout() — use expect(locator).toBeVisible() or page.waitForURL()
  3. Web-first assertions — expect(locator) auto-retries; expect(await locator.textContent()) does NOT
  4. Isolate every test — no shared state, no execution-order dependencies
  5. baseURL in config — zero hardcoded URLs in tests
  6. Retries: 2 in CI, 0 locally — surface flakiness where it matters
  7. Traces: 'on-first-retry' — rich debugging artifacts without CI slowdown
  8. Fixtures over globals — share state via test.extend(), not module-level variables
  9. One behavior per test — multiple related expect() calls are fine
  10. Mock external services only — never mock your own app; mock third-party APIs, payment gateways, email

Deep dives available in references/ directory — read them when working on the relevant topic.

Feature Tests vs Smoke Tests

Not all E2E tests are equal. Know what tier you're writing.

| Tier | What it tests | Example | Sufficient for feature coverage? | |------|--------------|---------|----------------------------------| | Smoke | Page loads, no 404, no crash | goto('/canvas'); expect(heading).toBeVisible() | NO — baseline only | | Feature | User completes a real workflow | Drag entry to project → rule created → future entries auto-link | YES — this is the goal | | Navigation | Links route correctly, active states work | Click "Canvas" in sidebar → URL is /canvas → heading visible | Required when nav changes |

The rule: Every feature shipped MUST have at least one tier-2 (feature) E2E test. Smoke tests are free but DO NOT count toward feature coverage.

Ask yourself: "If someone broke this feature tomorrow, would my E2E tests catch it?" If the answer is "only if they deleted the entire page" — you wrote smoke tests, not feature tests.

Navigation Tests — Required When Nav Changes

When you add or modify navigation (sidebar items, mobile tab bar, header links, route changes), you MUST write tests that verify:

  1. Nav item is visible at the correct viewport (desktop sidebar, mobile tab bar)
  2. Clicking it navigates to the correct URL
  3. Destination page renders its primary content (not just "no 404")
  4. Active/selected state highlights correctly

Desktop + Mobile navigation test template:

import { test, expect } from '@playwright/test';

test.describe('Navigation — Desktop', () => {
  test.use({ viewport: { width: 1280, height: 800 } });

  test('sidebar contains Canvas link and navigates correctly', async ({ page }) => {
    await page.goto('/');
    const sidebar = page.getByRole('navigation');
    const canvasLink = sidebar.getByRole('link', { name: 'Canvas' });
    await expect(canvasLink).toBeVisible();
    await canvasLink.click();
    await page.waitForURL('/canvas');
    await expect(page.getByRole('heading', { name: 'Canvas' })).toBeVisible();
  });
});

test.describe('Navigation — Mobile', () => {
  test.use({ viewport: { width: 375, height: 812 } });

  test('mobile tab bar contains Canvas and navigates correctly', async ({ page }) => {
    await page.goto('/');
    const tabBar = page.getByRole('navigation', { name: /mobile|tab/i });
    const canvasTab = tabBar.getByRole('link', { name: 'Canvas' });
    await expect(canvasTab).toBeVisible();
    await canvasTab.click();
    await page.waitForURL('/canvas');
    await expect(page.getByRole('heading', { name: 'Canvas' })).toBeVisible();
  });
});

Adapt names/selectors to the actual app. The structure is: find nav → find link → click → verify URL → verify content.

Next.js Config (App Router + Pages Router)

// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
  testDir: './tests/e2e',
  fullyParallel: true,
  forbidOnly: !!process.env.CI,
  retries: process.env.CI ? 2 : 0,
  workers: process.env.CI ? '50%' : undefined,
  reporter: process.env.CI ? 'html' : 'list',

  use: {
    baseURL: 'http://localhost:3000',
    trace: 'on-first-retry',
    screenshot: 'on',
    video: 'retain-on-failure',
  },

  expect: {
    toHaveScreenshot: {
      maxDiffPixelRatio: 0.01,
      animations: 'disabled',
    },
  },

  projects: [
    { name: 'chromium', use: { ...devices['Desktop Chrome'] } },
    { name: 'mobile', use: { ...devices['iPhone 14'] } },
  ],

  webServer: {
    command: process.env.CI
      ? 'npm run build && npm run start'  // production build in CI
      : 'npm run dev',                    // dev server locally
    url: 'http://localhost:3000',
    reuseExistingServer: !process.env.CI,
    timeout: 120_000,
    env: {
      NODE_ENV: process.env.CI ? 'production' : 'test',
    },
  },
});

Environment variables: Next.js loads .env.test automatically when NODE_ENV=test. Use .env.test for non-secret test config (committed), .env.test.local for secrets (gitignored).

Gitignore additions:

.env*.local
playwright-report/
playwright/.auth/
test-results/
blob-report/

Do NOT gitignore screenshot baselines. The *.spec.ts-snapshots/ directories created by toHaveScreenshot() MUST be committed — they are the source of truth for visual regression tests. Only ephemeral artifacts (test-results/, playwright-report/) should be ignored.

Locators — Priority Order

Use the first one that works:

page.getByRole('button', { name: 'Submit' })         // 1. Role (ALWAYS preferred)
page.getByLabel('Email address')                      // 2. Label (form fields)
page.getByText('Welcome back')                        // 3. Text (non-interactive content)
page.getByPlaceholder('Search...')                     // 4. Placeholder
page.getByAltText('Company logo')                     // 5. Alt text (images)
page.getByTitle('Close dialog')                       // 6. Title attribute
page.getByTestId('checkout-summary')                  // 7. Test ID (last resort)
page.locator('css=.legacy-widget')                    // 8. CSS/XPath (absolute last resort)

Role locator cheat sheet:

// Buttons — matches <button>, <input type="submit">, role="button"
page.getByRole('button', { name: 'Save changes' })

// Links — matches <a href>
page.getByRole('link', { name: 'View profile' })

// Headings — use level to target h1-h6
page.getByRole('heading', { name: 'Dashboard', level: 1 })

// Text inputs — by accessible name (label)
page.getByRole('textbox', { name: 'Email' })

// Checkboxes and radios
page.getByRole('checkbox', { name: 'Remember me' })
page.getByRole('radio', { name: 'Monthly billing' })

// Dropdowns — <select> elements
page.getByRole('combobox', { name: 'Country' })

// Navigation landmarks
page.getByRole('navigation', { name: 'Main' })

// Dialogs
page.getByRole('dialog', { name: 'Confirm deletion' })

// Exact matching — prevents "Log" matching "Log out"
page.getByRole('button', { name: 'Log', exact: true })

For deeper locator strategy guidance, read references/locators-deep-dive.md

Assertions — Web-First vs Non-Retrying

Web-first (auto-retry) — ALWAYS prefer:

await expect(page.getByRole('heading')).toBeVisible();
await expect(page.getByRole('heading')).toHaveText('Dashboard');
await expect(page.getByRole('listitem')).toHaveCount(5);
await expect(page.getByRole('button')).toBeEnabled();
await expect(page.getByLabel('Name')).toHaveValue('Jane');
await expect(page.getByTestId('card')).toHaveClass(/active/);
await expect(page.getByRole('checkbox')).toBeChecked();
await expect(page.getByRole('dialog')).not.toBeVisible();

Non-retrying — only for already-resolved values:

const title = await page.
Compartilhar X LinkedIn