crates/monetize-site-buy/tests/browser/page.html
<!doctype html>
<!-- **The buy page's markup, as the sites draw it** (Gunnar's `auth/signup.html` <main>, its
look left out): every element the shared `buy.js` reads, for the crate's browser test.
The `verdict` script reads what a buyer SEES (computed style, boxes), never the page's
own claim about itself. -->
<html lang="en"><head><meta charset="utf-8"><title>Sign up</title>
<style>
/* The sites' own layout rules that decide what a `hidden` element looks like (Gunnar's and
Holger's signup.html, PAID-3): an author `display` beats the browser's own `[hidden]`, so
a form drawn as a grid shows although it is `hidden`. */
form { display: grid; grid-template-columns: 1fr 1fr; gap: .2rem 1rem; margin: 0; }
.field[hidden] { display: none; }
</style></head>
<body>
<main class="entry">
<h1 id="heading">Sign up for an account</h1>
<!-- What this visitor may do here, as the server answered it. -->
<div class="choice" id="choice" role="group" aria-label="What to do" hidden></div>
<!-- The account's plans (owner 2026-10-15): drawn only when the account
is chosen; the server picture below only for a private Gunnar. -->
<div class="plans" id="plans" role="radiogroup" aria-label="Account plans" hidden></div>
<div id="state" role="status" aria-live="polite" hidden></div>
<form id="form" autocomplete="on" novalidate>
<p class="hint" id="first-account" hidden></p>
<div class="field" id="f-account">
<label for="account">Account name</label>
<input id="account" name="account" type="text" autocomplete="username" autocapitalize="none"
spellcheck="false" maxlength="32" placeholder="e.g. alice" required>
<p class="hint" id="account-hint"></p>
</div>
<div class="field" id="f-email">
<label for="email" id="email-label">E-mail for your welcome mail</label>
<input id="email" name="email" type="email" autocomplete="email" autocapitalize="none"
spellcheck="false" maxlength="254" placeholder="you@example.com" required>
</div>
<div class="field" id="f-company">
<label for="company">Company</label>
<input id="company" name="company" type="text" autocapitalize="none" spellcheck="false"
maxlength="16" placeholder="e.g. acme" required>
<p class="hint" id="company-hint"></p>
</div>
<div class="field" id="f-domain">
<label for="domain">Own domain (optional)</label>
<input id="domain" name="domain" type="text" autocapitalize="none" spellcheck="false"
placeholder="e.g. git.acme.com">
<p class="hint"></p>
</div>
</form>
<!-- ★ The picture: UI, SERVER and REPLICA — the front, main and twin —
inside the WireGuard overlay, drawn as two dashed rings. Its labels
are HOST_LABELS in the script (one line each). -->
<figure class="estate" id="estate" aria-label="Gunnar instance protected by VPN">
<svg id="pic" viewBox="0 0 640 150" role="img" aria-labelledby="pic-caption">
<rect class="ring outer" id="ring-outer" x="4" y="4" width="632" height="122" rx="28"></rect>
<rect class="ring inner" id="ring-inner" x="12" y="12" width="616" height="106" rx="21"></rect>
<g id="host-front">
<rect class="host" id="box-front" x="60" y="34" width="100" height="44" rx="7"></rect>
<text class="label" id="label-front" x="110" y="60"></text>
<text class="res" id="res-front" x="110" y="104"></text>
</g>
<g id="host-main">
<rect class="host" id="box-main" x="262" y="30" width="116" height="52" rx="7"></rect>
<rect class="slice" id="slice-main" x="263" y="70" width="20" height="11" hidden></rect>
<text class="label" id="label-main" x="320" y="60"></text>
<text class="res" id="res-main" x="320" y="104"></text>
</g>
<g id="host-twin">
<rect class="host" id="box-twin" x="480" y="34" width="100" height="44" rx="7"></rect>
<rect class="slice" id="slice-twin" x="481" y="66" width="20" height="11" hidden></rect>
<text class="label" id="label-twin" x="530" y="60"></text>
<text class="res" id="res-twin" x="530" y="104"></text>
</g>
<text class="caption" id="pic-caption" x="320" y="144">Gunnar instance protected by VPN</text>
</svg>
</figure>
<!-- ONE − / + for the whole setup. -->
<div class="size" id="size">
<button type="button" id="minus" aria-label="Smaller">−</button>
<div class="step" aria-live="polite">
<strong id="step-name">…</strong>
<span id="step-res"></span>
</div>
<button type="button" id="plus" aria-label="Bigger">+</button>
</div>
<p class="price" id="price"><strong id="price-month"></strong><span id="price-total"></span></p>
<p class="coming" id="coming" hidden></p>
<button id="go" class="primary" type="button" disabled>Pay</button>
<div id="msg" class="msg" role="status" aria-live="polite"></div>
</main>
<pre id="verdict"></pre>
<script>
/*@buy-script*/
</script>
<script>
(function () {
// What the buyer SEES: laid out boxes, never the `hidden` attribute's claim (PAID-3: a
// `hidden` form a site's CSS draws as a grid is on the screen).
const seen = (e) => !!e && getComputedStyle(e).visibility !== 'hidden' && e.getClientRects().length > 0;
const verdict = () => {
const shown = (id) => seen(document.getElementById(id));
document.getElementById('verdict').textContent = JSON.stringify({
buttons: [...document.querySelectorAll('button')].filter(seen).map(b => b.textContent.trim()),
fields: [...document.querySelectorAll('input')].filter(seen).map(i => i.id),
form: shown('form'), choice: shown('choice'), plans: shown('plans'), estate: shown('estate'),
size: shown('size'), price: shown('price'), go: shown('go'),
state: shown('state'), stateText: (document.getElementById('state').textContent || '').trim(),
heading: document.getElementById('heading').textContent.trim(),
labels: [...document.querySelectorAll('label')].filter(seen).map(l => l.textContent.trim()),
choices: [...document.querySelectorAll('#choice button')].filter(seen).map(b => b.textContent.trim()),
pressed: [...document.querySelectorAll('#choice button')].filter(b => b.getAttribute('aria-pressed') === 'true').map(b => b.textContent.trim()),
first: seen(document.getElementById('first-account')) ? document.getElementById('first-account').textContent.trim() : '',
hint: seen(document.getElementById('account-hint')) ? document.getElementById('account-hint').textContent.trim() : '',
beta: [...document.querySelectorAll('#beta-line')].filter(seen).map(e => e.textContent.trim()).join(''),
rows: [...document.querySelectorAll('#plans > *')].filter(seen).map(r => r.textContent.trim()),
msg: (document.getElementById('msg').textContent || '').trim(),
step: (document.getElementById('step-name').textContent || '').trim(),
progress: [...document.querySelectorAll('#progress li')].filter(seen).map(li => ({ text: li.textContent.trim(), at: li.className })),
links: [...document.querySelectorAll('#state a')].filter(seen).map(a => a.getAttribute('href')),
});
};
setInterval(verdict, 250);
})();
</script>
</body></html>