Skip to content

Playwright (Python)

Overview

The playwright-python plugin generates Python code using the Playwright library for browser automation and testing.

Within the Browser (plugin: playwright-python, type: browser)
go to "https://example.com"

Types

browser

The main browser context. Launches a Chromium browser and creates a new page.

Type Modifiers:

ModifierTypeDescription
urlquotedBase URL for the browser context (use go to "/" to navigate)
headlessquotedRun browser headlessly ('true') or with visible UI ('false')

Patterns:

PatternDescription
go to <url>Navigate to URL
see url <url>Assert current URL matches exactly
see url contains <url>Assert current URL contains string
click <target>Click element by selector
type <text> into <target>Type text into input element
press <key>Press a keyboard key (e.g., Enter, Tab, Escape)
see a/an/any/no <target>Assert element visibility or count
wait for <target>Wait for element to appear
expect failureExpect code block to raise an exception

Pattern Modifiers:

PatternModifierTypeDescription
go totimeoutquotedNavigation timeout
see urltimeoutquotedAssertion timeout
see url containstimeoutquotedAssertion timeout
clicktimeoutquotedClick timeout
clickrolequotedARIA role (uses get_by_role)
clicknamequotedAccessible name (combines with role or target)
clickidquotedElement ID selector
clickclassquotedCSS class(es), space-separated (e.g., 'btn primary' -> .btn.primary)
clicktestidquoteddata-testid attribute
clicktextquotedText content filter
clicktypequotedHTML type attribute (e.g., 'checkbox', 'submit')
typetimeoutquotedType timeout
typerolequotedARIA role (uses get_by_role)
typenamequotedAccessible name
typeidquotedElement ID selector
typeclassquotedCSS class
typetestidquoteddata-testid attribute
presstimeoutquotedKey press timeout
seetextquotedText content to match (substring)
seeexactquotedSet to 'true' for exact text match
seerolequotedARIA role
seenamequotedAccessible name
seeidquotedElement ID selector
seeclassquotedCSS class(es), space-separated (e.g., 'btn primary' -> .btn.primary)
seetestidquoteddata-testid attribute
seestatequotedAssertion state: attached or hidden (see below)
wait fortimeoutquotedWait timeout
wait forstatequotedWait state (visible, hidden, attached, detached)
wait foridquotedElement ID selector
wait forclassquotedCSS class(es), space-separated
wait fortestidquoteddata-testid attribute
wait fortextquotedText content to match
wait forrolequotedARIA role
wait fornamequotedAccessible name (combines with role)
expect failureerrorquotedExpected exception type
expect failurecapturequotedVariable name to capture exception

element

A scoped element context for targeting nested elements. Inherits patterns from browser type.

Type Modifiers:

ModifierTypeDescription
idquotedSelect by element ID (#id)
classquotedCSS class(es), space-separated (e.g., 'btn primary' -> .btn.primary)
testidquotedSelect by data-testid attribute
labelquotedSelect by accessible label
rolequotedSelect by ARIA role (uses get_by_role() for implicit role matching)
namequotedAccessible name for role-based selection (combines with role to use get_by_role)
Within the Login Form (type: element, testid: 'login-form')
Define email-input (label: 'Email')
type "user@example.com" into email-input

Patterns:

The element type supports the same patterns as browser: click, type, press, see, wait for, and expect failure.

failure_block

A passthrough block used inside expect failure to contain actions that should raise an exception.

Modifier Composition

When multiple modifiers are combined on an action, the generated selector combines them appropriately. Understanding how modifiers compose helps write precise selectors.

Selector Priority

Modifiers are evaluated in this order of priority:

  1. Role-based selectors (role + name) - uses get_by_role()
  2. ID selectors (id) - uses #id CSS selector
  3. Test ID selectors (testid) - uses [data-testid='...']
  4. Class + Text (class + text) - uses locator("tag.class", has_text="...")
  5. Class only (class) - uses tag.class CSS selector
  6. Text only (text) - uses locator("tag", has_text="...")
  7. Tag only - uses bare locator("tag")

Combining class and text

When both class: and text: are specified, they are combined into a single locator:

# Generates: locator("li.completed", has_text="Buy milk")
see a li (class: 'completed', text: 'Buy milk')
# Multiple classes (space-separated): generates locator("li.todo-item.completed", has_text="Buy milk")
see a li (class: 'todo-item completed', text: 'Buy milk')
# With exact matching: locator("li.completed").filter(has_text="Buy milk")
see a li (class: 'completed', text: 'Buy milk', exact: 'true')

Combining text with element tag

The element tag is always included in the selector when using text::

# Generates: locator("h1", has_text="Login")
see a h1 (text: 'Login')
# Generates: locator("button", has_text="Submit")
click button (text: 'Submit')

This ensures the selector targets the correct element type, avoiding strict-mode violations when multiple elements contain the same text.

Best practices

  1. Use role + name for interactive elements - Most accessible and reliable:

    click button (role: 'button', name: 'Submit')
  2. Use testid for unique identification - Stable across refactors:

    see a div (testid: 'welcome-message')
  3. Use class + text for list items - When you need both styling and content:

    see a li (class: 'completed', text: 'Buy groceries')
  4. Avoid text: alone on common text - May match multiple elements:

    # Risky: "Login" appears in heading and button
    see a h1 (text: 'Login') # Good: scoped to h1

Pattern Details

see

The see pattern requires a quantifier to specify the expected element count:

QuantifierAssertionGenerated Code
a or anElement is visibleto_be_visible()
anyAt least one element is visibleto_be_visible()
noZero elements existto_have_count(0)

Note: The a/an quantifier uses Playwright’s to_be_visible() assertion, which passes as long as at least one matching element is visible. It does NOT enforce exactly one match. For strict single-element assertions, use a more specific selector.

see a "Submit Button"
see no "Error Message"
see any "Product Card"

Using the state: modifier:

By default, see asserts visibility using to_be_visible(). For empty containers or hidden elements, use the state: modifier:

StateAssertionUse Case
(default)to_be_visible()Element is rendered and has non-zero size
attachedto_be_attached()Element exists in DOM (even if empty/hidden)
hiddento_be_hidden()Element exists but is not visible
# Assert element is visible (default)
see a ul (testid: 'todo-list')
# Assert element exists in DOM (useful for empty containers)
see a ul (testid: 'todo-list', state: 'attached')
# Assert element is hidden
see a div (class: 'loading-overlay', state: 'hidden')

Gotcha: Empty containers are “hidden”

Playwright’s to_be_visible() requires non-zero size. An empty <ul> or <div> with no content will fail visibility assertions. Use state: 'attached' for empty containers:

# This fails on an empty list (no visible items)
see a ul (testid: 'todo-list')
# This works - just checks the element exists
see a ul (testid: 'todo-list', state: 'attached')

Using exact for strict text matching:

By default, text: uses Playwright’s has_text which matches substrings and ancestor elements. This can cause strict-mode violations when multiple elements contain the same text. Use exact: 'true' for precise matching:

# May match parent elements containing this text
see a div (text: 'Error')
# Strict exact match - recommended for error messages
see a div (text: 'Invalid username', exact: 'true')

expect failure

Wrap actions that should raise an exception. Useful for testing error handling.

expect failure (error: "TimeoutError", capture: "exc_info")
click "Nonexistent Element"

Generated code uses pytest.raises context manager.

Example

Vanya test scenario (vanya-lang.org/v0.1)
Script title: "Complete checkout flow"
Within the Browser (plugin: playwright-python, type: browser, headless: "true")
go to "https://shop.example.com"
click "Add to Cart"
click "Checkout"
Within the Shipping Form (type: element, testid: "shipping-form")
type "123 Main St" into "Address"
type "10001" into "ZIP"
click "Place Order"
see a "Order Confirmation"
see no "Error Message"

Generated Code

When using --format pytest-function, vanya generates tests that use the pytest-playwright page fixture. This provides several benefits:

  • Browser reuse: Tests share browser instances for faster execution
  • Trace-on-failure: Automatic trace collection when tests fail
  • Video-on-failure: Optional video recording of failed tests
  • Screenshots: Automatic screenshot capture on assertion failures

The above example generates Python code similar to:

from playwright.sync_api import sync_playwright, expect
import pytest
def test_complete_checkout_flow(page):
# Uses pytest-playwright 'page' fixture - browser lifecycle managed automatically
page.goto("https://shop.example.com")
page.locator("Add to Cart").click()
page.locator("Checkout").click()
# Nested element scope
shipping_form = page.locator("[data-testid='shipping-form']")
shipping_form.locator("Address").fill("123 Main St")
shipping_form.locator("ZIP").fill("10001")
page.locator("Place Order").click()
expect(page.locator("Order Confirmation")).to_be_visible()
expect(page.locator("Error Message")).to_have_count(0)

To enable trace collection in CI, add to your pytest.ini or pyproject.toml:

pytest.ini
[pytest]
addopts = --tracing=retain-on-failure

More Examples

Selecting by different attributes

Using Within blocks for scoping:

# By ID
Within the Header (type: element, id: 'main-header')
click logout-button
# By CSS class
Within the Card (type: element, class: 'product-card')
see a price-label
# By data-testid
Within the Form (type: element, testid: 'login-form')
type "user@test.com" into email-input
# By accessible label
Within the Dialog (type: element, label: 'Confirmation')
click submit-button
# By ARIA role and name
Within the Navigation (type: element, role: 'navigation')
click home-link

Using inline modifiers on actions:

Modifiers can be placed directly on action targets instead of using Define:

Within the Browser (plugin: playwright-python, type: browser)
# Click by ARIA role + accessible name (recommended for buttons)
click button (role: 'button', name: 'Submit')
# Click by role only (matches implicit ARIA roles)
click input (role: 'checkbox')
# Click by HTML type attribute (e.g., input type="checkbox")
click input (type: 'checkbox')
# Type into element by testid
type "user@example.com" into input (testid: 'email-field')
# See element with tag + class combined
see a li (class: 'completed') # generates: li.completed
# Exact text matching to avoid ancestor matches
see a div (text: 'Error: Invalid input', exact: 'true')

Using Define for reusable selectors

Within the Browser (plugin: playwright-python, type: browser)
go to "/checkout"
# Define reusable selectors
Define email-field (testid: 'email-input')
Define submit-btn (testid: 'checkout-submit')
Define error-msg (class: 'error-message')
# Use them in actions
type "customer@example.com" into email-field
click submit-btn
see no error-msg

Asserting text content

Within the Browser (plugin: playwright-python, type: browser)
go to "/dashboard"
# Assert element exists with specific text
see a h1 (text: 'Welcome back')
see a div (text: 'No notifications')
# Assert element does NOT exist
see no alert (text: 'Error')

Waiting for elements

Within the Browser (plugin: playwright-python, type: browser)
go to "/async-page"
# Wait for element with default timeout
wait for loading-spinner
# Wait with custom timeout (ms)
wait for data-table (timeout: '10000')
# Wait for specific state
wait for modal (state: 'hidden')
# Wait with selector modifiers
wait for li (class: 'completed')
wait for button (role: 'button', name: 'Submit')
wait for div (testid: 'results-panel')

Pressing keyboard keys

Within the Browser (plugin: playwright-python, type: browser)
go to "/search"
# Type into search box and press Enter to submit
type "search query" into search-input
press Enter
# Other common keys: Tab, Escape, ArrowDown, Space
press Tab
press Escape

Testing error conditions

Within the Browser (plugin: playwright-python, type: browser)
go to "/form"
expect failure (error: 'TimeoutError')
click nonexistent-button (timeout: '1000')