/** * Tests for: consistent-dark-mode * * Ensures consistent dark mode theming in Tailwind CSS classes. */ import { RuleTester } from "@typescript-eslint/rule-tester"; import { describe, it, afterAll } from "vitest"; import rule from "./consistent-dark-mode.js"; RuleTester.afterAll = afterAll; RuleTester.describe = describe; RuleTester.it = it; const ruleTester = new RuleTester({ languageOptions: { ecmaVersion: 2022, sourceType: "module", parserOptions: { ecmaFeatures: { jsx: true }, }, }, }); ruleTester.run("consistent-dark-mode", rule, { valid: [ // ============================================ // CONSISTENT DARK MODE (all colors themed) // ============================================ { name: "all background colors have dark variants", code: `
`, }, { name: "all text colors have dark variants", code: ``, }, { name: "all border colors have dark variants", code: ``, }, { name: "multiple color types all with dark variants", code: ``, }, { name: "gradient colors all with dark variants", code: ``, }, { name: "ring colors with dark variants", code: ``, }, { name: "ring and ring-offset are independent - only ring themed is valid", code: ``, }, { name: "ring and ring-offset are independent - only ring-offset themed is valid", code: ``, }, // ============================================ // NO COLOR CLASSES (nothing to check) // ============================================ { name: "no color classes - layout only", code: ``, }, { name: "no color classes - sizing only", code: ``, }, { name: "no color classes - typography without color", code: ``, }, { name: "empty className", code: ``, }, { name: "whitespace only className", code: ``, }, // ============================================ // EXEMPT VALUES (don't need dark variants) // ============================================ { name: "transparent background is exempt", code: ``, }, { name: "inherit text color is exempt", code: ``, }, { name: "current border color is exempt", code: ``, }, { name: "multiple exempt values", code: ``, }, { name: "exempt mixed with properly themed colors", code: ``, }, { name: "shadow-none is exempt", code: ``, }, // ============================================ // ONLY DARK VARIANTS (edge case - valid) // ============================================ { name: "only dark variant colors", code: ``, }, // ============================================ // COMPLEX VARIANT CHAINS // ============================================ { name: "hover:dark: variant chain", code: ``, }, { name: "responsive dark variants", code: ``, }, { name: "focus and dark variants", code: ``, }, { name: "complex variant chain with dark", code: ``, }, // ============================================ // cn() / clsx() / classnames() CALLS - VALID // ============================================ { name: "cn() with consistent dark mode", code: `cn("bg-white dark:bg-slate-900 text-gray-900 dark:text-white")`, }, { name: "clsx() with consistent dark mode", code: `clsx("border-gray-200 dark:border-gray-700")`, }, { name: "classnames() with consistent dark mode", code: `classnames("bg-blue-500 dark:bg-blue-400")`, }, { name: "cva() with consistent dark mode", code: `cva("bg-white dark:bg-black text-gray-900 dark:text-gray-100")`, }, { name: "twMerge() with consistent dark mode", code: `twMerge("bg-red-500 dark:bg-red-400")`, }, { name: "cn() with no color classes", code: `cn("flex items-center gap-4")`, }, { name: "cn() with array of consistent classes", code: `cn(["bg-white dark:bg-black", "text-gray-900 dark:text-white"])`, }, // ============================================ // TEMPLATE LITERALS - VALID // ============================================ { name: "template literal with consistent dark mode", code: "", }, { name: "cn() with template literal - consistent", code: "cn(`bg-blue-500 dark:bg-blue-400 text-white dark:text-gray-100`)", }, // ============================================ // JSX EXPRESSION CONTAINERS - VALID // ============================================ { name: "JSX expression with string literal - consistent", code: ``, }, // ============================================ // DIFFERENT BORDER VARIANTS (grouped correctly) // ============================================ { name: "different border sides all themed", code: ``, }, // ============================================ // MISC VALID CASES // ============================================ { name: "class attribute (not className)", code: ``, }, { name: "placeholder color with dark variant", code: ``, }, { name: "divide color with dark variant", code: ``, }, { name: "accent color with dark variant", code: ``, }, { name: "caret color with dark variant", code: ``, }, { name: "outline color with dark variant", code: ``, }, { name: "fill and stroke with dark variants", code: ``, }, { name: "decoration color with dark variant", code: ``, }, // ============================================ // SEMANTIC/THEMED COLORS (shadcn, etc.) - EXEMPT // CSS variable-based colors that handle dark mode automatically. // These should NEVER trigger issues because they're not explicit Tailwind colors. // ============================================ { name: "shadcn background/foreground semantic colors", code: ``, }, { name: "shadcn card semantic colors", code: ``, }, { name: "shadcn popover semantic colors", code: ``, }, { name: "shadcn primary semantic colors", code: ``, }, { name: "shadcn secondary semantic colors", code: ``, }, { name: "shadcn muted semantic colors", code: ``, }, { name: "shadcn accent semantic colors", code: ``, }, { name: "shadcn destructive semantic colors", code: ``, }, { name: "shadcn border semantic color", code: ``, }, { name: "shadcn input semantic color", code: ``, }, { name: "shadcn ring semantic color", code: ``, }, { name: "shadcn sidebar semantic colors", code: ``, }, { name: "mixed semantic and properly themed hard-coded colors", code: ``, }, { name: "cn() with semantic colors only", code: `cn("bg-background text-foreground border-border")`, }, { name: "semantic colors with dark variant (redundant but valid)", code: ``, }, { name: "chart semantic colors", code: ``, }, { name: "all semantic color types in one element", code: ``, }, // ============================================ // CUSTOM/NON-TAILWIND COLORS - SHOULD NOT TRIGGER // Any color name that is NOT a built-in Tailwind color should be exempt. // These are assumed to be CSS variables or custom theme colors. // ============================================ { name: "custom brand color - should not trigger", code: ``, }, { name: "custom company colors - should not trigger", code: ``, }, { name: "custom gradient colors - should not trigger", code: ``, }, { name: "arbitrary custom color names - should not trigger", code: ``, }, { name: "custom color mixed with themed Tailwind color", code: ``, }, { name: "custom ring and fill colors - should not trigger", code: ``, }, // ============================================ // warnOnMissingDarkMode: false (file-level warning disabled) // ============================================ { name: "no dark mode but warning disabled", code: ``, options: [{ warnOnMissingDarkMode: false }], }, { name: "multiple elements no dark mode but warning disabled", code: ` function Component() { return ( <> > ); } `, options: [{ warnOnMissingDarkMode: false }], }, ], invalid: [ // ============================================ // INCONSISTENT DARK MODE - BACKGROUND // ============================================ { name: "bg has dark variant but text does not", code: ``, errors: [ { messageId: "inconsistentDarkMode", data: { unthemed: "text-gray-900" }, }, ], }, { name: "text has dark variant but bg does not", code: ``, errors: [ { messageId: "inconsistentDarkMode", data: { unthemed: "bg-white" }, }, ], }, // ============================================ // INCONSISTENT DARK MODE - BORDERS // ============================================ { name: "border has dark variant but bg does not", code: ``, errors: [ { messageId: "inconsistentDarkMode", data: { unthemed: "bg-gray-100" }, }, ], }, { name: "border-t has dark but border-b does not (same group)", code: ``, errors: [ { messageId: "inconsistentDarkMode", data: { unthemed: "border-b-gray-300" }, }, ], }, // ============================================ // INCONSISTENT DARK MODE - GRADIENTS // ============================================ { name: "from has dark but to does not (gradient group)", code: ``, errors: [ { messageId: "inconsistentDarkMode", data: { unthemed: "to-purple-500" }, }, ], }, { name: "gradient missing via dark variant", code: ``, errors: [ { messageId: "inconsistentDarkMode", data: { unthemed: "via-orange-500" }, }, ], }, // ============================================ // INCONSISTENT DARK MODE - RING // ============================================ { name: "ring has dark variant but ring-offset does not (independent properties)", code: ``, errors: [ { messageId: "inconsistentDarkMode", data: { unthemed: "ring-offset-white" }, }, ], }, // ============================================ // INCONSISTENT DARK MODE - MULTIPLE UNTHEMED // ============================================ { name: "multiple unthemed color classes", code: ``, errors: [ { messageId: "inconsistentDarkMode", data: { unthemed: "text-gray-900, border-gray-200" }, }, ], }, // ============================================ // cn() / clsx() / classnames() - INCONSISTENT // ============================================ { name: "cn() with inconsistent dark mode", code: `cn("bg-white dark:bg-slate-900 text-gray-900")`, errors: [ { messageId: "inconsistentDarkMode", data: { unthemed: "text-gray-900" }, }, ], }, { name: "clsx() with inconsistent dark mode", code: `clsx("bg-blue-500 dark:bg-blue-400 border-blue-600")`, errors: [ { messageId: "inconsistentDarkMode", data: { unthemed: "border-blue-600" }, }, ], }, { name: "classnames() with inconsistent dark mode", code: `classnames("text-red-500 dark:text-red-400 bg-red-50")`, errors: [ { messageId: "inconsistentDarkMode", data: { unthemed: "bg-red-50" }, }, ], }, { name: "cva() with inconsistent dark mode", code: `cva("bg-white dark:bg-black text-gray-900")`, errors: [ { messageId: "inconsistentDarkMode", data: { unthemed: "text-gray-900" }, }, ], }, { name: "twMerge() with inconsistent dark mode", code: `twMerge("bg-white dark:bg-black text-black")`, errors: [ { messageId: "inconsistentDarkMode", data: { unthemed: "text-black" }, }, ], }, // ============================================ // ARRAY ARGUMENTS - INCONSISTENT // ============================================ { name: "cn() array with inconsistent class string", code: `cn(["bg-white dark:bg-black text-gray-900"])`, errors: [ { messageId: "inconsistentDarkMode", data: { unthemed: "text-gray-900" }, }, ], }, // ============================================ // TEMPLATE LITERALS - INCONSISTENT // ============================================ { name: "template literal with inconsistent dark mode", code: "", errors: [ { messageId: "inconsistentDarkMode", data: { unthemed: "text-gray-900" }, }, ], }, { name: "cn() template literal - inconsistent", code: "cn(`bg-blue-500 dark:bg-blue-400 text-white`)", errors: [ { messageId: "inconsistentDarkMode", data: { unthemed: "text-white" }, }, ], }, // ============================================ // JSX EXPRESSION STRING - INCONSISTENT // ============================================ { name: "JSX expression string literal - inconsistent", code: ``, errors: [ { messageId: "inconsistentDarkMode", data: { unthemed: "text-gray-900" }, }, ], }, // ============================================ // class ATTRIBUTE (not className) // ============================================ { name: "class attribute with inconsistent dark mode", code: ``, errors: [ { messageId: "inconsistentDarkMode", data: { unthemed: "text-gray-900" }, }, ], }, // ============================================ // COMPLEX VARIANTS - INCONSISTENT // ============================================ { name: "hover variant present but no dark for one color type", code: ``, errors: [ { messageId: "inconsistentDarkMode", data: { unthemed: "text-gray-900, hover:text-black" }, }, ], }, // ============================================ // MISSING DARK MODE (file-level warning) // ============================================ { name: "single element with colors but no dark mode", code: ``, errors: [ { messageId: "missingDarkMode", }, ], }, { name: "multiple elements with colors but no dark mode", code: ` function Component() { return ( <> > ); } `, errors: [ { messageId: "missingDarkMode", }, ], }, { name: "cn() with colors but no dark mode", code: `cn("bg-white text-gray-900 border-gray-200")`, errors: [ { messageId: "missingDarkMode", }, ], }, { name: "only background color no dark mode", code: ``, errors: [ { messageId: "missingDarkMode", }, ], }, { name: "only text color no dark mode", code: ``, errors: [ { messageId: "missingDarkMode", }, ], }, { name: "only border color no dark mode", code: ``, errors: [ { messageId: "missingDarkMode", }, ], }, { name: "gradient without dark mode", code: ``, errors: [ { messageId: "missingDarkMode", }, ], }, // ============================================ // BOTH ERRORS IN SAME FILE // ============================================ { name: "inconsistent in one element, missing in file", code: ` function Component() { return ( <> > ); } `, errors: [ { messageId: "inconsistentDarkMode", data: { unthemed: "text-gray-900" }, }, ], }, // ============================================ // EDGE CASES - SPECIFIC COLOR CLASSES // ============================================ { name: "placeholder color inconsistent", code: ``, errors: [ { messageId: "inconsistentDarkMode", data: { unthemed: "placeholder-gray-400" }, }, ], }, { name: "divide color inconsistent", code: ``, errors: [ { messageId: "inconsistentDarkMode", data: { unthemed: "divide-gray-200" }, }, ], }, { name: "shadow color inconsistent", code: ``, errors: [ { messageId: "inconsistentDarkMode", data: { unthemed: "shadow-blue-500/50" }, }, ], }, { name: "fill inconsistent with stroke", code: ``, errors: [ { messageId: "inconsistentDarkMode", data: { unthemed: "stroke-gray-900" }, }, ], }, { name: "decoration color inconsistent", code: ``, errors: [ { messageId: "inconsistentDarkMode", data: { unthemed: "decoration-red-500" }, }, ], }, { name: "accent color inconsistent", code: ``, errors: [ { messageId: "inconsistentDarkMode", data: { unthemed: "accent-blue-500" }, }, ], }, { name: "caret color inconsistent", code: ``, errors: [ { messageId: "inconsistentDarkMode", data: { unthemed: "caret-blue-500" }, }, ], }, { name: "outline color inconsistent", code: ``, errors: [ { messageId: "inconsistentDarkMode", data: { unthemed: "outline-blue-500" }, }, ], }, // ============================================ // SEMANTIC MIXED WITH HARD-CODED - INCONSISTENT // ============================================ { name: "semantic bg but hard-coded text without dark variant when other hard-coded has dark", code: ``, errors: [ { messageId: "inconsistentDarkMode", data: { unthemed: "text-gray-900" }, }, ], }, // ============================================ // EXPLICIT TAILWIND COLORS - SHOULD TRIGGER // These are built-in Tailwind color names that should require dark variants. // ============================================ { name: "explicit Tailwind colors - white/black", code: ``, errors: [ { messageId: "missingDarkMode", }, ], }, { name: "explicit Tailwind colors - slate scale", code: ``, errors: [ { messageId: "missingDarkMode", }, ], }, { name: "explicit Tailwind colors - various palette colors", code: ``, errors: [ { messageId: "missingDarkMode", }, ], }, { name: "explicit Tailwind colors - all gray variants", code: ``, errors: [ { messageId: "missingDarkMode", }, ], }, { name: "explicit Tailwind colors - modern palette", code: ``, errors: [ { messageId: "missingDarkMode", }, ], }, { name: "explicit Tailwind colors - warm palette", code: ``, errors: [ { messageId: "missingDarkMode", }, ], }, { name: "explicit Tailwind colors - with opacity modifier", code: ``, errors: [ { messageId: "missingDarkMode", }, ], }, ], });