| 1 | package chrome |
| 2 | |
| 3 | import ( |
| 4 | "html/template" |
| 5 | "strings" |
| 6 | "testing" |
| 7 | "time" |
| 8 | ) |
| 9 | |
| 10 | // The door's copy is the suite's one copy set (shared-chrome spec §9.2, |
| 11 | // with Paul's 2026-10-01 ruling that the submit reads "Sign in"). Pinned |
| 12 | // word for word: a change here is a copy decision, made on purpose, in one |
| 13 | // place. |
| 14 | func TestDoorCopyIsPinned(t *testing.T) { |
| 15 | if LinkLifetime != 15*time.Minute { |
| 16 | t.Fatalf("LinkLifetime = %v; the copy below says 15 minutes", LinkLifetime) |
| 17 | } |
| 18 | for _, c := range []struct{ name, got, want string }{ |
| 19 | {"Heading", English.Heading, "Sign in"}, |
| 20 | {"AddressLabel", English.AddressLabel, "Email address"}, |
| 21 | {"Help", English.Help, "We’ll email you a link that signs you in. It works once and expires in 15 minutes."}, |
| 22 | {"Submit", English.Submit, "Sign in"}, |
| 23 | {"SentHeading", English.SentHeading, "Check your email"}, |
| 24 | {"SentBody", English.SentBody, "If that address can sign in here, we’ve sent it a link. It works once and expires in 15 minutes."}, |
| 25 | {"Again", English.Again, "Use a different address"}, |
| 26 | {"SignedOutPage", English.SignedOutPage, "Signed out"}, |
| 27 | {"SignedOutHeading", English.SignedOutHeading, "You’re signed out"}, |
| 28 | {"SignedOutBody", English.signedOutBody("Calls"), "You’ve signed out of Calls on this device."}, |
| 29 | {"SignIn", English.SignIn, "Sign in"}, |
| 30 | } { |
| 31 | if c.got != c.want { |
| 32 | t.Errorf("%s = %q, want %q", c.name, c.got, c.want) |
| 33 | } |
| 34 | } |
| 35 | for _, retired := range []string{"Email me a link", "Continue", "expires shortly", "on its way", "Back to sign in", "Send another link", "Use a different email address", "You are signed out", "Home"} { |
| 36 | for _, s := range []string{English.Heading, English.Help, English.Submit, English.SentHeading, English.SentBody, English.Again, English.SignedOutHeading, English.signedOutBody("Docs"), English.SignIn} { |
| 37 | if strings.Contains(s, retired) { |
| 38 | t.Errorf("%q carries retired copy %q", s, retired) |
| 39 | } |
| 40 | } |
| 41 | } |
| 42 | } |
| 43 | |
| 44 | // Every door page's tab title is "<Page> · <App>" (shared-chrome spec §10). |
| 45 | func TestDoorTitles(t *testing.T) { |
| 46 | for _, c := range []struct{ got, want string }{ |
| 47 | {Title("Sign in", "Docs"), "Sign in · Docs"}, |
| 48 | {Title("Sign in", ""), "Sign in"}, |
| 49 | {Title("", "Docs"), "Docs"}, |
| 50 | {SignIn{App: "Memoria"}.Title(), "Sign in · Memoria"}, |
| 51 | {SignIn{App: "Memoria"}.Page(), "Sign in"}, |
| 52 | {SignInNotice{App: "Calendar"}.Sent("/signin").Title(), "Check your email · Calendar"}, |
| 53 | {SignInNotice{App: "Calendar"}.Sent("/signin").Page(), "Check your email"}, |
| 54 | {SignInNotice{App: "Calls"}.SignedOut("/signin").Title(), "Signed out · Calls"}, |
| 55 | {SignInNotice{App: "Calls"}.SignedOut("/signin").Page(), "Signed out"}, |
| 56 | // A notice that is not one of the door's own is titled by its heading. |
| 57 | {SignInNotice{App: "Sheets", Heading: "Continue signing in"}.Title(), "Continue signing in · Sheets"}, |
| 58 | } { |
| 59 | if c.got != c.want { |
| 60 | t.Errorf("title %q, want %q", c.got, c.want) |
| 61 | } |
| 62 | } |
| 63 | } |
| 64 | |
| 65 | // The templates draw the door's copy when the app supplies none: the app |
| 66 | // passes its name, mark, sky and destinations, and nothing else. |
| 67 | func TestTheDoorDrawsItsOwnCopy(t *testing.T) { |
| 68 | html := render(t, "estilo-signin", SignIn{Icon: "/i.svg", App: "Calls", Action: "/signin"}) |
| 69 | for _, want := range []string{ |
| 70 | "<h1>Sign in</h1>", |
| 71 | ">Email address</label>", |
| 72 | "We’ll email you a link that signs you in. It works once and expires in 15 minutes.", |
| 73 | `<button rst-btn="primary" type="submit">Sign in</button>`, |
| 74 | } { |
| 75 | if !strings.Contains(html, want) { |
| 76 | t.Errorf("sign-in lacks %q:\n%s", want, html) |
| 77 | } |
| 78 | } |
| 79 | if strings.Contains(html, "Email me a link") { |
| 80 | t.Error("the door's submit must never read “Email me a link”") |
| 81 | } |
| 82 | |
| 83 | html = render(t, "estilo-signin-sent", SignInNotice{Icon: "/i.svg", App: "Calls"}.Sent("/signin?return_to=%2Fm%2Fabc")) |
| 84 | for _, want := range []string{ |
| 85 | "<h1>Check your email</h1>", |
| 86 | `<p estilo-signin-body role="status">If that address can sign in here, we’ve sent it a link. It works once and expires in 15 minutes.</p>`, |
| 87 | `<a href="/signin?return_to=%2Fm%2Fabc">Use a different address</a>`, |
| 88 | } { |
| 89 | if !strings.Contains(html, want) { |
| 90 | t.Errorf("sent lacks %q:\n%s", want, html) |
| 91 | } |
| 92 | } |
| 93 | |
| 94 | html = render(t, "estilo-signed-out", SignInNotice{Icon: "/i.svg", App: "Calls"}.SignedOut("/signin")) |
| 95 | for _, want := range []string{ |
| 96 | "<h1>You’re signed out</h1>", |
| 97 | "<p estilo-signin-body>You’ve signed out of Calls on this device.</p>", |
| 98 | `<a rst-btn="primary" href="/signin">Sign in</a>`, |
| 99 | } { |
| 100 | if !strings.Contains(html, want) { |
| 101 | t.Errorf("signed-out lacks %q:\n%s", want, html) |
| 102 | } |
| 103 | } |
| 104 | } |
| 105 | |
| 106 | // The sign-in email names the app and says the true lifetime, the same |
| 107 | // words as the door (shared-chrome spec §9.2). |
| 108 | func TestSignInEmail(t *testing.T) { |
| 109 | subject, body := SignInEmail("Docs", "https://docs.example/auth/verify?token=x") |
| 110 | if subject != "Sign in to Docs" { |
| 111 | t.Errorf("subject %q", subject) |
| 112 | } |
| 113 | want := "Here’s your link to sign in to Docs:\n\nhttps://docs.example/auth/verify?token=x\n\nIt works once and expires in 15 minutes. If you didn’t ask for it, you can ignore this email." |
| 114 | if body != want { |
| 115 | t.Errorf("body %q\nwant %q", body, want) |
| 116 | } |
| 117 | if got := EmailBody("Docs")("L"); !strings.Contains(got, "\n\nL\n\n") { |
| 118 | t.Errorf("EmailBody does not place the link: %q", got) |
| 119 | } |
| 120 | } |
| 121 | |
| 122 | // A locale other than English replaces every word through Copy. |
| 123 | func TestDoorCopyIsReplaceablePerLocale(t *testing.T) { |
| 124 | for locale, label := range locales { |
| 125 | t.Run(locale, func(t *testing.T) { |
| 126 | esc := template.HTMLEscapeString |
| 127 | c := &DoorCopy{Heading: label + "-h", AddressLabel: label + "-addr", Help: label + "-help", Submit: label + "-submit", SentHeading: label + "-sent", SentBody: label + "-sentbody", Again: label + "-again", SignedOutPage: label + "-outpage", SignedOutHeading: label + "-out", SignedOutBody: label + " {app}-outbody", SignIn: label + "-in"} |
| 128 | html := render(t, "estilo-signin", SignIn{Icon: "/i.svg", App: "Docs", Copy: c, Action: "/signin"}) |
| 129 | for _, want := range []string{"<h1>" + esc(label) + "-h</h1>", esc(label) + "-addr", esc(label) + "-help", esc(label) + "-submit"} { |
| 130 | if !strings.Contains(html, want) { |
| 131 | t.Fatalf("missing %q in %s", want, html) |
| 132 | } |
| 133 | } |
| 134 | n := SignInNotice{Icon: "/i.svg", App: "Docs", Copy: c} |
| 135 | if h := render(t, "estilo-signin-sent", n.Sent("/signin")); !strings.Contains(h, esc(label)+"-sentbody") || !strings.Contains(h, esc(label)+"-again") { |
| 136 | t.Fatal("sent", h) |
| 137 | } |
| 138 | if h := render(t, "estilo-signed-out", n.SignedOut("/signin")); !strings.Contains(h, esc(label)+" Docs-outbody") || !strings.Contains(h, esc(label)+"-in") { |
| 139 | t.Fatal("signed out", h) |
| 140 | } |
| 141 | if got := n.SignedOut("/signin").Title(); got != label+"-outpage · Docs" { |
| 142 | t.Fatalf("title %q", got) |
| 143 | } |
| 144 | }) |
| 145 | } |
| 146 | } |
| 147 | |
| 148 | // The copy's slots are tokens, {app} and {link}, not Printf verbs: a |
| 149 | // translation that drops the slot, moves it, or carries a literal "%" |
| 150 | // renders as written rather than as "%!(EXTRA …)" (Fable on estilo #19, |
| 151 | // finding 4). |
| 152 | func TestDoorCopyFillsTokensNotFormats(t *testing.T) { |
| 153 | c := English |
| 154 | c.SignedOutBody = "100% done: {app} is signed out here." |
| 155 | if got := (SignInNotice{App: "Docs", Copy: &c}).SignedOut("/signin").Body; got != "100% done: Docs is signed out here." { |
| 156 | t.Errorf("signed-out body %q", got) |
| 157 | } |
| 158 | c.SignedOutBody = "Signed out." |
| 159 | if got := (SignInNotice{App: "Docs", Copy: &c}).SignedOut("/signin").Body; got != "Signed out." { |
| 160 | t.Errorf("a body without {app} rendered %q", got) |
| 161 | } |
| 162 | // With no app, the suite is named, as the hub's own page does. |
| 163 | if got := (SignInNotice{}).SignedOut("/signin").Body; got != "You’ve signed out of Oficina on this device." { |
| 164 | t.Errorf("no-app signed-out body %q", got) |
| 165 | } |
| 166 | // The link is substituted once: a link that happens to carry "{app}" |
| 167 | // is not rewritten. |
| 168 | if _, body := English.SignInEmail("Docs", "https://x.example/?q={app}"); !strings.Contains(body, "\n\nhttps://x.example/?q={app}\n\n") { |
| 169 | t.Errorf("the link was rewritten: %q", body) |
| 170 | } |
| 171 | } |
| 172 | |
| 173 | // The email is part of the copy set: a locale's subject and body are what |
| 174 | // a locale's email says (Fable on estilo #19, finding 3), and the package |
| 175 | // helpers are English's. |
| 176 | func TestSignInEmailReadsTheCopy(t *testing.T) { |
| 177 | c := English |
| 178 | c.EmailSubject = "Entra en {app}" |
| 179 | c.EmailBody = "{app}: {link} (100%)" |
| 180 | subject, body := c.SignInEmail("Docs", "L") |
| 181 | if subject != "Entra en Docs" || body != "Docs: L (100%)" { |
| 182 | t.Errorf("locale email = %q, %q", subject, body) |
| 183 | } |
| 184 | if got := c.MailSubject("Docs"); got != "Entra en Docs" { |
| 185 | t.Errorf("MailSubject %q", got) |
| 186 | } |
| 187 | if got := c.MailBody("Docs")("L"); got != "Docs: L (100%)" { |
| 188 | t.Errorf("MailBody %q", got) |
| 189 | } |
| 190 | s, b := SignInEmail("Docs", "L") |
| 191 | if es, eb := English.SignInEmail("Docs", "L"); s != es || b != eb || EmailSubject("Docs") != es || EmailBody("Docs")("L") != eb { |
| 192 | t.Error("the package helpers are not English's email") |
| 193 | } |
| 194 | } |
| 195 | |