Playwright interviews test judgment, not vocabulary: an interviewer who hears "getByRole is better than CSS selectors" will follow up with why, and "it's more readable" is not a passing answer. Knowing that getByRole locators break when the accessible name changes, not when the CSS class changes, is the kind of depth that separates junior from senior answers. This guide covers the questions that appear most in interviews grouped by topic, with what a good answer includes and where most candidates fall short.
Core concepts
What is Playwright and how is it different from Selenium?Playwright is a Microsoft-built test automation framework for web applications. The key architectural differences from Selenium:
Auto-waiting: Playwright waits for elements to be actionable before interacting, so no WebDriverWait or ExpectedConditions are needed. Selenium requires explicit waiting code.
Built-in everything: Playwright ships with a test runner, assertion library, trace viewer, and HTML reporter. Selenium is a browser automation library, so you assemble the rest yourself.
Browser protocol: Playwright uses CDP (Chrome DevTools Protocol) for Chromium and similar protocols for Firefox and WebKit. Selenium uses the WebDriver protocol, which is slower due to more network round-trips.
Language-first design: Playwright's JavaScript/TypeScript API uses native async/await. Selenium's JS bindings were added after the Java implementation and feel like it.
Before executing an action, Playwright checks that the target element meets a set of "actionability" conditions. For click(): the element must be visible, in the viewport, not covered by another element, enabled, and stable (not moving due to animation). For fill(): the element must be visible and editable.
These checks run automatically on a polling loop until they pass or the timeout is reached. You can see the actionability check in the error message when a test fails. It tells you exactly which condition wasn't met.
What is the difference betweenpage.locator() and the getBy* methods?
page.locator() takes a CSS selector, XPath, or text string. It's a general-purpose locator.
The getBy* methods (getByRole, getByLabel, getByText, getByTestId, etc.) are semantic locators. They find elements the way a user would identify them, using accessibility attributes rather than CSS structure.
getBy* methods are preferred because they're more resilient to UI changes: renaming a CSS class doesn't break them, but changing a button's accessible name does, which is exactly the kind of change that affects real users.
Explain Playwright fixtures.
Fixtures are Playwright's dependency injection system. They provide pre-built objects to tests without requiring manual setup in each test. The built-in fixtures are: page (a browser page), context (a browser context), browser (the browser instance), request (an HTTP client), and browserName.
Custom fixtures extend these:
const test = base.extend({
loggedInPage: async ({ page }, use) => {
await page.goto('https://lab.becomeqa.com');
await page.getByRole('button', { name: 'Login' }).click();
await page.getByLabel('Username').fill('admin@becomeqa.com');
await page.getByLabel('Password').fill('testpass123');
await page.getByRole('button', { name: 'Submit' }).click();
await use(page); // hand the logged-in page to the test
}
});
test('dashboard shows items', async ({ loggedInPage }) => {
await expect(loggedInPage.getByText('My Travel Items')).toBeVisible();
});Each Playwright test runs in its own browser context by default: a clean slate with no cookies, no localStorage, no cached data from other tests. This means tests don't interfere with each other even when running in parallel.
You can share a context between tests within a test.describe block using test.describe.configure({ mode: 'serial' }), but this reduces isolation and should be used sparingly.
Locators and assertions
When would you usegetByTestId over getByRole?
getByRole is preferred when the element has a meaningful ARIA role and accessible name. getByTestId is useful when:
The element doesn't have a clear semantic role (a custom component that renders as a generic div).
The accessible name is dynamic or non-deterministic.
Developers have explicitly added data-testid attributes as a stable test hook.
The practical rule: use getByRole first, fall back to getByTestId when getByRole doesn't work cleanly.
expect(locator).toBeVisible() actually check?
It checks that the element is present in the DOM, not hidden via CSS (display: none, visibility: hidden, opacity: 0), and has non-zero dimensions. An element with display: none fails this check. An element scrolled out of viewport but not hidden still passes.
toBeVisible() also auto-retries. It keeps checking until the assertion passes or the timeout expires. This is different from a simple boolean check.
How do you assert on multiple elements at once?
// Check count
await expect(page.getByRole('row')).toHaveCount(6);
// Check that all items in a list contain text
const items = page.getByRole('listitem');
await expect(items).toContainText(['Tokyo', 'Paris', 'Oslo']);
// Check text of each item
await expect(items).toHaveText(['Tokyo', 'Paris', 'Oslo']); // exact matchpage.waitForSelector() and using a locator assertion?
page.waitForSelector() is a lower-level API that waits for the element to appear in the DOM. It predates Playwright's locator API.
Locator assertions like await expect(locator).toBeVisible() are the modern approach. They're more expressive, auto-retry, and work consistently with Playwright's locator system. Use assertions in tests, not waitForSelector.
Network and API
How do you intercept a network request in Playwright?// Intercept and modify a response
await page.route('**/api/items', route => {
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify([{ id: 1, destination: 'Mocked City' }])
});
});
// Block specific requests
await page.route('**/*.png', route => route.abort());
// Modify request headers
await page.route('**/api/**', route => {
route.continue({
headers: { ...route.request().headers(), 'X-Custom-Header': 'test' }
});
});page.request and the request fixture?
page.request shares the browser context. It sends requests with the same cookies and authentication state as the current browser page.
The request fixture is a standalone HTTP client with no browser session. Use request for pure API tests that don't need browser state. Use page.request when you need to make API calls with the currently logged-in user's session.
// Wait for a specific request and response
const responsePromise = page.waitForResponse('**/api/items');
await page.getByRole('button', { name: 'Refresh' }).click();
const response = await responsePromise;
expect(response.status()).toBe(200);Configuration and CI
How do you run Playwright tests in GitHub Actions?# .github/workflows/playwright.yml
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
- run: npm ci
- run: npx playwright install --with-deps
- run: npx playwright test
- uses: actions/upload-artifact@v4
if: always()
with:
name: playwright-report
path: playwright-report/Key points: npm ci instead of npm install for reproducibility. npx playwright install --with-deps installs browsers and their system dependencies. if: always() on the report upload ensures you get the report even when tests fail.
Sharding splits your test suite across multiple machines. Each shard runs a subset of tests:
npx playwright test --shard=1/4 # this machine runs 25% of tests
npx playwright test --shard=2/4
npx playwright test --shard=3/4
npx playwright test --shard=4/4Use it when your suite takes more than 5 minutes on a single machine and you want faster CI feedback. The tests must be isolated (no shared state) for sharding to work correctly.
What doestrace: 'on-first-retry' do in playwright.config.ts?
When a test fails and is retried, Playwright records a trace: a complete recording of the test execution including DOM snapshots, network requests, screenshots at each step, and console logs. The trace is saved to the test results folder and viewable with npx playwright show-report or by uploading to trace.playwright.dev.
Architecture and patterns
When would you use Page Object Model vs plain tests?Use plain tests when: you have a small number of tests (under 20), the tests are exploratory, or you're just getting started.
Use POM when: multiple tests interact with the same page, locators are repeated across test files, or the UI changes frequently and you want to update locators in one place.
The practical trigger: when you find yourself copy-pasting the same 5+ lines across tests, it's time for a page object.
How do you handle authentication across tests without logging in for each test?storageState saves and restores browser cookies and localStorage. Run login once in a global setup file, save the state, and configure tests to load it:
// global-setup.ts
import { chromium } from '@playwright/test';
export default async function globalSetup() {
const browser = await chromium.launch();
const page = await browser.newPage();
await page.goto('https://lab.becomeqa.com');
await page.getByRole('button', { name: 'Login' }).click();
await page.getByLabel('Username').fill('admin@becomeqa.com');
await page.getByLabel('Password').fill('testpass123');
await page.getByRole('button', { name: 'Submit' }).click();
await page.context().storageState({ path: 'auth.json' });
await browser.close();
}// playwright.config.ts
export default defineConfig({
globalSetup: './global-setup.ts',
use: { storageState: 'auth.json' }
});In order: environment variables (secrets missing in CI), base URL (hardcoded localhost), timing (CI runners are slower, so add retries: 1), missing await, browser version differences. Download the CI trace artifact and compare the step-by-step execution with the local trace.
PWDEBUG=1 npx playwright test opens the inspector with step-by-step execution. Use it to understand what Playwright is actually doing before assuming there's a bug.FAQ
Should I know Playwright internals for a senior interview?Understanding the CDP protocol, how auto-waiting is implemented, and how fixtures work under the hood shows depth. You won't be asked to implement them, but explaining "auto-waiting polls actionability checks on a timer with exponential backoff" is more impressive than "it waits automatically."
How do I demonstrate Playwright experience if I haven't used it professionally?A GitHub repo with 20+ tests covering a real app's login, CRUD operations, and API calls is concrete evidence. lab.becomeqa.com is specifically designed for this. Walk the interviewer through your test structure and explain why you made the architectural choices you did.