These advanced Playwright TypeScript interview questions go beyond the basics into how Playwright Test actually works: fixtures and their scopes, projects as a browser matrix and as a setup dependency graph, authentication with storageState, debugging, parallelism and sharding in CI, API and network features, and architecture decisions. For the core question set, start with the Playwright interview questions.

Section 1 — The Test Runner & Fixtures (Deep)

Advertisement

How the fixture system actually works

Explain Playwright fixtures in depth. How do setup and teardown work?

A fixture is a function that builds a value, hands it to the test, and then cleans it up — the runner injects it by name via destructuring.

A fixture is defined with base.extend(). Everything before the use() call is setup; the value you pass to use() is what the test receives; everything after use() is teardown, and it runs even if the test fails. The runner reads the fixture NAMES a test destructures and only builds those — lazy instantiation — so unused fixtures cost nothing.

export const test = base.extend<{ loginPage: LoginPage }>({
  loginPage: async ({ page }, use) => {
    const loginPage = new LoginPage(page);   // 1. setup
    await loginPage.goto();
    await use(loginPage);                     // 2. hand to the test
    // 3. teardown (runs after the test, even on failure)
  },
});

This is why fixtures replace the Java base-class + factory pattern: instead of @BeforeEach/@AfterEach in an inheritance chain, each concern (a page object, an authenticated context, seeded data) is an independent, composable unit that declares its own setup and teardown and its own dependencies.

Test-scoped vs worker-scoped fixtures — when and why?

Test fixtures are rebuilt for every test (isolation); worker fixtures are built once per worker process and shared across all tests that worker runs (performance).

  • Default scope is "test" — a fresh value per test, which is what you want for the page, for data that must not leak, for anything stateful.
  • Worker scope ({ scope: "worker" }) is for EXPENSIVE, SAFE-TO-SHARE setup: a logged-in account, an auth token, a seeded reference dataset, a started test server.
  • Trade-off: worker fixtures amortize cost across many tests but must be immutable/read-only, or tests will contaminate each other.
export const test = base.extend<{}, { account: Account }>({
  account: [async ({ browser }, use) => {
    const account = await createAccountOnce();   // once per worker
    await use(account);
    await deleteAccount(account);
  }, { scope: "worker" }],
});

What are automatic and option fixtures?

  • Automatic fixture ({ auto: true }) runs for every test WITHOUT being requested — ideal for cross-cutting concerns like attaching a logger, starting tracing, or wrapping each test in setup you always want.
  • Option fixture ({ option: true }) defines a configurable default that any project or test can override with test.use({...}) — this is how you parameterize behaviour (e.g. a "userRole" option) cleanly from config.

Together, auto + option fixtures let you build a framework where behaviour is declared in config and applied automatically, with zero boilerplate in the test body.

How do you override a built-in fixture, and why would you?

You redefine it in extend() or set it with test.use(). The classic case is the storageState fixture — you override it so a group of tests runs as a specific role. You can also override page/context options (locale, viewport, permissions) per file or per describe block.

test.use({ locale: 'de-DE', viewport: { width: 1280, height: 720 } });
test.describe('admin area', () => {
  test.use({ storageState: 'auth/admin.json' });   // this block runs as admin
  test('...', async ({ page }) => { /* ... */ });
});

Section 2 — Config, Projects & Authentication Architecture (Deep)

Projects as a matrix AND as a dependency graph

What are projects, and what two very different jobs do they do?

A project is a named test run with its own settings. Projects do double duty: they are your browser/device MATRIX, and they are a DEPENDENCY GRAPH for setup/teardown.

  • As a matrix: one project per browser/device (Desktop Chrome, Firefox, WebKit, iPhone), each with its own use options; the same tests run across all of them.
  • As a dependency graph: a project can declare dependencies: ["setup"] so a setup project runs first, and teardown: "cleanup" so a teardown project runs last.
  • This is how authentication is done idiomatically — a setup project logs in and writes storageState, and the real projects depend on it and consume that state.
export default defineConfig({
  projects: [
    { name: 'setup', testMatch: /.*\.setup\.ts/ },
    {
      name: 'chromium',
      use: { ...devices['Desktop Chrome'], storageState: 'auth/user.json' },
      dependencies: ['setup'],       // run setup first
    },
  ],
});

globalSetup vs a setup project — which and why?

Prefer a setup project. globalSetup is a lower-level hook that runs once with NO access to fixtures, the trace, or the reporter.

  • globalSetup: a plain function(config) that runs once before everything. Good for non-browser work — starting a server, seeding a database. It cannot use fixtures or produce per-test artifacts, and failures are harder to debug.
  • Setup project: a normal test file, so it has fixtures, auto-wait, assertions, tracing and reporting. It can log in through the real UI and save storageState, and other projects depend on it. This is the recommended pattern for auth.

Design authentication for an app with three roles.

Create a setup project that logs in as each role and writes one storageState file per role (admin.json, editor.json, viewer.json). Then either give each role its own project consuming its file, or use test.use({ storageState }) per describe block. For very large suites, make auth a worker-scoped fixture so each worker logs in once and reuses the session across its tests.

// auth.setup.ts
for (const role of ["admin", "editor", "viewer"]) {
  setup(`login as ${role}`, async ({ page }) => {
    await loginAs(page, role);
    await page.context().storageState({ path: `auth/${role}.json` });
  });
}
Why storageState beats logging in per test

Logging in through the UI in every test is slow and flaky (it exercises the login form thousands of times for no reason). storageState captures cookies + localStorage once and injects them, so tests start already authenticated — often cutting suite time by 30-50%.

Section 3 — Assertions, Waiting & Determinism (Deep)

The two kinds of expect, and killing flakiness

Explain retrying vs non-retrying assertions.

expect(locator) auto-retries; expect(value) checks once. Confusing the two is a top cause of flaky suites.

When you assert on a Locator — expect(locator).toBeVisible() — Playwright POLLS: it re-evaluates the DOM until the condition holds or the assertion timeout (5s) expires. When you assert on a plain value — expect(count).toBe(3) — it runs once, immediately. So if you read a value into a variable and then assert on the variable, you lose the retry and reintroduce the race you were trying to avoid. Keep the locator inside the expect wherever possible.

// GOOD — retries until the badge shows 3
await expect(page.getByTestId('cart-badge')).toHaveText('3');

// RISKY — reads once, asserts once (no retry)
const text = await page.getByTestId('cart-badge').textContent();
expect(text).toBe('3');

When do you use expect.soft, expect.poll and toPass?

  • expect.soft(...) — record a failure but keep going, so one test can report several problems at once (e.g. validating many fields on a page). The test still fails at the end.
  • expect.poll(fn) — retry a non-locator value that changes over time: await expect.poll(() => getApiCount()).toBe(3). Use it when the thing you are checking is not a Locator (an API count, a computed total).
  • expect(async () => { ...assertions... }).toPass() — retry a whole BLOCK of steps until they all pass; useful for eventually-consistent flows where several conditions must line up together.

How do you make tests deterministic instead of using sleeps?

Never use page.waitForTimeout() (a hard sleep). Rely on auto-wait for actions and web-first assertions for state. For explicit synchronization, wait on the exact event: waitForResponse for a specific API call, waitForURL for navigation, or locator.waitFor({ state }) for an element state. When triggering an event that produces a popup or download, start the wait BEFORE the click with Promise.all, so you never miss the event.

const [response] = await Promise.all([
  page.waitForResponse('**/api/cart'),
  page.getByRole('button', { name: 'Add to cart' }).click(),
]);
expect(response.ok()).toBeTruthy();

Section 4 — Parallelism, Sharding & Scale (Deep)

How the runner parallelizes, and how to scale it

Explain Playwright’s parallelism model end to end.

The runner spawns worker processes; it distributes test FILES across them; within a file tests run serially by default; each worker owns its own browser.

  • Workers are OS processes (default = half your CPU cores). Each worker launches its own browser and gives every test a fresh, isolated context+page — which is why there is no ThreadLocal to manage as in Java.
  • Files are the unit of distribution: different files run on different workers concurrently.
  • Within one file, tests run in order by default (they may share worker-scoped state). Opt a file into concurrent tests with test.describe.configure({ mode: "parallel" }); fullyParallel: true in config does this everywhere.
  • If a test fails and retries are on, it runs in a clean worker — so a poisoned worker never cascades failures.

How do you shard a large suite across CI machines?

Sharding splits the tests across N machines deterministically, and you merge the per-shard reports into one at the end.

  • Run --shard=1/3, --shard=2/3, --shard=3/3 on three CI runners in parallel. Playwright splits at the test level so each shard does roughly a third of the work.
  • Each shard produces a "blob" report (a portable, mergeable artifact). Configure the blob reporter for CI.
  • After all shards finish, download the blobs and run npx playwright merge-reports to produce a single unified HTML report.
  • Result: a 40-minute suite finishes in ~13 minutes on three runners, with one report to read.
# CI: three parallel jobs
npx playwright test --shard=1/3
npx playwright test --shard=2/3
npx playwright test --shard=3/3
# afterwards, in a merge job:
npx playwright merge-reports --reporter html ./all-blob-reports

A test is flaky only in CI. Walk through your investigation.

  • Open the trace from the failed CI run (trace: "on-first-retry" captures it cheaply) and time-travel to the exact failing step.
  • Check for a real race: an assertion reading data that arrives via a later network call → switch to a web-first assertion or waitForResponse.
  • Check isolation: is state leaking from a worker-scoped fixture or shared data? Make it read-only or test-scoped.
  • Check CI-only differences: viewport, timezone/locale, animations, a slower machine — pin them and disable animations; raise a targeted timeout if genuinely needed.
  • Only after ruling out real bugs, allow retries for a genuinely non-deterministic external dependency — retries mask flakiness, they do not fix it.

Section 5 — API, Network & Advanced (Deep)

Combining API and UI, and controlling the network

How do you design an API + UI hybrid test, and why?

Do slow, repetitive setup through the API and reserve the UI for the behaviour you are actually verifying.

The request fixture is a full HTTP client. Use it to create the preconditions fast — seed a user, place an order, set a feature flag — then switch to the page to assert the user-visible result. This keeps tests fast and focused: you are not clicking through five forms just to reach the screen under test. The two share nothing by default, but you can carry cookies/session between them when needed.

test('order shows in history', async ({ request, page }) => {
  const res = await request.post('/api/orders', { data: order });
  expect(res.ok()).toBeTruthy();
  await page.goto('/orders');
  await expect(page.getByText(order.id)).toBeVisible();
});

Explain the network interception options and a real use for each.

  • route.fulfill(...) — return a mock response. Use it to render a screen with fixed data, or to force an error state (status 500) that is hard to reproduce against a live backend.
  • route.abort() — block a request. Use it to drop images/analytics for speed, or to simulate an offline/failed resource.
  • route.fetch() then fulfill(...) — fetch the real response and tweak one field (e.g. flip a role to admin) to test a variant without backend changes.
  • routeFromHAR(...) — replay a recorded session so the whole test runs offline and deterministically.
Mocking is a determinism tool, not a shortcut

Mock third-party and error paths to make tests reliable, but keep a layer of real end-to-end tests against the actual backend — otherwise you are only testing your mocks. The interview-strong answer names both the power and the limit.

Section 6 — Architecture & Migration Scenarios (Deep)

Framework design & moving from Selenium/Java

Design a scalable Playwright + TypeScript framework. Walk me through the layers.

  • Config layer: playwright.config.ts with projects (browsers + setup/teardown), env-driven use options (baseURL, workers, retries via process.env), and reporters (html + blob for sharding).
  • Fixtures layer: custom fixtures inject page objects, per-role auth (storageState), and seeded data — this is the dependency-injection backbone.
  • Pages/components: POM with typed Locator fields; component objects for shared widgets; actions return page objects for fluent flows.
  • API layer: request-based service helpers with typed bodies for setup/verification.
  • Utils: data builders, custom expect matchers, a11y/visual helpers.
  • Tests/specs: thin, readable, locator-free; tagged for smoke/regression.
  • CI: GitHub Actions matrix + sharding, official Docker image, artifacts on failure, merged report, quality gate.

The interview signal is that behaviour lives in config + fixtures, tests stay thin, and everything is typed — so the suite scales to thousands of tests without turning into copy-paste.

How would you migrate a Java/Selenium suite to Playwright + TypeScript?

Migrate incrementally, feature by feature, starting with the flakiest and highest-value flows — never a risky big-bang rewrite.

  • Run both suites in parallel during the transition so coverage never drops.
  • Rebuild page objects with user-facing locators (getByRole/getByTestId) and delete explicit waits — auto-wait replaces them.
  • Adopt the runner idioms: fixtures instead of base classes, projects instead of testng.xml, storageState instead of per-test login.
  • Reuse what is language-agnostic: test data, environments, CI concepts, and the domain knowledge in your existing tests.
  • Measure the win: track flakiness rate and suite runtime before/after to justify the effort to stakeholders.

When is Java the right choice over TypeScript for Playwright?

When the team and stack are JVM-centric — a Selenium/Java shop, backend services in Java, engineers who own the tests writing Java daily. The Playwright capabilities are the same; you trade the TS runner’s config/fixtures/UI-Mode for JUnit/TestNG familiarity and easier integration with existing Java tooling. The honest answer names it as a team/ecosystem decision, not a capability gap.

Question Bank

Section A — Fundamentals & the Test Runner

QuestionModel Answer
Q1. What is @playwright/test?Playwright’s built-in test runner: it provides test(), expect(), fixtures, config, parallelism and reporters — the pieces Java delegates to JUnit/TestNG.
Q2. How is TS Playwright different from Java Playwright?Same browser API; TS adds the test runner (playwright.config.ts, projects, fixtures, built-in parallel/sharding, UI Mode). Java uses JUnit/TestNG for those.
Q3. How do you scaffold a project?npm init playwright@latest — installs @playwright/test, browsers, a config and sample tests.
Q4. Structure of a basic test?import { test, expect } from '@playwright/test'; test('name', async ({ page }) => { ... });
Q5. What is the page fixture?A fresh, isolated Page injected into each test automatically — no manual browser/context/page setup.
Q6. Why async/await everywhere?Every Playwright call returns a Promise; forgetting await causes race conditions and false results.
Q7. test.describe?Groups related tests and scopes hooks (beforeEach/afterEach) and config.
Q8. Available hooks?beforeAll, afterAll, beforeEach, afterEach — at file or describe scope.
Q9. test.only / test.skip / test.fixme?Run only this; skip; mark as known-broken (skipped). Use test.fail() for expected failures.
Q10. What is test.step()?Groups actions into a named, reported step — improves trace/report readability.
QuestionModel Answer
Q11. What are projects in the config?Named runs with their own settings — usually one per browser/device; also used for setup/teardown dependencies.
Q12. What is a fixture?A reusable, injected dependency (page objects, auth, data). Custom fixtures are made with test.extend().
Q13. Test vs worker fixtures?Test fixtures are recreated per test; worker fixtures ({ scope: "worker" }) are shared across tests in a worker (e.g. a logged-in state).
Q14. baseURL?Set in config use.baseURL so page.goto("/path") is relative and environment-independent.
Q15. What is auto-waiting?Actions wait for actionability (visible, stable, enabled, receives events) before running — same as Java.
Q16. What are web-first assertions?expect(locator).toXxx() assertions that auto-retry until they pass or time out.
Q17. Retrying vs non-retrying expect?expect(locator) auto-retries; expect(value) checks once immediately.
Q18. How do you run tests?npx playwright test (all), --project=chromium, -g "title", path/to/file.spec.ts.
Q19. UI Mode?npx playwright test --ui — watch mode with time-travel, DOM snapshots and locator picking. TS-only.
Q20. Is TS Playwright free/open-source?Yes — Apache 2.0, from Microsoft.

Section B — Locators & Interactions

What locators does Playwright provide (TS)?

page.getByRole('button', { name: 'Save' })
page.getByLabel('Email')   page.getByPlaceholder('Search')
page.getByText('Products')  page.getByTestId('submit')
page.locator('#id')  page.locator('//a')  // CSS / XPath
QuestionModel Answer
Q22. Locator vs ElementHandle?A Locator is lazy + auto-retrying (recommended); ElementHandle is a resolved reference that can go stale (avoid).
Q23. Why prefer getByRole?It targets the accessibility tree (role + name), resisting markup changes and doubling as an a11y check.
Q24. Exact vs substring text?getByText('x') is substring/case-insensitive; { exact: true } forces exact.
Q25. Regex match?getByText(/Total: \$[0-9]+/).
Q26. Filter a locator?loc.filter({ hasText: 'Backpack' }) or { has: page.getByRole('checkbox') }.
Q27. and() / or()?loc.and(other) needs both; loc.or(other) matches either (e.g. success or error).
Q28. nth / first / last / count?loc.nth(i) (0-based), loc.first(), loc.last(), await loc.count().
Q29. fill vs pressSequentially?fill() clears+sets instantly; pressSequentially() types key-by-key for autocomplete/masked inputs.
Q30. Dropdown?await loc.selectOption('value' | { label } | { index }).
Q31. Checkbox/radio?await loc.check()/uncheck(); await expect(loc).toBeChecked().

How do you handle dialogs, frames, tabs and downloads (TS)?

page.on('dialog', d => d.accept());        // register before trigger
await page.frameLocator('#f').getByLabel('Card').fill('4111');
const [popup] = await Promise.all([
  page.waitForEvent('popup'), page.getByText('Open').click() ]);
const [download] = await Promise.all([
  page.waitForEvent('download'), page.getByText('Export').click() ]);
await download.saveAs(await download.suggestedFilename());
QuestionModel Answer
Q33. Upload a file?await loc.setInputFiles('data/cv.pdf').
Q34. Shadow DOM?Handled automatically — locators pierce open shadow roots.
Q35. Hover / drag?await loc.hover(); await source.dragTo(target).
Q36. Keyboard?await loc.press('Control+A'); await page.keyboard.type('hi').
Q37. Scrolling?Automatic on actions; else loc.scrollIntoViewIfNeeded() / page.mouse.wheel(0,500).
Q38. Multiple elements loop?const items = await loc.all(); for (const it of items) { ... } — or loop with nth(i).

Section C — Assertions, Waiting & Debugging

List the common web-first assertions (TS).

await expect(loc).toBeVisible()/toBeHidden()/toBeEnabled()/toBeChecked();
await expect(loc).toHaveText(t)/toContainText(t)/toHaveValue(v);
await expect(loc).toHaveAttribute(n,v)/toHaveClass(c)/toHaveCount(n);
await expect(page).toHaveTitle(t)/toHaveURL(/regex/);
await expect(response).toBeOK();
QuestionModel Answer
Q40. Negate an assertion?await expect(loc).not.toBeVisible().
Q41. Soft assertions?expect.soft(loc).toHaveText(...) — collect failures without stopping; the test still fails at the end.
Q42. expect.poll?Polls a function until it returns the expected value: await expect.poll(() => getCount()).toBe(3).
Q43. toPass()?Retries a block of assertions until they all pass or time out: await expect(async () => {...}).toPass().
Q44. Per-assertion timeout?await expect(loc).toBeVisible({ timeout: 10000 }).
Q45. Configure expect globally?expect.configure({ timeout }) or expect.timeout in config.
Q46. Why avoid page.waitForTimeout()?It is a hard sleep — flaky/slow. Use auto-wait, waitForResponse, waitForURL, or web-first assertions.
Q47. Wait for a response?await page.waitForResponse('**/api/cart'); (or with an action in Promise.all).
Q48. Wait for navigation?await page.waitForURL('**/inventory').
Q49. Wait for element state?await loc.waitFor({ state: 'visible' | 'hidden' | 'attached' | 'detached' }).
Q50. Wait for a JS condition?await page.waitForFunction(() => window.ready === true).

How do you debug a failing TypeScript test?

  • UI Mode: npx playwright test --ui — time-travel every step with DOM snapshots.
  • --debug or PWDEBUG=1 opens the Inspector; add await page.pause().
  • Trace Viewer: set trace: "on-first-retry" then npx playwright show-trace.
  • HTML report: npx playwright show-report. Codegen to compare locators.
QuestionModel Answer
Q52. When are traces captured?Per config use.trace: off/on/retain-on-failure/on-first-retry (common).
Q53. Screenshots/video config?use.screenshot: 'only-on-failure'; use.video: 'retain-on-failure'.
Q54. Codegen?npx playwright codegen <url> — records clicks and generates TS + locators.
Q55. What does the trace contain?DOM snapshots, screenshots, console, network and a step timeline.
Q56. Difference between --ui and --debug?UI Mode is a watch/time-travel dashboard; --debug steps through live in the Inspector.

Section D — Fixtures, POM & Data-Driven

How do you write a Page Object in TypeScript?

import { Page, Locator } from '@playwright/test';
export class LoginPage {
  readonly page: Page;
  readonly loginButton: Locator;
  constructor(page: Page) {
    this.page = page;
    this.loginButton = page.getByRole('button', { name: 'Login' });
  }
  async login(u: string, p: string) { /* fill + click */ }
}

What are custom fixtures and why use them?

Custom fixtures inject ready-made objects (page objects, logged-in pages, test data) into tests — the idiomatic TS alternative to a Java base class + factory.

import { test as base } from '@playwright/test';
import { LoginPage } from '../pages/login.page';
export const test = base.extend<{ loginPage: LoginPage }>({
  loginPage: async ({ page }, use) => { await use(new LoginPage(page)); },
});
export { expect } from '@playwright/test';
QuestionModel Answer
Q59. How does a test consume a fixture?Destructure it: test('t', async ({ page, loginPage }) => { ... }).
Q60. Worker-scoped fixture use case?Expensive one-time setup shared across a worker’s tests (e.g. a seeded account or auth token).
Q61. Override a built-in fixture?Redefine it in test.extend or via test.use({ ... }) (e.g. test.use({ locale: "de-DE" })).
Q62. Automatic fixtures?Set { auto: true } so a fixture runs for every test without being requested (e.g. a logger).
Q63. Data-driven testing?Loop test() over an array of data; each iteration becomes its own reported test.
Q64. Read CSV/JSON data?Import a parser (csv-parse) or a JSON file, then loop test() over the rows.
Q65. Parameterize by project?Define projects with different use options (locale, viewport, baseURL) to run the same tests across configs.

How do you reuse authentication across tests?

Log in once in a setup project, save storageState to a file, and load it via config so every test starts authenticated.

// auth.setup.ts
setup('login', async ({ page }) => {
  await page.goto('/'); /* ...login... */
  await page.context().storageState({ path: 'auth/user.json' });
});
// playwright.config.ts
// { name: 'setup', testMatch: /auth\.setup\.ts/ },
// { name: 'chromium', dependencies: ['setup'], use: { storageState: 'auth/user.json' } }
QuestionModel Answer
Q67. Multiple roles auth?One storageState file per role, loaded by different projects or via test.use({ storageState }).
Q68. globalSetup vs setup project?globalSetup runs once before everything (no fixtures); a setup project is a normal test with fixtures + dependencies (preferred for auth).
Q69. Where do POM objects live?In /pages; expose them through fixtures so tests never construct them manually.
Q70. How to share state between steps?Fixtures or the page object instance — not global variables.
Q71. Reporter for CI?html + a machine format (junit/json), or allure-playwright.
Q72. test.slow()/test.setTimeout()?Mark a test slow (triples timeout) or set a custom per-test timeout.
Q73. Tag tests?Add a tag: test('checkout', { tag: '@smoke' }, ...) and run with --grep @smoke.
Q74. Annotate skips conditionally?test.skip(browserName === 'webkit', 'reason').

Section E — API, Network & Advanced

How do you test an API (TS)?

test('create user', async ({ request }) => {
  const res = await request.post('/api/users', { data: { name: 'naveed' } });
  expect(res.status()).toBe(201);
  expect(res.ok()).toBeTruthy();
  const body = await res.json();
  expect(body.name).toBe('naveed');
});
QuestionModel Answer
Q76. request fixture vs page.request?The request fixture is a standalone APIRequestContext; page.request shares the page’s cookies/session.
Q77. Send headers/auth?Pass { headers: { Authorization: `Bearer ${t}` } } per call or set extraHTTPHeaders on the context.
Q78. API + UI hybrid?Seed data via request, then assert it in the UI with page — fast setup, real verification.
Q79. Assert an API response?await expect(response).toBeOK() or check status()/json() with expect(value).
Q80. Intercept a request?await page.route('**/api', route => ...).
Q81. Mock a response?route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(data) }).
Q82. Block a request?route.abort() — e.g. block images/analytics to speed tests.
Q83. Modify a live response?const res = await route.fetch(); edit json; route.fulfill({ response: res, body }).
Q84. HAR record/replay?recordHar in context / await page.routeFromHAR('net.har') for offline tests.
Q85. Wait for a specific request?await page.waitForRequest('**/api') / waitForResponse('**/api').
QuestionModel Answer
Q86. Run JS in the page?await page.evaluate(() => document.title).
Q87. Run JS before load?await page.addInitScript(() => { window.__test = true; }).
Q88. Emulate dark mode/geolocation?test.use({ colorScheme: 'dark' }) / context geolocation + permissions.
Q89. Control time?await page.clock.install(); page.clock.setFixedTime(...); fastForward(...).
Q90. Visual & a11y checks?await expect(page).toHaveScreenshot() (built-in visual baseline); axe-core for a11y; toMatchAriaSnapshot for structure.

Section F — Config, Parallel, Sharding & CI

How does parallelism work in the TS runner?

  • Files run in parallel across worker processes (fullyParallel + workers).
  • Tests within a file run serially unless test.describe.configure({ mode: "parallel" }).
  • Each worker gets its own browser + isolated context automatically — no ThreadLocal.
QuestionModel Answer
Q92. Control worker count?workers in config or --workers=N; workers: 1 forces serial.
Q93. What is sharding?Split the suite across machines: --shard=1/3, 2/3, 3/3; merge reports afterward.
Q94. Merge sharded reports?Use the blob reporter per shard, then npx playwright merge-reports.
Q95. Retries?retries in config (e.g. CI ? 2 : 0); a test retried leaves a trace on first retry.
Q96. fullyParallel?Runs tests within files in parallel too, maximizing worker usage.
Q97. forbidOnly?CI setting that fails the build if a stray test.only is committed.
Q98. What is defineConfig?The typed helper you export from playwright.config.ts to declare all runner settings.
Q99. Projects for cross-browser?One project per device: { name: 'chromium', use: { ...devices['Desktop Chrome'] } }.
Q100. Project dependencies?dependencies: ['setup'] makes a project run after the setup project (auth).
QuestionModel Answer
Q101. globalSetup / globalTeardown?Functions in config that run once around the whole suite (seed DB, start a server).
Q102. Run in GitHub Actions?setup-node, npm ci, npx playwright install --with-deps, npx playwright test, upload the report artifact.
Q103. Run in Docker?Use mcr.microsoft.com/playwright:vX-jammy with browsers + deps preinstalled.
Q104. Reporters available?list, dot, line, html, json, junit, blob, plus allure-playwright.
Q105. Environment-specific config?Read process.env in the config (baseURL, workers, retries), driven by .env or CI variables.
Q106. Component testing?@playwright/experimental-ct-react/vue/svelte mounts components in a real browser.
Q107. Keep tests fast?storageState, API seeding, block heavy assets, fullyParallel + sharding, artifacts on failure only.
Q108. When choose Java over TS?When the team/stack is Java (Selenium shop, JVM services) — same Playwright power, different runner.

Section G — Scenarios & TypeScript Coding

Write: verify a product list is sorted A-Z.

const names = await page.locator('.inventory_item_name').allTextContents();
expect(names).toEqual([...names].sort());

Write: add all products and assert the cart badge.

const add = page.getByRole('button', { name: 'Add to cart' });
const n = await add.count();
for (let i = 0; i < n; i++) await add.first().click();  // each click turns that button into "Remove"
await expect(page.getByTestId('shopping-cart-badge')).toHaveText(String(n));

Write: mock an empty list and assert the empty state.

await page.route('**/api/products', r =>
  r.fulfill({ status: 200, contentType: 'application/json', body: '[]' }));
await page.goto('/products');
await expect(page.getByText('No products found')).toBeVisible();
QuestionModel Answer
Q112. Why Promise.all for popups?To start waiting for the event BEFORE the click that triggers it, avoiding a race.
Q113. await vs no-await bug?Missing await returns a pending Promise — assertions run too early and pass/fail wrongly.
Q114. Interface vs type in TS?Both describe shapes; interface is extendable/mergeable, type supports unions/intersections.
Q115. What is a union type?A value that can be one of several types, e.g. string | number.
Q116. Optional chaining / nullish?a?.b avoids errors on null; a ?? b uses b only when a is null/undefined.
Q117. map/filter/reduce?Array transforms — e.g. names.filter(n => n.includes("Bike")).
Q118. async function returns?A Promise; await it or chain .then().
Q119. Migrate Selenium to Playwright (TS)?Rebuild POM with user-facing locators, drop explicit waits, adopt the config/fixtures runner, migrate flakiest specs first.
Q120. Why is your framework maintainable?Fixtures inject POM + auth; locators live in page objects; config centralizes env/parallel; web-first assertions remove waits.

Rapid-Fire Round (One-Liners)

  • Runner package? @playwright/test. Config file? playwright.config.ts.
  • Fresh page per test? the page fixture.
  • Custom fixture? base.extend(). Worker fixture? { scope: "worker" }.
  • Run all? npx playwright test. UI mode? --ui. Debug? --debug.
  • Shard? --shard=1/3. Workers? --workers=N. Serial? workers:1.
  • By title? -g "text". One project? --project=chromium.
  • Visual baseline? await expect(page).toHaveScreenshot().
  • Soft assert? expect.soft(). Poll? expect.poll(). Retry block? toPass().
  • Negate? expect(loc).not... Timeout? { timeout: ms }.
  • Reuse login? storageState + setup project dependency.
  • API? request fixture. Assert? expect(res).toBeOK().
  • Mock? page.route(...).fulfill(). Block? route.abort().
  • Run JS? page.evaluate(). Before load? addInitScript().
  • Dialog? page.on("dialog", d => d.accept()) before trigger.
  • Popup/download? Promise.all([waitForEvent, action]).
  • Trace? use.trace:"on-first-retry"; show-trace to open.
  • Codegen? npx playwright codegen. Report? show-report.
  • Cross-browser? projects with devices[...]. CI image? mcr.microsoft.com/playwright.
  • Merge shard reports? blob reporter + merge-reports.
  • Component test? @playwright/experimental-ct-*.

120+ TypeScript questions — runner, fixtures & all. ⚡

FAQs

What are fixtures in Playwright Test?

Reusable setup and teardown that tests receive as parameters (page, context, request and your own). Fixtures can be test- or worker-scoped and replace most beforeEach/afterEach hooks.

What is sharding in Playwright?

Splitting a test suite across several machines with --shard=1/4 and similar, so each CI job runs a portion; reports from all shards can be merged afterwards.

How do you share login between tests in Playwright Test?

Use a setup project that logs in and saves storageState to a file, and make the other projects depend on it and load that state.