| 1 | package check |
| 2 | |
| 3 | import ( |
| 4 | "slices" |
| 5 | "strings" |
| 6 | "testing" |
| 7 | |
| 8 | "amadan.net/rastrillo/fiesta" |
| 9 | ) |
| 10 | |
| 11 | // The two media blocks that undo the depth language where it would do |
| 12 | // harm. Each flattens the axis at html:root, so no |
| 13 | // state rule can keep a shadow, a gradient, a tint or a drop; and each |
| 14 | // names the components that need a border or a system colour the |
| 15 | // tokens cannot give them. A token or a component left out keeps a |
| 16 | // shadow forced colours would strip half of, or a tint a printer would |
| 17 | // drop. The summary's edge is in the flat list too: it is a ring drawn |
| 18 | // in the shadow by rules at (0,4,1) and up, so a box-shadow: none at |
| 19 | // the summary's own selector never reaches it, and the token is the |
| 20 | // one thing that wins in every state. |
| 21 | func TestForcedColoursAndPrintFlattenTheAxisAndCoverEveryComponent(t *testing.T) { |
| 22 | css := string(fiesta.TokensCSS()) |
| 23 | flat := []string{"--rst-depth-hl: transparent", "--rst-depth-contact: transparent", "--rst-depth-ambient: transparent", "--rst-depth-face: 0", "--rst-press-drop: 0px", |
| 24 | "--rst-sheen-fill: transparent", "--rst-sheen-plain: transparent", "--rst-light-wash: transparent", "--rst-light-rim: var(--rst-line)", |
| 25 | "--rst-press-wash: 0", "--rst-press-spot: 0", "--rst-press-wash-plain: 0", "--rst-press-spot-plain: 0", |
| 26 | "--rst-tint-hover: var(--rst-surface)", "--rst-tint-press: var(--rst-surface)", "--rst-tint-tap: transparent", "--rst-row-tint-hover: var(--rst-surface)", "--rst-row-tint-press: var(--rst-surface)", "--rst-focus-halo: transparent", |
| 27 | "--rst-summary-edge: transparent", "--rst-summary-edge-hover: transparent"} |
| 28 | forced := append(flat, "html:root {", "[rst-btn]", "[rst-box]", "[rst-card]", "[rst-stats]", "[rst-list]", "[rst-input]", "[rst-textarea]", "[rst-search]", "[rst-dropdown]", "[rst-switch-track]", "[rst-switch-track]::after", |
| 29 | "[rst-shell-sidebar] [rst-shell-nav] a[aria-current]", "[rst-shell-console] [rst-shell-nav] a[aria-current]", "[rst-shell-topbar] [rst-shell-tail] > [rst-shell-nav] a[aria-current]", "[rst-dropdown-menu] a[aria-current]", "[rst-pagination] [aria-current=\"page\"]", "[rst-seg-tabs] a[aria-current]", `[rst-seg-tabs] > button[aria-pressed="true"]`, "[rst-side-nav] a[aria-current]", |
| 30 | `[rst-shell-sidebar][rst-shell-sidebar~="index"] [rst-shell-nav] > a[aria-current]:hover`, `[rst-shell-console][rst-shell-console~="index"] [rst-shell-nav] > a[aria-current]:active`, |
| 31 | "ButtonBorder", "CanvasText", "ButtonText", "Highlight", "HighlightText", "box-shadow: none") |
| 32 | print := append(flat, "html:root {", "--rst-accent-fill: transparent", "--rst-accent-fill-hover: transparent", "--rst-on-accent-fill: var(--rst-text)", "--rst-danger-fill: transparent", "--rst-on-danger-fill-hover: var(--rst-text)", "--rst-danger-hover-filter: none", "--rst-control-face-hover: transparent", "--rst-control-text-hover: var(--rst-text)", "--rst-knob: var(--rst-text)", |
| 33 | "[rst-btn]", "[rst-box]", "[rst-card]", "[rst-stats]", "[rst-list]", "[rst-input]", "[rst-textarea]", "[rst-search]", "[rst-dropdown]", "[rst-switch-track]", "[rst-switch-track]::after", "[rst-switch]:has(input:checked) [rst-switch-track]", "[rst-tblock] > [rst-tblock-head]:has(> input:checked) > [rst-switch-track]", "[rst-seg-tabs] a[aria-current]", `[rst-seg-tabs] > button[aria-pressed="true"]`, "[rst-side-nav] a[aria-current]", "[rst-pagination] [aria-current=\"page\"]", "[rst-dropdown-menu]", "[rst-row-menu-panel]", |
| 34 | "[rst-shell-sidebar] [rst-shell-nav] a[aria-current]", "[rst-shell-console] [rst-shell-nav] a[aria-current]", "var(--rst-line-strong)", "background-image: none", "box-shadow: none", "outline: 1px solid var(--rst-line-strong)") |
| 35 | // The blocks are read as rules, the last top-level block of each |
| 36 | // query being the one at the end of the file. A want is a rule's |
| 37 | // selector when it ends in " {", a declaration when it holds ": ", |
| 38 | // a selector a rule names when it starts with [ or ., a var() a value |
| 39 | // reads, and otherwise a word a value holds. Read as text, any of them was satisfied by a |
| 40 | // comment, and the block was cut by counting braces. |
| 41 | rules := leafRules(t, css) |
| 42 | printTop := -1 |
| 43 | for _, block := range []struct { |
| 44 | context string |
| 45 | want []string |
| 46 | }{ |
| 47 | {"@media (forced-colors: active)", forced}, |
| 48 | {"@media print", print}, |
| 49 | } { |
| 50 | last := -1 |
| 51 | for _, r := range rules { |
| 52 | if r.context == block.context { |
| 53 | last = max(last, r.top) |
| 54 | } |
| 55 | } |
| 56 | if last < 0 { |
| 57 | t.Fatalf("tokens.css has no %s block at the end of the file", block.context) |
| 58 | } |
| 59 | if block.context == "@media print" { |
| 60 | printTop = last |
| 61 | } |
| 62 | var in []leafRule |
| 63 | for _, r := range rules { |
| 64 | if r.top == last { |
| 65 | in = append(in, r) |
| 66 | } |
| 67 | } |
| 68 | for _, w := range block.want { |
| 69 | if !blockCarries(in, w) { |
| 70 | t.Errorf("the %s block does not carry %q", block.context, w) |
| 71 | } |
| 72 | } |
| 73 | for _, r := range in { |
| 74 | for _, d := range r.decls { |
| 75 | if d[2] != "" { |
| 76 | t.Errorf("the %s block uses !important (C11)", block.context) |
| 77 | } |
| 78 | } |
| 79 | } |
| 80 | } |
| 81 | // Nothing that paints a button, a card or a nav item may follow the |
| 82 | // print block: its rules win by order. |
| 83 | for _, sel := range []string{`[rst-btn~="primary"]`, `[rst-shell-nav] a[aria-current]`, `[rst-box]`} { |
| 84 | for _, r := range rules { |
| 85 | if r.top > printTop && slices.ContainsFunc(splitSelectorList(r.selector), func(s string) bool { return strings.Contains(s, sel) }) { |
| 86 | t.Errorf("%q is styled after the print block, by %s; the print rules rely on source order", sel, r.selector) |
| 87 | } |
| 88 | } |
| 89 | } |
| 90 | } |
| 91 | |
| 92 | // blockCarries reports whether the rules of one block carry want, read |
| 93 | // as TestForcedColoursAndPrintFlattenTheAxisAndCoverEveryComponent |
| 94 | // says. |
| 95 | func blockCarries(rules []leafRule, want string) bool { |
| 96 | return slices.ContainsFunc(rules, func(r leafRule) bool { |
| 97 | sels := splitSelectorList(r.selector) |
| 98 | switch { |
| 99 | case strings.HasSuffix(want, " {"): |
| 100 | return slices.Contains(sels, strings.TrimSuffix(want, " {")) |
| 101 | case strings.Contains(want, ": "): |
| 102 | prop, value, _ := strings.Cut(want, ": ") |
| 103 | return slices.Contains(r.decls, [3]string{prop, value, ""}) |
| 104 | case strings.HasPrefix(want, "[") || strings.HasPrefix(want, "."): |
| 105 | return slices.ContainsFunc(sels, func(s string) bool { return strings.Contains(s, want) }) |
| 106 | case strings.HasPrefix(want, "var("): |
| 107 | name := strings.TrimSuffix(strings.TrimPrefix(want, "var("), ")") |
| 108 | return slices.ContainsFunc(r.decls, func(d [3]string) bool { |
| 109 | return slices.ContainsFunc(varRefs(d[1]), func(ref varRef) bool { return ref.name == name }) |
| 110 | }) |
| 111 | } |
| 112 | return slices.ContainsFunc(r.decls, func(d [3]string) bool { |
| 113 | return slices.Contains(strings.FieldsFunc(d[1], func(c rune) bool { return strings.ContainsRune(" ,()/", c) }), want) |
| 114 | }) |
| 115 | }) |
| 116 | } |
| 117 | |