Patterns cookbook
Annotated patterns for common UI scenarios. Each includes the reasoning behind the assertion choices so you can adapt them to your own markup.
1. Button click → DOM update (counter)
A button increments a counter displayed on the page.
Reasoning. The counter element already exists — its text content changes. Use updated with text-matches to verify the new value is a positive number.
<button
fs-assert="counter/increment"
fs-trigger="click"
fs-assert-updated='#counter[text-matches=Count: [1-9]\d*]'>
Increment
</button>
<div id="counter">Count: 0</div>
2. Form submit → element added (todo)
Submitting a form adds a new item to a list.
Reasoning. The new item doesn't exist yet. Use added. Trigger on the submit button click (the user's interaction point).
<button type="submit"
fs-assert="todo/add-item"
fs-trigger="click"
fs-assert-added=".todo-item">
Add Todo
</button>
3. Modal open / close
A button opens a modal; a cancel button closes it.
Reasoning. Opening: the modal overlay already exists in the DOM (hidden). Use visible to verify it now has layout dimensions. Closing: the modal content is removed from the DOM entirely. Use removed.
<button
fs-assert="modal/open"
fs-trigger="click"
fs-assert-visible=".modal-overlay">
Open Modal
</button>
<button
fs-assert="modal/close"
fs-trigger="click"
fs-assert-removed=".modal-content">
Cancel
</button>
If the modal is conditionally rendered (not in the DOM until opened), use added instead of visible for the open trigger.
4. Tab switching
Clicking a tab shows the corresponding content panel.
Reasoning. Tab panels typically exist in the DOM but are hidden. Use visible to verify the target panel has layout dimensions after the switch.
<button class="tab-button" data-tab="settings"
fs-assert="tabs/switch-tab"
fs-trigger="click"
fs-assert-visible=".tab-content[data-tab='settings']">
Settings
</button>
5. Multi-step wizard with sequence validation
Clicking "Next" advances to the next wizard step. Each step requires the previous to have completed.
Reasoning. Use fs-assert-after to validate step ordering. after and visible are independent assertions — a sequence violation with a correct-looking UI is a visible finding.
<button fs-assert="wizard/step-1" fs-trigger="click"
fs-assert-visible=".wizard-step[data-step='2']">
Next
</button>
<button fs-assert="wizard/step-2" fs-trigger="click"
fs-assert-after="wizard/step-1"
fs-assert-visible=".wizard-step[data-step='3']">
Next
</button>
<button fs-assert="wizard/step-3" fs-trigger="click"
fs-assert-after="wizard/step-2"
fs-assert-visible=".confirmation">
Submit
</button>
6. Form submit with conditional outcomes
A contact form that shows success, validation errors, or a server error.
Reasoning. The outcome depends on what the app renders. Use conditional assertions to branch: the first condition key whose selector matches wins, others are dismissed. No server-side integration required.
<form
fs-assert="contact/submit-form"
fs-trigger="submit"
fs-assert-added-success=".success-msg"
fs-assert-added-validation-error=".validation-errors"
fs-assert-added-server-error=".server-error"
fs-assert-timeout="2000">
<input name="email" type="email" />
<button type="submit">Send</button>
</form>
7. MPA navigation
A form submission triggers a full page navigation, and the success message appears on the next page.
Reasoning. The assertion must survive the page reload. Use fs-assert-mpa="true" to persist it to localStorage. On the next page, the agent picks it up and resolves it.
<button
fs-assert="mpa-form/submit"
fs-trigger="click"
fs-assert-mpa="true"
fs-assert-visible=".success-message">
Submit
</button>
8. Data load with conditional DOM update
Clicking a button fetches data. On success, results update. On error, an error element appears.
Reasoning. The results container already exists (updated for the success path). The error element is new (added for the error path). Longer timeout for network operations.
<button
fs-assert="data/load-posts"
fs-trigger="click"
fs-assert-updated-success="#results"
fs-assert-added-error=".error"
fs-assert-timeout="2600">
Load Posts
</button>
<div id="results"></div>
9. OOB side-effect validation (count label)
A count label shows "N/M remaining" and should update whenever a todo is added, toggled, or deleted. The count label lives in a different component from the trigger elements.
Reasoning. Without OOB, you'd need to prop-drill count data into every trigger just to compute the expected text. OOB assertions let the count label declare its own assertion triggered by other assertions passing. Use state types with OOB, not event types.
<button fs-assert="todos/add-item" fs-trigger="click"
fs-assert-added=".todo-item">Add</button>
<input type="checkbox" fs-assert="todos/toggle-complete"
fs-trigger="change"
fs-assert-updated=".todo-item[classlist=completed:true]" />
<button fs-assert="todos/remove-item" fs-trigger="click"
fs-assert-removed=".todo-item">Delete</button>
<div id="todo-count"
fs-assert="todos/count-updated"
fs-assert-oob="todos/add-item,todos/toggle-complete,todos/remove-item"
fs-assert-visible="[text-matches=\d+/\d+ remaining]">
2/3 remaining
</div>
10. Invariant continuous monitoring
The main navigation should always be visible. An error banner should never appear.
Reasoning. No user action triggers these — they're page-level contracts. Use fs-trigger="invariant" for continuous monitoring.
<nav
fs-assert="layout/nav-visible"
fs-trigger="invariant"
fs-assert-visible=".main-nav">
</nav>
<div
fs-assert="layout/no-error-banner"
fs-trigger="invariant"
fs-assert-hidden=".global-error-banner">
</div>
11. Custom event trigger + emitted assertion
The app dispatches custom events for state changes. Verify both the event and the resulting UI.
Reasoning. Use event:<name> trigger to activate on custom events. Use emitted to assert a custom event fires after a user action. detail-matches on triggers uses string equality; on emitted it uses regex.
<div fs-assert="cart/sync-check"
fs-trigger="event:cart-updated[detail-matches=action:add]"
fs-assert-visible="#cart-count[text-matches=\d+]">
</div>
<button fs-assert="checkout/payment" fs-trigger="click"
fs-assert-emitted="payment:complete[detail-matches=orderId:\d+]"
fs-assert-visible=".confirmation">
Pay Now
</button>
12. Stable assertion (no flickering)
After adding to cart, the price total should not flicker or update unexpectedly.
Reasoning. Use stable (inverted updated) with OOB to start the stability window after the expected mutation. Any mutation during the timeout window fails the assertion.
<button fs-assert="cart/add-item" fs-trigger="click"
fs-assert-updated="#cart-total">
Add to Cart
</button>
<div
fs-assert="cart/price-stable"
fs-assert-oob="cart/add-item"
fs-assert-stable="#cart-total"
fs-assert-timeout="500">
</div>
13. Count assertions (cardinality)
After a search, verify the correct number of results appear.
Reasoning. Use count, count-min, or count-max to verify element cardinality. count checks querySelectorAll(selector).length.
<button fs-assert="search/execute" fs-trigger="click"
fs-assert-added=".result-card[count-min=1]">Search</button>
<div
fs-assert="todos/item-count"
fs-assert-oob="todos/add-item,todos/remove-item"
fs-assert-visible=".todo-item[count=5]">
</div>
Dynamic assertion values
For bidirectional interactions (toggles, checkboxes, accordions), compute the expected next state in the attribute value using your framework's interpolation syntax:
// React
<input type="checkbox"
fs-assert="todos/toggle-complete"
fs-trigger="change"
fs-assert-updated={`.todo-item[classlist=completed:${!todo.completed}]`} />
When todo.completed is false, the assertion checks for completed:true (checking). When true, it checks for completed:false (unchecking).
This works identically in Vue (:fs-assert-updated="..."), Svelte (fs-assert-updated={expression}), and server templates (EJS <%= !todo.completed %>, ERB <%= !todo.completed %>, HEEx {[email protected]}).
Progressive assertion — todo delete
Start simple, layer on precision as confidence needs grow.
Level 1 — did it disappear?
<button
fs-assert="todos/remove-item"
fs-trigger="click"
fs-assert-removed=".todo-item">
Remove
</button>
Catches the basics.
Level 2 — success or error?
<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">
Remove
</button>
Now you know which outcome occurred. fs-assert-mutex="each" ties the cross-type conditionals together so exactly one resolves.
Level 3 — multi-check (did the toast also appear?)
<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">
Remove
</button>
<div class="toast-container"
fs-assert="todos/remove-item-toast"
fs-assert-oob="todos/remove-item"
fs-assert-visible=".toast[text-matches=Item deleted]">
</div>
Three independent assertions: conditional on the trigger, OOB on the toast container, and no coupling between components.