50+ real interactive Playwright labs.
Every lab below is a real, working widget you can write Playwright tests against. No toys — these are the exact scenarios you'll face in interviews and on production codebases. Click any lab, write the test, copy the solution.
Dynamic Locators
Locate elements whose IDs change on every page load using resilient Playwright locators like [id^="submit-"] regex matches.
const { test, expect } = require('@playwright/test');
test('dynamic id locator', async ({ page }) => {
await page.goto('/playwright-labs#dynamic-locators');
class=class="s">"c">// Regex match — survives id changes
const btn = page.locator('button[id^="submit-"]');
await expect(btn).toBeVisible();
await btn.click();
await expect(page.locator('#result-dynamic-id')).toHaveText('Clicked!');
});
import { test, expect } from '@playwright/test';
test('dynamic id locator', async ({ page }) => {
await page.goto('/playwright-labs#dynamic-locators');
const btn = page.locator('button[id^="submit-"]');
await expect(btn).toBeVisible();
await btn.click();
await expect(page.locator('#result-dynamic-id')).toHaveText('Clicked!');
});
Auto Waiting
Playwright auto-waits for elements to be visible, enabled, and stable before interacting — no more flaky page.waitForTimeout() calls.
const { test, expect } = require('@playwright/test');
test('auto-wait for delayed button', async ({ page }) => {
await page.goto('/playwright-labs#auto-waiting');
class=class="s">"c">// No sleep — Playwright auto-waits up to 5s by default
await page.locator('#delayed-button').click();
await expect(page.locator('#result-auto-wait')).toHaveText('Loaded!');
});
import { test, expect } from '@playwright/test';
test('auto-wait for delayed button', async ({ page }) => {
await page.goto('/playwright-labs#auto-waiting');
await page.locator('#delayed-button').click();
await expect(page.locator('#result-auto-wait')).toHaveText('Loaded!');
});
Frames (iFrame)
Use Playwright's frameLocator() to interact with elements inside same-origin iframes. Modern alternative to legacy frame(name) API.
const { test, expect } = require('@playwright/test');
test('iframe interaction', async ({ page }) => {
await page.goto('/playwright-labs#frames');
const frame = page.frameLocator('#lab-iframe');
await frame.locator('#iframe-button').click();
await expect(frame.locator('#iframe-result')).toHaveText('Iframe clicked!');
});
import { test, expect } from '@playwright/test';
test('iframe interaction', async ({ page }) => {
await page.goto('/playwright-labs#frames');
const frame = page.frameLocator('#lab-iframe');
await frame.locator('#iframe-button').click();
await expect(frame.locator('#iframe-result')).toHaveText('Iframe clicked!');
});
Nested Frames
Chain frameLocator() calls to drill into frames within frames — the modern alternative to traversing window.frames.
const { test, expect } = require('@playwright/test');
test('nested iframes', async ({ page }) => {
await page.goto('/playwright-labs#nested-frames');
const inner = page.frameLocator('#outer-frame').frameLocator('#inner-frame');
await inner.locator('#nested-button').click();
await expect(inner.locator('#nested-result')).toHaveText('Nested click!');
});
import { test, expect } from '@playwright/test';
test('nested iframes', async ({ page }) => {
await page.goto('/playwright-labs#nested-frames');
const inner = page.frameLocator('#outer-frame').frameLocator('#inner-frame');
await inner.locator('#nested-button').click();
await expect(inner.locator('#nested-result')).toHaveText('Nested click!');
});
Drag and Drop
Use dragTo() for simple HTML5 drag and drop, or manual mouse.down()/move()/up() for pixel-perfect scenarios.
const { test, expect } = require('@playwright/test');
test('drag and drop card', async ({ page }) => {
await page.goto('/playwright-labs#drag-and-drop');
const card = page.locator('[data-testid="card-a"]');
const done = page.locator('[data-testid="col-done"]');
await card.dragTo(done);
await expect(done.locator('[data-testid="card-a"]')).toBeVisible();
});
import { test, expect } from '@playwright/test';
test('drag and drop card', async ({ page }) => {
await page.goto('/playwright-labs#drag-and-drop');
const card = page.locator('[data-testid="card-a"]');
const done = page.locator('[data-testid="col-done"]');
await card.dragTo(done);
await expect(done.locator('[data-testid="card-a"]')).toBeVisible();
});
Alert Dialogs
Auto-dismiss native window.alert() dialogs by registering a page.on('dialog') handler before triggering the action.
const { test, expect } = require('@playwright/test');
test('handle alert dialog', async ({ page }) => {
const alertMsg = [];
page.on('dialog', async d => {
alertMsg.push(d.message());
await d.accept();
});
await page.goto('/playwright-labs#alerts');
await page.locator('#alert-button').click();
await expect(page.locator('#result-alert')).toHaveText('Alert accepted');
expect(alertMsg[0]).toBe('Hello from QA CSEIAN!');
});
import { test, expect } from '@playwright/test';
test('handle alert dialog', async ({ page }) => {
const alertMsg: string[] = [];
page.on('dialog', async (d) => {
alertMsg.push(d.message());
await d.accept();
});
await page.goto('/playwright-labs#alerts');
await page.locator('#alert-button').click();
await expect(page.locator('#result-alert')).toHaveText('Alert accepted');
expect(alertMsg[0]).toBe('Hello from QA CSEIAN!');
});
Confirm Dialogs
Accept or dismiss window.confirm() dialogs programmatically by passing true/false to dialog.accept().
const { test, expect } = require('@playwright/test');
test('handle confirm dialog', async ({ page }) => {
page.on('dialog', async d => await d.accept());
await page.goto('/playwright-labs#confirm-dialogs');
await page.locator('#confirm-button').click();
await expect(page.locator('#result-confirm')).toHaveText('Confirmed');
});
import { test, expect } from '@playwright/test';
test('handle confirm dialog', async ({ page }) => {
page.on('dialog', async (d) => d.accept());
await page.goto('/playwright-labs#confirm-dialogs');
await page.locator('#confirm-button').click();
await expect(page.locator('#result-confirm')).toHaveText('Confirmed');
});
Prompt Dialogs
Provide text input to window.prompt() dialogs by passing the text to dialog.accept('text').
const { test, expect } = require('@playwright/test');
test('handle prompt dialog with input', async ({ page }) => {
page.on('dialog', async d => await d.accept('Playwright'));
await page.goto('/playwright-labs#prompt-dialogs');
await page.locator('#prompt-button').click();
await expect(page.locator('#result-prompt')).toHaveText('Hello, Playwright!');
});
import { test, expect } from '@playwright/test';
test('handle prompt dialog with input', async ({ page }) => {
page.on('dialog', async (d) => d.accept('Playwright'));
await page.goto('/playwright-labs#prompt-dialogs');
await page.locator('#prompt-button').click();
await expect(page.locator('#result-prompt')).toHaveText('Hello, Playwright!');
});
File Upload
Upload files via setInputFiles() — works even on hidden inputs without triggering the OS file picker.
const { test, expect } = require('@playwright/test');
const path = require('path');
test('file upload', async ({ page }) => {
await page.goto('/playwright-labs#file-upload');
await page.locator('input#file-input').setInputFiles({
name: 'sample.csv',
mimeType: 'text/csv',
buffer: Buffer.from('id,name\n1,QACSEIAN')
});
await expect(page.locator('#result-upload')).toContainText('sample.csv');
});
import { test, expect } from '@playwright/test';
test('file upload', async ({ page }) => {
await page.goto('/playwright-labs#file-upload');
await page.locator('input#file-input').setInputFiles({
name: 'sample.csv',
mimeType: 'text/csv',
buffer: Buffer.from('id,name\n1,QACSEIAN')
});
await expect(page.locator('#result-upload')).toContainText('sample.csv');
});
Multiple File Upload
Upload multiple files in a single setInputFiles() call by passing an array.
const { test, expect } = require('@playwright/test');
test('multiple file upload', async ({ page }) => {
await page.goto('/playwright-labs#multiple-file-upload');
await page.locator('input#multi-file-input').setInputFiles([
{ name: 'a.txt', mimeType: 'text/plain', buffer: Buffer.from('aaa') },
{ name: 'b.txt', mimeType: 'text/plain', buffer: Buffer.from('bbb') }
]);
await expect(page.locator('#result-multi-upload')).toContainText('a.txt');
await expect(page.locator('#result-multi-upload')).toContainText('b.txt');
});
import { test, expect } from '@playwright/test';
test('multiple file upload', async ({ page }) => {
await page.goto('/playwright-labs#multiple-file-upload');
await page.locator('input#multi-file-input').setInputFiles([
{ name: 'a.txt', mimeType: 'text/plain', buffer: Buffer.from('aaa') },
{ name: 'b.txt', mimeType: 'text/plain', buffer: Buffer.from('bbb') }
]);
await expect(page.locator('#result-multi-upload')).toContainText('a.txt');
await expect(page.locator('#result-multi-upload')).toContainText('b.txt');
});
File Download
Capture file downloads via the download event and save them to disk with download.saveAs().
const { test, expect } = require('@playwright/test');
const fs = require('fs');
test('file download', async ({ page }) => {
await page.goto('/playwright-labs#file-download');
const [download] = await Promise.all([
page.waitForEvent('download'),
page.locator('#download-button').click()
]);
expect(download.suggestedFilename()).toBe('qa-cseian-report.txt');
const path = await download.path();
const content = fs.readFileSync(path, 'utf8');
expect(content).toContain('QA CSEIAN');
});
import { test, expect } from '@playwright/test';
import fs from 'fs';
test('file download', async ({ page }) => {
await page.goto('/playwright-labs#file-download');
const [download] = await Promise.all([
page.waitForEvent('download'),
page.locator('#download-button').click()
]);
expect(download.suggestedFilename()).toBe('qa-cseian-report.txt');
const path = await download.path();
const content = fs.readFileSync(path!, 'utf8');
expect(content).toContain('QA CSEIAN');
});
New Window
Capture new windows opened via window.open() using page.waitForEvent('popup').
const { test, expect } = require('@playwright/test');
test('new window popup', async ({ page }) => {
await page.goto('/playwright-labs#new-window');
const [popup] = await Promise.all([
page.waitForEvent('popup'),
page.locator('#popup-button').click()
]);
await popup.waitForLoadState();
await expect(popup.locator('h1')).toHaveText('Popup Window');
await popup.close();
});
import { test, expect, Page } from '@playwright/test';
test('new window popup', async ({ page }) => {
await page.goto('/playwright-labs#new-window');
const [popup] = await Promise.all([
page.waitForEvent('popup') as Promise<Page>,
page.locator('#popup-button').click()
]);
await popup.waitForLoadState();
await expect(popup.locator('h1')).toHaveText('Popup Window');
await popup.close();
});
Multiple Tabs
Same as new window — Playwright treats every browser tab as a new Page context. Use page.context().pages to enumerate.
const { test, expect } = require('@playwright/test');
test('multiple tabs', async ({ page }) => {
await page.goto('/playwright-labs#multiple-tabs');
await page.locator('#popup-button').click();
await page.locator('#popup-button').click();
class=class="s">"c">// wait for both popups
await page.waitForTimeout(500);
const pages = page.context().pages;
expect(pages.length).toBe(3); class=class="s">"c">// original + 2 popups
});
import { test, expect } from '@playwright/test';
test('multiple tabs', async ({ page }) => {
await page.goto('/playwright-labs#multiple-tabs');
await page.locator('#popup-button').click();
await page.locator('#popup-button').click();
await page.waitForTimeout(500);
const pages = page.context().pages;
expect(pages.length).toBe(3);
});
Hover
Trigger CSS :hover states reliably with locator.hover() — useful for tooltips, dropdown menus, and revealing hidden elements.
const { test, expect } = require('@playwright/test');
test('hover reveals tooltip', async ({ page }) => {
await page.goto('/playwright-labs#hover');
await page.locator('#hover-target').hover();
await expect(page.locator('#hover-tooltip')).toBeVisible();
await expect(page.locator('#hover-tooltip')).toHaveText('You hovered!');
});
import { test, expect } from '@playwright/test';
test('hover reveals tooltip', async ({ page }) => {
await page.goto('/playwright-labs#hover');
await page.locator('#hover-target').hover();
await expect(page.locator('#hover-tooltip')).toBeVisible();
await expect(page.locator('#hover-tooltip')).toHaveText('You hovered!');
});
Keyboard Actions
Use page.keyboard to type text, press keys, and trigger shortcuts like Ctrl+Enter or Shift+Tab.
const { test, expect } = require('@playwright/test');
test('keyboard input', async ({ page }) => {
await page.goto('/playwright-labs#keyboard-actions');
await page.locator('#kbd-input').click();
await page.keyboard.type('QA CSEIAN');
await page.keyboard.press('Enter');
await expect(page.locator('#result-keyboard')).toHaveText('Submitted: QA CSEIAN');
});
import { test, expect } from '@playwright/test';
test('keyboard input', async ({ page }) => {
await page.goto('/playwright-labs#keyboard-actions');
await page.locator('#kbd-input').click();
await page.keyboard.type('QA CSEIAN');
await page.keyboard.press('Enter');
await expect(page.locator('#result-keyboard')).toHaveText('Submitted: QA CSEIAN');
});
Mouse Actions
Use page.mouse for pixel-perfect interactions — down(), move(), up(), and wheel() for scroll.
const { test, expect } = require('@playwright/test');
test('mouse click at coordinates', async ({ page }) => {
await page.goto('/playwright-labs#mouse-actions');
const box = await page.locator('#mouse-area').boundingBox();
await page.mouse.move(box.x + 50, box.y + 50);
await page.mouse.down();
await page.mouse.up();
await expect(page.locator('#result-mouse')).toContainText('50,50');
});
import { test, expect } from '@playwright/test';
test('mouse click at coordinates', async ({ page }) => {
await page.goto('/playwright-labs#mouse-actions');
const box = await page.locator('#mouse-area').boundingBox();
await page.mouse.move(box!.x + 50, box!.y + 50);
await page.mouse.down();
await page.mouse.up();
await expect(page.locator('#result-mouse')).toContainText('50,50');
});
Shadow DOM
Playwright pierces open AND closed shadow roots by default — no special CSS combinators needed.
const { test, expect } = require('@playwright/test');
test('shadow DOM click', async ({ page }) => {
await page.goto('/playwright-labs#shadow-dom');
class=class="s">"c">// Playwright pierces shadow DOM by default
await page.locator('#shadow-button').click();
await expect(page.locator('#shadow-host')).toContainText('Shadow clicked!');
});
import { test, expect } from '@playwright/test';
test('shadow DOM click', async ({ page }) => {
await page.goto('/playwright-labs#shadow-dom');
await page.locator('#shadow-button').click();
await expect(page.locator('#shadow-host')).toContainText('Shadow clicked!');
});
Infinite Scroll
Use page.mouse.wheel() or scrollIntoViewIfNeeded() to load additional content as the user scrolls.
const { test, expect } = require('@playwright/test');
test('infinite scroll', async ({ page }) => {
await page.goto('/playwright-labs#infinite-scroll');
const list = page.locator('#scroll-list li');
await expect(list).toHaveCount(5);
class=class="s">"c">// Scroll to bottom multiple times
for (let i = 0; i < 4; i++) {
await page.locator('#scroll-list').lastElementChild().scrollIntoViewIfNeeded();
await page.waitForTimeout(300);
}
await expect(list).toHaveCount(20, { timeout: 5000 });
});
import { test, expect } from '@playwright/test';
test('infinite scroll', async ({ page }) => {
await page.goto('/playwright-labs#infinite-scroll');
const list = page.locator('#scroll-list li');
await expect(list).toHaveCount(5);
for (let i = 0; i < 4; i++) {
await page.locator('#scroll-list li').last().scrollIntoViewIfNeeded();
await page.waitForTimeout(300);
}
await expect(list).toHaveCount(20, { timeout: 5000 });
});
Lazy Loading
Wait for IntersectionObserver-triggered image loads using Playwright's network idle or img loading states.
const { test, expect } = require('@playwright/test');
test('lazy loaded images', async ({ page }) => {
await page.goto('/playwright-labs#lazy-loading');
const imgs = page.locator('#lazy-grid img');
class=class="s">"c">// Scroll each into view to trigger loading
for (let i = 0; i < 6; i++) {
await imgs.nth(i).scrollIntoViewIfNeeded();
}
class=class="s">"c">// All should have loaded now
for (let i = 0; i < 6; i++) {
const w = await imgs.nth(i).evaluate(el => el.naturalWidth);
expect(w).toBeGreaterThan(0);
}
});
import { test, expect } from '@playwright/test';
test('lazy loaded images', async ({ page }) => {
await page.goto('/playwright-labs#lazy-loading');
const imgs = page.locator('#lazy-grid img');
for (let i = 0; i < 6; i++) {
await imgs.nth(i).scrollIntoViewIfNeeded();
}
for (let i = 0; i < 6; i++) {
const w = await imgs.nth(i).evaluate((el: HTMLImageElement) => el.naturalWidth);
expect(w).toBeGreaterThan(0);
}
});
Network Interception
Use page.route() to intercept, modify, or fully mock XHR and fetch responses in your tests.
const { test, expect } = require('@playwright/test');
test('mock API response', async ({ page }) => {
await page.route('**/api/lab-data', route =>
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ items: ['Mocked A', 'Mocked B'] })
})
);
await page.goto('/playwright-labs#network-interception');
await page.locator('#fetch-button').click();
await expect(page.locator('#result-network')).toContainText('Mocked A');
await expect(page.locator('#result-network')).toContainText('Mocked B');
});
import { test, expect } from '@playwright/test';
test('mock API response', async ({ page }) => {
await page.route('**/api/lab-data', (route) =>
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ items: ['Mocked A', 'Mocked B'] })
})
);
await page.goto('/playwright-labs#network-interception');
await page.locator('#fetch-button').click();
await expect(page.locator('#result-network')).toContainText('Mocked A');
await expect(page.locator('#result-network')).toContainText('Mocked B');
});
API Mocking
Same as network interception — stub any HTTP call with computed responses, fixtures, or status codes.
const { test, expect } = require('@playwright/test');
test('mock 500 error', async ({ page }) => {
await page.route('**/api/lab-data', route =>
route.fulfill({ status: 500, body: 'Server error' })
);
await page.goto('/playwright-labs#api-mocking');
await page.locator('#fetch-button').click();
await expect(page.locator('#result-network')).toHaveText('Error: 500');
});
import { test, expect } from '@playwright/test';
test('mock 500 error', async ({ page }) => {
await page.route('**/api/lab-data', (route) =>
route.fulfill({ status: 500, body: 'Server error' })
);
await page.goto('/playwright-labs#api-mocking');
await page.locator('#fetch-button').click();
await expect(page.locator('#result-network')).toHaveText('Error: 500');
});
Route Mocking
Mock entire route trees including query strings and POST bodies — useful for testing state transitions without backend.
const { test, expect } = require('@playwright/test');
test('route mocking with query', async ({ page }) => {
await page.route('**/api/lab-data**', route => {
const url = new URL(route.request().url());
const pageParam = url.searchParams.get('page') || '1';
return route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ items: [class="s">`Item from page ${pageParam}`] })
});
});
await page.goto('/playwright-labs#route-mocking');
await page.locator('#fetch-button').click();
await expect(page.locator('#result-network')).toContainText('Item from page 1');
});
import { test, expect } from '@playwright/test';
test('route mocking with query', async ({ page }) => {
await page.route('**/api/lab-data**', (route) => {
const url = new URL(route.request().url());
const pageParam = url.searchParams.get('page') || '1';
return route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ items: [class="s">`Item from page ${pageParam}`] })
});
});
await page.goto('/playwright-labs#route-mocking');
await page.locator('#fetch-button').click();
await expect(page.locator('#result-network')).toContainText('Item from page 1');
});
Authentication
Save authenticated storage state once with storageState() and reuse across every test for fast, parallel runs.
class=class="s">"c">// global-setup.js
const { test: setup } = require('@playwright/test');
setup('login and save state', async ({ page }) => {
await page.goto('/playwright-labs#authentication');
await page.locator('#auth-username').fill('qa@imcseian.com');
await page.locator('#auth-login').click();
await page.waitForSelector('#auth-status:has-text("Logged in")');
await page.context().storageState({ path: 'auth.json' });
});
class=class="s">"c">// playwright.config.js
const { defineConfig } = require('@playwright/test');
module.exports = defineConfig({
use: { storageState: 'auth.json' }
});
class=class="s">"c">// global-setup.ts
import { test as setup } from '@playwright/test';
setup('login and save state', async ({ page }) => {
await page.goto('/playwright-labs#authentication');
await page.locator('#auth-username').fill('qa@imcseian.com');
await page.locator('#auth-login').click();
await page.waitForSelector('#auth-status:has-text("Logged in")');
await page.context().storageState({ path: 'auth.json' });
});
class=class="s">"c">// playwright.config.ts
import { defineConfig } from '@playwright/test';
export default defineConfig({
use: { storageState: 'auth.json' }
});
Local Storage
Read, write, and clear localStorage entries pre-test using page.evaluate() or context.addInitScript().
const { test, expect } = require('@playwright/test');
test('pre-set localStorage', async ({ page }) => {
await page.addInitScript(() => {
localStorage.setItem('qa-cseian-theme', 'dark');
});
await page.goto('/playwright-labs#local-storage');
await expect(page.locator('#storage-display')).toHaveText('dark');
});
import { test, expect } from '@playwright/test';
test('pre-set localStorage', async ({ page }) => {
await page.addInitScript(() => {
localStorage.setItem('qa-cseian-theme', 'dark');
});
await page.goto('/playwright-labs#local-storage');
await expect(page.locator('#storage-display')).toHaveText('dark');
});
Session Storage
Persist session-only keys between page reloads using sessionStorage — cleared when the tab closes.
const { test, expect } = require('@playwright/test');
test('set sessionStorage', async ({ page }) => {
await page.goto('/playwright-labs#session-storage');
await page.evaluate(() => {
sessionStorage.setItem('qa-cseian-session', 'abc123');
});
await page.reload();
await expect(page.locator('#storage-display')).toHaveText('abc123');
});
import { test, expect } from '@playwright/test';
test('set sessionStorage', async ({ page }) => {
await page.goto('/playwright-labs#session-storage');
await page.evaluate(() => {
sessionStorage.setItem('qa-cseian-session', 'abc123');
});
await page.reload();
await expect(page.locator('#storage-display')).toHaveText('abc123');
});
Cookies
Inspect, set, and clear cookies with context.addCookies() and context.cookies().
const { test, expect } = require('@playwright/test');
test('pre-set cookie', async ({ page }) => {
await page.context().addCookies([{
name: 'qa-cseian-user',
value: 'ram',
url: 'https:class=class="s">"c">//qa.imcseian.com'
}]);
await page.goto('/playwright-labs#cookies');
await expect(page.locator('#cookie-display')).toHaveText('ram');
});
import { test, expect } from '@playwright/test';
test('pre-set cookie', async ({ page }) => {
await page.context().addCookies([{
name: 'qa-cseian-user',
value: 'ram',
url: 'https:class=class="s">"c">//qa.imcseian.com'
}]);
await page.goto('/playwright-labs#cookies');
await expect(page.locator('#cookie-display')).toHaveText('ram');
});
Web Tables
Read and assert tabular data row-by-row using locator.locator('tr') and toContainText() assertions.
const { test, expect } = require('@playwright/test');
test('read web table', async ({ page }) => {
await page.goto('/playwright-labs#web-tables');
const row = page.locator('#web-table tr', { hasText: 'Ram Pothuraju' });
await expect(row).toContainText('Test Engineer');
await expect(row).toContainText('qa@imcseian.com');
});
import { test, expect } from '@playwright/test';
test('read web table', async ({ page }) => {
await page.goto('/playwright-labs#web-tables');
const row = page.locator('#web-table tr', { hasText: 'Ram Pothuraju' });
await expect(row).toContainText('Test Engineer');
await expect(row).toContainText('qa@imcseian.com');
});
Dynamic Tables
Handle tables whose rows appear after async fetches — wait for the row count to grow.
const { test, expect } = require('@playwright/test');
test('dynamic table load', async ({ page }) => {
await page.goto('/playwright-labs#dynamic-tables');
await page.locator('#load-table').click();
await expect(page.locator('#dynamic-table tbody tr')).toHaveCount(5, { timeout: 5000 });
});
import { test, expect } from '@playwright/test';
test('dynamic table load', async ({ page }) => {
await page.goto('/playwright-labs#dynamic-tables');
await page.locator('#load-table').click();
await expect(page.locator('#dynamic-table tbody tr')).toHaveCount(5, { timeout: 5000 });
});
Pagination
Walk through numbered pagination by clicking Next/Prev and verifying the active page changes.
const { test, expect } = require('@playwright/test');
test('navigate pagination', async ({ page }) => {
await page.goto('/playwright-labs#pagination');
await page.locator('#page-next').click();
await page.locator('#page-next').click();
await expect(page.locator('#page-indicator')).toHaveText('Page 3 of 3');
});
import { test, expect } from '@playwright/test';
test('navigate pagination', async ({ page }) => {
await page.goto('/playwright-labs#pagination');
await page.locator('#page-next').click();
await page.locator('#page-next').click();
await expect(page.locator('#page-indicator')).toHaveText('Page 3 of 3');
});
Calendar
Navigate custom calendar widgets by clicking next/prev and selecting the day cell.
const { test, expect } = require('@playwright/test');
test('calendar pick date', async ({ page }) => {
await page.goto('/playwright-labs#calendar');
await page.locator('#cal-open').click();
await page.locator('#cal-next').click();
await page.locator('#cal-grid [data-day="15"]').click();
await expect(page.locator('#result-calendar')).toContainText('15');
});
import { test, expect } from '@playwright/test';
test('calendar pick date', async ({ page }) => {
await page.goto('/playwright-labs#calendar');
await page.locator('#cal-open').click();
await page.locator('#cal-next').click();
await page.locator('#cal-grid [data-day="15"]').click();
await expect(page.locator('#result-calendar')).toContainText('15');
});
Date Picker
Fill native date inputs with ISO-format strings (YYYY-MM-DD) for reliable cross-browser behavior.
const { test, expect } = require('@playwright/test');
test('date picker fill', async ({ page }) => {
await page.goto('/playwright-labs#date-picker');
await page.locator('#date-input').fill('2025-12-25');
await expect(page.locator('#date-input')).toHaveValue('2025-12-25');
await expect(page.locator('#result-date')).toHaveText('2025-12-25');
});
import { test, expect } from '@playwright/test';
test('date picker fill', async ({ page }) => {
await page.goto('/playwright-labs#date-picker');
await page.locator('#date-input').fill('2025-12-25');
await expect(page.locator('#date-input')).toHaveValue('2025-12-25');
await expect(page.locator('#result-date')).toHaveText('2025-12-25');
});
Slider
Set the value of native range sliders via fill() or simulate drag with mouse events.
const { test, expect } = require('@playwright/test');
test('slider fill', async ({ page }) => {
await page.goto('/playwright-labs#slider');
await page.locator('#range-slider').fill('75');
await expect(page.locator('#result-slider')).toHaveText('75');
});
import { test, expect } from '@playwright/test';
test('slider fill', async ({ page }) => {
await page.goto('/playwright-labs#slider');
await page.locator('#range-slider').fill('75');
await expect(page.locator('#result-slider')).toHaveText('75');
});
Dropdown
Select single-value native dropdowns with selectOption() — works with value, label, or index.
const { test, expect } = require('@playwright/test');
test('dropdown select', async ({ page }) => {
await page.goto('/playwright-labs#dropdown');
await page.locator('#tool-select').selectOption('cypress');
await expect(page.locator('#result-dropdown')).toHaveText('cypress');
});
import { test, expect } from '@playwright/test';
test('dropdown select', async ({ page }) => {
await page.goto('/playwright-labs#dropdown');
await page.locator('#tool-select').selectOption('cypress');
await expect(page.locator('#result-dropdown')).toHaveText('cypress');
});
Multi Select
Choose multiple options in select[multiple] by passing an array to selectOption().
const { test, expect } = require('@playwright/test');
test('multi select', async ({ page }) => {
await page.goto('/playwright-labs#multi-select');
await page.locator('#multi-select').selectOption(['playwright', 'cypress']);
await expect(page.locator('#result-multi-select')).toContainText('playwright');
await expect(page.locator('#result-multi-select')).toContainText('cypress');
});
import { test, expect } from '@playwright/test';
test('multi select', async ({ page }) => {
await page.goto('/playwright-labs#multi-select');
await page.locator('#multi-select').selectOption(['playwright', 'cypress']);
await expect(page.locator('#result-multi-select')).toContainText('playwright');
await expect(page.locator('#result-multi-select')).toContainText('cypress');
});
SVG
Click SVG paths and assert on attribute changes — Playwright treats SVG elements like any other.
const { test, expect } = require('@playwright/test');
test('svg click', async ({ page }) => {
await page.goto('/playwright-labs#svg');
await page.locator('#svg-circle').click();
await expect(page.locator('#result-svg')).toHaveText('SVG clicked!');
});
import { test, expect } from '@playwright/test';
test('svg click', async ({ page }) => {
await page.goto('/playwright-labs#svg');
await page.locator('#svg-circle').click();
await expect(page.locator('#result-svg')).toHaveText('SVG clicked!');
});
Canvas
Interact with canvas pixels by clicking at specific coordinates — use boundingBox() to compute positions.
const { test, expect } = require('@playwright/test');
test('canvas click center', async ({ page }) => {
await page.goto('/playwright-labs#canvas');
const box = await page.locator('#lab-canvas').boundingBox();
await page.mouse.click(box.x + box.width / 2, box.y + box.height / 2);
await expect(page.locator('#result-canvas')).toHaveText('Clicks: 1');
});
import { test, expect } from '@playwright/test';
test('canvas click center', async ({ page }) => {
await page.goto('/playwright-labs#canvas');
const box = await page.locator('#lab-canvas').boundingBox();
await page.mouse.click(box!.x + box!.width / 2, box!.y + box!.height / 2);
await expect(page.locator('#result-canvas')).toHaveText('Clicks: 1');
});
Charts
Assert on chart series data via data attributes or aria-labels — most modern chart libraries expose these.
const { test, expect } = require('@playwright/test');
test('chart assertions', async ({ page }) => {
await page.goto('/playwright-labs#charts');
await expect(page.locator('#chart-bars [role="img"]')).toHaveCount(5);
await expect(page.locator('#chart-bars [role="img"]').nth(2)).toHaveAttribute('data-value', '30');
});
import { test, expect } from '@playwright/test';
test('chart assertions', async ({ page }) => {
await page.goto('/playwright-labs#charts');
await expect(page.locator('#chart-bars [role="img"]')).toHaveCount(5);
await expect(page.locator('#chart-bars [role="img"]').nth(2)).toHaveAttribute('data-value', '30');
});
Dynamic Forms
Fill forms whose fields appear based on previous inputs — wait for the conditional field before filling.
const { test, expect } = require('@playwright/test');
test('dynamic form field', async ({ page }) => {
await page.goto('/playwright-labs#dynamic-forms');
await page.locator('#role-select').selectOption('other');
await expect(page.locator('#specify-field')).toBeVisible();
await page.locator('#specify-field').fill('SDET');
await expect(page.locator('#result-form')).toHaveText('SDET');
});
import { test, expect } from '@playwright/test';
test('dynamic form field', async ({ page }) => {
await page.goto('/playwright-labs#dynamic-forms');
await page.locator('#role-select').selectOption('other');
await expect(page.locator('#specify-field')).toBeVisible();
await page.locator('#specify-field').fill('SDET');
await expect(page.locator('#result-form')).toHaveText('SDET');
});
Auto Complete
Trigger autocomplete suggestions by typing, then click the desired option from the dropdown.
const { test, expect } = require('@playwright/test');
test('autocomplete select', async ({ page }) => {
await page.goto('/playwright-labs#auto-complete');
await page.locator('#auto-input').fill('play');
await expect(page.locator('#auto-suggestions')).toBeVisible();
await page.locator('#auto-suggestions [data-value="Playwright"]').click();
await expect(page.locator('#auto-input')).toHaveValue('Playwright');
});
import { test, expect } from '@playwright/test';
test('autocomplete select', async ({ page }) => {
await page.goto('/playwright-labs#auto-complete');
await page.locator('#auto-input').fill('play');
await expect(page.locator('#auto-suggestions')).toBeVisible();
await page.locator('#auto-suggestions [data-value="Playwright"]').click();
await expect(page.locator('#auto-input')).toHaveValue('Playwright');
});
Accordions
Expand and collapse accordion sections by clicking headers and asserting on the body visibility.
const { test, expect } = require('@playwright/test');
test('accordion expand', async ({ page }) => {
await page.goto('/playwright-labs#accordions');
await page.locator('#acc-header-2').click();
await expect(page.locator('#acc-body-2')).toBeVisible();
await expect(page.locator('#acc-body-2')).toHaveText('Cypress content here.');
});
import { test, expect } from '@playwright/test';
test('accordion expand', async ({ page }) => {
await page.goto('/playwright-labs#accordions');
await page.locator('#acc-header-2').click();
await expect(page.locator('#acc-body-2')).toBeVisible();
await expect(page.locator('#acc-body-2')).toHaveText('Cypress content here.');
});
Tree View
Expand tree nodes by clicking the expand icon and assert on nested children appearing.
const { test, expect } = require('@playwright/test');
test('tree view expand', async ({ page }) => {
await page.goto('/playwright-labs#tree-view');
await page.locator('#tree-node-automation [data-action="expand"]').click();
await expect(page.locator('#tree-node-playwright')).toBeVisible();
});
import { test, expect } from '@playwright/test';
test('tree view expand', async ({ page }) => {
await page.goto('/playwright-labs#tree-view');
await page.locator('#tree-node-automation [data-action="expand"]').click();
await expect(page.locator('#tree-node-playwright')).toBeVisible();
});
Modal Dialog
Open, assert, and close modal dialogs reliably with role-based locators.
const { test, expect } = require('@playwright/test');
test('modal open and close', async ({ page }) => {
await page.goto('/playwright-labs#modal-dialog');
await page.locator('#open-modal').click();
await expect(page.locator('#lab-modal')).toBeVisible();
await expect(page.locator('#modal-text')).toHaveText('Are you sure?');
await page.locator('#close-modal').click();
await expect(page.locator('#lab-modal')).toBeHidden();
});
import { test, expect } from '@playwright/test';
test('modal open and close', async ({ page }) => {
await page.goto('/playwright-labs#modal-dialog');
await page.locator('#open-modal').click();
await expect(page.locator('#lab-modal')).toBeVisible();
await expect(page.locator('#modal-text')).toHaveText('Are you sure?');
await page.locator('#close-modal').click();
await expect(page.locator('#lab-modal')).toBeHidden();
});
Toast Messages
Wait for transient toast notifications using toBeVisible() — they auto-dismiss after a timeout.
const { test, expect } = require('@playwright/test');
test('toast appears', async ({ page }) => {
await page.goto('/playwright-labs#toast-messages');
await page.locator('#toast-button').click();
await expect(page.locator('#lab-toast')).toBeVisible();
await expect(page.locator('#lab-toast')).toHaveText('Saved successfully');
});
import { test, expect } from '@playwright/test';
test('toast appears', async ({ page }) => {
await page.goto('/playwright-labs#toast-messages');
await page.locator('#toast-button').click();
await expect(page.locator('#lab-toast')).toBeVisible();
await expect(page.locator('#lab-toast')).toHaveText('Saved successfully');
});
Loading Spinner
Wait for spinners to disappear before the next action using toBeHidden().
const { test, expect } = require('@playwright/test');
test('wait for spinner', async ({ page }) => {
await page.goto('/playwright-labs#loading-spinner');
await page.locator('#submit-spinner').click();
await expect(page.locator('#lab-spinner')).toBeHidden({ timeout: 5000 });
await expect(page.locator('#result-spinner')).toHaveText('Done!');
});
import { test, expect } from '@playwright/test';
test('wait for spinner', async ({ page }) => {
await page.goto('/playwright-labs#loading-spinner');
await page.locator('#submit-spinner').click();
await expect(page.locator('#lab-spinner')).toBeHidden({ timeout: 5000 });
await expect(page.locator('#result-spinner')).toHaveText('Done!');
});
Retry Logic & Flakiness
Configure retries at the runner level and detect flaky tests using the HTML reporter.
class=class="s">"c">// playwright.config.js
const { defineConfig } = require('@playwright/test');
module.exports = defineConfig({
retries: 2,
reporter: [['html'], ['junit', { outputFile: 'results.xml' }]],
use: { trace: 'on-first-retry' }
});
class=class="s">"c">// spec
test('flaky scenario', async ({ page }) => {
await page.goto('/playwright-labs#retry-logic');
await page.locator('#flaky-button').click();
await expect(page.locator('#result-retry')).toHaveText('Success', { timeout: 5000 });
});
class=class="s">"c">// playwright.config.ts
import { defineConfig } from '@playwright/test';
export default defineConfig({
retries: 2,
reporter: [['html'], ['junit', { outputFile: 'results.xml' }]],
use: { trace: 'on-first-retry' }
});
class=class="s">"c">// spec
test('flaky scenario', async ({ page }) => {
await page.goto('/playwright-labs#retry-logic');
await page.locator('#flaky-button').click();
await expect(page.locator('#result-retry')).toHaveText('Success', { timeout: 5000 });
});
XPath Practice
Master XPath axes: parent, ancestor, following-sibling, and more. Use page.locator('xpath=...') syntax.
const { test, expect } = require('@playwright/test');
test('xpath following-sibling', async ({ page }) => {
await page.goto('/playwright-labs#xpath-practice');
class=class="s">"c">// Find button that is a following-sibling of the QA label
await page.locator('xpath=class=class="s">"c">//label[text()="QA"]/following-sibling::button').click();
await expect(page.locator('#result-xpath')).toHaveText('XPath found me!');
});
import { test, expect } from '@playwright/test';
test('xpath following-sibling', async ({ page }) => {
await page.goto('/playwright-labs#xpath-practice');
await page.locator('xpath=class=class="s">"c">//label[text()="QA"]/following-sibling::button').click();
await expect(page.locator('#result-xpath')).toHaveText('XPath found me!');
});
CSS Selector Practice
Nth-child, attribute, and pseudo-class selector drills for resilient test locators.
const { test, expect } = require('@playwright/test');
test('css nth-child', async ({ page }) => {
await page.goto('/playwright-labs#css-selector-practice');
await page.locator('#css-list li:nth-child(2) button').click();
await expect(page.locator('#result-css')).toHaveText('Item 2 clicked');
});
import { test, expect } from '@playwright/test';
test('css nth-child', async ({ page }) => {
await page.goto('/playwright-labs#css-selector-practice');
await page.locator('#css-list li:nth-child(2) button').click();
await expect(page.locator('#result-css')).toHaveText('Item 2 clicked');
});
Popup Windows
Capture popups with waitForEvent('popup') and assert on their content.
const { test, expect } = require('@playwright/test');
test('popup window assertion', async ({ page }) => {
await page.goto('/playwright-labs#popup-windows');
const [popup] = await Promise.all([
page.waitForEvent('popup'),
page.locator('#popup-button').click()
]);
await popup.waitForLoadState();
await expect(popup.locator('h1')).toHaveText('Popup Window');
});
import { test, expect, Page } from '@playwright/test';
test('popup window assertion', async ({ page }) => {
await page.goto('/playwright-labs#popup-windows');
const [popup] = await Promise.all([
page.waitForEvent('popup') as Promise<Page>,
page.locator('#popup-button').click()
]);
await popup.waitForLoadState();
await expect(popup.locator('h1')).toHaveText('Popup Window');
});
iFrame (Same Origin)
Same-origin iframes are accessible via frameLocator() — no special permissions needed.