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

nordisk/holger-ui

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();
}