Interacting with Elements — click, fill, check, select

8 min read

You've found the element. Now you need to do something with it. Playwright's action API in Python is the same toolbox as the TypeScript courseclick, fill, type, press, check, select_option, set_input_files, hover, clear — in snake_case form, called from sync def tests with no await. Crucially, every action auto-waits for the element to be attached, visible, stable, enabled, and able to receive events. You don't sprinkle time.sleep calls; the framework handles the timing.

click — the workhorse

page.get_by_role("button", name="Submit").click()
page.get_by_role("link", name="Products").click()
page.get_by_text("Read more").click(force=True)  # skip actionability checks

Three things to know:

  1. Auto-wait built in. click() waits up to the action timeout (30 seconds default) for the element to be visible, stable, and able to receive events. No manual wait needed.
  2. force=True skips the actionability checks — clicks the element even if it's covered, off-screen, or disabled. Reach for it only when you've debugged why the normal click doesn't work and decided to bypass the safety net deliberately.
  3. Click variants: dblclick() for double-click, click(button="right") for right-click, click(modifiers=["Shift"]) to hold a modifier key.

If you're translating from the TypeScript course, the change is await locator.click()locator.click(). That's it.

fill — clear and type (preferred for forms)

page.get_by_label("Email").fill("alice@test.com")
page.get_by_label("Password").fill("password123")
page.get_by_label("Search").fill("")  # clears the input

fill is the right choice for ~95% of form interactions. It clears the existing value, sets the new one in a single operation, and dispatches the input and change events React/Vue/Angular need. For most QA needs, this is the only text-entry method you'll reach for.

type — character by character

When you specifically need keypress events for autocomplete, character counters, or input masks:

page.get_by_label("Search").type("laptop", delay=100)  # 100ms between keys

type (or its newer alias press_sequentially) dispatches keydown, keypress, and keyup for each character. It's slower than fill and only necessary when the page has key-by-key behaviour. The delay parameter throttles between keys — useful when an autocomplete dropdown only appears after a user-realistic typing speed.

If you don't have a specific reason to use type, use fill. It's faster and more reliable.

press — keyboard keys

page.get_by_label("Search").press("Enter")
page.keyboard.press("Escape")
page.keyboard.press("Control+a")    # select all
page.keyboard.press("Shift+Tab")    # focus previous

Two flavours:

  • locator.press(key) — focuses the locator first, then presses the key. Right tool for "submit form by pressing Enter on the email field."
  • page.keyboard.press(key) — presses without focusing anything specific. Right tool for global shortcuts like Escape, Ctrl+A, Cmd+K.

Modifier syntax matches the platform's actual key names: "Control", "Shift", "Alt", "Meta" (Cmd on macOS). Combine with +. The full list of recognised key names is in the Playwright docs.

check / uncheck — checkboxes and radios

page.get_by_label("Remember me").check()
page.get_by_label("Newsletter").uncheck()
page.get_by_label("Monthly plan").check()  # radios use check too

check() is idempotent — it ensures the box ends up checked, regardless of starting state, and verifies the result. uncheck() is the inverse. Compare to click(), which blindly toggles the box — calling click on an already-checked box would uncheck it. Always reach for check/uncheck over click for boolean inputs.

select_option — native dropdowns

page.get_by_label("Country").select_option("UK")               # by value attribute
page.get_by_label("Country").select_option(value="uk")          # explicit by value
page.get_by_label("Country").select_option(label="United Kingdom")  # by visible label
page.get_by_label("Country").select_option(index=2)             # by zero-indexed position
page.get_by_label("Size").select_option(["S", "M"])            # multi-select

select_option works on native <select> elements only. For custom (div-based) dropdowns, you click to open and click the option as a normal element:

page.get_by_role("combobox", name="Role").click()
page.get_by_role("option", name="Tester").click()

The combobox role + option role pattern is what most modern design systems (Material UI, Headless UI, Radix) render. When in doubt, inspect the DOM — if it's a <select>, use select_option; if it's <div role="combobox">, click and click.

set_input_files — file uploads

page.get_by_label("Upload photo").set_input_files("photo.jpg")
page.get_by_label("Documents").set_input_files(["doc1.pdf", "doc2.pdf"])
page.get_by_label("Upload").set_input_files([])  # clear the selection

set_input_files skips the OS file picker entirely — it attaches files to the input directly and fires the right change events. Pass a single path for one file, a list for multi-upload, or an empty list to clear. For in-memory uploads, pass a dict with the buffer:

page.get_by_label("Upload").set_input_files({
    "name": "report.csv",
    "mimeType": "text/csv",
    "buffer": b"id,name\n1,Alice\n2,Bob"
})

This is invaluable for tests that don't want to rely on a real file existing on disk — generate the bytes in the test, upload, assert.

hover — mouseover interactions

page.get_by_text("Menu").hover()
page.get_by_role("img", name="Avatar").hover()

hover triggers mouseenter and mouseover events. Use it to reveal hover-only menus, tooltips, or "edit" buttons that appear on row hover.

clear — empty an input

page.get_by_label("Email").clear()

clear is the explicit form of fill(""). Use it when you want to communicate intent: "empty this field, then assert the validation error appears." Either call works; clear reads better.

All actions auto-wait — no manual waits needed

Every action listed above waits for the element to be:

  1. Attached to the DOM
  2. Visible (non-zero size, not display: none)
  3. Stable (no animation in progress)
  4. Enabled (not disabled, not aria-disabled)
  5. Receiving events (no other element on top)

If the element doesn't meet these conditions within the action timeout, Playwright throws an Error describing exactly which condition failed. This is what makes time.sleep(2) an anti-pattern — Playwright already waits, and it does so smarter than a fixed sleep.

A complete form interaction — the flow

Step 1 of 5

Navigate

page.goto('/register') resolves against base_url and waits for the load event before returning. Auto-waits do the rest.

A complete QA example — registration form

from playwright.sync_api import Page, expect
 
def test_registration_form_happy_path(page: Page):
    page.goto("/register")
 
    # Text inputs — fill is the default
    page.get_by_label("Full name").fill("Alice Smith")
    page.get_by_label("Email").fill("alice@test.com")
    page.get_by_label("Password").fill("SecurePass123")
 
    # Native dropdown
    page.get_by_label("Country").select_option(label="United Kingdom")
 
    # Custom combobox (div-based)
    page.get_by_role("combobox", name="Role").click()
    page.get_by_role("option", name="Tester").click()
 
    # Checkboxes — idempotent toggles
    page.get_by_label("I agree to the terms").check()
    page.get_by_label("Subscribe to newsletter").uncheck()
 
    # Radio button
    page.get_by_label("Monthly plan").check()
 
    # File upload
    page.get_by_label("Profile photo").set_input_files("photo.jpg")
 
    # Submit
    page.get_by_role("button", name="Register").click()
 
    # Verify
    expect(page).to_have_url("/welcome")
    expect(page.get_by_text("Welcome, Alice")).to_be_visible()

Read it as a flow: navigate, fill the form, pick from dropdowns, toggle the booleans, upload a file, submit, verify. Twelve lines of action, three lines of assertion — the shape of every form-driven QA test you'll ever write.

Coming from Playwright TypeScript?

The mapping is the same as locators — drop await, switch to snake_case keyword args:

  • await locator.click()locator.click()
  • await locator.fill('...')locator.fill("...")
  • await locator.selectOption({ label: 'UK' })locator.select_option(label="UK")
  • await locator.setInputFiles('...')locator.set_input_files("...")
  • await locator.press('Enter')locator.press("Enter")

The behaviour, the auto-wait, the actionability checks — all identical. Same engine.

⚠️ Common mistakes

  • Reaching for click() on a checkbox. click() blindly toggles, so calling it on an already-checked box unchecks it. check() is idempotent and asserts the resulting state. Always prefer check/uncheck for boolean inputs — same applies to radios.
  • Using type() when fill() would do. type() dispatches per-character events and runs noticeably slower than fill(). Reach for it only when the page genuinely has key-by-key behaviour (autocomplete that fires per keystroke, character counters that update live). For a normal <input> filled and submitted, fill is faster and more reliable.
  • Forgetting that custom dropdowns aren't native <select> elements. select_option only works on real <select> tags. If the dropdown is a styled <div role="combobox"> — common in Material UI, Headless UI, Radix — select_option raises Element is not a <select>. Click to open, then click the option as a normal element.

🎯 Practice task

Build a registration-form interaction test on your own demo or a public form. 25-30 minutes.

  1. Create tests/test_form_actions.py against Sauce Demo's checkout flow (login + add to cart + checkout):

    from playwright.sync_api import Page, expect
     
    class TestCheckoutForm:
        def test_complete_checkout(self, page: Page):
            # Log in and add a product
            page.goto("/")
            page.get_by_placeholder("Username").fill("standard_user")
            page.get_by_placeholder("Password").fill("secret_sauce")
            page.get_by_role("button", name="Login").click()
     
            page.locator(".inventory_item").filter(has_text="Backpack") \
                .get_by_role("button", name="Add to cart").click()
            expect(page.locator(".shopping_cart_badge")).to_have_text("1")
     
            # Open the cart and start checkout
            page.locator(".shopping_cart_link").click()
            page.get_by_role("button", name="Checkout").click()
     
            # Fill the checkout form
            page.get_by_placeholder("First Name").fill("Alice")
            page.get_by_placeholder("Last Name").fill("Smith")
            page.get_by_placeholder("Zip/Postal Code").fill("SW1A 1AA")
            page.get_by_role("button", name="Continue").click()
     
            # Confirm the order summary and finish
            expect(page.get_by_text("Backpack")).to_be_visible()
            page.get_by_role("button", name="Finish").click()
            expect(page.get_by_text("Thank you for your order")).to_be_visible()
  2. Run it with pytest tests/test_form_actions.py -v --headed. Watch the form fill itself in Chromium.

  3. Demonstrate auto-wait. Add a deliberate slowdown — open Chrome devtools when the test pauses (use page.pause()), throttle the network to "Slow 3G", resume. The test still passes — fill and click waited for each element to be ready. No time.sleep needed.

  4. Force the wrong action. Replace the .check() on the agree-to-terms checkbox in your registration form (if you have one) with .click(). Run the test twice in a row without resetting state. The second run unchecks it — that's the click-toggle bug. Switch back to .check() and the test is idempotent again.

  5. Stretch: add an in-memory file upload test. Find a form on your demo app that takes a file and use the buffer-dict form: page.get_by_label("Upload").set_input_files({"name": "test.csv", "mimeType": "text/csv", "buffer": b"..."} ). Confirm the upload succeeds without ever touching the disk. This pattern is gold for CI runners that don't have your dev-machine fixture files.

You've got the action vocabulary. Next lesson is the assertion side of the equation — expect, web-first vs non-retrying assertions, soft assertions, and the snake_case differences that matter.

// tip to track lessons you complete and pick up where you left off across devices.