PRACTICAL GUIDE / playwright mouse and keyboard actions

Mouse, Keyboard, and Drag-and-Drop Workflows in Playwright

Model reliable Playwright mouse, keyboard, and drag-and-drop workflows with semantic actions, deliberate event sequences, and stable coordinates.

By The Testing AcademyUpdated July 11, 20269 min read
All field guides
In this guide9 sections
  1. Choose the Interaction Contract First
  2. Model Keyboard Workflows Around Focus
  3. Test Shortcuts Across Operating Systems
  4. Use Mouse Coordinates in a Stable Reference Frame
  5. Start Drag-and-Drop With dragTo
  6. Account for Touch and Mobile Input
  7. Diagnose Event Sequence Failures
  8. Operational Checklist for Complex Input
  9. Action Plan

What you will learn

  • Choose the Interaction Contract First
  • Model Keyboard Workflows Around Focus
  • Test Shortcuts Across Operating Systems
  • Use Mouse Coordinates in a Stable Reference Frame

Input automation becomes fragile when a test reproduces hand movement without stating user intent. Ten mouse coordinates and four key events may happen to complete a workflow, yet nobody reviewing the test can tell which control should own focus, which drag threshold matters, or what state proves success.

Playwright offers two useful levels. Locator actions express interaction with a named control and include readiness checks. Device APIs such as page.keyboard and page.mouse expose event sequences and coordinates. Choose the highest level that still exercises the behavior under test, then assert the application state produced by the sequence.

Choose the Interaction Contract First

For text fields, buttons, checkboxes, and native select controls, begin with fill, click, check, and other locator actions. They communicate the target and benefit from Playwright's actionability model. Drop to keyboard or pointer primitives when the event sequence itself matters: an editor shortcut, a press-and-hold command, a drawing canvas, a range selection, or a custom drag surface.

The official Playwright input guide distinguishes normal text entry, character-by-character typing, key presses, mouse actions, and drag operations. That separation should appear in test design too. A search test normally cares that a query value is entered, while an autocomplete test may care that each input event updates suggestions.

Animated field map

From Interaction Intent to Verified State

Select a target and input level based on the event contract, then assert what the application changed.

  1. 01 / interaction intent

    Interaction intent

    Name the user behavior and the events that actually matter.

  2. 02 / target locator

    Target locator

    Resolve the semantic control or stable interaction surface.

  3. 03 / device sequence

    Input device sequence

    Send a locator action or deliberate keyboard and pointer steps.

  4. 04 / event handlers

    Application handlers

    Let focus, pointer, keyboard, and drag handlers process input.

  5. 05 / state assertion

    State assertion

    Verify the durable UI or data result of the interaction.

A low-level test is not automatically more realistic. It can bypass locator checks, hard-code geometry, and expose browser details that the product does not promise. Use low-level control deliberately and keep the observable business result at the center of the test.

Model Keyboard Workflows Around Focus

Keyboard events go to the currently focused element. Before pressing a shortcut, prove where focus is or focus a semantic locator through a user-visible step. A test that calls page.keyboard.press('Enter') without establishing focus depends on whatever previous action happened to leave active.

TypeScript
import { expect, test } from '@playwright/test'

test('keyboard user creates a command palette item', async ({ page }) => {
  await page.goto('/workspace')

  await page.getByRole('button', { name: 'Open command palette' }).focus()
  await page.keyboard.press('Enter')

  const palette = page.getByRole('dialog', { name: 'Command palette' })
  await expect(palette).toBeVisible()

  const search = palette.getByRole('combobox', { name: 'Commands' })
  await expect(search).toBeFocused()
  await search.fill('Create issue')
  await search.press('ArrowDown')
  await search.press('Enter')

  await expect(page.getByRole('heading', { name: 'New issue' })).toBeVisible()
})

Use fill() for setting an input value efficiently. Use pressSequentially() only when the application behavior depends on individual key events, such as debounce logic or an input mask. For a shortcut, locator.press() is often clearer because it couples the keystroke to its intended recipient.

Press-and-hold workflows require balanced down and up calls. Wrap release in finally so a failed assertion does not leave the modifier pressed for later steps.

TypeScript
await page.keyboard.down('Shift')
try {
  await page.getByRole('option', { name: 'Report Q2' }).click()
  await page.getByRole('option', { name: 'Report Q4' }).click()
} finally {
  await page.keyboard.up('Shift')
}

Test Shortcuts Across Operating Systems

Product shortcuts may map to Meta on macOS and Control elsewhere. Keep the mapping in one helper or project option rather than scattering platform branches through tests. More importantly, verify the resulting command and ensure the shortcut does not conflict with a browser-reserved action in the environments you support.

TypeScript
function primaryModifier(): 'Meta' | 'Control' {
  return process.platform === 'darwin' ? 'Meta' : 'Control'
}

const editor = page.getByRole('textbox', { name: 'Issue description' })
await editor.fill('Release blocker')
await editor.press(`${primaryModifier()}+a`)
await editor.press(`${primaryModifier()}+b`)

await expect(page.getByRole('button', { name: 'Bold' })).toHaveAttribute(
  'aria-pressed',
  'true',
)

The assertion checks editor state instead of merely recording that keyboard events occurred. For accessibility coverage, also test Tab and Shift+Tab order, Escape behavior, focus restoration after dialogs, and visible focus styling. Shortcuts supplement navigable controls; they should not be the only way to perform an essential command.

Use Mouse Coordinates in a Stable Reference Frame

page.mouse operates in main-frame CSS pixels relative to the viewport. Hard-coded page coordinates are therefore vulnerable to responsive layout and scrolling. Calculate points from the current bounding box of a stable surface, then express movement relative to that box.

TypeScript
const canvas = page.getByTestId('floor-plan-canvas')
await expect(canvas).toBeVisible()

const box = await canvas.boundingBox()
if (!box) throw new Error('Canvas has no visible bounding box')

const start = { x: box.x + box.width * 0.25, y: box.y + box.height * 0.5 }
const end = { x: box.x + box.width * 0.75, y: box.y + box.height * 0.5 }

await page.mouse.move(start.x, start.y)
await page.mouse.down()
await page.mouse.move(end.x, end.y, { steps: 12 })
await page.mouse.up()

await expect(page.getByTestId('selection-width')).toHaveText('50%')

Fix the project viewport for coordinate-sensitive scenarios and avoid relying on the machine's desktop size. If browser zoom or transforms are part of the product, test them explicitly because the relationship between visual geometry and application coordinates becomes part of the contract.

Start Drag-and-Drop With dragTo

For a normal draggable element and drop target, locator.dragTo() provides the clearest intent. Scope source and destination semantically, perform the drag, and assert an order or state change that persists after the gesture.

TypeScript
const source = page
  .getByRole('listitem')
  .filter({ has: page.getByText('Investigate login failure', { exact: true }) })
const destination = page.getByRole('region', { name: 'In progress' })

await source.dragTo(destination)

await expect(
  destination.getByText('Investigate login failure', { exact: true }),
).toBeVisible()
await expect(page.getByRole('status')).toHaveText('Issue moved')

Custom components sometimes require movement beyond a threshold, an intermediate hover over a lane, or a particular handle. Then use source and target bounding boxes with page.mouse, but retain semantic locators to discover those boxes. Avoid copying coordinates from a recording because they describe one layout, not the interaction rule.

Some applications implement HTML drag events with a data payload, while others use pointer events. Do not immediately inject synthetic dragstart and drop events. That may bypass the pointer path and actionability the browser scenario is meant to cover. Establish which event contract the component implements before selecting the fallback.

Hover-driven menus deserve the same state-machine treatment. Hover the named trigger, assert that the menu is visible, move to a menu item, and verify the selected result. If the product intentionally uses an open delay or close corridor, assert the visible state instead of sleeping for the animation duration. Keep the pointer inside the documented trigger and menu geometry; moving directly by coordinates can accidentally cross another element and close the menu. Retain keyboard coverage for the same command because a feature available only through hover is inaccessible to touch and keyboard users.

Double-click behavior should be isolated from single-click behavior. locator.dblclick() sends the expected sequence, but the test must prove the product did not execute an unintended single-click action twice before handling the double click. Seed a state where the two outcomes differ, then assert the final command and any counter or selection state that would expose duplicate processing.

Account for Touch and Mobile Input

A mobile viewport does not automatically turn mouse input into touch input. A project must use a touch-capable context when the scenario relies on touchscreen behavior, and page.touchscreen.tap() represents a tap rather than a full multi-touch gesture system. Keep swipe, pinch, and platform gesture expectations aligned with what the browser automation surface can actually produce.

For responsive controls that respond identically to pointer input, test the business workflow through locator actions in device projects. Add a smaller set of touch-specific cases only where handlers or interaction rules differ. This avoids duplicating every scenario while still covering the risk introduced by touch behavior.

Diagnose Event Sequence Failures

When an interaction fails, determine whether targeting, focus, geometry, event order, or application state is responsible.

SymptomLikely issueEvidence to collect
Key press affects the page, not the fieldFocus is elsewhereActive element, focus trace, and focused assertion
Shortcut works locally onlyPlatform modifier differsWorker OS, project name, and emitted key combination
Drag starts but never dropsThreshold or target path is wrongSource/target boxes and pointer event log
Pointer lands outside the surfaceScroll or responsive layout changed geometryScreenshot, viewport, scroll position, and bounding box
UI changes but data revertsOptimistic update failedNetwork response and persisted state assertion
Click works, keyboard failsControl lacks keyboard semanticsRole, focusability, and key handler behavior

Temporary in-page event logging can reveal order, but remove broad listeners after diagnosis because they add noise and can affect timing. The trace should remain the default artifact because it connects actions, DOM snapshots, and application output.

Operational Checklist for Complex Input

  • Start with a locator action and move lower only when event detail is relevant.
  • Establish and assert focus before sending keyboard input.
  • Balance every modifier or mouse down with an up, even on failure.
  • Map platform shortcuts centrally and verify the command result.
  • Derive coordinates from current bounding boxes inside a fixed viewport.
  • Prefer dragTo() before implementing a manual pointer path.
  • Assert persisted state, not merely animation or a transient toast.
  • Cover keyboard reachability and focus restoration for essential actions.
  • Capture geometry, focus, project, and trace data for CI-only failures.

Action Plan

Choose one complex workflow and label every step as target discovery, input sequence, or result assertion. Replace coordinates with semantic locator actions wherever event geometry is irrelevant. For the remaining low-level steps, calculate positions from current boxes, make focus and modifier state explicit, and add an assertion against durable application state. That structure produces tests that still read like user behavior while retaining the precision needed for editors, canvases, shortcuts, and drag surfaces.

// LIVE COURSE / THE TESTING ACADEMY

Playwright Automation Mastery

Go beyond Selenium. Master Playwright with JS/TS in 90 days.

From the instructor behind this guide.

Playwright jobs are growing 8x faster than Selenium. 90 days / 75+ live hrs / Tue-Thu-Sat 7 AM IST.

Code PROMODE / 10% offJoin the batch

The Testing Academy editorial desk

Practical QA guidance built around test evidence, production tradeoffs, and interview-ready explanations.

Published July 11, 2026 / Reviewed July 11, 2026

PRIMARY REFERENCES

Verify the details at the source

QABattle guides are practical explanations. Product behavior, standards, and APIs can change, so use these primary references for the canonical details.

  1. 01
    Playwright documentation

    Microsoft

    Canonical API, locator, fixture, browser, and test-runner behavior.

  2. 02
    Playwright best practices

    Microsoft

    Official guidance for resilient tests, isolation, and user-facing locators.

  3. 03
    WebDriver standard

    W3C

    The browser automation protocol specification used by major automation stacks.

FAQ / QUICK ANSWERS

Questions testers ask

Should Playwright tests use locator actions or page.mouse?

Prefer locator actions for ordinary controls because they include targeting and actionability checks. Use page.mouse when coordinates, paths, or low-level pointer sequencing are the behavior being tested, such as a canvas editor.

What is the difference between press() and pressSequentially()?

press() sends a key or shortcut such as Enter or Control+A, while pressSequentially() emits key events for each character in order. Use fill() for normal text entry and reserve sequential typing for applications that react to each keystroke.

How do I drag an element with Playwright?

Start with locator.dragTo() and assert the resulting state. If the application depends on custom pointer thresholds or intermediate moves, use bounding boxes with page.mouse and make the sequence explicit.

Why does drag-and-drop pass locally but fail in CI?

Responsive layout, zoom, scrolling, animation, and different target geometry can invalidate hard-coded coordinates. Calculate points from current bounding boxes and use a fixed project viewport for coordinate-sensitive coverage.

Can Playwright test keyboard accessibility with shortcuts?

Yes, but test focus movement and resulting state, not only whether a key event fired. A keyboard workflow should prove that controls are reachable, focus remains visible, and the shortcut does not trap or misdirect the user.