Click & Type in Playwright
Clicking a button. Typing into a search box. These are the most basic human actions on the web—and they must work flawlessly in your tests. Playwright's actionability system ensures they do.
📖 The Story: The Invisible Button
Maya wrote a test that clicked a "Submit" button. It worked on her machine. In CI, it failed with: "Element is not clickable — element has pointer-events: none." The button existed in the DOM, it was visible, but a CSS animation hadn't finished, and the button was temporarily covered by a loading spinner. The test clicked right through the spinner and missed the button entirely.
Maya added await page.waitForTimeout(2000). It worked—until the CI server was slow and 2 seconds wasn't enough. Then she learned about Playwright's actionability checks. Playwright automatically waits until the button is visible, stable, enabled, and not obscured. No timeouts needed. No flaky failures. Just reliable automation.
🎯 Why Actionability Matters
Auto-Wait
Playwright waits for elements to be ready before acting—no manual waits needed.
Precision
Clicks go to the exact element you targeted, even if the page shifts.
Debugging
When an action fails, Playwright tells you exactly WHY—covered, disabled, detached, or animating.
🧒 Explain Like I'm 10
Imagine you're at a crosswalk. The sign says "Don't Walk." You could run across anyway (that's what Selenium does sometimes)—but you might get hit. Playwright is like a patient friend who watches the sign and ONLY crosses when it says "Walk." That's actionability: waiting until it's safe to act.
🎓 Professional Explanation
Before Playwright performs any action (click, fill, hover, etc.), it runs a series of actionability checks. These checks verify that the element is: (1) Attached to the DOM, (2) Visible, (3) Stable (not animating), (4) Enabled (not disabled), and (5) Receives events (not covered by another element). If any check fails, Playwright waits and retries until the timeout expires. This eliminates the vast majority of flaky test failures caused by timing issues.
👆 Click Actions
// Simple click await page.getByRole('button', { name: 'Submit' }).click(); // Click with specific mouse button (left, right, middle) await page.getByRole('button', { name: 'Submit' }).click({ button: 'right' }); // Click with modifier keys await page.getByRole('link', { name: 'Open' }).click({ modifiers: ['Control'] }); // Double click await page.getByText('Edit me').dblclick(); // Force click (skip actionability checks — use with caution!) await page.getByRole('button', { name: 'Submit' }).click({ force: true });
When to use force: true: Only when you intentionally need to click an element that is genuinely covered by something (e.g., a chat widget overlay) and you know it's safe. Force-clicking skips ALL actionability checks, which can lead to unexpected behavior.
⌨️ Fill vs Type
| Method | Behavior | Speed | Use When |
|---|---|---|---|
.fill(value) | Clears field, sets value instantly | ⚡ Fast | Standard form filling (default choice) |
.type(text) | Simulates individual keypresses | 🕐 Slow | Search autocomplete, real-time validation |
.press(key) | Presses a single key or key combo | ⚡ Fast | Enter, Tab, Escape, Ctrl+A, etc. |
// fill() — instant, clears existing value first await page.getByLabel('Email').fill('admin@test.com'); // type() — character by character (triggers key events) await page.getByLabel('Search').type('Playwright'); // type() with delay between keystrokes (like a human) await page.getByLabel('Search').type('Playwright', { delay: 100 }); // press() — single key or keyboard shortcut await page.getByLabel('Search').press('Enter'); await page.getByLabel('Search').press('Control+a'); // Clear a field await page.getByLabel('Email').clear();
☑️ Checkbox & Radio
// Check a checkbox await page.getByRole('checkbox', { name: 'Accept terms' }).check(); // Uncheck a checkbox await page.getByRole('checkbox', { name: 'Newsletter' }).uncheck(); // Assert checkbox is checked await expect(page.getByRole('checkbox', { name: 'Accept terms' })).toBeChecked(); // Select a radio button await page.getByRole('radio', { name: 'Credit Card' }).check();
📋 Select Dropdowns
// Select by value attribute await page.getByLabel('Country').selectOption('us'); // Select by visible text label await page.getByLabel('Country').selectOption({ label: 'United States' }); // Select by index await page.getByLabel('Country').selectOption({ index: 2 }); // Multi-select await page.getByLabel('Colors').selectOption(['red', 'blue']);
🎹 Keyboard & Mouse
// Keyboard — page-level keyboard input await page.keyboard.press('Escape'); await page.keyboard.press('Control+Shift+I'); await page.keyboard.type('Hello World'); // Mouse — low-level mouse control await page.mouse.click(100, 200); // x, y coordinates await page.mouse.dblclick(100, 200); await page.mouse.hover(100, 200); // Hover over an element (preferred over mouse coordinates) await page.getByRole('link', { name: 'Menu' }).hover(); // Drag and drop await page.getByText('Item A').dragTo(page.getByText('Drop Zone'));
⚠️ Common Mistakes
Mistake 1: Using click() Before the Element Is Ready
// ❌ Manually waiting before click — unnecessary and flaky await page.waitForSelector('#submit'); await page.locator('#submit').click(); // ✅ Just click — Playwright auto-waits for actionability await page.getByRole('button', { name: 'Submit' }).click();
Mistake 2: Using type() When fill() Is Better
// ❌ type() is slow for simple form filling await page.getByLabel('Email').type('admin@test.com'); // ✅ fill() is instant and recommended await page.getByLabel('Email').fill('admin@test.com');
✅ Best Practices
- Use
fill()by default — it's fast and reliable. Usetype()only for real-time search. - Trust actionability checks — don't add manual waits before clicks.
- Use
check()/uncheck()for checkboxes — notclick(). - Use
selectOption()for dropdowns — notclick()+click(). - Use
force: truesparingly — only when you understand the consequences. - Prefer locators over coordinates —
getByRole().click()>mouse.click(x, y).
🏗️ Senior Engineer Deep Dive
The 5 Actionability Checks
Before every action, Playwright runs these checks in order:
- Attached: Element exists in the DOM.
- Visible: Element has non-zero dimensions and is not hidden (display:none, visibility:hidden, opacity:0).
- Stable: Element is not animating (bounding box hasn't changed in the last 2 animation frames).
- Enabled: Element doesn't have
disabledattribute. - Receives Events: Element is the topmost hit target at the click point — no overlay is blocking it.
If any check fails, Playwright re-evaluates at the next animation frame until the action timeout (default 0ms for actions, configurable). This is why Playwright rarely needs explicit waits.
fill() vs type() Under the Hood
fill() dispatches a single input and change event after clearing the field. type() dispatches keydown, keypress, keyup, and input for EACH character. This is why type() triggers autocomplete listeners but fill() may not for some frameworks.
💼 Interview Questions
Show answer
Show answer
Show answer
🏋️ Practical Exercises
Form Interaction
- Navigate to
https://demo.playwright.dev/todomvc. - Use
fill()to type "Learn Playwright" into the input. - Press
Enterusingpress('Enter'). - Verify the todo item appears.
- Add another item using
type()with a 50ms delay to see the difference.
Advanced Interactions
- Write a test that right-clicks a link and verifies a context menu appears.
- Write a test that hovers over a menu item and verifies a dropdown appears.
- Write a test that uses keyboard shortcuts (Ctrl+A to select all text in an input).
- Test drag-and-drop by dragging an item in a sortable list.
🧠 Quiz
type()fill()press()write()click({ force: true }) do?click()check()fill()select().fill('Enter').press('Enter').type('Enter').key('Enter')type() instead of fill()?.fill('Option').selectOption({ label: 'Option' }).click('Option').type('Option').click({ clickCount: 2 }).dblclick().doubleClick().click().click()mouse.down() + mouse.up().dragTo(targetLocator).moveTo(target).drop(target)❓ FAQ
Show answer
--debug mode to see exactly what's happening.Show answer
page.mouse.click(x, y) for coordinate-based clicks. However, this is fragile—any layout change shifts coordinates. Always prefer element-based clicks via locators.Show answer
page.setInputFiles(): await page.getByLabel('Upload').setInputFiles('path/to/file.pdf'). For multiple files, pass an array. To remove files, pass an empty array.📌 Summary
🎯 Key Takeaways
- fill() is the default for text input — fast, clears first, sets value instantly.
- type() simulates keypresses — use for autocomplete and real-time search.
- Actionability checks auto-wait for elements to be ready — no manual waits needed.
- 5 checks: Attached → Visible → Stable → Enabled → Receives Events.
- check()/uncheck() for checkboxes — not click().
- selectOption() for dropdowns — not click() chains.
- force: true skips checks — use only when you know what you're doing.
- Prefer locators over mouse coordinates for maintainable tests.
