browser-tests/tests/fit_and_levels.rs
//! ★ **Follow-ups of HOL-UI-FIX** (coordinator 2026-11-01):
//!
//! * one artifact's security view fits a 400 px phone (it was ~600 px wide: a sideways scroll);
//! * the Disks pane says what holger says of a repository's storage folder that is not made
//! yet: "created at the first upload", never "sits on no mount" (holger
//! `AdminUnresolved.made_at_first_upload`);
//! * Henrik's three zoom levels of the flow (HOL-FLOW-SMART) still work from the Flow page: level
//! 1 the machines (`/flow?open=`), a click on a machine opens level 2 (`/flow?open=main`), and
//! level 3 is the full RON (`/flow?open=all`); the Overview's links lead to the right level.
use chromiumoxide::cdp::browser_protocol::emulation::SetDeviceMetricsOverrideParams;
use holger_ui_browser_tests::browser::{Browser, Tab, NEVER};
use holger_ui_browser_tests::rig::{self, Rig, CONSOLE, WHEEL};
async fn register(tab: &Tab, user: &str) {
tab.until("the register button", "!!document.getElementById('register')").await;
tab.js("document.getElementById('reg').open = true").await.unwrap();
tab.fill("#user", user).await;
tab.click("#register").await;
tab.until("registered", holger_ui_browser_tests::browser::SIGNED_IN_OR_REGISTERED).await;
if !tab.signed_in().await {
tab.press("#login").await;
tab.until("holger's session in this tab", holger_ui_browser_tests::browser::SIGNED_IN).await;
}
}
async fn owner_of(rig: &Rig, browser: &Browser) -> Tab {
let owner = browser.tab().await.unwrap();
owner.open(&rig.at("/login")).await.unwrap();
owner.until("the bootstrap door", "!document.getElementById('reg').hidden").await;
owner.fill("#token", CONSOLE).await;
owner.js("document.getElementById('role').value = 'admin'").await.unwrap();
register(&owner, "owner").await;
owner
}
/// The drawing's zoom level, as holger wrote it into the image (`data-level`).
const LEVEL: &str = "(() => { const i = document.querySelector('[data-board=estate] img.board-img'); if (!i) return ''; const m = decodeURIComponent(i.src).match(/data-level=\"(\\d)\"/); return m ? m[1] : ''; })()";
/// Every element that sticks out past `width` px and lies in no frame of its own that scrolls.
fn sticking_out(width: u32) -> String {
format!(
"(() => {{ const out = []; for (const e of document.querySelectorAll('body *')) {{ const r = e.getBoundingClientRect(); if (r.width === 0 || r.right <= {width} + 1) continue; let p = e.parentElement, framed = false; while (p) {{ const o = getComputedStyle(p).overflowX; if (o === 'auto' || o === 'scroll' || o === 'hidden') {{ framed = true; break; }} p = p.parentElement; }} if (!framed) out.push(e.tagName + '.' + e.className + ' ' + Math.round(r.right)); }} return out.slice(0, 12); }})()"
)
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn one_artifacts_security_view_fits_a_phone() {
let rig = Rig::start().await;
let browser = Browser::launch().await.expect("chromium starts");
let owner = owner_of(&rig, &browser).await;
let writer = browser.person().await.unwrap();
let invite = rig.invite("wanda", "holger-writers");
writer.open(&rig.at(&format!("/login?invite={invite}"))).await.unwrap();
register(&writer, "wanda").await;
let files = tempfile::tempdir().unwrap();
let wheel = rig::write_wheel(files.path());
writer.open(&rig.at("/repo/incoming")).await.unwrap();
writer.until("the upload panel", "!!document.querySelector('details.upload')").await;
writer.js("document.querySelector('details.upload').open = true").await.unwrap();
writer.pick_file("details.upload input[type=file]", &wheel).await.unwrap();
writer.until("the name from the file", &format!("[...document.querySelectorAll('details.upload input[type=text]')].some(i => i.value === {})", serde_json::to_string(WHEEL).unwrap())).await;
writer.click_text("Upload").await;
writer.until("uploaded", "!!document.querySelector('details.upload .ok')").await;
for want in ["SCANNED", "POLICY_CHECKED"] {
let (status, body) = rig.console_post(&format!("/-/intake/advance?name={WHEEL}&verb=advance"), None);
assert!(status == 200 && body.contains(want), "→ {want}: {status} {body}");
}
owner.page.execute(SetDeviceMetricsOverrideParams::new(400, 860, 1.0, true)).await.unwrap();
owner.open(&rig.at(&format!("/security/artifact?ns=&name={WHEEL}&version="))).await.unwrap();
owner.until("the artifact's history", "document.body.innerText.includes('POLICY_CHECKED') && !/loading/.test(document.querySelector('main').innerText)").await;
tokio::time::sleep(std::time::Duration::from_millis(800)).await;
let wide = owner.js("document.documentElement.scrollWidth").await.unwrap();
let out = owner.js(&sticking_out(400)).await.unwrap();
assert!(wide.as_i64().unwrap_or(9999) <= 400, "the artifact's security view is {wide} px wide at a 400 px phone; sticking out: {out}");
owner.assert_no_link_to(NEVER).await.unwrap();
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn the_disks_pane_says_a_storage_folder_not_made_yet_is_created_at_the_first_upload() {
let rig = Rig::start().await;
let browser = Browser::launch().await.expect("chromium starts");
let owner = owner_of(&rig, &browser).await;
owner.open(&rig.at("/admin")).await.unwrap();
owner.until("the admin panes", "[...document.querySelectorAll('nav.tabs button')].some(b => b.textContent === 'Disks')").await;
owner.click_text("Disks").await;
owner.until("the Disks pane", "document.body.innerText.includes('Floor:')").await;
// A fresh box: no repository has stored anything, so no storage folder is made yet.
let later = owner.until("the folders made at the first upload", "(document.querySelector('[data-disks=later]') || {}).textContent || ''").await;
let later = later.as_str().unwrap().to_owned();
assert!(later.contains("created at the first upload") && later.contains("incoming"), "the server's words for a folder not made yet: {later}");
let text = owner.text().await;
assert!(!text.contains("sits on no mount"), "a folder made at the first upload is said to sit on no mount: {text}");
let warned = owner.js("[...document.querySelectorAll('.banner-warn, .badge-warn')].map(e => e.textContent).filter(t => /mount|not placed/.test(t))").await.unwrap();
assert_eq!(warned, serde_json::json!([]), "a folder made at the first upload is drawn as a warning");
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn the_flow_has_its_three_levels_and_the_overview_links_to_them() {
let rig = Rig::start_with_graph().await;
let browser = Browser::launch().await.expect("chromium starts");
let owner = owner_of(&rig, &browser).await;
// Level 1: the machines, nothing open.
owner.open(&rig.at("/flow?open=")).await.unwrap();
assert_eq!(owner.until("level 1", LEVEL).await, serde_json::json!("1"), "/flow?open= is the machines");
// A click on a machine (holger's link over the image) opens level 2, that machine unfolded.
owner.until("main's link over the image", "!!document.querySelector('[data-board=estate] a.board-hit[href=\"flow?open=main\"]')").await;
owner.click("[data-board=estate] a.board-hit[href=\"flow?open=main\"]").await;
owner.until("the Flow, main unfolded", "location.pathname === '/flow' && location.search === '?open=main'").await;
owner.until("level 2", &format!("{LEVEL} === '2'")).await;
// Level 3: the full RON.
owner.open(&rig.at("/flow?open=all")).await.unwrap();
owner.until("level 3", &format!("{LEVEL} === '3'")).await;
// The Overview's links: "Open the flow" is level 2 (main unfolded), the boxes are the
// machines (level 1).
owner.open(&rig.at("/overview")).await.unwrap();
owner.until("the Overview's boxes", "!!document.querySelector('[data-overview=boxes] a.card-more')").await;
let boxes = owner.js("document.querySelector('[data-overview=boxes] a.card-more').getAttribute('href')").await.unwrap();
assert_eq!(boxes, serde_json::json!("/flow?open="), "the boxes link to the machines, level 1");
owner.click("[data-overview=boxes] a.card-more").await;
owner.until("level 1 from the boxes", &format!("location.pathname === '/flow' && {LEVEL} === '1'")).await;
owner.open(&rig.at("/overview")).await.unwrap();
owner.click("[data-overview=flow-link]").await;
owner.until("level 2 from Open the flow", &format!("location.pathname === '/flow' && {LEVEL} === '2'")).await;
owner.assert_no_link_to(NEVER).await.unwrap();
}