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.
📖 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):
| Check | Means |
|---|---|
| Attached | The element is in the page |
| Visible | It has a size and is not hidden |
| Stable | It is not moving (for example, not animating) |
| Receives events | No other element (such as an overlay) covers it |
| Enabled | It is not disabled |
| Editable | For 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 aninputevent. 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 forkeyupevents.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 includeEnter,Tab,Escape,Backspace,Delete,ArrowDown,PageDown,Home,F1toF12, and single characters such asa.- Combine keys with
+:Control+A,Shift+Tab.ControlOrMeta+Auses 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=andtimeout=.dblclick()is a double click.- Clicking the “Go to Bottom” link (
href="#bottom") changes the URL fragment, whichto_have_urlverifies.
☑️ 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 withcheck()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. Uselabel=orindex=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:hoverstyles and menus that open on hover. The practice button only shows a browser tooltip from itstitleattribute, 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 locatortarget.locator("#drag1")finds it there.- Actions scroll automatically;
scroll_into_view_if_needed()is for when you only want to bring something into view, andto_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.
fillreplaces text;press_sequentiallytypes key by key;presshandles keys and shortcuts.check,uncheck,set_checkedandselect_option(value, label, index, multiple) cover form controls; date inputs take ISO-style values.hover,drag_to,focusand 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”