Jest-Style Assertions
Updated 2026-01-15 — Comprehensive guide to Predicate assertions: predicates, state-aware checks, retries, confidence gating, and optional vision fallback.
Think Jest for AI web agents. Take a browser snapshot (rendered DOM + layout), then evaluate predicates that return pass/fail with structured details.
Introduction
What is an assertion in Predicate?
In Predicate, an assertion is a predicate:
- Input:
AssertContext(currentsnapshot, currenturl, andstepId) - Output:
AssertOutcome(passed,reason,details)
Two layers
Predicates are pure functions — they read snapshot state and return AssertOutcome.
Runtime execution decides how and when to evaluate predicates (once vs retry, confidence gating, fallback, tracing).
Getting Started
Install
pip install sentienceapiQuick start: assert that text exists
from predicate import AsyncPredicateBrowser
from predicate.agent_runtime import AgentRuntime
from predicate.verification import exists
from predicate.tracing import Tracer, JsonlTraceSink
async with AsyncPredicateBrowser() as browser:Python keyword
assert is a Python keyword, so the Python API uses assert_ instead.
Using Assertions
Basic predicates
These predicates are for simple checks:
URL predicates:
- Python:
url_matches,url_contains - TypeScript:
urlMatches,urlContains
Element predicates:
- Python:
exists,not_exists,element_count - TypeScript:
exists,notExists,elementCount
Composition:
- Python:
all_of,any_of,custom - TypeScript:
allOf,anyOf,custom
Composed predicate example
from predicate.verification import all_of, exists, url_contains
on_checkout = all_of(
url_contains("/checkout"),
exists("text~'Order summary'")
)
runtime.assert_(on_checkout, label="checkout_loaded"State-Aware Assertions
These predicates are deterministic checks against element state extracted from the live DOM:
is_enabled/isEnabled— enabled/disabledis_checked/isChecked— checked/uncheckedvalue_contains/valueContains— value equals/containsis_expanded/isExpanded— expanded/collapsed
from predicate.verification import is_enabled, is_checked, value_contains
runtime.assert_(is_enabled("role=button text~'Continue'"), label="continue_enabled", required=True)
runtime.assert_(is_checked("role=checkbox name~'Accept terms'"Assertion DSL (E, expect)
The DSL gives you a more expressive, Jest-like API while staying deterministic: DSL expressions compile to predicates you can pass into runtime.assert/assert_ and runtime.check(...).eventually().
Not a separate system
The DSL is just a nicer way to build predicates. The runtime still decides when to snapshot, retry, and emit trace events.
Import
from predicate.asserts import E, expect, in_dominant_listElement Query Builder: E()
Build element queries by semantic attributes (role/text/name/href) plus visibility state:
# By role / text
E(role="button")
E(role="button", text_contains="Submit")
# By href
E(role="link", href_contains="/checkout")
# Visibility filtersExpectation Builder: expect()
Use expect(...) to create matchers like toExist() / toBeVisible():
# Element exists
runtime.assert_(
expect(E(role="button", text_contains="Save")).to_exist(),
label="save_button_exists",
required=True,Global Text Assertions
runtime.assert_(
expect.text_present("Welcome back"),
label="user_logged_in",
)
runtime.assert_(
expect.no_text("Error"),
label="no_errors",Dominant List Queries: in_dominant_list() / inDominantList()
For “click the first / third / top‑N result” tasks, query the dominant group as an ordered list:
runtime.assert_(
expect(in_dominant_list().nth(0)).to_have_text_contains("Show HN"),
label="first_item_is_show_hn",
)
top_5 = in_dominant_list().Retrying Assertions with .eventually()
Predicate provides a fluent assertion handle:
.once()— evaluates a predicate once (same semantics asassert/assert_).eventually()— retries with fresh snapshots until success, timeout, or exhaustion
ok = await runtime.check(
exists("role=button text~'Continue'"),
label="continue_visible",
required=True,
).eventually(timeout_s=10, poll_s=0.25Snapshot confidence gating + exhaustion
If you pass minConfidence / min_confidence, .eventually() can treat low-confidence snapshots as failures and resnapshot.
ok = await runtime.check(
exists("text~'Payment method'"),
label="payment_ready",
required=True,
).eventually(
timeout_s=15,
poll_s=Snapshot exhaustion
If confidence stays below the threshold for maxSnapshotAttempts, the assertion ends with reason_code = "snapshot_exhausted" and includes snapshot.diagnostics for debugging.
Expanded deterministic verifications: adaptive resnapshotting
On long / virtualized pages, a small snapshot limit can miss the target element and cause a false failure. You can make .eventually() more reliable by growing the snapshot element limit across retries.
from predicate.verification import exists
ok = await runtime.check(
exists("text~'Checkout'"),
label="checkout_visible",
required=True,
).eventually(
timeout_sMore verification primitives
See Verifications for scroll verification and other “expanded verification” patterns.
Optional vision fallback (last resort)
After snapshot_exhausted, you can optionally provide a vision-capable LLMProvider and let Predicate ask a strict YES/NO verifier question using a screenshot.
ok = await runtime.check(
exists("text~'Order confirmed'"),
label="order_confirmed",
required=True,
).eventually(
min_confidence=0.8,
max_snapshot_attemptsVision is last resort
The verifier question is derived from the assertion label. Vision is only used after structured snapshot attempts are exhausted — never by default.
Setup and Teardown
Jest uses beforeAll/beforeEach/afterEach/afterAll. In Predicate, the idea is the same: create a browser + runtime once, snapshot as needed, then close.
# pytest-asyncio example
import pytest
from predicate import AsyncPredicateBrowser
from predicate.agent_runtime import AgentRuntime
from predicate.tracing import Tracer, JsonlTraceSink
from predicate.verification import exists
@pytest.fixtureGuides
Use AgentRuntime
AgentRuntime is the recommended place to run assertions because it:
- keeps the latest snapshot as assertion context
- emits trace events (useful for debugging / Studio)
- provides
.check().eventually()for robust "wait until true" verification
When to use basic vs state-aware assertions
- Use basic assertions for "presence/absence/count/url" checks.
- Use state-aware assertions when you need deterministic checks of element state (enabled/checked/value/expanded) that commonly changes in SPAs.
How to read failures (reason codes + suggestions)
When an assertion fails, Predicate includes structured details:
reason_code(e.g.no_match,state_mismatch,snapshot_low_confidence,snapshot_exhausted)nearest_matches(best-effort suggestions when a selector fails)
FAQ
Jest also has snapshot testing — how is that different from a Predicate snapshot?
Short answer
Jest snapshots freeze output. Predicate snapshots model interactive state. They solve different problems, even though they share the word "snapshot".
What Jest snapshot testing is
Jest snapshots are essentially golden files:
- serialized React component trees
- HTML strings, JSON responses, text output
- captures a static representation
- answers: "Did this output change?"
Jest snapshot characteristics:
- Static: captures a representation at one point in time
- Text/JSON-based: stored as serialized snapshots
- Comparison-oriented: meant to diff over time
- Great for regression detection
- Not interaction-aware: no notion of timing, user actions, or success
What a Predicate snapshot is
A Predicate snapshot is a runtime perception frame of a live browser:
- rendered DOM after hydration
- semantic roles + names
- UI state (enabled, checked, expanded, value)
- geometry + ordering
- grouping (lists, cards, feeds)
- stability diagnostics (confidence, quiet time)
Predicate snapshot characteristics:
- Live: gathered from a running page, not static HTML
- Semantic: role/name/text-focused, not raw HTML strings
- Spatial/ordinal-aware: uses layout and ordering
- Repeated during execution
- Designed for verification: evaluate assertions, not diff against golden files
Key difference
| Aspect | Jest snapshot | Predicate snapshot |
|---|---|---|
| Purpose | Detect output changes | Enable reasoning & verification |
| Lifecycle | Stored & diffed | Ephemeral, per-step |
| Input | Render output / strings | Live browser state |
| Stability | Assumes determinism | Measures instability |
| Used for | Regression testing | Agent execution + QA verification |
| Handles SPAs | Poorly | Natively |
| Geometry / ordinality | No | Yes |
Do I need .eventually() for every assertion?
No. A good rule of thumb:
- Use single-shot
assert/assert_for stable, immediate checks (URL checks, "element exists", etc.) - Use
.check(...).eventually()for anything that depends on async UI timing (SPAs, loading states, transitions, network-backed components)
Why does .eventually() sometimes fail with snapshot_low_confidence / snapshot_exhausted?
If you set minConfidence / min_confidence, the runtime treats low-confidence snapshots as unreliable and will resnapshot. After maxSnapshotAttempts / max_snapshot_attempts, it stops with snapshot_exhausted to avoid infinite retries and surfaces snapshot.diagnostics in details to help you debug instability.
Related
- Agent Runtime — Runtime verification support for agent loops
- AI-Driven QA — Enterprise QA workflows with Predicate assertions