Learn Playwright in 10 DaysDay 4: Page Navigation and Forms
books.chapter 4Learn Playwright in 10 Days

Day 4: Page Navigation and Forms

What You Will Learn Today

  • page.goto() and navigation options (waitUntil)
  • page.goBack(), page.goForward(), page.reload()
  • Filling forms: fill(), clear(), pressSequentially()
  • Checkboxes: check(), uncheck()
  • Radio buttons
  • Select dropdowns: selectOption()
  • File upload: setInputFiles()
  • File download handling
  • Dialog handling (alert, confirm, prompt)
  • Keyboard and mouse actions (press, click, dblclick, hover, dragTo)

Page Navigation

page.goto() Basics

page.goto() navigates to the specified URL. It returns a response object that you can inspect.

import { test, expect } from '@playwright/test';

test('basic navigation', async ({ page }) => {
  // Basic navigation
  await page.goto('https://example.com');

  // Get the response object
  const response = await page.goto('https://example.com/api');
  console.log(response?.status()); // 200
});

The waitUntil Option

The waitUntil option controls when navigation is considered complete.

test('waitUntil options', async ({ page }) => {
  // Default: 'load' - window.onload event fires
  await page.goto('https://example.com');

  // Wait until DOM content is loaded (faster)
  await page.goto('https://example.com', {
    waitUntil: 'domcontentloaded'
  });

  // Wait until there are no network connections for 500ms
  await page.goto('https://example.com', {
    waitUntil: 'networkidle'
  });

  // Don't wait - just start navigation
  await page.goto('https://example.com', {
    waitUntil: 'commit'
  });
});
Option Description Use Case
load Waits for the load event (default) General pages
domcontentloaded Waits for DOMContentLoaded event When you want to start interacting quickly
networkidle Waits until no network connections for 500ms SPAs or pages with many API calls
commit Waits until response is received Minimal waiting

Setting Timeouts

test('navigation with timeout', async ({ page }) => {
  await page.goto('https://slow-site.com', {
    timeout: 60000 // 60 seconds
  });
});

Browser Navigation

test('browser navigation', async ({ page }) => {
  await page.goto('https://example.com/page1');
  await page.goto('https://example.com/page2');

  // Go back to page1
  await page.goBack();
  await expect(page).toHaveURL(/page1/);

  // Go forward to page2
  await page.goForward();
  await expect(page).toHaveURL(/page2/);

  // Reload the current page
  await page.reload();
  await expect(page).toHaveURL(/page2/);
});

Form Input

fill() - Basic Text Input

fill() clears the existing value and sets the new one in a single operation.

test('fill text inputs', async ({ page }) => {
  await page.goto('https://example.com/form');

  // Fill text input
  await page.getByLabel('Username').fill('testuser');

  // Fill email input
  await page.getByLabel('Email').fill('test@example.com');

  // Fill password input
  await page.getByLabel('Password').fill('SecurePass123!');

  // Fill textarea
  await page.getByLabel('Bio').fill('Hello, I am a test user.\nNice to meet you.');
});

clear() - Clearing Input Values

test('clear input', async ({ page }) => {
  await page.goto('https://example.com/form');

  const input = page.getByLabel('Username');
  await input.fill('testuser');

  // Clear the input
  await input.clear();
  await expect(input).toHaveValue('');
});

pressSequentially() - Typing One Character at a Time

pressSequentially() simulates typing each character individually via the keyboard. This is useful for testing autocomplete or real-time validation.

test('press sequentially for autocomplete', async ({ page }) => {
  await page.goto('https://example.com/search');

  // Type one character at a time with delay
  await page.getByLabel('Search').pressSequentially('playwright', {
    delay: 100 // 100ms delay between each keystroke
  });

  // Wait for autocomplete suggestions
  await expect(page.getByRole('listbox')).toBeVisible();
});

fill() vs pressSequentially(): fill() sets the value directly and is much faster. pressSequentially() fires individual key events, making it suitable for testing UIs that respond to input events in real time.


Checkboxes and Radio Buttons

check() and uncheck()

test('checkboxes', async ({ page }) => {
  await page.goto('https://example.com/settings');

  // Check a checkbox
  await page.getByLabel('Enable notifications').check();
  await expect(page.getByLabel('Enable notifications')).toBeChecked();

  // Uncheck a checkbox
  await page.getByLabel('Enable notifications').uncheck();
  await expect(page.getByLabel('Enable notifications')).not.toBeChecked();

  // check() is idempotent - does nothing if already checked
  await page.getByLabel('Accept terms').check();
  await page.getByLabel('Accept terms').check(); // No error
});

Radio Buttons

test('radio buttons', async ({ page }) => {
  await page.goto('https://example.com/survey');

  // Select a radio button
  await page.getByLabel('Monthly plan').check();
  await expect(page.getByLabel('Monthly plan')).toBeChecked();

  // Select a different radio button in the same group
  await page.getByLabel('Annual plan').check();
  await expect(page.getByLabel('Annual plan')).toBeChecked();
  await expect(page.getByLabel('Monthly plan')).not.toBeChecked();
});

Select Dropdowns

selectOption()

test('select dropdowns', async ({ page }) => {
  await page.goto('https://example.com/form');

  // Select by value
  await page.getByLabel('Country').selectOption('jp');

  // Select by label text
  await page.getByLabel('Country').selectOption({ label: 'Japan' });

  // Select by index
  await page.getByLabel('Country').selectOption({ index: 2 });

  // Multiple selection (for <select multiple>)
  await page.getByLabel('Languages').selectOption(['en', 'ja', 'ko']);
});

Verifying Selected Values

test('verify selected option', async ({ page }) => {
  await page.goto('https://example.com/form');

  await page.getByLabel('Country').selectOption('jp');
  await expect(page.getByLabel('Country')).toHaveValue('jp');
});

File Upload

setInputFiles()

test('file upload', async ({ page }) => {
  await page.goto('https://example.com/upload');

  // Upload a single file
  await page.getByLabel('Profile picture').setInputFiles('tests/fixtures/avatar.png');

  // Upload multiple files
  await page.getByLabel('Documents').setInputFiles([
    'tests/fixtures/doc1.pdf',
    'tests/fixtures/doc2.pdf'
  ]);

  // Clear file selection
  await page.getByLabel('Profile picture').setInputFiles([]);
});

Uploading from a Buffer

When you don't want to prepare actual files, you can use buffers instead.

test('upload from buffer', async ({ page }) => {
  await page.goto('https://example.com/upload');

  await page.getByLabel('CSV file').setInputFiles({
    name: 'data.csv',
    mimeType: 'text/csv',
    buffer: Buffer.from('name,age\nAlice,30\nBob,25')
  });
});

File Download

Listening for the download Event

import { test, expect } from '@playwright/test';
import path from 'path';

test('file download', async ({ page }) => {
  await page.goto('https://example.com/downloads');

  // Start waiting for download before clicking
  const downloadPromise = page.waitForEvent('download');
  await page.getByRole('link', { name: 'Download Report' }).click();
  const download = await downloadPromise;

  // Verify the file name
  expect(download.suggestedFilename()).toBe('report.pdf');

  // Save the file to a specific path
  await download.saveAs(path.join('tests/downloads', download.suggestedFilename()));
});

Dialog Handling

In Playwright, you handle browser dialogs (alert, confirm, prompt) with page.on('dialog'). You must register the handler before the dialog appears.

alert

test('handle alert', async ({ page }) => {
  await page.goto('https://example.com');

  page.on('dialog', async dialog => {
    expect(dialog.type()).toBe('alert');
    expect(dialog.message()).toBe('Operation completed!');
    await dialog.accept();
  });

  await page.getByRole('button', { name: 'Show Alert' }).click();
});

confirm

test('handle confirm - accept', async ({ page }) => {
  await page.goto('https://example.com');

  page.on('dialog', async dialog => {
    expect(dialog.type()).toBe('confirm');
    await dialog.accept(); // Click OK
  });

  await page.getByRole('button', { name: 'Delete' }).click();
  await expect(page.getByText('Item deleted')).toBeVisible();
});

test('handle confirm - dismiss', async ({ page }) => {
  await page.goto('https://example.com');

  page.on('dialog', async dialog => {
    await dialog.dismiss(); // Click Cancel
  });

  await page.getByRole('button', { name: 'Delete' }).click();
  await expect(page.getByText('Item deleted')).not.toBeVisible();
});

prompt

test('handle prompt', async ({ page }) => {
  await page.goto('https://example.com');

  page.on('dialog', async dialog => {
    expect(dialog.type()).toBe('prompt');
    expect(dialog.defaultValue()).toBe('');
    await dialog.accept('My Answer'); // Enter text and click OK
  });

  await page.getByRole('button', { name: 'Enter Name' }).click();
  await expect(page.getByText('Hello, My Answer')).toBeVisible();
});

Keyboard Actions

press() - Special Keys

test('keyboard actions', async ({ page }) => {
  await page.goto('https://example.com/editor');

  const editor = page.getByRole('textbox');
  await editor.fill('Hello World');

  // Press Enter
  await editor.press('Enter');

  // Press keyboard shortcuts
  await editor.press('Control+a'); // Select all
  await editor.press('Control+c'); // Copy
  await editor.press('End');
  await editor.press('Control+v'); // Paste

  // Press Escape
  await page.press('body', 'Escape');

  // Press Tab to move focus
  await editor.press('Tab');
});

Common Key Names

Key Value
Enter Enter
Tab Tab
Escape Escape
Backspace Backspace
Delete Delete
Arrow keys ArrowUp, ArrowDown, ArrowLeft, ArrowRight
Modifier keys Control, Shift, Alt, Meta

Mouse Actions

Click Variations

test('mouse click variations', async ({ page }) => {
  await page.goto('https://example.com');

  // Standard click
  await page.getByRole('button', { name: 'Submit' }).click();

  // Double click
  await page.getByText('Editable text').dblclick();

  // Right click (context menu)
  await page.getByText('Right click me').click({ button: 'right' });

  // Click with modifier keys
  await page.getByRole('link', { name: 'Open' }).click({ modifiers: ['Control'] });

  // Click at specific position within the element
  await page.getByTestId('canvas').click({ position: { x: 100, y: 200 } });
});

Hover

test('hover action', async ({ page }) => {
  await page.goto('https://example.com');

  await page.getByText('Hover me').hover();
  await expect(page.getByText('Tooltip content')).toBeVisible();
});

Drag and Drop

test('drag and drop', async ({ page }) => {
  await page.goto('https://example.com/kanban');

  // Drag source to target
  const source = page.getByText('Task 1');
  const target = page.getByTestId('done-column');
  await source.dragTo(target);

  await expect(target).toContainText('Task 1');
});

Putting It All Together: Registration Form Test

Here is a practical test combining everything covered today.

import { test, expect } from '@playwright/test';

test.describe('User Registration Form', () => {
  test.beforeEach(async ({ page }) => {
    await page.goto('https://example.com/register');
  });

  test('complete registration with all fields', async ({ page }) => {
    // Text inputs
    await page.getByLabel('Full Name').fill('Taro Yamada');
    await page.getByLabel('Email').fill('taro@example.com');
    await page.getByLabel('Password').fill('SecurePass123!');
    await page.getByLabel('Confirm Password').fill('SecurePass123!');

    // Select dropdown
    await page.getByLabel('Country').selectOption({ label: 'Japan' });

    // Radio button
    await page.getByLabel('Monthly plan').check();

    // Checkboxes
    await page.getByLabel('Technology').check();
    await page.getByLabel('Music').check();

    // File upload
    await page.getByLabel('Avatar').setInputFiles({
      name: 'avatar.png',
      mimeType: 'image/png',
      buffer: Buffer.from('fake-image-data')
    });

    // Terms agreement
    await page.getByLabel('I agree to the terms').check();

    // Submit
    await page.getByRole('button', { name: 'Register' }).click();

    // Verify success
    await expect(page).toHaveURL(/\/welcome/);
    await expect(page.getByText('Registration complete')).toBeVisible();
  });

  test('shows validation errors for empty required fields', async ({ page }) => {
    await page.getByRole('button', { name: 'Register' }).click();

    await expect(page.getByText('Name is required')).toBeVisible();
    await expect(page.getByText('Email is required')).toBeVisible();
  });
});

Summary

Category Method Purpose
Navigation page.goto(url, options) Navigate to a page
Navigation page.goBack() Go back to the previous page
Navigation page.goForward() Go forward to the next page
Navigation page.reload() Reload the current page
Text Input locator.fill(value) Set value (clears existing)
Text Input locator.clear() Clear the input value
Text Input locator.pressSequentially(text) Type one character at a time
Checkbox locator.check() Check a checkbox or radio
Checkbox locator.uncheck() Uncheck a checkbox
Select locator.selectOption(value) Select a dropdown option
File locator.setInputFiles(files) Upload files
Download page.waitForEvent('download') Wait for a download
Dialog page.on('dialog', handler) Handle browser dialogs
Keyboard locator.press(key) Press a key
Mouse locator.click() Click
Mouse locator.dblclick() Double-click
Mouse locator.hover() Hover
Mouse locator.dragTo(target) Drag and drop

Key Takeaways

  1. Choose waitUntil wisely - Use networkidle for SPAs, domcontentloaded for faster tests
  2. Prefer fill() by default - Use pressSequentially() only for specific cases like autocomplete
  3. Register dialog handlers first - Set up page.on('dialog') before the dialog appears
  4. Use the Promise pattern for downloads - Call waitForEvent before clicking the download link
  5. check() is idempotent - It won't throw an error if the checkbox is already checked

Practice Exercises

Basic

  1. Open a page with page.goto() using waitUntil: 'domcontentloaded' and compare the speed with networkidle
  2. Write a navigation test using page.goBack() and page.goForward()
  3. Input the same text with both fill() and pressSequentially() and observe the difference

Intermediate

  1. Create a test that interacts with select dropdowns, checkboxes, and radio buttons
  2. Write a file upload test using setInputFiles() with a buffer
  3. Handle all three dialog types (alert, confirm, prompt) in separate tests

Challenge

  1. Create a drag-and-drop UI test
  2. Write a test that downloads a file and verifies its contents

References


Next Preview

In Day 5, we will learn about Assertions and Snapshots. We will cover the rich set of expect() matchers, auto-retry mechanisms, and visual regression testing with screenshot comparison to make your tests more reliable and robust.