Interview Q&A by Experience

69Interview Q&A by Experience Level

Tiers covered

  • 1–2 years — fundamentals, basic Playwright, basic JS/HTTP
  • 3–5 years — architecture, advanced Playwright, framework design
  • 5–8 years — system design, scaling, leadership scenarios
  • Round-by-round: phone screen, technical, design, behavioral

69.1 1–2 years SDET — Foundations

What's the difference between functional and non-functional testing?
Functional: what the system does — login works, checkout creates an order. Non-functional: how it does it — performance, security, accessibility, usability, scalability.
What's a test case? What fields does it have?
A specific scenario to validate behaviour. Fields: id, title, preconditions, steps, expected result, actual result, status, severity, priority, environment.
Severity vs Priority?
Severity = how bad technically (impact on system). Priority = how urgent to fix (business need). Typo on a launch landing page = low severity, high priority.
Smoke vs Sanity vs Regression?
Smoke = build acceptance (does it boot?). Sanity = narrow check of a recent change. Regression = full suite to ensure nothing broke.
What's Playwright?
A Node.js library for automating Chromium, Firefox, WebKit with one API. Cross-browser, auto-wait, parallel by default, built-in network mocking and trace viewer.
What is a locator?
A description of how to find an element — not a reference. Re-resolved on every action, so it never goes stale. Prefer getByRole; fall back to CSS / data-testid.
What is strict mode in Playwright?
Action methods require the locator to resolve to exactly one element. If more match, Playwright throws a clear error. Forces explicit narrowing instead of silently picking the first.
What is web-first assertion?
An assertion on a locator that auto-retries until it passes or times out. expect(locator).toHaveText('Hi'). Removes timing flakes — never compare extracted values.
What's the difference between page.click and locator.click?
page.click takes a selector string and re-resolves; locator.click acts on a pre-built locator. Modern Playwright prefers page.getByRole(...) → returns a Locator → call .click().
What is auto-wait?
Before each action, Playwright runs actionability checks (attached, visible, stable, enabled, receives events) in a retry loop until they pass or the timeout expires. No manual sleeps needed.
HTTP methods you know?
GET (read), POST (create), PUT (replace), PATCH (partial update), DELETE (remove), HEAD (headers only), OPTIONS (CORS preflight / discover allowed methods).
200 vs 201 vs 204?
200 generic success with body. 201 new resource created (POST). 204 success no body (DELETE).
401 vs 403?
401 — server doesn't know who you are. 403 — knows but not allowed.
What is REST?
Resources at URLs, HTTP verbs for actions, stateless, JSON usually. Each request carries enough info to be processed; no server-side session memory between calls.
Why use TypeScript over JavaScript for tests?
Compile-time type checking catches bugs before runtime, IDE auto-complete is richer, refactors are safer. Output is plain JS at runtime — no runtime cost.

69.2 3–5 years SDET — Framework & Architecture

How do you structure a Playwright TS framework?
Layered: tests/ (intent), pages/ (POM), services/ (API clients), fixtures/ (setup), factories/ (data), utils/, config/, data/. Tests don't touch selectors or low-level browser APIs.
What's a fixture and how do you write one?
Async function returning a value the test consumes, with setup before and teardown after via await use(value). Define via test.extend. Worker- vs test-scoped based on lifetime.
How do you reuse auth across tests?
Setup project that logs in once, calls page.context().storageState({ path: 'auth.json' }). Other projects use use: { storageState: 'auth.json' }. Skip UI login on every test.
How do you parallelise across machines?
Sharding: --shard=1/4 per CI runner. Combine with in-shard workers for total parallelism = shards × workers. Merge blob reports at the end.
How do you handle a flaky test?
Quarantine immediately, open ticket. Reproduce with --repeat-each, diagnose via trace. Fix root cause — usually replacing waitForTimeout with web-first assertion, isolating data, or mocking unstable upstream. Lift quarantine when --repeat-each=100 passes on CI.
What's API chaining?
Sequence of HTTP calls where each uses data from the previous. Linear or DAG. State held in local vars, fixtures, or a context object. Cleanup in reverse order of creation.
How do you mock a backend call in Playwright?
page.route('**/api/x', route => route.fulfill({ status, body })). Or modify the real response via await route.fetch() + mutate + fulfill.
How do you choose between POM and Screenplay?
POM by default — fits 99% of teams. Screenplay only for very large suites (1000+ tests, many contributors) where actor/task abstraction pays off.
How do you handle Cross-browser testing efficiently?
Per-project config (chromium / firefox / webkit). Run smoke on all three on every PR; full regression on chromium + nightly cross-browser. Avoid duplicating UI tests three times for unrelated logic.
How do you assert on a network call's payload?
Promise.all the action with page.waitForRequest; capture the request; assert on request.postDataJSON() and headers.
What's the difference between mocks, stubs, spies, fakes?
Stub: returns canned response. Mock: stub + verifies it was called as expected. Spy: passes through real call AND records details. Fake: working alternative implementation (in-memory DB). All four eliminate dependence on real systems.
How do you balance UI vs API tests?
API tests for business logic, edge cases, contracts — fast, cheap. UI for happy-path user journeys and visuals. Hybrid: API setup + UI verify gives both speed and rendering coverage.
How do you measure test suite health?
Pass rate, retry rate (proxy for flakiness), runtime p95, top slowest tests, top flakiest tests, coverage trend, escaped-defect rate. Tracked over time as KPIs.

69.3 5–8 years SDET — System Design & Leadership

Design a test execution platform for 10K tests with 10-min PR SLA.
Orchestrator API → smart test selector → queue → worker fleet (autoscaling on spot) → blob reports → merge → unified report. Capacity: ~700 tests/sec at peak = ~21k concurrent slots. Trace artifacts to S3, metadata to Postgres. See Module 26.
Design a flaky-test detection pipeline.
Capture pass/fail per test per run. Compute flakiness score as failure rate over rolling window. Auto-quarantine if > threshold for N consecutive days. Open a JIRA. Track time-to-fix. Nightly job retries quarantined tests to detect self-healing.
Design a self-healing locator system.
On green runs, snapshot each locator's context (role, name, neighbours, attributes, position). On failure, compute similarity of current DOM candidates against snapshot. Suggest top match. Auto-apply if confidence > threshold; surface in PR for review. Never silently merge.
How do you scale a test team from 5 → 50 engineers?
Platform vs feature teams. Platform owns the framework, common services (test data, auth, runner). Feature teams own their tests within that framework. Style guides, code review, lint enforce consistency. Office hours; pair sessions. KPIs visible org-wide.
How do you balance "more tests" vs "faster suite"?
Pyramid first: most coverage at unit/API where it's cheap. Reserve UI E2E for true user journeys. Smart test selection on PRs (only affected). Full regression nightly. Continuously profile and refactor slowest tests.
How do you justify SDET investment to leadership?
Business outcomes: deploy frequency, lead time, mean time to detect, escaped defect rate. "Cutting flake rate from 12% to 1% lets every PR ship same-day. That's ~10 more deploys/dev/month." Numbers, not opinions.
What's your stance on AI in testing?
Useful for boilerplate, trace analysis, locator suggestion, judging non-deterministic features. NOT a substitute for deterministic regression. Always human-review AI output. Watch for hallucinated locators and shallow assertions.
How do you handle conflict with a developer who says "this test is flaky, ignore it"?
Pull the trace, show data. Ask: is it flake because of the test or because the system has a real race? Both happen. If system race, file as bug. If test race, accept it's the SDET's bug and fix. Don't dismiss; investigate.
How do you onboard a new SDET to your framework?
README + "first PR" guide. Pair on writing one test end-to-end. Walk through layers (test → page object → service → fixture). Pair on debugging a real failing test using trace. Define what good looks like (style guide). Pair-review their first 5 PRs.
What's your KPI dashboard for the platform team?
PR feedback p95 latency, pass rate, retry rate, top 10 slowest tests, top 10 flakiest tests, cost per PR, infra utilisation, escaped defect rate from prod. Reviewed weekly in platform standup; quarterly with leadership.
How do you handle a release blocker found 30 minutes before ship?
Triage severity vs business impact. If critical: stop the ship, fix forward or rollback. If not: file, communicate, ship with a known issue documented. Decision is shared with PM, eng lead, on-call.
Walk me through your proudest SDET work.
STAR — Situation (large suite with 12% flake rate, blocking PRs); Task (improve PR feedback time and reliability); Action (introduced 4-shard sharding + storageState + replaced 47 waitForTimeout + introduced flake-detector pipeline); Result (flake rate to 1%, PR feedback 38m → 7m, dev satisfaction up 40%).

69.4 5-Years SDET Interview — Deep Q&A (version-aware, Playwright 1.61)

Designed for the 5-year mark

  • You are expected to own a framework, not just write tests.
  • Architecture, scaling, debugging, and trade-off questions dominate the round.
  • Interviewers expect concrete numbers (flake rate, runtime, infra cost) — not platitudes.
  • Version-aware questions are common — answer with what 1.61 enables, not generic Playwright.

69.4.1 Version, environment & setup

Which Playwright version do you use and how do you decide when to upgrade?
We pin @playwright/test at 1.61.0 exactly (no caret) in package.json, matched against the Docker image mcr.microsoft.com/playwright:v1.61.0-jammy. Browser binaries are version-locked, so any drift between the library and the image causes "works locally, fails in CI" mismatches. Upgrade cadence: every 2-3 minors, after the .0 release has had a .1 patch (about two weeks). The upgrade is a single PR that bumps three places (package.json, Docker tag, install step) and re-baselines visual snapshots intentionally.
Why pin the version exactly instead of using a caret range?
Two reasons. (1) Browser binaries are downloaded by npx playwright install and pinned to the library version. A caret can silently bump the library while the cached binaries stay old, producing protocol mismatches. (2) CI reproducibility — a teammate running npm ci a month later must get the exact same environment as the green CI run. package-lock.json helps, but exact pinning makes intent explicit.
How do you keep CI reproducible with Playwright?
Five things. (1) Exact version pin. (2) npm ci not npm install. (3) Official Playwright Docker image matching the version. (4) Cache ~/.cache/ms-playwright in CI keyed on the Playwright version. (5) Re-install browsers on cache miss with npx playwright install --with-deps. The cache key is critical — keying on lockfile hash works; keying on the version string is simpler and equally correct.
What does npx playwright install actually do?
Downloads the pinned Chromium/Firefox/WebKit revisions plus FFmpeg (for video). It does not install npm packages — that's npm install's job. The --with-deps flag also apt-get installs OS libraries (libgbm, libnss3, etc.) that the browser binaries need on Linux. On macOS/Windows, --with-deps is a no-op.

69.4.2 Framework design & architecture

Walk me through your test framework structure.
Layered. tests/ holds intent only — no selectors. pages/ has Page Objects exposing user-level actions like login(email, pw), never raw locators. services/ wraps API calls using APIRequestContext. fixtures/ defines custom test/worker fixtures via test.extend. factories/ generates test data with Faker. config/ holds playwright.config.ts with projects for chromium/firefox/webkit + setup project for storageState. utils/ for pure helpers. Tests never import from @playwright/test directly — they import from fixtures/index.ts which re-exports an extended test.
How do you decide what goes in a Page Object vs a fixture?
Page Object = stateful interaction with one page/component. Fixture = setup & teardown of a resource that multiple tests share. Rule: if it logs in, sets up data, or starts a server, it's a fixture. If it clicks buttons or asserts on UI, it's a Page Object. A common mistake is putting "logged-in page" creation in a Page Object — it should be a fixture that returns an already-authenticated page.
How do you handle test data?
Three tiers. (1) Static fixtures for reference data (countries, currencies) in JSON. (2) Generated synthetic data via Faker, seeded per-worker for determinism. (3) API-created data via a dataFactory fixture that POSTs to the backend, returns the entity, and registers a cleanup hook to DELETE in afterEach. Never share data across tests; never depend on prod or shared staging data. Each test owns its data lifecycle.
Custom fixtures with worker vs test scope — when do you choose which?
Test scope (default) for anything mutable per-test — page, request context, test data. Worker scope for expensive one-time setup that's safe to reuse — a logged-in browser context's storageState, a database connection pool, an environment health check. Worker fixtures run once per worker process, so if you run with workers: 4, you pay the cost 4 times, not 100 times.
How do you implement parallel-safe authentication?
Setup project pattern (1.31+). One project named setup with a single spec that logs in via the UI (or API) and calls page.context().storageState({ path: 'auth/admin.json' }). Every other project declares dependencies: ['setup'] and use: { storageState: 'auth/admin.json' }. The setup runs once per npx playwright test invocation; all workers share the resulting cookie file. For multi-role testing, run multiple setup specs in parallel, each writing to a different file.
What's your stance on POM vs Screenplay vs Functional helpers?
POM by default — fits 99% of teams and is what new joiners expect. Screenplay only when you have 1000+ tests, multiple actor types, and the actor/task model genuinely simplifies. Functional helpers (just exported functions) work for small projects but break down once shared state grows. The wrong choice is usually too-clever Screenplay on a 200-test suite — overhead with no payoff.

69.4.3 Locators & assertions — deep mastery

Walk me through your locator priority and why.
In order: getByRole > getByLabel > getByPlaceholder > getByText > getByTestId > CSS > XPath. The reason is twofold: (1) the higher-priority locators are how a real user (or screen reader) identifies the element, so they double as accessibility checks; (2) they survive refactors — designers can change classes and DOM nesting and the test still works. CSS/XPath are escape hatches when nothing else is available, never the first choice.
What is strict mode and why does it matter?
Action methods (click, fill, etc.) require the locator to resolve to exactly one element. If two match, Playwright throws with a clear error showing both candidates. This forces explicit narrowing (.first(), .filter(), .nth()) instead of silently picking the first match and producing non-deterministic behaviour when the DOM changes. It's one of the design choices that makes Playwright less flaky than Selenium.
How do you narrow a locator without using nth?
Three primary tools. (1) .filter({ hasText: 'Total' }) — keep elements containing certain text. (2) .filter({ has: page.getByRole('button') }) — keep elements that contain another locator. (3) Chaining — parent.getByRole('button', { name: 'Save' }) scopes the lookup to descendants of parent. Use .nth() only when the position is semantically meaningful (the "second row" of a table).
Web-first assertion — what's the rule and why?
Rule: assert on a locator, never on an extracted value. expect(locator).toHaveText('Hi') auto-retries the DOM lookup until the text matches or the timeout fires. expect(await locator.textContent()).toBe('Hi') captures the text once, then asserts statically — if the text hasn't appeared yet, it fails immediately. The auto-retry is what eliminates 90% of timing-related flakes.
What's the difference between toBeVisible and toBeAttached?
toBeAttached = the element is in the DOM tree. toBeVisible = attached AND not display:none, not visibility:hidden, not size zero, and inside the viewport. Use toBeAttached when a hidden element matters (a modal exists in the DOM before fading in); use toBeVisible for "the user can see it".
How do you use aria snapshots? (1.49+ feature)
expect(locator).toMatchAriaSnapshot() serialises the accessibility tree of the locator and compares it against a YAML snapshot. Great for stable assertions on complex widgets (a date picker, a select-with-search) without coupling to specific CSS or DOM structure. The snapshot lives next to the spec; you regenerate it intentionally with --update-snapshots. Caveat: re-baseline carefully — accidental drift is easy.
How would you test a component that flashes a transient toast for 3 seconds?
Two assertions. (1) await expect(toast).toBeVisible() — confirms it appeared. (2) await expect(toast).toBeHidden({ timeout: 5000 }) — confirms it auto-dismisses within the window. Don't waitForTimeout(3000); use the assertion's built-in retry. If you also need to verify content, chain toHaveText between the two visibility checks.

69.4.4 Network, mocking & API hybrid

When do you mock a network response and when do you hit the real backend?
Mock when: (a) you're testing UI behaviour given a known backend response (loading, error, empty state), (b) the upstream is unstable or rate-limited, (c) you need to test edge cases the real backend can't produce (500 errors, slow responses). Hit real when: (a) you're validating the end-to-end contract, (b) the test is part of a smoke suite gating deployment, (c) the response shape changes frequently and a mock would drift. A mature suite uses both — most UI tests mock, a small "real backend" smoke runs against staging.
Show me how you'd modify a real API response without fully mocking it.
await page.route('**/api/products', async (route) => {
  const response = await route.fetch();
  const json = await response.json();
  json.items = json.items.filter(p => p.inStock); // mutate
  await route.fulfill({ response, json });
});
Useful for testing UI given partial data without writing a fake fixture for the whole payload.
How do you intercept a WebSocket message? (1.48+ feature)
page.routeWebSocket('wss://api.example.com', ws => { ... }) gives you handlers for messages flowing both ways. You can ws.send() a custom message back to the client, or pass-through with ws.connectToServer(). Critical for testing live-updating dashboards or chat features without standing up a real ws server.
How do you assert on a network request the page makes?
const [request] = await Promise.all([
  page.waitForRequest('**/api/orders'),
  page.getByRole('button', { name: 'Submit' }).click(),
]);
expect(request.method()).toBe('POST');
expect(request.postDataJSON()).toMatchObject({ items: expect.any(Array) });
expect(request.headers()['x-trace-id']).toBeDefined();
The Promise.all pattern is the trick — register the listener before triggering the action so the request isn't missed.
API test in Playwright — when and why?
Use request fixture / APIRequestContext when you need: (a) pure backend tests with no browser, (b) setup data via API before a UI test, (c) tear-down via API after a UI test, (d) chained API flows (login -> create -> list -> delete). Faster than UI (no rendering), same fixtures and reporters, runs in the same parallel pipeline. Cuts UI test count by 60-70% for typical SaaS apps.
Hybrid test pattern — what does it look like?
Use API to set up state, UI to verify rendering. Example: create 50 orders via request.post(), then load the dashboard via page.goto() and assert the pagination shows "1 of 3 pages". Much faster than clicking through 50 order creations in the UI, and the API path is independently tested elsewhere. Cleanup via API too.

69.4.5 Flakiness — root-cause playbook

A test passes locally but fails in CI. What's your debug sequence?
Six checks, in order. (1) Pull the trace artifact — open trace.zip in the trace viewer; the action and DOM state at the moment of failure usually tells the story. (2) Compare CPU/memory: CI runners are often slower; expand timeouts targeted to the slow assertion. (3) Compare viewport — local dev uses full screen, CI uses 1280x720; an off-viewport element won't toBeVisible. (4) Compare locale/timezone — CI is UTC by default; tests asserting on dates need explicit TZ=America/New_York. (5) Compare network — staging from CI may have stricter rate limits than your laptop. (6) Compare browser version — if CI uses Docker image v1.61.0-jammy and you're on v1.60 locally, behaviour differs. The trace solves 80% of cases on first read.
What's your strategy for fixing a flaky test?
Never silence — root-cause. Step 1: reproduce with npx playwright test name --repeat-each=20. Step 2: open the trace from a failing run; look for the action where state diverges from expectation. Step 3: identify root cause — race condition, animation, network jitter, shared data. Step 4: fix appropriately — usually replacing a waitForTimeout with a proper web-first assertion, scoping a locator more tightly, or moving state setup into a fixture for isolation. Step 5: gate the merge on --repeat-each=50 passing in CI. Step 6: track the flake metric over time so we know if it returns.
You see a 12% flake rate. How do you bring it under 1%?
Three-phase plan. Phase 1 (week 1): instrument — log every test's pass/fail across last 100 CI runs, rank by failure frequency, quarantine top-10 worst offenders so they stop blocking PRs. Phase 2 (weeks 2-4): root-cause the top 10 one by one, each fix verified with --repeat-each=50. Phase 3 (ongoing): trend the flake metric weekly; any test that flakes twice in a rolling 14 days gets auto-quarantined and a ticket assigned. In practice 80% of flakes come from 20% of patterns: waitForTimeout, shared test data, animations, surprise dialogs. Fix those patterns at the framework level (linters, fixtures) and the metric stays low.
How do retries hide flakiness — and when are they OK?
Retries are a band-aid. retries: 2 means a test passing 1/3 times still shows green — you've lost signal. They're acceptable for known transient infra issues (a flaky upstream API you can't fix) but every retry should be tracked. I gate the metric: "max 2% of runs use any retry; if higher, investigation required". Playwright's retry: 'on-first-failure' via test.fail.fixme + retry analytics gives the right balance — auto-recovery with a paper trail.
How do you handle animations and transitions?
Three tools. (1) animations: 'disabled' in toHaveScreenshot options — disables CSS animations during visual asserts. (2) page.emulateMedia({ reducedMotion: 'reduce' }) at the project level — many apps respect this. (3) For unavoidable animations, assert on the post-animation state with a longer timeout. Never waitForTimeout(500) — it's fragile and slows the suite linearly with the number of tests.

69.4.6 Parallelism, sharding, and CI/CD

Explain workers vs sharding.
Workers are parallel processes within one CI runner. workers: 4 means 4 browser instances on the same machine. Sharding splits tests across multiple CI runners. --shard=1/4 runs the first 1/4 of tests on this runner; spawn 4 runners and they cover the whole suite in parallel. Total parallelism = shards × workers. A 1000-test suite at 4 shards × 4 workers = 16 concurrent tests, finishes in ~1/16 the time.
How do you merge reports across shards?
Each shard runs with --reporter=blob, producing an opaque .zip artifact. After all shards finish, a final CI job downloads every blob and runs npx playwright merge-reports --reporter=html ./blob-reports. The output is a single unified HTML report covering all tests across all shards. Critically, the trace artifacts are also merged — you can click on any failed test and see its trace, regardless of which shard ran it.
What does your GitHub Actions matrix look like for sharded Playwright?
jobs:
  test:
    strategy:
      fail-fast: false
      matrix:
        shard: [1, 2, 3, 4]
    steps:
      - run: npx playwright test --shard=${{ matrix.shard }}/4 --reporter=blob
      - uses: actions/upload-artifact@v4
        with:
          name: blob-${{ matrix.shard }}
          path: blob-report/
  merge:
    needs: test
    if: always()
    steps:
      - uses: actions/download-artifact@v4
      - run: npx playwright merge-reports --reporter=html ./all-blob-reports
      - uses: actions/upload-artifact@v4
        with: { name: html-report, path: playwright-report/ }
if: always() on the merge job is critical — you want the report even when tests fail.
How do you choose the number of shards/workers?
Math + cost. Goal: PR feedback under 10 minutes. If total runtime = T seconds at 1 worker, sharded = T / (shards × workers / overhead). Overhead per shard is ~30-60s (setup, browser install on cache miss). Sweet spot: enough shards to hit the SLA, capped by CI cost. Practical heuristic: 4 shards × 4 workers for a 500-test suite, scale up linearly. Always measure — don't over-shard, because each shard has fixed overhead.
What's "fully parallel" mode and when do you turn it off?
fullyParallel: true means tests within a file also run in parallel across workers, not just across files. Default in modern configs. Turn it off (test.describe.configure({ mode: 'serial' })) only for tests that genuinely depend on order — e.g. a sequence creating a user, logging in, deleting. Even then, prefer rewriting as independent tests that each set up their own state.

69.4.7 Debugging mastery

Walk me through using the Trace Viewer.
Trace captures every action, screenshot before/after, network calls, console logs, and source location. Enable with trace: 'on-first-retry' in config. After a failed run, open with npx playwright show-trace trace.zip. Three panels: timeline (every action), DOM snapshot (clickable, inspectable like Chrome DevTools at that moment), and network/console. The DOM snapshot is the killer feature — you can hover-inspect elements as they were at the failure, no reproduction needed.
How do you use Playwright's UI Mode for development?
npx playwright test --ui. Pick tests visually, watch them run live, step through actions, pick locators inside the running browser, edit-and-rerun without restart. Replaces 90% of headed-mode + console.log debugging. Pairs especially well with test.only when iterating on one test.
What's the difference between --debug, --ui, and PWDEBUG=1?
--ui: the modern UI Mode app — visual, persistent, pickable. --debug: opens Playwright Inspector with the test paused at the first action; step-through, see actionability checks. PWDEBUG=1: same as --debug but as an env var (works for non-Playwright runners too) and disables timeouts. Use UI Mode for daily development; Inspector when debugging actionability issues; PWDEBUG=1 for one-off CI debug runs without changing scripts.
How do you debug a "click is intercepted" error?
The error means another element is on top of the target. Three diagnostics. (1) Open the trace and inspect the DOM at the moment of failure — usually a modal, cookie banner, or floating chat icon. (2) Use page.locator('xxx').click({ force: true }) as a temporary unblocker, but never as the fix. (3) Register a page.addLocatorHandler() (1.42+) for the interceptor — Playwright auto-dismisses it before retrying the click. Final fix is usually closing the dialog programmatically in beforeEach or via the handler.

69.4.8 Performance, visual, accessibility & cross-browser

How do you do visual regression in Playwright?
await expect(page).toHaveScreenshot('login.png', { maxDiffPixels: 100 }). First run creates the baseline; subsequent runs compare. Tune maxDiffPixels or maxDiffPixelRatio to absorb anti-aliasing differences. Critical: snapshots are platform-specific (font rendering differs between Linux and macOS) — always generate baselines inside the Playwright Docker image to match CI. Mask volatile regions (timestamps, ads) with mask: [locator].
How do you integrate accessibility testing?
@axe-core/playwright — run new AxeBuilder({ page }).analyze() after the page is in a stable state. Returns a violations array. Fail the test if critical/serious violations exceed zero; warn on moderate/minor. For per-component testing, scope to a selector: new AxeBuilder({ page }).include('main').analyze(). Gate the PR in CI on the critical-violations count not going up. The 1.44+ web-first matchers like toHaveAccessibleName complement axe — they catch specific regressions axe might miss.
How do you measure page performance in Playwright?
Three layers. (1) Page-level: page.evaluate(() => performance.timing) for Navigation Timing metrics (DOMContentLoaded, load). (2) Web Vitals: inject the web-vitals library and read LCP, CLS, INP from JS. (3) Lighthouse via playwright-lighthouse — full audit, scores, budgets. Hook these into the same test run and fail on threshold breach. For ongoing perf monitoring, log to Datadog/Grafana, not just CI.
Cross-browser strategy — what runs where?
Tiered. (1) Every PR: full suite on Chromium only — fastest feedback. (2) Nightly: full suite on Chromium + Firefox + WebKit. (3) Pre-release: add real Safari and real Edge via BrowserStack. Most bugs are Chromium-platform-agnostic; the cross-browser passes catch the ~5% of CSS/JS engine quirks. Don't run the full suite three times on every PR — wasteful and slow.
How do you test on mobile?
Two paths. (1) Mobile emulation: use: { ...devices['iPhone 15'] } — viewport, user-agent, touch events, device pixel ratio. Catches 80% of responsive bugs without a real device. (2) Real device: Appium for native, or cloud farms (BrowserStack, Sauce Labs) for real mobile browsers via Playwright's remote endpoint. Emulation for PRs, real devices weekly. Don't conflate emulation with "tested on mobile" in a status report — be precise.

69.4.9 System design questions (5y SDET often gets a scaled-down version)

Design a test platform that runs 5,000 tests in < 10 minutes.
Components. (1) Orchestrator — REST API receives "run suite X on commit Y", emits jobs. (2) Test runner pool — Kubernetes pods, each running Playwright in Docker, autoscaled on a queue depth metric. (3) Test selector — given a code diff, computes affected tests via a dependency graph (or a tag heuristic) so PRs run a subset. (4) Result store — Postgres for metadata (run id, test id, status, duration), S3 for artifacts (traces, screenshots, videos). (5) Reporter — merges blob reports, publishes HTML, posts PR comment with summary. (6) Flake detector — separate pipeline reading the result store, computing per-test failure rate, auto-quarantining outliers. Capacity math: 5000 tests × avg 8s = 40,000 test-seconds. To finish in 600s, need 67 concurrent slots → 17 pods × 4 workers. Plus 30% headroom = 22 pods. Cost-wise, ~$0.50/PR on spot instances.
Design a test data management system.
Layered. (1) Static reference data in JSON, version-controlled. (2) Dynamic generation: Faker, seeded per worker with a per-run salt so two parallel runs don't collide. (3) API-created entities via a dataFactory service — POST to backend, return entity ID, register cleanup hook to DELETE in afterEach. (4) Per-env isolation: separate database schema per test run on shared infra, or namespaced records (test_run_id column). (5) PII rules: synthetic data only, lint-check repos for hard-coded real names/emails/SSNs. (6) Cleanup safety: scheduled "garbage collection" job deletes orphaned test data older than 1 hour, catches anything fixtures missed.
Design a flaky-test detection pipeline.
Pipeline. (1) Every CI run writes per-test results to a results DB (run_id, test_id, status, duration, retried_bool). (2) Nightly batch job computes, per test, the failure rate over the rolling 14-day window. (3) If failure_rate > 5% AND total_runs >= 20 → mark as flaky, file a JIRA ticket auto-assigned to the test author. (4) Quarantine list lives in a config file; CI reads it to skip those tests on PRs (still runs nightly to track if fixed). (5) Dashboard surfaces top 20 flakiest tests, trend over time, time-to-fix per ticket. KPI: "flake rate of trunk < 1%". This makes flakiness visible and creates accountability.
Design a self-healing locator system.
On every green run, snapshot for each locator: role, accessible name, text content, attributes, position relative to landmark elements. On a future failure, compute similarity between the failed locator's intent and current DOM candidates. If a single candidate exceeds 0.85 confidence, suggest it in the trace viewer — don't auto-apply. Auto-apply only with human approval via a PR-bot that opens a "self-healing suggestion" PR. Always keep humans in the loop — silent self-healing has a bad failure mode (false negatives that hide real bugs).

69.4.10 Scenario / "How would you test X" questions

How would you test a file upload feature?
Five tests. (1) Happy path: setInputFiles('file.png'), assert success message and that the file appears in the list. (2) File-too-large: synthetic 100MB file, assert error message. (3) Wrong type: setInputFiles('malware.exe'), assert rejection. (4) Drag-and-drop: page.dispatchEvent('drop', {...}) with DataTransfer. (5) Backend: request.post('/upload', { multipart: {...} }) independently asserts the API handles edge cases without the UI. For files that need to come from disk, store small fixtures in the repo; for large files, generate in beforeEach with fs.writeFile.
How would you test a payment flow?
Tiered. (1) Unit/API: validate every payment-state transition (initiated → authorised → captured → settled) with mocked gateway responses. Edge cases: declined, expired card, fraud-flag, partial capture, refund, chargeback. (2) UI: happy path with the gateway's test mode (Stripe/Adyen sandbox). Never use real cards. (3) Resilience: gateway timeout, gateway 500 — verify the order doesn't get corrupted state. (4) Idempotency: simulate double-submit; verify only one charge. (5) Manual exploratory once per release; auditing logs is the most important check.
How would you test a search feature?
Several axes. (1) Exact match — query returns the expected single result. (2) Partial match — substring queries return the right set. (3) No results — assert empty-state UI. (4) Special chars — unicode, quotes, SQL injection attempts (the test gates a security regression too). (5) Pagination — query returning 100+ results paginates correctly. (6) Latency — assert search responds within an SLA (e.g. 95th percentile < 500ms via page.waitForResponse timing). (7) Debounce — type fast, verify only one API call fires after the pause.
How would you test a real-time chat?
Three browsers, one test. browser.newContext() twice creates two isolated user sessions. User A sends a message; assert that User B's page shows it within a timeout. Test direction both ways. For WebSocket-level testing, page.routeWebSocket (1.48+) lets you inject messages or simulate disconnects. Negative tests: send a message while User B's WS is disconnected → reconnect → assert backlog delivery.
How would you test a multi-step wizard / checkout?
Each step is an independent test that starts via API at the prior step's state. Don't run the wizard end-to-end in every test — slow and shifts focus from the step under test. One full E2E "happy path" test runs the whole flow in order. The rest are step-isolated: "given a cart with items, when on shipping page, then can submit a US address and proceed". Negative cases per step: invalid postcode on shipping, expired card on payment, out-of-stock between cart and confirmation.
How would you test a complex date picker?
Five tests. (1) Default state — placeholder visible, no selection. (2) Open → select today → verify input shows today's date in the locale-correct format. (3) Open → navigate to next month → select a date — verify both display and the underlying value. (4) Disabled dates — assert past dates can't be selected. (5) Keyboard a11y — open with Enter, navigate with arrows, select with Enter, close with Esc. The aria snapshot (toMatchAriaSnapshot) is great for asserting the picker's structure without coupling to CSS.

69.4.11 Leadership & cross-team collaboration (real at 5y)

How do you partner with developers on test ownership?
Shared accountability. Devs write unit tests; SDETs own integration/E2E. Both pair on framework design. PR check: any new feature ships with at least one E2E happy-path test. The SDET reviews it; devs maintain it (it's code, like any code). The SDET role is to provide the rails (framework, fixtures, patterns), unblock devs, and own the integration test infrastructure — not to be a bottleneck writing every test.
How do you onboard a new SDET to your framework in their first week?
Day 1: README, dev setup, run the suite, browse the trace viewer on a recent failure. Day 2: pair on writing one E2E test for an existing feature. Day 3: review their first PR — but pair-review, not gate-review. Days 4-5: pair on debugging a real flaky test using the trace. End of week 2: they own a small feature area's tests. Pair-review first 5 PRs. Never throw a doc at them and expect mastery — pairing accelerates everything 5×.
A developer says "this test is flaky, just ignore it". How do you respond?
Three steps. (1) Pull the trace — facts, not opinions. (2) Classify: is it the test's fault (race in the spec) or the system's fault (race in the product)? Both happen. If system race, file as a real bug. If test race, accept it's an SDET-owned fix. (3) Never silently ignore — that erodes trust in the whole suite. Quarantine with a ticket, fix, lift quarantine. The SDET role here is to be the calm root-cause investigator, not to win arguments.
How do you justify SDET investment to leadership?
Business metrics, not test metrics. Bad: "100% of tests pass". Good: "Flake rate dropped from 12% to 1%, allowing PRs to ship same-day instead of next-day. That's ~10 more deploys per dev per month, or ~2x team throughput." Connect to DORA metrics: deploy frequency, lead time, change failure rate, MTTR. The dollar value: "blocking 30% of PRs from same-day ship cost us X engineer-hours/month at $Y/hour = $Z." Numbers leadership recognises.
How do you decide what NOT to test?
Cost vs payoff. Don't E2E-test pure UI styling — visual regression covers it cheaper. Don't write a UI test for a backend edge case — API test, same coverage, 10× faster. Don't test the framework or the browser itself. Don't duplicate coverage already in unit tests. The rule: every test must answer "what bug would this catch that nothing else does?" If the answer is none, delete the test.

69.4.12 STAR stories — five ready-to-deliver narratives

STAR: "Tell me about a time you reduced test flakiness."
Situation: Our 800-test Playwright suite had a 12% flake rate; PRs averaged 3 retries to merge. Task: Bring flake rate under 2% in one quarter. Action: Built a flake-detection pipeline (per-test failure rate over rolling 14d, dashboard, auto-quarantine). Audited top-20 worst offenders — found 70% used waitForTimeout or shared test data. Replaced waitForTimeout with web-first assertions; moved data setup into per-test fixtures with API cleanup; added page.addLocatorHandler() for a recurring cookie banner. Wrote a lint rule to ban waitForTimeout in new tests. Result: Flake rate to 0.8% in 11 weeks. PR median merge time 38m → 7m. Eliminated ~6 engineer-hours/day of "rerun the test" toil.
STAR: "Tell me about scaling a test suite."
Situation: 1,200-test suite, 90 minutes on a single runner — too slow for our same-day deploy goal. Task: Cut PR feedback to under 10 minutes. Action: Migrated to 4-shard sharding (--shard=1/4) with blob reports + merge-reports. Cached browser binaries in GitHub Actions keyed on Playwright version. Introduced a setup project to log in once and reuse storageState via dependencies: ['setup'] — eliminated ~12 minutes of repeated UI logins. Removed retries on stable tests to recover ~5 minutes of hidden retry time. Result: 90m → 8m PR feedback. Same test count, same coverage. CI cost rose 35% but engineer-hours saved >> CI cost.
STAR: "Tell me about a difficult production bug your tests caught — or missed."
Situation: An order-creation race condition only manifested under concurrent load — never reproduced in our test suite. Task: Catch this class of bug going forward without slowing the test suite. Action: Added a small concurrent-load layer using request.post() in parallel from a single test, asserting on database invariants (no duplicate order IDs, no inconsistent totals). Built a "Friday chaos" job that runs once weekly and includes random delays, retries, and simulated downstream failures. Result: Caught two similar race conditions in the next quarter before they hit prod. Cost: one small test file + 4 minutes of weekly chaos time.
STAR: "Tell me about a time you disagreed with a senior engineer."
Situation: A senior engineer wanted to ship a major refactor with "we'll test it in prod with a feature flag". Task: Push back without escalating. Action: Acknowledged the value of canarying. Proposed instead: ship behind the flag, but require an automated test suite covering the new path before flipping the flag past 1% traffic. Wrote the spec, prototyped the missing test infrastructure, demo'd in two days. Result: Adopted as policy. The next refactor caught a regression in the test suite that would've affected ~5% of users.
STAR: "Tell me about a framework or pattern you introduced."
Situation: Engineers were duplicating fixture setup across 20+ specs — each manually creating users, products, and auth state. Task: Make this composable and reusable. Action: Built a typed test.extend-based fixture library: user, authenticatedUser, product, cart, each with API-driven setup and afterEach cleanup. Documented the patterns and pair-migrated five teams over a month. Result: Average new-test PR size dropped from 80 lines to 25. Test data leakage between tests dropped to zero. New SDETs ramping in a week instead of a month.

69.4.13 Rapid-fire (expect these in screens)

What's test.use()?
Per-describe or per-file fixture override. test.use({ viewport: { width: 360, height: 800 } }) at the top of a file gives all tests in it a mobile viewport without touching the global config.
What's the difference between test.skip and test.fixme?
test.skip: skipped, no expected behaviour. test.fixme: skipped, but documented as "known broken, must fix later". Surfaces in reports differently — fixme acts as a TODO marker.
test.beforeAll vs test.beforeEach?
beforeAll: once per file (or describe block). beforeEach: before every test. With fullyParallel: true, beforeAll runs once per worker that picks up tests from this file — so a file run across 4 workers can run beforeAll 4 times. Don't rely on beforeAll for unique-resource creation in parallel suites.
How do you handle env vars in tests?
dotenv loaded at the top of playwright.config.ts. Different .env.staging / .env.production files; the active one chosen via NODE_ENV. Never commit secrets — use CI secrets and inject via env. env is also commonly added as a project metadata to fixture decisions ("if env is prod, skip destructive tests").
What's the most underrated Playwright API?
page.addInitScript(). Runs JS in the page before any navigation, before any user script. Use it to mock Date.now() for deterministic time-based UI, to seed a feature flag, to inject mock analytics, or to stub localStorage before the app reads it. The 1.46 page.clock API is the modern replacement for time mocking but addInitScript still rules for general "patch the page before it runs" needs.
If interviewer asks "what's the latest Playwright feature you've used?"
Pick something genuine. Three solid candidates: (a) page.clock for time-based UI testing (1.46) — "we had a countdown timer test that took 30 seconds; clock.fastForward made it instant". (b) toMatchAriaSnapshot (1.49) — "replaced 200 lines of brittle CSS-coupled assertions with a single accessibility-tree snapshot". (c) page.addLocatorHandler (1.42) — "auto-dismissed a cookie banner that was causing ~12 flaky failures/week". Honest specifics > vague claims.
Walk me through your CV / projects (60 sec).
"I'm an SDET with N years, currently at X focused on Y. My biggest impact has been Z. I work in TS/Playwright primarily, with experience in API testing, CI/CD, and framework design. Open to roles where I can [reason for moving]."
What's a recent technical challenge you solved?
Concrete STAR; pick something specific with a number. Avoid abstract platitudes.
Why are you leaving your current role?
Forward-looking — what you want next, not what's wrong now. "I want a role where I can own the test infrastructure end-to-end."
What's your salary expectation?
Range based on market research for the role/level/location. Anchor with the upper bound you'd accept. Be ready to discuss flexibility on equity/cash mix.

69.5 Behavioral — STAR examples

"Tell me about a time you disagreed with a colleague."
S: Dev wanted to ship a feature flag-off behind "we'll test in prod". T: Push back without escalating. A: Proposed a 1% canary with monitoring; demo'd the dashboard. R: Adopted; caught a 500-error regression that would have hit all users.
"Tell me about a time you missed a deadline."
Own it. S: 3-week test infra migration ran 1 week late. T: Communicate and recover. A: Status update day 1 of slip; cut scope (deferred visual regression); pulled in a teammate for paired sessions. R: Shipped MVP on day 22; deferred items shipped 1 week later.
"Tell me about a time you had to learn something quickly."
Specific tech and outcome. "Had to add Cucumber to a Playwright project in two days because a client demanded BDD. Read the docs, prototyped the World class, wired hooks, demo'd. Hit the deadline."
"Why this company?"
Researched, specific. Reference the product, the engineering blog, or a public talk. Connect to your goals. Avoid "great culture / fast-paced" generics.
"Do you have any questions for us?" (always yes)
Ask about: team structure, how SDET partners with dev, the test pyramid as it stands, what success looks like at 6 months, the hardest current challenge they want help solving. Shows you're thinking about the actual job, not just the offer.