playwright-labs

Playwright Playground — 50+ Interactive Practice Labs | QA CSEIAN
Playwright Playground

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.

52 labs
easy Playwright

Dynamic Locators

Locate elements whose IDs change on every page load using resilient Playwright locators like [id^="submit-"] regex matches.

#01
Live widget
Challenge: Find and click the button even though its ID changes from #submit-a3f to #submit-7b2 every refresh.
Expected: Button clicked, success message appears.
Interactive area
JavaScript TypeScript
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!');
});
easy Playwright

Auto Waiting

Playwright auto-waits for elements to be visible, enabled, and stable before interacting — no more flaky page.waitForTimeout() calls.

#02
Live widget
Challenge: Click a button that appears 2 seconds after page load. Do not use any sleep.
Expected: Test passes without any manual waitForTimeout.
Interactive area
Button appears 2s after page load.
JavaScript TypeScript
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!');
});
medium Playwright

Frames (iFrame)

Use Playwright's frameLocator() to interact with elements inside same-origin iframes. Modern alternative to legacy frame(name) API.

#03
Live widget
Challenge: Click the button inside the iframe below and verify the result text appears.
Expected: Click reaches the iframe button, result updates.
Interactive area
JavaScript TypeScript
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!');
});
medium Playwright

Nested Frames

Chain frameLocator() calls to drill into frames within frames — the modern alternative to traversing window.frames.

#04
Live widget
Challenge: Click the button inside the innermost iframe (an iframe inside an iframe).
Expected: Click reaches the inner frame's button.
Interactive area
JavaScript TypeScript
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!');
});
medium Playwright

Drag and Drop

Use dragTo() for simple HTML5 drag and drop, or manual mouse.down()/move()/up() for pixel-perfect scenarios.

#05
Live widget
Challenge: Drag Card A from the To-Do column into the Done column.
Expected: Card A appears in the Done column after the drop.
Interactive area
To-Do
Card A
Card B
Done
JavaScript TypeScript
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();
});
medium Playwright

Alert Dialogs

Auto-dismiss native window.alert() dialogs by registering a page.on('dialog') handler before triggering the action.

#06
Live widget
Challenge: Click the button that fires alert('Hello!') and verify the alert was accepted.
Expected: Alert auto-accepted, status text appears.
Interactive area
JavaScript TypeScript
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!');
});
medium Playwright

Confirm Dialogs

Accept or dismiss window.confirm() dialogs programmatically by passing true/false to dialog.accept().

#07
Live widget
Challenge: Accept the confirm() dialog and verify the UI reflects the acceptance.
Expected: Dialog accepted, status shows 'Confirmed'.
Interactive area
JavaScript TypeScript
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');
});
medium Playwright

Prompt Dialogs

Provide text input to window.prompt() dialogs by passing the text to dialog.accept('text').

#08
Live widget
Challenge: Type 'Playwright' into the prompt and verify the page echoes it back.
Expected: Echo shows the typed name.
Interactive area
JavaScript TypeScript
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!');
});
medium Playwright

File Upload

Upload files via setInputFiles() — works even on hidden inputs without triggering the OS file picker.

#09
Live widget
Challenge: Upload a file to the hidden input and verify the filename appears.
Expected: Selected filename shown in the result.
Interactive area
JavaScript TypeScript
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');
});
medium Playwright

Multiple File Upload

Upload multiple files in a single setInputFiles() call by passing an array.

#10
Live widget
Challenge: Upload two files and verify both filenames appear in the result.
Expected: Both file names listed in the result.
Interactive area
JavaScript TypeScript
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');
});
medium Playwright

File Download

Capture file downloads via the download event and save them to disk with download.saveAs().

#11
Live widget
Challenge: Click the download button, capture the file, and verify its contents.
Expected: Download captured and saved successfully.
Interactive area
Click to download qa-cseian-report.txt
JavaScript TypeScript
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');
});
medium Playwright

New Window

Capture new windows opened via window.open() using page.waitForEvent('popup').

#12
Live widget
Challenge: Click a button that opens a new window and verify the popup's URL.
Expected: Popup captured and asserted.
Interactive area
Open via link ↗
Popups open about:blank — replace URL in your test.
JavaScript TypeScript
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();
});
medium Playwright

Multiple Tabs

Same as new window — Playwright treats every browser tab as a new Page context. Use page.context().pages to enumerate.

#13
Live widget
Challenge: Open two tabs and verify both are accessible from page.context().pages.
Expected: Both pages exist in the context.
Interactive area
Open via link ↗
Popups open about:blank — replace URL in your test.
JavaScript TypeScript
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);
});
easy Playwright

Hover

Trigger CSS :hover states reliably with locator.hover() — useful for tooltips, dropdown menus, and revealing hidden elements.

#14
Live widget
Challenge: Hover over the card to reveal the tooltip and verify the tooltip text.
Expected: Tooltip appears on hover.
Interactive area
Hover me
JavaScript TypeScript
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!');
});
medium Playwright

Keyboard Actions

Use page.keyboard to type text, press keys, and trigger shortcuts like Ctrl+Enter or Shift+Tab.

#15
Live widget
Challenge: Type 'QA' into the input, press Enter, and verify the echoed text.
Expected: Input captures the typed text and Enter submits it.
Interactive area
JavaScript TypeScript
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');
});
medium Playwright

Mouse Actions

Use page.mouse for pixel-perfect interactions — down(), move(), up(), and wheel() for scroll.

#16
Live widget
Challenge: Click on the canvas at coordinates (50, 50) and verify the click was registered.
Expected: Click coordinates captured and displayed.
Interactive area
Click anywhere in here
JavaScript TypeScript
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');
});
hard Playwright

Shadow DOM

Playwright pierces open AND closed shadow roots by default — no special CSS combinators needed.

#17
Live widget
Challenge: Click the button inside the closed shadow DOM below.
Expected: Click reaches the shadow button, no ElementNotFound.
Interactive area
JavaScript TypeScript
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!');
});
medium Playwright

Infinite Scroll

Use page.mouse.wheel() or scrollIntoViewIfNeeded() to load additional content as the user scrolls.

#18
Live widget
Challenge: Scroll the container until at least 20 items are loaded.
Expected: Item count grows as you scroll.
Interactive area
    JavaScript TypeScript
    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 });
    });
    medium Playwright

    Lazy Loading

    Wait for IntersectionObserver-triggered image loads using Playwright's network idle or img loading states.

    #19
    Live widget
    Challenge: Scroll to trigger lazy loading and verify all 6 images eventually load.
    Expected: All images have non-empty naturalWidth.
    Interactive area
    lazy 1 lazy 2 lazy 3 lazy 4 lazy 5 lazy 6
    JavaScript TypeScript
    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);
      }
    });
    hard Playwright

    Network Interception

    Use page.route() to intercept, modify, or fully mock XHR and fetch responses in your tests.

    #20
    Live widget
    Challenge: Mock the API to return a custom response and verify the UI displays it.
    Expected: UI shows mocked data instead of real response.
    Interactive area
    Calls /api/lab-data — intercept with page.route()
    JavaScript TypeScript
    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');
    });
    hard Playwright

    API Mocking

    Same as network interception — stub any HTTP call with computed responses, fixtures, or status codes.

    #21
    Live widget
    Challenge: Mock a 500 error on the API and verify the error UI renders.
    Expected: Error UI appears without real request.
    Interactive area
    Calls /api/lab-data — intercept with page.route()
    JavaScript TypeScript
    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');
    });
    hard Playwright

    Route Mocking

    Mock entire route trees including query strings and POST bodies — useful for testing state transitions without backend.

    #22
    Live widget
    Challenge: Mock the route to return different data based on the page query parameter.
    Expected: Mocked route returns correct data per page.
    Interactive area
    Calls /api/lab-data — intercept with page.route()
    JavaScript TypeScript
    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');
    });
    hard Playwright

    Authentication

    Save authenticated storage state once with storageState() and reuse across every test for fast, parallel runs.

    #23
    Live widget
    Challenge: Log in once, save storageState to auth.json, then reuse it for subsequent specs.
    Expected: Tests skip login and start authenticated.
    Interactive area
    JavaScript TypeScript
    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' }
    });
    medium Playwright

    Local Storage

    Read, write, and clear localStorage entries pre-test using page.evaluate() or context.addInitScript().

    #24
    Live widget
    Challenge: Set a localStorage key before navigation and verify the widget reads it.
    Expected: Pre-set value appears in the widget.
    Interactive area
    Current value: (none)
    JavaScript TypeScript
    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');
    });
    medium Playwright

    Session Storage

    Persist session-only keys between page reloads using sessionStorage — cleared when the tab closes.

    #25
    Live widget
    Challenge: Set sessionStorage via Playwright and verify the widget picks it up.
    Expected: Session value displayed in widget.
    Interactive area
    Current value: (none)
    JavaScript TypeScript
    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');
    });
    medium Playwright

    Cookies

    Inspect, set, and clear cookies with context.addCookies() and context.cookies().

    #26
    Live widget
    Challenge: Pre-set a cookie and verify the page reads it on load.
    Expected: Cookie value displayed in widget.
    Interactive area
    Cookie "qa-cseian-user": (not set)
    JavaScript TypeScript
    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');
    });
    medium Playwright

    Web Tables

    Read and assert tabular data row-by-row using locator.locator('tr') and toContainText() assertions.

    #27
    Live widget
    Challenge: Find the row containing 'Ram Pothuraju' and verify the role is 'Test Engineer'.
    Expected: Row found, role verified.
    Interactive area
    NameRoleEmail
    Ram PothurajuTest Engineerqa@imcseian.com
    Ananya RaoSDETananya@example.com
    Marcus KimQA Leadmarcus@example.com
    JavaScript TypeScript
    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');
    });
    medium Playwright

    Dynamic Tables

    Handle tables whose rows appear after async fetches — wait for the row count to grow.

    #28
    Live widget
    Challenge: Click 'Load' and wait for 5 rows to appear in the table.
    Expected: All 5 rows visible after load.
    Interactive area
    IDItemStatus
    JavaScript TypeScript
    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 });
    });
    medium Playwright

    Pagination

    Walk through numbered pagination by clicking Next/Prev and verifying the active page changes.

    #29
    Live widget
    Challenge: Navigate from page 1 to page 3 and verify the page indicator updates.
    Expected: Page indicator shows 'Page 3 of 3'.
    Interactive area
    Page 1 of 3
    JavaScript TypeScript
    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');
    });
    medium Playwright

    Calendar

    Navigate custom calendar widgets by clicking next/prev and selecting the day cell.

    #30
    Live widget
    Challenge: Open the calendar, navigate to next month, and pick the 15th.
    Expected: Selected date shows in the result.
    Interactive area
    JavaScript TypeScript
    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');
    });
    medium Playwright

    Date Picker

    Fill native date inputs with ISO-format strings (YYYY-MM-DD) for reliable cross-browser behavior.

    #31
    Live widget
    Challenge: Set the date input to 2025-12-25 and verify the displayed value.
    Expected: Date input shows the chosen date.
    Interactive area
    JavaScript TypeScript
    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');
    });
    medium Playwright

    Slider

    Set the value of native range sliders via fill() or simulate drag with mouse events.

    #32
    Live widget
    Challenge: Set the slider to 75 and verify the displayed value.
    Expected: Slider value shows 75.
    Interactive area
    50
    JavaScript TypeScript
    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');
    });
    medium Playwright

    Multi Select

    Choose multiple options in select[multiple] by passing an array to selectOption().

    #34
    Live widget
    Challenge: Select Playwright and Cypress together and verify both appear.
    Expected: Result shows both tools.
    Interactive area
    JavaScript TypeScript
    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');
    });
    hard Playwright

    SVG

    Click SVG paths and assert on attribute changes — Playwright treats SVG elements like any other.

    #35
    Live widget
    Challenge: Click the SVG circle and verify the result text changes.
    Expected: Click registers on the SVG element.
    Interactive area
    JavaScript TypeScript
    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!');
    });
    hard Playwright

    Canvas

    Interact with canvas pixels by clicking at specific coordinates — use boundingBox() to compute positions.

    #36
    Live widget
    Challenge: Click on the canvas at its center and verify a dot appears.
    Expected: Canvas receives the click, count increments.
    Interactive area
    Clicks: 0
    JavaScript TypeScript
    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');
    });
    hard Playwright

    Charts

    Assert on chart series data via data attributes or aria-labels — most modern chart libraries expose these.

    #37
    Live widget
    Challenge: Verify the chart has 5 bars and the third bar has data-value='30'.
    Expected: Bar count and value verified.
    Interactive area
    JavaScript TypeScript
    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');
    });
    medium Playwright

    Dynamic Forms

    Fill forms whose fields appear based on previous inputs — wait for the conditional field before filling.

    #38
    Live widget
    Challenge: Select 'Other' from the role dropdown and verify the 'Specify' text field appears.
    Expected: Conditional field appears after selecting 'Other'.
    Interactive area
    JavaScript TypeScript
    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');
    });
    medium Playwright

    Auto Complete

    Trigger autocomplete suggestions by typing, then click the desired option from the dropdown.

    #39
    Live widget
    Challenge: Type 'play' and select 'Playwright' from the suggestions.
    Expected: Input value updates to 'Playwright'.
    Interactive area
    JavaScript TypeScript
    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');
    });
    easy Playwright

    Accordions

    Expand and collapse accordion sections by clicking headers and asserting on the body visibility.

    #40
    Live widget
    Challenge: Click the second accordion header and verify its body content is visible.
    Expected: Second accordion body visible with correct text.
    Interactive area
    Playwright +
    Playwright content here.
    Cypress +
    Cypress content here.
    Selenium +
    Selenium content here.
    JavaScript TypeScript
    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.');
    });
    medium Playwright

    Tree View

    Expand tree nodes by clicking the expand icon and assert on nested children appearing.

    #41
    Live widget
    Challenge: Expand 'Automation' node and verify 'Playwright' child appears.
    Expected: Child node visible after expand.
    Interactive area
    Automation
    Playwright
    Cypress
    Manual Testing
    Exploratory
    JavaScript TypeScript
    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();
    });
    medium Playwright

    Toast Messages

    Wait for transient toast notifications using toBeVisible() — they auto-dismiss after a timeout.

    #43
    Live widget
    Challenge: Click the button and verify the toast appears with the correct text.
    Expected: Toast appears with 'Saved successfully'.
    Interactive area
    Saved successfully
    JavaScript TypeScript
    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');
    });
    easy Playwright

    Loading Spinner

    Wait for spinners to disappear before the next action using toBeHidden().

    #44
    Live widget
    Challenge: Click 'Submit' and wait for the spinner to disappear, then verify the success message.
    Expected: Spinner hides, success message appears.
    Interactive area
    JavaScript TypeScript
    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!');
    });
    hard Playwright

    Retry Logic & Flakiness

    Configure retries at the runner level and detect flaky tests using the HTML reporter.

    #45
    Live widget
    Challenge: The button below is intentionally flaky (70% success). Configure retries to handle this.
    Expected: Test passes within retries, marked flaky in report.
    Interactive area
    JavaScript TypeScript
    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 });
    });
    medium Playwright

    XPath Practice

    Master XPath axes: parent, ancestor, following-sibling, and more. Use page.locator('xpath=...') syntax.

    #46
    Live widget
    Challenge: Use XPath to find the button that follows the 'QA' label and click it.
    Expected: XPath-targeted button clicked.
    Interactive area
    JavaScript TypeScript
    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!');
    });
    easy Playwright

    CSS Selector Practice

    Nth-child, attribute, and pseudo-class selector drills for resilient test locators.

    #47
    Live widget
    Challenge: Use CSS :nth-child(2) to click the second item in the list.
    Expected: Second item clicked.
    Interactive area
    JavaScript TypeScript
    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');
    });
    medium Playwright

    iFrame (Same Origin)

    Same-origin iframes are accessible via frameLocator() — no special permissions needed.

    #49
    Live widget
    Challenge: Interact with the input inside the iframe and verify the echoed value.
    Expected: Iframe input captured and echoed.
    Interactive area
    JavaScript TypeScript
    const { test, expect } = require('@playwright/test');
    
    test('iframe input', async ({ page }) => {
      await page.goto('/playwright-labs#frames-iframe');
      const frame = page.frameLocator('#lab-iframe');
      await frame.locator('#iframe-input').fill('Hello iframe');
      await expect(frame.locator('#iframe-result')).toHaveText('Hello iframe');
    });
    import { test, expect } from '@playwright/test';
    
    test('iframe input', async ({ page }) => {
      await page.goto('/playwright-labs#frames-iframe');
      const frame = page.frameLocator('#lab-iframe');
      await frame.locator('#iframe-input').fill('Hello iframe');
      await expect(frame.locator('#iframe-result')).toHaveText('Hello iframe');
    });
    hard Playwright

    Multi-Window Orchestration

    Coordinate multiple popup windows in a single test by tracking them via context.pages.

    #50
    Live widget
    Challenge: Open two popups, interact with both, then close them.
    Expected: Both popups accessible and closable.
    Interactive area
    Open via link ↗
    Popups open about:blank — replace URL in your test.
    JavaScript TypeScript
    const { test, expect } = require('@playwright/test');
    
    test('multi-window orchestration', async ({ page }) => {
      await page.goto('/playwright-labs#multi-window-orchestration');
      const popup1Promise = page.waitForEvent('popup');
      await page.locator('#popup-button').click();
      const popup1 = await popup1Promise;
      await popup1.waitForLoadState();
      await expect(popup1.locator('h1')).toHaveText('Popup Window');
      await popup1.close();
    });
    import { test, expect, Page } from '@playwright/test';
    
    test('multi-window orchestration', async ({ page }) => {
      await page.goto('/playwright-labs#multi-window-orchestration');
      const popup1Promise = page.waitForEvent('popup');
      await page.locator('#popup-button').click();
      const popup1 = await popup1Promise as Page;
      await popup1.waitForLoadState();
      await expect(popup1.locator('h1')).toHaveText('Popup Window');
      await popup1.close();
    });
    medium Playwright

    iFrames

    Standard same-origin iframe interaction — the bread and butter of legacy automation.

    #51
    Live widget
    Challenge: Switch into the iframe and click the button inside.
    Expected: Button inside iframe clicked.
    Interactive area
    JavaScript TypeScript
    const { test, expect } = require('@playwright/test');
    
    test('iframe button click', async ({ page }) => {
      await page.goto('/playwright-labs#iframes');
      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 button click', async ({ page }) => {
      await page.goto('/playwright-labs#iframes');
      const frame = page.frameLocator('#lab-iframe');
      await frame.locator('#iframe-button').click();
      await expect(frame.locator('#iframe-result')).toHaveText('Iframe clicked!');
    });
    medium Playwright

    Popup Windows

    Popup windows opened via target=_blank or window.open() — captured via waitForEvent('popup').

    #52
    Live widget
    Challenge: Click the link with target=_blank and verify the popup loads.
    Expected: Popup loads with correct content.
    Interactive area
    Open via link ↗
    Popups open about:blank — replace URL in your test.
    JavaScript TypeScript
    const { test, expect } = require('@playwright/test');
    
    test('popup target blank', async ({ page }) => {
      await page.goto('/playwright-labs#multiple-tabs-popup');
      const [popup] = await Promise.all([
        page.waitForEvent('popup'),
        page.locator('#popup-link').click()
      ]);
      await popup.waitForLoadState();
      await expect(popup.locator('h1')).toHaveText('Popup Window');
    });
    import { test, expect, Page } from '@playwright/test';
    
    test('popup target blank', async ({ page }) => {
      await page.goto('/playwright-labs#multiple-tabs-popup');
      const [popup] = await Promise.all([
        page.waitForEvent('popup') as Promise<Page>,
        page.locator('#popup-link').click()
      ]);
      await popup.waitForLoadState();
      await expect(popup.locator('h1')).toHaveText('Popup Window');
    });