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

nordisk/gunnar-ui

crates/gunnar-ui-server/src/passkeys_at_gunnar/first_touch/encryption_info.rs

//! ★ **UI-ENC (owner 2026-10-26): the info box beside the repository encryption switch**, in a
//! real browser. The console's own stylesheet (`gunnar-ui-web/styles/console.css`) and the box's
//! own markup (`gunnar-ui-web/src/encryption_info.html`, the ONE place its words live: the
//! console draws exactly that file beside the switch) on a page in headless Chromium, driven
//! as a person drives it: a mouse over the "i", the keyboard's Tab onto it, a finger's tap.
//! Each shows the box with exactly the owner's words; away, it is gone. Needs `CHROME`.

use std::time::Duration;

use serde_json::json;

use super::{chrome, Chromium};

const CSS: &str = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../gunnar-ui-web/styles/console.css"));
const MARKUP: &str = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../gunnar-ui-web/src/encryption_info.html"));

/// The owner's words, exactly (2026-10-26).
const WORDS: &str = "Encryption keeps this repository unreadable to anyone but its members, including the server operator, the cloud and backups. Files are sealed on your computer before push; open it with your security key or the Gunnar CLI. Best on private repositories: on a public one everyone here can fetch the ciphertext. A new encrypted repository is quantum-safe: a copy taken today stays closed after quantum computers arrive.";

/// Whether the box is laid out on the screen, as a person sees it.
const BOX_SHOWN: &str = "(() => { const b = document.getElementById('enc-info'); return !!b && getComputedStyle(b).visibility !== 'hidden' && b.getClientRects().length > 0 && b.getBoundingClientRect().width > 0; })()";

fn page() -> String {
    format!(
        "<!doctype html><html><head><meta charset=\"utf-8\"><style>{CSS}</style></head><body><main class=\"page\">\
         <p id=\"away\" style=\"margin:4rem\">elsewhere</p>\
         <div class=\"field\"><label for=\"enc\"><input id=\"enc\" type=\"checkbox\"> encrypted</label> <span class=\"infotip\">{MARKUP}</span></div>\
         <p style=\"height:40rem\"></p></main></body></html>"
    )
}

fn open(c: &Chromium) {
    c.open("about:blank");
    c.until("a blank page", "document.readyState === 'complete'");
    let frame = c.on_page("Page.getFrameTree", json!({}))["frameTree"]["frame"]["id"].as_str().expect("the frame").to_owned();
    c.on_page("Page.setDocumentContent", json!({ "frameId": frame, "html": page() }));
    c.until("the switch and its info", "!!document.querySelector('.infotip-btn')");
}

fn middle(c: &Chromium, selector: &str) -> (f64, f64) {
    let sel = serde_json::to_string(selector).expect("json");
    let at = c.js(&format!("(() => {{ const r = document.querySelector({sel}).getBoundingClientRect(); return [r.x + r.width / 2, r.y + r.height / 2]; }})()"));
    (at[0].as_f64().expect("x"), at[1].as_f64().expect("y"))
}

fn mouse_to(c: &Chromium, (x, y): (f64, f64)) {
    c.on_page("Input.dispatchMouseEvent", json!({ "type": "mouseMoved", "x": x, "y": y }));
}

fn the_box_says_the_words(c: &Chromium, how: &str) {
    c.until(&format!("{how}: the box shows"), BOX_SHOWN);
    assert_eq!(c.js("document.getElementById('enc-info').textContent"), json!(WORDS), "{how}: the owner's words, exactly");
    assert_eq!(c.js("document.querySelector('.infotip-btn').getAttribute('aria-describedby')"), json!("enc-info"), "{how}: the button names the box");
    assert_eq!(c.js("document.getElementById('enc-info').getAttribute('role')"), json!("tooltip"));
}

fn the_box_is_gone(c: &Chromium, how: &str) {
    assert!(c.holds(&format!("!{BOX_SHOWN}"), Duration::from_secs(5)), "{how}: the box is still on the screen");
}

#[test]
fn the_encryption_info_shows_on_hover_on_keyboard_focus_and_on_tap() {
    let Some(chrome) = chrome() else { return };
    let c = Chromium::launch(&chrome);
    open(&c);
    // Nothing asked: no box.
    the_box_is_gone(&c, "nobody asked");

    // Hover.
    mouse_to(&c, middle(&c, ".infotip-btn"));
    the_box_says_the_words(&c, "hover");
    mouse_to(&c, middle(&c, "#away"));
    the_box_is_gone(&c, "the mouse went away");

    // Keyboard: Tab from the checkbox lands on the "i" (a real button, in the tab order).
    c.js("document.getElementById('enc').focus(); true");
    c.on_page("Input.dispatchKeyEvent", json!({ "type": "keyDown", "key": "Tab", "code": "Tab", "windowsVirtualKeyCode": 9 }));
    c.on_page("Input.dispatchKeyEvent", json!({ "type": "keyUp", "key": "Tab", "code": "Tab", "windowsVirtualKeyCode": 9 }));
    c.until("Tab reaches the info button", "document.activeElement && document.activeElement.classList.contains('infotip-btn')");
    the_box_says_the_words(&c, "keyboard focus");
    c.js("document.activeElement.blur(); true");
    the_box_is_gone(&c, "focus left");

    // Touch: a tap on the "i".
    c.on_page("Emulation.setTouchEmulationEnabled", json!({ "enabled": true, "maxTouchPoints": 1 }));
    let (x, y) = middle(&c, ".infotip-btn");
    c.on_page("Input.dispatchTouchEvent", json!({ "type": "touchStart", "touchPoints": [{ "x": x, "y": y }] }));
    c.on_page("Input.dispatchTouchEvent", json!({ "type": "touchEnd", "touchPoints": [] }));
    the_box_says_the_words(&c, "tap");
}