Testing Guide

Comprehensive guide for testing Mosaic Builder, including unit tests, integration tests, and end-to-end testing.

Quick Start

# Run all tests
pnpm test

# Run specific test suite
pnpm test:unit
pnpm test:integration
pnpm test:e2e

# Watch mode for development
pnpm test:watch

# Coverage report
pnpm test:coverage

Test Structure

tests/
├── unit/               # Unit tests for individual functions
├── integration/        # API and database integration tests
├── e2e/               # End-to-end browser tests
├── fixtures/          # Test data and mocks
└── helpers/           # Test utilities

Unit Testing

Testing DSL Extraction

// tests/unit/dsl-extractor.test.js
import { describe, it, expect } from 'vitest';
import { DSLExtractor } from '@/lib/dsl/extractor';

describe('DSL Extractor', () => {
  it('should extract entities from simple message', () => {
    const extractor = new DSLExtractor();
    const message = "I need a blog with posts and comments";
    
    const result = extractor.extract(message);
    
    expect(result.entities).toHaveProperty('Post');
    expect(result.entities).toHaveProperty('Comment');
    expect(result.confidence).toBeGreaterThan(0.8);
  });

  it('should detect relationships', () => {
    const extractor = new DSLExtractor();
    const message = "Users can write many posts";
    
    const result = extractor.extract(message);
    
    expect(result.relationships).toContainEqual({
      from: 'User',
      to: 'Post',
      type: 'one-to-many'
    });
  });

  it('should infer field types', () => {
    const extractor = new DSLExtractor();
    const message = "Posts have title, content, and published date";
    
    const result = extractor.extract(message);
    
    expect(result.entities.Post.fields).toMatchObject({
      title: { type: 'string' },
      content: { type: 'text' },
      publishedDate: { type: 'datetime' }
    });
  });
});

Testing Code Generation

// tests/unit/code-generator.test.js
import { DatabaseSchemaGenerator } from '@/lib/generation/database-generator';
import { blogDSL, ecommerceDSL } from '../fixtures/example-dsls';

describe('Code Generator', () => {
  let generator;

  beforeEach(() => {
    generator = new DatabaseSchemaGenerator();
  });

  it('should validate DSL before generation', () => {
    const emptyDSL = { entities: {}, relationships: {} };
    const result = generator.validateDSL(emptyDSL);
    
    expect(result.valid).toBe(false);
    expect(result.errors).toContain('No entities defined');
  });

  it('should generate valid Prisma schema', async () => {
    const result = await generator.generate(blogDSL, {
      database: { target: 'prisma', provider: 'postgresql' }
    });
    
    expect(result.success).toBe(true);
    expect(result.files).toHaveLength(2); // schema + migration
    
    const schema = result.files.find(f => f.path === 'prisma/schema.prisma');
    expect(schema.content).toContain('model Post');
    expect(schema.content).toContain('model Comment');
  });

  it('should handle complex relationships', async () => {
    const result = await generator.generate(ecommerceDSL, {
      database: { target: 'prisma', provider: 'postgresql' }
    });
    
    const schema = result.files[0].content;
    expect(schema).toContain('@relation');
    expect(schema).toContain('@@index');
  });
});

Integration Testing

Testing API Endpoints

// tests/integration/chat-api.test.js
import { describe, it, expect, beforeAll } from 'vitest';
import { createTestClient } from '../helpers/test-client';

describe('Chat API', () => {
  let client;
  let authToken;

  beforeAll(async () => {
    client = createTestClient();
    authToken = await client.authenticate('test@example.com', 'password');
  });

  it('should create new chat session', async () => {
    const response = await client.post('/api/chat', {
      messages: [{ role: 'user', content: 'Create a task manager' }]
    }, { 
      headers: { Authorization: `Bearer ${authToken}` }
    });

    expect(response.status).toBe(200);
    expect(response.headers['content-type']).toContain('text/event-stream');
  });

  it('should extract DSL from conversation', async () => {
    const response = await client.post('/api/chat', {
      messages: [{ 
        role: 'user', 
        content: 'I need a blog with posts, authors, and comments' 
      }],
      enableDSL: true
    });

    const events = await client.parseSSE(response);
    const dslEvent = events.find(e => e.type === 'dsl');
    
    expect(dslEvent).toBeDefined();
    expect(dslEvent.content.entities).toHaveProperty('Post');
    expect(dslEvent.content.entities).toHaveProperty('Comment');
  });

  it('should handle rate limiting', async () => {
    // Send many requests quickly
    const promises = Array(20).fill(0).map(() => 
      client.post('/api/chat', { messages: [] })
    );

    const results = await Promise.allSettled(promises);
    const rateLimited = results.filter(r => 
      r.status === 'rejected' && r.reason.status === 429
    );

    expect(rateLimited.length).toBeGreaterThan(0);
  });
});

Testing Database Operations

// tests/integration/database.test.js
import { prisma } from '@/lib/prisma';
import { DSLManager } from '@/lib/dsl/manager';

describe('Database Operations', () => {
  beforeEach(async () => {
    // Clean database
    await prisma.message.deleteMany();
    await prisma.chat.deleteMany();
    await prisma.user.deleteMany();
  });

  it('should store and retrieve DSL events', async () => {
    const chat = await prisma.chat.create({
      data: { userId: testUser.id }
    });

    const manager = new DSLManager(chat.id);
    
    await manager.addEntity('User', {
      id: { type: 'uuid', required: true },
      email: { type: 'string', required: true }
    }, 'User entity detected');

    const dsl = await manager.getCurrentDSL();
    
    expect(dsl.entities).toHaveProperty('User');
    expect(dsl.entities.User.fields).toHaveProperty('email');
  });

  it('should create snapshots after many events', async () => {
    const manager = new DSLManager(testChat.id);
    
    // Create many events
    for (let i = 0; i < 60; i++) {
      await manager.addEntity(`Entity${i}`, {}, 'Test entity');
    }

    const snapshots = await prisma.dslSnapshot.findMany({
      where: { chatId: testChat.id }
    });

    expect(snapshots.length).toBeGreaterThan(0);
  });
});

End-to-End Testing

Playwright Tests

// tests/e2e/chat-flow.test.js
import { test, expect } from '@playwright/test';

test.describe('Chat Flow', () => {
  test.beforeEach(async ({ page }) => {
    await page.goto('/');
    await page.fill('[name=email]', 'test@example.com');
    await page.fill('[name=password]', 'password');
    await page.click('button[type=submit]');
  });

  test('complete conversation flow', async ({ page }) => {
    // Start conversation
    await page.fill('[placeholder="Send a message..."]', 
      'Create a blog system with posts and comments');
    await page.click('button[aria-label="Send message"]');

    // Wait for response
    await page.waitForSelector('[data-testid="ai-response"]');

    // Check DSL panel updates
    await expect(page.locator('[data-testid="dsl-panel"]')).toBeVisible();
    await expect(page.locator('[data-testid="phase-indicator"]'))
      .toContainText('Discovering');

    // Continue conversation
    await page.fill('[placeholder="Send a message..."]', 
      'Add user authentication and roles');
    await page.click('button[aria-label="Send message"]');

    // Wait for phase change
    await page.waitForSelector('text=Clarifying');

    // Check readiness increases
    const readiness = await page.locator('[data-testid="readiness"]').textContent();
    expect(parseInt(readiness)).toBeGreaterThan(25);
  });

  test('code generation', async ({ page }) => {
    // Load existing conversation with high readiness
    await page.goto('/chat/test-chat-id');

    // Wait for generate button
    await page.waitForSelector('button:has-text("Generate Code")', {
      state: 'visible'
    });

    // Click generate
    await page.click('button:has-text("Generate Code")');

    // Select options
    await page.check('input[value="prisma"]');
    await page.check('input[value="api"]');
    await page.click('button:has-text("Generate")');

    // Wait for download
    await page.waitForSelector('[data-testid="download-link"]');
  });
});

Visual Regression Testing

// tests/e2e/visual.test.js
import { test, expect } from '@playwright/test';

test.describe('Visual Regression', () => {
  test('chat interface', async ({ page }) => {
    await page.goto('/');
    await expect(page).toHaveScreenshot('chat-interface.png');
  });

  test('DSL panel states', async ({ page }) => {
    await page.goto('/chat/sample-chat');
    
    // Test each phase appearance
    const phases = ['discovering', 'clarifying', 'proposing', 'confirming', 'building'];
    
    for (const phase of phases) {
      await page.evaluate((p) => {
        window.setTestPhase(p);
      }, phase);
      
      await expect(page.locator('[data-testid="dsl-panel"]'))
        .toHaveScreenshot(`dsl-panel-${phase}.png`);
    }
  });
});

Test Fixtures

Example DSLs

// tests/fixtures/example-dsls.js
export const blogDSL = {
  entities: {
    User: {
      fields: {
        id: { type: 'uuid', required: true },
        email: { type: 'string', required: true, unique: true },
        name: { type: 'string', required: true }
      }
    },
    Post: {
      fields: {
        id: { type: 'uuid', required: true },
        title: { type: 'string', required: true },
        content: { type: 'text', required: true },
        authorId: { type: 'uuid', required: true }
      }
    }
  },
  relationships: [
    { from: 'Post', to: 'User', type: 'many-to-one', name: 'author' }
  ],
  phase: 'confirming',
  readiness: 85,
  confidence: 0.92
};

export const ecommerceDSL = {
  // ... complex e-commerce DSL
};

Mock LLM Responses

// tests/fixtures/mock-llm.js
export const mockOpenRouter = {
  async complete(prompt) {
    if (prompt.includes('blog')) {
      return {
        text: "I'll help you create a blog system...",
        dsl: blogDSL
      };
    }
    // ... other mock responses
  }
};

Test Helpers

Test Client

// tests/helpers/test-client.js
export function createTestClient() {
  return {
    async post(url, data, options = {}) {
      return fetch(`http://localhost:3000${url}`, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          ...options.headers
        },
        body: JSON.stringify(data)
      });
    },

    async parseSSE(response) {
      const text = await response.text();
      return text.split('\n')
        .filter(line => line.startsWith('data: '))
        .map(line => JSON.parse(line.slice(6)));
    }
  };
}

Database Helpers

// tests/helpers/database.js
export async function setupTestDatabase() {
  // Run migrations
  await prisma.$executeRaw`DELETE FROM "Message"`;
  await prisma.$executeRaw`DELETE FROM "Chat"`;
  await prisma.$executeRaw`DELETE FROM "User"`;
  
  // Create test user
  const user = await prisma.user.create({
    data: {
      email: 'test@example.com',
      password: await hash('password'),
      name: 'Test User'
    }
  });
  
  return { user };
}

Performance Testing

Load Testing

// tests/performance/load.test.js
import { check } from 'k6';
import http from 'k6/http';

export const options = {
  stages: [
    { duration: '30s', target: 20 },  // Ramp up
    { duration: '1m', target: 20 },   // Stay at 20 users
    { duration: '30s', target: 0 },   // Ramp down
  ],
  thresholds: {
    http_req_duration: ['p(95)<500'], // 95% of requests under 500ms
    http_req_failed: ['rate<0.1'],    // Error rate under 10%
  },
};

export default function() {
  const response = http.post('http://localhost:3000/api/chat', {
    messages: [{ role: 'user', content: 'Create a blog' }]
  });
  
  check(response, {
    'status is 200': (r) => r.status === 200,
    'response time < 500ms': (r) => r.timings.duration < 500,
  });
}

Test Configuration

Vitest Config

// vitest.config.js
import { defineConfig } from 'vitest/config';
import path from 'path';

export default defineConfig({
  test: {
    environment: 'node',
    globals: true,
    setupFiles: ['./tests/setup.js'],
    coverage: {
      provider: 'v8',
      reporter: ['text', 'json', 'html'],
      exclude: ['node_modules', 'tests']
    },
  },
  resolve: {
    alias: {
      '@': path.resolve(__dirname, './'),
    },
  },
});

Playwright Config

// playwright.config.js
export default {
  testDir: './tests/e2e',
  timeout: 30000,
  use: {
    baseURL: 'http://localhost:3000',
    screenshot: 'only-on-failure',
    video: 'retain-on-failure',
  },
  projects: [
    { name: 'chromium', use: { ...devices['Desktop Chrome'] } },
    { name: 'firefox', use: { ...devices['Desktop Firefox'] } },
    { name: 'webkit', use: { ...devices['Desktop Safari'] } },
  ],
};

Continuous Integration

GitHub Actions

# .github/workflows/test.yml
name: Tests

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    
    services:
      postgres:
        image: postgres:16
        env:
          POSTGRES_PASSWORD: postgres
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5
        ports:
          - 5432:5432

    steps:
      - uses: actions/checkout@v3
      
      - uses: pnpm/action-setup@v2
        with:
          version: 9
      
      - uses: actions/setup-node@v3
        with:
          node-version: 20
          cache: 'pnpm'
      
      - run: pnpm install
      
      - name: Run migrations
        run: pnpm prisma migrate deploy
        env:
          DATABASE_URL: postgresql://postgres:postgres@localhost:5432/test
      
      - name: Run unit tests
        run: pnpm test:unit
      
      - name: Run integration tests
        run: pnpm test:integration
        env:
          DATABASE_URL: postgresql://postgres:postgres@localhost:5432/test
          OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}
      
      - name: Run E2E tests
        run: pnpm test:e2e
      
      - name: Upload coverage
        uses: codecov/codecov-action@v3
        with:
          files: ./coverage/coverage.json

Test Commands

Package.json Scripts

{
  "scripts": {
    "test": "vitest run",
    "test:unit": "vitest run tests/unit",
    "test:integration": "vitest run tests/integration",
    "test:e2e": "playwright test",
    "test:watch": "vitest watch",
    "test:coverage": "vitest run --coverage",
    "test:ui": "vitest --ui",
    "test:load": "k6 run tests/performance/load.test.js"
  }
}

Debugging Tests

VS Code Launch Config

// .vscode/launch.json
{
  "configurations": [
    {
      "type": "node",
      "request": "launch",
      "name": "Debug Tests",
      "runtimeExecutable": "pnpm",
      "runtimeArgs": ["test:watch"],
      "console": "integratedTerminal"
    }
  ]
}

Debug Helpers

// tests/helpers/debug.js
export function logDSL(dsl) {
  console.log(JSON.stringify(dsl, null, 2));
}

export function pauseTest(ms = 10000) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

Best Practices

  • Test Isolation: Each test should be independent
  • Use Fixtures: Reuse test data across tests
  • Mock External Services: Don't call real APIs in tests
  • Test Edge Cases: Empty inputs, large inputs, invalid data
  • Maintain Test Coverage: Aim for >80% coverage
  • Fast Tests: Keep unit tests under 100ms
  • Descriptive Names: Test names should explain what they test
  • Clean Up: Always clean up test data
  • Related Documentation

  • Contributing Guide
  • Development Setup
  • CI/CD Pipeline