Add aria check program to go/cmd
This commit is contained in:
175
go/cmd/aria-check/check_test.go
Normal file
175
go/cmd/aria-check/check_test.go
Normal file
@@ -0,0 +1,175 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user