QA Engineer Skills 2026QA-2026Contexts, Waiting, and Assertions

Contexts, Waiting, and Assertions

Three concepts form the core of Playwright's reliability advantage over earlier tools: browser contexts for isolation, auto-waiting to eliminate timing bugs, and web-first assertions that retry until conditions are met. Master these three and most flakiness disappears.


Browser Contexts

A browser context is a lightweight, isolated browser session — like an incognito window. Each context has its own cookies, localStorage, and cache. Unlike Selenium, where each test typically launches a new browser process, Playwright creates contexts within a single browser instance.

// Each test gets its own context automatically in @playwright/test
test('user A sees their dashboard', async ({ page }) => {
  // `page` belongs to a fresh context — isolated from other tests
});

// Manual context creation for multi-user scenarios
test('two users collaborate', async ({ browser }) => {
  const adminContext = await browser.newContext();
  const userContext = await browser.newContext();
  const adminPage = await adminContext.newPage();
  const userPage = await userContext.newPage();

  // Admin and user have completely separate sessions
  await adminPage.goto('/admin');
  await userPage.goto('/dashboard');
});

Why Contexts Matter

Selenium Approach Playwright Approach
One browser per test (slow startup) One browser, many contexts (fast)
Shared state leaks between tests Complete isolation per context
Parallel = multiple browser processes Parallel = multiple contexts in one browser
Cookie cleanup between tests is manual Contexts are disposable — no cleanup needed

Auto-Waiting

In Selenium, the #1 source of test flakiness is timing: the test tries to interact with an element before it is ready. Playwright eliminates this by auto-waiting before every action.

When you call page.click('button'), Playwright automatically waits for the element to be:

  1. Attached to the DOM
  2. Visible (not hidden by CSS)
  3. Stable (not animating)
  4. Enabled (not disabled)
  5. Receiving events (not obscured by another element)

Only then does the click execute. If any condition is not met, Playwright retries until the timeout.

// Playwright: just click — auto-waiting handles timing
await page.getByRole('button', { name: 'Submit' }).click();

// Selenium: must manually wait, then click
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

WebDriverWait(driver, 10).until(
    EC.element_to_be_clickable((By.XPATH, "//button[text()='Submit']"))
).click()

What Auto-Waiting Covers

Action Waits For
click() Attached, visible, stable, enabled, receives events
fill() Attached, visible, enabled, editable
check() Attached, visible, stable, enabled
selectOption() Attached, visible, enabled
textContent() Attached
isVisible() Nothing (returns immediately)

When You Still Need Explicit Waits

Auto-waiting handles interactions, but sometimes you need to wait for a condition before proceeding:

// Wait for a specific response after triggering an action
await page.waitForResponse('**/api/orders');

// Wait for navigation
await page.waitForURL('**/dashboard');

// Wait for an element to disappear (loading spinner)
await page.getByTestId('spinner').waitFor({ state: 'hidden' });

Web-First Assertions

Playwright's expect() assertions are "web-first" — they retry until the condition is met or the timeout expires. This is fundamentally different from instant assertions that check once and fail.

// Web-first: retries until text appears (up to 5s by default)
await expect(page.getByRole('alert')).toHaveText('Saved successfully');

// Instant assertion: checks once, fails immediately if not ready
// DON'T DO THIS:
const text = await page.getByRole('alert').textContent();
expect(text).toBe('Saved successfully'); // fragile!

Common Web-First Assertions

// Element state
await expect(page.getByRole('button')).toBeVisible();
await expect(page.getByRole('button')).toBeEnabled();
await expect(page.getByTestId('input')).toHaveValue('hello');
await expect(page.getByRole('alert')).toHaveText('Success');

// Page state
await expect(page).toHaveTitle(/Dashboard/);
await expect(page).toHaveURL(/\/dashboard/);

// Element count
await expect(page.getByRole('listitem')).toHaveCount(5);

// CSS and attributes
await expect(page.getByTestId('status')).toHaveClass(/active/);
await expect(page.getByRole('link')).toHaveAttribute('href', '/about');

Assertion Timeout

// Default timeout: 5 seconds. Override per assertion:
await expect(page.getByText('Report ready')).toBeVisible({ timeout: 30_000 });

// Or set globally in config:
// expect: { timeout: 10_000 }

Putting It All Together

test('add item to cart', async ({ page }) => {
  await page.goto('/products');

  // Auto-wait: clicks when button is ready
  await page.getByRole('button', { name: 'Add to Cart' }).first().click();

  // Web-first assertion: retries until cart badge shows "1"
  await expect(page.getByTestId('cart-count')).toHaveText('1');

  // Navigate and verify
  await page.getByRole('link', { name: 'Cart' }).click();
  await expect(page).toHaveURL(/.*cart/);
  await expect(page.getByRole('listitem')).toHaveCount(1);
});

No explicit waits. No sleep statements. No flaky timing issues. The auto-waiting and web-first assertions handle all synchronization.


Key Takeaways

  • Browser contexts provide lightweight, isolated sessions — no state leaks between tests
  • Auto-waiting checks actionability (visible, stable, enabled) before every interaction
  • Web-first assertions retry until the condition is met, eliminating race conditions
  • Use waitFor() and waitForResponse() only when you need to wait for async operations beyond simple interactions
  • Never use instant assertions on dynamic content — always use expect() from @playwright/test