Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/edda

crates/nornir-testmatrix/.nornir/testmatrix-uiplane-guide.md

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

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

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.