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
addedvsupdatedvsvisible.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(orfs-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.