pytest-playwright-softassert
A complete soft-assertion layer over Playwright's native expect() API for Python.
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
Links
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)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| pytest_playwright_softassert-1.0.1.tar.gz | 18.1 kB | Without provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| 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.