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/flow_front_and_links.rs

//! ★ **The front unfolds, and another Holger leads to its own flow** (HOL-FLOW-FRONT,
//! Henrik's Q1 and Q4 of HOL-FLOW-SMART), in a real browser against a real holger.
//!
//! * **Q1.** The front reports its doors and the names it serves to main (holger
//!   `front_report`); the console's Flow page then unfolds the front at level 2 (a click on the
//!   front, holger's link over the image) and at level 3 (`open=all`), with what it reported
//!   inside its border. Before its report the front is one closed box with nothing to click.
//! * **Q4.** A downstream that reported its console's address carries a link to THAT server's
//!   own flow: a person's press on it lands on the other console's `/flow`. A downstream with
//!   no known console has no such link.
//!
//! The console stays thin: it lays holger's links over holger's image, and decides nothing.
//! Its own test binary: [`Rig::pair_with_front`] is process-wide.
//!
//! With `SHOTS_DIR` set, the camera keeps a picture of each step (`SHOTS_TAG`, default `now`).

use chromiumoxide::cdp::browser_protocol::emulation::SetDeviceMetricsOverrideParams;
use chromiumoxide::page::ScreenshotParams;
use holger_ui_browser_tests::browser::{Browser, Tab};
use holger_ui_browser_tests::rig::{Rig, CONSOLE, FRONT_REPO_HOST, GRAPH_DOWNSTREAM};

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();
    owner.until("the register button", "!!document.getElementById('register')").await;
    owner.js("document.getElementById('reg').open = true").await.unwrap();
    owner.fill("#user", "owner").await;
    owner.click("#register").await;
    owner.until("registered", holger_ui_browser_tests::browser::SIGNED_IN_OR_REGISTERED).await;
    if !owner.signed_in().await {
        owner.press("#login").await;
        owner.until("holger's session in this tab", holger_ui_browser_tests::browser::SIGNED_IN).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] : ''; })()";

/// The drawing the page shows, decoded from its image.
async fn drawn(tab: &Tab) -> String {
    tab.until("the drawing", "!!document.querySelector('[data-board=estate] img.board-img')").await;
    tab.js("(() => { const s = document.querySelector('[data-board=estate] img.board-img').src; return decodeURIComponent(s.slice(s.indexOf(',') + 1)); })()")
        .await
        .unwrap()
        .as_str()
        .unwrap()
        .to_owned()
}

/// A selector for holger's link over the image to `href`.
fn hit(href: &str) -> String {
    format!("[data-board=estate] a.board-hit[href=\"{href}\"]")
}

/// The camera: one picture of the page as it is, when `SHOTS_DIR` is set.
async fn shot(tab: &Tab, name: &str) {
    let Ok(dir) = std::env::var("SHOTS_DIR") else {
        return;
    };
    let dir = std::path::PathBuf::from(dir).join(std::env::var("SHOTS_TAG").unwrap_or_else(|_| "now".into()));
    std::fs::create_dir_all(&dir).unwrap();
    // The whole drawing in the picture: a wide window, and the Flow's scroll frame let out.
    tab.page.execute(SetDeviceMetricsOverrideParams::new(2400, 1600, 1.0, false)).await.unwrap();
    let _ = tab.js("(() => { const s = document.createElement('style'); s.textContent = '.board-scroll{max-height:none!important;overflow:visible!important}'; document.head.appendChild(s); return true; })()").await;
    tokio::time::sleep(std::time::Duration::from_millis(600)).await;
    let png = tab.page.screenshot(ScreenshotParams::builder().full_page(true).build()).await.expect("a picture");
    tab.page.execute(SetDeviceMetricsOverrideParams::new(1600, 1000, 1.0, false)).await.unwrap();
    std::fs::write(dir.join(format!("{name}.png")), png).unwrap();
}

/// **The other server's console**, on loopback: its own Flow page, so a press on the link
/// lands somewhere that says whose flow it is.
async fn other_console() -> String {
    let app = axum::Router::new().route(
        "/lab/flow",
        axum::routing::get(|| async { axum::response::Html("<!doctype html><title>Flow</title><h1>lab-holger's own flow</h1>") }),
    );
    let l = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
    let at = l.local_addr().unwrap();
    tokio::spawn(async move { axum::serve(l, app).await });
    format!("http://127.0.0.1:{}/lab", at.port())
}

#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn the_front_unfolds_at_levels_two_and_three_and_a_downstream_leads_to_its_own_flow() {
    let rig = Rig::start_with_graph().await;
    rig.pair_with_front();
    let browser = Browser::launch().await.expect("chromium starts");
    let owner = owner_of(&rig, &browser).await;
    owner.page.execute(SetDeviceMetricsOverrideParams::new(1600, 1000, 1.0, false)).await.unwrap();
    let down = format!("downstream:{GRAPH_DOWNSTREAM}");

    // ── before any report: the front is one closed box, nothing to click on it ──
    owner.open(&rig.at("/flow?open=")).await.unwrap();
    assert_eq!(owner.until("level 1", LEVEL).await, serde_json::json!("1"));
    let svg = drawn(&owner).await;
    assert!(svg.contains(r#"data-node="estate:front""#), "the pairing names a front: it is drawn");
    owner.until("main's link over the image", &format!("!!document.querySelector('{}')", hit("flow?open=main").replace('\'', "\\'"))).await;
    let none = owner.js(&format!("document.querySelectorAll('{}').length", hit("flow?open=front").replace('\'', "\\'"))).await.unwrap();
    assert_eq!(none, serde_json::json!(0), "a front that never reported has nothing to unfold");
    let out = owner.js("[...document.querySelectorAll('[data-board=estate] a.board-hit')].filter(a => !a.getAttribute('href').startsWith('flow?')).length").await.unwrap();
    assert_eq!(out, serde_json::json!(0), "no console known for any server: no link out");
    shot(&owner, "1-level-1-no-reports").await;
    owner.open(&rig.at("/flow?open=all")).await.unwrap();
    owner.until("level 3", &format!("{LEVEL} === '3'")).await;
    shot(&owner, "2-level-3-no-reports").await;

    // ── the front reports its doors and routes; the downstream its console ──
    let kept = rig.front_reports();
    let lab = other_console().await;
    rig.downstream_reports_console(&lab);
    let own_flow = format!("{lab}/flow");
    // The pictures first, then what they must show.
    owner.open(&rig.at("/flow?open=")).await.unwrap();
    owner.until("level 1", &format!("{LEVEL} === '1'")).await;
    shot(&owner, "3-level-1-reported").await;
    owner.open(&rig.at("/flow?open=all")).await.unwrap();
    owner.until("level 3", &format!("{LEVEL} === '3'")).await;
    shot(&owner, "4-level-3-reported").await;
    assert_eq!(kept, 200, "holger keeps its front's report");

    // Level 1: the front now unfolds; a click on it is level 2, the front unfolded in place.
    owner.open(&rig.at("/flow?open=")).await.unwrap();
    owner.until("level 1", &format!("{LEVEL} === '1'")).await;
    owner.click(&hit("flow?open=front")).await;
    owner.until("the Flow, the front unfolded", "location.pathname === '/flow' && location.search === '?open=front'").await;
    owner.until("level 2", &format!("{LEVEL} === '2'")).await;
    let svg = drawn(&owner).await;
    for part in ["estate:front:door:https", "estate:front:door:http", &format!("estate:front:host:{FRONT_REPO_HOST}"), "estate:front:host:www.holger.test"] {
        assert!(svg.contains(&format!(r#"data-node="{part}""#)), "level 2 shows the front's {part}");
    }
    assert!(!svg.contains(r#"data-node="door:web""#) && !svg.contains(r#"data-node="incoming""#), "main stays folded");
    let (_, model) = rig.console_get("/-/flow.json?open=front");
    let model: serde_json::Value = serde_json::from_str(&model).unwrap();
    for n in model["nodes"].as_array().unwrap() {
        let id = n["id"].as_str().unwrap();
        assert!(svg.contains(&format!(r#"data-node="{id}""#)), "the console shows holger's node {id}, as holger drew it");
    }
    shot(&owner, "5-level-2-front-unfolded").await;
    // The front folds again from its own head.
    owner.until("the front's fold link", &format!("!!document.querySelector('{}')", hit("flow?open=").replace('\'', "\\'"))).await;

    // Level 3: everything, the front's parts and main's.
    owner.open(&rig.at("/flow?open=all")).await.unwrap();
    owner.until("level 3", &format!("{LEVEL} === '3'")).await;
    let svg = drawn(&owner).await;
    assert!(svg.contains(&format!(r#"data-node="estate:front:host:{FRONT_REPO_HOST}""#)) && svg.contains(r#"data-node="incoming""#), "level 3 shows the front's parts and main's");

    // ── Q4: the downstream leads to its own flow, at its console ──
    owner.open(&rig.at("/flow?open=")).await.unwrap();
    owner.until("level 1", &format!("{LEVEL} === '1'")).await;
    let link = hit(&own_flow);
    owner.until("the downstream's own-flow link over the image", &format!("!!document.querySelector('{}')", link.replace('\'', "\\'"))).await;
    let label = owner.js(&format!("document.querySelector('{}').getAttribute('aria-label')", link.replace('\'', "\\'"))).await.unwrap();
    assert_eq!(label, serde_json::json!(format!("open {GRAPH_DOWNSTREAM}'s own flow at its console")));
    let svg = drawn(&owner).await;
    assert!(svg.contains(&format!(r#"data-own-flow="{down}""#)), "holger drew it");
    // On top: the point a person presses is the link out, not the box's unfold.
    let top = owner
        .js(&format!("(() => {{ const a = document.querySelector('{}'); a.scrollIntoView({{ block: 'center', inline: 'center' }}); const r = a.getBoundingClientRect(); const e = document.elementFromPoint(r.left + r.width / 2, r.top + r.height / 2); return e === a; }})()", link.replace('\'', "\\'")))
        .await
        .unwrap();
    assert_eq!(top, serde_json::json!(true), "the link out lies on top of the box");
    shot(&owner, "6-level-1-own-flow-link").await;
    owner.press(&link).await;
    owner.until_url("the other server's own flow", &own_flow).await;
    owner.until("its page", "document.body.innerText.includes(\"lab-holger's own flow\")").await;
    shot(&owner, "7-the-other-servers-flow").await;
}