rastrillo / fiesta Public

Clone
git clone https://amadan.net/rastrillo/fiesta

Plain git — no account needed to clone.

Download

Download this file

1package gallery
2
3import (
4 "html"
5 "io/fs"
6 "regexp"
7 "strings"
8 "testing"
9
10 xhtml "golang.org/x/net/html"
11
12 "amadan.net/rastrillo/fiesta"
13 "amadan.net/rastrillo/fiesta/example"
14)
15
16// The frame document: what every frame carries in its head, the stage
17// under the sign-in card, the links a shell demo needs to work as the
18// app it stands for, and the attribute that lets a document opened in
19// a tab of its own remember the reader's scheme.
20
21// The rules every frame's style carries, as written: a component
22// sample gets breathing room, a whole-page sample fills the frame, by
23// its markup or by the manifest's word.
24const (
25 paddingRule = `body { padding: 1rem; }`
26 fillRule = `body:is([rst-stage], [rst-shell-topbar], [rst-shell-sidebar], [rst-shell-console]), body:has(> [rst-shell-topbar], > [rst-shell-sidebar], > [rst-shell-console], > [rst-backdrop], > [rst-stage]) { padding: 0; }`
27 documentRule = `body[data-gallery-document] { padding: 0; }`
28)
29
30// frameDocs is every frame document of the tree, by path.
31func frameDocs(t *testing.T, files map[string][]byte) map[string][]byte {
32 t.Helper()
33 out := map[string][]byte{}
34 for name, body := range files {
35 if isFrame(name) {
36 out[name] = body
37 }
38 }
39 if len(out) == 0 {
40 t.Fatal("the render wrote no frame document; the check that follows would read nothing")
41 }
42 return out
43}
44
45// elementChildren is the element children of n, in order.
46func elementChildren(n node) []node {
47 var out []node
48 for c := n.n.FirstChild; c != nil; c = c.NextSibling {
49 if c.Type == xhtml.ElementNode {
50 out = append(out, node{c})
51 }
52 }
53 return out
54}
55
56// TestEveryFrameDocumentCarriesNoindexAndItsPaddingRule: every frame
57// document has one robots meta saying noindex, right after the
58// viewport meta, and one style, the last element of its head, with the
59// padding rule, the fill rule and the document rule each exactly once,
60// every selector present as written.
61func TestEveryFrameDocumentCarriesNoindexAndItsPaddingRule(t *testing.T) {
62 frames := frameDocs(t, render(t, ""))
63 for name, body := range frames {
64 doc := parse(t, body)
65 head, ok := doc.first(func(n node) bool { return n.n.Data == "head" })
66 if !ok {
67 t.Errorf("%s has no <head>", name)
68 continue
69 }
70 kids := elementChildren(head)
71 robots := -1
72 for i, k := range kids {
73 if k.n.Data != "meta" {
74 continue
75 }
76 if v, _ := k.attr("name"); v == "robots" {
77 if robots >= 0 {
78 t.Errorf("%s carries two robots metas", name)
79 }
80 robots = i
81 if c, _ := k.attr("content"); c != "noindex" {
82 t.Errorf("%s says robots %q, want noindex", name, c)
83 }
84 }
85 }
86 if robots < 0 {
87 t.Errorf("%s carries no robots meta", name)
88 } else if robots == 0 {
89 t.Errorf("%s: the robots meta is the head's first element; it follows the viewport meta", name)
90 } else if prev := kids[robots-1]; prev.n.Data != "meta" || func() bool { v, _ := prev.attr("name"); return v != "viewport" }() {
91 t.Errorf("%s: the robots meta does not follow the viewport meta", name)
92 }
93 if last := kids[len(kids)-1]; last.n.Data != "style" {
94 t.Errorf("%s: the last element of the head is <%s>, want the frame's <style>", name, last.n.Data)
95 }
96 s := string(body)
97 if n := strings.Count(s, paddingRule); n != 1 {
98 t.Errorf("%s carries the padding rule %d times, want once", name, n)
99 }
100 if n := strings.Count(s, fillRule); n != 1 {
101 t.Errorf("%s carries the fill rule %d times, want once", name, n)
102 }
103 if n := strings.Count(s, documentRule); n != 1 {
104 t.Errorf("%s carries the document rule %d times, want once", name, n)
105 }
106 }
107 t.Logf("%d frame documents checked", len(frames))
108}
109
110// TestALayoutDocumentIsMarkedAWholePage: the body of every layout
111// example's frame carries data-gallery-document, which the document
112// rule fills to the frame's edge, and no other frame's body does. The
113// column shell is the case the markup rules cannot reach: its body has
114// no wrapper, so without the mark it would sit in a component's padding.
115func TestALayoutDocumentIsMarkedAWholePage(t *testing.T) {
116 frames := frameDocs(t, render(t, ""))
117 m := manifest(t)
118 marked, layouts := 0, 0
119 for name, body := range frames {
120 parts := strings.Split(name, "/")
121 e, ok := m.Entry(parts[len(parts)-2])
122 if !ok {
123 t.Fatalf("%s is no example's frame", name)
124 }
125 bodyEl, ok := parse(t, body).first(func(n node) bool { return n.n.Data == "body" })
126 if !ok {
127 t.Errorf("%s has no <body>", name)
128 continue
129 }
130 has := hasAttr(bodyEl, "data-gallery-document")
131 if e.Document {
132 layouts++
133 if has {
134 marked++
135 } else {
136 t.Errorf("%s is a layout document whose body is not marked data-gallery-document", name)
137 }
138 } else if has {
139 t.Errorf("%s is not a layout document and its body is marked data-gallery-document", name)
140 }
141 }
142 if layouts == 0 {
143 t.Fatal("no frame belongs to a layout example; the check above read nothing")
144 }
145 column := "day/en/frames/layout-column/" + mustEntry(t, "layout-column").Default + ".html"
146 if _, ok := frames[column]; !ok {
147 t.Fatalf("no %s", column)
148 }
149 t.Logf("%d of %d layout frames marked", marked, layouts)
150}
151
152// sceneOf lifts the stage's scene from the layout-stage example's own
153// document, independently of the render, so the test and the code
154// cannot agree on a wrong scene.
155func sceneOf(t *testing.T) string {
156 t.Helper()
157 doc, err := example.Document(fiesta.FS(), "layout-stage", mustEntry(t, "layout-stage").Default)
158 if err != nil {
159 t.Fatal(err)
160 }
161 scene := regexp.MustCompile(`(?s)<div rst-stage-scene aria-hidden="true">.*?</div>`).FindString(doc)
162 if scene == "" {
163 t.Fatal("the layout-stage example carries no scene to lift")
164 }
165 return scene
166}
167
168// hasAttr says whether the element carries the attribute, whatever its
169// value.
170func hasAttr(n node, name string) bool {
171 _, ok := n.attr(name)
172 return ok
173}
174
175// TestTheSigninScreensAreOnTheStage: the ten sign-in states whose body
176// opens with the card are framed on the stage, the scene first and the
177// card inside the page column; the two bare rst-box screens are not;
178// every scene is the layout-stage example's own; and the signin page's
179// HTML tab shows the bare card, never the stage. Read in every theme
180// and locale, the structure parsed in day/en.
181func TestTheSigninScreensAreOnTheStage(t *testing.T) {
182 files := render(t, "")
183 e := mustEntry(t, "signin")
184 scene := sceneOf(t)
185 bare := map[string]bool{"email-and-password": true, "google-apple-and-the-rest": true}
186
187 staged := 0
188 for _, dir := range dirs() {
189 for _, st := range e.States {
190 name := dir + "frames/signin/" + st.ID + ".html"
191 body, ok := files[name]
192 if !ok {
193 t.Fatalf("no %s", name)
194 }
195 // The body alone: the frame style's selectors name the
196 // stage in every frame's head.
197 s := string(body)
198 s = s[strings.Index(s, "<body"):]
199 if bare[st.ID] {
200 if strings.Contains(s, "rst-stage") {
201 t.Errorf("%s is staged; the rst-box screens stay bare", name)
202 }
203 continue
204 }
205 if dir == "day/en/" {
206 staged++
207 }
208 if !strings.Contains(s, `<div rst-stage><div rst-stage-scene aria-hidden="true">`) {
209 t.Errorf("%s does not open its stage with the scene", name)
210 }
211 if n := strings.Count(s, scene); n != 1 {
212 t.Errorf("%s carries the layout-stage scene %d times, want once", name, n)
213 }
214 }
215 }
216 if staged != 10 {
217 t.Errorf("%d signin states are staged in day/en, want 10", staged)
218 }
219
220 for _, st := range e.States {
221 if bare[st.ID] {
222 continue
223 }
224 name := "day/en/frames/signin/" + st.ID + ".html"
225 doc := parse(t, files[name])
226 body, _ := doc.first(func(n node) bool { return n.n.Data == "body" })
227 kids := elementChildren(body)
228 if len(kids) != 1 || kids[0].n.Data != "div" || !hasAttr(kids[0], "rst-stage") {
229 t.Errorf("%s: the body's one child is not <div rst-stage>", name)
230 continue
231 }
232 parts := elementChildren(kids[0])
233 if len(parts) != 2 || !hasAttr(parts[0], "rst-stage-scene") || !hasAttr(parts[1], "rst-page") {
234 t.Errorf("%s: the stage holds %d parts, want the scene then the page column", name, len(parts))
235 continue
236 }
237 if v, _ := parts[0].attr("aria-hidden"); v != "true" {
238 t.Errorf("%s: the scene is not aria-hidden", name)
239 }
240 card := elementChildren(parts[1])
241 if len(card) != 1 || !hasAttr(card[0], "rst-signin") {
242 t.Errorf("%s: the page column holds %d elements, want the one [rst-signin] card", name, len(card))
243 }
244 }
245
246 page := files["day/en/signin.html"]
247 for _, st := range e.States {
248 if tab := htmlTab(t, page, "signin", st.ID); strings.Contains(tab, "rst-stage") {
249 t.Errorf("signin/%s: the HTML tab shows the stage; it shows the card a reader copies", st.ID)
250 }
251 }
252}
253
254// bodyLinks is every <a> in the document's body, in order.
255func bodyLinks(t *testing.T, doc node) []node {
256 t.Helper()
257 body, ok := doc.first(func(n node) bool { return n.n.Data == "body" })
258 if !ok {
259 t.Fatal("the document has no <body>")
260 }
261 return body.find(func(n node) bool { return n.n.Data == "a" })
262}
263
264// within says whether n has an ancestor pred accepts.
265func within(n node, pred func(node) bool) bool {
266 for p := n.n.Parent; p != nil; p = p.Parent {
267 if p.Type == xhtml.ElementNode && pred(node{p}) {
268 return true
269 }
270 }
271 return false
272}
273
274// TestAShellDemosLinksAreLive: in the sidebar shell's frame document
275// the back link, the nav links, the brand and the twelve language links
276// point where the interface says, each marked as the gallery's own and
277// the current language marked; in the sidebar's page document the way
278// up is the sidebar's index with its first nav id; in the stage shell
279// the foot link is the Overview; and every other href in a layout's
280// body is a fragment. Rendered at a mount, so a link written for the
281// root could not pass.
282func TestAShellDemosLinksAreLive(t *testing.T) {
283 const mount = "/x"
284 files := render(t, mount)
285 s := newTestSite(t, fiesta.FS(), mount)
286 overview := s.href("day", "en", "index.html")
287 frameOf := func(theme, locale, name string) string {
288 return s.href(theme, locale, "frames/"+name+"/"+mustEntry(t, name).Default+".html")
289 }
290 nav := func(t *testing.T, n node, where string) {
291 t.Helper()
292 if !hasAttr(n, "data-gallery-nav") {
293 t.Errorf("%s is not marked data-gallery-nav", where)
294 }
295 }
296 attr := func(n node, name string) string { v, _ := n.attr(name); return v }
297 hrefOf := func(n node) string { return attr(n, "href") }
298
299 // The sidebar's index document, in English and in Arabic for the
300 // language mark.
301 for _, locale := range []string{"en", "ar"} {
302 name := "day/" + locale + "/frames/layout-sidebar/default.html"
303 doc := parse(t, files[name])
304 live := map[*xhtml.Node]bool{}
305 links := bodyLinks(t, doc)
306 mark := func(n node) { live[n.n] = true }
307
308 back := doc.find(func(n node) bool {
309 return n.n.Data == "a" && attr(n, "rel") == "up" && within(n, func(p node) bool { return hasAttr(p, "rst-shell-back") })
310 })
311 if len(back) != 1 {
312 t.Fatalf("%s has %d back links, want 1", name, len(back))
313 }
314 if want := frameOf("day", locale, "layout-sidebar") + "#nav-posts"; hrefOf(back[0]) != want {
315 t.Errorf("%s: the back link goes to %q, want %q", name, hrefOf(back[0]), want)
316 }
317 nav(t, back[0], name+": the back link")
318 mark(back[0])
319
320 navLinks := doc.find(func(n node) bool {
321 return n.n.Data == "a" && within(n, func(p node) bool { return hasAttr(p, "rst-shell-nav") })
322 })
323 if len(navLinks) != 3 {
324 t.Fatalf("%s has %d nav links, want 3", name, len(navLinks))
325 }
326 for _, a := range navLinks {
327 if want := frameOf("day", locale, "layout-sidebar-page"); hrefOf(a) != want {
328 t.Errorf("%s: nav link %q goes to %q, want %q", name, a.text(), hrefOf(a), want)
329 }
330 nav(t, a, name+": nav link "+a.text())
331 mark(a)
332 }
333
334 brands := doc.find(func(n node) bool { return n.n.Data == "a" && hasAttr(n, "rst-shell-brand") })
335 if len(brands) != 2 {
336 t.Fatalf("%s has %d brand links, want 2", name, len(brands))
337 }
338 for _, a := range brands {
339 if want := s.href("day", locale, "index.html"); hrefOf(a) != want {
340 t.Errorf("%s: the brand goes to %q, want the Overview %q", name, hrefOf(a), want)
341 }
342 nav(t, a, name+": the brand")
343 mark(a)
344 }
345
346 langs := doc.find(func(n node) bool {
347 return n.n.Data == "a" && within(n, func(p node) bool { return hasAttr(p, "rst-locale") })
348 })
349 if len(langs) != len(Locales) {
350 t.Fatalf("%s has %d language links, want %d", name, len(langs), len(Locales))
351 }
352 current := 0
353 seen := map[string]bool{}
354 for _, a := range langs {
355 l := attr(a, "lang")
356 seen[l] = true
357 if want := frameOf("day", l, "layout-sidebar"); hrefOf(a) != want {
358 t.Errorf("%s: the %s link goes to %q, want %q", name, l, hrefOf(a), want)
359 }
360 if d := attr(a, "dir"); d != Dir(l) {
361 t.Errorf("%s: the %s link says dir=%q, want %q", name, l, d, Dir(l))
362 }
363 cur, _ := a.attr("aria-current")
364 if cur == "true" {
365 current++
366 if l != locale {
367 t.Errorf("%s marks %s current, want %s", name, l, locale)
368 }
369 }
370 nav(t, a, name+": the "+l+" link")
371 mark(a)
372 }
373 for _, l := range Locales {
374 if !seen[l] {
375 t.Errorf("%s: the language menu has no %s", name, l)
376 }
377 }
378 if current != 1 {
379 t.Errorf("%s marks %d languages current, want 1", name, current)
380 }
381
382 // The foot link is the layout's own sample text, so it is in
383 // the page's language.
384 backWords := html.UnescapeString(localMarkup(locale, "", footWords))
385 backs := doc.find(func(n node) bool {
386 return n.n.Data == "a" && strings.TrimSpace(n.text()) == backWords
387 })
388 if len(backs) == 0 {
389 t.Fatalf("%s has no link back to the design system", name)
390 }
391 for _, a := range backs {
392 if want := s.href("day", locale, "index.html"); hrefOf(a) != want {
393 t.Errorf("%s: the way back to the design system goes to %q, want %q", name, hrefOf(a), want)
394 }
395 nav(t, a, name+": the way back to the design system")
396 mark(a)
397 }
398
399 rest := 0
400 for _, a := range links {
401 if live[a.n] {
402 continue
403 }
404 rest++
405 if h := hrefOf(a); !strings.HasPrefix(h, "#") {
406 t.Errorf("%s: <a href=%q>%s</a> is neither the gallery's nor a fragment", name, h, a.text())
407 }
408 if hasAttr(a, "data-gallery-nav") {
409 t.Errorf("%s: <a href=%q> is marked the gallery's and points nowhere the interface says", name, hrefOf(a))
410 }
411 }
412 if rest == 0 {
413 t.Errorf("%s: every body link was rewritten; the sample's own links should stay fragments", name)
414 }
415 }
416
417 // The sidebar's page document: the way up is the index and its
418 // first nav id.
419 pageDoc := parse(t, files["day/en/frames/layout-sidebar-page/default.html"])
420 up := pageDoc.find(func(n node) bool { return n.n.Data == "a" && attr(n, "rel") == "up" })
421 if len(up) != 1 {
422 t.Fatalf("the sidebar's page document has %d up links, want 1", len(up))
423 }
424 if want := frameOf("day", "en", "layout-sidebar") + "#nav-posts"; hrefOf(up[0]) != want {
425 t.Errorf("the sidebar's page document goes up to %q, want %q", hrefOf(up[0]), want)
426 }
427 nav(t, up[0], "the sidebar page's way up")
428
429 // The stage: its foot link is the Overview.
430 stageDoc := parse(t, files["day/en/frames/layout-stage/default.html"])
431 foot := stageDoc.find(func(n node) bool {
432 return n.n.Data == "a" && within(n, func(p node) bool { return hasAttr(p, "rst-stage-foot") })
433 })
434 if len(foot) != 1 {
435 t.Fatalf("the stage shell has %d foot links, want 1", len(foot))
436 }
437 if hrefOf(foot[0]) != overview {
438 t.Errorf("the stage's foot link goes to %q, want %q", hrefOf(foot[0]), overview)
439 }
440 nav(t, foot[0], "the stage's foot link")
441
442 // A shell with no page document: its nav links stay fragments.
443 topbar := parse(t, files["day/en/frames/layout-topbar/default.html"])
444 for _, a := range topbar.find(func(n node) bool {
445 return n.n.Data == "a" && within(n, func(p node) bool { return hasAttr(p, "rst-shell-nav") })
446 }) {
447 if h := hrefOf(a); h != "#" {
448 t.Errorf("the topbar, which has no page document, links its nav to %q, want #", h)
449 }
450 }
451
452 // A component's frame has no gallery link at all: the rewrite is
453 // the layouts' alone.
454 for _, a := range bodyLinks(t, parse(t, files["day/en/frames/button/"+mustEntry(t, "button").Default+".html"])) {
455 if hasAttr(a, "data-gallery-nav") {
456 t.Errorf("the button frame carries a gallery link to %q", hrefOf(a))
457 }
458 }
459}
460
461// TestEveryFrameCarriesTheSchemeStore: every frame document's <html>
462// says data-rst-scheme-store="local", so fiesta.js in a document
463// opened in a tab of its own remembers the reader's choice where the
464// gallery's pages do; the one example that already says so says it
465// once.
466func TestEveryFrameCarriesTheSchemeStore(t *testing.T) {
467 frames := frameDocs(t, render(t, ""))
468 for name, body := range frames {
469 html, ok := parse(t, body).first(func(n node) bool { return n.n.Data == "html" })
470 if !ok {
471 t.Errorf("%s has no <html>", name)
472 continue
473 }
474 if v, _ := html.attr("data-rst-scheme-store"); v != "local" {
475 t.Errorf("%s says data-rst-scheme-store=%q, want local", name, v)
476 }
477 if n := strings.Count(string(body), "data-rst-scheme-store="); n != 1 {
478 t.Errorf("%s carries data-rst-scheme-store %d times, want once", name, n)
479 }
480 }
481}
482
483// countingFS counts the opens of one file in the source it serves, so
484// a test can say how often a render read it.
485type countingFS struct {
486 fs.FS
487 name string
488 opens *int
489}
490
491func (c countingFS) Open(name string) (fs.File, error) {
492 if name == c.name {
493 *c.opens++
494 }
495 return c.FS.Open(name)
496}
497
498// TestTheStageSceneIsLiftedOncePerRender: the scene the ten staged
499// sign-in frames share is read from the layout-stage example once, when
500// the site is set up, not once per staged frame: a whole theme and
501// locale rendered after the setup reads that document once more, for
502// the layout-stage example's own page, and no more.
503func TestTheStageSceneIsLiftedOncePerRender(t *testing.T) {
504 opens := 0
505 src := countingFS{FS: fiesta.FS(), name: "examples/layout-stage/" + mustEntry(t, "layout-stage").Default + ".html", opens: &opens}
506 s := newTestSite(t, src, "")
507 setup := opens
508 if err := s.theme("day", "en"); err != nil {
509 t.Fatal(err)
510 }
511 if staged := strings.Count(string(s.out["day/en/frames/signin/asking-for-an-address.html"]), "<div rst-stage-scene"); staged != 1 {
512 t.Fatalf("the staged sign-in frame carries %d scenes, so the count below measures nothing", staged)
513 }
514 if setup != 1 {
515 t.Errorf("the setup read the layout-stage document %d times, want once", setup)
516 }
517 if during := opens - setup; during != 1 {
518 t.Errorf("rendering day/en read the layout-stage document %d times, want once (its own page); the scene is lifted per frame", during)
519 }
520}
521
522// TestALayoutsLanguageLinksNameTheStateItFrames: the language menu of a
523// layout's frame document goes to the same state's document in each
524// locale, the state being the one framed, so a layout with a second
525// state would not send a reader of that state to the first. Driven
526// through frame() with a state the layout does not have, since every
527// layout has one state today.
528func TestALayoutsLanguageLinksNameTheStateItFrames(t *testing.T) {
529 s := newTestSite(t, fiesta.FS(), "")
530 e := mustEntry(t, "layout-sidebar")
531 doc, err := example.Document(fiesta.FS(), e.Name, e.Default)
532 if err != nil {
533 t.Fatal(err)
534 }
535 framed, err := s.frame(doc, "day", "ga", e, "second")
536 if err != nil {
537 t.Fatal(err)
538 }
539 langs := parse(t, []byte(framed)).find(func(n node) bool {
540 return n.n.Data == "a" && within(n, func(p node) bool { return hasAttr(p, "rst-locale") })
541 })
542 if len(langs) != len(Locales) {
543 t.Fatalf("%d language links, want %d", len(langs), len(Locales))
544 }
545 for _, a := range langs {
546 lang, _ := a.attr("lang")
547 href, _ := a.attr("href")
548 if want := s.href("day", lang, "frames/layout-sidebar/second.html"); href != want {
549 t.Errorf("the %s link goes to %q, want %q", lang, href, want)
550 }
551 }
552}
553
554// TestTheColumnDemosMenusAreInItsFooter: the column demo, opened from
555// the Demos list in a tab of its own, shows its menus where the column
556// shell now puts them, in the footer row under the page, and its
557// language menu still works there: each of the twelve links opens the
558// column demo in its locale, and the page's own language is the one
559// marked. Nothing that drops down comes before the page header.
560func TestTheColumnDemosMenusAreInItsFooter(t *testing.T) {
561 files := render(t, "")
562 s := newTestSite(t, fiesta.FS(), "")
563 state := mustEntry(t, "layout-column").Default
564 for _, locale := range []string{"en", "ar"} {
565 name := "day/" + locale + "/frames/layout-column/" + state + ".html"
566 body, ok := files[name]
567 if !ok {
568 t.Fatalf("the render wrote no %s", name)
569 }
570 head := strings.Index(string(body), "<header rst-page-header")
571 if head < 0 {
572 t.Fatalf("%s has no page header", name)
573 }
574 if strings.Contains(string(body[:head]), "<details") {
575 t.Errorf("%s: a <details> comes before the page header; the title must be the first thing on the page", name)
576 }
577 doc := parse(t, body)
578 inFoot := func(n node) bool { return within(n, func(p node) bool { return hasAttr(p, "rst-page-foot") }) }
579 for _, menu := range []string{"rst-locale", "rst-scheme"} {
580 found := doc.find(func(n node) bool { return n.n.Data == "details" && hasAttr(n, menu) })
581 if len(found) != 1 || !inFoot(found[0]) {
582 t.Errorf("%s: %d %s menus, want one, inside rst-page-foot", name, len(found), menu)
583 }
584 }
585 langs := doc.find(func(n node) bool {
586 return n.n.Data == "a" && within(n, func(p node) bool { return hasAttr(p, "rst-locale") })
587 })
588 if len(langs) != len(Locales) {
589 t.Fatalf("%s: %d language links, want %d", name, len(langs), len(Locales))
590 }
591 for _, a := range langs {
592 lang, _ := a.attr("lang")
593 href, _ := a.attr("href")
594 if !inFoot(a) {
595 t.Errorf("%s: the %s link is not in the footer row", name, lang)
596 }
597 if want := s.href("day", lang, "frames/layout-column/"+state+".html"); href != want {
598 t.Errorf("%s: the %s link goes to %q, want %q", name, lang, href, want)
599 }
600 current, _ := a.attr("aria-current")
601 if want := map[bool]string{true: "true"}[lang == locale]; current != want {
602 t.Errorf("%s: the %s link has aria-current=%q, want %q", name, lang, current, want)
603 }
604 }
605 }
606}
607