Clicking, Typing & Interacting

Learn all the ways to interact with page elements

8 min readNode.js

Clicking, Typing & Interacting

In the previous tutorial, we learned how to find elements on the page. Now let's do stuff with them — click buttons, fill forms, drag things around. Playwright makes this surprisingly simple, and handles most of the timing headaches for you.

The Magic of Auto-Waiting

"Do I need to add waits before clicking?"

Nope! Before we dive into actions, you need to understand Playwright's killer feature: auto-waiting.

When you do this:

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

Playwright automatically:

  1. Waits for the element to appear in the DOM
  2. Waits for it to be visible
  3. Waits for it to be stable (not animating)
  4. Waits for it to receive pointer events (not covered by other elements)
  5. Waits for it to be enabled

Only then does it click. No sleep(), no waitForElement(). It just works. How cool is that?

Clicking

Basic Click

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

// Click a link
await page.getByRole('link', { name: 'Learn more' }).click();

// Click anything
await page.locator('.card').click();

Double Click

// Select a word in text
await page.getByText('Click to edit').dblclick();

// Open a file in a file manager
await page.locator('.file-item').dblclick();

Right Click (Context Menu)

// Open context menu
await page.getByText('Document.pdf').click({ button: 'right' });

// Then interact with the menu
await page.getByRole('menuitem', { name: 'Delete' }).click();

Click Position

Sometimes you need to click a specific spot on an element:

// Click the top-left corner
await page.locator('.slider').click({ position: { x: 0, y: 0 } });

// Click the center (default)
await page.locator('.canvas').click({ position: { x: 100, y: 50 } });

Click with Modifiers

Hold keys while clicking:

// Ctrl+Click (or Cmd+Click on Mac) - often opens in new tab
await page.getByRole('link', { name: 'Article' }).click({ modifiers: ['Control'] });

// Shift+Click - often selects a range
await page.getByRole('row').last().click({ modifiers: ['Shift'] });

// Multiple modifiers
await page.locator('.item').click({ modifiers: ['Control', 'Shift'] });

Force Click

"Playwright won't let me click this element!"

When Playwright refuses to click (element covered, disabled, etc.) and you know what you're doing:

// Skip actionability checks
await page.locator('.hidden-trigger').click({ force: true });

Warning: Use force sparingly. If Playwright won't click something, there's usually a reason — often the same reason a real user couldn't click it.

Typing Text

fill() — The Fast Way

Use fill() for most text input. It clears existing content and types instantly:

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

// Fill a textarea
await page.getByLabel('Message').fill('Hello!\n\nThis is a multi-line message.');

// Fill clears existing text first
await page.getByLabel('Search').fill('new search'); // Replaces any existing text

type() — The Slow Way

Use type() when you need to simulate real keystrokes (triggers keyboard events):

// Type slowly, like a human
await page.getByLabel('Search').type('playwright', { delay: 100 }); // 100ms between keys

// Useful for autocomplete testing
await page.getByRole('combobox').type('new y');
// Now wait for suggestions to appear
await page.getByRole('option', { name: 'New York' }).click();

pressSequentially() — Character by Character

Similar to type() but clearer about intent:

await page.getByLabel('Code').pressSequentially('ABC123', { delay: 50 });

Clearing Input

// Method 1: fill with empty string
await page.getByLabel('Search').fill('');

// Method 2: clear() method
await page.getByLabel('Search').clear();

// Method 3: select all and delete
await page.getByLabel('Search').press('Control+a');
await page.getByLabel('Search').press('Backspace');

Keyboard Actions

Single Keys

// Press Enter
await page.getByLabel('Search').press('Enter');

// Press Escape
await page.keyboard.press('Escape');

// Press Tab
await page.keyboard.press('Tab');

// Arrow keys
await page.keyboard.press('ArrowDown');
await page.keyboard.press('ArrowUp');

Key Combinations

// Ctrl+A (select all)
await page.keyboard.press('Control+a');

// Ctrl+C (copy)
await page.keyboard.press('Control+c');

// Ctrl+V (paste)
await page.keyboard.press('Control+v');

// Ctrl+Shift+K (e.g., delete line in editors)
await page.keyboard.press('Control+Shift+k');

// On Mac? Playwright handles it - Control maps to Meta automatically
// Or be explicit:
await page.keyboard.press('Meta+c'); // Cmd+C on Mac

Hold Keys

// Hold Shift while doing something
await page.keyboard.down('Shift');
await page.getByText('First item').click();
await page.getByText('Last item').click();
await page.keyboard.up('Shift');

Working with Forms

Checkboxes

// Check a checkbox
await page.getByLabel('Subscribe to newsletter').check();

// Uncheck
await page.getByLabel('Subscribe to newsletter').uncheck();

// Toggle (use setChecked for explicit state)
await page.getByLabel('Enable notifications').setChecked(true);
await page.getByLabel('Enable notifications').setChecked(false);

// Check if it's checked
const isChecked = await page.getByLabel('Remember me').isChecked();

Radio Buttons

// Select a radio option
await page.getByLabel('Express shipping').check();

// Or by role
await page.getByRole('radio', { name: 'Credit card' }).check();

Dropdowns (Select)

For native <select> elements:

// By visible text
await page.getByLabel('Country').selectOption('United States');

// By value attribute
await page.getByLabel('Country').selectOption({ value: 'us' });

// By label text
await page.getByLabel('Country').selectOption({ label: 'United States' });

// Multiple selections (for multi-select)
await page.getByLabel('Toppings').selectOption(['cheese', 'pepperoni', 'mushrooms']);

For custom dropdowns (divs styled as dropdowns):

// Click to open
await page.getByRole('combobox', { name: 'Country' }).click();

// Then click the option
await page.getByRole('option', { name: 'United States' }).click();

File Uploads

// Single file
await page.getByLabel('Upload document').setInputFiles('path/to/file.pdf');

// Multiple files
await page.getByLabel('Upload photos').setInputFiles([
  'photo1.jpg',
  'photo2.jpg',
  'photo3.jpg'
]);

// Clear selected files
await page.getByLabel('Upload').setInputFiles([]);

// For drag-and-drop style uploaders
await page.locator('.dropzone').setInputFiles('document.pdf');

For file chooser dialogs:

// Handle the file chooser event
const fileChooserPromise = page.waitForEvent('filechooser');
await page.getByRole('button', { name: 'Upload' }).click();
const fileChooser = await fileChooserPromise;
await fileChooser.setFiles('path/to/file.pdf');

Hover and Focus

Hover

// Hover to reveal a dropdown menu
await page.getByRole('button', { name: 'Account' }).hover();
await page.getByRole('menuitem', { name: 'Settings' }).click();

// Hover over an element to see tooltip
await page.getByText('What is this?').hover();
await expect(page.getByRole('tooltip')).toBeVisible();

Focus

// Focus an input without typing
await page.getByLabel('Search').focus();

// Check if element is focused
await expect(page.getByLabel('Search')).toBeFocused();

// Blur (unfocus) the active element
await page.getByLabel('Search').blur();

Drag and Drop

"Can Playwright handle drag and drop?"

Absolutely.

Simple Drag and Drop

// Drag element to target
await page.getByText('Drag me').dragTo(page.getByText('Drop here'));

// Or with locators
await page.locator('.draggable').dragTo(page.locator('.droppable'));

Manual Drag (for complex cases)

// More control over the drag operation
const source = page.locator('.card');
const target = page.locator('.column-2');

await source.hover();
await page.mouse.down();
await target.hover();
await page.mouse.up();

Drag with Offset

// Drag to specific position
await page.locator('.slider-handle').dragTo(page.locator('.slider-track'), {
  targetPosition: { x: 100, y: 0 }
});

Scrolling

Scroll into View

Most of the time, Playwright auto-scrolls. But sometimes you need control:

// Scroll element into view
await page.getByText('Footer').scrollIntoViewIfNeeded();

// Scroll to specific position
await page.mouse.wheel(0, 500); // Scroll down 500px

// Scroll within a container
await page.locator('.scroll-container').evaluate(el => {
  el.scrollTop = 1000;
});

Actions on Frames

For iframes:

// Get the frame
const frame = page.frameLocator('iframe[name="editor"]');

// Now use it like page
await frame.getByRole('textbox').fill('Hello from iframe');
await frame.getByRole('button', { name: 'Save' }).click();

Waiting for Actions to Complete

Usually auto-waiting handles everything. But sometimes you need to wait for side effects:

// Wait for navigation after click
await Promise.all([
  page.waitForURL('**/dashboard'),
  page.getByRole('button', { name: 'Login' }).click(),
]);

// Wait for network request after action
await Promise.all([
  page.waitForResponse(resp => resp.url().includes('/api/save')),
  page.getByRole('button', { name: 'Save' }).click(),
]);

// Wait for element to disappear
await page.getByRole('button', { name: 'Delete' }).click();
await expect(page.getByText('Item deleted')).toBeVisible();
await expect(page.getByText('Item deleted')).toBeHidden();

Common Patterns

Login Flow

async function login(page, username, password) {
  await page.getByLabel('Username').fill(username);
  await page.getByLabel('Password').fill(password);
  await page.getByRole('button', { name: 'Sign in' }).click();
  await page.waitForURL('**/dashboard');
}

Form Submission

// Fill and submit a form
await page.getByLabel('Name').fill('John Doe');
await page.getByLabel('Email').fill('john@example.com');
await page.getByLabel('Message').fill('Hello there!');
await page.getByRole('button', { name: 'Submit' }).click();

// Wait for success message
await expect(page.getByText('Message sent!')).toBeVisible();

Handling Dialogs

// Accept an alert
page.on('dialog', dialog => dialog.accept());
await page.getByRole('button', { name: 'Delete' }).click();

// Dismiss a confirm dialog
page.on('dialog', dialog => dialog.dismiss());

// Handle prompt with input
page.on('dialog', async dialog => {
  await dialog.accept('My input');
});

Debugging Failed Actions

When an action fails:

  1. Check the locator — Is it finding the right element?

    console.log(await page.getByRole('button', { name: 'Submit' }).count());
    
  2. Take a screenshot — See what the page looks like

    await page.screenshot({ path: 'debug.png' });
    
  3. Use pause() — Interactive debugging

    await page.pause();
    
  4. Check if element is covered — Common issue

    await page.getByRole('button', { name: 'Submit' }).click({ trial: true });
    // This checks if click would work without actually clicking
    

What's Next?

You're now a Playwright action hero! You learned:

  • click(), dblclick(), and right-click with { button: 'right' }
  • fill() for fast input, type() for realistic typing
  • check(), uncheck(), selectOption() for form controls
  • setInputFiles() for uploads
  • hover(), focus(), dragTo() for interactions
  • Playwright auto-waits, so you rarely need explicit waits

But how do you verify things actually worked? Let's learn about assertions. Let's go!