Playwright Python Actions: click, fill, press, check, select_option, hover and drag_to

Locators find elements; actions use them. Playwright’s actions behave like a real user: they scroll the element into view, wait until it is ready, and then click, type or drag. In this lesson you will perform every common action on the SQA Tools practice page, learn the difference between fill and typing key by key, handle dropdowns, date fields, hover and drag and drop, and understand the actionability checks that make actions reliable.

Advertisement

📖 What You’ll Learn: click, double click, right click and modifier keys, fill, clear and press_sequentially, keyboard keys and shortcuts with press, check, uncheck and set_checked, select_option by value, label, index and multiple values, date and time inputs, hover, drag_to, focus and scrolling, the page.mouse and page.keyboard APIs, actionability checks, force and trial, and reading the “element is not enabled” error.


⚙️ What Happens When You Call an Action

Before any action, Playwright performs actionability checks and waits until they pass (up to the action timeout, 30 seconds by default):

CheckMeans
AttachedThe element is in the page
VisibleIt has a size and is not hidden
StableIt is not moving (for example, not animating)
Receives eventsNo other element (such as an overlay) covers it
EnabledIt is not disabled
EditableFor fill: it is not read-only

Then it scrolls the element into view and performs the action with real mouse and keyboard events. This is why Playwright tests rarely need waits.

Every example below uses this fixture to open the practice page:

📄 tests/conftest.py

import pytest
from playwright.sync_api import Page


@pytest.fixture
def practice(page: Page) -> Page:
    page.goto("https://sqatools.in/automation-practice-page/")
    return page

⌨️ Typing: fill, clear and press_sequentially

📄 tests/test_typing.py

from playwright.sync_api import Page, expect


def test_fill_and_clear(practice: Page):
    username = practice.get_by_placeholder("Enter username")

    username.fill("qa_learner")                 # replaces any existing text
    expect(username).to_have_value("qa_learner")

    username.fill("second_value")               # fill always replaces, never appends
    expect(username).to_have_value("second_value")

    username.clear()                            # empty the field
    expect(username).to_have_value("")

    # Multi-line text in a <textarea>
    address = practice.locator("#address")
    address.fill("Line 1\nLine 2")
    expect(address).to_have_value("Line 1\nLine 2")


def test_type_key_by_key(practice: Page):
    search = practice.get_by_placeholder("Search...")

    # The page shows suggestions on every key press (onkeyup), so fill() is not enough
    search.press_sequentially("pla", delay=100)
    expect(practice.locator("#suggestions div")).to_have_text(["Playwright"])

    practice.locator("#suggestions div").first.click()
    expect(search).to_have_value("Playwright")
  • fill(text) sets the value in one step and fires an input event. It is fast and right for most fields.
  • press_sequentially(text, delay=100) types one character at a time, firing key events for each, with an optional delay in milliseconds. Use it when the page reacts to individual key presses, such as this autocomplete box, which only listens for keyup events.
  • to_have_text([...]) with a list checks the texts of all matched elements, in order: exactly one suggestion, “Playwright”.

🎹 Keyboard Keys and Shortcuts: press

📄 tests/test_typing.py

def test_press_keys(practice: Page):
    key_box = practice.get_by_placeholder("Press any key")
    key_box.press("Enter")
    expect(practice.locator("#keyOutput")).to_have_text("Key Pressed: Enter")

    username = practice.get_by_placeholder("Enter username")
    username.fill("delete me")
    username.press("Control+A")                 # select all
    username.press("Backspace")                 # delete the selection
    expect(username).to_have_value("")
  • press(key) presses one key or a combination. Key names include Enter, Tab, Escape, Backspace, Delete, ArrowDown, PageDown, Home, F1 to F12, and single characters such as a.
  • Combine keys with +: Control+A, Shift+Tab. ControlOrMeta+A uses Control on Windows and Linux and Command on macOS, which is handy for cross-platform shortcuts.

🖱️ Clicking

📄 tests/test_clicks.py

from playwright.sync_api import Page, expect


def test_click_variants(practice: Page):
    button = practice.get_by_role("button", name="Normal Button")

    button.click()                              # left click
    button.dblclick()                           # double click
    button.click(button="right")                # right click (context menu)
    button.click(modifiers=["Shift"])           # Shift + click
    button.click(position={"x": 5, "y": 5})     # click at an offset inside the element
    expect(button).to_be_visible()


def test_link_inside_page(practice: Page):
    practice.get_by_role("link", name="Go to Bottom").click()
    expect(practice).to_have_url("https://sqatools.in/automation-practice-page/#bottom")
  • click() takes options: button= (“left”, “right”, “middle”), click_count=, modifiers= ([“Shift”], [“Control”], [“Alt”], [“Meta”]), position= and timeout=.
  • dblclick() is a double click.
  • Clicking the “Go to Bottom” link (href="#bottom") changes the URL fragment, which to_have_url verifies.

☑️ Checkboxes, Radios and Dropdowns

📄 tests/test_forms.py

from playwright.sync_api import Page, expect


def test_checkboxes_and_radios(practice: Page):
    java = practice.get_by_role("checkbox", name="Java")
    java.check()
    expect(java).to_be_checked()
    java.uncheck()
    expect(java).not_to_be_checked()
    java.set_checked(True)                      # handy when the state comes from test data
    expect(java).to_be_checked()

    practice.get_by_role("radio", name="Other").check()
    expect(practice.get_by_role("radio", name="Other")).to_be_checked()


def test_dropdowns(practice: Page):
    country = practice.locator("#country")

    print("\nby value:", country.select_option("usa"))
    expect(country).to_have_value("usa")

    print("by label:", country.select_option(label="UK"))
    print("by index:", country.select_option(index=1))
    expect(country).to_have_value("india")

    skills = practice.locator("#skills")         # <select multiple>
    print("multiple:", skills.select_option(["Python", "Playwright"]))
    expect(skills).to_have_values(["Python", "Playwright"])
  • check() / uncheck() set the state and verify it changed; clicking an already checked checkbox with check() does nothing, which keeps tests predictable.
  • set_checked(True/False) picks the state from a value.
  • select_option(value) matches an option by its value or its label. Use label= or index= to be explicit. It returns the list of selected values, which the test prints.
  • For a <select multiple>, pass a list; to_have_values([...]) checks all selected values.

🖥️ Output

by value: ['usa']
by label: ['uk']
by index: ['india']
multiple: ['Python', 'Playwright']

📅 Date and Time Inputs

Native date and time pickers are just inputs with a fixed value format, so use fill() with that format instead of clicking through a calendar:

📄 tests/test_forms.py

def test_date_and_time(practice: Page):
    practice.locator("#datePicker").fill("2026-11-01")              # YYYY-MM-DD
    practice.locator("#timePicker").fill("13:45")                   # HH:MM (24-hour)
    practice.locator("#dateTimePicker").fill("2026-11-01T09:30")    # YYYY-MM-DDTHH:MM

    expect(practice.locator("#datePicker")).to_have_value("2026-11-01")
    expect(practice.locator("#timePicker")).to_have_value("13:45")
    expect(practice.locator("#dateTimePicker")).to_have_value("2026-11-01T09:30")

The browser displays the date in the user’s local format, but the input’s value always uses this ISO-style format. Custom JavaScript date pickers (not native inputs) are different: you click their buttons like any other element.


🖐️ Hover, Drag and Drop, Focus and Scroll

📄 tests/test_mouse.py

from playwright.sync_api import Page, expect


def test_hover(practice: Page):
    button = practice.get_by_role("button", name="Hover Over Me")
    button.hover()
    expect(button).to_have_attribute("title", "Hover text")   # the tooltip text


def test_drag_and_drop(practice: Page):
    source = practice.locator("#drag1")
    target = practice.locator(".drop")
    source.drag_to(target)
    expect(target.locator("#drag1")).to_be_visible()          # the box is now inside the target


def test_focus_and_scroll(practice: Page):
    bottom = practice.get_by_role("heading", name="Bottom of Page")
    bottom.scroll_into_view_if_needed()
    expect(bottom).to_be_in_viewport()

    username = practice.get_by_placeholder("Enter username")
    username.focus()
    expect(username).to_be_focused()
  • hover() moves the mouse over the element, which triggers CSS :hover styles and menus that open on hover. The practice button only shows a browser tooltip from its title attribute, so the test checks that attribute.
  • drag_to(target) presses the mouse on the source, moves it to the target and releases it. After the drop, the page moved the box into the drop zone, so the chained locator target.locator("#drag1") finds it there.
  • Actions scroll automatically; scroll_into_view_if_needed() is for when you only want to bring something into view, and to_be_in_viewport() checks that it is visible on screen.
  • focus() puts the keyboard focus on an element; to_be_focused() checks it.

Low-level mouse and keyboard

For drawing, sliders or custom gestures, page.mouse and page.keyboard give raw control: page.mouse.move(x, y), page.mouse.down(), page.mouse.up(), page.mouse.wheel(0, 500), page.keyboard.type("text"), page.keyboard.press("Tab"). Prefer locator actions whenever possible, because they include the actionability checks.


🚫 When an Action Cannot Happen

The practice page has a disabled input. Trying to fill it waits for the “enabled” check and then times out:

📄 tests/test_mouse.py

import pytest
from playwright.sync_api import TimeoutError as PlaywrightTimeoutError


def test_disabled_field(practice: Page):
    disabled = practice.locator("#disabledInput")
    expect(disabled).to_be_disabled()
    with pytest.raises(PlaywrightTimeoutError) as info:
        disabled.fill("x", timeout=2000)
    print("\n" + str(info.value))

🖥️ Output

Locator.fill: Timeout 2000ms exceeded.
Call log:
  - waiting for locator("#disabledInput")
    - locator resolved to <input disabled type="text" value="Disabled" id="disabledInput"/>
    - fill("x")
  - attempting fill action
    2 × waiting for element to be visible, enabled and editable
      - element is not enabled
    - retrying fill action
    - waiting 20ms
    2 × waiting for element to be visible, enabled and editable
      - element is not enabled
    - retrying fill action
      - waiting 100ms
    ...

Playwright kept retrying with growing pauses (20 ms, 100 ms, 500 ms…) until the 2-second timeout ran out. The call log tells you exactly which check failed: element is not enabled. When a real test times out, this is the first place to look: “not visible”, “not stable” or “element intercepts pointer events” (something is covering it) each point to a different cause.

force and trial

  • click(force=True) skips the actionability checks. Use it only when you are sure the check is wrong for your case; it can hide real bugs such as a button covered by a popup.
  • click(trial=True) performs the checks without clicking, which is a way to wait until an element is actionable.

▶️ Run Everything

⌨️ Terminal

pytest -q -s tests

🖥️ Output

12 passed, 1 warning in 28.29s

❓ Frequently Asked Questions

🤔 fill or press_sequentially?

Use fill() by default; it is faster and reliable. Use press_sequentially() only when the page reacts to individual key events, such as autocomplete, input masks or character counters.

🤔 Why did my click “time out” although the button is visible?

Read the call log: usually another element intercepts the click (a cookie banner, a loading overlay) or the button is disabled or still animating. Fix the cause rather than using force=True.

🤔 How do I select an option whose value I do not know?

Use select_option(label="Visible text"), or index= if the order is fixed.

🤔 Is type() still available?

locator.type() is deprecated. Use fill() or, for key-by-key typing, press_sequentially().


🏁 Summary

  • Actions wait for actionability (attached, visible, stable, receives events, enabled, editable) and act like real users.
  • fill replaces text; press_sequentially types key by key; press handles keys and shortcuts.
  • check, uncheck, set_checked and select_option (value, label, index, multiple) cover form controls; date inputs take ISO-style values.
  • hover, drag_to, focus and scrolling cover the rest; the call log explains any action that cannot happen.

Next, you will handle JavaScript dialogs, iframes, popups and new tabs.

🎬 Watch the Playwright tutorial series on YouTube!

Subscribe to SqaToolsByDY • Practice on the SQA Tools Automation Practice Page

1 thought on “Playwright Python Actions: click, fill, press, check, select_option, hover and drag_to”

Leave a Comment