package check import ( "slices" "strings" "testing" "amadan.net/rastrillo/fiesta" ) // The two media blocks that undo the depth language where it would do // harm. Each flattens the axis at html:root, so no // state rule can keep a shadow, a gradient, a tint or a drop; and each // names the components that need a border or a system colour the // tokens cannot give them. A token or a component left out keeps a // shadow forced colours would strip half of, or a tint a printer would // drop. The summary's edge is in the flat list too: it is a ring drawn // in the shadow by rules at (0,4,1) and up, so a box-shadow: none at // the summary's own selector never reaches it, and the token is the // one thing that wins in every state. func TestForcedColoursAndPrintFlattenTheAxisAndCoverEveryComponent(t *testing.T) { css := string(fiesta.TokensCSS()) flat := []string{"--rst-depth-hl: transparent", "--rst-depth-contact: transparent", "--rst-depth-ambient: transparent", "--rst-depth-face: 0", "--rst-press-drop: 0px", "--rst-sheen-fill: transparent", "--rst-sheen-plain: transparent", "--rst-light-wash: transparent", "--rst-light-rim: var(--rst-line)", "--rst-press-wash: 0", "--rst-press-spot: 0", "--rst-press-wash-plain: 0", "--rst-press-spot-plain: 0", "--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", "--rst-summary-edge: transparent", "--rst-summary-edge-hover: transparent"} 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", "[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]", `[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`, "ButtonBorder", "CanvasText", "ButtonText", "Highlight", "HighlightText", "box-shadow: none") 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)", "[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]", "[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)") // The blocks are read as rules, the last top-level block of each // query being the one at the end of the file. A want is a rule's // selector when it ends in " {", a declaration when it holds ": ", // a selector a rule names when it starts with [ or ., a var() a value // reads, and otherwise a word a value holds. Read as text, any of them was satisfied by a // comment, and the block was cut by counting braces. rules := leafRules(t, css) printTop := -1 for _, block := range []struct { context string want []string }{ {"@media (forced-colors: active)", forced}, {"@media print", print}, } { last := -1 for _, r := range rules { if r.context == block.context { last = max(last, r.top) } } if last < 0 { t.Fatalf("tokens.css has no %s block at the end of the file", block.context) } if block.context == "@media print" { printTop = last } var in []leafRule for _, r := range rules { if r.top == last { in = append(in, r) } } for _, w := range block.want { if !blockCarries(in, w) { t.Errorf("the %s block does not carry %q", block.context, w) } } for _, r := range in { for _, d := range r.decls { if d[2] != "" { t.Errorf("the %s block uses !important (C11)", block.context) } } } } // Nothing that paints a button, a card or a nav item may follow the // print block: its rules win by order. for _, sel := range []string{`[rst-btn~="primary"]`, `[rst-shell-nav] a[aria-current]`, `[rst-box]`} { for _, r := range rules { if r.top > printTop && slices.ContainsFunc(splitSelectorList(r.selector), func(s string) bool { return strings.Contains(s, sel) }) { t.Errorf("%q is styled after the print block, by %s; the print rules rely on source order", sel, r.selector) } } } } // blockCarries reports whether the rules of one block carry want, read // as TestForcedColoursAndPrintFlattenTheAxisAndCoverEveryComponent // says. func blockCarries(rules []leafRule, want string) bool { return slices.ContainsFunc(rules, func(r leafRule) bool { sels := splitSelectorList(r.selector) switch { case strings.HasSuffix(want, " {"): return slices.Contains(sels, strings.TrimSuffix(want, " {")) case strings.Contains(want, ": "): prop, value, _ := strings.Cut(want, ": ") return slices.Contains(r.decls, [3]string{prop, value, ""}) case strings.HasPrefix(want, "[") || strings.HasPrefix(want, "."): return slices.ContainsFunc(sels, func(s string) bool { return strings.Contains(s, want) }) case strings.HasPrefix(want, "var("): name := strings.TrimSuffix(strings.TrimPrefix(want, "var("), ")") return slices.ContainsFunc(r.decls, func(d [3]string) bool { return slices.ContainsFunc(varRefs(d[1]), func(ref varRef) bool { return ref.name == name }) }) } return slices.ContainsFunc(r.decls, func(d [3]string) bool { return slices.Contains(strings.FieldsFunc(d[1], func(c rune) bool { return strings.ContainsRune(" ,()/", c) }), want) }) }) }