package main import ( "os" "path/filepath" "testing" ) // ───────────────────────────────────────────── // checkTabState // ───────────────────────────────────────────── // checkTabState only scans files under "routes/" within the webSrcDir. func TestCheckTabState_FlagsUseStateWithTabInRoutes(t *testing.T) { dir := t.TempDir() routesDir := filepath.Join(dir, "routes") if err := os.MkdirAll(routesDir, 0755); err != nil { t.Fatalf("mkdir routes: %v", err) } writeFile(t, routesDir, "settings.tsx", ` import { useState } from 'react' export function Settings() { const [activeTab, setActiveTab] = useState('general') return } `) vs := checkTabState(dir) if len(vs) == 0 { t.Fatal("expected violation for useState activeTab in routes/, got 0") } if vs[0].file != "routes/settings.tsx" { t.Fatalf("expected file 'routes/settings.tsx', got %q", vs[0].file) } } func TestCheckTabState_FlagsUseStateTabAlt(t *testing.T) { dir := t.TempDir() routesDir := filepath.Join(dir, "routes") if err := os.MkdirAll(routesDir, 0755); err != nil { t.Fatalf("mkdir routes: %v", err) } // Alt pattern: [tabValue, setTabValue] = useState(...) writeFile(t, routesDir, "page.tsx", ` import { useState } from 'react' export function Page() { const [tabValue, setTabValue] = useState('first') return
} `) vs := checkTabState(dir) if len(vs) == 0 { t.Fatal("expected violation for tabValue useState alt pattern, got 0") } } func TestCheckTabState_AllowsPreferTabs(t *testing.T) { dir := t.TempDir() routesDir := filepath.Join(dir, "routes") if err := os.MkdirAll(routesDir, 0755); err != nil { t.Fatalf("mkdir routes: %v", err) } // "preferTabs" is in tabStateIgnorePatterns — should be allowed writeFile(t, routesDir, "settings.tsx", ` import { useState } from 'react' export function Settings() { const [preferTabs, setPreferTabs] = useState(true) return
} `) vs := checkTabState(dir) if len(vs) != 0 { t.Fatalf("expected 0 violations for preferTabs ignore pattern, got %d: %#v", len(vs), vs) } } func TestCheckTabState_IgnoresNonRoutesFiles(t *testing.T) { dir := t.TempDir() // File is in components/ — not routes/, should be skipped compDir := filepath.Join(dir, "components") if err := os.MkdirAll(compDir, 0755); err != nil { t.Fatalf("mkdir components: %v", err) } writeFile(t, compDir, "tabs.tsx", ` import { useState } from 'react' export function MyTabs() { const [activeTab, setActiveTab] = useState('a') return
} `) vs := checkTabState(dir) if len(vs) != 0 { t.Fatalf("expected 0 violations for useState in components/ (not routes/), got %d", len(vs)) } } func TestCheckTabState_IgnoresCommentedLines(t *testing.T) { dir := t.TempDir() routesDir := filepath.Join(dir, "routes") if err := os.MkdirAll(routesDir, 0755); err != nil { t.Fatalf("mkdir routes: %v", err) } writeFile(t, routesDir, "page.tsx", ` export function Page() { // const [activeTab, setActiveTab] = useState('a') return
} `) vs := checkTabState(dir) if len(vs) != 0 { t.Fatalf("expected 0 violations for commented-out useState, got %d", len(vs)) } } func TestCheckTabState_IgnoresNonTSXFiles(t *testing.T) { dir := t.TempDir() routesDir := filepath.Join(dir, "routes") if err := os.MkdirAll(routesDir, 0755); err != nil { t.Fatalf("mkdir routes: %v", err) } writeFile(t, routesDir, "helpers.ts", ` const [activeTab, setActiveTab] = useState('a') `) vs := checkTabState(dir) if len(vs) != 0 { t.Fatalf("expected 0 violations for .ts file (not .tsx), got %d", len(vs)) } } func TestCheckTabState_CleanRouteFile(t *testing.T) { dir := t.TempDir() routesDir := filepath.Join(dir, "routes") if err := os.MkdirAll(routesDir, 0755); err != nil { t.Fatalf("mkdir routes: %v", err) } // Uses URL search params — correct pattern writeFile(t, routesDir, "settings.tsx", ` import { useSearch } from '@tanstack/react-router' export function Settings() { const { tab } = useSearch({ from: '/settings' }) return } `) vs := checkTabState(dir) if len(vs) != 0 { t.Fatalf("expected 0 violations for URL-based tab routing, got %d: %#v", len(vs), vs) } } // ───────────────────────────────────────────── // checkESLintDisableNextLine // ───────────────────────────────────────────── func TestCheckESLintDisableNextLine_FlagsInTSX(t *testing.T) { dir := t.TempDir() writeFile(t, dir, "component.tsx", ` export function Foo() { // eslint-disable-next-line @typescript-eslint/no-explicit-any const x: any = {} return
{x}
} `) vs := checkESLintDisableNextLine(dir) if len(vs) == 0 { t.Fatal("expected violation for eslint-disable-next-line in .tsx, got 0") } if vs[0].line == 0 { t.Error("expected non-zero line number") } } func TestCheckESLintDisableNextLine_FlagsInTS(t *testing.T) { dir := t.TempDir() writeFile(t, dir, "utils.ts", ` // eslint-disable-next-line prefer-const var x = 5 `) vs := checkESLintDisableNextLine(dir) if len(vs) == 0 { t.Fatal("expected violation for eslint-disable-next-line in .ts, got 0") } } func TestCheckESLintDisableNextLine_FlagsInJS(t *testing.T) { dir := t.TempDir() writeFile(t, dir, "script.js", ` // eslint-disable-next-line no-console console.log('debug') `) vs := checkESLintDisableNextLine(dir) if len(vs) == 0 { t.Fatal("expected violation for eslint-disable-next-line in .js, got 0") } } func TestCheckESLintDisableNextLine_CleanFile(t *testing.T) { dir := t.TempDir() writeFile(t, dir, "clean.tsx", ` export function Foo() { const x = 5 return
{x}
} `) vs := checkESLintDisableNextLine(dir) if len(vs) != 0 { t.Fatalf("expected 0 violations for clean file, got %d: %#v", len(vs), vs) } } func TestCheckESLintDisableNextLine_SkipsNodeModules(t *testing.T) { dir := t.TempDir() nmDir := filepath.Join(dir, "node_modules", "somepkg") if err := os.MkdirAll(nmDir, 0755); err != nil { t.Fatalf("mkdir node_modules: %v", err) } writeFile(t, nmDir, "index.ts", ` // eslint-disable-next-line no-console console.log('pkg') `) vs := checkESLintDisableNextLine(dir) if len(vs) != 0 { t.Fatalf("expected 0 violations (node_modules should be skipped), got %d", len(vs)) } } func TestCheckESLintDisableNextLine_MultipleViolations(t *testing.T) { dir := t.TempDir() writeFile(t, dir, "messy.tsx", ` // eslint-disable-next-line @typescript-eslint/no-explicit-any const a: any = 1 // eslint-disable-next-line prefer-const var b = 2 `) vs := checkESLintDisableNextLine(dir) if len(vs) < 2 { t.Fatalf("expected at least 2 violations, got %d: %#v", len(vs), vs) } } // ───────────────────────────────────────────── // checkBareImports // ───────────────────────────────────────────── func TestCheckBareImports_FlagsBareAPIImport(t *testing.T) { dir := t.TempDir() writeFile(t, dir, "component.tsx", ` import { AdminUser } from '@block-ninja/api' export function MyComp() { return
} `) vs := checkBareImports(dir) if len(vs) == 0 { t.Fatal("expected violation for bare @block-ninja/api import, got 0") } if vs[0].line == 0 { t.Error("expected non-zero line number") } } func TestCheckBareImports_AllowsSubpathImport(t *testing.T) { dir := t.TempDir() writeFile(t, dir, "component.tsx", ` import type { AdminUser } from '@block-ninja/api/types' import { AdminService } from '@block-ninja/api/services' import { listAdminUsers } from '@block-ninja/api/queries' export function MyComp() { return
} `) vs := checkBareImports(dir) if len(vs) != 0 { t.Fatalf("expected 0 violations for subpath imports, got %d: %#v", len(vs), vs) } } func TestCheckBareImports_SkipsNonTSTSX(t *testing.T) { dir := t.TempDir() // .js file — not scanned by checkBareImports writeFile(t, dir, "script.js", ` import { something } from '@block-ninja/api' `) vs := checkBareImports(dir) if len(vs) != 0 { t.Fatalf("expected 0 violations for .js file (not .ts/.tsx), got %d", len(vs)) } } func TestCheckBareImports_SkipsNodeModules(t *testing.T) { dir := t.TempDir() nmDir := filepath.Join(dir, "node_modules", "@block-ninja", "ui") if err := os.MkdirAll(nmDir, 0755); err != nil { t.Fatalf("mkdir node_modules: %v", err) } writeFile(t, nmDir, "index.ts", ` import { something } from '@block-ninja/api' `) vs := checkBareImports(dir) if len(vs) != 0 { t.Fatalf("expected 0 violations in node_modules, got %d", len(vs)) } } func TestCheckBareImports_CleanTSFile(t *testing.T) { dir := t.TempDir() writeFile(t, dir, "util.ts", ` import type { AdminUser } from '@block-ninja/api/types' export function transform(u: AdminUser): string { return u.email } `) vs := checkBareImports(dir) if len(vs) != 0 { t.Fatalf("expected 0 violations for clean .ts file, got %d: %#v", len(vs), vs) } } // ───────────────────────────────────────────── // checkLockfiles // ───────────────────────────────────────────── func TestCheckLockfiles_FlagsPackageLockJSON(t *testing.T) { dir := t.TempDir() writeFile(t, dir, "package-lock.json", `{"lockfileVersion":2}`) vs := checkLockfiles(dir) if len(vs) == 0 { t.Fatal("expected violation for package-lock.json, got 0") } found := false for _, v := range vs { if v.file == "package-lock.json" { found = true } } if !found { t.Fatalf("expected 'package-lock.json' in violations, got: %#v", vs) } } func TestCheckLockfiles_FlagsYarnLock(t *testing.T) { dir := t.TempDir() writeFile(t, dir, "yarn.lock", `# yarn lockfile v1`) vs := checkLockfiles(dir) if len(vs) == 0 { t.Fatal("expected violation for yarn.lock, got 0") } found := false for _, v := range vs { if v.file == "yarn.lock" { found = true } } if !found { t.Fatalf("expected 'yarn.lock' in violations, got: %#v", vs) } } func TestCheckLockfiles_AllowsPnpmLockYaml(t *testing.T) { dir := t.TempDir() writeFile(t, dir, "pnpm-lock.yaml", `lockfileVersion: '9.0'`) vs := checkLockfiles(dir) if len(vs) != 0 { t.Fatalf("expected 0 violations for pnpm-lock.yaml, got %d: %#v", len(vs), vs) } } func TestCheckLockfiles_SkipsNodeModules(t *testing.T) { dir := t.TempDir() nmDir := filepath.Join(dir, "node_modules", "somepkg") if err := os.MkdirAll(nmDir, 0755); err != nil { t.Fatalf("mkdir node_modules: %v", err) } writeFile(t, nmDir, "package-lock.json", `{}`) vs := checkLockfiles(dir) if len(vs) != 0 { t.Fatalf("expected 0 violations (node_modules should be skipped), got %d", len(vs)) } } func TestCheckLockfiles_NoLockfile(t *testing.T) { dir := t.TempDir() writeFile(t, dir, "package.json", `{"name":"test"}`) vs := checkLockfiles(dir) if len(vs) != 0 { t.Fatalf("expected 0 violations for repo with no lockfiles, got %d", len(vs)) } } // ───────────────────────────────────────────── // checkButtonAutomation // ───────────────────────────────────────────── func TestCheckButtonAutomation_FlagsButtonWithoutAction(t *testing.T) { dir := t.TempDir() writeFile(t, dir, "page.tsx", ` export function Page() { return ( ) } `) vs := checkButtonAutomation(dir) if len(vs) == 0 { t.Fatal("expected violation for ) } `) vs := checkButtonAutomation(dir) if len(vs) != 0 { t.Fatalf("expected 0 violations for ) } `) vs := checkButtonAutomation(dir) if len(vs) != 0 { t.Fatalf("expected 0 violations for type='submit' button, got %d: %#v", len(vs), vs) } } func TestCheckButtonAutomation_AllowsAlertDialogAction(t *testing.T) { dir := t.TempDir() // AlertDialogAction doesn't support action= per CLAUDE.md writeFile(t, dir, "confirm.tsx", ` export function Confirm() { return ( Confirm Delete ) } `) vs := checkButtonAutomation(dir) if len(vs) != 0 { t.Fatalf("expected 0 violations for AlertDialogAction, got %d: %#v", len(vs), vs) } } func TestCheckButtonAutomation_SkipsUIComponents(t *testing.T) { dir := t.TempDir() uiDir := filepath.Join(dir, "components", "ui") if err := os.MkdirAll(uiDir, 0755); err != nil { t.Fatalf("mkdir components/ui: %v", err) } // UI primitive definitions — exempt from check writeFile(t, uiDir, "button.tsx", ` import * as React from 'react' const Button = React.forwardRef( ({ className, variant, ...props }, ref) => { return
content
) } `) vs := checkButtonAutomation(dir) if len(vs) != 0 { t.Fatalf("expected 0 violations for commented-out Button, got %d: %#v", len(vs), vs) } } func TestCheckButtonAutomation_MultiLineActionOnNextLine(t *testing.T) { dir := t.TempDir() // action= is on the next line — collectJSXTag lookahead should find it writeFile(t, dir, "page.tsx", ` export function Page() { return ( ) } `) vs := checkButtonAutomation(dir) if len(vs) != 0 { t.Fatalf("expected 0 violations for multi-line ' `) vs := checkButtonAutomation(dir) if len(vs) != 0 { t.Fatalf("expected 0 violations for non-.tsx file, got %d", len(vs)) } } // ───────────────────────────────────────────── // collectJSXTag // ───────────────────────────────────────────── func TestCollectJSXTag_SingleLine(t *testing.T) { lines := []string{``} result := collectJSXTag(lines, 0) if result == "" { t.Fatal("collectJSXTag() returned empty string for single-line tag") } } func TestCollectJSXTag_MultiLinePropExtraction(t *testing.T) { lines := []string{ ``, ` Save`, ``, } result := collectJSXTag(lines, 0) // Should contain action= because it's prop-level content if !reButtonAction.MatchString(result) { t.Fatalf("collectJSXTag() = %q, expected to contain 'action='", result) } } func TestCollectJSXTag_StripsBraceExpressions(t *testing.T) { lines := []string{ ` { doSomething() }}`, ` action="save"`, `>`, } result := collectJSXTag(lines, 0) // The brace expression is stripped but action= should still be present if !reButtonAction.MatchString(result) { t.Fatalf("collectJSXTag() = %q, expected 'action=' to be preserved outside braces", result) } }