// internal
A requirement isn't done until it's testable, a bug isn't closed until it's proven fixed, and a test suite that can't survive a UI change isn't automation — it's a liability. This is the pipeline we built to fix that, and PulseOps is the tool that runs it.
01
Requirements
02
Bugs
03
Automated Runs
04
Self-Healing
05
Reporting
01 · Requirements & bugs, linked — not two spreadsheets
Every user story gets acceptance criteria before anyone talks about test cases. Each AC can carry its own suggested tasks and tests, and every bug traces back to the requirement it broke — so "is this covered?" and "did this regress?" are one click, not an afternoon of searching Slack.
// what we require of a requirement
// what we require of a bug
mock data — illustrative only
02 · Automated runs, real browsers
Tests are declared as JSON flows — navigate, click, fill, wait, assert — run through Playwright against a real browser context. No shared sessions between flows, no shortcuts: if a journey needs auth, login is a step in that flow, every time.
| Flow | Status | Duration | Env |
|---|---|---|---|
| login | pass | 1.2s | prod |
| complete_quiz | pass | 4.8s | prod |
| navigate_routes | healed | 3.1s | prod |
| logged_in_user_starts_quiz | fail | 2.4s | prod |
| quiz_session_sees_results_screen | pass | 5.6s | prod |
mock run output — dummy timings
// how a flow is written
Steps and assertions, not scripts — a `navigate` → `waitForSelector` → `click` chain with a `repeatUntil` loop for variable-length journeys like a quiz of unknown length. Every selector is data-testid; never a class name, tag, or visible text alone.
// how a run is triggered
By suite, by explicit test IDs, or all-at-once via a single trigger endpoint — from CI on every deploy, on a nightly schedule, or by hand when chasing a specific regression. Results land in the same table whichever way they were started.
03 · Page objects that update themselves
A page object is the map from a UI concept ("the login button") to a selector. Ours aren't hand-maintained files that quietly rot — they're generated from the component source, and every selector carries an intent string describing what it is, not just how to find it. That intent is what makes healing possible later.
mock diff — selector re-pointed after a checkout redesign, intent unchanged
04 · Self-healing, in escalating tiers
A broken selector doesn't fail the run on the spot. It first tries any locator strategies the author declared up front, then escalates through a healing chain that gets broader (and slower) at each step — and every heal is logged so nothing silently drifts.
Declared locator strategies
Author-supplied, ordered fallbacks attached to the step or page object itself — role+name, label, text, placeholder, another page object's alternates. Tried top-to-bottom as native Playwright locators before any healing runs. Not really "healing" — it's the primary selector's own backup plan — but a win here is still logged so drift is visible.
Fallback chain
Cheap, deterministic rewrites of the original selector — a data-testid that moved gets tried as an id, a name, an aria-label; a stale class gets tried as a partial-class match. Milliseconds, no network call.
Structural search
Widens the search to ARIA role + accessible name and visible text content on the live DOM — catches re-labelled or re-ordered elements that tier 1's pattern rewrites can't.
AI regeneration
If both fail and AI healing is enabled: a DOM snapshot plus the step's intent string goes to Claude, which proposes a replacement selector ranked by stability (data-testid > role/name > semantic attribute > text > class). The suggestion is verified against the live page before it's accepted, then logged as a healed selector like tier 1 and 2.
Healed selectors
mock run detail — dummy selectors
05 · Promotion: automatic for a real heal, manual for a declared one
A confirmed tier 1–3 heal is folded straight back into the page object — the working
replacement is promoted to the front of its alternates, deduped, so the next
run tries it first. A tier-0 match is treated differently: it means one of your own
declared fallbacks worked after the primary failed, which can be intentional
ordering rather than proof the primary is dead — so it's surfaced on the confidence map
with a one-click "promote to primary" instead of being rewritten automatically.
| Flow | Original | Replacement | Tier | Uses | |
|---|---|---|---|---|---|
| parent_login_and_logout | text=Logout1 | text:logout | Tier 0 | 1 | promote to primary |
| parent_login_and_logout | #submit | button[type=submit] | Tier 0 | 1 | promote to primary |
mock data — a tier-0 match sits here until someone promotes it; tier 1–3 heals skip this and update the page object immediately
06 · Reporting that traces back, not just a pass/fail count
Every run result carries its flow name, status, duration, and — for anything short of a clean pass — the error, the console log, and (when it healed) the exact tier and selector swap that happened. That rolls up to the requirement it was proving, so a dashboard answers "is REQ-142 safe to ship" directly, not "did some tests pass somewhere."
navigate_routes// what shows up where
mock data — illustrative only
+ · Drive the pipeline from an agent
The same pipeline is exposed as an MCP server, so an AI agent can work the loop through tools instead of the dashboard — read a requirement, draft and automate its tests, trigger a run, triage what failed. It calls the same service layer the dashboard does, so there is one implementation of the rules, not two.
// what an agent can do
list_requirements, analyze_requirement, add_acceptance_criteriadraft_test_steps, automate_test, accept_flowscout_page scans a page for verified selectors before a flow is savedtrigger_run, list_runs, get_runget_traceability, coverage_gaps, analyze_failure// what keeps it safe
This whole pipeline is what powers PulseOps — live requirements, bugs, page objects, and run history, watching our own apps in production. Sign in to the read-only demo to look around; nothing in it can be changed or run.