diff --git a/MOCKING.md b/MOCKING.md new file mode 100644 index 000000000..c3e111da9 --- /dev/null +++ b/MOCKING.md @@ -0,0 +1,159 @@ +# Mock Mode Documentation + +This document explains how to use mock mode for local development and testing of React Grab's Visual Edit feature. + +## Overview + +Mock mode enables local development and E2E testing without requiring external API dependencies like the Cerebras AI API. When enabled, all requests to external services are intercepted and handled by local mock endpoints that return pattern-based responses. + +## Setup + +### 1. Enable Mock Mode + +Create a `.env.local` file in `packages/website/` (or copy from `.env.local.example`): + +```bash +USE_MOCKS=true +``` + +### 2. Start Development Server + +```bash +nr dev +``` + +The development server will automatically detect mock mode and route all external API requests to mock endpoints. + +## How It Works + +### Mock Endpoint + +When mock mode is enabled, the `/api/mock/cerebras` endpoint becomes available. This endpoint: + +1. Only responds when `USE_MOCKS=true` +2. Returns a `403 Forbidden` error when accessed outside mock mode +3. Generates pattern-based JavaScript code matching the real Cerebras API format +4. Simulates a 100ms delay for realistic behavior + +### Pattern-Based Code Generation + +The mock endpoint recognizes keywords in user requests and generates appropriate DOM manipulation code: + +| Pattern | Keywords | Generated Code | +|---------|----------|----------------| +| Text Change | text, content | Changes element text content | +| Class Modification | class, style | Adds CSS classes | +| Hide Element | hide, remove | Hides element via display:none | +| Color Change | color, background, bg | Modifies element colors | +| Animation | animate, animation, fade, slide | Adds CSS animations | +| Default | (other) | Sets data attribute | + +### Visual Edit Endpoint Integration + +The `/api/visual-edit` endpoint automatically detects mock mode: + +```typescript +if (isMockMode()) { + // Route to mock endpoint + const mockResponse = await fetch("/api/mock/cerebras", { + method: "POST", + body: JSON.stringify({ messages }), + }); +} else { + // Use real Cerebras API + const result = await generateText({ + model: "cerebras/glm-4.6", + system: SYSTEM_PROMPT, + messages, + }); +} +``` + +### Health Check + +The `/api/check-visual-edit` endpoint reports mock mode status: + +```json +{ + "healthy": true, + "mockMode": true +} +``` + +## Disabling Mock Mode + +To disable mock mode and use real external services: + +1. Set `USE_MOCKS=false` in `.env.local` (or remove the variable) +2. Restart the development server +3. Configure required API keys for external services + +## Testing + +### Manual Testing + +1. Enable mock mode: `USE_MOCKS=true` +2. Start dev server: `nr dev` +3. Test direct mock endpoint: + ```bash + curl -X POST http://localhost:3000/api/mock/cerebras \ + -H "Content-Type: application/json" \ + -d '{"messages": [{"role": "user", "content": "change text to loading"}]}' + ``` +4. Test via visual-edit endpoint: + ```bash + curl -X POST http://localhost:3000/api/visual-edit \ + -H "Content-Type: application/json" \ + -d '{"messages": [{"role": "user", "content": "add animation"}]}' + ``` +5. Check mock status: + ```bash + curl http://localhost:3000/api/check-visual-edit + ``` + +### E2E Testing + +Mock mode is automatically enabled in E2E test environments. Playwright tests will use mock endpoints by default. + +## Production Safety + +Mock mode is designed to NEVER be available in production: + +1. The `/api/mock/cerebras` endpoint returns `403 Forbidden` when `USE_MOCKS=false` +2. Environment variables should never set `USE_MOCKS=true` in production +3. The `isMockMode()` function checks environment variables at runtime + +## Troubleshooting + +### Mock endpoint returns 403 + +Ensure `USE_MOCKS=true` is set in `.env.local` and restart the dev server. + +### Mock responses don't match expected patterns + +Check the mock state manager in `lib/mock-cerebras-state.ts` and verify keyword matching logic. + +### Real API still being called + +Verify that `isMockMode()` returns `true` and check console logs for the mock mode status. + +## Implementation Details + +### Files + +- `lib/env.ts` - Global mock toggle and service resolution +- `lib/mock-cerebras-state.ts` - Singleton in-memory state manager +- `app/api/mock/cerebras/route.ts` - Mock endpoint implementation +- `app/api/visual-edit/route.ts` - Updated to support mock mode +- `app/api/check-visual-edit/route.ts` - Reports mock status + +### State Management + +Mock state is maintained in-memory using a singleton pattern. Request history is stored in a `Map` structure and can be accessed via: + +```typescript +import { getMockRequestHistory, clearMockState } from "@/lib/mock-cerebras-state"; + +const history = getMockRequestHistory(); +clearMockState(); +``` \ No newline at end of file diff --git a/README.md b/README.md index 3e2fa6b8e..7fc9ba038 100644 --- a/README.md +++ b/README.md @@ -509,6 +509,61 @@ export default function RootLayout({ children }) { +## Mock Development + +React Grab supports mock mode for local development and E2E testing without external API dependencies. + +### Enable Mock Mode + +Create `.env.local` in `packages/website/`: + +```bash +USE_MOCKS=true +``` + +Then start the development server: + +```bash +nr dev +``` + +### How Mock Mode Works + +When enabled, all external API requests (like Cerebras AI) are routed to local mock endpoints that return pattern-based responses. This enables: + +- Local development without API keys +- E2E testing without external dependencies +- Faster iteration cycles +- Consistent test results + +Mock endpoints recognize keywords in requests and generate appropriate DOM manipulation code: + +- **Text changes**: "change text", "update content" +- **CSS classes**: "add class", "apply style" +- **Visibility**: "hide element", "remove" +- **Colors**: "change color", "set background" +- **Animations**: "add animation", "fade in" + +### Check Mock Status + +```bash +curl http://localhost:3000/api/check-visual-edit +``` + +Response: +```json +{ + "healthy": true, + "mockMode": true +} +``` + +### Disable Mock Mode + +Set `USE_MOCKS=false` in `.env.local` or remove the variable, then restart the server. + +For detailed documentation, see [MOCKING.md](MOCKING.md). + ## Extending React Grab React Grab provides an public customization API. Check out the [type definitions](https://github.com/aidenybai/react-grab/blob/main/packages/react-grab/src/types.ts) to see all available options for extending React Grab. diff --git a/package.json b/package.json index c0b97e772..b2ed017d7 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "scripts": { "build": "turbo run build --filter=react-grab --filter=@react-grab/cursor --filter=@react-grab/claude-code --filter=@react-grab/ami --filter=@react-grab/opencode --filter=@react-grab/codex --filter=@react-grab/gemini --filter=@react-grab/amp --filter=@react-grab/cli --filter=@react-grab/utils --filter=@react-grab/visual-edit && pnpm --filter grab build", "dev": "turbo dev --filter=react-grab --filter=@react-grab/cursor --filter=@react-grab/claude-code --filter=@react-grab/ami --filter=@react-grab/opencode --filter=@react-grab/codex --filter=@react-grab/gemini --filter=@react-grab/amp --filter=@react-grab/cli --filter=@react-grab/utils --filter=@react-grab/visual-edit", - "test": "turbo run test --filter=react-grab --filter=@react-grab/cli", + "test": "turbo run test --filter=react-grab --filter=@react-grab/cli --filter=@react-grab/website", "lint": "pnpm --filter react-grab lint", "lint:fix": "pnpm --filter react-grab lint:fix", "format": "prettier --write .", diff --git a/packages/grab/README.md b/packages/grab/README.md index c5c365757..7bcf6676b 100644 --- a/packages/grab/README.md +++ b/packages/grab/README.md @@ -509,6 +509,61 @@ export default function RootLayout({ children }) { +## Mock Development + +React Grab supports mock mode for local development and E2E testing without external API dependencies. + +### Enable Mock Mode + +Create `.env.local` in `packages/website/`: + +```bash +USE_MOCKS=true +``` + +Then start the development server: + +```bash +nr dev +``` + +### How Mock Mode Works + +When enabled, all external API requests (like Cerebras AI) are routed to local mock endpoints that return pattern-based responses. This enables: + +- Local development without API keys +- E2E testing without external dependencies +- Faster iteration cycles +- Consistent test results + +Mock endpoints recognize keywords in requests and generate appropriate DOM manipulation code: + +- **Text changes**: "change text", "update content" +- **CSS classes**: "add class", "apply style" +- **Visibility**: "hide element", "remove" +- **Colors**: "change color", "set background" +- **Animations**: "add animation", "fade in" + +### Check Mock Status + +```bash +curl http://localhost:3000/api/check-visual-edit +``` + +Response: +```json +{ + "healthy": true, + "mockMode": true +} +``` + +### Disable Mock Mode + +Set `USE_MOCKS=false` in `.env.local` or remove the variable, then restart the server. + +For detailed documentation, see [MOCKING.md](MOCKING.md). + ## Extending React Grab React Grab provides an public customization API. Check out the [type definitions](https://github.com/aidenybai/react-grab/blob/main/packages/react-grab/src/types.ts) to see all available options for extending React Grab. diff --git a/packages/website/app/api/check-visual-edit/route.ts b/packages/website/app/api/check-visual-edit/route.ts index d39bec9ec..2e567a9f0 100644 --- a/packages/website/app/api/check-visual-edit/route.ts +++ b/packages/website/app/api/check-visual-edit/route.ts @@ -1,3 +1,5 @@ +import { isMockMode } from "@/lib/env"; + const IS_HEALTHY = true; const getCorsHeaders = () => { @@ -15,8 +17,9 @@ export const OPTIONS = () => { export const GET = () => { const corsHeaders = getCorsHeaders(); + const mockModeEnabled = isMockMode(); return Response.json( - { healthy: IS_HEALTHY }, + { healthy: IS_HEALTHY, mockMode: mockModeEnabled }, { headers: { ...corsHeaders, "Content-Type": "application/json" } }, ); }; diff --git a/packages/website/app/api/mock/cerebras/route.ts b/packages/website/app/api/mock/cerebras/route.ts new file mode 100644 index 000000000..d2a4356f9 --- /dev/null +++ b/packages/website/app/api/mock/cerebras/route.ts @@ -0,0 +1,76 @@ +import { isMockMode } from "@/lib/env"; +import { + generateMockResponse, + storeMockRequest, +} from "@/lib/mock-cerebras-state"; + +interface ConversationMessage { + role: "user" | "assistant"; + content: string; +} + +const getCorsHeaders = () => { + return { + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Methods": "POST, OPTIONS", + "Access-Control-Allow-Headers": "Content-Type", + }; +}; + +export const OPTIONS = () => { + const corsHeaders = getCorsHeaders(); + return new Response(null, { status: 204, headers: corsHeaders }); +}; + +export const POST = async (request: Request) => { + const corsHeaders = getCorsHeaders(); + + if (!isMockMode()) { + return new Response( + JSON.stringify({ + error: "Mock endpoint not available outside mock mode", + }), + { + status: 403, + headers: { ...corsHeaders, "Content-Type": "application/json" }, + }, + ); + } + + let body: { messages: ConversationMessage[] }; + try { + body = await request.json(); + } catch { + return new Response(JSON.stringify({ error: "Invalid JSON" }), { + status: 400, + headers: { ...corsHeaders, "Content-Type": "application/json" }, + }); + } + + const { messages } = body; + + if (!messages || messages.length === 0) { + return new Response( + JSON.stringify({ error: "messages array is required" }), + { + status: 400, + headers: { ...corsHeaders, "Content-Type": "application/json" }, + }, + ); + } + + const requestId = `mock-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`; + storeMockRequest(requestId, messages); + + const mockCode = generateMockResponse(messages); + + await new Promise((resolve) => setTimeout(resolve, 100)); + + return new Response(mockCode, { + headers: { + ...corsHeaders, + "Content-Type": "text/javascript", + "Cache-Control": "no-cache, no-store, must-revalidate", + }, + }); +}; \ No newline at end of file diff --git a/packages/website/app/api/visual-edit/route.ts b/packages/website/app/api/visual-edit/route.ts index 9825783e2..a92761c38 100644 --- a/packages/website/app/api/visual-edit/route.ts +++ b/packages/website/app/api/visual-edit/route.ts @@ -1,5 +1,6 @@ import { generateText } from "ai"; import type { ModelMessage } from "ai"; +import { isMockMode, getServiceUrl } from "@/lib/env"; interface ConversationMessage { role: "user" | "assistant"; @@ -156,21 +157,29 @@ export const POST = async (request: Request) => { try { let generatedCode: string; - console.log("shouldUsePrimaryModel", shouldUsePrimaryModel); - // if (shouldUsePrimaryModel) { - const result = await generateText({ - model: "cerebras/glm-4.6", - system: SYSTEM_PROMPT, - messages, - }); - // eslint-disable-next-line prefer-const - generatedCode = result.text; - // } else { - // generatedCode = await generateTextWithOpenCodeZen( - // SYSTEM_PROMPT, - // messages, - // ); - // } + if (isMockMode()) { + const mockUrl = getServiceUrl("cerebras"); + const mockResponse = await fetch(mockUrl, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ messages: rawMessages }), + }); + + if (!mockResponse.ok) { + const errorText = await mockResponse.text(); + throw new Error(`Mock API error: ${mockResponse.status} ${errorText}`); + } + + generatedCode = await mockResponse.text(); + } else { + console.log("shouldUsePrimaryModel", shouldUsePrimaryModel); + const result = await generateText({ + model: "cerebras/glm-4.6", + system: SYSTEM_PROMPT, + messages, + }); + generatedCode = result.text; + } return new Response(generatedCode, { headers: { diff --git a/packages/website/lib/env.ts b/packages/website/lib/env.ts new file mode 100644 index 000000000..a9f3618f3 --- /dev/null +++ b/packages/website/lib/env.ts @@ -0,0 +1,41 @@ +interface EnvironmentConfig { + useMocks: boolean; + mockCerebrasUrl: string; + realCerebrasModel: string; +} + +const getEnvironmentConfig = (): EnvironmentConfig => { + const useMocks = process.env.USE_MOCKS === "true"; + const mockCerebrasUrl = "http://localhost:3000/api/mock/cerebras"; + const realCerebrasModel = "cerebras/glm-4.6"; + + return { + useMocks, + mockCerebrasUrl, + realCerebrasModel, + }; +}; + +export const isMockMode = (): boolean => { + return getEnvironmentConfig().useMocks; +}; + +export const getServiceUrl = (service: "cerebras"): string => { + const config = getEnvironmentConfig(); + if (config.useMocks && service === "cerebras") { + return config.mockCerebrasUrl; + } + return ""; +}; + +export const getServiceApiKey = (service: "cerebras"): string | undefined => { + if (isMockMode()) { + return "mock-api-key"; + } + return undefined; +}; + +export const getCerebrasModel = (): string => { + const config = getEnvironmentConfig(); + return config.realCerebrasModel; +}; \ No newline at end of file diff --git a/packages/website/lib/mock-cerebras-state.ts b/packages/website/lib/mock-cerebras-state.ts new file mode 100644 index 000000000..21034a761 --- /dev/null +++ b/packages/website/lib/mock-cerebras-state.ts @@ -0,0 +1,94 @@ +interface MockRequest { + requestId: string; + messages: Array<{ role: string; content: string }>; + timestamp: number; +} + +interface MockState { + requests: Map; +} + +const createMockState = (): MockState => { + return { + requests: new Map(), + }; +}; + +const mockState = createMockState(); + +const generateMockCode = (userMessage: string): string => { + const lowerMessage = userMessage.toLowerCase(); + + if (lowerMessage.includes("text") || lowerMessage.includes("content")) { + return `// Changes element text based on user request +$el.textContent = "Updated content"`; + } + + if (lowerMessage.includes("class") || lowerMessage.includes("style")) { + return `// Adds CSS class to element +$el.classList.add("highlighted")`; + } + + if (lowerMessage.includes("hide") || lowerMessage.includes("remove")) { + return `// Hides the element +$el.style.display = "none"`; + } + + if ( + lowerMessage.includes("color") || + lowerMessage.includes("background") || + lowerMessage.includes("bg") + ) { + return `// Changes element color +$el.style.backgroundColor = "#3b82f6"`; + } + + if ( + lowerMessage.includes("animate") || + lowerMessage.includes("animation") || + lowerMessage.includes("fade") || + lowerMessage.includes("slide") + ) { + return `// Adds animation to element +const styleId = 'mock-animation'; +if (!document.getElementById(styleId)) { + const style = document.createElement('style'); + style.id = styleId; + style.textContent = '@keyframes mockFade { 0% { opacity: 1; } 50% { opacity: 0.5; } 100% { opacity: 1; } }'; + document.head.appendChild(style); +} +$el.style.animation = 'mockFade 2s infinite'`; + } + + return `// Modifies element based on user request +$el.setAttribute("data-modified", "true")`; +}; + +export const storeMockRequest = ( + requestId: string, + messages: Array<{ role: string; content: string }>, +): void => { + mockState.requests.set(requestId, { + requestId, + messages, + timestamp: Date.now(), + }); +}; + +export const generateMockResponse = ( + messages: Array<{ role: string; content: string }>, +): string => { + const lastUserMessage = + messages + .filter((message) => message.role === "user") + .pop()?.content ?? ""; + return generateMockCode(lastUserMessage); +}; + +export const getMockRequestHistory = (): MockRequest[] => { + return Array.from(mockState.requests.values()); +}; + +export const clearMockState = (): void => { + mockState.requests.clear(); +}; \ No newline at end of file diff --git a/packages/website/package.json b/packages/website/package.json index 88d5a381a..0f18bb2f0 100644 --- a/packages/website/package.json +++ b/packages/website/package.json @@ -6,7 +6,9 @@ "dev": "next dev", "build": "pnpm --filter react-grab build && next build", "start": "next start", - "lint": "eslint" + "lint": "eslint", + "test": "vitest run", + "test:watch": "vitest" }, "browserslist": [ "chrome >= 91", @@ -44,6 +46,7 @@ "eslint-config-next": "16.0.7", "tailwindcss": "^4", "tw-animate-css": "^1.4.0", - "typescript": "^5" + "typescript": "^5", + "vitest": "^3.2.4" } } diff --git a/packages/website/test/check-visual-edit-route.test.ts b/packages/website/test/check-visual-edit-route.test.ts new file mode 100644 index 000000000..98aeebe78 --- /dev/null +++ b/packages/website/test/check-visual-edit-route.test.ts @@ -0,0 +1,79 @@ +import { describe, it, expect, beforeEach, vi } from "vitest"; + +const mockIsMockMode = vi.fn(); + +vi.mock("@/lib/env", () => ({ + isMockMode: mockIsMockMode, +})); + +describe("Check Visual Edit API Route", () => { + let GET: () => Response; + let OPTIONS: () => Response; + + beforeEach(async () => { + vi.resetModules(); + vi.clearAllMocks(); + + const route = await import("../app/api/check-visual-edit/route"); + GET = route.GET; + OPTIONS = route.OPTIONS; + }); + + describe("OPTIONS", () => { + it("should return CORS headers", () => { + const response = OPTIONS(); + + expect(response.status).toBe(204); + expect(response.headers.get("Access-Control-Allow-Origin")).toBe("*"); + expect(response.headers.get("Access-Control-Allow-Methods")).toBe("GET, OPTIONS"); + expect(response.headers.get("Access-Control-Allow-Headers")).toBe("Content-Type"); + }); + }); + + describe("GET", () => { + it("should return healthy status with mock mode disabled", async () => { + mockIsMockMode.mockReturnValue(false); + + const response = GET(); + const data = await response.json(); + + expect(response.status).toBe(200); + expect(response.headers.get("Content-Type")).toBe("application/json"); + expect(data).toEqual({ + healthy: true, + mockMode: false, + }); + }); + + it("should return healthy status with mock mode enabled", async () => { + mockIsMockMode.mockReturnValue(true); + + const response = GET(); + const data = await response.json(); + + expect(response.status).toBe(200); + expect(response.headers.get("Content-Type")).toBe("application/json"); + expect(data).toEqual({ + healthy: true, + mockMode: true, + }); + }); + + it("should include CORS headers", async () => { + mockIsMockMode.mockReturnValue(false); + + const response = GET(); + + expect(response.headers.get("Access-Control-Allow-Origin")).toBe("*"); + expect(response.headers.get("Access-Control-Allow-Methods")).toBe("GET, OPTIONS"); + }); + + it("should call isMockMode function", async () => { + mockIsMockMode.mockReturnValue(true); + + GET(); + + expect(mockIsMockMode).toHaveBeenCalledTimes(1); + }); + }); +}); diff --git a/packages/website/test/env.test.ts b/packages/website/test/env.test.ts new file mode 100644 index 000000000..c83363d01 --- /dev/null +++ b/packages/website/test/env.test.ts @@ -0,0 +1,93 @@ +import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; +import { + isMockMode, + getServiceUrl, + getServiceApiKey, + getCerebrasModel, +} from "../lib/env"; + +describe("Environment Configuration", () => { + const originalEnv = process.env; + + beforeEach(() => { + vi.resetModules(); + process.env = { ...originalEnv }; + }); + + afterEach(() => { + process.env = originalEnv; + }); + + describe("isMockMode", () => { + it("should return true when USE_MOCKS is 'true'", () => { + process.env.USE_MOCKS = "true"; + expect(isMockMode()).toBe(true); + }); + + it("should return false when USE_MOCKS is not set", () => { + delete process.env.USE_MOCKS; + expect(isMockMode()).toBe(false); + }); + + it("should return false when USE_MOCKS is 'false'", () => { + process.env.USE_MOCKS = "false"; + expect(isMockMode()).toBe(false); + }); + + it("should return false when USE_MOCKS is any other value", () => { + process.env.USE_MOCKS = "1"; + expect(isMockMode()).toBe(false); + }); + }); + + describe("getServiceUrl", () => { + it("should return mock URL for cerebras when in mock mode", () => { + process.env.USE_MOCKS = "true"; + expect(getServiceUrl("cerebras")).toBe( + "http://localhost:3000/api/mock/cerebras", + ); + }); + + it("should return empty string for cerebras when not in mock mode", () => { + process.env.USE_MOCKS = "false"; + expect(getServiceUrl("cerebras")).toBe(""); + }); + + it("should return empty string when USE_MOCKS is not set", () => { + delete process.env.USE_MOCKS; + expect(getServiceUrl("cerebras")).toBe(""); + }); + }); + + describe("getServiceApiKey", () => { + it("should return mock API key when in mock mode", () => { + process.env.USE_MOCKS = "true"; + expect(getServiceApiKey("cerebras")).toBe("mock-api-key"); + }); + + it("should return undefined when not in mock mode", () => { + process.env.USE_MOCKS = "false"; + expect(getServiceApiKey("cerebras")).toBeUndefined(); + }); + + it("should return undefined when USE_MOCKS is not set", () => { + delete process.env.USE_MOCKS; + expect(getServiceApiKey("cerebras")).toBeUndefined(); + }); + }); + + describe("getCerebrasModel", () => { + it("should return the correct model name", () => { + expect(getCerebrasModel()).toBe("cerebras/glm-4.6"); + }); + + it("should return the same model name regardless of mock mode", () => { + process.env.USE_MOCKS = "true"; + const modelInMockMode = getCerebrasModel(); + process.env.USE_MOCKS = "false"; + const modelInRealMode = getCerebrasModel(); + expect(modelInMockMode).toBe(modelInRealMode); + expect(modelInMockMode).toBe("cerebras/glm-4.6"); + }); + }); +}); diff --git a/packages/website/test/mock-cerebras-route.test.ts b/packages/website/test/mock-cerebras-route.test.ts new file mode 100644 index 000000000..a5559e546 --- /dev/null +++ b/packages/website/test/mock-cerebras-route.test.ts @@ -0,0 +1,189 @@ +import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; + +const mockIsMockMode = vi.fn(); +const mockGenerateMockResponse = vi.fn(); +const mockStoreMockRequest = vi.fn(); + +vi.mock("@/lib/env", () => ({ + isMockMode: mockIsMockMode, +})); + +vi.mock("@/lib/mock-cerebras-state", () => ({ + generateMockResponse: mockGenerateMockResponse, + storeMockRequest: mockStoreMockRequest, +})); + +describe("Mock Cerebras API Route", () => { + let POST: (request: Request) => Promise; + let OPTIONS: () => Response; + + beforeEach(async () => { + vi.resetModules(); + vi.clearAllMocks(); + + const route = await import("../app/api/mock/cerebras/route"); + POST = route.POST; + OPTIONS = route.OPTIONS; + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + describe("OPTIONS", () => { + it("should return CORS headers", () => { + const response = OPTIONS(); + + expect(response.status).toBe(204); + expect(response.headers.get("Access-Control-Allow-Origin")).toBe("*"); + expect(response.headers.get("Access-Control-Allow-Methods")).toBe( + "POST, OPTIONS", + ); + expect(response.headers.get("Access-Control-Allow-Headers")).toBe( + "Content-Type", + ); + }); + }); + + describe("POST", () => { + describe("when not in mock mode", () => { + it("should return 403 error", async () => { + mockIsMockMode.mockReturnValue(false); + + const request = new Request("http://localhost:3000/api/mock/cerebras", { + method: "POST", + body: JSON.stringify({ messages: [{ role: "user", content: "test" }] }), + }); + + const response = await POST(request); + const data = await response.json(); + + expect(response.status).toBe(403); + expect(data.error).toBe("Mock endpoint not available outside mock mode"); + expect(mockStoreMockRequest).not.toHaveBeenCalled(); + expect(mockGenerateMockResponse).not.toHaveBeenCalled(); + }); + }); + + describe("when in mock mode", () => { + beforeEach(() => { + mockIsMockMode.mockReturnValue(true); + }); + + it("should return 400 for invalid JSON", async () => { + const request = new Request("http://localhost:3000/api/mock/cerebras", { + method: "POST", + body: "invalid json", + }); + + const response = await POST(request); + const data = await response.json(); + + expect(response.status).toBe(400); + expect(data.error).toBe("Invalid JSON"); + }); + + it("should return 400 for missing messages", async () => { + const request = new Request("http://localhost:3000/api/mock/cerebras", { + method: "POST", + body: JSON.stringify({}), + }); + + const response = await POST(request); + const data = await response.json(); + + expect(response.status).toBe(400); + expect(data.error).toBe("messages array is required"); + }); + + it("should return 400 for empty messages array", async () => { + const request = new Request("http://localhost:3000/api/mock/cerebras", { + method: "POST", + body: JSON.stringify({ messages: [] }), + }); + + const response = await POST(request); + const data = await response.json(); + + expect(response.status).toBe(400); + expect(data.error).toBe("messages array is required"); + }); + + it("should generate and return mock code for valid request", async () => { + const messages = [{ role: "user" as const, content: "change text" }]; + const mockCode = "$el.textContent = 'Updated content'"; + mockGenerateMockResponse.mockReturnValue(mockCode); + + const request = new Request("http://localhost:3000/api/mock/cerebras", { + method: "POST", + body: JSON.stringify({ messages }), + }); + + const response = await POST(request); + const responseText = await response.text(); + + expect(response.status).toBe(200); + expect(response.headers.get("Content-Type")).toBe("text/javascript"); + expect(response.headers.get("Cache-Control")).toBe( + "no-cache, no-store, must-revalidate", + ); + expect(responseText).toBe(mockCode); + expect(mockStoreMockRequest).toHaveBeenCalled(); + expect(mockGenerateMockResponse).toHaveBeenCalledWith(messages); + }); + + it("should store request with generated ID", async () => { + const messages = [{ role: "user" as const, content: "test" }]; + mockGenerateMockResponse.mockReturnValue("// mock code"); + + const request = new Request("http://localhost:3000/api/mock/cerebras", { + method: "POST", + body: JSON.stringify({ messages }), + }); + + await POST(request); + + expect(mockStoreMockRequest).toHaveBeenCalledTimes(1); + const [requestId, storedMessages] = mockStoreMockRequest.mock.calls[0]; + expect(requestId).toMatch(/^mock-\d+-[a-z0-9]+$/); + expect(storedMessages).toEqual(messages); + }); + + it("should include CORS headers in response", async () => { + const messages = [{ role: "user" as const, content: "test" }]; + mockGenerateMockResponse.mockReturnValue("// code"); + + const request = new Request("http://localhost:3000/api/mock/cerebras", { + method: "POST", + body: JSON.stringify({ messages }), + }); + + const response = await POST(request); + + expect(response.headers.get("Access-Control-Allow-Origin")).toBe("*"); + expect(response.headers.get("Access-Control-Allow-Methods")).toBe( + "POST, OPTIONS", + ); + }); + + it("should handle multiple messages in conversation", async () => { + const messages = [ + { role: "user" as const, content: "first message" }, + { role: "assistant" as const, content: "first response" }, + { role: "user" as const, content: "second message" }, + ]; + mockGenerateMockResponse.mockReturnValue("// response code"); + + const request = new Request("http://localhost:3000/api/mock/cerebras", { + method: "POST", + body: JSON.stringify({ messages }), + }); + + const response = await POST(request); + + expect(response.status).toBe(200); + expect(mockGenerateMockResponse).toHaveBeenCalledWith(messages); + }); + }); + }); +}); diff --git a/packages/website/test/mock-cerebras-state.test.ts b/packages/website/test/mock-cerebras-state.test.ts new file mode 100644 index 000000000..71cae87e8 --- /dev/null +++ b/packages/website/test/mock-cerebras-state.test.ts @@ -0,0 +1,289 @@ +import { describe, it, expect, beforeEach } from "vitest"; +import { + storeMockRequest, + generateMockResponse, + getMockRequestHistory, + clearMockState, +} from "../lib/mock-cerebras-state"; + +describe("Mock Cerebras State Manager", () => { + beforeEach(() => { + clearMockState(); + }); + + describe("storeMockRequest", () => { + it("should store a request with correct structure", () => { + const requestId = "test-request-1"; + const messages = [ + { role: "user", content: "change text" }, + { role: "assistant", content: "code" }, + ]; + + storeMockRequest(requestId, messages); + const history = getMockRequestHistory(); + + expect(history).toHaveLength(1); + expect(history[0].requestId).toBe(requestId); + expect(history[0].messages).toEqual(messages); + expect(history[0].timestamp).toBeGreaterThan(0); + }); + + it("should store multiple requests", () => { + storeMockRequest("req-1", [{ role: "user", content: "test 1" }]); + storeMockRequest("req-2", [{ role: "user", content: "test 2" }]); + storeMockRequest("req-3", [{ role: "user", content: "test 3" }]); + + const history = getMockRequestHistory(); + expect(history).toHaveLength(3); + expect(history.map((r) => r.requestId)).toEqual(["req-1", "req-2", "req-3"]); + }); + + it("should overwrite request with same ID", () => { + const requestId = "same-id"; + storeMockRequest(requestId, [{ role: "user", content: "first" }]); + storeMockRequest(requestId, [{ role: "user", content: "second" }]); + + const history = getMockRequestHistory(); + expect(history).toHaveLength(1); + expect(history[0].messages[0].content).toBe("second"); + }); + }); + + describe("generateMockResponse", () => { + describe("text manipulation patterns", () => { + it("should generate text content change code for 'text' keyword", () => { + const messages = [{ role: "user", content: "change the text" }]; + const code = generateMockResponse(messages); + + expect(code).toContain("textContent"); + expect(code).toContain("$el"); + expect(code).toContain("Updated content"); + }); + + it("should generate text content change code for 'content' keyword", () => { + const messages = [{ role: "user", content: "update content" }]; + const code = generateMockResponse(messages); + + expect(code).toContain("textContent"); + expect(code).toContain("$el"); + }); + }); + + describe("class manipulation patterns", () => { + it("should generate class addition code for 'class' keyword", () => { + const messages = [{ role: "user", content: "add a class" }]; + const code = generateMockResponse(messages); + + expect(code).toContain("classList.add"); + expect(code).toContain("highlighted"); + expect(code).toContain("$el"); + }); + + it("should generate class addition code for 'style' keyword", () => { + const messages = [{ role: "user", content: "apply style" }]; + const code = generateMockResponse(messages); + + expect(code).toContain("classList.add"); + }); + }); + + describe("visibility patterns", () => { + it("should generate hide code for 'hide' keyword", () => { + const messages = [{ role: "user", content: "hide the element" }]; + const code = generateMockResponse(messages); + + expect(code).toContain("display"); + expect(code).toContain("none"); + expect(code).toContain("$el.style"); + }); + + it("should generate hide code for 'remove' keyword", () => { + const messages = [{ role: "user", content: "remove this" }]; + const code = generateMockResponse(messages); + + expect(code).toContain("display"); + expect(code).toContain("none"); + }); + }); + + describe("color patterns", () => { + it("should generate color change code for 'color' keyword", () => { + const messages = [{ role: "user", content: "change color" }]; + const code = generateMockResponse(messages); + + expect(code).toContain("backgroundColor"); + expect(code).toContain("#3b82f6"); + expect(code).toContain("$el.style"); + }); + + it("should generate color change code for 'background' keyword", () => { + const messages = [{ role: "user", content: "set background" }]; + const code = generateMockResponse(messages); + + expect(code).toContain("backgroundColor"); + }); + + it("should generate color change code for 'bg' keyword", () => { + const messages = [{ role: "user", content: "update bg" }]; + const code = generateMockResponse(messages); + + expect(code).toContain("backgroundColor"); + }); + }); + + describe("animation patterns", () => { + it("should generate animation code for 'animate' keyword", () => { + const messages = [{ role: "user", content: "animate this" }]; + const code = generateMockResponse(messages); + + expect(code).toContain("animation"); + expect(code).toContain("@keyframes"); + expect(code).toContain("mockFade"); + expect(code).toContain("document.createElement('style')"); + }); + + it("should generate animation code for 'animation' keyword", () => { + const messages = [{ role: "user", content: "add animation" }]; + const code = generateMockResponse(messages); + + expect(code).toContain("animation"); + expect(code).toContain("mockFade"); + }); + + it("should generate animation code for 'fade' keyword", () => { + const messages = [{ role: "user", content: "fade in" }]; + const code = generateMockResponse(messages); + + expect(code).toContain("animation"); + expect(code).toContain("mockFade"); + }); + + it("should generate animation code for 'slide' keyword", () => { + const messages = [{ role: "user", content: "slide left" }]; + const code = generateMockResponse(messages); + + expect(code).toContain("animation"); + expect(code).toContain("mockFade"); + }); + }); + + describe("default pattern", () => { + it("should generate default code for unmatched patterns", () => { + const messages = [{ role: "user", content: "do something random" }]; + const code = generateMockResponse(messages); + + expect(code).toContain("setAttribute"); + expect(code).toContain("data-modified"); + expect(code).toContain("true"); + expect(code).toContain("$el"); + }); + + it("should generate default code for empty message", () => { + const messages = [{ role: "user", content: "" }]; + const code = generateMockResponse(messages); + + expect(code).toContain("setAttribute"); + expect(code).toContain("data-modified"); + }); + }); + + describe("message handling", () => { + it("should use the last user message", () => { + const messages = [ + { role: "user", content: "hide this" }, + { role: "assistant", content: "some code" }, + { role: "user", content: "change text instead" }, + ]; + const code = generateMockResponse(messages); + + expect(code).toContain("textContent"); + expect(code).not.toContain("display"); + }); + + it("should handle messages without user role", () => { + const messages = [{ role: "assistant", content: "some code" }]; + const code = generateMockResponse(messages); + + expect(code).toContain("setAttribute"); + expect(code).toContain("data-modified"); + }); + + it("should handle empty messages array", () => { + const messages: Array<{ role: string; content: string }> = []; + const code = generateMockResponse(messages); + + expect(code).toContain("setAttribute"); + expect(code).toContain("data-modified"); + }); + }); + + describe("case insensitivity", () => { + it("should match patterns regardless of case", () => { + const upperCase = generateMockResponse([ + { role: "user", content: "CHANGE TEXT" }, + ]); + const lowerCase = generateMockResponse([ + { role: "user", content: "change text" }, + ]); + const mixedCase = generateMockResponse([ + { role: "user", content: "ChAnGe TeXt" }, + ]); + + expect(upperCase).toContain("textContent"); + expect(lowerCase).toContain("textContent"); + expect(mixedCase).toContain("textContent"); + }); + }); + }); + + describe("getMockRequestHistory", () => { + it("should return empty array when no requests stored", () => { + const history = getMockRequestHistory(); + expect(history).toEqual([]); + }); + + it("should return all stored requests", () => { + storeMockRequest("req-1", [{ role: "user", content: "test 1" }]); + storeMockRequest("req-2", [{ role: "user", content: "test 2" }]); + + const history = getMockRequestHistory(); + expect(history).toHaveLength(2); + }); + + it("should return requests in insertion order", () => { + storeMockRequest("req-1", [{ role: "user", content: "first" }]); + storeMockRequest("req-2", [{ role: "user", content: "second" }]); + storeMockRequest("req-3", [{ role: "user", content: "third" }]); + + const history = getMockRequestHistory(); + expect(history[0].requestId).toBe("req-1"); + expect(history[1].requestId).toBe("req-2"); + expect(history[2].requestId).toBe("req-3"); + }); + }); + + describe("clearMockState", () => { + it("should clear all stored requests", () => { + storeMockRequest("req-1", [{ role: "user", content: "test 1" }]); + storeMockRequest("req-2", [{ role: "user", content: "test 2" }]); + storeMockRequest("req-3", [{ role: "user", content: "test 3" }]); + + expect(getMockRequestHistory()).toHaveLength(3); + + clearMockState(); + + expect(getMockRequestHistory()).toHaveLength(0); + expect(getMockRequestHistory()).toEqual([]); + }); + + it("should allow storing new requests after clearing", () => { + storeMockRequest("req-1", [{ role: "user", content: "test 1" }]); + clearMockState(); + storeMockRequest("req-2", [{ role: "user", content: "test 2" }]); + + const history = getMockRequestHistory(); + expect(history).toHaveLength(1); + expect(history[0].requestId).toBe("req-2"); + }); + }); +}); diff --git a/packages/website/vitest.config.ts b/packages/website/vitest.config.ts new file mode 100644 index 000000000..9d62172b8 --- /dev/null +++ b/packages/website/vitest.config.ts @@ -0,0 +1,16 @@ +import { defineConfig } from "vitest/config"; +import path from "path"; + +export default defineConfig({ + test: { + globals: true, + environment: "node", + include: ["test/**/*.test.ts"], + testTimeout: 10000, + }, + resolve: { + alias: { + "@": path.resolve(__dirname, "./"), + }, + }, +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c6f9e2946..23d31daf1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -669,6 +669,9 @@ importers: typescript: specifier: ^5 version: 5.9.3 + vitest: + specifier: ^3.2.4 + version: 3.2.4(@types/node@20.19.23)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.6)(yaml@2.8.1) packages: @@ -2210,8 +2213,8 @@ packages: resolution: {integrity: sha512-y9/ltK2TY+0HD1H2Sz7MvU3zFh4SjER6eQVNQfBx/0gK9N7S0QwHW6cmhHLx3CP25zN190LKHXPieMGqsVvrOQ==} engines: {node: '>=18'} - '@sourcegraph/amp@0.0.1766606479-gbadae7': - resolution: {integrity: sha512-E+MDo1wrARCyEbHCVUA10jQd4XusofKZDamrDVx281tGOEJnKUwnE+Am94ZHJuN3QP8xzy7/xBbafh8fBe6qxQ==} + '@sourcegraph/amp@0.0.1767556880-gd24d7a': + resolution: {integrity: sha512-kA5jmU8Azl824pnTjKcVuYzFzr3hiOW+Jw8DGVMlEA52nthVxZZMuRDH8jD4N5BULm1D+5zyoj6vU6ygRuHMUA==} engines: {node: '>=20'} hasBin: true @@ -7467,10 +7470,10 @@ snapshots: '@sourcegraph/amp-sdk@0.1.0-20251210081226-g90e3892': dependencies: - '@sourcegraph/amp': 0.0.1766606479-gbadae7 + '@sourcegraph/amp': 0.0.1767556880-gd24d7a zod: 3.25.76 - '@sourcegraph/amp@0.0.1766606479-gbadae7': + '@sourcegraph/amp@0.0.1767556880-gd24d7a': dependencies: '@napi-rs/keyring': 1.1.9 @@ -8007,6 +8010,14 @@ snapshots: chai: 5.3.3 tinyrainbow: 2.0.0 + '@vitest/mocker@3.2.4(vite@6.4.1(@types/node@20.19.23)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.6)(yaml@2.8.1))': + dependencies: + '@vitest/spy': 3.2.4 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 6.4.1(@types/node@20.19.23)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.6)(yaml@2.8.1) + '@vitest/mocker@3.2.4(vite@6.4.1(@types/node@24.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.6)(yaml@2.8.1))': dependencies: '@vitest/spy': 3.2.4 @@ -8912,8 +8923,8 @@ snapshots: '@typescript-eslint/parser': 8.46.1(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3) eslint: 9.37.0(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.37.0(jiti@2.6.1)) - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.46.1(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.37.0(jiti@2.6.1)) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.46.1(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.37.0(jiti@2.6.1)))(eslint@9.37.0(jiti@2.6.1)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.46.1(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.46.1(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.37.0(jiti@2.6.1)))(eslint@9.37.0(jiti@2.6.1)))(eslint@9.37.0(jiti@2.6.1)) eslint-plugin-jsx-a11y: 6.10.2(eslint@9.37.0(jiti@2.6.1)) eslint-plugin-react: 7.37.5(eslint@9.37.0(jiti@2.6.1)) eslint-plugin-react-hooks: 5.2.0(eslint@9.37.0(jiti@2.6.1)) @@ -8952,7 +8963,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(eslint@9.37.0(jiti@2.6.1)))(eslint@9.37.0(jiti@2.6.1)): + eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.46.1(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.37.0(jiti@2.6.1)))(eslint@9.37.0(jiti@2.6.1)): dependencies: '@nolyfill/is-core-module': 1.0.39 debug: 4.4.3 @@ -8963,11 +8974,11 @@ snapshots: tinyglobby: 0.2.15 unrs-resolver: 1.11.1 optionalDependencies: - eslint-plugin-import: 2.32.0(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(eslint@9.37.0(jiti@2.6.1)))(eslint@9.37.0(jiti@2.6.1)))(eslint@9.37.0(jiti@2.6.1)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.46.1(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.46.1(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.37.0(jiti@2.6.1)))(eslint@9.37.0(jiti@2.6.1)))(eslint@9.37.0(jiti@2.6.1)) transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.37.0(jiti@2.6.1)): + eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(eslint@9.37.0(jiti@2.6.1)))(eslint@9.37.0(jiti@2.6.1)): dependencies: '@nolyfill/is-core-module': 1.0.39 debug: 4.4.3 @@ -8978,18 +8989,18 @@ snapshots: tinyglobby: 0.2.15 unrs-resolver: 1.11.1 optionalDependencies: - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.46.1(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.37.0(jiti@2.6.1)) + eslint-plugin-import: 2.32.0(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(eslint@9.37.0(jiti@2.6.1)))(eslint@9.37.0(jiti@2.6.1)))(eslint@9.37.0(jiti@2.6.1)) transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.1(@typescript-eslint/parser@8.46.1(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.37.0(jiti@2.6.1)): + eslint-module-utils@2.12.1(@typescript-eslint/parser@8.46.1(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.46.1(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.37.0(jiti@2.6.1)))(eslint@9.37.0(jiti@2.6.1)))(eslint@9.37.0(jiti@2.6.1)): dependencies: debug: 3.2.7 optionalDependencies: '@typescript-eslint/parser': 8.46.1(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3) eslint: 9.37.0(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.37.0(jiti@2.6.1)) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.46.1(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.37.0(jiti@2.6.1)))(eslint@9.37.0(jiti@2.6.1)) transitivePeerDependencies: - supports-color @@ -9003,7 +9014,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.46.1(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.37.0(jiti@2.6.1)): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.46.1(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.46.1(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.37.0(jiti@2.6.1)))(eslint@9.37.0(jiti@2.6.1)))(eslint@9.37.0(jiti@2.6.1)): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -9014,7 +9025,7 @@ snapshots: doctrine: 2.1.0 eslint: 9.37.0(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.46.1(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.37.0(jiti@2.6.1)) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.46.1(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.46.1(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.37.0(jiti@2.6.1)))(eslint@9.37.0(jiti@2.6.1)))(eslint@9.37.0(jiti@2.6.1)) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -11493,6 +11504,27 @@ snapshots: d3-time: 3.1.0 d3-timer: 3.0.1 + vite-node@3.2.4(@types/node@20.19.23)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.6)(yaml@2.8.1): + dependencies: + cac: 6.7.14 + debug: 4.4.3 + es-module-lexer: 1.7.0 + pathe: 2.0.3 + vite: 6.4.1(@types/node@20.19.23)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.6)(yaml@2.8.1) + transitivePeerDependencies: + - '@types/node' + - jiti + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + vite-node@3.2.4(@types/node@24.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.6)(yaml@2.8.1): dependencies: cac: 6.7.14 @@ -11542,6 +11574,23 @@ snapshots: - terser - tsx + vite@6.4.1(@types/node@20.19.23)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.6)(yaml@2.8.1): + dependencies: + esbuild: 0.25.11 + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + postcss: 8.5.6 + rollup: 4.52.5 + tinyglobby: 0.2.15 + optionalDependencies: + '@types/node': 20.19.23 + fsevents: 2.3.3 + jiti: 2.6.1 + lightningcss: 1.30.2 + terser: 5.37.0 + tsx: 4.20.6 + yaml: 2.8.1 + vite@6.4.1(@types/node@24.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.6)(yaml@2.8.1): dependencies: esbuild: 0.25.11 @@ -11559,6 +11608,47 @@ snapshots: tsx: 4.20.6 yaml: 2.8.1 + vitest@3.2.4(@types/node@20.19.23)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.6)(yaml@2.8.1): + dependencies: + '@types/chai': 5.2.3 + '@vitest/expect': 3.2.4 + '@vitest/mocker': 3.2.4(vite@6.4.1(@types/node@20.19.23)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.6)(yaml@2.8.1)) + '@vitest/pretty-format': 3.2.4 + '@vitest/runner': 3.2.4 + '@vitest/snapshot': 3.2.4 + '@vitest/spy': 3.2.4 + '@vitest/utils': 3.2.4 + chai: 5.3.3 + debug: 4.4.3 + expect-type: 1.2.2 + magic-string: 0.30.21 + pathe: 2.0.3 + picomatch: 4.0.3 + std-env: 3.10.0 + tinybench: 2.9.0 + tinyexec: 0.3.2 + tinyglobby: 0.2.15 + tinypool: 1.1.1 + tinyrainbow: 2.0.0 + vite: 6.4.1(@types/node@20.19.23)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.6)(yaml@2.8.1) + vite-node: 3.2.4(@types/node@20.19.23)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.6)(yaml@2.8.1) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 20.19.23 + transitivePeerDependencies: + - jiti + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + vitest@3.2.4(@types/node@24.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.6)(yaml@2.8.1): dependencies: '@types/chai': 5.2.3