Public: anyone can read this; the crew can change it.

Sign-in shows no "Passkey off" badge, and offers the passkey button only where a passkey can run #7924

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

torque/forge:torque/7921-sign-in-shows-no-passkey-off-badge-and-o → main 1 commit fork

  • Merged. torque/7921-sign-in-shows-no-passkey-off-badge-and-o was merged into main as b93a2cc.

Review

Sign in to review this change.
2 files changed +30 −15
src/web/pages/site.ts +8 −9
@@ -35,7 +35,7 @@ import { SignupSlot, signupWindow } from "../../actions/signup-window.ts";
3535 import { signinWindow } from "../../core/signin-window.ts";
3636 import { field, readForm } from "../form.ts";
3737 import { passkeyJs } from "../passkey-client.ts";
38 import { passkeyNote, passkeyState } from "../passkey-state.ts";
38 import { passkeyNote } from "../passkey-state.ts";
3939 import { html, when, type Html } from "../html.ts";
4040 import { icon } from "../icons.ts";
4141 import { flashTo, page, redirect } from "../layout.ts";
@@ -285,18 +285,17 @@ function authForm(
285285 // Sign-in leads with the passkey: one press, nothing typed; the password
286286 // form below stays for accounts that were given one. The button needs a
287287 // script and a browser with passkeys, so the block is `data-passkey="on"`: shown
288 // only once `passkey-client.ts` marks `:root.passkey`. The form's Sign in
289 // comes in both variants, outline beside the lead and primary everywhere
290 // else, so the page always has one primary action. The sentence tells a
291 // newcomer before they look for a door that is not there.
288 // only once `passkey-client.ts` marks `:root.passkey`. No "Passkey off"
289 // badge here: a visitor signing in did not ask about passkeys, and with no
290 // script the page is the password form alone, its margin included (#7921).
291 // The form's Sign in comes in both variants, outline beside the lead and
292 // primary everywhere else, so the page always has one primary action. The
293 // sentence tells a newcomer before they look for a door that is not there.
292294 const passkeyFirst = when(
293295 kind === "signin",
294 () => html`<div class="stack stack-tight mb">
295 ${passkeyState()}
296 <div class="stack stack-tight" data-passkey="on" hidden>
296 () => html`<div class="stack stack-tight mb" data-passkey="on" hidden>
297297 <button type="button" id="passkey-signin" class="btn" data-variant="primary" data-size="lg">Sign in with a passkey</button>
298298 ${passkeyNote("passkey-signin-note")}
299 </div>
300299 </div>
301300 ${when(SIGNUP_SENTENCES[timePhase(ctx.request, "settings", () => getSignup(ctx.app.db))], (said) => html`<p class="muted small mb" id="signup-mode">${said}</p>`)}`,
302301 );
test/signin-page.test.ts +22 −6
@@ -32,6 +32,10 @@ describe("/signin", () => {
3232 await rm(dataDir, { recursive: true, force: true });
3333 });
3434
35 // The markup a person sees: the inline client script, shared with the pages
36 // that do draw the badge, names it too.
37 const markup = (page: string) => page.replace(/<script\b[\s\S]*?<\/script>/g, "");
38
3539 const signin = async (mode: SignupMode) => {
3640 setSignup(app.db, mode, "boot");
3741 const res = await fetch(`${servers.url}/signin`);
@@ -47,18 +51,30 @@ describe("/signin", () => {
4751 expect(page).toMatch(/<button[^>]*id="passkey-signin"[^>]*data-variant="primary"/);
4852 });
4953
50 test("the passkey state is a badge, and no passkey-on class remains", async () => {
51 const html = await signin("open");
52 expect(html).toContain("Passkey off");
53 expect(html).toMatch(/<span\s+class="badge"\s+data-variant="outline"\s*>\s*Passkey off/);
54 test("no passkey state word, and the passkey button waits hidden for a browser that can run it (#7921)", async () => {
55 const html = markup(await signin("open"));
56 expect(html).not.toContain("Passkey off");
57 expect(html).not.toContain("Passkey on");
58 expect(html).not.toContain("data-passkey-state");
5459 expect(html).not.toContain("passkey-on");
5560 expect(html).not.toContain('class="passkey');
56 expect(insideHiddenDiv(html, "data-passkey-state")).toBe(false);
61 expect(insideHiddenDiv(html, 'id="passkey-signin"')).toBe(true);
62 });
63
64 test("with no script the page is the password form with one visible Sign in button", async () => {
65 const html = markup(await signin("open"));
66 // Drop every hidden element, as a browser with no script shows the page.
67 const shown = html
68 .replace(/<div class="stack stack-tight mb" data-passkey="on" hidden>[\s\S]*?<\/div>\s*<\/div>/, "")
69 .replace(/<button[^>]*\shidden>[\s\S]*?<\/button>/g, "");
70 expect(shown).not.toContain("passkey-signin");
71 expect(shown.match(/<button[^>]*>\s*Sign in\s*<\/button>/g)?.length).toBe(1);
72 expect(shown).toContain('name="password"');
5773 });
5874
5975 test("the passkey lead shows only where a passkey can run, and Sign in is primary everywhere else", async () => {
6076 const page = await signin("open");
61 expect(page).toMatch(/<div class="stack stack-tight mb">\s*<span data-passkey-state>[\s\S]*?Passkey off[\s\S]*?<\/span>\s*<div class="stack stack-tight" data-passkey="on" hidden>\s*<button[^>]*id="passkey-signin"/);
77 expect(page).toMatch(/<div class="stack stack-tight mb" data-passkey="on" hidden>\s*<button[^>]*id="passkey-signin"/);
6278 expect(page).not.toMatch(/passkey-lead|passkey-note/);
6379 expect(page).toMatch(/<span class="muted small" id="passkey-signin-note" data-fail="error" role="status"><\/span><\/div>\s*<\/div>/);
6480 // Both variants of Sign in, each a named btn() variant picked in markup.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs