nigig-org/crates/apps/nigig-traffic/tests/runtime_ui.rs

123 lines
3.9 KiB
Rust

//! Headless runtime-journey gates (TRAFFIC-14).
//!
//! The Makepad GUI harness cannot boot in CI (see tests/ui.rs ignore
//! reasons), so these journeys drive the PRODUCTION path one layer down:
//! scenario selection → phase gating → tick → terminal result → restart /
//! next / previous → persistence-shape result — through `TrafficWorld`,
//! the same simulation/rules/scoring the GUI drives. `tests/ui_basic.rs`
//! remains the headless smoke test and is labelled as such; this file is
//! the lifecycle/assessment-honesty coverage, not GUI coverage.
use nigig_traffic::traffic::scenario::all;
use nigig_traffic::traffic::{allowed_transition, Phase, TrafficWorld, Transition};
#[test]
fn journey_select_drive_complete_restart() {
let mut w = TrafficWorld::new();
w.load_scenario(0);
assert_eq!(w.run.phase, Phase::Intro);
// Intro ignores throttle: no head start.
let full = makepad_game_blocks::DriveInput {
throttle: 1.0,
..makepad_game_blocks::DriveInput::default()
};
let start = w.sim.world.entity(0u64).map(|e| (e.pos.x, e.pos.z));
for _ in 0..60 {
w.tick(&full);
}
assert_eq!(w.run.phase, Phase::Driving);
let now = w.sim.world.entity(0u64).map(|e| (e.pos.x, e.pos.z));
assert_eq!(start, now, "intro must not displace the car");
// Restart returns to Intro with cleared score.
w.load_scenario(0);
assert_eq!(w.run.phase, Phase::Intro);
assert_eq!(w.run.score.mistakes, 0);
}
#[test]
fn journey_next_prev_cycle_all_scenarios() {
let mut w = TrafficWorld::new();
w.load_scenario(0);
let n = all().len();
let first = w.scenario().expect("scenario").id;
for _ in 0..n {
w.next();
}
assert_eq!(w.scenario().expect("scenario").id, first);
w.next();
let second = w.scenario().expect("scenario").id;
w.prev();
assert_eq!(w.scenario().expect("scenario").id, first);
assert_ne!(first, second);
}
#[test]
fn journey_pause_resume_freezes_and_continues() {
let mut w = TrafficWorld::new();
w.load_scenario(0);
for _ in 0..61 {
w.tick(&makepad_game_blocks::DriveInput::default());
}
assert_eq!(w.run.phase, Phase::Driving);
assert!(w.pause());
let tick = w.run.tick;
for _ in 0..120 {
w.tick(&makepad_game_blocks::DriveInput {
throttle: 1.0,
..makepad_game_blocks::DriveInput::default()
});
}
assert_eq!(w.run.tick, tick);
assert!(w.resume());
w.tick(&makepad_game_blocks::DriveInput::default());
assert_eq!(w.run.tick, tick + 1);
}
#[test]
fn journey_terminal_result_is_immutable() {
let mut w = TrafficWorld::new();
w.load_scenario(0);
for _ in 0..61 {
w.tick(&makepad_game_blocks::DriveInput::default());
}
// Force terminal through the public transition table only.
assert!(allowed_transition(w.run.phase, Transition::Complete));
// Direct terminal simulation: complete via score path then hammer ticks.
w.run.score.completed = true;
w.run.phase = Phase::Complete;
let snap = (
w.run.phase,
w.run.score.total_points,
w.run.score.mistakes,
w.run.tick,
);
for _ in 0..1000 {
w.tick(&makepad_game_blocks::DriveInput {
throttle: 1.0,
steer: 1.0,
..makepad_game_blocks::DriveInput::default()
});
}
assert_eq!(
(
w.run.phase,
w.run.score.total_points,
w.run.score.mistakes,
w.run.tick
),
snap
);
}
#[test]
fn journey_result_binds_id_and_content_version() {
// Results bind scenario ID + content version, never list index: moving
// through the catalog keeps identity stable.
let w = TrafficWorld::new();
let ids: Vec<&str> = all().iter().map(|s| s.id).collect();
for (i, s) in all().iter().enumerate() {
assert_eq!(ids[i], s.id);
assert_eq!(s.content_version(), 0);
}
assert!(w.scenario().is_none());
}