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