browser-tests/tests/link_prefill.rs
//! ★ **The mailed sign-in link goes straight to "make your passkey"** (HOL-LINK-PREFILL, the owner
//! on holger.rs 2026-10-30). `site first-link holger-rs` had the main mail
//! `https://holger.rs/login?invite=<secret>&user=ignalina` (monetize-estate-side
//! `sign_in::link`, the shape of the shop's first-login link). The page read the invite, but its
//! "Break-glass admin token" and "Role" fields stayed on screen, empty, under "Register your
//! security key": `hidden` set by the script lost to the fields' own `display: flex`. The owner
//! took the empty token field for the place the key goes, and cut the key out of the link to
//! paste it there. Gunnar's page shows no such field for a link.
//!
//! What must hold for a link in the exact mailed shape: the invite and the name are taken out of
//! the address bar, the page offers only the passkey step for the named account (no token, no
//! role, no name to type), and one press registers the passkey and signs the person in.
use std::time::Duration;
use holger_ui_browser_tests::browser::{Browser, NEVER, SIGNED_IN};
use holger_ui_browser_tests::rig::Rig;
use monetize_site_buy::testkit::STRIPE;
/// Whether the element `#id` takes room on screen (laid out, not `display: none`), whatever its
/// `hidden` property says.
fn on_screen(id: &str) -> String {
format!("(() => {{ const e = document.getElementById({id:?}); if (!e) return false; const r = e.getBoundingClientRect(); return getComputedStyle(e).display !== 'none' && r.width > 0 && r.height > 0; }})()")
}
/// **The link the main mails**, as a path on this rig: monetize-estate-side's own `link`, its
/// host cut off (the rig serves plain `http://localhost:<port>`).
fn mailed_path(secret: &str, account: &str) -> String {
let link = monetize_estate_side::sign_in::link("holger.mock.invalid", secret, account);
let path = link.strip_prefix("https://holger.mock.invalid").expect("the mailed link names its host").to_owned();
assert!(path.starts_with("/login?invite=") && path.ends_with(&format!("&user={account}")), "the mailed shape changed: {link}");
path
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn the_mailed_sign_in_link_fills_in_the_invite_and_goes_straight_to_making_the_passkey() {
let rig = Rig::start().await;
let browser = Browser::launch().await.expect("chromium starts");
let tab = browser.tab().await.unwrap();
tab.watch_ceremonies().await.unwrap();
let secret = rig.invite("ignalina", "holger-writers");
tab.open(&rig.at(&mailed_path(&secret, "ignalina"))).await.unwrap();
tab.until("the register panel", "document.getElementById('register').textContent === 'Touch your security key'").await;
// The secret and the name have left the address bar (and so the history).
let url = tab.url().await;
assert!(!url.contains(&secret) && !url.contains("invite=") && !url.contains("user="), "the link stayed in the address bar: {url}");
assert_eq!(url, rig.at("/login"), "only the page is left in the address bar");
// The page says whose account it is, and offers the passkey step only.
let said = tab.text().await;
assert!(said.contains("Your account ignalina is ready. Touch your security key to finish."), "{said}");
assert_eq!(tab.js("document.getElementById('user').value").await.unwrap(), "ignalina", "the link's name is the registration's name");
for (id, what) in [("token-row", "the break-glass admin token"), ("role-row", "the role"), ("user-row", "a name to type"), ("login", "a second Log in")] {
assert_eq!(tab.js(&on_screen(id)).await.unwrap(), false, "a link's panel shows {what} (#{id}):\n{said}");
}
assert_eq!(tab.js(&on_screen("register")).await.unwrap(), true, "the passkey button is on screen:\n{said}");
// One press registers the passkey and signs the person in.
tab.press("#register").await;
let mut signed_in = false;
for _ in 0..60 {
if tab.js(SIGNED_IN).await.ok().and_then(|v| v.as_bool()).unwrap_or(false) {
signed_in = true;
break;
}
tokio::time::sleep(Duration::from_millis(500)).await;
}
let ceremonies = tab.ceremonies().await;
assert!(signed_in, "the mailed link's press did not sign the person in: {ceremonies:?}\n{}", tab.text().await);
assert_eq!(ceremonies.iter().filter(|c| c["kind"] == "create").count(), 1, "one registration: {ceremonies:?}");
for c in &ceremonies {
assert_eq!(c["gesture"], true, "a ceremony started without the person's press: {c}");
assert_eq!(c["outcome"], "ok", "{c}");
}
browser.assert_nothing_went_to(NEVER, &rig.base, STRIPE);
}