Assertions

An assertion answers one question: after this trigger fires, what DOM change proves the feature worked correctly? The fs-assert-<type> attribute names the expected outcome; its value is the CSS selector (optionally with inline modifiers) that identifies the target element.

<button
  fs-assert="checkout/submit-order"
  fs-trigger="click"
  fs-assert-added=".confirmation">
  Place Order
</button>

Every element needs fs-assert (the key), fs-trigger (the when), and at least one fs-assert-<type> (the what).

Decision tree

Is the target element NEW (doesn't exist before the action)?
├── YES → fs-assert-added=".selector"
│         (element will be created in the DOM)
└── NO → The element already exists. What changes?
         ├── Content/attributes change → fs-assert-updated=".selector"
         ├── Element will be removed → fs-assert-removed=".selector"
         ├── Need to verify it's visible → fs-assert-visible=".selector"
         ├── Need to verify it's hidden → fs-assert-hidden=".selector"
         ├── Media element loads → fs-assert-loaded=".selector"
         └── Element should NOT change → fs-assert-stable=".selector"

The #1 instrumentation mistake is getting added vs updated wrong. added = the element doesn't exist yet, will be created. updated = the element already exists, content changes. A class toggle on an existing node is updated, not added, even when the new class name looks like "something new appeared."

DOM assertions

These resolve by watching MutationObserver or running a querySelector check.

Type Resolves when
added A new element matching the selector appears in the DOM
removed An element matching the selector is removed from the DOM
updated The matched element or its subtree is mutated (text, attributes, children)
visible The matched element exists and has layout dimensions
hidden The matched element exists but has no layout dimensions
loaded A media element (img, video, iframe) finishes loading
stable The matched element's subtree is NOT mutated during the timeout window (inverted updated)

Mutation-observed vs query-based. added, removed, and updated resolve only from MutationObserver records — they capture the exact moment a DOM change happens and can't be missed. A pre-existing match at trigger time is not a pass: added waits for an actual insertion. visible and hidden resolve via point-in-time querySelector + layout checks and pass immediately if the current state already satisfies the assertion. Prefer mutation-observed types for elements with short lifetimes.

Event assertions

Type Resolves when
emitted A matching CustomEvent fires on document. Supports [detail-matches=key:pattern] for regex on event.detail.

Not compatible with MPA mode. Synchronous dispatch from the same trigger handler fires before the assertion is created — use async dispatch.

Sequence assertions

Type Resolves when
after All referenced parent assertion keys have already passed. Comma-separated for multiple (AND semantics).

Resolves immediately at creation time. Produces an independent data point alongside any DOM assertions on the same element. Don't combine with fs-trigger="invariant".

Route assertions

Type Resolves when
route window.location matches the given pattern — pathname, query params, and hash fragment are each validated as anchored regex.

Resolves via window.location reads, not MutationObserver. Re-evaluated after each navigation-like event. Pair with MPA mode for assertions that span a full page navigation.

Feature pages

Assertions have several cross-cutting features that shape how they behave but aren't new types themselves:

  • Conditional assertions — one element, multiple outcomes: fs-assert-added-success + fs-assert-added-error
  • Mutex (fs-assert-mutex) — how conditional siblings compete across types: type, each, conditions, selective
  • Out-of-band (OOB) — fire side-effect checks when a parent assertion passes or fails: fs-assert-oob, fs-assert-oob-fail
  • MPA (multi-page) — persist an assertion across full page navigation: fs-assert-mpa="true"
  • Timeout — opt-in SLA: fs-assert-timeout="2000" plus the GC / unload model
  • Self-referencing selectors — omit the selector and provide only modifiers to check the element itself

Required attributes on every instrumented element

Attribute Purpose Example
fs-assert Assertion key (hierarchical, /-separated) "checkout/submit-order"
fs-trigger User action that starts it "click", "submit", "invariant"
fs-assert-<type> Expected DOM outcome fs-assert-added=".success-msg"

For OOB elements, fs-assert-oob or fs-assert-oob-fail replaces fs-trigger.

Assertion key convention

Use / as a hierarchical separator. Feature prefix, action suffix:

todos/add-item
todos/remove-item
checkout/submit-order
profile/media/upload-photo

Keys must be stable across releases — the collector uses the key as the primary identity for aggregating stats. Human-readable labels are configured on the collector side, not in the HTML.

Common mistakes

  • added vs updated vs visible. added = doesn't exist yet. updated = exists, content mutates. visible = exists, check layout dimensions.
  • Using event types (updated, loaded, emitted) with OOB or invariant. These need a witnessed mutation or event, which already happened by the time the OOB or invariant assertion is created. Use state types (visible, hidden, added, removed).
  • Missing required attributes. Every element needs fs-assert + fs-trigger (or fs-assert-oob / fs-assert-oob-fail) + at least one assertion type.
  • Dispatching CustomEvents synchronously in the trigger handler. The event fires before the emitted listener is created. Use async dispatch.