Back to Home

Stabilizing Playwright Tests in CI

The article describes the methodology for stabilizing Playwright tests in CI using Dependency Projects, web-first asserts and proper locator selection. Examples of configuration and code for enterprise projects are provided.

Playwright: Eliminating Flakes in CI with Dependency Projects
Advertisement 728x90

Stabilizing Playwright Tests in CI: Dependency Projects and Web-First Assertions

Chained dependencies in Playwright prevent test execution in unstable environments, saving CI resources and reducing false positives.

First, authentication via API is performed with storageState persistence. Then, a health check verifies API availability. Only after this do the main UI tests run.

Advantages over globalSetup:

Google AdInline article slot
  • Full traces with network requests, screenshots, and console logs.
  • Fail-fast behavior: if setup fails, dependent projects are halted immediately.
  • Isolation: individual stages can be tested using npx playwright test --project=auth-setup.

Configuration in playwright.config.ts:

export default defineConfig({
  projects: [
    {
      name: 'auth-setup',
      testMatch: /.*\.auth\.setup\.ts/,
    },
    {
      name: 'healthcheck',
      testMatch: /.*\.health\.setup\.ts/,
      dependencies: ['auth-setup'],
    },
    {
      name: 'chromium',
      use: { ...devices['Desktop Chrome'] },
      dependencies: ['healthcheck'],
    },
  ],
});

API-based authentication cuts setup time dramatically: a POST request takes 50–100 ms versus 2–5 seconds for UI login.

Example auth-setup test:

Google AdInline article slot
test('setup', async ({ request }) => {
  await request.post('/api/login', { data: { user, pass } });
  await request.storageState({ path: '.auth/user.json' });
});

Dependency graphs eliminate race conditions during parallel test runs.

Choosing Locators Wisely

Locator selection directly impacts test resilience to UI changes. Separate them by purpose: use getByTestId for actions, and getByRole or getByLabel for assertions.

Best practices:

Google AdInline article slot
  • Actions (click, input): page.getByTestId('add-to-cart-button').click() — independent of text or styling.
  • Text/state validation: expect(page.getByRole('heading')).toHaveText('My Account').
  • Input fields: page.getByLabel('Email').fill('[email protected]').
  • Counters: expect(locator).toHaveText('3').

CSS selectors should only be used as fallbacks. Training your team on proper locator usage pays off tenfold at scale.

Web-First Assertions vs Flakiness

Snapshot checks like await loc.isVisible() are prone to race conditions. Web-first assertions with polling deliver stability.

Comparison of Assertions:

| Check | Snapshot (no wait) | Web-First (polling) |

|-------|--------------------|---------------------|

| Visibility | await loc.isVisible() | expect(loc).toBeVisible() |

| Text | await loc.textContent() | expect(loc).toHaveText('Text') |

| Count | await loc.count() | expect(loc).toHaveCount(3) |

| Attribute | await loc.getAttribute('href') | expect(loc).toHaveAttribute('href', '/path') |

| Checkbox | await loc.isChecked() | expect(loc).toBeChecked() |

| State | await loc.isEnabled() | expect(loc).toBeEnabled() |

Polling retries every 100ms until timeout, ignoring temporary UI flickers.

Hydration and force: true

Hydration mismatch occurs in SSR/SSG apps when server-rendered markup exists but JS handlers aren’t attached yet.

Wait for hydration markers:

  • await page.waitForSelector('.hydrated', { state: 'attached' })
  • await expect(page.locator('#global-loader')).toBeHidden()

force: true disables actionability checks (visible, stable, enabled, event reception), turning the test into a raw DOM script. Use sparingly and always with a comment—ideal for edge cases like file inputs.

Key Takeaways

  • Dependency Projects build a dependency graph, preventing tests from running on unready environments.
  • Web-first assertions with polling eliminate 80% of flakiness caused by race conditions.
  • Separating locators by role increases resilience to UI refactoring.
  • API authentication speeds up setup 20–50x compared to UI login.
  • Traces over screenshots provide full context for failures in CI.

— Editorial Team

Advertisement 728x90

Read Next