Playwright Challenges
Pick the correct answer for each Playwright question. Instant feedback with detailed explanations. Covers real interview topics.
Challenge 1
Beginner
Locators
Solved
Which locator should you prefer first?
Playwright recommends a priority order for choosing locators. Which is the most preferred option for resilient tests?
Answer: C — getByRole
Playwright's official recommendation: prefer user-facing attributes in this order: 1)
Playwright's official recommendation: prefer user-facing attributes in this order: 1)
getByRole (most resilient — mirrors how users and screen readers interact), 2) getByLabel, 3) getByPlaceholder, 4) getByText, 5) getByTestId. CSS selectors and XPath are brittle because they depend on implementation details that change with styling refactors.
Challenge 2
Beginner
Locators
Solved
How do you locate a "Sign In" submit button?
Using
getByRole, which call correctly targets a button with the visible text "Sign In"?<button type="submit">Sign In</button>
Answer: B — page.getByRole('button', { name: 'Sign In' })
getByRole uses ARIA roles. A <button> element always has the implicit ARIA role of 'button' — not 'submit' (that's a type attribute, not a role). The name option matches the accessible name, which for a button is its text content. Option A fails because 'submit' is not a valid ARIA role.
Challenge 3
Intermediate
Assertions
Solved
toHaveText vs toContainText — which passes?
An element contains the text
"Welcome, Alice!". Which assertion passes?// Element inner text: "Welcome, Alice!"
await expect(heading).toHaveText('Alice'); // (A)
await expect(heading).toContainText('Alice'); // (B)
await expect(heading).toHaveText(/Alice/); // (C)
await expect(heading).toContainText('Welcome'); // (D)
Answer: C — B, C and D pass; A fails
Use
toHaveText('Alice') requires the element's full text to equal 'Alice' exactly — it fails because the text is 'Welcome, Alice!'.toContainText('Alice') only requires a substring match — passes.toHaveText(/Alice/) uses a regex — matches anywhere in the text — passes.toContainText('Welcome') — substring match — passes.Use
toContainText for partial matches; toHaveText for exact full-text.
Challenge 4
Beginner
Auto-Waiting
Solved
What does Playwright automatically wait for before clicking?
Playwright's auto-wait mechanism checks several conditions before performing an action. Which set is correct?
Answer: C — Attached, visible, stable, enabled, and receives events
Before a
Before a
click(), Playwright automatically waits for the element to be:
1) Attached to the DOM,
2) Visible (not hidden/display:none),
3) Stable (not animating),
4) Enabled (not disabled),
5) Receives pointer events (not obscured by another element).
This eliminates most flaky test patterns that require manual waitForSelector calls. The default action timeout is 30 seconds.
Challenge 5
Intermediate
Strict Mode
Solved
What happens when a locator matches 2 elements?
Playwright locators are strict by default. What happens when you call
click() on a locator that resolves to two elements?// Page has two <button class="save"> elements
await page.locator('button.save').click();
Answer: C — Throws an error
Playwright locators are strict: if an action targets more than one element, it throws
Playwright locators are strict: if an action targets more than one element, it throws
Error: strict mode violation: locator('button.save') resolved to 2 elements. This prevents accidental interactions with the wrong element. To target a specific one, use .first(), .last(), .nth(n), or a more specific locator. This is intentional — ambiguity is a test design smell.
Challenge 6
Beginner
Fixtures
Solved
What does the page fixture provide?
Playwright's built-in
page fixture is injected into every test. What does it represent?test('example', async ({ page }) => {
// What is page?
});
Answer: B — An isolated browser tab scoped to this one test
The
The
page fixture is a Page object — a single browser tab. Each test gets its own page in a fresh browser context (isolated cookies, localStorage, etc.). After the test finishes, Playwright closes it automatically. This isolation is why Playwright tests don't interfere with each other even when run in parallel. The hierarchy is: Browser → BrowserContext → Page.
Challenge 7
Intermediate
Configuration
Solved
Where do you set the default action timeout globally?
You want all actions (click, fill, etc.) to time out after 10 seconds by default. Where is the correct place in
playwright.config.ts?export default defineConfig({
timeout: 60_000, // (A) top-level
use: {
actionTimeout: 10_000, // (B) inside use
navigationTimeout: 15_000,
},
expect: {
timeout: 5_000, // (C) inside expect
}
});
Answer: B — use.actionTimeout
Playwright config has separate timeout controls:
Playwright config has separate timeout controls:
timeout (top-level) = max time per entire test. use.actionTimeout = max time for each individual action (click, fill, etc.). use.navigationTimeout = max time for navigation actions (goto, waitForNavigation). expect.timeout = max time for assertions. They are independent — setting one does not affect the others.
Challenge 8
Beginner
Test Hooks
Solved
What is the scope of test.beforeEach?
You have a
test.beforeEach inside a test.describe block. Which tests does it run before?test.describe('Login', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/login');
});
test('valid login', async ({ page }) => { /* ... */ });
test('invalid login', async ({ page }) => { /* ... */ });
});
test('homepage', async ({ page }) => { /* ... */ });
Answer: B — Only the two tests inside the describe block
test.beforeEach defined inside a test.describe block has that describe block as its scope. It runs before each test within that block only — not before tests outside it. The 'homepage' test is outside the 'Login' describe, so the hook does not run for it. To apply a hook globally, define it at the top level of the file.
Challenge 9
Advanced
Network
Solved
What does this page.route() call do?
Read the code and identify what happens when the app makes an API request to
/api/users.await page.route('**/api/users', async route => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify([{ id: 1, name: 'Alice' }])
});
});
Answer: C — Intercepts and returns a mock response
page.route(pattern, handler) intercepts matching network requests. route.fulfill() responds with synthetic data — the real server is never called. This is perfect for testing UI behaviour with controlled data, testing error states (e.g., status: 500), or running tests without a backend. Use route.continue() to pass through to the real server, or route.abort() to simulate network failure.
Challenge 10
Intermediate
Fixtures
Solved
How do you create a custom fixture in Playwright?
Which pattern correctly extends Playwright's
test with a custom loginPage fixture?// fixtures.ts
import { test as base } from '@playwright/test';
export const test = base.extend<{ loginPage: LoginPage }>({
loginPage: async ({ page }, use) => {
const lp = new LoginPage(page);
await use(lp); // value available inside test
// teardown here if needed
}
});
Answer: B — Correct pattern
base.extend() creates a new test object with additional fixtures. The fixture function receives built-in fixtures (like page) and a use callback. Everything before await use(value) is setup; everything after is teardown. The exported test is used in test files instead of Playwright's default test. This is the official Playwright pattern for Page Object injection and is far cleaner than beforeEach.
Challenge 11
Beginner
Assertions
Solved
What does this assertion verify?
After a login action, you want to confirm the user landed on the dashboard. Which assertion is correct and why?
await expect(page).toHaveURL('/dashboard');
Answer: B — Auto-retries until URL matches
Playwright's
Playwright's
expect(page).toHaveURL() is a web-first assertion. It automatically retries — polling the current URL — until it matches or the timeout expires. This means you don't need a manual waitForURL() before the assertion. The string '/dashboard' is matched as a substring by default (or you can pass a regex for full control). It does not navigate — it only asserts.
Challenge 12
Advanced
Parallelism
Solved
What is the difference between parallel and default execution?
Playwright has two modes for running tests inside a
describe block. What is the key difference?// Mode A
test.describe('Suite', () => {
test('test 1', async ({ page }) => { ... });
test('test 2', async ({ page }) => { ... });
});
// Mode B
test.describe.parallel('Suite', () => {
test('test 1', async ({ page }) => { ... });
test('test 2', async ({ page }) => { ... });
});
Answer: C — Serial vs parallel within workers
By default (Mode A), tests inside a
By default (Mode A), tests inside a
describe run serially (one after another) in the same worker. test.describe.parallel (Mode B) allows the tests in the block to run concurrently across available workers. This reduces total test time but requires tests to be fully independent (no shared state). Note: file-level parallelism is controlled by the workers config option — .parallel applies within a single describe block.