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));
}