Files
kjol/go/cmd/aria-check/check_test.go

176 lines
5.7 KiB
Go

package main
import "testing"
// newTestResolver compiles the kjol theme layer (no app brand) against a fixed token
// set. baseDir "." is fine — nothing in the entry @imports a local file.
func newTestResolver(t *testing.T, tokens ...string) *Resolver {
t.Helper()
r, err := NewResolver("", ".", tokens)
if err != nil {
t.Fatalf("NewResolver: %v", err)
}
return r
}
func TestResolverSemanticTokens(t *testing.T) {
r := newTestResolver(t, "bg-surface", "text-ink", "text-white", "bg-red-500")
// Semantic surface: white in light, near-black in dark.
if c, ok := r.resolveToken("bg-surface", light); !ok || hexOf(c) != "#ffffff" {
t.Errorf("bg-surface light = %v (%s), want #ffffff", ok, hexOf(c))
}
if c, ok := r.resolveToken("bg-surface", dark); !ok || hexOf(c) != "#101013" {
t.Errorf("bg-surface dark = %v (%s), want #101013", ok, hexOf(c))
}
// text-white chases var(--color-white) → #fff.
if c, ok := r.resolveToken("text-white", light); !ok || hexOf(c) != "#ffffff" {
t.Errorf("text-white = %v (%s), want #ffffff", ok, hexOf(c))
}
// The palette OKLCH resolves to Tailwind's published hex.
if c, ok := r.resolveToken("bg-red-500", light); !ok || hexOf(c) != "#fb2c36" {
t.Errorf("bg-red-500 = %v (%s), want #fb2c36", ok, hexOf(c))
}
}
func TestResolverSideClassification(t *testing.T) {
r := newTestResolver(t, "bg-red-500", "text-ink", "flex", "text-sm", "p-4")
cases := map[string]string{
"bg-red-500": "bg",
"text-ink": "fg",
"flex": "", // not a colour utility
"text-sm": "", // font-size, not a colour
"p-4": "",
}
for tok, want := range cases {
if got := r.side(tok); got != want {
t.Errorf("side(%q) = %q, want %q", tok, got, want)
}
}
}
func TestResolverOpacityModifier(t *testing.T) {
r := newTestResolver(t, "bg-white/50")
c, ok := r.resolveToken("bg-white/50", light)
if !ok {
t.Fatal("bg-white/50 did not resolve")
}
approx(t, "bg-white/50 alpha", c.A, 0.5, 0.02)
}
func TestResolverArbitraryValue(t *testing.T) {
r := newTestResolver(t, "bg-[#123456]")
c, ok := r.resolveToken("bg-[#123456]", light)
if !ok || hexOf(c) != "#123456" {
t.Errorf("bg-[#123456] = %v (%s), want #123456", ok, hexOf(c))
}
}
func TestResolverDarkVariantToken(t *testing.T) {
r := newTestResolver(t, "dark:bg-surface")
// A dark: token resolves against the dark environment.
if c, ok := r.resolveToken("dark:bg-surface", dark); !ok || hexOf(c) != "#101013" {
t.Errorf("dark:bg-surface (dark) = %v (%s), want #101013", ok, hexOf(c))
}
}
// End to end: white-on-white fails; the semantic surface/ink pair passes in both
// themes (the two tokens move together, so dark mode stays legible).
func TestCheckGroupContrast(t *testing.T) {
r := newTestResolver(t, "bg-white", "text-white", "bg-surface", "text-ink")
// White on white: identical in both themes, so it collapses to one finding.
fail := checkGroup(ClassGroup{
File: "x.tsx", Line: 1, Tokens: []string{"bg-white", "text-white"},
}, r, Options{Level: "AA"})
if len(fail) != 1 || fail[0].Pass {
t.Fatalf("white-on-white should fail once, got %+v", fail)
}
// Surface/ink: light and dark are both checked (colours differ per theme) and
// both must pass.
pass := checkGroup(ClassGroup{
File: "x.tsx", Line: 2, Tokens: []string{"bg-surface", "text-ink"},
}, r, Options{Level: "AA"})
if len(pass) == 0 {
t.Fatal("surface/ink produced no findings")
}
for _, f := range pass {
if !f.Pass {
t.Errorf("surface/ink should pass in %s, got %.2f:1", f.ThemeName(), f.Ratio)
}
}
}
// A fixed palette background paired with an inverting semantic text token is a real
// dark-mode trap: bg-white stays white while text-ink climbs to near-white.
func TestCheckGroupFixedVsSemanticDarkTrap(t *testing.T) {
r := newTestResolver(t, "bg-white", "text-ink")
got := checkGroup(ClassGroup{
File: "x.tsx", Line: 1, Tokens: []string{"bg-white", "text-ink"},
}, r, Options{Level: "AA"})
var lightPass, darkFail bool
for _, f := range got {
if f.Theme == light && f.Pass {
lightPass = true
}
if f.Theme == dark && !f.Pass {
darkFail = true
}
}
if !lightPass || !darkFail {
t.Errorf("expected light pass + dark fail, got %+v", got)
}
}
// Scanner: a comment apostrophe ("panel's") must not open a string literal and
// swallow the class constants below it — the desync that produced dozens of bogus
// cross-paired findings.
func TestLexerSkipsCommentApostrophe(t *testing.T) {
src := []byte("// ModalSize selects the panel's max width.\n" +
"const a = \"text-white\"\n" +
"const b = \"bg-surface\"\n")
lits := extractLiterals(src)
if len(lits) != 2 {
t.Fatalf("expected 2 literals, got %d: %+v", len(lits), lits)
}
if lits[0].content != "text-white" || lits[1].content != "bg-surface" {
t.Errorf("unexpected literal contents: %+v", lits)
}
}
func TestLexerSingleQuoteExpressionPosition(t *testing.T) {
// Apostrophe in JSX text is not a string; a single-quoted attribute value is.
src := []byte("<p>don't click</p>\n<a class='bg-red-500 text-white'>x</a>\n")
lits := extractLiterals(src)
found := false
for _, l := range lits {
if l.content == "bg-red-500 text-white" {
found = true
}
}
if !found {
t.Errorf("single-quoted class attribute not extracted: %+v", lits)
}
}
func TestSplitVariants(t *testing.T) {
cases := []struct {
token string
base string
nvar int
}{
{"bg-red-500", "bg-red-500", 0},
{"dark:bg-surface", "bg-surface", 1},
{"dark:hover:text-ink", "text-ink", 2},
{"text-[color:red]", "text-[color:red]", 0}, // ':' inside [] is not a variant sep
}
for _, c := range cases {
v, base := splitVariants(c.token)
if base != c.base || len(v) != c.nvar {
t.Errorf("splitVariants(%q) = %v,%q; want %d variants, base %q", c.token, v, base, c.nvar, c.base)
}
}
}