Skip to content
ExponaBLOG
PLAYWRIGHT & BROWSER AUTOMATION · DAY 079 / 100

Playwright interview questions: best practices explained

Questions for QA and automation roles.

Expona Admin8 min read

Whether you're interviewing for a QA automation role or preparing your team for conversations with new hires, understanding Playwright interview questions: best practices explained gives you a framework to assess real-world competency. These aren't trick questions—they're about how to write maintainable, reliable tests that scale across teams and environments.

How does auto-waiting work?

Auto-waiting is one of Playwright's most powerful features, and it's often the first thing interviewers ask about. The concept is straightforward: before executing any action, Playwright waits for the target element to be actionable. This means the element must be visible, enabled, and not covered by other elements. For assertions, Playwright retries them automatically until they pass or timeout.

In practice, this eliminates an entire category of flaky tests. Consider a form submit scenario:

typescript
// Without auto-waiting (old Selenium thinking)
await page.click('button[type="submit"]');
await page.waitForNavigation(); // Manual, error-prone
await page.waitForSelector('.success-message', { timeout: 5000 });

// With Playwright auto-waiting
await page.click('button[type="submit"]'); // Waits for button to be actionable
await expect(page.locator('.success-message')).toBeVisible(); // Retries until visible or timeout

The key difference: Playwright's action waits for actionability before clicking, and assertions retry. This means you're testing intent, not timing. During an interview, a strong candidate explains that auto-waiting reduces boilerplate, improves reliability, and makes tests read closer to user behavior.

Ask candidates: "What happens if you try to click an element that's covered by a modal?" A good answer recognizes that Playwright will fail with a clear error—preventing hidden bugs where tests passed but users couldn't interact with the UI.

How do you handle authentication?

Authentication is a common source of test flakiness and slow suite execution. Replicating login flows in every test wastes time and creates brittle dependencies on login UI elements that may change. The professional approach uses storage state.

Storage state captures cookies, local storage, and session storage after a successful login, then reuses it across tests. This decouples your test suite from login UI changes and dramatically speeds up parallel execution.

Here's the setup pattern we use across teams:

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

export default defineConfig({
  projects: [
    {
      name: 'chromium',
      use: { ...devices['Desktop Chrome'] },
    },
    {
      name: 'setup',
      testMatch: '**/auth.setup.ts',
      use: { ...devices['Desktop Chrome'] },
    },
    {
      name: 'authenticated',
      use: {
        ...devices['Desktop Chrome'],
        storageState: '.auth/user.json', // Load saved auth state
      },
      dependencies: ['setup'], // Run setup project first
    },
  ],
});
typescript
// tests/auth.setup.ts
import { test as setup } from '@playwright/test';

setup('authenticate user', async ({ page }) => {
  await page.goto('https://app.example.com/login');
  await page.fill('input[name="email"]', 'test@example.com');
  await page.fill('input[name="password"]', 'secure-password');
  await page.click('button[type="submit"]');
  
  // Wait for navigation to indicate success
  await page.waitForURL('https://app.example.com/dashboard');
  
  // Save storage state (cookies, local storage, etc.)
  await page.context().storageState({ path: '.auth/user.json' });
});
typescript
// tests/dashboard.spec.ts
import { test, expect } from '@playwright/test';

test('user can view dashboard', async ({ page }) => {
  // No login needed—auth state is already loaded
  await page.goto('https://app.example.com/dashboard');
  await expect(page.locator('h1')).toContainText('Welcome');
});

During interviews, candidates should mention that this approach:

  • Runs setup once per project, not per test
  • Survives UI changes to login forms
  • Speeds up parallel execution significantly
  • Works with multiple user roles (create separate storage states)

A follow-up question: "How would you handle OAuth or multi-factor authentication?" A seasoned answer uses page context API interception or direct API calls to bypass UI complexity while still validating the logged-in state.

How do you fix flaky tests?

Flakiness is the enemy of test automation credibility. During an interview, this question separates practitioners from theory-only candidates. There are three core strategies: eliminate fixed waits, isolate data, and wait for the right events.

Remove fixed waits. Hard-coded `sleep` calls are a red flag. They hide timing issues and slow your suite unnecessarily:

typescript
// ❌ Flaky: sleep doesn't guarantee the element is ready
await page.waitForTimeout(2000);
await page.click('button');

// ✅ Reliable: wait for the actual condition
await page.waitForSelector('button:not(:disabled)');
await page.click('button');

// ✅ Even better: use actionability checks (built-in)
await page.click('button'); // Auto-waits for actionability

Isolate data. Tests that depend on shared data or specific ordering are flaky by design. Each test should set up its own state or use API fixtures:

typescript
// ❌ Flaky: depends on external state
test('user can delete account', async ({ page }) => {
  await page.goto('/accounts');
  await page.click('text=Delete Last Account');
  // What if the last account was already deleted?
});

// ✅ Reliable: create isolated data
test('user can delete account', async ({ page, request }) => {
  const account = await request.post('/api/accounts', {
    data: { name: 'Test Account' },
  });
  
  await page.goto(`/accounts/${account.id}`);
  await page.click('button:has-text("Delete")');
  await expect(page).toHaveURL('/accounts');
});

Wait for the right events. Network events, animations, and state changes need explicit waits. Use `waitForLoadState`, `waitForFunction`, or event listeners:

typescript
// ❌ Flaky: doesn't account for async data loading
await page.click('button[name="filter"]');
await expect(page.locator('table tbody')).toContainText('Expected Result');

// ✅ Reliable: wait for network activity
await page.click('button[name="filter"]');
await page.waitForLoadState('networkidle');
await expect(page.locator('table tbody')).toContainText('Expected Result');

// ✅ Even better: wait for specific API response
await Promise.all([
  page.waitForResponse(response => response.url().includes('/api/filter')),
  page.click('button[name="filter"]'),
]);
await expect(page.locator('table tbody')).toContainText('Expected Result');

Interview tip: Ask candidates to describe a flaky test they debugged. The answer reveals whether they use trace files, video, or just random retries. Professionals know Playwright's debugging tools: `--debug`, `playwright show-trace`, and headed mode.

How do you scale test suites?

Moving from dozens of tests to thousands requires infrastructure thinking. Interviewers ask this to understand whether candidates think beyond single machines.

Parallel workers. By default, Playwright runs one test per worker in parallel. Configure workers in `playwright.config.ts`:

typescript
// playwright.config.ts
export default defineConfig({
  workers: process.env.CI ? 1 : 4, // Single worker in CI for stability, 4 locally
  fullyParallel: true, // All tests run in parallel
  // ... rest of config
});

On a CI runner with 8 CPUs, you can safely run 8 workers. However, database or API rate limits may require fewer workers—this is where monitoring becomes critical.

Sharding. For larger suites, split tests across multiple CI jobs:

typescript
// Run in GitHub Actions (or similar CI):
// Job 1: npx playwright test --shard=1/3
// Job 2: npx playwright test --shard=2/3
// Job 3: npx playwright test --shard=3/3

Each shard runs a subset of tests independently, then results merge into a single report.

API-based setup. Database seeding via API is faster than UI setup at scale. Combine it with worker isolation:

typescript
// tests/api-setup.ts
test.beforeAll(async ({ playwright }) => {
  const context = await playwright.chromium.launchPersistentContext('');
  const client = context.request;
  
  await client.post('https://api.example.com/seed', {
    data: { testRun: process.env.TEST_RUN_ID },
  });
  
  await context.close();
});

Strong candidates also mention monitoring: test flakiness rates, execution time trends, and failure patterns. These metrics inform whether you need more workers or better test design.

Common mistakes

  • Ignoring trace and video. Candidates who say "I just retry the test" instead of analyzing traces miss the real root cause. Always enable traces and video in CI: `trace: 'retain-on-failure'` and `video: 'retain-on-failure'`.
  • Over-using hard waits. `page.waitForTimeout(1000)` is not a fallback—it's a sign the test lacks proper event handling. This is a junior-level mistake.
  • Not leveraging fixtures. Candidates who manually create and clean up resources in every test miss Playwright's fixture system, which is both more reliable and readable.
  • Running tests sequentially on CI. Setting `workers: 1` globally hurts execution time. Most suites can run in parallel safely; exceptions should be marked explicitly.
  • Storing passwords in code. Use environment variables or a secrets vault. Hard-coded credentials fail audits and pose security risks.

Key takeaways

  • Auto-waiting eliminates manual sleep calls and makes tests read like user behavior—verify candidates understand both action and assertion retry logic.
  • Storage state for auth is the industry standard; it decouples tests from login UI and speeds up parallel execution significantly.
  • Flaky tests require data isolation, proper event waits, and traces for debugging—not random retries or increased timeouts.
  • Scaling requires parallel workers, sharding strategies, and API-based setup; candidates should think in terms of CI infrastructure, not just local execution.
  • Strong interview answers include concrete examples, mention Playwright's debugging tools (trace, video, headed mode), and show production thinking around monitoring and reliability.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles