{"post":{"slug":"how-to-write-e2e-tests-2026","title":"How to write e2e tests in 2026","excerpt":"End-to-end tests catch what unit tests miss — broken flows, real browser quirks, and integration failures. This guide covers Playwright setup, writing reliable tests, and running them in CI.","category":"code","date":"March 18, 2026","author":"ByteLedger Team","content":"<span class=\"dropcap\">E</span>nd-to-end tests are the only tests that catch the class of bugs where every unit works but the whole thing is broken — a mis-wired form submit, a redirect that dropped a query param, a payment that silently failed. In 2026, Playwright is the tool the industry converged on, and it's genuinely good. This guide gets you from zero to a reliable, CI-running e2e suite.\n\n## What changed in 2026\n\n- **Playwright 1.4x** added AI-assisted locator generation in the VS Code extension — you point, it writes a resilient `getByRole` locator.\n- **Component testing** (React/Vue/Svelte in a real browser) stabilized, blurring the line between integration and e2e.\n- **Trace viewer got a network panel** — you can inspect every XHR/fetch call in a failing trace without a proxy.\n- **Cypress 14** exists but the ecosystem momentum is clearly with Playwright; most new projects start on Playwright.\n- **`@playwright/test` ships built-in `expect`** with web-specific matchers, removing the need for extra assertion libraries.\n\n## Install\n\n```bash\nnpm init playwright@latest\n# Installs @playwright/test, creates playwright.config.ts, adds example tests\n```\n\nOr manually:\n\n```bash\nnpm install -D @playwright/test\nnpx playwright install --with-deps chromium firefox webkit\n```\n\n## Configure\n\n```ts\n// playwright.config.ts\nimport { defineConfig, devices } from '@playwright/test'\n\nexport default defineConfig({\n  testDir: './e2e',\n  fullyParallel: true,\n  retries: process.env.CI ? 2 : 0,\n  reporter: [['html'], ['list']],\n  use: {\n    baseURL: 'http://localhost:5173',\n    trace: 'on-first-retry',\n    screenshot: 'only-on-failure',\n  },\n  projects: [\n    { name: 'chromium', use: { ...devices['Desktop Chrome'] } },\n    { name: 'firefox',  use: { ...devices['Desktop Firefox'] } },\n    { name: 'webkit',   use: { ...devices['Desktop Safari'] } },\n  ],\n  webServer: {\n    command: 'npm run dev',\n    url: 'http://localhost:5173',\n    reuseExistingServer: !process.env.CI,\n  },\n})\n```\n\n## Write a test\n\n```ts\n// e2e/checkout.spec.ts\nimport { test, expect } from '@playwright/test'\n\ntest('user can complete checkout', async ({ page }) => {\n  await page.goto('/products/widget')\n  await page.getByRole('button', { name: /add to cart/i }).click()\n  await page.getByRole('link', { name: /cart/i }).click()\n  await expect(page.getByText('1 item')).toBeVisible()\n\n  await page.getByRole('button', { name: /checkout/i }).click()\n  await page.getByLabel('Email').fill('test@example.com')\n  await page.getByRole('button', { name: /place order/i }).click()\n\n  await expect(page).toHaveURL(/\\/confirmation/)\n  await expect(page.getByRole('heading', { name: /order confirmed/i })).toBeVisible()\n})\n```\n\n## Page Object Model\n\nFor large suites, POM keeps tests readable and DRY:\n\n```ts\n// e2e/pages/LoginPage.ts\nimport { type Page, type Locator } from '@playwright/test'\n\nexport class LoginPage {\n  readonly emailInput: Locator\n  readonly passwordInput: Locator\n  readonly submitButton: Locator\n\n  constructor(private page: Page) {\n    this.emailInput    = page.getByLabel('Email')\n    this.passwordInput = page.getByLabel('Password')\n    this.submitButton  = page.getByRole('button', { name: /sign in/i })\n  }\n\n  async login(email: string, password: string) {\n    await this.page.goto('/login')\n    await this.emailInput.fill(email)\n    await this.passwordInput.fill(password)\n    await this.submitButton.click()\n  }\n}\n```\n\n## Locator quality table\n\n| Locator type | Resilience | Use when |\n| --- | --- | --- |\n| `getByRole` | Highest | Interactive elements |\n| `getByLabel` | High | Form fields |\n| `getByText` | Medium-high | Unique text content |\n| `getByTestId` | High | No semantic role available |\n| `locator('css')` | Low | Last resort only |\n| `locator('xpath')` | Very low | Never |\n\n## How to pick test scope\n\n| Scenario | Use |\n| --- | --- |\n| Pure function logic | Unit test (Vitest) |\n| Component render + interaction | Integration test |\n| Full user journey (login → action → result) | E2e (Playwright) |\n| Third-party widget integration | E2e |\n| API contract | API test (Playwright request fixture) |\n\n## CI setup\n\n```yaml\n# .github/workflows/e2e.yml\n- name: Install Playwright browsers\n  run: npx playwright install --with-deps\n\n- name: Run e2e tests\n  run: npx playwright test\n\n- name: Upload test report\n  if: always()\n  uses: actions/upload-artifact@v4\n  with:\n    name: playwright-report\n    path: playwright-report/\n```\n\n## Common mistakes\n\n**Relying on CSS class names** — classes change with refactors. Always prefer ARIA roles or labels.\n\n**No `webServer` config** — tests that assume the dev server is running will pass locally and fail in CI. Use `webServer` in the config to start it automatically.\n\n**Sleeping instead of asserting** — `await page.waitForTimeout(2000)` is a flakiness trap. Replace with `await expect(locator).toBeVisible()` which auto-waits up to the configured timeout.\n\n**One giant spec file** — split by feature domain; Playwright runs files in parallel so splitting speeds up the suite.\n\n## What to skip\n\n- **Testing third-party auth redirects** — mock them with Playwright's `route` API or use a test-specific auth bypass.\n- **Visual regression for every page** — pixel diffs are expensive to maintain; limit to critical brand-sensitive views.\n- **Running e2e in watch mode locally** — the feedback loop is too slow; run unit tests in watch mode and e2e on demand.\n\n## FAQ\n\n**Playwright vs Cypress in 2026?**\nPlaywright is faster (true parallelism across browsers), has a better trace debugger, and supports multiple browser engines. Cypress still has a friendlier UI for beginners. New projects should default to Playwright.\n\n**How do I handle authentication state?**\nUse `storageState` — log in once, save cookies/localStorage to a file, and load that state in every test that needs it. No re-login per test.\n\n**What about mobile testing?**\nUse `devices` in the projects config (`devices['iPhone 15']`). Playwright emulates device viewport, user-agent, and touch events.\n\n**How many e2e tests is too many?**\nA rough guideline: 10–30 critical journey tests run in under 3 minutes. If your suite exceeds 10 minutes, you have too many e2e tests and not enough unit/integration tests.\n\n## Where to go next\n\n- [How to set up Vitest in 2026](/blog/how-to-set-up-vitest-2026)\n- [How to set up Storybook in 2026](/blog/how-to-set-up-storybook-2026)\n- [How to configure Vite in 2026](/blog/how-to-configure-vite-2026)","hue":316,"metaTitle":"How to Write E2E Tests in 2026 with Playwright","metaDescription":"Complete 2026 guide to writing reliable end-to-end tests with Playwright. Setup, locators, page objects, CI integration, and flakiness fixes.","keywords":["how to write e2e tests 2026","playwright tutorial 2026","end-to-end testing 2026","playwright vs cypress 2026","playwright page object model","e2e testing best practices 2026","playwright ci setup","playwright typescript 2026","flaky test fixes playwright","playwright locators guide"],"tldr":["**Playwright is the default in 2026** — it runs Chromium, Firefox, and WebKit in one suite with a clean async API.","**Locators over selectors** — `getByRole`, `getByLabel`, and `getByText` survive UI refactors better than CSS selectors.","**Auto-waiting removes most flakiness** — Playwright retries assertions until the element is ready; explicit `waitFor` is rarely needed.","**Trace viewer** records every step, screenshot, and network call so failures are reproducible without re-running locally.","**Skip:** testing every edge case with e2e — unit tests are 10× cheaper; e2e should cover only critical user journeys."],"readTime":"4 min read","wordCount":897},"related":[{"slug":"how-to-write-integration-tests-2026","title":"How to write integration tests in 2026","excerpt":"Integration tests catch the bugs unit tests miss by exercising real component boundaries. Here is a practical 2026 guide with patterns that actually ship.","category":"code","date":"March 2, 2026","author":"ByteLedger Team","readTime":"5 min read","hue":305},{"slug":"unit-testing-best-practices-2026","title":"Unit Testing Best Practices for 2026","excerpt":"Good unit tests are fast, independent, and test behavior instead of implementation. Here are the practices that actually hold up in 2026, and the anti-patterns that quietly rot a test suite.","category":"code","date":"July 16, 2026","author":"ByteLedger Team","readTime":"4 min read","hue":290},{"slug":"unit-vs-integration-testing-2026","title":"Unit vs Integration Testing Explained 2026: Where Each Pays Off","excerpt":"Unit tests check one piece in isolation; integration tests check pieces working together. Here is the 2026 guide to the difference, the right balance, and how to avoid slow, brittle test suites.","category":"code","date":"July 1, 2026","author":"ByteLedger Team","readTime":"4 min read","hue":150}],"sameCategory":[{"slug":"bazel-build-explained-2026","title":"Bazel Explained: What Hermetic Builds Actually Cost","excerpt":"Bazel delivers reproducible, precisely cached builds and demands that you describe everything. Here is what you get, what it costs, and who should say no.","category":"code","date":"August 19, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":145},{"slug":"graphql-federation-2026","title":"GraphQL Federation: One Graph, Many Teams","excerpt":"Federation composes independently-owned subgraphs into a single API. Here is what it solves, the operational cost it adds, and when a simpler gateway is enough.","category":"code","date":"August 19, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":300},{"slug":"hermetic-builds-explained-2026","title":"Hermetic Builds Explained: Same Inputs, Same Output","excerpt":"A hermetic build depends only on declared inputs. Here is what that property gives you, the common leaks that break it, and how to move toward it incrementally.","category":"code","date":"August 19, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":130},{"slug":"monorepo-tooling-compared-2026","title":"Monorepo Tooling Compared: What You Actually Need","excerpt":"Monorepo tools solve task orchestration and caching, not repository layout. Here is what each tier provides and how to tell when you have outgrown the simple option.","category":"code","date":"August 19, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":150}],"newer":{"slug":"how-to-write-clean-functions-2026","title":"How to write clean functions in 2026","excerpt":"Clean functions are the single highest-leverage thing you can do for code quality. Here is the 2026 playbook with concrete rules and real examples.","category":"code","date":"March 18, 2026","author":"ByteLedger Team","readTime":"4 min read","hue":318},"older":{"slug":"hsa-vs-ppo-2026","title":"HSA vs PPO in 2026: which health plan actually saves you money","excerpt":"Choosing between an HSA-eligible HDHP and a PPO is one of the highest-value benefits decisions you make each year. Here is how to run the real numbers.","category":"finance","date":"March 18, 2026","author":"ByteLedger Team","readTime":"4 min read","hue":284}}