uiplane — adoption guide (wire LAW 9 for YOUR app)
This guide shows how to model an app as a UiPlan, drive it, and turn the walk into matrix rows. The framework primitive is generic; you supply your app's planes
- a
PlaneDriver.
1. Declare the plan (planes + transitions)
use nornir_testmatrix::uiplane::{UiPlan, UiPlane, UiSurface, Transition};
let plan = UiPlan::new("Home") // boot plane = BFS root
.plane(UiPlane::new("Home").surface(UiSurface::passive("title"))) // present-only
.plane(UiPlane::new("Bench").surface(UiSurface::new("bench_chart"))) // present + ran
.edge("Home", "Bench", "click_tab:Bench"); // a named transition action
UiSurface::new(id)⇒ must be present AND ran;UiSurface::passive(id)⇒ present is enough.- The plane set should be discovered from the running app (e.g. nornir reads
state_json["all_tabs"]), not hand-listed — a surface added to the app then automatically becomes a plane the walk must reach. - Before walking, sanity-check the plan is whole:
assert!(plan.orphan_planes().is_empty())andassert!(plan.dangling_transitions().is_empty()).
2. Implement a PlaneDriver over your app
Read plane / present / ran from your app's emitted data (never pixels):
use nornir_testmatrix::uiplane::PlaneDriver;
use std::collections::BTreeSet;
struct MyDriver<'a> { app: &'a mut MyApp }
impl PlaneDriver for MyDriver<'_> {
fn current_plane(&mut self) -> String {
self.app.state_json()["tab"].as_str().unwrap_or_default().to_string()
}
fn apply(&mut self, action: &str) -> Result<(), String> {
let tab = action.strip_prefix("click_tab:").ok_or("bad action")?;
self.app.click_tab(tab); // the REAL control path the user/robot uses
Ok(())
}
fn surfaces_present(&mut self) -> BTreeSet<String> { /* from state_json */ }
fn surfaces_ran(&mut self) -> BTreeSet<String> { /* populated ⇒ ran */ }
}
apply only performs the action — the walk verifies the landing, so a transition that fails to land (a missing control in the shipped app) is caught automatically.
3. Walk + assert
use nornir_testmatrix::uiplane::{RobotPlan, SurfaceVerdict};
let report = RobotPlan::walk(&plan, &mut MyDriver { app: &mut app });
assert!(report.is_green(), "LAW 9: {}", report.summary());
// or assert pieces:
assert!(report.unreachable_planes.is_empty(), "no orphan/unreachable plane");
let absent: Vec<_> = report.rows.iter()
.filter(|r| r.verdict == SurfaceVerdict::ReachableAbsent).collect();
assert!(absent.is_empty(), "no declared surface is absent on its plane");
4. Emit into the matrix → warehouse
report.emit("my-app-uiplane"); // one functional_status row per (plane,surface)
// or: let rows: Vec<TestResultRow> = report.to_rows("my-app-uiplane");
Each row is aspect uiplane, test_name = "<plane>/<surface>", pass iff ReachableAndRan. The same drain → nornir test → warehouse path as every other functional row (LAW 8: the wire is not the home — reconcile via the lock-owning server).
5. Unit-test your plan with MockDriver (no UI needed)
use nornir_testmatrix::uiplane::MockDriver;
let mut driver = MockDriver::new("Home")
.with_transition("click_tab:Bench", "Bench")
.with_plane_state("Home", ["title"], [] as [&str;0])
.with_plane_state("Bench", ["bench_chart"], ["bench_chart"]); // present + ran
let report = RobotPlan::walk(&plan, &mut driver);
assert!(report.is_green());
Prove the RED, too (sensitivity): flip a plane to present-but-not-ran (ran = []) → ReachableNotRan; drop the surface from present → ReachableAbsent; add an orphan plane with no with_transition → Unreachable.
The four verdicts (what each RED means + how to fix)
| verdict | meaning | fix |
ReachableAndRan |
reached the plane, surface present + ran | (green) |
ReachableNotRan |
reached, surface present, emitter never fired | populate the pane / wire the handler (RAGNARÖK) |
ReachableAbsent |
reached the plane, declared surface missing | the shipped build dropped it (wasm parity) — restore it |
Unreachable |
the plane is an orphan / no transition lands on it | add the missing transition (the Sverige catch) |
Driving the SHIPPED wasm (LAW 1)
Implement uiplane_plan::wasm::BrowserBridge (two JS calls: eval_state → window.__facett_state(), eval_click(action) → window.__facett_click(action)) over a headless browser, hand it to WasmPlaneDriver::new(bridge, block_of, surface_id), and run the SAME UiPlan. native↔wasm verdicts are computed by the same pane_block_ran logic — only the transport differs.