uitest/src/oneshop.rs
//! **One shop** (owner, 2026-10-07): only gunnar.rs's monetize sells. A SOLD
//! (provided) Gunnar — a private estate gunnar.rs laid — is bought and grown
//! at its provider; its own monetize only executes, and its console offers no
//! purchase of its own.
//!
//! The rig runs a second console for it ([`Rig::start_provided`]): the same
//! gunnar and the same bench monetize, told it is acme's, sold by
//! `https://gunnar.rs` (`GUNNAR_UI_PROVIDER`), as the kit feeds a laid
//! estate's front. The rig's monetize still sells (it is the rig's one), so
//! what a sold estate's own monetize holds — no shop — is the kit's unit tests
//! and its functional test (`functest::sold_window`); these rows hold the
//! CONSOLE: no account door, no sign-up page, no route that starts an order,
//! and on the front page "New instance" at the provider.
//!
//! Pictures go to `<work>/shots/oneshop/`.
use crate::cdp::Cdp;
use crate::http;
use crate::json::esc;
use crate::matrix::{go, want, Row};
use crate::rig::Rig;
const NAMES: [&str; 3] = [
"one-shop-a-sold-gunnar-names-no-account-door",
"one-shop-a-sold-gunnar-starts-no-order",
"one-shop-a-sold-gunnars-front-page-leads-to-its-providers-sign-up",
];
/// One shop, items 2 and 3 (owner 2026-10-07): on the rig's own console, which
/// sells — a visitor buying a private Gunnar first creates their account here
/// in ONE round trip, and that account's Storage names what it bought instead
/// of a room of nothing.
const SHOP_NAMES: [&str; 2] = [
"one-shop-a-visitor-creates-their-account-here-and-comes-back-to-the-private-order",
"one-shop-an-account-here-without-room-shows-no-room-of-nothing",
];
/// The visitor's account name and address (example.com: never a real one).
const ALMA: &str = "alma";
pub fn shop_rows(r: &Rig, rows: &mut Vec<Row>, c: &mut Cdp, boxadmin: &str) {
if r.bench_control.is_none() {
for name in SHOP_NAMES {
go(rows, name, || Err("NOT RUN: no --bench-monetize (monetize makes the account)".into()));
}
return;
}
let o = r.origin();
let dir = r.work.join("shots").join("oneshop");
let _ = std::fs::create_dir_all(&dir);
let mut alma = String::new();
go(rows, SHOP_NAMES[0], || {
let sess = c.isolated_page()?;
let auth = c.add_authenticator(&sess)?;
c.viewport(&sess, 1440, 900)?;
c.navigate(&sess, &format!("{o}/signup?plan=private"))?;
c.wait_for(&sess, "!document.getElementById('first-account').hidden", 20_000)
.map_err(|_| "the private choice never offered a visitor their account first".to_owned())?;
let said = c.eval_str(&sess, "document.getElementById('first-account').textContent + ' | ' + document.getElementById('go').textContent")?;
want(said.contains("is where you see and grow what you bought") && said.ends_with("| Create your account at localhost"), || format!("the first step says {said:?}"))?;
want(!said.to_lowercase().contains("free"), || format!("{said:?}"))?;
c.screenshot(&sess, &dir.join("2-create-your-account-here.png"))?;
c.eval(&sess, &format!(
"(() => {{ const set = (id, v) => {{ const i = document.getElementById(id); Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value').set.call(i, v); i.dispatchEvent(new Event('input', {{bubbles: true}})); }}; set('account', {}); set('email', {}); document.getElementById('go').click(); return 1; }})()",
esc(ALMA),
esc("alma@example.com")
))?;
// The account is made at once, and the browser follows THIS console's own welcome
// page, carrying where to come back to.
c.wait_for(&sess, "location.pathname === '/welcome'", 60_000)
.map_err(|_| format!("the account was not made: the page says {}", http::cut(&c.eval_str(&sess, "document.body.innerText").unwrap_or_default(), 300)))?;
let search = c.eval_str(&sess, "location.search")?;
let params: Vec<(String, String)> = search.trim_start_matches('?').split('&').filter_map(|kv| kv.split_once('=')).map(|(k, v)| (k.to_owned(), v.to_owned())).collect();
let next = params.iter().find(|(k, _)| k == "next").map(|(_, v)| v.clone()).unwrap_or_default();
want(next == "%2Fsignup%3Fplan%3Dprivate" || next == "/signup?plan=private", || format!("the welcome page is not told to come back to the private order: {search}"))?;
// monetize says the account is made (with the open sink there is no principal at
// Gunnar yet, so the page waits for it: "monetize says the account … exists").
c.wait_for(&sess, "document.body.innerText.includes('Your account is ready') || document.body.innerText.includes('says the account')", 30_000)
.map_err(|_| format!("monetize did not say the account is made: {}", http::cut(&c.eval_str(&sess, "document.body.innerText").unwrap_or_default(), 300)))?;
// The bench's monetize runs the OPEN signup sink, which creates no principal at
// Gunnar (the shop's sink, monetize-signup-impl, does: its own tests). The rig
// stands in for it: the box admin's invite for alma, opened with where to come back.
let j = c.eval(
boxadmin,
&format!("fetch('/auth/passkey/invites', {{method:'POST', headers:{{'Content-Type':'application/json'}}, body: JSON.stringify({{user:{}, role:'viewer'}})}}).then(r => r.json())", esc(ALMA)),
)?;
let invite = j.text("url");
want(invite.contains("/login?invite="), || format!("no invite for alma: {}", http::cut(&j.dump(), 200)))?;
let path = invite.find("/login?").map(|i| invite[i..].to_owned()).unwrap_or(invite.clone());
c.navigate(&sess, &format!("{o}{path}&next=%2Fsignup%3Fplan%3Dprivate"))?;
c.wait_for(&sess, "!!document.getElementById('register')", 30_000).map_err(|_| "alma's registration never opened".to_owned())?;
c.eval(&sess, "(() => { document.getElementById('label').value = 'alma laptop'; document.getElementById('register').click(); return 1; })()")?;
c.wait_for(&sess, "document.getElementById('msg').classList.contains('ok') || document.getElementById('msg').classList.contains('err')", 30_000)?;
let registered = c.eval_str(&sess, "document.getElementById('msg').textContent")?;
want(registered.contains("registered"), || format!("the registration said {registered:?}"))?;
want(c.credentials(&sess, &auth)?.len() == 1, || "alma's authenticator holds no key".to_owned())?;
c.eval(&sess, "document.getElementById('login').click()")?;
// …and back on the private choice, signed in, the account locked.
c.wait_for(&sess, "location.pathname === '/signup' && document.getElementById('account').readOnly === true", 30_000)
.map_err(|_| "after the passkey the visitor did not come back to the private order".to_owned())?;
let back = c.eval_str(&sess, "document.getElementById('heading').textContent + ' | ' + document.getElementById('account').value + ' | ' + new URLSearchParams(location.search).get('plan')")?;
want(back == format!("Sign up for a new private Gunnar | {ALMA} | private"), || format!("back on {back:?}"))?;
c.screenshot(&sess, &dir.join("3-back-on-the-private-order-signed-in.png"))?;
alma = sess;
Ok(back)
});
go(rows, SHOP_NAMES[1], || {
want(!alma.is_empty(), || "the row before failed".to_owned())?;
// An account that holds only an instance is a FREE account here: its
// Plan reads "Free" and its ONE button "Buy a plan" (owner 2026-10-17).
c.navigate(&alma, &format!("{o}/account"))?;
c.wait_for(&alma, "!!document.getElementById('account-buy-grow')", 30_000)
.map_err(|_| "alma's Account drew no button".to_owned())?;
let plan = c.eval_str(&alma, "document.getElementById('account-plan').innerText")?;
want(plan.contains("Free") && plan.contains("Buy a plan"), || format!("alma's Plan: {}", http::cut(&plan, 300)))?;
for never in ["0 B", "0 GB"] {
want(!plan.contains(never), || format!("{never:?} on alma's Plan: {}", http::cut(&plan, 300)))?;
}
c.screenshot(&alma, &dir.join("4-account-of-an-account-without-room.png"))?;
Ok("no room of nothing; Buy a plan".into())
});
}
pub fn rows(r: &Rig, rows: &mut Vec<Row>, c: &mut Cdp) {
let (Some(at), Some(origin)) = (r.provided_addr(), r.provided_origin()) else {
for name in NAMES {
go(rows, name, || Err("NOT RUN: no sold Gunnar's console (it runs beside the bench monetize: --bench-monetize)".into()));
}
return;
};
let dir = r.work.join("shots").join("oneshop");
let _ = std::fs::create_dir_all(&dir);
go(rows, NAMES[0], || {
let s = http::req(&at, "GET", "/auth/status", &[], None)?;
want(s.status == 200, || format!("/auth/status answered {}", s.status))?;
let j = crate::json::parse(&s.body)?;
let text = |k: &str| j.path(k).and_then(|v| v.as_str().map(str::to_owned));
want(text("buy_url").is_none(), || format!("a sold Gunnar names an account door: {}", http::cut(&s.body, 300)))?;
want(text("private_url").as_deref() == Some("https://gunnar.rs/signup?plan=private"), || format!("New instance is not the provider's: {:?}", text("private_url")))?;
want(j.path("shop_here").and_then(|v| v.as_bool()) == Some(false), || "the console says it sells".to_owned())?;
let line = text("shop_line").unwrap_or_default();
want(line.contains("gunnar.rs"), || format!("no line naming the provider: {line:?}"))?;
// One shop 4: the admin page's "Grow this instance" is drawn from
// these two (gunnar-ui-web offers::grow_this_instance) and leads to
// <provider>/signup?plan=grow&instance=<name>.
want(text("provider").as_deref() == Some("https://gunnar.rs") && text("instance").as_deref() == Some("acme"), || format!("the console is not told its provider and its name: {:?} {:?}", text("provider"), text("instance")))?;
Ok(format!("no account door; New instance {}; {line:?}; grown at {}/signup?plan=grow&instance={}", text("private_url").unwrap_or_default(), text("provider").unwrap_or_default(), text("instance").unwrap_or_default()))
});
go(rows, NAMES[1], || {
let bearer = [("authorization", format!("Bearer {}", r.token))];
let mut said = Vec::new();
// The pages are not this console's: a GET is no sign-up page — 404, or the console
// app's own page (its `index.html`, which draws its not-found), never the shop's.
for path in ["/signup", "/welcome", "/private"] {
let resp = http::req(&at, "GET", path, &bearer, None)?;
let console = resp.status == 200 && resp.body.contains("<title>Gunnar</title>") && !resp.body.contains("sign up") && !resp.body.contains("first-account");
want(resp.status == 404 || console, || format!("GET {path} answered {} on a sold Gunnar: {}", resp.status, http::cut(&resp.body, 200)))?;
said.push(format!("GET {path} {}", if resp.status == 404 { "404".to_owned() } else { "the console's own page".to_owned() }));
}
for (method, path, absent) in [
("POST", "/api/account/subscription/renew", 404),
("POST", "/api/account/subscription/buy-bundle", 404),
("POST", "/api/namespaces/acme/increase/bundle", 404),
("POST", "/api/namespaces/acme/increase", 405),
] {
let body = (method == "POST").then_some("{}");
let resp = http::req(&at, method, path, &bearer, body)?;
// No such order route: 404, or 405 where the path answers another method only.
want(resp.status == absent || (resp.status == 404 || resp.status == 405), || format!("{method} {path} answered {} on a sold Gunnar: {}", resp.status, http::cut(&resp.body, 200)))?;
said.push(format!("{method} {path} {}", resp.status));
}
// The same door is open on the rig's own console, which sells: the
// 404s above are the sold console's, not the rig's.
let mine = http::req(&r.ui_addr(), "GET", "/signup", &bearer, None)?;
want(mine.status == 200, || format!("/signup on the console that sells answered {}", mine.status))?;
Ok(said.join(", "))
});
go(rows, NAMES[2], || {
let anon = c.isolated_page()?;
c.viewport(&anon, 1440, 900)?;
c.navigate(&anon, &format!("{origin}/login"))?;
// ONE sign-up button (owner 2026-10-15), "Sign up account" — on a
// sold Gunnar it leads to its provider's sign-up page.
c.wait_for(&anon, "!document.getElementById('paid-link').hidden", 20_000)
.map_err(|_| "the front page never showed its sign-up button".to_owned())?;
let shown = c.eval_str(&anon, "document.getElementById('paid-link').innerText.trim() + ' ' + document.getElementById('paid-link').getAttribute('href')")?;
want(shown == "Sign up account https://gunnar.rs/signup", || format!("the sold Gunnar's front page offers: {shown}"))?;
c.screenshot(&anon, &dir.join("1-sold-gunnar-front-page.png"))?;
Ok(shown)
});
}