Metadata-Version: 2.4
Name: pytest-playwright-softassert
Version: 1.0.1
Summary: Complete soft-assertion layer over Playwright's native expect() API
Author-email: Gopinath M <Gopinath.matheswaran1995@gmail.com>
License-Expression: MIT
Project-URL: Homepage, https://github.com/gopinathsasi/PlaywrightSoftAssertion
Project-URL: Repository, https://github.com/gopinathsasi/PlaywrightSoftAssertion
Project-URL: Issues, https://github.com/gopinathsasi/PlaywrightSoftAssertion/issues
Project-URL: Changelog, https://github.com/gopinathsasi/PlaywrightSoftAssertion/blob/main/CHANGELOG.md
Keywords: playwright,pytest,soft-assert,testing,automation
Classifier: Development Status :: 5 - Production/Stable
Classifier: Framework :: Pytest
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Testing
Classifier: Topic :: Software Development :: Quality Assurance
Requires-Python: >=3.8
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: playwright>=1.40
Requires-Dist: pytest>=7.0
Provides-Extra: dev
Requires-Dist: build; extra == "dev"
Requires-Dist: twine; extra == "dev"
Dynamic: license-file

# pytest-playwright-softassert

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

[![PyPI version](https://img.shields.io/pypi/v/pytest-playwright-softassert)](https://pypi.org/project/pytest-playwright-softassert/)
[![Python](https://img.shields.io/pypi/pyversions/pytest-playwright-softassert)](https://pypi.org/project/pytest-playwright-softassert/)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/gopinathsasi/PlaywrightSoftAssertion/blob/main/LICENSE)

---

## 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

```bash
pip install pytest-playwright-softassert
```

Requires:
- Python >= 3.8
- playwright >= 1.40
- pytest >= 7.0

---

## Quick start

```python
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.

```python
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.

```python
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

```python
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:

```python
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

```python
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:

```python
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

```python
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:

```python
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)

```python
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:

```python
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

```python
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:

```python
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

```python
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:

```python
# 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

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

Examples:

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

---

### JavaScript property

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

Examples:

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

---

### ARIA / Accessibility

```python
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:

```python
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

```python
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:

```python
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)`.

```python
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:

```python
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)`.

```python
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:

```python
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.

```python
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.

```python
# 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`

```python
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`

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

### Regular expressions

```python
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

```python
# 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.

```python
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

```python
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.

```python
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

```python
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.

```bash
pytest tests/test_compatibility.py -v
```

### Integration tests (browser required)

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

---

## Links

- [PyPI](https://pypi.org/project/pytest-playwright-softassert/)
- [GitHub](https://github.com/gopinathsasi/PlaywrightSoftAssertion)
- [Playwright Python docs](https://playwright.dev/python/docs/test-assertions)
- [Changelog](https://github.com/gopinathsasi/PlaywrightSoftAssertion/blob/main/CHANGELOG.md)
- [Report an issue](https://github.com/gopinathsasi/PlaywrightSoftAssertion/issues)

---

## License

MIT © [Gopinath M](https://github.com/gopinathsasi)
