/** * Tests for: import-graph utilities * * Tests the cross-file import resolution and library detection. */ import { describe, it, expect, beforeEach, afterEach } from "vitest"; import { writeFileSync, mkdirSync, rmSync, existsSync, realpathSync } from "fs"; import { join } from "path"; import { tmpdir } from "os"; import { getComponentLibrary, clearCache } from "../rules/no-mixed-component-libraries/lib/import-graph"; import { detectLibraryFromSource } from "../rules/no-mixed-component-libraries/lib/component-parser"; import { resolveImportPath, resolveExport, clearResolverCaches, } from "../rules/no-mixed-component-libraries/lib/export-resolver"; describe("detectLibraryFromSource", () => { it("detects MUI from @mui/material", () => { expect(detectLibraryFromSource("@mui/material")).toBe("mui"); }); it("detects MUI from @mui/icons-material", () => { expect(detectLibraryFromSource("@mui/icons-material")).toBe("mui"); }); it("detects shadcn from @/components/ui path", () => { expect(detectLibraryFromSource("@/components/ui/button")).toBe("shadcn"); }); it("detects shadcn from relative components/ui path", () => { expect(detectLibraryFromSource("./components/ui/button")).toBe("shadcn"); }); it("detects shadcn from @radix-ui", () => { expect(detectLibraryFromSource("@radix-ui/react-dialog")).toBe("shadcn"); }); it("detects chakra from @chakra-ui", () => { expect(detectLibraryFromSource("@chakra-ui/react")).toBe("chakra"); }); it("detects antd from antd package", () => { expect(detectLibraryFromSource("antd")).toBe("antd"); }); it("detects antd from @ant-design", () => { expect(detectLibraryFromSource("@ant-design/icons")).toBe("antd"); }); it("returns null for unknown libraries", () => { expect(detectLibraryFromSource("./my-custom-component")).toBeNull(); }); it("returns null for react", () => { expect(detectLibraryFromSource("react")).toBeNull(); }); it("returns null for next", () => { expect(detectLibraryFromSource("next/link")).toBeNull(); }); }); describe("getComponentLibrary - direct imports", () => { beforeEach(() => { clearCache(); }); it("identifies MUI component from direct import", () => { const result = getComponentLibrary( "/project/page.tsx", "Button", "@mui/material" ); expect(result.library).toBe("mui"); expect(result.isLocalComponent).toBe(false); expect(result.internalLibraries.size).toBe(0); }); it("identifies Chakra component from direct import", () => { const result = getComponentLibrary( "/project/page.tsx", "Box", "@chakra-ui/react" ); expect(result.library).toBe("chakra"); expect(result.isLocalComponent).toBe(false); }); it("identifies Ant Design component from direct import", () => { const result = getComponentLibrary("/project/page.tsx", "Button", "antd"); expect(result.library).toBe("antd"); expect(result.isLocalComponent).toBe(false); }); it("identifies shadcn component from @/components/ui import", () => { const result = getComponentLibrary( "/project/page.tsx", "Button", "@/components/ui/button" ); expect(result.library).toBe("shadcn"); expect(result.isLocalComponent).toBe(false); }); it("returns null library for unknown external imports", () => { const result = getComponentLibrary( "/project/page.tsx", "SomeComponent", "some-unknown-package" ); expect(result.library).toBeNull(); expect(result.isLocalComponent).toBe(false); }); }); describe("cross-file analysis with temp files", () => { let testDir: string; beforeEach(() => { clearCache(); clearResolverCaches(); // Create a temporary test directory testDir = join(tmpdir(), `uilint-test-${Date.now()}`); mkdirSync(testDir, { recursive: true }); }); afterEach(() => { // Clean up if (existsSync(testDir)) { rmSync(testDir, { recursive: true, force: true }); } }); it("detects MUI usage in local component", () => { // Create a local component that uses MUI const cardPath = join(testDir, "MyCard.tsx"); writeFileSync( cardPath, ` import { Card, CardContent } from "@mui/material"; export function MyCard() { return ( Hello ); } ` ); // Create tsconfig.json for path resolution writeFileSync( join(testDir, "tsconfig.json"), JSON.stringify({ compilerOptions: { baseUrl: ".", paths: { "@/*": ["./*"] }, }, }) ); const pagePath = join(testDir, "page.tsx"); const result = getComponentLibrary(pagePath, "MyCard", "./MyCard"); expect(result.isLocalComponent).toBe(true); expect(result.internalLibraries.has("mui")).toBe(true); expect(result.libraryEvidence.some((e) => e.library === "mui")).toBe(true); }); it("follows re-exports through barrel files", () => { // Create the actual component const buttonPath = join(testDir, "components", "Button.tsx"); mkdirSync(join(testDir, "components"), { recursive: true }); writeFileSync( buttonPath, ` import { Button as MuiButton } from "@mui/material"; export function Button() { return Click me; } ` ); // Create barrel file writeFileSync( join(testDir, "components", "index.ts"), `export { Button } from "./Button";` ); const pagePath = join(testDir, "page.tsx"); const result = getComponentLibrary(pagePath, "Button", "./components"); expect(result.isLocalComponent).toBe(true); expect(result.internalLibraries.has("mui")).toBe(true); }); it("handles components with no library usage", () => { const componentPath = join(testDir, "PureComponent.tsx"); writeFileSync( componentPath, ` export function PureComponent() { return
Pure HTML
; } ` ); const pagePath = join(testDir, "page.tsx"); const result = getComponentLibrary( pagePath, "PureComponent", "./PureComponent" ); expect(result.isLocalComponent).toBe(true); expect(result.library).toBeNull(); expect(result.internalLibraries.size).toBe(0); }); it("detects nested library usage (component uses component that uses library)", () => { // Create nested structure mkdirSync(join(testDir, "components"), { recursive: true }); // Inner component uses MUI writeFileSync( join(testDir, "components", "InnerCard.tsx"), ` import { Card } from "@mui/material"; export function InnerCard({ children }) { return {children}; } ` ); // Outer component uses inner component writeFileSync( join(testDir, "components", "OuterCard.tsx"), ` import { InnerCard } from "./InnerCard"; export function OuterCard({ title }) { return {title}; } ` ); const pagePath = join(testDir, "page.tsx"); const result = getComponentLibrary( pagePath, "OuterCard", "./components/OuterCard" ); expect(result.isLocalComponent).toBe(true); expect(result.internalLibraries.has("mui")).toBe(true); // Should show evidence chain expect(result.libraryEvidence.length).toBeGreaterThan(0); }); it("handles circular dependencies gracefully", () => { mkdirSync(join(testDir, "components"), { recursive: true }); // Component A imports B writeFileSync( join(testDir, "components", "ComponentA.tsx"), ` import { ComponentB } from "./ComponentB"; export function ComponentA() { return ; } ` ); // Component B imports A (circular) writeFileSync( join(testDir, "components", "ComponentB.tsx"), ` import { ComponentA } from "./ComponentA"; export function ComponentB() { return
B
; } ` ); const pagePath = join(testDir, "page.tsx"); // Should not hang or throw const result = getComponentLibrary( pagePath, "ComponentA", "./components/ComponentA" ); expect(result.isLocalComponent).toBe(true); // Should complete without error }); }); describe("resolveImportPath", () => { let testDir: string; beforeEach(() => { clearResolverCaches(); testDir = join(tmpdir(), `uilint-resolve-test-${Date.now()}`); mkdirSync(testDir, { recursive: true }); }); afterEach(() => { if (existsSync(testDir)) { rmSync(testDir, { recursive: true, force: true }); } }); it("resolves relative imports", () => { writeFileSync( join(testDir, "button.tsx"), "export const Button = () => {};" ); const result = resolveImportPath("./button", join(testDir, "page.tsx")); // Use realpathSync to normalize paths (handles /var vs /private/var on macOS) expect(result).toBe(realpathSync(join(testDir, "button.tsx"))); }); it("resolves index files", () => { mkdirSync(join(testDir, "components"), { recursive: true }); writeFileSync( join(testDir, "components", "index.ts"), "export const X = 1;" ); const result = resolveImportPath("./components", join(testDir, "page.tsx")); // Use realpathSync to normalize paths (handles /var vs /private/var on macOS) expect(result).toBe(realpathSync(join(testDir, "components", "index.ts"))); }); it("returns null for node_modules packages", () => { const result = resolveImportPath("react", join(testDir, "page.tsx")); expect(result).toBeNull(); }); it("returns null for @mui packages", () => { const result = resolveImportPath( "@mui/material", join(testDir, "page.tsx") ); expect(result).toBeNull(); }); }); describe("resolveExport", () => { let testDir: string; beforeEach(() => { clearResolverCaches(); testDir = join(tmpdir(), `uilint-export-test-${Date.now()}`); mkdirSync(testDir, { recursive: true }); }); afterEach(() => { if (existsSync(testDir)) { rmSync(testDir, { recursive: true, force: true }); } }); it("resolves direct named export", () => { const filePath = join(testDir, "button.tsx"); writeFileSync(filePath, "export function Button() { return