Skip to main content

pytest-playwright-softassert

A complete soft-assertion layer over Playwright's native expect() API for Python.

PyPI version Python License: MIT


What is a soft assertion?

A hard assertion (Playwright's default expect()) stops the test immediately on the first failure.

A soft assertion (soft_expect()) collects the failure, lets the test continue running, and reports all failures together at the end.

Hard assertion failure  →  test stops immediately
Soft assertion failure  →  failure collected, test continues, all failures reported at end

This is useful when you want to check multiple things on a page in one test run without stopping at the first problem.


Installation

pip install pytest-playwright-softassert

Requires:

  • Python >= 3.8
  • playwright >= 1.40
  • pytest >= 7.0

Quick start

from pytest_playwright_softassert import soft_expect

def test_dashboard(page):
    page.goto("/dashboard")

    # All three run even if one fails
    soft_expect(page.get_by_role("heading")).to_be_visible()
    soft_expect(page.get_by_role("heading")).to_have_text("Dashboard")
    soft_expect(page).to_have_url("/dashboard")

    # If any of the above failed, the test is marked FAILED here

How it works

soft_expect(target, message) is a direct, thin wrapper around Playwright's own expect.soft(target, message). It returns Playwright's native LocatorAssertions, PageAssertions, or APIResponseAssertions object unchanged.

This means:

  • Every Playwright assertion method is available automatically — no wrapper code needed.
  • Every future Playwright assertion method will work automatically.
  • All assertion options (timeout, ignore_case, regex, lists, etc.) pass through unchanged.
  • Playwright's auto-retry, error messages, and call logs are fully preserved.
  • No Playwright assertion logic is reimplemented.
soft_expect(locator)
        ↓
expect.soft(locator)          ← Playwright's native soft assertion
        ↓
LocatorAssertions             ← full native object returned unchanged
        ↓
.to_be_visible()              ← called directly on the native object

API reference

soft_expect(actual, message=None)

Sync soft assertion. Use inside regular def test_ functions.

from pytest_playwright_softassert import soft_expect
Parameter Type Description
actual Locator, Page, or APIResponse The target to assert against
message str (optional) Custom message shown in the failure report

Returns: LocatorAssertions, PageAssertions, or APIResponseAssertions


async_soft_expect(actual, message=None)

Async soft assertion. Use inside async def test_ functions.

from pytest_playwright_softassert import async_soft_expect

Same parameters as soft_expect. Returns the async Playwright assertions object.


Locator assertions

All methods below are available on soft_expect(locator).

Every method supports a timeout keyword argument (milliseconds or timedelta). Playwright auto-retries each assertion until it passes or the timeout is reached.

Visibility & attachment

soft_expect(locator).to_be_visible(*, visible=None, timeout=None)
soft_expect(locator).not_to_be_visible(*, visible=None, timeout=None)

soft_expect(locator).to_be_hidden(*, timeout=None)
soft_expect(locator).not_to_be_hidden(*, timeout=None)

soft_expect(locator).to_be_attached(*, attached=None, timeout=None)
soft_expect(locator).not_to_be_attached(*, attached=None, timeout=None)

soft_expect(locator).to_be_in_viewport(*, ratio=None, timeout=None)
soft_expect(locator).not_to_be_in_viewport(*, ratio=None, timeout=None)

Examples:

soft_expect(page.get_by_role("dialog")).to_be_visible()
soft_expect(page.get_by_text("Error")).not_to_be_visible()
soft_expect(page.locator("#banner")).to_be_in_viewport(ratio=0.5)

State

soft_expect(locator).to_be_enabled(*, enabled=None, timeout=None)
soft_expect(locator).not_to_be_enabled(*, enabled=None, timeout=None)

soft_expect(locator).to_be_disabled(*, timeout=None)
soft_expect(locator).not_to_be_disabled(*, timeout=None)

soft_expect(locator).to_be_editable(*, editable=None, timeout=None)
soft_expect(locator).not_to_be_editable(*, editable=None, timeout=None)

soft_expect(locator).to_be_focused(*, timeout=None)
soft_expect(locator).not_to_be_focused(*, timeout=None)

soft_expect(locator).to_be_empty(*, timeout=None)
soft_expect(locator).not_to_be_empty(*, timeout=None)

soft_expect(locator).to_be_checked(*, checked=None, indeterminate=None, timeout=None)
soft_expect(locator).not_to_be_checked(*, timeout=None)

Examples:

soft_expect(page.get_by_role("button", name="Submit")).to_be_enabled()
soft_expect(page.get_by_label("Username")).to_be_editable()
soft_expect(page.get_by_role("checkbox")).to_be_checked()
soft_expect(page.get_by_role("checkbox")).to_be_checked(indeterminate=True)
soft_expect(page.get_by_role("textbox")).to_be_empty()

Text content

soft_expect(locator).to_have_text(expected, *, use_inner_text=None, timeout=None, ignore_case=None)
soft_expect(locator).not_to_have_text(expected, *, use_inner_text=None, timeout=None, ignore_case=None)

soft_expect(locator).to_contain_text(expected, *, use_inner_text=None, timeout=None, ignore_case=None)
soft_expect(locator).not_to_contain_text(expected, *, use_inner_text=None, timeout=None, ignore_case=None)

expected accepts: str, re.Pattern, list[str], list[re.Pattern]

Examples:

import re

# Exact string
soft_expect(page.locator("h1")).to_have_text("Welcome")

# Case-insensitive
soft_expect(page.locator("h1")).to_have_text("welcome", ignore_case=True)

# Regular expression
soft_expect(page.locator("h1")).to_have_text(re.compile(r"Order\s+#\d+"))

# List — asserts each item in a list of locators
soft_expect(page.locator("li")).to_have_text(["Item 1", "Item 2", "Item 3"])

# Contains (partial match)
soft_expect(page.locator("p")).to_contain_text("successfully")

# With timeout
soft_expect(page.locator(".toast")).to_have_text("Saved!", timeout=10_000)

# Using inner text instead of text content
soft_expect(page.locator("p")).to_have_text("Hello", use_inner_text=True)

Value (inputs)

soft_expect(locator).to_have_value(value, *, timeout=None)
soft_expect(locator).not_to_have_value(value, *, timeout=None)

soft_expect(locator).to_have_values(values, *, timeout=None)
soft_expect(locator).not_to_have_values(values, *, timeout=None)

value accepts: str, re.Pattern values accepts: list[str], list[re.Pattern] — for multi-select elements

Examples:

soft_expect(page.get_by_label("Email")).to_have_value("user@example.com")
soft_expect(page.get_by_label("Email")).to_have_value(re.compile(r".+@.+"))

# Multi-select
soft_expect(page.get_by_role("listbox")).to_have_values(["Option A", "Option B"])

Attributes & CSS

soft_expect(locator).to_have_attribute(name, value=None, *, ignore_case=None, timeout=None)
soft_expect(locator).not_to_have_attribute(name, value=None, *, ignore_case=None, timeout=None)

soft_expect(locator).to_have_css(name, value, *, timeout=None, pseudo=None)
soft_expect(locator).not_to_have_css(name, value, *, timeout=None)

soft_expect(locator).to_have_id(id, *, timeout=None)
soft_expect(locator).not_to_have_id(id, *, timeout=None)

Examples:

soft_expect(page.locator("img")).to_have_attribute("alt", "Company logo")
soft_expect(page.locator("a")).to_have_attribute("href", re.compile(r"https://.*"))
soft_expect(page.locator("input")).not_to_have_attribute("disabled")

soft_expect(page.locator(".btn")).to_have_css("background-color", "rgb(0, 128, 0)")
soft_expect(page.locator("::before")).to_have_css("content", '""', pseudo="before")

soft_expect(page.locator("section")).to_have_id("main-content")

Class

soft_expect(locator).to_have_class(expected, *, timeout=None)
soft_expect(locator).not_to_have_class(expected, *, timeout=None)

soft_expect(locator).to_contain_class(expected, *, timeout=None)
soft_expect(locator).not_to_contain_class(expected, *, timeout=None)

expected accepts: str, re.Pattern, list[str], list[re.Pattern]

Examples:

# Exact class attribute match
soft_expect(page.locator("div")).to_have_class("active highlighted")

# Contains a specific class
soft_expect(page.locator("div")).to_contain_class("active")

# List — one per matched element
soft_expect(page.locator("li")).to_have_class(["item selected", "item", "item"])

Count

soft_expect(locator).to_have_count(count, *, timeout=None)
soft_expect(locator).not_to_have_count(count, *, timeout=None)

Examples:

soft_expect(page.locator("li.product")).to_have_count(5)
soft_expect(page.locator(".error")).to_have_count(0)

JavaScript property

soft_expect(locator).to_have_js_property(name, value, *, timeout=None)
soft_expect(locator).not_to_have_js_property(name, value, *, timeout=None)

Examples:

soft_expect(page.locator("input")).to_have_js_property("checked", True)
soft_expect(page.locator("video")).to_have_js_property("paused", False)

ARIA / Accessibility

soft_expect(locator).to_have_role(role, *, timeout=None)
soft_expect(locator).not_to_have_role(role, *, timeout=None)

soft_expect(locator).to_have_accessible_name(name, *, ignore_case=None, timeout=None)
soft_expect(locator).not_to_have_accessible_name(name, *, ignore_case=None, timeout=None)

soft_expect(locator).to_have_accessible_description(description, *, ignore_case=None, timeout=None)
soft_expect(locator).not_to_have_accessible_description(description, *, ignore_case=None, timeout=None)

soft_expect(locator).to_have_accessible_error_message(error_message, *, ignore_case=None, timeout=None)
soft_expect(locator).not_to_have_accessible_error_message(error_message, *, ignore_case=None, timeout=None)

role is one of the ARIA role literals: "button", "checkbox", "dialog", "heading", "link", "listbox", "menu", "menuitem", "option", "radio", "textbox", "alert", "alertdialog", "application", "article", "banner", "cell", "columnheader", "combobox", "complementary", "contentinfo", "definition", "document", "feed", "figure", "form", "grid", "gridcell", "group", "img", "list", "listitem", "log", "main", "navigation", "note", "progressbar", "radiogroup", "region", "row", "rowgroup", "rowheader", "scrollbar", "search", "searchbox", "separator", "slider", "spinbutton", "status", "switch", "tab", "table", "tablist", "tabpanel", "term", "timer", "toolbar", "tooltip", "tree", "treegrid", "treeitem"

Examples:

soft_expect(page.locator("button#save")).to_have_role("button")
soft_expect(page.locator("button#save")).to_have_accessible_name("Save changes")
soft_expect(page.locator("button#save")).to_have_accessible_description("Saves the current form")
soft_expect(page.locator("input#email")).to_have_accessible_error_message("Invalid email address")

ARIA snapshot

soft_expect(locator).to_match_aria_snapshot(expected, *, timeout=None)
soft_expect(locator).not_to_match_aria_snapshot(expected, *, timeout=None)

expected is a YAML string describing the expected ARIA tree.

Examples:

soft_expect(page.locator("nav")).to_match_aria_snapshot("""
  - navigation:
    - link "Home"
    - link "About"
    - link "Contact"
""")

Page assertions

All methods below are available on soft_expect(page).

soft_expect(page).to_have_url(url_or_reg_exp, *, timeout=None, ignore_case=None)
soft_expect(page).not_to_have_url(url_or_reg_exp, *, timeout=None, ignore_case=None)

soft_expect(page).to_have_title(title_or_reg_exp, *, timeout=None)
soft_expect(page).not_to_have_title(title_or_reg_exp, *, timeout=None)

soft_expect(page).to_match_aria_snapshot(expected, *, timeout=None)
soft_expect(page).not_to_match_aria_snapshot(expected, *, timeout=None)

url_or_reg_exp and title_or_reg_exp accept: str, re.Pattern

Examples:

import re

soft_expect(page).to_have_url("https://example.com/dashboard")
soft_expect(page).to_have_url(re.compile(r"/dashboard$"))
soft_expect(page).to_have_url("/dashboard", ignore_case=True)
soft_expect(page).not_to_have_url(re.compile(r"/error"))

soft_expect(page).to_have_title("Dashboard — MyApp")
soft_expect(page).to_have_title(re.compile(r"Dashboard"))
soft_expect(page).not_to_have_title("404 Not Found")

API response assertions

All methods below are available on soft_expect(response).

soft_expect(response).to_be_ok()
soft_expect(response).not_to_be_ok()

to_be_ok() passes when the HTTP status code is in the 200–299 range.

Examples:

def test_api(page):
    with page.expect_response("**/api/users") as resp_info:
        page.get_by_role("button", name="Load users").click()
    response = resp_info.value

    soft_expect(response).to_be_ok()

Custom messages

Pass a second argument to soft_expect() to add a custom message to the failure report.

soft_expect(
    page.get_by_role("heading"),
    "Page heading must be visible after login"
).to_be_visible()

soft_expect(
    page.get_by_label("Email"),
    "Email field must be pre-filled with the user's address"
).to_have_value("user@example.com")

The custom message appears at the top of the failure output, followed by Playwright's full error details.


Assertion options

timeout

Override the default 5-second assertion timeout per assertion.

# Wait up to 10 seconds for this specific assertion
soft_expect(page.locator(".slow-element")).to_be_visible(timeout=10_000)

# Short timeout for elements that should already be present
soft_expect(page.locator(".cached-data")).to_have_text("Ready", timeout=1_000)

ignore_case

soft_expect(page.locator("h1")).to_have_text("hello world", ignore_case=True)
soft_expect(page).to_have_url("/Dashboard", ignore_case=True)

use_inner_text

soft_expect(page.locator("p")).to_have_text("Hello", use_inner_text=True)

Regular expressions

import re

soft_expect(page.locator("h1")).to_have_text(re.compile(r"Order\s+#\d+"))
soft_expect(page.locator("a")).to_have_attribute("href", re.compile(r"https://.*"))
soft_expect(page).to_have_url(re.compile(r"/products/\d+"))

Lists

# Assert text of each matched element
soft_expect(page.locator("li")).to_have_text(["Apple", "Banana", "Cherry"])

# Assert text contains substring for each element
soft_expect(page.locator("li")).to_contain_text(["App", "Ban", "Che"])

# Mix of strings and regex
soft_expect(page.locator("li")).to_have_text([
    "Apple",
    re.compile(r"Ban\w+"),
    "Cherry",
])

# Multi-select values
soft_expect(page.get_by_role("listbox")).to_have_values(["opt1", "opt2"])

Negative assertions

Every assertion has a not_to_ counterpart.

soft_expect(locator).not_to_be_visible()
soft_expect(locator).not_to_be_enabled()
soft_expect(locator).not_to_be_checked()
soft_expect(locator).not_to_have_text("Error")
soft_expect(locator).not_to_have_attribute("disabled")
soft_expect(locator).not_to_contain_text("Failed")
soft_expect(locator).not_to_have_class("error")
soft_expect(locator).not_to_have_count(0)
soft_expect(page).not_to_have_url(re.compile(r"/error"))
soft_expect(page).not_to_have_title("404")
soft_expect(response).not_to_be_ok()

Async usage

from pytest_playwright_softassert import async_soft_expect

async def test_dashboard(page):
    await page.goto("/dashboard")

    await async_soft_expect(page.get_by_role("heading")).to_be_visible()
    await async_soft_expect(page.get_by_role("heading")).to_have_text("Dashboard")
    await async_soft_expect(page).to_have_url("/dashboard")

All the same methods, options, and patterns described above apply to async_soft_expect — just add await.


Mixing soft and hard assertions

soft_expect and Playwright's native expect can be used together in the same test.

from playwright.sync_api import expect
from pytest_playwright_softassert import soft_expect

def test_checkout(page):
    page.goto("/checkout")

    # Hard assertion — stops the test if the page didn't load
    expect(page).to_have_url("/checkout")

    # Soft assertions — all run even if some fail
    soft_expect(page.get_by_label("Name")).to_be_visible()
    soft_expect(page.get_by_label("Email")).to_be_visible()
    soft_expect(page.get_by_label("Card number")).to_be_visible()
    soft_expect(page.get_by_role("button", name="Pay")).to_be_enabled()

Behaviour reference

Scenario Behaviour
Soft assertion passes Test continues normally
Soft assertion fails Failure collected; test continues
All soft assertions pass Test passes
One or more soft assertions fail Test marked FAILED at the end
Multiple soft assertions fail All failures reported together
Hard expect() fails Test fails immediately (unchanged)
Hard failure + soft failures Both reported together

Complete example

import re
from playwright.sync_api import expect
from pytest_playwright_softassert import soft_expect

def test_product_page(page):
    page.goto("/products/42")

    # Hard assertion — must pass before continuing
    expect(page).to_have_url(re.compile(r"/products/\d+"))

    # Page-level soft assertions
    soft_expect(page).to_have_title(re.compile(r"Product"))

    # Visibility
    soft_expect(page.locator("h1")).to_be_visible()
    soft_expect(page.locator(".price")).to_be_visible()
    soft_expect(page.locator(".out-of-stock-banner")).not_to_be_visible()

    # Text content
    soft_expect(page.locator("h1")).to_have_text(
        re.compile(r"Product \d+")
    )
    soft_expect(page.locator(".price")).to_contain_text("$")

    # Attributes
    soft_expect(page.locator("img.product-image")).to_have_attribute(
        "alt", re.compile(r"Product")
    )

    # State
    soft_expect(page.get_by_role("button", name="Add to cart")).to_be_enabled()

    # ARIA
    soft_expect(page.get_by_role("button", name="Add to cart")).to_have_role("button")
    soft_expect(page.get_by_role("button", name="Add to cart")).to_have_accessible_name(
        "Add to cart"
    )

    # Count
    soft_expect(page.locator(".review")).to_have_count(3)

    # Custom message
    soft_expect(
        page.locator(".rating"),
        "Product rating must be displayed"
    ).to_be_visible()

Running the tests

Compatibility tests (no browser required)

Verifies that soft_expect exposes the same assertion surface as Playwright's native expect(). Run in CI on every push.

pytest tests/test_compatibility.py -v

Integration tests (browser required)

playwright install chromium
pytest tests/test_integration.py --browser chromium -v


License

MIT © Gopinath M

Metadata

Release files for pytest-playwright-softassert 1.0.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for pytest-playwright-softassert 1.0.1
File Size Uploaded Provenance
pytest_playwright_softassert-1.0.1.tar.gz 18.1 kB Without provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for pytest-playwright-softassert 1.0.1
File Interpreter ABI Platform
pytest_playwright_softassert-1.0.1-py3-none-any.whl Python 3 none any Details

Total release size: 27.8 kB

Release files / pytest_playwright_softassert-1.0.1.tar.gz

Download URL pytest_playwright_softassert-1.0.1.tar.gz
Size 18.1 kB
Tags Source
SHA-256 checksum
How to use checksums
2c970686de47dc1710dbd780bfff28c7c645a1fc2fc6f6bd897091462b27ba81
BLAKE2b-256 checksum
How to use checksums
3160651122bf0c4bf762d8a89f8c702be8025758c340db228678fd55a1948e7c
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.14

Release files / pytest_playwright_softassert-1.0.1-py3-none-any.whl

Download URL pytest_playwright_softassert-1.0.1-py3-none-any.whl
Size 9.7 kB
Tags Python 3
SHA-256 checksum
How to use checksums
3e1a62e410c4ed9ea739ecac35950e00222ec4364e7001ea1db800935148e66d
BLAKE2b-256 checksum
How to use checksums
52a8cece55f969de09e6e3b4b0b74ae87da217c0e038221b647a89dd421ed4bd
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.14

Security for pytest-playwright-softassert 1.0.1

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

1.0.1 This release

2 release files No provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page