Available since v0.1.0

Conditional assertions

Stable

Some user actions have more than one valid outcome. Submitting a form can succeed, fail validation, or hit a server error. Clicking a search button can return results, an empty state, or a rate-limited message. Conditional assertions let you declare all the possibilities on a single element — the first outcome whose selector matches wins, the others are dismissed.

Syntax

Append a {condition-key} suffix to any assertion type:

fs-assert-{type}-{condition-key}="<selector>"
<button fs-assert="auth/login" fs-trigger="click"
  fs-assert-added-success=".dashboard"
  fs-assert-added-error=".error-msg">
  Log in
</button>

One of added-success or added-error wins. The other is dismissed and never reported.

Condition key rules

  • Freeform lowercase alphanumeric with hyphens. Examples: success, error, empty, rate-limited, validation-error, server-error.
  • Avoid assertion type names as condition keys. added, removed, updated, visible, hidden, loaded, oob, oob-fail, stable, emitted, after are parsed as assertion types, not condition keys. Using them leads to confusing parser errors.
  • Keys are grouped by element + assertion type. Multiple conditional siblings with the same type on the same element form a sibling group — first to match wins.

Sibling groups — same type, different conditions

<button fs-assert="search/execute" fs-trigger="click"
  fs-assert-added-results=".search-results"
  fs-assert-added-empty=".no-results"
  fs-assert-added-error=".search-error">
  Search
</button>

Three added conditionals, same element. First matching selector wins. The other two are dismissed.

Cross-type conditional groups

To group conditionals across different types on the same element, add fs-assert-mutex. Without mutex, cross-type conditionals (added-success + removed-success) resolve independently — each producing its own pass/fail signal.

See mutex for the full semantics.

Full outcome coverage — conditionals + OOB

A single conditional branch can have multiple expectations via OOB. For example, a delete should remove the item AND show a success toast:

<!-- Primary: conditional on the trigger -->
<button fs-assert="todos/remove-item" fs-trigger="click"
  fs-assert-mutex="each"
  fs-assert-removed-success=".todo-item"
  fs-assert-added-error=".error-msg">
  Delete
</button>

<!-- Secondary: OOB checks the toast appeared on successful delete -->
<div class="toast-container"
  fs-assert="todos/delete-toast"
  fs-assert-oob="todos/remove-item"
  fs-assert-visible=".success-toast">
</div>

Dismissed vs failed

Dismissed means the assertion was retired without producing a signal — it's not a failure. A conditional's losing siblings are dismissed, not failed. This means:

  • Dismissed assertions don't appear in the collector's failure metrics
  • fs-assert-oob-fail does NOT fire on dismissed conditionals — only on timeouts, GC sweep, SLA misses, and explicit failures

Pairs well with

Gotchas

  • Using reserved words as condition keys. success and error are fine. added, removed, and other assertion type names are not.
  • Expecting AND semantics within a sibling group. First-match-wins is OR semantics. For AND, use OOB on a secondary element or mutex="conditions" with same-key grouping.
  • Condition key collisions between types. Without mutex, added-success and removed-success on the same element are independent — they don't race. Use fs-assert-mutex="each" or fs-assert-mutex="conditions" if you need them to compete.
  • React JSX boolean drop. React drops bare boolean attributes. Always use explicit string values — fs-assert-added-success=".dashboard", never fs-assert-added-success. See the React boolean attribute trap.

See also