| 1 | package check |
| 2 | |
| 3 | import ( |
| 4 | "regexp" |
| 5 | "strings" |
| 6 | "testing" |
| 7 | |
| 8 | "amadan.net/rastrillo/fiesta" |
| 9 | ) |
| 10 | |
| 11 | // The column's footer row: the column shell ends with it, and it holds |
| 12 | // the language and colour-scheme menus that used to stand above the |
| 13 | // page's title. A menu there pushed the title down and read as the |
| 14 | // page's first action; at the foot of the column it is out of the way |
| 15 | // and still one tap from every page. |
| 16 | |
| 17 | // summaryFace matches the four selectors, one per state, of the rules |
| 18 | // that give a dropdown's summary its face, in the attribute spelling: |
| 19 | // the :not() list of the dropdowns that stay flat, then the summary |
| 20 | // and its state. |
| 21 | var summaryFace = regexp.MustCompile(`^\[rst-dropdown\]:not\((.*)\) > summary:not\(\[rst-btn\]\)(|:active|:hover|:hover:active)$`) |
| 22 | |
| 23 | // summaryFaceClass is summaryFace in the class spelling. |
| 24 | var summaryFaceClass = regexp.MustCompile(`^\.rst-dropdown:not\((.*)\) > summary:not\(\.rst-btn\)(|:active|:hover|:hover:active)$`) |
| 25 | |
| 26 | // TestTheColumnShellEndsWithItsFooterRow: the column example opens its |
| 27 | // page with the slot, so the page header is the first thing in it, and |
| 28 | // closes it with the footer row holding the language menu and then the |
| 29 | // scheme menu; no menu is left above the slot. The stylesheet keeps |
| 30 | // the row's menus flat in every state of the summary face, hides a row |
| 31 | // that holds only template whitespace, and makes the row, not each |
| 32 | // menu, the box a panel hangs from where nothing can anchor it. |
| 33 | func TestTheColumnShellEndsWithItsFooterRow(t *testing.T) { |
| 34 | doc := defaultMarkup(t, "layout-column") |
| 35 | const open, slot, slotEnd = `<main rst-page id="main">`, "<!-- fiesta:slot -->", "<!-- /fiesta:slot -->" |
| 36 | i, j, k := strings.Index(doc, open), strings.Index(doc, slot), strings.Index(doc, slotEnd) |
| 37 | if i < 0 || j < i || k < j { |
| 38 | t.Fatalf("layout-column has no %s followed by its slot; the checks below read nothing", open) |
| 39 | } |
| 40 | if before := doc[i+len(open) : j]; strings.TrimSpace(before) != "" { |
| 41 | t.Errorf("layout-column: between %s and the slot there is %.80q; the page header must be the first thing in the page", open, before) |
| 42 | } |
| 43 | if strings.Contains(doc[i:j], "<details") { |
| 44 | t.Error("layout-column: a <details> sits between <main> and the slot") |
| 45 | } |
| 46 | rest := doc[k+len(slotEnd):] |
| 47 | for _, want := range []string{ |
| 48 | `<footer rst-page-foot>`, |
| 49 | `<details rst-dropdown rst-locale name="rst-menus">`, `</details>`, |
| 50 | `<details rst-dropdown rst-scheme name="rst-menus">`, `</details>`, |
| 51 | `</footer>`, `</main>`, |
| 52 | } { |
| 53 | rest = strings.TrimLeft(rest, " \t\n") |
| 54 | if strings.HasPrefix(want, "</details>") { |
| 55 | // The menu's own markup comes before its closing tag. |
| 56 | end := strings.Index(rest, want) |
| 57 | if end < 0 || strings.Contains(rest[:end], "<details") { |
| 58 | t.Fatalf("layout-column: after the slot, a menu is not closed before the next one opens") |
| 59 | } |
| 60 | rest = rest[end+len(want):] |
| 61 | continue |
| 62 | } |
| 63 | if !strings.HasPrefix(rest, want) { |
| 64 | t.Fatalf("layout-column: after the slot comes %.60q, want %s next: the footer row, the language menu, the scheme menu, then </footer> and </main>", rest, want) |
| 65 | } |
| 66 | rest = rest[len(want):] |
| 67 | } |
| 68 | |
| 69 | decls, _ := parseDeclarations(t, "tokens.css", string(fiesta.TokensCSS())) |
| 70 | faces := map[string]bool{} |
| 71 | has := map[string]bool{} |
| 72 | for _, d := range decls { |
| 73 | has[d.Selector+" { "+d.Property+": "+d.Value+" }"] = true |
| 74 | for _, spelling := range []struct { |
| 75 | re *regexp.Regexp |
| 76 | flat string |
| 77 | }{{summaryFace, "[rst-page-foot] [rst-dropdown]"}, {summaryFaceClass, ".rst-page__foot .rst-dropdown"}} { |
| 78 | m := spelling.re.FindStringSubmatch(d.Selector) |
| 79 | if m == nil || d.Property != "box-shadow" { |
| 80 | continue |
| 81 | } |
| 82 | state := m[2] |
| 83 | if state == "" { |
| 84 | state = "at rest" |
| 85 | } |
| 86 | key := "rule " + state + " does not list " + spelling.flat |
| 87 | if _, seen := faces[key]; !seen { |
| 88 | faces[key] = false |
| 89 | } |
| 90 | for _, arg := range splitSelectorList(m[1]) { |
| 91 | if arg == spelling.flat { |
| 92 | faces[key] = true |
| 93 | } |
| 94 | } |
| 95 | } |
| 96 | } |
| 97 | if len(faces) != 8 { |
| 98 | t.Fatalf("read %d summary-face rules by spelling and state, want 8 (four states, two spellings): %v", len(faces), faces) |
| 99 | } |
| 100 | for _, key := range sortedKeys(faces) { |
| 101 | if !faces[key] { |
| 102 | t.Errorf("the summary face's %s in its :not(); a footer menu would wear a face in that state", key) |
| 103 | } |
| 104 | } |
| 105 | for _, want := range []string{ |
| 106 | "[rst-page-foot]:not(:has(> *)) { display: none }", |
| 107 | ".rst-page__foot:not(:has(> *)) { display: none }", |
| 108 | "[rst-page-foot] [rst-dropdown] { position: static }", |
| 109 | ".rst-page__foot .rst-dropdown { position: static }", |
| 110 | } { |
| 111 | if !has[want] { |
| 112 | t.Errorf("tokens.css does not carry %s", want) |
| 113 | } |
| 114 | } |
| 115 | } |
| 116 | |