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