# Playwright Best Practices for a Suite That's Starting to Hurt

Written by Dominik Szahidewicz
Reviewed by Bartek Krzyzanowski
Published: 2026-09-28
Updated: 2026-09-29

\[TABLE\_OF\_CONTENTS]

Fifty tests ran clean. Four hundred tests fight each other — a flaky retry here, a test automation issue there. `beforeEach` nobody wants to touch, an auth strategy three people on the team quietly disagree about.

Most "Playwright best practices" posts hand you fifteen rules with equal weight and move on. This guide (targeting Playwright 1.60.x) covers the same ground — locators, assertions, data setup, CI — but it tells you which of these you can get slightly wrong and fix next sprint, and which ones are worth getting right the first time.

## Test architecture as an irreversible decision - Best Practices for Playwright

Here's the thing nobody puts in the checklist: your folder structure, your auth strategy, and your abstraction layer (Page Object Model, [component objects](/blog/software-testing/playwright-page-object-model/), or nothing at all) aren't three items on a list of fifteen. They're the foundation everything else gets built on.

"Irreversible" isn't dramatic framing. Unwinding a bad auth strategy at 400 tests doesn't mean refactoring a function — it means touching most of the suite, one file at a time, while the team keeps shipping features on top of it. Same with abstraction: if the app outgrew Page Object Model eighteen months ago and nobody noticed, the fix isn't a weekend project.

Someone made this call. Usually it was whoever set up the repo first — the person with the most context and the least time to document why. They picked an auth approach because it worked for the five tests that existed that week. They reached for POM because it's what the last project used. Reasonable calls, made fast, under no pressure to justify them.

Then that person moves teams, or leaves, and the decision stays. Nobody currently on the team remembers _why_ auth state is shared across all tests, or why there's a `PageObjects are essential for organizing test scripts in a structured manner.` folder nobody's added to in a year. The suite keeps the shape of a decision nobody present agreed to.

None of this means the usual rules don't matter — they do, and we'll get specific about all of them below. It means: read the rest of this with an eye for which ones you can adjust next sprint, and which ones you're actually choosing for the next two years.

## Isolation Is Free in the Browser. Not in Your Database.

Playwright gives you browser isolation for free, which enhances the reliability of your test automation. Every test runs in its own context — separate cookies, separate localStorage, separate cache. Two tests can't accidentally read each other's session state even if you never think about it.

That's the easy half. The half that actually causes 2 am flake is the one Playwright can't isolate for you: your backend.

 

```css
// test-isolation.spec.ts
import { test, expect } from '@playwright/test';

test.beforeEach(async ({ page }) => {
  await page.goto('/login');
  await page.getByLabel('Email').fill('[email protected]');
  await page.getByLabel('Password').fill('securepassword');
  await page.getByRole('button', { name: 'Sign in' }).click();
  await expect(page.getByText('My Account')).toBeVisible();
});
```

**Browser context:** isolated automatically. Database: not isolated unless you make it so. Two tests hitting the same seeded account, writing to the same rows, running in parallel — that's a race condition. It looks exactly like flake in your CI logs. Nobody debugging it thinks "isolation problem." They think "flaky test," retry it, and move on. The actual cause never gets fixed because it never gets diagnosed.

The fix is a data factory — generate fresh, unique data per test instead of reusing fixtures:

 

```css
// test-data-factory.ts
export class TestDataFactory {
  constructor(private request: APIRequestContext, private baseUrl: string) {}

  async createUser(overrides?: Partial<{ email: string; name: string }>) {
    const response = await this.request.post(`${this.baseUrl}/api/test/users`, {
      data: {
        email: `user-${Date.now()}-${Math.random().toString(36).slice(2)}@test.com`,
        password: 'test-password-123',
        firstName: 'Test',
        lastName: 'User',
        ...overrides,
      },
    });
    return response.json();
  }
}
```

Fresh data per test closes off the collision. It doesn't close off the mess a test leaves behind. A test that creates an order should delete that order — or your suite passes clean today and starts failing next week for no code reason anyone can find.

 

```css
// cleanup.spec.ts
test.afterEach(async ({ request }) => {
  await request.delete(`/api/test/orders?runId=${process.env.RUN_ID}`);
});
```

This is the isolation principle in full, not two separate tips: contexts isolated, data isolated, state reset. Skip the backend half and you haven't actually isolated your tests — you've isolated the part that was already isolated by default and left the part that actually breaks things untouched.

## Use Playwright Selectors and Assertions That Don't Fight the DOM

A user doesn't know your button is `<button class="btn btn--primary checkout-cta" data-v-3f2a1>`. They see a button that says "Pay." Assert against the class name and your test breaks every time someone refactors markup the user never notices, affecting the reliability of your test scripts. Assert against what the user sees and it survives.

 

```css
// Brittle — breaks on any markup refactor
await page.locator(".btn.btn--primary.checkout-cta").click();

// Resilient — survives refactors, mirrors the user
await page.getByRole("button", { name: "Pay" }).click();
```

Rule of thumb: if you strip the class attribute and the experience is unchanged, your test shouldn't notice either.

Playwright ranks locators by how closely they match what a user actually perceives. Work down this list, and only drop a rung when you have to:

1. `getByRole()` — the accessible role and name
2. `getByLabel()` — form fields, by their label
3. `getByPlaceholder()` — inputs, by placeholder
4. `getByText()` — non-interactive content
5. `getByTestId()` — an explicit hook, when nothing semantic exists
6. CSS or XPath — last resort, not a starting point

[Role locators](/blog/testing-frameworks/playwright-locators/) alone eliminate more flaky tests than any other single change on this list, because they're tied to what's rendered, not how it's built.

Assertions have the same failure mode. A manual check runs once, at the instant you call it, and fails if the UI is a frame behind:

 

```css
// Bad — checks once, at that exact instant
expect(await page.getByText('Cart').isVisible()).toBe(true);

// Good — retries automatically until true or timeout
await expect(page.getByText('Cart')).toBeVisible();
```

Web-first assertions retry until the condition holds or the timeout expires. That single behavior accounts for a large share of "flaky" tests that were never actually flaky — they were just checked too early, impacting test coverage. Never paper over that with `waitForTimeout()`. If you're tempted to wait, you're describing something you should be asserting instead.

Last piece, and it's cheap: name tests after the failure, not the feature. "Checkout works" tells you nothing when CI goes red at 6pm. "Checkout charges the card and shows the order number" tells you exactly what broke before you've opened a single trace, enhancing test coverage. Wrap multi-step flows in `test.step()` so the trace reads like a sentence and points straight at the action that failed.

Unlike the last section, none of this is contested. It's not a decision your team commits to for two years — it's hygiene you can retrofit one file at a time without a project plan. Get it right because it's easy to get right, not because getting it wrong is expensive.

## Skip the UI to Get to the UI You're Testing

Clicking through a sign-up form before every single test is slow, and it's fragile — the moment someone redesigns that form, every test that walks through it to get somewhere else breaks too, even though the sign-up flow itself was never what you were testing.

Hit the API instead. What takes ten UI interactions becomes one request: this workflow improves [test automation efficiency](/blog/software-testing/test-automation/).

 

```css
// user-seed.spec.ts
test.beforeEach(async ({ request }) => {
  const userResponse = await request.post('/api/test/users', {
    data: {
      email: '[email protected]',
      password: 'secure-password-123',
      firstName: 'Test',
      lastName: 'User',
    },
  });
  const user = await userResponse.json();
});
```

For anything beyond a single fixed user, reach for a factory rather than repeating the same request body across specs — the same `TestDataFactory` pattern from the isolation section works here too, since seeding and isolating unique data are really the same problem approached from two angles.

One practical note: create a `/api/test/*` The endpoint that only exists outside production can be automated for better test execution. It keeps seeding fast and gives you a clean place to layer in cleanup, rather than reusing real signup endpoints that were never designed to be called a few thousand times a day by CI.

This doesn't apply everywhere, and it's worth saying plainly: if the thing you're testing _is_ the sign-up form — validation messages, password rules, the actual UI flow — then walking through it isn't overhead, it's the test. Reserve UI steps for the behavior you're actually proving; let everything upstream of that arrive through the fastest door available.

One distinction worth keeping straight: seeding through the API gets you a fast, known starting point. It doesn't clean up after itself. A test that creates an order via a factory still leaves that order in the database when it's done — seeding solves speed, not state. That's still a cleanup problem, and it's still yours.

## POM, Component Objects, or Neither: Pick Based on Your App, Not Habit

Most Playwright content ducks this question. "It depends" is technically true and completely useless if you're the one deciding it this week, especially when considering the workflow for test automation. Here's an actual stance.

Full-page Page Object Model earns its keep when you have a dedicated automation engineer maintaining the suite as their real job, and a large, stable set of pages that get reused across dozens of specs. If that's your team, POM is fine — it's a known pattern, it's documented everywhere, and the maintenance overhead is justified by the reuse.

That's not most teams running Playwright. If you're a [small engineering team without a dedicated automation owner](https://bugbug.io/blog/test-automation/automation-testing-guide-for-startups-level-1/) — a developer who inherited the test suite, or a QA person covering automation alongside everything else — full-page POM tends to become its own maintenance project. You end up maintaining a second codebase that mirrors your app's page structure, and every UI change means updating two places instead of one.

For that more common case, component objects usually fit better than full-page POM:

 

```css
// components/CartDrawer.ts
export class CartDrawer {
  constructor(private page: Page) {}

  get() {
    return this.page.getByRole('dialog', { name: 'Cart' });
  }

  async removeItem(name: string) {
    await this.get().getByRole('row', { name }).getByRole('button', { name: 'Remove' }).click();
  }
}
```

A modal, a table, a date picker, a nav bar — these repeat across pages more than full pages repeat across specs, and wrapping just those pays for itself faster. You get the same "change it in one place" benefit without committing to model every page in the app up front.

Pair that with fixture composition instead of a growing pile of `beforeEach` hooks:

 

```css
// fixtures.ts
export const test = base.extend<{ cartDrawer: CartDrawer }>({
  cartDrawer: async ({ page }, use) => {
    await use(new CartDrawer(page));
  },
});
```

Fixtures compose — you opt into exactly what a test needs. A shared `beforeEach` runs for every test in the file whether it needs that setup or not, and it's the first place suites quietly slow down as they grow.

**Our stance, plainly:** default to component objects and fixtures. Reach for full-page POM only when a specific page's structure genuinely repeats across enough specs that duplicating it becomes the bigger cost — not because it's the "proper" pattern to start with.

This is one of the two decisions flagged back in the first section. It's not a style preference — it's a maintenance commitment with a real owner and a real cost if that owner leaves. If your team picks one, write down _why_ in a short ADR. Not for process theater — so that eighteen months from now, whoever's touching the suite isn't reverse-engineering the reasoning from git blame.

## Auth Strategy: Shared State Is a Tradeoff, Not a Shortcut

Shared auth state — one `storageState` file, reused across many tests — is fast and simple, and it's fine when your tests genuinely don't collide: read-only flows, tests that don't mutate data tied to that user, low parallelism. Plenty of suites run this way without issue.

 

```css
// playwright.config.ts
export default defineConfig({
  projects: [
    { name: 'setup', testMatch: /.*\.setup\.ts/ },
    {
      name: 'chromium',
      dependencies: ['setup'],
      use: { storageState: './auth.json' },
    },
  ],
});
```

The risk shows up specifically when tests mutate state tied to that shared user and run in parallel. Two tests touching the same account's data at the same time produces exactly the kind of intermittent, hard-to-reproduce failure that gets misdiagnosed as flake instead of a concurrency bug. When that's a real risk for your suite, auth state per worker, per role, or per tenant removes the collision instead of just hoping tests don't step on each other:

 

```css
// auth.setup.ts
setup('authenticate', async ({ page }, testInfo) => {
  await page.goto('/login');
  await page.getByLabel('Email').fill(`worker-${testInfo.workerIndex}@test.com`);
  await page.getByLabel('Password').fill('password');
  await page.getByRole('button', { name: 'Sign in' }).click();
  await page.context().storageState({ path: `./auth-${testInfo.workerIndex}.json` });
});
```

Which one is right depends on what your tests actually do — how much they mutate, how much they run in parallel, how expensive per-worker auth is to set up for your app. There isn't a universal default worth stating here; it's genuinely a call to make deliberately for your specific suite, not import from a blog post.

What matters more than which option you pick is that it's a _decision_, not a default nobody chose. Same as the last section: this is infrastructure, not a config toggle. If your team is sharing auth state today, it's worth confirming that's still true of what the suite tests now — not just what it tested when someone set it up.

## What Breaks at CI Scale (and How to Catch It Before It's a Habit)

Everything above works on your laptop. A few of these problems only show up once the suite is running in CI, on every PR, every day — and by the time you notice, "we have a flaky test" has usually been quietly true for weeks.

**Treat retries as data, not a fix.** A test that fails and passes on retry isn't solved — it's logged. Configure retries in CI, but track which tests actually use them:

 

```css
// playwright.config.ts
export default defineConfig({
  retries: process.env.CI ? 2 : 0,
  reporter: [['html'], ['@testdino/playwright']],
});
```

A test that retries once and passes occasionally is fine to leave alone. A test that retries on every third run is a bug in your suite, and muting it by raising the retry count just delays the day it fails for real and nobody trusts the result.

**Trace failures, not just report them.** A failing assertion with a trace attached tells you what the DOM, network, and console looked like at the moment it broke. A failing assertion with just a red X in CI tells you to go reproduce it locally and hope it happens again.

 

```css
// playwright.config.ts
export default defineConfig({
  use: { trace: 'on-first-retry' },
});
```

**Keep local and CI environments close enough that "works on my machine" is rare, not routine.** Same Node version, same browser binaries, same environment variables where it matters. Every gap between the two is a category of failure that only reproduces in CI, which is the worst place to debug anything.

**Separate secrets and accounts per environment.** Staging tests hitting staging credentials, not a shared set reused across local, CI, and staging. A CI run that accidentally mutates data another environment depends on is its own flavor of the isolation problem from earlier in this piece, just at the environment level instead of the test level.

Sharding helps once the suite is big enough that wall-clock time itself is the problem:

 

```css
# GitHub Actions
strategy:
  matrix:
    shard: [1, 2, 3, 4]
steps:
  - run: npx playwright test --shard=${{ matrix.shard }}/4
```

None of this is exotic. But this is usually the point where the cost of the suite stops being invisible. Someone's now spending real hours — sometimes weeks, over a quarter — just keeping the test infrastructure itself healthy: chasing intermittent retries, diagnosing environment drift, deciding whether a red CI run is a real bug or infra noise. That work is separate from writing tests, and it's easy to not notice it's happening until it's already eating a meaningful slice of someone's time.

## Which of These Should You Actually Write Down?

Not all fifteen-odd things in this piece deserve the same treatment. Some are hygiene — apply them, move on, fix them next sprint if you got them wrong. A few are the structural calls from the opening section, and those are worth an ADR: a short written record of what you chose and why.

**Worth writing down:**

* **Abstraction approach** (component objects vs. full-page POM vs. neither) — because the cost of guessing wrong compounds with every spec written on top of it.
* **Auth strategy** (shared vs. per-worker/role/tenant) — because it depends on specifics of your suite that won't be obvious to whoever's debugging a concurrency failure eighteen months from now without the context you have today.
* **Isolation boundaries** — what "isolated" means for your suite specifically: just browser context, or backend state too, and how cleanup actually happens.

Not because Playwright is complicated. It isn't — the framework gives you clean primitives for all of this. It's because these are infrastructure decisions, made under time pressure, by whoever happened to be setting up the repo first. Writing down the reasoning is cheap. Reconstructing it later from git blame and Slack history isn't.

**Applied without a project plan:**

* Role-based locators over CSS/XPath
* Web-first assertions over manual checks or `waitForTimeout()`
* API-driven test data setup
* Descriptive test names and `test.step()` wrapping
* Tracing on retry, retry tracking, sharding

Get these right because they're easy to get right. Nobody needs a meeting to switch a locator strategy.

Step back and the actual pattern is this: none of the above is a [Playwright](https://bugbug.io/blog/test-automation-tools/open-source-automation-tools/) problem. Playwright gives you good primitives — role locators, web-first assertions, `storageState`, sharding, tracing. What it doesn't give you is a team member whose job is to keep all of it coherent as the suite grows. That's the platform work sitting underneath the testing work: isolation boundaries, auth architecture, CI flake triage, and — the part almost nothing about Playwright itself prepares you for — continuity when the person who made these calls moves on.

That's an ongoing cost, not a one-time setup cost. Worth asking directly: who owns this in your team, specifically, a year from now? For plenty of teams the honest answer is "whoever's around when it breaks" — which works, until it doesn't. If that's the situation you're in, a [managed or governed layer](https://bugbug.io/blog/test-automation/scriptless-test-automation/) on top of the same underlying discipline is a legitimate option to weigh against building and maintaining it yourselves. Not because Playwright's too hard — it isn't — but because platform ownership is a real, recurring line item most growing suites underestimate until it's already costing someone a few hours every week.

## FAQs

`<custom-faq><json>{"name":"faq","children":[{"name":"faq-question","children":[{"name":"heading2","children":[{"attributes":{"bold":true},"data":"What are Playwright best practices?"}]},{"name":"heading2"}]},{"name":"faq-answer","children":[{"name":"paragraph","children":[{"data":"They're the patterns that keep a Playwright test suite reliable as it grows — stable locators, web-first assertions, independent tests, API-driven test data, and centralized reporting in CI. Most are hygiene you can apply to any single test today. A couple — your abstraction layer and auth strategy — are closer to infrastructure decisions worth writing down, because they're expensive to reverse once a test suite grows around them."}]},{"name":"paragraph"},{"name":"paragraph"}]}]}</json>  <faq-question-md>  ### **What are Playwright best practices?**  </faq-question-md>  <faq-answer-md>  They're the patterns that keep a Playwright test suite reliable as it grows — stable locators, web-first assertions, independent tests, API-driven test data, and centralized reporting in CI. Most are hygiene you can apply to any single test today. A couple — your abstraction layer and auth strategy — are closer to infrastructure decisions worth writing down, because they're expensive to reverse once a test suite grows around them.  </faq-answer-md>  </custom-faq>`

``<custom-faq><json>{"name":"faq","children":[{"name":"faq-question","children":[{"name":"heading2","children":[{"attributes":{"bold":true},"data":"What's the most important Playwright best practice?"}]},{"name":"paragraph"}]},{"name":"faq-answer","children":[{"name":"paragraph","children":[{"data":"Day to day, stable locators — "},{"attributes":{"code":true},"data":"getByRole()"},{"data":" over CSS or XPath — remove more flaky tests than any other single change, because they survive markup refactors instead of breaking on them. But the highest-stakes decisions in a scalable test setup aren't selector-level at all. They're architectural: how you handle auth, and whether your abstraction layer matches the shape of your app."}]},{"name":"paragraph"}]}]}</json>  <faq-question-md>  ### **What's the most important Playwright best practice?**  </faq-question-md>  <faq-answer-md>  Day to day, stable locators — `getByRole()` over CSS or XPath — remove more flaky tests than any other single change, because they survive markup refactors instead of breaking on them. But the highest-stakes decisions in a scalable test setup aren't selector-level at all. They're architectural: how you handle auth, and whether your abstraction layer matches the shape of your app.  </faq-answer-md>  </custom-faq>``

`<custom-faq><json>{"name":"faq","children":[{"name":"faq-question","children":[{"name":"heading2","children":[{"attributes":{"bold":true},"data":"Do I need the Page Object Model (POM)?"}]},{"name":"paragraph"}]},{"name":"faq-answer","children":[{"name":"paragraph","children":[{"data":"Only if you have a dedicated automation owner and a large, stable set of pages reused across many test files. For most small teams without that, component objects — wrapping repeated UI pieces like modals, tables, and pickers rather than full pages — cause less maintenance cost than committing to full-page POM upfront. Playwright doesn't require POM; it's a pattern you opt into, not something built-in to how the framework works."}]},{"name":"paragraph"}]}]}</json>  <faq-question-md>  ### **Do I need the Page Object Model (POM)?**  </faq-question-md>  <faq-answer-md>  Only if you have a dedicated automation owner and a large, stable set of pages reused across many test files. For most small teams without that, component objects — wrapping repeated UI pieces like modals, tables, and pickers rather than full pages — cause less maintenance cost than committing to full-page POM upfront. Playwright doesn't require POM; it's a pattern you opt into, not something built-in to how the framework works.  </faq-answer-md>  </custom-faq>`

``<custom-faq><json>{"name":"faq","children":[{"name":"faq-question","children":[{"name":"heading2","children":[{"attributes":{"bold":true},"data":"How do I stop Playwright tests from being flaky?"}]},{"name":"paragraph"}]},{"name":"faq-answer","children":[{"name":"paragraph","children":[{"data":"Most flaky test failures aren't actually random — they're isolation problems (shared test data, shared auth state) or timing problems (manual waits instead of web-first assertions) misdiagnosed as flakiness. Fix isolation at both the browser and backend level, replace manual waits with "},{"attributes":{"code":true},"data":"await expect(...)"},{"data":", and log retries in CI instead of muting them. A test that passes on retry every time is telling you something specific about test reliability, not being unreliable at random."}]},{"name":"paragraph"},{"name":"paragraph"}]}]}</json>  <faq-question-md>  ### **How do I stop Playwright tests from being flaky?**  </faq-question-md>  <faq-answer-md>  Most flaky test failures aren't actually random — they're isolation problems (shared test data, shared auth state) or timing problems (manual waits instead of web-first assertions) misdiagnosed as flakiness. Fix isolation at both the browser and backend level, replace manual waits with `await expect(...)`, and log retries in CI instead of muting them. A test that passes on retry every time is telling you something specific about test reliability, not being unreliable at random.  </faq-answer-md>  </custom-faq>``

``<custom-faq><json>{"name":"faq","children":[{"name":"faq-question","children":[{"name":"heading2","children":[{"attributes":{"bold":true},"data":"How should I structure a Playwright project as the test suite grows?"}]},{"name":"paragraph"}]},{"name":"faq-answer","children":[{"name":"paragraph","children":[{"data":"Group test files by feature, not by test type — a "},{"attributes":{"code":true},"data":"checkout/"},{"data":" folder with everything checkout-related beats a flat folder of 200 files sorted by whether they're \"smoke\" or \"regression.\" Pull repeated setup into fixtures so a UI change touches one file, not fifty. This is one of the structural decisions worth deciding early: a scalable test suite is a folder structure that still makes sense at 500 tests, not just at 50."}]},{"name":"paragraph"}]}]}</json>  <faq-question-md>  ### **How should I structure a Playwright project as the test suite grows?**  </faq-question-md>  <faq-answer-md>  Group test files by feature, not by test type — a `checkout/` folder with everything checkout-related beats a flat folder of 200 files sorted by whether they're "smoke" or "regression." Pull repeated setup into fixtures so a UI change touches one file, not fifty. This is one of the structural decisions worth deciding early: a scalable test suite is a folder structure that still makes sense at 500 tests, not just at 50.  </faq-answer-md>  </custom-faq>``

`<custom-faq><json>{"name":"faq","children":[{"name":"faq-question","children":[{"name":"heading2","children":[{"attributes":{"bold":true},"data":"Does Playwright support cross-browser testing?"}]},{"name":"paragraph"}]},{"name":"faq-answer","children":[{"name":"paragraph","children":[{"data":"Yes — Playwright runs the same test scripts against Chromium, Firefox, and WebKit without separate automation practices per browser. That said, cross-browser coverage adds real execution time to every CI run, so it's worth reserving for the workflows where browser-specific bugs actually show up (rendering, CSS quirks) rather than running your entire suite three times by default."}]},{"name":"paragraph"}]}]}</json>  <faq-question-md>  ### **Does Playwright support cross-browser testing?**  </faq-question-md>  <faq-answer-md>  Yes — Playwright runs the same test scripts against Chromium, Firefox, and WebKit without separate automation practices per browser. That said, cross-browser coverage adds real execution time to every CI run, so it's worth reserving for the workflows where browser-specific bugs actually show up (rendering, CSS quirks) rather than running your entire suite three times by default.  </faq-answer-md>  </custom-faq>`

`<custom-faq><json>{"name":"faq","children":[{"name":"faq-question","children":[{"name":"heading2","children":[{"attributes":{"bold":true},"data":"How do I debug a failing Playwright test?"}]},{"name":"paragraph"}]},{"name":"faq-answer","children":[{"name":"paragraph","children":[{"data":"Match the tool to the failure. UI mode lets you watch a test execute and time-travel through steps. The inspector steps through a single test and lets you try locators live. Trace viewer opens a recorded trace from a CI failure and shows you the DOM, network, and console log at the moment it broke. Turn tracing on for retries specifically — a red CI run should hand you a full trace, not just a stack trace to reproduce blind."}]},{"name":"paragraph"}]}]}</json>  <faq-question-md>  ### **How do I debug a failing Playwright test?**  </faq-question-md>  <faq-answer-md>  Match the tool to the failure. UI mode lets you watch a test execute and time-travel through steps. The inspector steps through a single test and lets you try locators live. Trace viewer opens a recorded trace from a CI failure and shows you the DOM, network, and console log at the moment it broke. Turn tracing on for retries specifically — a red CI run should hand you a full trace, not just a stack trace to reproduce blind.  </faq-answer-md>  </custom-faq>`

`<custom-faq><json>{"name":"faq","children":[{"name":"faq-question","children":[{"name":"heading2","children":[{"attributes":{"bold":true},"data":"How do I manage test data in Playwright?"}]},{"name":"paragraph"}]},{"name":"faq-answer","children":[{"name":"paragraph","children":[{"data":"Seed it through the API instead of walking through the UI to create it — a signup form redesign shouldn't break fifty unrelated tests that only needed a logged-in user to get somewhere else. For anything beyond a single fixed account, a data factory generates unique test data per test and keeps tests independent of each other. Test data management doesn't stop at creation, either — clean up what a test writes, or a suite that passes today fails next week for no code reason anyone can find."}]},{"name":"paragraph"}]}]}</json>  <faq-question-md>  ### **How do I manage test data in Playwright?**  </faq-question-md>  <faq-answer-md>  Seed it through the API instead of walking through the UI to create it — a signup form redesign shouldn't break fifty unrelated tests that only needed a logged-in user to get somewhere else. For anything beyond a single fixed account, a data factory generates unique test data per test and keeps tests independent of each other. Test data management doesn't stop at creation, either — clean up what a test writes, or a suite that passes today fails next week for no code reason anyone can find.  </faq-answer-md>  </custom-faq>`

`<custom-faq><json>{"name":"faq","children":[{"name":"faq-question","children":[{"name":"heading2","children":[{"attributes":{"bold":true},"data":"Can I run Playwright tests in parallel, and does it work in CI?"}]},{"name":"paragraph"}]},{"name":"faq-answer","children":[{"name":"paragraph","children":[{"data":"Playwright runs tests in parallel by default, and sharding splits a suite across multiple CI machines so execution time drops as the suite grows instead of climbing with it. Integrating Playwright with GitHub Actions is a matter of a matrix strategy across shards — a few lines of YAML, not a separate CI setup. The tradeoff: tests in parallel only stay reliable if they're actually isolated from each other, which is why isolation comes before parallelization, not after."}]},{"name":"paragraph"}]}]}</json>  <faq-question-md>  ### **Can I run Playwright tests in parallel, and does it work in CI?**  </faq-question-md>  <faq-answer-md>  Playwright runs tests in parallel by default, and sharding splits a suite across multiple CI machines so execution time drops as the suite grows instead of climbing with it. Integrating Playwright with GitHub Actions is a matter of a matrix strategy across shards — a few lines of YAML, not a separate CI setup. The tradeoff: tests in parallel only stay reliable if they're actually isolated from each other, which is why isolation comes before parallelization, not after.  </faq-answer-md>  </custom-faq>`
