check-safety/colors_test.go
Alex Dunmow cd88c808b0 initial: standalone check-safety module hoisted from CMS
Static safety/lint runner for the BlockNinja codebase. ~25 invariant
checks across Go and frontend sources. Was at git.dev.alexdunmow.com:block/ninja
in backend/cmd/check-safety/ until the 2026-06-06 consolidation moved
the BlockNinja repos under a shared ~/src/blockninja/ parent.

This repo is the standalone extraction:
- Own go.mod (git.dev.alexdunmow.com/block/check-safety, go 1.26.4)
- Vendored internal/{helpers,theme} from CMS (Go's internal/ rule
  blocks cross-module imports; vendoring is the workaround)
- CLI contract unchanged: `check-safety <target-dir> [--flags]`
- CMS Makefile shells into ../check-safety for safety-check /
  install-safety-checker targets

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-06 13:04:02 +08:00

176 lines
5.5 KiB
Go

package main
import (
"os"
"path/filepath"
"strings"
"testing"
)
func TestCheckTemplColorsFlagsHardcodedColorsAndInlineStyles(t *testing.T) {
root := t.TempDir()
file := filepath.Join(root, "sample.templ")
content := `package sample
templ Example() {
<div class="text-blue-600 bg-[#fff]"></div>
<div class="border border-border" style="color: #fff; background: rgb(255, 0, 0)"></div>
<div style={ "background-color: hsl(12 100% 50%)" }></div>
}
`
if err := os.WriteFile(file, []byte(content), 0644); err != nil {
t.Fatalf("write sample templ: %v", err)
}
violations := checkTemplColors(root)
if len(violations) < 5 {
t.Fatalf("checkTemplColors() returned %d violations, want at least 5: %#v", len(violations), violations)
}
rules := map[string]bool{}
for _, violation := range violations {
rules[violation.rule] = true
}
for _, rule := range []string{
"no-hardcoded-tw-color",
"no-hex-in-class",
"no-tw-arbitrary-hex",
"no-inline-hex-style",
"no-inline-rgb-style",
} {
if !rules[rule] {
t.Fatalf("missing rule %q in violations: %#v", rule, violations)
}
}
}
func TestCheckTemplColorsAllowsSemanticTokensAndCSSVariables(t *testing.T) {
root := t.TempDir()
file := filepath.Join(root, "sample.templ")
content := `package sample
templ Example() {
<div class="bg-success/10 text-success border-border"></div>
<div style="background: hsl(var(--background)); color: hsl(var(--foreground))"></div>
<div style={ "background: hsl(var(--primary)); color: hsl(var(--primary-foreground))" }></div>
}
`
if err := os.WriteFile(file, []byte(content), 0644); err != nil {
t.Fatalf("write sample templ: %v", err)
}
violations := checkTemplColors(root)
if len(violations) != 0 {
t.Fatalf("checkTemplColors() returned %d violations, want 0: %#v", len(violations), violations)
}
}
func TestCheckNinjaTplColorsFlagsHardcodedColors(t *testing.T) {
root := t.TempDir()
tplDir := filepath.Join(root, "templates", "blocks")
if err := os.MkdirAll(tplDir, 0755); err != nil {
t.Fatalf("mkdir: %v", err)
}
file := filepath.Join(tplDir, "hero.ninjatpl")
content := `<section style="background:hsl(232,55%,4%);min-height:28rem;">
<h1 class="text-[hsl(210,18%,96%)] text-5xl">{{ headline }}</h1>
<p class="text-[rgba(220,232,248,0.75)]">{{ subtext }}</p>
<a style="background:hsl(168,82%,46%)">{{ primary_cta }}</a>
<div style="background:#0a0e24">hardcoded hex</div>
<span class="bg-[rgba(130,70,220,0.1)]">badge</span>
<span class="text-blue-500">named color</span>
</section>
`
if err := os.WriteFile(file, []byte(content), 0644); err != nil {
t.Fatalf("write ninjatpl: %v", err)
}
violations := checkNinjaTplColors(root)
if len(violations) < 6 {
t.Fatalf("checkNinjaTplColors() returned %d violations, want at least 6: %#v", len(violations), violations)
}
rules := map[string]bool{}
for _, v := range violations {
rules[v.rule] = true
}
for _, rule := range []string{
"no-hardcoded-hsl",
"no-tw-arbitrary-color",
"no-inline-hex",
"no-hardcoded-tw-color",
} {
if !rules[rule] {
t.Fatalf("missing rule %q in violations: %#v", rule, violations)
}
}
}
func TestCheckNinjaTplColorsAllowsCSSVariablesAndNeutralOverlays(t *testing.T) {
root := t.TempDir()
tplDir := filepath.Join(root, "templates", "blocks")
if err := os.MkdirAll(tplDir, 0755); err != nil {
t.Fatalf("mkdir: %v", err)
}
file := filepath.Join(tplDir, "hero.ninjatpl")
content := `<section class="bg-background text-foreground">
<h1 class="text-primary text-5xl">{{ headline }}</h1>
<p class="text-muted-foreground">{{ subtext }}</p>
<a style="background:hsl(var(--primary));color:hsl(var(--primary-foreground))">{{ cta }}</a>
<div style="background:rgba(0,0,0,0.5)">dark overlay</div>
<div style="background:rgba(255,255,255,0.1)">light overlay</div>
<span class="bg-primary/10 text-accent">badge</span>
</section>
`
if err := os.WriteFile(file, []byte(content), 0644); err != nil {
t.Fatalf("write ninjatpl: %v", err)
}
violations := checkNinjaTplColors(root)
if len(violations) != 0 {
t.Fatalf("checkNinjaTplColors() returned %d violations, want 0: %#v", len(violations), violations)
}
}
func TestFormatColorViolationsGroupsByFile(t *testing.T) {
report := formatColorViolations(".ninjatpl files", []colorViolation{
{file: "b.ninjatpl", line: 12, rule: "no-hardcoded-rgb", snippet: "rgba(1,2,3,0.5)"},
{file: "a.ninjatpl", line: 9, rule: "no-hardcoded-hsl", snippet: "hsl(10,20%,30%)"},
{file: "a.ninjatpl", line: 9, rule: "no-tw-arbitrary-color", snippet: "text-[hsl(10,20%,30%)]"},
{file: "a.ninjatpl", line: 4, rule: "no-inline-hex", snippet: "#fff"},
})
expected := strings.Join([]string{
" FAIL: 4 hardcoded color(s) in .ninjatpl files:",
" a.ninjatpl",
" 4 [no-inline-hex] #fff",
" 9 [no-hardcoded-hsl] hsl(10,20%,30%)",
" 9 [no-tw-arbitrary-color] text-[hsl(10,20%,30%)]",
" b.ninjatpl",
" 12 [no-hardcoded-rgb] rgba(1,2,3,0.5)",
}, "\n")
if report != expected {
t.Fatalf("formatColorViolations() mismatch\nwant:\n%s\n\ngot:\n%s", expected, report)
}
}
func TestCollectColorHintsDeduplicatesAndSorts(t *testing.T) {
hints := collectColorHints(
[]colorViolation{
{snippet: "text-blue-500"},
{snippet: "bg-amber-100"},
},
[]colorViolation{
{snippet: "text-blue-500"},
{snippet: "text-[hsl(38,52%,54%)]"},
},
)
expected := []string{"bg-warning/10", "hsl(var(--token))", "text-info"}
if strings.Join(hints, "|") != strings.Join(expected, "|") {
t.Fatalf("collectColorHints() = %#v, want %#v", hints, expected)
}
}