How We Test

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

  • Written as a user story with explicit acceptance criteria (AC-1, AC-2…), not a paragraph of prose
  • An AI pass flags ambiguities and risks against the story before work starts
  • Suggested tasks and suggested tests are generated per-AC, with a rationale — a human still approves them
  • Every automated or manual test case links back to the AC it proves

// what we require of a bug

  • Filed against the requirement or test run that surfaced it, never floating free
  • Carries repro steps, environment, and severity — auto-drafted from a failing run where possible
  • Stays open until a re-run of the exact failing case passes, not until someone eyeballs it
  • Rolls up per-requirement, so a story with 3 open bugs can't quietly ship as "done"
pulseops.dojolabz.com / requirements / REQ-142

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.

runner/trigger — suite: sanity
FlowStatusDurationEnv
loginpass1.2sprod
complete_quizpass4.8sprod
navigate_routeshealed3.1sprod
logged_in_user_starts_quizfail2.4sprod
quiz_session_sees_results_screenpass5.6sprod

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.

page-objects / checkout.promo-input.json
{
"name": "checkout_promo_input",
- "selector": "div.checkout > form > input:nth-child(4)",
+ "selector": "[data-testid=\"promo-code-input\"]",
"intent": "promo code text field on checkout",
"lastVerified": "2026-09-21T04:12:00Z"
}

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.

T0

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.

T1

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.

T2

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.

T3

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.

automation / runs / #run-8823 — parent_login_and_logout

Healed selectors

Tier 0 — declared fallback "submit login form"
#submit → button[type=submit]
Tier 0 — declared fallback "log out"
text=Logout1 → text:logout

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.

automation / runs / #run-8823 — selector confidence map
FlowOriginalReplacementTierUses
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."

report / run #4127

// what shows up where

  • Run history — every trigger, manual or scheduled, with full flow-level detail
  • Requirement view — AC-by-AC coverage, linked tests, and any open bugs against it
  • Bug view — repro, severity, and the run that last reproduced (or failed to reproduce) it
  • Healing log — every tier-1/2/3 swap, so selector drift is visible before it becomes a pattern

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

  • Requirements — list_requirements, analyze_requirement, add_acceptance_criteria
  • Test authoring — draft_test_steps, automate_test, accept_flow
  • Locators — scout_page scans a page for verified selectors before a flow is saved
  • Runs — trigger_run, list_runs, get_run
  • Audit & triage — get_traceability, coverage_gaps, analyze_failure

// what keeps it safe

  • AI tools return a draft; saving is a separate call, so a human can review in between
  • Each token is scoped to one app and revocable instantly — an agent can't reach another app's data
  • Tools that spend AI or runner budget need their own scope and are rate-limited
  • Filing a bug from a failure stays a dashboard decision — the agent analyses, a person decides

See it running

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.

Try the Read-only Demo