Simulate real user interactions in Playwright: fill forms, handle checkboxes and dropdowns, upload files, press keyboard shortcuts, accept dialogs, and handle new tab navigation.
Real users do more than click links. They fill multi-step forms, upload files, use keyboard shortcuts, dismiss dialogs, and sometimes navigate to new browser tabs. Playwright provides a rich interaction API that mirrors every action a user can take. This lesson covers all the interaction patterns you will encounter in real-world E2E tests, with a focus on the patterns that are commonly mishandled: dialogs that block execution if not pre-handled, file uploads that require special treatment, and new-tab navigation that opens outside the current page context.
1import { test, expect } from '@playwright/test';
2
3test('complete checkout form', async ({ page }) => {
4 await page.goto('/checkout');
5
6 // Text inputs — fill() clears existing text first
7 await page.getByLabel('Card number').fill('4242424242424242');
8 await page.getByLabel('Expiry date').fill('12/28');
9 await page.getByLabel('CVC').fill('123');
10
11 // textarea
12 await page.getByLabel('Order notes').fill('Please leave at front door.');
13
14 // select dropdown
15 await page.getByLabel('Country').selectOption('US'); // by value
16 await page.getByLabel('State').selectOption({ label: 'California' }); // by label
17 await page.getByRole('combobox', { name: 'Level' }).selectOption({ index: 1 }); // by index
18
19 // Multi-select
20 await page.getByLabel('Interests').selectOption(['javascript', 'typescript']);
21
22 // Checkbox
23 await page.getByLabel('Save payment method').check();
24 await expect(page.getByLabel('Save payment method')).toBeChecked();
25 await page.getByLabel('Marketing emails').uncheck();
26 await expect(page.getByLabel('Marketing emails')).not.toBeChecked();
27
28 // Radio button
29 await page.getByRole('radio', { name: 'Monthly billing' }).check();
30 await expect(page.getByRole('radio', { name: 'Monthly billing' })).toBeChecked();
31 await expect(page.getByRole('radio', { name: 'Annual billing' })).not.toBeChecked();
32
33 // Clearing an input
34 await page.getByLabel('Promo code').fill('INVALID');
35 await page.getByLabel('Promo code').clear();
36 await expect(page.getByLabel('Promo code')).toHaveValue('');
37
38 // Typing slowly (for inputs with debounced listeners)
39 await page.getByRole('searchbox').pressSequentially('TypeScript', { delay: 50 });
40});Browser dialogs (alert, confirm, prompt) block JavaScript execution until dismissed. In Playwright, register a dialog listener BEFORE triggering the action that shows the dialog.
1test('confirm dialog on delete', async ({ page }) => {
2 await page.goto('/dashboard/courses');
3
4 // WRONG: click first, then handle dialog
5 // await page.getByRole('button', { name: 'Delete' }).click();
6 // await page.getByRole('dialog') ... — this may miss the dialog!
7
8 // CORRECT: register listener before the action
9 page.once('dialog', async (dialog) => {
10 expect(dialog.type()).toBe('confirm');
11 expect(dialog.message()).toContain('Are you sure you want to delete');
12 await dialog.accept(); // or dialog.dismiss() to cancel
13 });
14
15 await page.getByRole('button', { name: 'Delete course' }).click();
16 // After dialog is accepted, the delete proceeds
17 await expect(page.getByRole('status')).toContainText('Course deleted');
18});
19
20test('prompt dialog captures user input', async ({ page }) => {
21 await page.goto('/admin/rename');
22
23 page.once('dialog', async (dialog) => {
24 expect(dialog.type()).toBe('prompt');
25 await dialog.accept('New Course Title'); // provide the input value
26 });
27
28 await page.getByRole('button', { name: 'Rename' }).click();
29 await expect(page.getByRole('heading', { level: 1 })).toHaveText('New Course Title');
30});
31
32test('alert dialog is dismissed', async ({ page }) => {
33 await page.goto('/admin');
34
35 page.once('dialog', async (dialog) => {
36 expect(dialog.type()).toBe('alert');
37 await dialog.dismiss();
38 });
39
40 await page.getByRole('button', { name: 'Run maintenance' }).click();
41});
42
43// For multiple sequential dialogs:
44const dialogs: string[] = [];
45page.on('dialog', async (dialog) => {
46 dialogs.push(dialog.type());
47 await dialog.accept();
48});1test('keyboard navigation and shortcuts', async ({ page }) => {
2 await page.goto('/');
3
4 // Press a single key
5 await page.keyboard.press('Escape');
6 await page.keyboard.press('Enter');
7 await page.keyboard.press('Tab');
8 await page.keyboard.press('Shift+Tab'); // backwards tab
9 await page.keyboard.press('Control+k'); // keyboard shortcut (Ctrl+K)
10 await page.keyboard.press('Meta+k'); // Cmd+K on macOS
11
12 // Type text into the currently focused element
13 await page.getByRole('textbox').focus();
14 await page.keyboard.type('Hello, World!');
15
16 // Select all and replace
17 await page.keyboard.press('Control+a');
18 await page.keyboard.type('Replacement text');
19
20 // Arrow key navigation
21 await page.getByRole('listbox').focus();
22 await page.keyboard.press('ArrowDown');
23 await page.keyboard.press('ArrowDown');
24 await page.keyboard.press('Enter');
25
26 // Tab through a form and assert focus moves correctly
27 await page.getByLabel('Email').focus();
28 await page.keyboard.press('Tab');
29 await expect(page.getByLabel('Password')).toBeFocused();
30 await page.keyboard.press('Tab');
31 await expect(page.getByRole('button', { name: 'Sign in' })).toBeFocused();
32
33 // Command palette (Cmd+K / Ctrl+K)
34 await page.keyboard.press('Control+k');
35 await expect(page.getByRole('dialog', { name: 'Command palette' })).toBeVisible();
36 await page.keyboard.type('TypeScript');
37 await page.keyboard.press('ArrowDown');
38 await page.keyboard.press('Enter');
39
40 // Hold modifier while pressing other keys
41 await page.keyboard.down('Shift');
42 await page.keyboard.press('Tab');
43 await page.keyboard.up('Shift');
44});1test('upload profile avatar', async ({ page }) => {
2 await page.goto('/settings/profile');
3
4 // setInputFiles works on <input type="file">
5 // Use a real test file from the e2e fixtures directory
6 await page.getByLabel('Profile photo').setInputFiles('e2e/fixtures/avatar.jpg');
7
8 // Multiple files
9 await page.getByLabel('Course attachments').setInputFiles([
10 'e2e/fixtures/lesson-1.pdf',
11 'e2e/fixtures/lesson-2.pdf',
12 ]);
13
14 // Clear an input file
15 await page.getByLabel('Profile photo').setInputFiles([]);
16
17 // Handle a file chooser dialog triggered by button click
18 const fileChooserPromise = page.waitForEvent('filechooser');
19 await page.getByRole('button', { name: 'Upload video' }).click();
20 const fileChooser = await fileChooserPromise;
21 await fileChooser.setFiles('e2e/fixtures/lesson-video.mp4');
22
23 // Assert the upload completed
24 await expect(page.getByRole('status')).toContainText('Upload complete', { timeout: 30_000 });
25 await expect(page.getByAltText('Profile photo')).toBeVisible();
26});
27
28// Creating fixture files programmatically in a test setup:
29// import { writeFileSync } from 'fs';
30// test.beforeAll(() => {
31// writeFileSync('e2e/fixtures/test.txt', 'test file content');
32// });When a click opens a new tab, Playwright loses track of it unless you listen for the new page event on the browser context.
1test('external link opens in new tab', async ({ page, context }) => {
2 await page.goto('/courses/1');
3
4 // Wait for new page before clicking the link that opens it
5 const pagePromise = context.waitForEvent('page');
6 await page.getByRole('link', { name: 'View on GitHub' }).click();
7 const newPage = await pagePromise;
8
9 // Wait for new tab to load
10 await newPage.waitForLoadState();
11
12 expect(newPage.url()).toContain('github.com');
13 await expect(newPage.getByRole('heading')).toBeVisible();
14
15 // You can interact with the new page
16 await newPage.close();
17});
18
19// Popup window (window.open):
20test('payment widget opens as popup', async ({ page, context }) => {
21 await page.goto('/checkout');
22
23 const popupPromise = context.waitForEvent('page');
24 await page.getByRole('button', { name: 'Pay with Stripe' }).click();
25 const popup = await popupPromise;
26
27 await popup.waitForLoadState();
28 expect(popup.url()).toContain('stripe.com');
29 await popup.getByLabel('Card number').fill('4242424242424242');
30});
31
32// Asserting a link has target="_blank" without clicking it:
33await expect(
34 page.getByRole('link', { name: 'Privacy Policy' })
35).toHaveAttribute('target', '_blank');A three-step course creation wizard tested with Playwright covering text inputs, rich text, file upload, select menus, checkbox terms, and dialog confirmation.
1// e2e/course-creation.spec.ts
2import { test, expect } from '@playwright/test';
3import path from 'path';
4
5test.describe('Course Creation Wizard', () => {
6 test.beforeEach(async ({ page }) => {
7 // Log in as instructor
8 await page.goto('/login');
9 await page.getByLabel('Email').fill('[email protected]');
10 await page.getByLabel('Password').fill('InstructorPass1!');
11 await page.getByRole('button', { name: 'Sign in' }).click();
12 await expect(page).toHaveURL('/dashboard');
13 await page.goto('/instructor/courses/new');
14 });
15
16 test('completes step 1 — basic information', async ({ page }) => {
17 // Step 1: Basic Info
18 await expect(page.getByRole('heading', { name: 'Course Details' })).toBeVisible();
19
20 await page.getByLabel('Course title').fill('TypeScript for React Developers');
21 await page.getByLabel('Short description').fill(
22 'Learn TypeScript patterns specifically tailored for React development.'
23 );
24 await page.getByLabel('Level').selectOption('INTERMEDIATE');
25 await page.getByLabel('Category').selectOption('frontend');
26 await page.getByLabel('Price (USD)').fill('59.99');
27
28 // Upload thumbnail
29 await page.getByLabel('Course thumbnail').setInputFiles(
30 path.join(__dirname, 'fixtures/course-thumbnail.jpg')
31 );
32 await expect(page.getByAltText('Course thumbnail preview')).toBeVisible({ timeout: 10_000 });
33
34 await page.getByRole('button', { name: 'Next: Curriculum' }).click();
35 await expect(page.getByRole('heading', { name: 'Build Your Curriculum' })).toBeVisible();
36 });
37
38 test('completes step 2 — adds a module and lesson', async ({ page }) => {
39 // Skip to step 2 (assume step 1 was saved)
40 await page.goto('/instructor/courses/draft-001/curriculum');
41
42 await page.getByRole('button', { name: 'Add module' }).click();
43 await page.getByLabel('Module title').fill('TypeScript Fundamentals');
44 await page.getByRole('button', { name: 'Save module' }).click();
45 await expect(page.getByText('TypeScript Fundamentals')).toBeVisible();
46
47 // Add a lesson to the module
48 const module = page.getByRole('region', { name: 'TypeScript Fundamentals' });
49 await module.getByRole('button', { name: 'Add lesson' }).click();
50 await page.getByLabel('Lesson title').fill('Variables and Types');
51 await page.getByLabel('Duration (minutes)').fill('15');
52 await page.getByRole('button', { name: 'Save lesson' }).click();
53
54 await expect(module.getByText('Variables and Types')).toBeVisible();
55 });
56
57 test('step 3 publish confirmation requires checkbox and dialog acceptance', async ({ page }) => {
58 await page.goto('/instructor/courses/draft-001/publish');
59
60 // Review checklist
61 await expect(page.getByRole('list', { name: 'Publish checklist' })).toBeVisible();
62
63 // Must check terms before publishing
64 await page.getByLabel('I confirm this course meets the content guidelines').check();
65 await page.getByLabel('I have reviewed the pricing').check();
66
67 // Publish button is now enabled
68 const publishBtn = page.getByRole('button', { name: 'Publish Course' });
69 await expect(publishBtn).toBeEnabled();
70
71 // Confirm dialog
72 page.once('dialog', async (dialog) => {
73 expect(dialog.message()).toContain('publish');
74 await dialog.accept();
75 });
76
77 await publishBtn.click();
78
79 // Success state
80 await expect(page.getByRole('status')).toContainText('Course published successfully');
81 await expect(page).toHaveURL(/\/instructor\/courses\/[^/]+$/);
82 });
83
84 test('keyboard shortcut Ctrl+S saves the draft', async ({ page }) => {
85 await page.goto('/instructor/courses/draft-001/curriculum');
86 await page.getByLabel('Module title').fill('New Module');
87
88 await page.keyboard.press('Control+s');
89 await expect(page.getByRole('status')).toContainText('Draft saved');
90 });
91});This wizard test covers: getByLabel for all form fields, selectOption by value string, setInputFiles for thumbnail upload, getByRole('region') to scope module-level actions, checkbox checking with check(), dialog event handling registered before the publish click, and keyboard shortcut testing with page.keyboard.press(). Each test step is independent — step 3 navigates directly to the publish URL rather than navigating through steps 1 and 2, keeping tests fast and isolated.
The next lesson covers the Page Object Model pattern: encapsulating page interactions in reusable classes so tests stay focused on behaviour rather than DOM navigation details.
Your progress is saved in this browser, on this device only.