A locator answers “which element?” An action answers “what should happen?” Playwright checks that an element is usable before acting.
It resembles checking that a door is visible and unlocked before pushing it.
find → wait until actionable → act → application responds
Series: Playwright Zero to Expert | Demo app used throughout this series: saucedemo.com
You can now reliably find any element on a page. This part covers the full range of things Playwright can actually do to that element once found — the verbs, so to speak, that pair with the nouns (locators) from Part 7. Every one of these follows the exact same rule you learned about .click() in Part 7: Playwright auto-waits for the element to be genuinely actionable before performing the action, every single time, for every one of these methods.
Analogy: The Standardized Driver’s Interface of a Car Think of browser actions like driving a modern vehicle:
- Direct engine manipulation (Old way): In early automation tools, you occasionally had to dispatch raw JavaScript click events directly onto elements. This was like reaching under the car’s hood, turning the throttle cable manually, and spraying fuel directly into the cylinders. It worked, but it completely ignored whether the steering wheel was locked, whether the gear was in Park, or whether the road was clear.
- Standardized Controls (Playwright Actions): You sit in the driver’s seat and step on the gas pedal (click, fill). Playwright translates this simple press into complex mechanical checks (the actionability checklist) and coordinates the engine response. If you step on the gas while the parking brake is set (the button is covered or disabled), the car alerts you rather than forcing the engine to turn.
📊 Visual Flowchart: The User Action Dispatch Pipeline
Here is how Playwright coordinates element location checks, visual scrolling, and raw OS pointer event dispatches:
graph TD
Start["Action Call: locator.click()"] --> CheckDOM["Find unique target element in DOM"]
CheckDOM --> Scroll["Scroll element into viewport"]
Scroll --> Actionability["Verify Actionability Checklist<br>(Visible, Enabled, Stable, Unobscured)"]
Actionability --> Dispatch["Dispatch Pointer Events<br>(mousedown, mouseup, click)"]
Dispatch --> WaitNavigation{"Does action trigger<br>page navigation?"}
WaitNavigation -->|Yes| WaitForNav["Auto-wait for Network / Page Load"]
WaitNavigation -->|No| Done["Action Completed Successfully"]
WaitForNav --> Done
click()
await page.getByRole("button", { name: "Login" }).click();
The most common action in all of browser automation. Worth knowing: .click() accepts options for more specific scenarios you’ll eventually need — { button: 'right' } for a right-click, { clickCount: 2 } for a double-click, { force: true } to bypass Playwright’s actionability checks entirely (a genuine last resort, covered with a proper warning below).
fill()
await page.getByPlaceholder("Username").fill("standard_user");
Sets a text field’s value directly and immediately — it clears whatever was there first, then sets the new value in one atomic step. This is the right default choice for the overwhelming majority of “type text into a field” scenarios, because it’s fast and reliable.
type() (or pressSequentially())
await page
.getByPlaceholder("Username")
.pressSequentially("standard_user", { delay: 100 });
Unlike fill(), this genuinely simulates individual key presses, one character at a time, with an optional delay between each. This matters specifically when a page has JavaScript listening for actual keystroke events — a live search-as-you-type field, or a password-strength meter that updates character by character, for instance. fill() sets the value in one step and won’t trigger that kind of per-keystroke logic; pressSequentially() will.
A common early mistake is reaching for this by default out of habit (or because older Selenium-style tutorials always “type” character by character) — it’s meaningfully slower than fill(), so it’s worth reserving specifically for the cases that actually need real keystroke simulation, not using everywhere out of habit.
press()
await page.getByPlaceholder("Password").press("Enter");
Simulates pressing a single specific key — Enter, Tab, Escape, arrow keys, and more. Genuinely useful for testing keyboard-driven flows (submitting a form with Enter instead of clicking a button, tabbing between fields) — and, notably, testing keyboard navigation at all is itself a meaningful piece of accessibility testing, a theme Part 35 returns to properly.
check() and uncheck()
await page.getByLabel("Remember me").check();
await page.getByLabel("Remember me").uncheck();
For checkboxes and radio buttons specifically. Notice these are idempotent by design — calling .check() on an already-checked box does nothing harmful and doesn’t throw an error; it simply confirms the end state you want, which is exactly the intent-revealing way to write this kind of action, rather than manually checking current state first and conditionally clicking.
selectOption()
await page.getByLabel("Sort by").selectOption("Price (low to high)");
// or, equally valid, by the underlying <option>'s value attribute:
await page.getByLabel("Sort by").selectOption({ value: "lohi" });
For <select> dropdown elements — SauceDemo’s inventory sort dropdown is a real, live example of exactly this kind of element. You can select by the option’s visible label text or by its underlying value attribute; which one is more stable depends entirely on which one is less likely to change — a value like lohi used purely as an internal identifier is often more stable than display text like “Price (low to high),” which a copywriter might tweak.
hover()
await page.getByText("Sauce Labs Backpack").hover();
Moves the mouse over an element without clicking — necessary for testing tooltips, dropdown menus that only appear on hover, or any UI that specifically reveals something in response to a hover state rather than a click.
dragAndDrop()
await page.dragAndDrop("#item-1", "#drop-zone");
Simulates dragging one element and dropping it onto another — used for reordering lists, drag-based file uploads, kanban-board-style interfaces, and similar UI patterns.
Drag-and-drop interactions are, honestly, one of the more finicky things to automate reliably across different applications, because so much depends on exactly how the underlying application implements the dragging behavior in JavaScript — if dragAndDrop() doesn’t work cleanly on a particular app, it’s often necessary to fall back to manually simulating the sequence of mouse-down, mouse-move, and mouse-up events instead, a technique worth knowing exists even if it’s beyond what you need on day one.
File Upload
await page.getByLabel("Upload file").setInputFiles("path/to/file.pdf");
// Multiple files at once:
await page.getByLabel("Upload files").setInputFiles(["file1.pdf", "file2.pdf"]);
// Clearing a file input:
await page.getByLabel("Upload file").setInputFiles([]);
Notice something genuinely convenient here: this doesn’t require Playwright to interact with your operating system’s native file-picker dialog at all (which would otherwise be a real headache to automate, since that dialog isn’t part of the web page’s DOM). setInputFiles() sets the file directly on the underlying <input type="file"> element, sidestepping the OS dialog entirely — fast, reliable, and something that would be a genuinely painful problem to solve with older automation approaches.
File Download
const downloadPromise = page.waitForEvent("download");
await page.getByText("Download Report").click();
const download = await downloadPromise;
console.log(download.suggestedFilename()); // e.g., "report.pdf"
await download.saveAs("/path/to/save/report.pdf");
This one’s worth reading carefully, because the pattern — setting up a “wait for this event” before triggering the action that causes it — is genuinely important and reappears constantly once you reach Part 10 (multiple tabs) and Part 13 (dialogs/popups). A download doesn’t happen as a direct, synchronous result of the click; it’s triggered by the click, but happens as a separate browser event.
Setting up page.waitForEvent('download') first means Playwright is already “listening” the moment the click happens, rather than the click firing before you’ve told Playwright to pay attention — get this order backwards, and you risk missing the event entirely.
force: true — a Genuine Last Resort, With a Real Warning
Every action so far respects Playwright’s actionability checks from Part 7 automatically. { force: true } explicitly bypasses them:
await page.getByRole("button", { name: "Submit" }).click({ force: true });
It’s worth being honest about what this actually does: it tells Playwright “click this element regardless of whether it’s visible, whether something is covering it, or whether it’s actually enabled.” This can genuinely be necessary in rare, specific edge cases — but reaching for it as a quick fix whenever a normal .click() times out is a real, common mistake worth naming directly.
If a click is timing out because the element is covered by something else, that’s usually Playwright correctly telling you something a real user would also experience — a real user can’t click a button hidden behind an overlay either. Forcing the click makes the test pass while potentially leaving a genuine usability bug in the application completely undetected.
Treat a failed actionability check as a signal worth investigating first, and force: true as a deliberate, justified exception — not a default habit for making red tests turn green quickly.
How It Works in a Real Test Run
An action is a small synchronization workflow. Before click, Playwright resolves the locator, verifies that one element matches, performs relevant actionability checks such as visibility, stability, event reception, and enabled state, then sends the input event.
After the action, the test should verify the observable outcome rather than assume the click worked. For example: click Save → wait through a web-first assertion → confirm the success message or persisted data.
Interview Questions
Q: What’s the difference between fill() and pressSequentially() (formerly type()), and when would you choose one over the other?
Ans: fill() sets a field’s value directly and immediately, in one step, without simulating individual keystrokes — it’s the faster default for the vast majority of text-entry scenarios. pressSequentially() genuinely simulates each keystroke individually, which matters specifically when a page has JavaScript reacting to real keystroke events, like a live search-as-you-type suggestion box or a password-strength indicator that updates per character — fill() wouldn’t trigger that logic since it doesn’t fire individual key events.
Q: Why is check() considered a better choice than manually checking a checkbox’s current state and conditionally clicking it?
Ans: check() is idempotent and intent-revealing — it guarantees the checkbox ends up checked, doing nothing if it’s already checked, without you needing to manually read its current state first and branch your test logic around it. This makes the test’s intent clearer (I want this to be checked, however it currently stands) and avoids extra, unnecessary conditional logic cluttering the test.
Q: How does Playwright handle file uploads without needing to interact with the operating system’s native file-picker dialog?
Ans: setInputFiles() sets the file directly on the underlying <input type="file"> element in the DOM, bypassing the native OS file dialog entirely, since that dialog isn’t part of the web page and would otherwise be very difficult to automate reliably across different operating systems.
Q: When handling a file download, why is it important to call page.waitForEvent('download') before triggering the action that causes the download, rather than after?
Ans: A download is triggered by an action like a click, but occurs as a separate, asynchronous browser event rather than a direct, synchronous result of that click. Setting up the event listener first ensures Playwright is already watching for the download the moment it happens; setting it up afterward risks missing the event entirely if the download completes, or begins, before Playwright started listening for it.
Q: What does { force: true } actually do, and why should it be used cautiously?
Ans: It bypasses Playwright’s normal actionability checks — visibility, not being obscured, being enabled — and performs the action regardless. It should be used cautiously because those checks usually reflect genuine constraints a real user would also face; forcing past them can make a test pass while masking a real usability bug in the application, like a button that’s actually hidden behind an overlay for real users too.
Q: A test needs to interact with a <select> dropdown. What are the two ways you can select an option, and which would you generally prefer for long-term stability?
Ans: You can select by the option’s visible display text, or by its underlying value attribute. Generally, the value attribute tends to be the more stable choice, since it’s often used purely as an internal identifier and is less likely to be changed casually than display text, which copywriters or designers might edit for wording or clarity without realizing it would break a test relying on it.
Exercises — Part 8
Understand:
Without looking back, explain in your own words the practical difference between fill() and pressSequentially(), using a concrete example of a page feature where the difference would actually matter.
Simple Practice:
On SauceDemo’s inventory page, write a test that uses selectOption() on the sort dropdown to sort products by “Price (low to high),” then reads the first product’s price and asserts it’s genuinely lower than the last product’s price on the page.
Real-World Scenario:
You’re testing a checkout form with a “Submit Order” button that a teammate’s test forces past with { force: true }, because it otherwise times out. Investigate (in your own head, or on a real similar site) what a normal actionability timeout on a click is actually telling you, and write a short explanation of what you’d want to check about the application itself before agreeing to keep force: true in the test.
Challenge:
Find any public site with a file upload feature (many demo/testing sites offer one) and write a Playwright test using setInputFiles() to upload a small file, then assert that the page reflects the successful upload in some way (a filename appearing, a success message, etc.).
Next: Part 9 — Assertions
— now that you can find and act on elements, it’s time to properly verify outcomes: expect, web-first assertions, auto-retry, and the real distinction between an action and an assertion.
- Author
- TechByteByByte Editorial Team
- Reviewed by
- TechByteByByte Admin
- Published
- Last reviewed