diff --git a/packages/tests/README.md b/packages/tests/README.md index 3d11972b5c0..1620fb49e29 100644 --- a/packages/tests/README.md +++ b/packages/tests/README.md @@ -1,34 +1,246 @@ # Tests -This package contains integration tests that verify the shadcn CLI works correctly with a local registry. The tests run actual CLI commands against test fixtures to ensure files are created and updated properly. - + + +This package contains comprehensive integration tests that verify the shadcn CLI works correctly with a local registry. The tests run actual CLI commands against test fixtures to ensure files are created and updated properly. + +## 🚀 Features + +- **Style Refactoring Tests**: Validates the new `base-lyra` and `radix-nova` style variants +- **Component Validation**: Ensures consistency across different style implementations +- **Registry Integration**: Tests component addition from various registries +- **Performance Monitoring**: Tracks test execution performance +- **Comprehensive Coverage**: Validates imports, exports, CSS variables, and component structure + +## 📋 Test Categories -Run the following command from the root of the workspace: +### Core Tests +- `add.test.ts` - Tests component addition functionality +- `init.test.ts` - Tests project initialization +- `registries.test.ts` - Tests registry configuration and usage +- `search.test.ts` - Tests component search functionality +- `view.test.ts` - Tests component viewing +### Style Refactoring Tests +- `style-refactoring.test.ts` - Validates new style architecture +- `component-differences.test.ts` - Tests differences between style variants +- `validation.test.ts` - Comprehensive validation utilities + +## 🏃 Running Tests + +### From Workspace Root ```bash pnpm tests:test ``` -## Writing Tests +### Individual Test Commands +```bash +# Run all tests +npm run test + +# Run tests in watch mode +npm run test:watch + +# Run tests with UI +npm run test:ui + +# Run coverage +npm run test:coverage + +# Type checking +npm run typecheck + +# Format code +npm run format:write +``` + +## 📝 Writing Tests +### Basic Test Structure ```typescript import { createFixtureTestDirectory, - fileExists, npxShadcn, } from "../utils/helpers" +import { configureRegistries, createRegistryServer } from "../utils/registry" describe("my test suite", () => { it("should do something", async () => { // Create a test directory from a fixture const testDir = await createFixtureTestDirectory("next-app") + // Configure registry if needed + await configureRegistries(testDir, { + "@my-registry": "http://localhost:4000/r/{name}", + }) + // Run CLI command - await npxShadcn(testDir, ["init", "--base-color=neutral"]) + await npxShadcn(testDir, ["add", "button"]) // Make assertions - expect(await fileExists(path.join(testDir, "components.json"))).toBe(true) + expect(await fs.pathExists(path.join(testDir, "components/ui/button.tsx"))).toBe(true) }) }) ``` + +### Style Variant Testing +```typescript +import { TestHelper } from "../utils/test-helpers" + +describe("style variant tests", () => { + it("should validate style consistency", async () => { + const testDir = await createFixtureTestDirectory("next-app") + await npxShadcn(testDir, ["add", "accordion"]) + + const baseLyraPath = path.join(testDir, "components/ui/base-lyra/accordion.tsx") + const radixNovaPath = path.join(testDir, "components/ui/radix-nova/accordion.tsx") + + const consistency = await TestHelper.checkStyleVariantConsistency(baseLyraPath, radixNovaPath) + expect(consistency.consistent).toBe(true) + }) +}) +``` + +### Performance Testing +```typescript +import { performanceMonitor } from "../utils/test-helpers" + +describe("performance tests", () => { + it("should complete within time limit", async () => { + const duration = performanceMonitor.measure("test-operation", async () => { + // Perform operation + await someAsyncOperation() + }) + + expect(duration).toBeLessThan(5000) // 5 seconds max + }) +}) +``` + +## 🛠️ Test Utilities + +### Core Helpers +- `createFixtureTestDirectory()` - Creates isolated test directories +- `npxShadcn()` - Executes CLI commands +- `runCommand()` - Low-level command execution +- `cssHasProperties()` - Validates CSS properties + +### Advanced Helpers +- `TestHelper` - Component validation and consistency checking +- `PerformanceMonitor` - Performance measurement utilities +- `TestDataGenerator` - Test data generation utilities + +### Registry Utilities +- `createRegistryServer()` - Creates mock registry servers +- `configureRegistries()` - Sets up test registries + +## 📊 Test Configuration + +### Vitest Configuration +- Test timeout: 120 seconds +- Hook timeout: 120 seconds +- Max concurrency: 4 +- Environment: Node.js +- Retry attempts: 2 + +### Coverage Areas +- ✅ Component addition and removal +- ✅ Style variant consistency +- ✅ Registry integration +- ✅ Import/export validation +- ✅ CSS variable consistency +- ✅ Performance benchmarks +- ✅ Error handling +- ✅ Edge cases + +## 🎯 Style Refactoring Validation + +The test suite specifically validates the recent style refactoring changes: + +### Key Validations +1. **Import Consistency**: Ensures `base-lyra` uses `@base-ui/react` and `radix-nova` uses `radix-ui` +2. **Type Safety**: Validates correct prop types across variants +3. **Component Structure**: Ensures consistent component exports and structure +4. **CSS Variables**: Validates proper CSS variable naming conventions +5. **Data Slots**: Ensures consistent `data-slot` attributes + +### Test Coverage Matrix +| Feature | base-lyra | radix-nova | Validation | +|---------|-----------|-----------|------------| +| Accordion | ✅ | ✅ | Import consistency | +| Button | ✅ | ✅ | Export consistency | +| Tooltip | ✅ | ✅ | Type safety | +| CSS Variables | ✅ | ✅ | Naming conventions | + +## 🐛 Debugging + +### Enable Debug Mode +```bash +DEBUG_TESTS=true npm run test +``` + +### Individual Test Debugging +```typescript +// Enable debug for specific test +await npxShadcn(testDir, ["add", "button"], { debug: true }) +``` + +### Performance Profiling +```typescript +// Use performance monitor +const duration = performanceMonitor.measure("operation", () => { + // Your code here +}) +console.log(`Operation took ${duration}ms`) +``` + +## 📈 Performance Benchmarks + +- **Test Suite Duration**: ~2-3 minutes +- **Individual Test Duration**: <30 seconds average +- **Memory Usage**: <512MB peak +- **Concurrent Tests**: 4 max + +## 🤝 Contributing + +When adding new tests: + +1. **Use Test Helpers**: Leverage existing utilities for consistency +2. **Add Performance Monitoring**: Include timing for critical operations +3. **Validate Style Variants**: Ensure both `base-lyra` and `radix-nova` are tested +4. **Add Edge Cases**: Cover error scenarios and edge cases +5. **Document Purpose**: Add clear descriptions for test intent + +### Test Naming Convention +- Use descriptive test names +- Follow pattern: `should [expected behavior] when [condition]` +- Include style variant in name when relevant + +### Example +```typescript +it("should create consistent component exports across base-lyra and radix-nova variants", async () => { + // Test implementation +}) +``` + +## 🔍 Troubleshooting + +### Common Issues +1. **CLI Not Found**: Build the CLI first with `pnpm build:shadcn` +2. **Registry Connection**: Ensure registry server is running +3. **Timeout Issues**: Increase timeout in vitest config +4. **Path Issues**: Verify fixture directories exist + +### Test Failures +- Check test logs for detailed error messages +- Verify fixture integrity +- Ensure proper cleanup between tests +- Validate registry server status + +## 📚 Additional Resources + +- [Vitest Documentation](https://vitest.dev/) +- [shadcn CLI Documentation](../../docs) +- [Style Refactoring Guide](../../STYLE_REFACTORING.md) diff --git a/packages/tests/package.json b/packages/tests/package.json index 49b18894edd..f824b6ff5db 100644 --- a/packages/tests/package.json +++ b/packages/tests/package.json @@ -6,9 +6,13 @@ "type": "module", "scripts": { "test": "vitest run", + "test:watch": "vitest", + "test:ui": "vitest --ui", + "test:coverage": "vitest run --coverage", "format:write": "prettier --write \"**/*.{ts,tsx,mdx}\" --cache", "format:check": "prettier --check \"**/*.{ts,tsx,mdx}\" --cache", - "typecheck": "tsc --noEmit" + "typecheck": "tsc --noEmit", + "clean": "rimraf dist temp coverage" }, "dependencies": { "shadcn": "workspace:*" @@ -21,6 +25,8 @@ "rimraf": "^6.0.1", "typescript": "^5.5.3", "vite-tsconfig-paths": "^4.2.0", - "vitest": "^2.1.9" + "vitest": "^2.1.9", + "@vitest/ui": "^2.1.9", + "@vitest/coverage-v8": "^2.1.9" } -} +} \ No newline at end of file diff --git a/packages/tests/src/tests/component-differences.test.ts b/packages/tests/src/tests/component-differences.test.ts new file mode 100644 index 00000000000..7209c5279ef --- /dev/null +++ b/packages/tests/src/tests/component-differences.test.ts @@ -0,0 +1,510 @@ +import { afterAll, beforeAll, describe, expect, it } from "vitest" +import fs from "fs-extra" +import path from "path" + +import { createFixtureTestDirectory, npxShadcn } from "../utils/helpers" +import { configureRegistries, createRegistryServer } from "../utils/registry" + +// Registry for testing component differences between style variants +const componentDiffRegistry = await createRegistryServer( + [ + { + name: "tooltip", + type: "registry:ui", + description: "Tooltip component with style variant differences", + files: [ + { + path: "components/ui/base-lyra/tooltip.tsx", + content: `"use client" + +import * as React from "react" +import * as TooltipPrimitive from "@base-ui/react/tooltip" + +import { cn } from "@/lib/utils" + +function TooltipProvider({ ...props }: TooltipPrimitive.Provider.Props) { + return +} + +function Tooltip({ ...props }: TooltipPrimitive.Root.Props) { + return +} + +function TooltipTrigger({ ...props }: TooltipPrimitive.Trigger.Props) { + return +} + +function TooltipContent({ + className, + sideOffset = 4, + ...props +}: TooltipPrimitive.Portal.Props) { + return ( + + + + ) +} + +export { TooltipProvider, Tooltip, TooltipTrigger, TooltipContent }`, + type: "registry:ui", + }, + { + path: "components/ui/radix-nova/tooltip.tsx", + content: `"use client" + +import * as React from "react" +import * as TooltipPrimitive from "@radix-ui/react-tooltip" + +import { cn } from "@/lib/utils" + +function TooltipProvider({ + delayDuration = 0, + skipDelayDuration = 0, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function Tooltip({ + ...props +}: React.ComponentProps) { + return +} + +function TooltipTrigger({ + ...props +}: React.ComponentProps) { + return +} + +function TooltipContent({ + className, + sideOffset = 4, + ...props +}: React.ComponentProps) { + return ( + + + + ) +} + +export { TooltipProvider, Tooltip, TooltipTrigger, TooltipContent }`, + type: "registry:ui", + }, + ], + }, + { + name: "accordion", + type: "registry:ui", + description: "Accordion component with style variant differences", + files: [ + { + path: "components/ui/base-lyra/accordion.tsx", + content: `"use client" + +import { Accordion as AccordionPrimitive } from "@base-ui/react/accordion" +import { ChevronDownIcon, ChevronUpIcon } from "lucide-react" + +import { cn } from "@/lib/utils" + +function Accordion({ className, ...props }: AccordionPrimitive.Root.Props) { + return ( + + ) +} + +function AccordionItem({ className, ...props }: AccordionPrimitive.Item.Props) { + return ( + + ) +} + +function AccordionTrigger({ + className, + children, + ...props +}: AccordionPrimitive.Trigger.Props) { + return ( + + + {children} + + + + + ) +} + +function AccordionContent({ + className, + children, + ...props +}: AccordionPrimitive.Panel.Props) { + return ( + +
+ {children} +
+
+ ) +} + +export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }`, + type: "registry:ui", + }, + { + path: "components/ui/radix-nova/accordion.tsx", + content: `"use client" + +import * as React from "react" +import { ChevronDownIcon, ChevronUpIcon } from "lucide-react" +import { Accordion as AccordionPrimitive } from "radix-ui" + +import { cn } from "@/lib/utils" + +function Accordion({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AccordionItem({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AccordionTrigger({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + + + {children} + + + + + ) +} + +function AccordionContent({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + +
+ {children} +
+
+ ) +} + +export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }`, + type: "registry:ui", + }, + ], + }, + ], + { + port: 9999, + path: "/component-diff", + } +) + +beforeAll(async () => { + await componentDiffRegistry.start() +}) + +afterAll(async () => { + await componentDiffRegistry.stop() +}) + +describe("component differences between style variants", () => { + describe("import differences", () => { + it("should handle different import patterns for tooltip", async () => { + const fixturePath = await createFixtureTestDirectory("next-app-init") + await configureRegistries(fixturePath, { + "@component-diff": "http://localhost:9999/component-diff/{name}", + }) + + await npxShadcn(fixturePath, ["add", "@component-diff/tooltip"]) + + const baseLyraPath = path.join(fixturePath, "components/ui/base-lyra/tooltip.tsx") + const radixNovaPath = path.join(fixturePath, "components/ui/radix-nova/tooltip.tsx") + + const baseLyraContent = await fs.readFile(baseLyraPath, "utf-8") + const radixNovaContent = await fs.readFile(radixNovaPath, "utf-8") + + // base-lyra should use @base-ui/react + expect(baseLyraContent).toContain("@base-ui/react/tooltip") + expect(baseLyraContent).not.toContain("@radix-ui/react-tooltip") + + // radix-nova should use @radix-ui/react-tooltip + expect(radixNovaContent).toContain("@radix-ui/react-tooltip") + expect(radixNovaContent).not.toContain("@base-ui/react/tooltip") + }) + + it("should handle different prop types for tooltip", async () => { + const fixturePath = await createFixtureTestDirectory("next-app-init") + await configureRegistries(fixturePath, { + "@component-diff": "http://localhost:9999/component-diff/{name}", + }) + + await npxShadcn(fixturePath, ["add", "@component-diff/tooltip"]) + + const baseLyraPath = path.join(fixturePath, "components/ui/base-lyra/tooltip.tsx") + const radixNovaPath = path.join(fixturePath, "components/ui/radix-nova/tooltip.tsx") + + const baseLyraContent = await fs.readFile(baseLyraPath, "utf-8") + const radixNovaContent = await fs.readFile(radixNovaPath, "utf-8") + + // base-lyra should use primitive prop types + expect(baseLyraContent).toContain("TooltipPrimitive.Provider.Props") + expect(baseLyraContent).toContain("TooltipPrimitive.Root.Props") + expect(baseLyraContent).toContain("TooltipPrimitive.Trigger.Props") + expect(baseLyraContent).toContain("TooltipPrimitive.Portal.Props") + + // radix-nova should use React.ComponentProps + expect(radixNovaContent).toContain("React.ComponentProps") + expect(radixNovaContent).toContain("React.ComponentProps") + expect(radixNovaContent).toContain("React.ComponentProps") + expect(radixNovaContent).toContain("React.ComponentProps") + }) + }) + + describe("styling differences", () => { + it("should maintain consistent styling patterns with variant-specific differences", async () => { + const fixturePath = await createFixtureTestDirectory("next-app-init") + await configureRegistries(fixturePath, { + "@component-diff": "http://localhost:9999/component-diff/{name}", + }) + + await npxShadcn(fixturePath, ["add", "@component-diff/accordion"]) + + const baseLyraPath = path.join(fixturePath, "components/ui/base-lyra/accordion.tsx") + const radixNovaPath = path.join(fixturePath, "components/ui/radix-nova/accordion.tsx") + + const baseLyraContent = await fs.readFile(baseLyraPath, "utf-8") + const radixNovaContent = await fs.readFile(radixNovaPath, "utf-8") + + // Both should have common styling classes + expect(baseLyraContent).toContain("flex w-full flex-col") + expect(radixNovaContent).toContain("flex w-full flex-col") + expect(baseLyraContent).toContain("not-last:border-b") + expect(radixNovaContent).toContain("not-last:border-b") + + // base-lyra specific styling + expect(baseLyraContent).toContain("rounded-none") + expect(baseLyraContent).toContain("text-xs") + expect(baseLyraContent).toContain("ring-1") + + // radix-nova specific styling + expect(radixNovaContent).toContain("rounded-lg") + expect(radixNovaContent).toContain("text-sm") + expect(radixNovaContent).toContain("ring-3") + }) + + it("should handle different CSS variable naming conventions", async () => { + const fixturePath = await createFixtureTestDirectory("next-app-init") + await configureRegistries(fixturePath, { + "@component-diff": "http://localhost:9999/component-diff/{name}", + }) + + await npxShadcn(fixturePath, ["add", "@component-diff/accordion"]) + + const baseLyraPath = path.join(fixturePath, "components/ui/base-lyra/accordion.tsx") + const radixNovaPath = path.join(fixturePath, "components/ui/radix-nova/accordion.tsx") + + const baseLyraContent = await fs.readFile(baseLyraPath, "utf-8") + const radixNovaContent = await fs.readFile(radixNovaPath, "utf-8") + + // base-lyra should use --accordion-panel-height + expect(baseLyraContent).toContain("--accordion-panel-height") + + // radix-nova should use --radix-accordion-content-height + expect(radixNovaContent).toContain("--radix-accordion-content-height") + }) + }) + + describe("component structure consistency", () => { + it("should export the same components across variants", async () => { + const fixturePath = await createFixtureTestDirectory("next-app-init") + await configureRegistries(fixturePath, { + "@component-diff": "http://localhost:9999/component-diff/{name}", + }) + + await npxShadcn(fixturePath, ["add", "@component-diff/tooltip", "@component-diff/accordion"]) + + // Check tooltip exports + const baseLyraTooltip = await fs.readFile( + path.join(fixturePath, "components/ui/base-lyra/tooltip.tsx"), + "utf-8" + ) + const radixNovaTooltip = await fs.readFile( + path.join(fixturePath, "components/ui/radix-nova/tooltip.tsx"), + "utf-8" + ) + + expect(baseLyraTooltip).toContain("export { TooltipProvider, Tooltip, TooltipTrigger, TooltipContent }") + expect(radixNovaTooltip).toContain("export { TooltipProvider, Tooltip, TooltipTrigger, TooltipContent }") + + // Check accordion exports + const baseLyraAccordion = await fs.readFile( + path.join(fixturePath, "components/ui/base-lyra/accordion.tsx"), + "utf-8" + ) + const radixNovaAccordion = await fs.readFile( + path.join(fixturePath, "components/ui/radix-nova/accordion.tsx"), + "utf-8" + ) + + expect(baseLyraAccordion).toContain("export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }") + expect(radixNovaAccordion).toContain("export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }") + }) + + it("should maintain data-slot attributes across variants", async () => { + const fixturePath = await createFixtureTestDirectory("next-app-init") + await configureRegistries(fixturePath, { + "@component-diff": "http://localhost:9999/component-diff/{name}", + }) + + await npxShadcn(fixturePath, ["add", "@component-diff/accordion"]) + + const baseLyraPath = path.join(fixturePath, "components/ui/base-lyra/accordion.tsx") + const radixNovaPath = path.join(fixturePath, "components/ui/radix-nova/accordion.tsx") + + const baseLyraContent = await fs.readFile(baseLyraPath, "utf-8") + const radixNovaContent = await fs.readFile(radixNovaPath, "utf-8") + + // Both should have the same data-slot attributes + expect(baseLyraContent).toContain('data-slot="accordion"') + expect(radixNovaContent).toContain('data-slot="accordion"') + expect(baseLyraContent).toContain('data-slot="accordion-item"') + expect(radixNovaContent).toContain('data-slot="accordion-item"') + expect(baseLyraContent).toContain('data-slot="accordion-trigger"') + expect(radixNovaContent).toContain('data-slot="accordion-trigger"') + expect(baseLyraContent).toContain('data-slot="accordion-content"') + expect(radixNovaContent).toContain('data-slot="accordion-content"') + }) + }) + + describe("functionality differences", () => { + it("should handle different primitive component behaviors", async () => { + const fixturePath = await createFixtureTestDirectory("next-app-init") + await configureRegistries(fixturePath, { + "@component-diff": "http://localhost:9999/component-diff/{name}", + }) + + await npxShadcn(fixturePath, ["add", "@component-diff/tooltip"]) + + const baseLyraPath = path.join(fixturePath, "components/ui/base-lyra/tooltip.tsx") + const radixNovaPath = path.join(fixturePath, "components/ui/radix-nova/tooltip.tsx") + + const baseLyraContent = await fs.readFile(baseLyraPath, "utf-8") + const radixNovaContent = await fs.readFile(radixNovaPath, "utf-8") + + // radix-nova should have additional props for Provider + expect(radixNovaContent).toContain("delayDuration") + expect(radixNovaContent).toContain("skipDelayDuration") + expect(baseLyraContent).not.toContain("delayDuration") + expect(baseLyraContent).not.toContain("skipDelayDuration") + + // Content component differences + expect(baseLyraContent).toContain("TooltipPrimitive.Portal.Props") + expect(radixNovaContent).toContain("TooltipPrimitive.Content") + }) + }) +}) diff --git a/packages/tests/src/tests/registries.test.ts b/packages/tests/src/tests/registries.test.ts index bdbd894ace1..ed0833fbfa7 100644 --- a/packages/tests/src/tests/registries.test.ts +++ b/packages/tests/src/tests/registries.test.ts @@ -401,8 +401,8 @@ describe("registries", () => { expect(output.stdout).toContain('Unknown registry "@non-existent"') expect(output.stdout).toContain( '"registries": {\n' + - ' "@non-existent": "[URL_TO_REGISTRY]"\n' + - " }\n" + ' "@non-existent": "[URL_TO_REGISTRY]"\n' + + " }\n" ) }) @@ -1347,3 +1347,70 @@ describe("registries:init", () => { ).toBe(true) }) }) + +describe("style variants support", () => { + it("should add components with multiple style variants", async () => { + const fixturePath = await createFixtureTestDirectory("next-app-init") + + // Create a registry server with style variants + const styleVariantRegistry = await createRegistryServer( + [ + { + name: "accordion", + type: "registry:ui", + description: "Accordion with style variants", + files: [ + { + path: "components/ui/base-lyra/accordion.tsx", + content: `"use client"\n\nimport { Accordion as AccordionPrimitive } from "@base-ui/react/accordion"\nimport { cn } from "@/lib/utils"\n\nfunction Accordion({ className, ...props }: AccordionPrimitive.Root.Props) {\n return (\n \n )\n}\n\nexport { Accordion }`, + type: "registry:ui", + }, + { + path: "components/ui/radix-nova/accordion.tsx", + content: `"use client"\n\nimport * as React from "react"\nimport { Accordion as AccordionPrimitive } from "radix-ui"\nimport { cn } from "@/lib/utils"\n\nfunction Accordion({\n className,\n ...props\n}: React.ComponentProps) {\n return (\n \n )\n}\n\nexport { Accordion }`, + type: "registry:ui", + }, + ], + }, + ], + { + port: 7777, + path: "/style-variants", + } + ) + + await styleVariantRegistry.start() + + try { + await configureRegistries(fixturePath, { + "@style-variants": "http://localhost:7777/style-variants/{name}", + }) + + await npxShadcn(fixturePath, ["add", "@style-variants/accordion"]) + + // Check both variants were created + expect( + await fs.pathExists(path.join(fixturePath, "components/ui/base-lyra/accordion.tsx")) + ).toBe(true) + expect( + await fs.pathExists(path.join(fixturePath, "components/ui/radix-nova/accordion.tsx")) + ).toBe(true) + + // Verify content differences + const baseLyraContent = await fs.readFile( + path.join(fixturePath, "components/ui/base-lyra/accordion.tsx"), + "utf-8" + ) + const radixNovaContent = await fs.readFile( + path.join(fixturePath, "components/ui/radix-nova/accordion.tsx"), + "utf-8" + ) + + expect(baseLyraContent).toContain("@base-ui/react/accordion") + expect(radixNovaContent).toContain("radix-ui") + expect(radixNovaContent).toContain("React.ComponentProps") + } finally { + await styleVariantRegistry.stop() + } + }) +}) diff --git a/packages/tests/src/tests/search.test.ts b/packages/tests/src/tests/search.test.ts index f92f2934de1..50852441778 100644 --- a/packages/tests/src/tests/search.test.ts +++ b/packages/tests/src/tests/search.test.ts @@ -117,9 +117,8 @@ const registryLarge = await createRegistryServer( files: [ { path: `components/ui/component-${i + 1}.tsx`, - content: `export function Component${i + 1}() { return
Component ${ - i + 1 - }
}`, + content: `export function Component${i + 1}() { return
Component ${i + 1 + }
}`, type: "registry:ui", }, ], @@ -1001,4 +1000,191 @@ describe("shadcn search", () => { ) expect(itemItem.addCommandArgument).toBe("@two/item") }) + + describe("style variants search", () => { + it("should search across style variant registries", async () => { + // Create a style variant registry + const styleVariantRegistry = await createRegistryServer( + [ + { + name: "accordion", + type: "registry:ui", + description: "Accordion component with style variants", + files: [ + { + path: "components/ui/base-lyra/accordion.tsx", + content: "export function Accordion() { return
Base Lyra Accordion
}", + type: "registry:ui", + }, + { + path: "components/ui/radix-nova/accordion.tsx", + content: "export function Accordion() { return
Radix Nova Accordion
}", + type: "registry:ui", + }, + ], + }, + { + name: "button", + type: "registry:ui", + description: "Button component with style variants", + files: [ + { + path: "components/ui/base-lyra/button.tsx", + content: "export function Button() { return }", + type: "registry:ui", + }, + { + path: "components/ui/radix-nova/button.tsx", + content: "export function Button() { return }", + type: "registry:ui", + }, + ], + }, + ], + { + port: 9185, + path: "/style-variants", + } + ) + + await styleVariantRegistry.start() + + try { + const fixturePath = await createFixtureTestDirectory("next-app-init") + await configureRegistries(fixturePath, { + "@style-variants": "http://localhost:9185/style-variants/{name}", + }) + + const output = await npxShadcn(fixturePath, ["search", "@style-variants"]) + const parsed = JSON.parse(output.stdout) + + expect(parsed.items).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + name: "accordion", + type: "registry:ui", + description: "Accordion component with style variants", + registry: "@style-variants", + addCommandArgument: "@style-variants/accordion", + }), + expect.objectContaining({ + name: "button", + type: "registry:ui", + description: "Button component with style variants", + registry: "@style-variants", + addCommandArgument: "@style-variants/button", + }), + ]) + ) + } finally { + await styleVariantRegistry.stop() + } + }) + + it("should handle fuzzy search for style variant components", async () => { + const styleVariantRegistry = await createRegistryServer( + [ + { + name: "accordion", + type: "registry:ui", + description: "Accordion component with style variants", + files: [ + { + path: "components/ui/base-lyra/accordion.tsx", + content: "export function Accordion() { return
Base Lyra Accordion
}", + type: "registry:ui", + }, + ], + }, + ], + { + port: 9186, + path: "/style-fuzzy", + } + ) + + await styleVariantRegistry.start() + + try { + const fixturePath = await createFixtureTestDirectory("next-app-init") + await configureRegistries(fixturePath, { + "@style-fuzzy": "http://localhost:9186/style-fuzzy/{name}", + }) + + // Test fuzzy matching for "accordion" + const output = await npxShadcn(fixturePath, [ + "search", + "@style-fuzzy", + "--query", + "acordn", // typo + ]) + + const parsed = JSON.parse(output.stdout) + expect( + parsed.items.some((item: any) => item.name === "accordion"), + "Fuzzy search should find 'accordion' with typo 'acordn'" + ).toBe(true) + } finally { + await styleVariantRegistry.stop() + } + }) + + it("should paginate style variant search results", async () => { + // Create a registry with many style variant components + const manyStyleComponents = Array.from({ length: 15 }, (_, i) => ({ + name: `component-${i + 1}`, + type: "registry:ui", + description: `Style variant component ${i + 1}`, + files: [ + { + path: `components/ui/base-lyra/component-${i + 1}.tsx`, + content: `export function Component${i + 1}() { return
Component ${i + 1}
}`, + type: "registry:ui", + }, + { + path: `components/ui/radix-nova/component-${i + 1}.tsx`, + content: `export function Component${i + 1}() { return
Component ${i + 1}
}`, + type: "registry:ui", + }, + ], + })) + + const manyStyleRegistry = await createRegistryServer(manyStyleComponents, { + port: 9187, + path: "/many-styles", + }) + + await manyStyleRegistry.start() + + try { + const fixturePath = await createFixtureTestDirectory("next-app-init") + await configureRegistries(fixturePath, { + "@many-styles": "http://localhost:9187/many-styles/{name}", + }) + + // Test pagination + const output = await npxShadcn(fixturePath, [ + "search", + "@many-styles", + "--limit", + "5", + "--offset", + "0", + ]) + + const parsed = JSON.parse(output.stdout) + expect(parsed.items).toHaveLength(5) + expect(parsed.pagination.limit).toBe(5) + expect(parsed.pagination.offset).toBe(0) + expect(parsed.pagination.total).toBe(15) + expect(parsed.pagination.hasMore).toBe(true) + + // Check first page items + expect(parsed.items[0].name).toBe("component-1") + expect(parsed.items[4].name).toBe("component-5") + } finally { + await manyStyleRegistry.stop() + } + }) + }) }) diff --git a/packages/tests/src/tests/style-refactoring.test.ts b/packages/tests/src/tests/style-refactoring.test.ts new file mode 100644 index 00000000000..ecb1b8f5fa8 --- /dev/null +++ b/packages/tests/src/tests/style-refactoring.test.ts @@ -0,0 +1,315 @@ +import { afterAll, beforeAll, describe, expect, it } from "vitest" +import fs from "fs-extra" +import path from "path" + +import { createFixtureTestDirectory, npxShadcn } from "../utils/helpers" +import { configureRegistries, createRegistryServer } from "../utils/registry" + +// Mock registry with style variants for testing +const styleRegistry = await createRegistryServer( + [ + { + name: "accordion", + type: "registry:ui", + description: "An accordion component with style variants", + files: [ + { + path: "components/ui/base-lyra/accordion.tsx", + content: `"use client" + +import { Accordion as AccordionPrimitive } from "@base-ui/react/accordion" +import { ChevronDownIcon, ChevronUpIcon } from "lucide-react" + +import { cn } from "@/lib/utils" + +function Accordion({ className, ...props }: AccordionPrimitive.Root.Props) { + return ( + + ) +} + +export { Accordion }`, + type: "registry:ui", + }, + { + path: "components/ui/radix-nova/accordion.tsx", + content: `"use client" + +import * as React from "react" +import { ChevronDownIcon, ChevronUpIcon } from "lucide-react" +import { Accordion as AccordionPrimitive } from "radix-ui" + +import { cn } from "@/lib/utils" + +function Accordion({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { Accordion }`, + type: "registry:ui", + }, + ], + }, + { + name: "tooltip", + type: "registry:ui", + description: "A tooltip component with style variants", + files: [ + { + path: "components/ui/base-lyra/tooltip.tsx", + content: `"use client" + +import * as React from "react" +import * as TooltipPrimitive from "@base-ui/react/tooltip" + +import { cn } from "@/lib/utils" + +function TooltipProvider({ ...props }: TooltipPrimitive.Provider.Props) { + return +} + +function Tooltip({ ...props }: TooltipPrimitive.Root.Props) { + return +} + +export { TooltipProvider, Tooltip }`, + type: "registry:ui", + }, + { + path: "components/ui/radix-nova/tooltip.tsx", + content: `"use client" + +import * as React from "react" +import * as TooltipPrimitive from "@radix-ui/react-tooltip" + +import { cn } from "@/lib/utils" + +function TooltipProvider({ ...props }: React.ComponentProps) { + return +} + +function Tooltip({ ...props }: React.ComponentProps) { + return +} + +export { TooltipProvider, Tooltip }`, + type: "registry:ui", + }, + ], + }, + ], + { + port: 8888, + path: "/styles", + } +) + +beforeAll(async () => { + await styleRegistry.start() +}) + +afterAll(async () => { + await styleRegistry.stop() +}) + +describe("style refactoring tests", () => { + describe("component import consistency", () => { + it("should maintain consistent imports across style variants", async () => { + const fixturePath = await createFixtureTestDirectory("next-app-init") + await configureRegistries(fixturePath, { + "@styles": "http://localhost:8888/styles/{name}", + }) + + // Add accordion component + await npxShadcn(fixturePath, ["add", "@styles/accordion"]) + + // Check both style variants exist + const baseLyraPath = path.join(fixturePath, "components/ui/base-lyra/accordion.tsx") + const radixNovaPath = path.join(fixturePath, "components/ui/radix-nova/accordion.tsx") + + expect(await fs.pathExists(baseLyraPath)).toBe(true) + expect(await fs.pathExists(radixNovaPath)).toBe(true) + + // Verify import differences + const baseLyraContent = await fs.readFile(baseLyraPath, "utf-8") + const radixNovaContent = await fs.readFile(radixNovaPath, "utf-8") + + // base-lyra should use @base-ui/react + expect(baseLyraContent).toContain("@base-ui/react/accordion") + expect(baseLyraContent).not.toContain("radix-ui") + + // radix-nova should use radix-ui + expect(radixNovaContent).toContain("radix-ui") + expect(radixNovaContent).not.toContain("@base-ui/react") + }) + + it("should handle React import differences correctly", async () => { + const fixturePath = await createFixtureTestDirectory("next-app-init") + await configureRegistries(fixturePath, { + "@styles": "http://localhost:8888/styles/{name}", + }) + + // Add tooltip component + await npxShadcn(fixturePath, ["add", "@styles/tooltip"]) + + const baseLyraPath = path.join(fixturePath, "components/ui/base-lyra/tooltip.tsx") + const radixNovaPath = path.join(fixturePath, "components/ui/radix-nova/tooltip.tsx") + + const baseLyraContent = await fs.readFile(baseLyraPath, "utf-8") + const radixNovaContent = await fs.readFile(radixNovaPath, "utf-8") + + // Both should have React import but radix-nova should use React.ComponentProps + expect(baseLyraContent).toContain("import * as React from \"react\"") + expect(radixNovaContent).toContain("import * as React from \"react\"") + + // radix-nova should use React.ComponentProps + expect(radixNovaContent).toContain("React.ComponentProps") + expect(baseLyraContent).not.toContain("React.ComponentProps") + }) + }) + + describe("component structure validation", () => { + it("should maintain consistent component structure across variants", async () => { + const fixturePath = await createFixtureTestDirectory("next-app-init") + await configureRegistries(fixturePath, { + "@styles": "http://localhost:8888/styles/{name}", + }) + + await npxShadcn(fixturePath, ["add", "@styles/accordion"]) + + const baseLyraPath = path.join(fixturePath, "components/ui/base-lyra/accordion.tsx") + const radixNovaPath = path.join(fixturePath, "components/ui/radix-nova/accordion.tsx") + + const baseLyraContent = await fs.readFile(baseLyraPath, "utf-8") + const radixNovaContent = await fs.readFile(radixNovaPath, "utf-8") + + // Both should export the same components + expect(baseLyraContent).toContain("export { Accordion }") + expect(radixNovaContent).toContain("export { Accordion }") + + // Both should have the same data-slot attributes + expect(baseLyraContent).toContain('data-slot="accordion"') + expect(radixNovaContent).toContain('data-slot="accordion"') + + // Both should use cn utility + expect(baseLyraContent).toContain("cn(") + expect(radixNovaContent).toContain("cn(") + }) + + it("should preserve styling patterns across variants", async () => { + const fixturePath = await createFixtureTestDirectory("next-app-init") + await configureRegistries(fixturePath, { + "@styles": "http://localhost:8888/styles/{name}", + }) + + await npxShadcn(fixturePath, ["add", "@styles/accordion"]) + + const baseLyraPath = path.join(fixturePath, "components/ui/base-lyra/accordion.tsx") + const radixNovaPath = path.join(fixturePath, "components/ui/radix-nova/accordion.tsx") + + const baseLyraContent = await fs.readFile(baseLyraPath, "utf-8") + const radixNovaContent = await fs.readFile(radixNovaPath, "utf-8") + + // Both should have similar CSS classes + expect(baseLyraContent).toContain("flex w-full flex-col") + expect(radixNovaContent).toContain("flex w-full flex-col") + }) + }) + + describe("style variant organization", () => { + it("should create proper directory structure for style variants", async () => { + const fixturePath = await createFixtureTestDirectory("next-app-init") + await configureRegistries(fixturePath, { + "@styles": "http://localhost:8888/styles/{name}", + }) + + await npxShadcn(fixturePath, ["add", "@styles/accordion", "@styles/tooltip"]) + + // Check that directories are created correctly + const baseLyraDir = path.join(fixturePath, "components/ui/base-lyra") + const radixNovaDir = path.join(fixturePath, "components/ui/radix-nova") + + expect(await fs.pathExists(baseLyraDir)).toBe(true) + expect(await fs.pathExists(radixNovaDir)).toBe(true) + + // Check that components are in the right directories + expect(await fs.pathExists(path.join(baseLyraDir, "accordion.tsx"))).toBe(true) + expect(await fs.pathExists(path.join(baseLyraDir, "tooltip.tsx"))).toBe(true) + expect(await fs.pathExists(path.join(radixNovaDir, "accordion.tsx"))).toBe(true) + expect(await fs.pathExists(path.join(radixNovaDir, "tooltip.tsx"))).toBe(true) + }) + }) + + describe("migration compatibility", () => { + it("should handle migration from old radix to new structure", async () => { + const fixturePath = await createFixtureTestDirectory("next-app-init") + + // Simulate old structure + const oldComponentsDir = path.join(fixturePath, "components/ui") + await fs.ensureDir(oldComponentsDir) + + // Create old-style component + const oldAccordionPath = path.join(oldComponentsDir, "accordion.tsx") + await fs.writeFile(oldAccordionPath, `"use client" + +import { Accordion as AccordionPrimitive } from "radix-ui" +import { cn } from "@/lib/utils" + +function Accordion({ className, ...props }) { + return ( + + ) +} + +export { Accordion }`) + + // Configure and add new style variants + await configureRegistries(fixturePath, { + "@styles": "http://localhost:8888/styles/{name}", + }) + + await npxShadcn(fixturePath, ["add", "@styles/accordion"]) + + // New variants should be created alongside old one + expect(await fs.pathExists(oldAccordionPath)).toBe(true) // Old file preserved + expect(await fs.pathExists(path.join(fixturePath, "components/ui/base-lyra/accordion.tsx"))).toBe(true) + expect(await fs.pathExists(path.join(fixturePath, "components/ui/radix-nova/accordion.tsx"))).toBe(true) + }) + }) + + describe("type safety validation", () => { + it("should maintain type safety across all variants", async () => { + const fixturePath = await createFixtureTestDirectory("next-app-init") + await configureRegistries(fixturePath, { + "@styles": "http://localhost:8888/styles/{name}", + }) + + await npxShadcn(fixturePath, ["add", "@styles/accordion"]) + + const baseLyraPath = path.join(fixturePath, "components/ui/base-lyra/accordion.tsx") + const radixNovaPath = path.join(fixturePath, "components/ui/radix-nova/accordion.tsx") + + const baseLyraContent = await fs.readFile(baseLyraPath, "utf-8") + const radixNovaContent = await fs.readFile(radixNovaPath, "utf-8") + + // base-lyra should use AccordionPrimitive.Root.Props + expect(baseLyraContent).toContain("AccordionPrimitive.Root.Props") + + // radix-nova should use React.ComponentProps + expect(radixNovaContent).toContain("React.ComponentProps") + }) + }) +}) diff --git a/packages/tests/src/tests/validation.test.ts b/packages/tests/src/tests/validation.test.ts new file mode 100644 index 00000000000..ce8ee1b4d67 --- /dev/null +++ b/packages/tests/src/tests/validation.test.ts @@ -0,0 +1,294 @@ +import { afterAll, beforeAll, describe, expect, it } from "vitest" +import fs from "fs-extra" +import path from "path" + +import { createFixtureTestDirectory, npxShadcn } from "../utils/helpers" +import { configureRegistries, createRegistryServer } from "../utils/registry" +import { TestHelper, TestDataGenerator, performanceMonitor } from "../utils/test-helpers" + +// Mock registry for validation tests +const validationRegistry = await createRegistryServer( + [ + ...TestDataGenerator.generateStyleVariantTest("accordion", ["base-lyra", "radix-nova"]), + ...TestDataGenerator.generateStyleVariantTest("button", ["base-lyra", "radix-nova"]), + ...TestDataGenerator.generateStyleVariantTest("tooltip", ["base-lyra", "radix-nova"]), + ], + { + port: 7777, + path: "/validation", + } +) + +beforeAll(async () => { + await validationRegistry.start() +}) + +afterAll(async () => { + await validationRegistry.stop() +}) +// hamza +describe("test validation and utilities", () => { + describe("TestHelper utilities", () => { + it("should validate component structure correctly", async () => { + const fixturePath = await createFixtureTestDirectory("next-app-init") + await configureRegistries(fixturePath, { + "@validation": "http://localhost:7777/validation/{name}", + }) + + await npxShadcn(fixturePath, ["add", "@validation/accordion"]) + + const baseLyraPath = path.join(fixturePath, "components/ui/base-lyra/accordion.tsx") + const isValid = await TestHelper.validateComponentStructure(baseLyraPath, ["Accordion"]) + + expect(isValid).toBe(true) + }) + + it("should check style variant consistency", async () => { + const fixturePath = await createFixtureTestDirectory("next-app-init") + await configureRegistries(fixturePath, { + "@validation": "http://localhost:7777/validation/{name}", + }) + + await npxShadcn(fixturePath, ["add", "@validation/button"]) + + const baseLyraPath = path.join(fixturePath, "components/ui/base-lyra/button.tsx") + const radixNovaPath = path.join(fixturePath, "components/ui/radix-nova/button.tsx") + + const consistency = await TestHelper.checkStyleVariantConsistency(baseLyraPath, radixNovaPath) + + expect(consistency.consistent).toBe(true) + expect(consistency.differences).toHaveLength(0) + }) + + it("should validate import differences between variants", async () => { + const fixturePath = await createFixtureTestDirectory("next-app-init") + await configureRegistries(fixturePath, { + "@validation": "http://localhost:7777/validation/{name}", + }) + + await npxShadcn(fixturePath, ["add", "@validation/tooltip"]) + + const baseLyraPath = path.join(fixturePath, "components/ui/base-lyra/tooltip.tsx") + const radixNovaPath = path.join(fixturePath, "components/ui/radix-nova/tooltip.tsx") + + const importAnalysis = await TestHelper.validateImportDifferences(baseLyraPath, radixNovaPath) + + expect(importAnalysis.baseLyraImports).toBeDefined() + expect(importAnalysis.radixNovaImports).toBeDefined() + expect(importAnalysis.expectedDifferences).toHaveLength(3) + }) + + it("should extract exports correctly", () => { + const testContent = ` + export function Button() { return } + export const Input = () => + export { Button, Input } + ` + + const exports = TestHelper.extractExports(testContent) + + expect(exports).toContain("Button") + expect(exports).toContain("Input") + }) + + it("should extract data slots correctly", () => { + const testContent = ` +
Content
+ Trigger + ` + + const slots = TestHelper.extractDataSlots(testContent) + + expect(slots).toContain("accordion") + expect(slots).toContain("accordion-trigger") + }) + + it("should extract CSS variables correctly", () => { + const testContent = ` + .class { --accordion-height: 100px; --button-bg: blue; } + ` + + const variables = TestHelper.extractCSSVariables(testContent) + + expect(variables).toContain("--accordion-height") + expect(variables).toContain("--button-bg") + }) + }) + + describe("TestDataGenerator", () => { + it("should generate component names", () => { + const name = TestDataGenerator.generateComponentName() + + expect(typeof name).toBe("string") + expect(name.length).toBeGreaterThan(0) + }) + + it("should generate registry items", () => { + const item = TestDataGenerator.generateRegistryItem("TestComponent") + + expect(item.name).toBe("TestComponent") + expect(item.type).toBe("registry:ui") + expect(item.files).toHaveLength(1) + expect(item.files[0].path).toContain("TestComponent") + }) + + it("should generate style variant tests", () => { + const variants = TestDataGenerator.generateStyleVariantTest("accordion", ["base-lyra", "radix-nova"]) + + expect(variants).toHaveLength(2) + expect(variants[0].name).toBe("accordion") + expect(variants[0].files[0].path).toContain("base-lyra") + expect(variants[1].files[0].path).toContain("radix-nova") + }) + }) + + describe("PerformanceMonitor", () => { + it("should measure execution time", () => { + const monitor = new PerformanceMonitor() + + monitor.start("test") + const result = monitor.end("test") + + expect(typeof result).toBe("number") + expect(result).toBeGreaterThanOrEqual(0) + }) + + it("should measure synchronous operations", () => { + const monitor = new PerformanceMonitor() + + const result = monitor.measure("test", () => { + return 42 + }) + + expect(result).toBe(42) + }) + + it("should measure asynchronous operations", async () => { + const monitor = new PerformanceMonitor() + + const result = await monitor.measureAsync("test", async () => { + return await Promise.resolve(42) + }) + + expect(result).toBe(42) + }) + }) + + describe("test coverage validation", () => { + it("should validate test coverage", async () => { + const testFiles = [ + "src/tests/add.test.ts", + "src/tests/init.test.ts", + "src/tests/registries.test.ts", + "src/tests/search.test.ts", + "src/tests/view.test.ts", + "src/tests/style-refactoring.test.ts", + "src/tests/component-differences.test.ts", + "src/tests/validation.test.ts" + ] + + const coverage = await TestHelper.validateTestCoverage( + testFiles.map(file => path.join(__dirname, "..", file)) + ) + + expect(coverage.totalFiles).toBe(testFiles.length) + expect(coverage.coverage).toBeGreaterThan(0) + expect(coverage.filesWithTests).toBeGreaterThan(0) + }) + + it("should generate test reports", () => { + const results = { + totalChecks: 10, + passed: 8, + failed: 2, + successRate: 80, + details: [ + { + name: "Test 1", + status: "✅ PASSED", + checks: ["Check 1: ✅", "Check 2: ✅"] + }, + { + name: "Test 2", + status: "❌ FAILED", + checks: ["Check 1: ❌", "Check 2: ✅"] + } + ], + recommendations: [ + "Fix failing tests", + "Add more edge case coverage" + ] + } + + const report = TestHelper.generateTestReport(results) + + expect(report).toContain("# Test Validation Report") + expect(report).toContain("## Summary") + expect(report).toContain("## Details") + expect(report).toContain("## Recommendations") + expect(report).toContain("Total Checks: 10") + expect(report).toContain("Passed: 8") + expect(report).toContain("Failed: 2") + }) + }) + + describe("integration validation", () => { + it("should validate complete style refactoring workflow", async () => { + const timer = performanceMonitor.start("integration-test") + + const fixturePath = await createFixtureTestDirectory("next-app-init") + await configureRegistries(fixturePath, { + "@validation": "http://localhost:7777/validation/{name}", + }) + + // Add multiple components + await npxShadcn(fixturePath, ["add", "@validation/accordion", "@validation/button", "@validation/tooltip"]) + + // Validate all components were created + const components = ["accordion", "button", "tooltip"] + const variants = ["base-lyra", "radix-nova"] + + for (const component of components) { + for (const variant of variants) { + const filePath = path.join(fixturePath, `components/ui/${variant}/${component}.tsx`) + expect(await fs.pathExists(filePath)).toBe(true) + } + } + + // Validate consistency across variants + for (const component of components) { + const baseLyraPath = path.join(fixturePath, `components/ui/base-lyra/${component}.tsx`) + const radixNovaPath = path.join(fixturePath, `components/ui/radix-nova/${component}.tsx`) + + const consistency = await TestHelper.checkStyleVariantConsistency(baseLyraPath, radixNovaPath) + expect(consistency.consistent).toBe(true) + } + + const duration = performanceMonitor.end("integration-test") + expect(duration).toBeLessThan(10000) // Should complete within 10 seconds + }) + + it("should handle performance monitoring during tests", async () => { + const results = [] + + for (let i = 0; i < 5; i++) { + const result = performanceMonitor.measure(`iteration-${i}`, () => { + // Simulate some work + let sum = 0 + for (let j = 0; j < 1000; j++) { + sum += j + } + return sum + }) + + results.push(result) + } + + expect(results).toHaveLength(5) + results.forEach(result => { + expect(typeof result).toBe("number") + expect(result).toBeGreaterThan(0) + }) + }) + }) +}) diff --git a/packages/tests/src/utils/test-helpers.ts b/packages/tests/src/utils/test-helpers.ts new file mode 100644 index 00000000000..10743e2d3aa --- /dev/null +++ b/packages/tests/src/utils/test-helpers.ts @@ -0,0 +1,290 @@ +import fs from "fs-extra" +import path from "path" + +// Additional test utilities for comprehensive testing +export class TestHelper { + static async validateComponentStructure(filePath: string, expectedExports: string[]): Promise { + if (!await fs.pathExists(filePath)) return false + + const content = await fs.readFile(filePath, 'utf-8') + + // Check for expected exports + const hasAllExports = expectedExports.every(exp => content.includes(`export ${exp}`)) + + // Check for proper component structure + const hasFunction = content.includes('function') || content.includes('const') + const hasReturn = content.includes('return') + const hasImport = content.includes('import') + + return hasAllExports && hasFunction && hasReturn && hasImport + } + + static async checkStyleVariantConsistency(baseLyraPath: string, radixNovaPath: string): Promise<{ + consistent: boolean + differences: string[] + }> { + const differences: string[] = [] + + if (!await fs.pathExists(baseLyraPath) || !await fs.pathExists(radixNovaPath)) { + differences.push('Missing variant files') + return { consistent: false, differences } + } + + const baseLyraContent = await fs.readFile(baseLyraPath, 'utf-8') + const radixNovaContent = await fs.readFile(radixNovaPath, 'utf-8') + + // Check for consistent exports + const baseExports = this.extractExports(baseLyraContent) + const novaExports = this.extractExports(radixNovaContent) + + if (JSON.stringify(baseExports.sort()) !== JSON.stringify(novaExports.sort())) { + differences.push('Inconsistent exports between variants') + } + + // Check for consistent data-slot attributes + const baseSlots = this.extractDataSlots(baseLyraContent) + const novaSlots = this.extractDataSlots(radixNovaContent) + + if (JSON.stringify(baseSlots.sort()) !== JSON.stringify(novaSlots.sort())) { + differences.push('Inconsistent data-slot attributes') + } + + return { + consistent: differences.length === 0, + differences + } + } + + static extractExports(content: string): string[] { + const exportRegex = /export\s+(?:function|const|class)\s+(\w+)/g + const exports: string[] = [] + let match + + while ((match = exportRegex.exec(content)) !== null) { + exports.push(match[1]) + } + + // Also check for export { ... } syntax + const exportBlockRegex = /export\s*{\s*([^}]+)\s*}/g + while ((match = exportBlockRegex.exec(content)) !== null) { + const items = match[1].split(',').map(item => item.trim().split(' as ')[0]) + exports.push(...items) + } + + return [...new Set(exports)] // Remove duplicates + } + + static extractDataSlots(content: string): string[] { + const slotRegex = /data-slot="([^"]+)"/g + const slots: string[] = [] + let match + + while ((match = slotRegex.exec(content)) !== null) { + slots.push(match[1]) + } + + return [...new Set(slots)] // Remove duplicates + } + + static async validateImportDifferences(baseLyraPath: string, radixNovaPath: string): Promise<{ + baseLyraImports: string[] + radixNovaImports: string[] + expectedDifferences: string[] + }> { + const baseLyraContent = await fs.readFile(baseLyraPath, 'utf-8') + const radixNovaContent = await fs.readFile(radixNovaPath, 'utf-8') + + const baseLyraImports = this.extractImports(baseLyraContent) + const radixNovaImports = this.extractImports(radixNovaContent) + + // Expected differences based on style refactoring + const expectedDifferences = [ + 'base-lyra should use @base-ui/react', + 'radix-nova should use radix-ui', + 'radix-nova should use React.ComponentProps' + ] + + return { + baseLyraImports, + radixNovaImports, + expectedDifferences + } + } + + static extractImports(content: string): string[] { + const importRegex = /import\s+.*?from\s+['"]([^'"]+)['"];?/g + const imports: string[] = [] + let match + + while ((match = importRegex.exec(content)) !== null) { + imports.push(match[1]) + } + + return [...new Set(imports)] // Remove duplicates + } + + static async checkCSSVariableConsistency(baseLyraPath: string, radixNovaPath: string): Promise<{ + consistent: boolean + baseVariables: string[] + novaVariables: string[] + }> { + const baseLyraContent = await fs.readFile(baseLyraPath, 'utf-8') + const radixNovaContent = await fs.readFile(radixNovaPath, 'utf-8') + + const baseVariables = this.extractCSSVariables(baseLyraContent) + const novaVariables = this.extractCSSVariables(radixNovaContent) + + return { + consistent: JSON.stringify(baseVariables.sort()) === JSON.stringify(novaVariables.sort()), + baseVariables, + novaVariables + } + } + + static extractCSSVariables(content: string): string[] { + const variableRegex = /--[\w-]+/g + const variables: string[] = [] + let match + + while ((match = variableRegex.exec(content)) !== null) { + variables.push(match[0]) + } + + return [...new Set(variables)] // Remove duplicates + } + + static async validateTestCoverage(testFiles: string[]): Promise<{ + totalFiles: number + filesWithTests: number + coverage: number + missingTests: string[] + }> { + const results = { + totalFiles: testFiles.length, + filesWithTests: 0, + coverage: 0, + missingTests: [] as string[] + } + + for (const file of testFiles) { + if (await fs.pathExists(file)) { + const content = await fs.readFile(file, 'utf-8') + if (content.includes('describe(') && content.includes('it(')) { + results.filesWithTests++ + } else { + results.missingTests.push(file) + } + } else { + results.missingTests.push(file) + } + } + + results.coverage = results.totalFiles > 0 ? (results.filesWithTests / results.totalFiles) * 100 : 0 + + return results + } + + static generateTestReport(results: any): string { + const report = [ + '# Test Validation Report', + `Generated: ${new Date().toISOString()}`, + '', + '## Summary', + `- Total Checks: ${results.totalChecks || 0}`, + `- Passed: ${results.passed || 0}`, + `- Failed: ${results.failed || 0}`, + `- Success Rate: ${results.successRate || 0}%`, + '', + '## Details', + ...results.details?.map((detail: any) => + `### ${detail.name}\n- Status: ${detail.status}\n- Checks: ${detail.checkes?.join(', ') || 'N/A'}` + ) || [], + '', + '## Recommendations', + ...results.recommendations || ['No recommendations available'] + ].join('\n') + + return report + } +} + +// Performance monitoring +export class PerformanceMonitor { + private timers: Map = new Map() + + start(label: string): void { + this.timers.set(label, Date.now()) + } + + end(label: string): number { + const start = this.timers.get(label) + if (!start) return 0 + + const duration = Date.now() - start + this.timers.delete(label) + + if (process.env.DEBUG_TESTS) { + console.log(`⏱️ ${label}: ${duration}ms`) + } + + return duration + } + + measure(label: string, fn: () => T): T { + this.start(label) + const result = fn() + this.end(label) + return result + } + + async measureAsync(label: string, fn: () => Promise): Promise { + this.start(label) + const result = await fn() + this.end(label) + return result + } +} + +// Test data generators +export class TestDataGenerator { + static generateComponentName(): string { + const prefixes = ['Test', 'Mock', 'Sample', 'Demo'] + const suffixes = ['Component', 'Widget', 'Element', 'Item'] + const prefix = prefixes[Math.floor(Math.random() * prefixes.length)] + const suffix = suffixes[Math.floor(Math.random() * suffixes.length)] + return `${prefix}${suffix}` + } + + static generateRegistryItem(name?: string): any { + return { + name: name || this.generateComponentName(), + type: 'registry:ui', + description: `Test component: ${name || 'Generated'}`, + files: [ + { + path: `components/ui/${name || 'test'}.tsx`, + content: `export function ${name || 'Test'}() { return
Test Component
}`, + type: 'registry:ui' + } + ] + } + } + + static generateStyleVariantTest(baseComponent: string, variants: string[]): any[] { + return variants.map(variant => ({ + name: baseComponent, + type: 'registry:ui', + description: `${baseComponent} with ${variant} style`, + files: [ + { + path: `components/ui/${variant}/${baseComponent}.tsx`, + content: `export function ${baseComponent}() { return
${variant} ${baseComponent}
}`, + type: 'registry:ui' + } + ] + })) + } +} + +// Global performance monitor instance +export const performanceMonitor = new PerformanceMonitor()