Interactions
Browser dialogs, mouse and keyboard actions, frames, windows and shadow DOM: the parts of a page that a plain click() and fill() can't handle on their own.
1. JavaScript dialogs
IntermediateThese buttons open the browser's own alert, confirm and prompt boxes, which aren't part of the page.
Result:
Expected: accepting the alert shows You clicked OK on the alert; cancelling the confirm shows You clicked: Cancel; answering the prompt with Mario shows You entered: Mario.
Show me how
import { test, expect } from '@playwright/test'; test('handles alert, confirm and prompt', async ({ page }) => { await page.goto('/interactions.html'); const result = page.getByTestId('dialog-result'); // Register the handler BEFORE the click. page.once() handles one dialog only. // With no handler at all, Playwright dismisses every dialog automatically. page.once('dialog', (dialog) => dialog.accept()); await page.getByTestId('js-alert').click(); await expect(result).toHaveText('You clicked OK on the alert'); page.once('dialog', (dialog) => dialog.dismiss()); await page.getByTestId('js-confirm').click(); await expect(result).toHaveText('You clicked: Cancel'); page.once('dialog', async (dialog) => { expect(dialog.message()).toBe('What is your name?'); await dialog.accept('Mario'); // the text typed into the prompt }); await page.getByTestId('js-prompt').click(); await expect(result).toHaveText('You entered: Mario'); });2. Modal dialog
BeginnerA real in-page modal (an HTML <dialog>), not a browser dialog.
Choice:
Expected: the modal opens with its title, clicking Subscribe closes it, and the choice reads Subscribed.
Show me how
import { test, expect } from '@playwright/test'; test('subscribes through the modal', async ({ page }) => { await page.goto('/interactions.html'); await page.getByTestId('open-modal').click(); // A3. Drag and drop
IntermediateMove the tools into the "Selected" box.
Selected
Selected: none
Expected: after dragging all three, the source box is empty, the Selected box holds 3 items, and the summary reads Selenium, Playwright, Cypress.
Show me how
import { test, expect } from '@playwright/test'; test('drags every tool into Selected', async ({ page }) => { await page.goto('/interactions.html'); const target = page.getByTestId('target-zone'); // dragTo() presses the mouse on the source, moves it over the target // and releases, which fires the page's HTML5 drag-and-drop events. for (const tool of ['selenium', 'playwright', 'cypress']) { await page.getByTestId(`drag-${tool}`).dragTo(target); } await expect(page.getByTestId('source-zone').locator('.draggable')).toHaveCount(0); await expect(target.locator('.draggable')).toHaveCount(3); await expect(page.getByTestId('drop-result')).toHaveText('Selenium, Playwright, Cypress'); });4. Mouse actions
IntermediateA menu that only appears on hover, a button that needs a double-click, and a custom right-click menu.
Last action:
Expected: choosing Settings from the hover menu shows Menu: Settings; the double-click shows Double click; right-click → Rename shows Context menu: Rename.
Show me how
import { test, expect } from '@playwright/test'; test('hovers, double-clicks and right-clicks', async ({ page }) => { await page.goto('/interactions.html'); const result = page.getByTestId('mouse-result'); // The menu items are invisible until hovered, so hover first. await page.getByTestId('hover-trigger').hover(); await page.getByTestId('menu-settings').click(); await expect(result).toHaveText('Menu: Settings'); await page.getByTestId('double-click').dblclick(); await expect(result).toHaveText('Double click'); await page.getByTestId('context-target').click({ button: 'right' }); await expect(page.getByTestId('context-menu')).toBeVisible(); await page.getByTestId('ctx-rename').click(); await expect(result).toHaveText('Context menu: Rename'); });5. Keyboard
BeginnerYou pressed:
Expected: pressing Enter shows Enter, Escape shows Escape, and Ctrl+A shows Ctrl+a (modifiers come first).
Show me how
import { test, expect } from '@playwright/test'; const keys = [ { press: 'Enter', shown: 'Enter' }, { press: 'Escape', shown: 'Escape' }, { press: 'Control+a', shown: 'Ctrl+a' }, { press: 'Shift+ArrowLeft', shown: 'Shift+ArrowLeft' }, ]; test('reports key presses', async ({ page }) => { await page.goto('/interactions.html'); const input = page.getByTestId('key-input'); for (const key of keys) { // locator.press() focuses the element first, then sends the key. // Combine keys with "+"; names follow KeyboardEvent.key values. await input.press(key.press); await expect(page.getByTestId('key-result')).toHaveText(key.shown); } });6. iframe
IntermediateThe form below lives in a separate document inside an iframe.
Message from frame:
Expected: sending Hello from Playwright shows Sent: Hello from Playwright inside the frame, and the same message appears on the main page.
Show me how
import { test, expect } from '@playwright/test'; test('sends a message from inside the iframe', async ({ page }) => { await page.goto('/interactions.html'); // contentFrame() turns the iframe element into a locator scope: // everything after it searches inside the frame's document. const frame = page.getByTestId('practice-frame').contentFrame(); await frame.getByTestId('frame-input').fill('Hello from Playwright'); await frame.getByTestId('frame-submit').click(); await expect(frame.getByTestId('frame-output')).toHaveText('Sent: Hello from Playwright'); // Back on the main page: no "switching back" needed, just use page. await expect(page.getByTestId('frame-result')).toHaveText('Hello from Playwright'); });7. Tabs & windows
AdvancedExpected: both open a new page whose heading is Inside the frame. Your test works in the new page, closes it, and carries on in the original one.
Show me how
import { test, expect } from '@playwright/test'; test('works with a new tab and a popup', async ({ page }) => { await page.goto('/interactions.html'); // Start waiting for the new page BEFORE clicking, then await both together. // Links with target="_blank" and window.open() both fire "popup". const [newTab] = await Promise.all([ page.waitForEvent('popup'), page.getByTestId('new-tab-link').click(), ]); await expect(newTab.getByTestId('frame-heading')).toHaveText('Inside the frame'); await newTab.close(); const [popup] = await Promise.all([ page.waitForEvent('popup'), page.getByTestId('new-window').click(), ]); await expect(popup).toHaveURL(/frame\.html\?standalone=1/); await popup.close(); // The original page is untouched and still usable. await expect(page.getByTestId('page-title')).toHaveText('Interactions'); });8. Shadow DOM
AdvancedThe input and button below live inside an open shadow root, hidden from document.querySelector.
Expected: typing Mario and pressing Greet shows Hello, Mario!; with an empty name it shows Hello, stranger!