browser-tests/tests/first_touch.rs
//! ★ **The first touch after paying** (A29-20; the owner, 2026-10-22: "passkey request was
//! cancelled … Payment received — your account x14 is ready"). A visitor pays for a plan at the
//! (fake) vendor, comes back to the site's welcome page, follows "Register your security key now", and
//! presses "Touch your security key" — a real press, so the page holds the person's gesture.
//! Right after paying the registration's start waits on the box (the rig holds it back 6 s,
//! longer than Chromium keeps a press live).
//!
//! The ceremony watch ([`holger_ui_browser_tests::browser::CEREMONY_WATCH`]) writes down every
//! passkey ceremony the pages start, and refuses one started without a live gesture as Safari
//! does. What must hold: exactly one registration, started inside the person's press, with no
//! other ceremony running and no navigation while it ran; the passkey is registered and the
//! person is signed in at once; the page never shows a browser's own words.
use std::time::Duration;
use holger_ui_browser_tests::browser::{Browser, NEVER};
use holger_ui_browser_tests::rig::Rig;
use monetize_site_buy::testkit::{PAID_WORDS, STRIPE};
/// What the welcome page said, every change of its detail line, from the moment it is
/// installed (`window.__said`, `<detail>|<class>`).
const SAID_WATCH: &str = "window.__said = window.__said || []; setInterval(() => { const d = document.getElementById('detail'); if (!d) return; const l = d.textContent + '|' + d.className; if (window.__said[window.__said.length - 1] !== l) window.__said.push(l); }, 50); true";
/// Longer than Chromium keeps a press live (5 s).
const SLOW_START: Duration = Duration::from_secs(6);
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn the_first_touch_after_paying_registers_the_passkey_inside_the_press_and_signs_in() {
let rig = Rig::start().await;
let browser = Browser::launch().await.expect("chromium starts");
let tab = browser.tab().await.unwrap();
tab.watch_ceremonies().await.unwrap();
// ── a visitor buys a plan and pays at the vendor ──
tab.open(&rig.at("/signup?plan=account")).await.unwrap();
tab.until("the plans", "document.querySelectorAll('#plans > *').length >= 2").await;
let paid = serde_json::to_string(PAID_WORDS).unwrap();
tab.js(&format!("[...document.querySelectorAll('#plans > *')].find(r => r.textContent.includes({paid})).click()")).await.unwrap();
tab.fill("#account", "x14").await;
tab.fill("#email", "x14@mock.invalid").await;
tab.press("#go").await;
let checkout = tab.until_url("the plan goes to the vendor", STRIPE).await;
let (session, back) = {
let b = rig.fake.book();
let c = b.checkout(&checkout).expect("the checkout the page went to").clone();
(c.id.clone(), c.return_url())
};
assert!(rig.fake.pay(&checkout), "the vendor's paid event");
rig.the_box_makes_the_account("x14", &session);
// ★ PAID-WORDS (owner's real-card test 2026-10-26): the account settles over a few polls,
// and the box misses one of them; the page reads "Setting up your account", never
// maintenance and never an error.
rig.fake.settle_after(3);
rig.fake.status_fails_at(&[2]);
tab.open(&rig.back(&back)).await.unwrap();
tab.js(SAID_WATCH).await.unwrap();
// ── "Payment received", and the passkey step ──
tab.until("the welcome page says the account is ready", "document.getElementById('headline').textContent === 'Your account is ready'").await;
assert!(tab.text().await.contains("Payment received"), "{}", tab.text().await);
let said = tab.js("window.__said").await.unwrap();
let lines: Vec<String> = said.as_array().into_iter().flatten().filter_map(|l| l.as_str()).map(str::to_owned).collect();
assert!(lines.iter().any(|l| l.contains("Payment received. Setting up your account; this takes a few seconds.")), "the settle in the buyer's words: {lines:?}");
assert!(lines.iter().all(|l| !l.to_lowercase().contains("maintenance") && !l.ends_with("|err")), "never maintenance, never an error while it settles: {lines:?}");
// PAID-2: the button says what it does, with one line under it.
assert!(tab.text().await.contains("Or open the link we sent to your e-mail."), "{}", tab.text().await);
rig.slow_register_start(SLOW_START);
tab.press_text("Register your security key now").await;
tab.until_url("the passkey step", &rig.at("/login")).await;
tab.until("the register panel", "document.getElementById('register').textContent === 'Touch your security key'").await;
assert!(tab.text().await.contains("Your account x14 is ready"), "{}", tab.text().await);
// ── the first touch ──
// The person presses once the button is offered (it is enabled when it can be pressed).
tokio::time::sleep(SLOW_START + Duration::from_secs(1)).await;
tab.press("#register").await;
let signed_in = wait_for(&tab, holger_ui_browser_tests::browser::SIGNED_IN, Duration::from_secs(30)).await;
let ceremonies = tab.ceremonies().await;
let said = tab.text().await;
assert!(
signed_in,
"the first touch after paying did not sign the person in.\nceremonies: {ceremonies:?}\n--- the page says ---\n{said}"
);
let creates: Vec<_> = ceremonies.iter().filter(|c| c["kind"] == "create").collect();
assert_eq!(creates.len(), 1, "one registration: {ceremonies:?}");
for c in &ceremonies {
assert_eq!(c["gesture"], true, "a ceremony started without the person's press: {c} (all: {ceremonies:?})");
assert_eq!(c["overlap"], false, "two ceremonies at once: {c}");
assert_eq!(c["navigated"], false, "the page navigated while a ceremony ran: {c}");
assert_eq!(c["outcome"], "ok", "{c}");
}
assert!(rig.holger_root().join(holger_server_lib::sold::SOLD_REPOS_REL).is_file());
let bound = holger_server_lib::sold::read(&rig.holger_root());
assert!(!bound["x14"].owner.is_empty(), "the passkey is bound to the paid account: {:?}", bound["x14"]);
browser.assert_nothing_went_to(NEVER, &rig.base, STRIPE);
}
/// ★ **A press the page cannot answer at once is not wasted** (A29-20): the start is still on
/// its way when the person presses (held back past the gesture's life), so the ceremony is not
/// started without a gesture; the page says to press again, and the second press registers.
/// A ceremony the browser refuses is said in the page's own words, never the browser's.
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn the_first_touch_pressed_before_the_start_answers_is_asked_again_never_a_ceremony_without_a_gesture() {
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("dana", "holger-writers");
rig.slow_register_start(SLOW_START);
tab.open(&rig.at(&format!("/login?invite={secret}"))).await.unwrap();
tab.until("the register panel", "document.getElementById('register').textContent === 'Touch your security key'").await;
// Pressed while the start is still on its way (the button may wait for it).
tab.js("document.getElementById('register').click(); true").await.unwrap();
tokio::time::sleep(SLOW_START + Duration::from_secs(1)).await;
let ceremonies = tab.ceremonies().await;
assert!(ceremonies.iter().all(|c| c["gesture"] == true), "a ceremony without a gesture: {ceremonies:?}\n{}", tab.text().await);
rig.slow_register_start(Duration::ZERO);
tab.press("#register").await;
let signed_in = wait_for(&tab, holger_ui_browser_tests::browser::SIGNED_IN, Duration::from_secs(30)).await;
let ceremonies = tab.ceremonies().await;
assert!(signed_in, "the second press registers: {ceremonies:?}\n{}", tab.text().await);
assert_eq!(ceremonies.iter().filter(|c| c["kind"] == "create").count(), 1, "{ceremonies:?}");
let said = tab.text().await;
assert!(!said.contains("not allowed by the user agent"), "the browser's own words: {said}");
}
/// ★ RA2-6: **the plain Log in starts its passkey inside the press.** A person who holds a
/// passkey comes back signed out and presses Log in while the box is slow to hand out the start
/// (held back past a press's life). The start is asked as the person reaches for the button,
/// never on a bare view; a press that finds it still on its way asks for one more press instead
/// of starting a ceremony nobody pressed for, and the next press signs in.
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn the_plain_log_in_starts_its_passkey_inside_the_press() {
let rig = Rig::start().await;
let browser = Browser::launch().await.expect("chromium starts");
let tab = browser.tab().await.unwrap();
tab.watch_ceremonies().await.unwrap();
// A person with a passkey: registered from an invite, then signed out.
let secret = rig.invite("dana", "holger-writers");
tab.open(&rig.at(&format!("/login?invite={secret}"))).await.unwrap();
tab.until("the register panel", "document.getElementById('register').textContent === 'Touch your security key' && !document.getElementById('register').disabled").await;
tab.press("#register").await;
assert!(wait_for(&tab, holger_ui_browser_tests::browser::SIGNED_IN, Duration::from_secs(30)).await, "registered and signed in: {}", tab.text().await);
let before = tab.ceremonies().await.len();
tab.js(holger_ui_browser_tests::browser::SIGN_OUT_TAB).await.unwrap();
tab.open(&rig.at("/login")).await.unwrap();
tab.until("the Log in button", "!!document.getElementById('login') && document.getElementById('login').offsetParent !== null").await;
tokio::time::sleep(Duration::from_millis(500)).await;
rig.slow_login_start(SLOW_START);
tab.hover("#login").await;
tokio::time::sleep(Duration::from_millis(200)).await;
tab.press("#login").await;
let again = wait_for(&tab, "document.body.innerText.includes('Ready. Touch your security key again.')", SLOW_START + Duration::from_secs(8)).await;
let now: Vec<_> = tab.ceremonies().await.into_iter().skip(before).collect();
let said = tab.text().await;
assert!(now.iter().all(|c| c["gesture"] == true), "a sign-in ceremony without the person's press: {now:?}\n--- the page says ---\n{said}");
assert!(again, "the slow start did not ask for one more press: {now:?}\n--- the page says ---\n{said}");
tab.press("#login").await;
let signed_in = wait_for(&tab, holger_ui_browser_tests::browser::SIGNED_IN, Duration::from_secs(30)).await;
let now: Vec<_> = tab.ceremonies().await.into_iter().skip(before).collect();
assert!(signed_in, "the second press signs in: {now:?}\n{}", tab.text().await);
assert_eq!(now.iter().filter(|c| c["kind"] == "get").count(), 1, "one sign-in ceremony: {now:?}");
assert!(now.iter().all(|c| c["gesture"] == true && c["outcome"] == "ok"), "{now:?}");
}
/// ★ RA2-17: **a name typed into a link that names nobody asks the start again, ahead of the
/// press.** The start is asked when the panel opens, then again once a field it names (here the
/// user name) is left; so a slow box answers that start while the person reads, and the one
/// press registers at once, inside the gesture. Asked only at the press, the slow start would
/// outlive it and the press would have to be repeated.
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn a_name_typed_into_the_link_asks_the_start_again_before_the_press() {
let rig = Rig::start().await;
let browser = Browser::launch().await.expect("chromium starts");
let tab = browser.tab().await.unwrap();
tab.watch_ceremonies().await.unwrap();
// A link that names nobody: the person chooses the name.
let secret = rig.invite("", "holger-writers");
tab.open(&rig.at(&format!("/login?invite={secret}"))).await.unwrap();
tab.until("the register panel, its start answered", "document.getElementById('register').textContent === 'Touch your security key' && !document.getElementById('register').disabled").await;
rig.slow_register_start(SLOW_START);
tab.fill("#user", "erik").await;
// The person reads the line; the start asked when the field was left is answered meanwhile.
tokio::time::sleep(SLOW_START + Duration::from_secs(1)).await;
tab.press("#register").await;
let signed_in = wait_for(&tab, holger_ui_browser_tests::browser::SIGNED_IN, Duration::from_secs(20)).await;
let ceremonies = tab.ceremonies().await;
let said = tab.text().await;
assert!(signed_in, "one press after typing the name did not register: {ceremonies:?}\n--- the page says ---\n{said}");
assert_eq!(ceremonies.iter().filter(|c| c["kind"] == "create").count(), 1, "one registration: {ceremonies:?}");
assert!(ceremonies.iter().all(|c| c["gesture"] == true && c["outcome"] == "ok"), "{ceremonies:?}");
}
async fn wait_for(tab: &holger_ui_browser_tests::browser::Tab, js: &str, within: Duration) -> bool {
let end = std::time::Instant::now() + within;
while std::time::Instant::now() < end {
if tab.js(js).await.ok().is_some_and(|v| v == serde_json::Value::Bool(true)) {
return true;
}
tokio::time::sleep(Duration::from_millis(100)).await;
}
false
}