fix: Restore all files lost during destructive rebase
A previous `git pull --rebase origin main` dropped 177 local commits,
losing 3400+ files across admin-v2, backend, studio-v2, website,
klausur-service, and many other services. The partial restore attempt
(660295e2) only recovered some files.
This commit restores all missing files from pre-rebase ref 98933f5e
while preserving post-rebase additions (night-scheduler, night-mode UI,
NightModeWidget dashboard integration).
Restored features include:
- AI Module Sidebar (FAB), OCR Labeling, OCR Compare
- GPU Dashboard, RAG Pipeline, Magic Help
- Klausur-Korrektur (8 files), Abitur-Archiv (5+ files)
- Companion, Zeugnisse-Crawler, Screen Flow
- Full backend, studio-v2, website, klausur-service
- All compliance SDKs, agent-core, voice-service
- CI/CD configs, documentation, scripts
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
343
website/tests/quality-dashboard.test.ts
Normal file
343
website/tests/quality-dashboard.test.ts
Normal file
@@ -0,0 +1,343 @@
|
||||
/**
|
||||
* Quality Dashboard Tests
|
||||
*
|
||||
* Tests for the BQAS Quality Dashboard components and API integration
|
||||
*/
|
||||
|
||||
describe('Quality Dashboard', () => {
|
||||
describe('Page Structure', () => {
|
||||
it('should have the quality page file', () => {
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
|
||||
const pagePath = path.join(__dirname, '../app/admin/quality/page.tsx')
|
||||
expect(fs.existsSync(pagePath)).toBe(true)
|
||||
})
|
||||
|
||||
it('should have navigation entry in AdminLayout', () => {
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
|
||||
const layoutPath = path.join(__dirname, '../components/admin/AdminLayout.tsx')
|
||||
const content = fs.readFileSync(layoutPath, 'utf-8')
|
||||
|
||||
expect(content).toContain("name: 'Qualitaet'")
|
||||
expect(content).toContain("href: '/admin/quality'")
|
||||
})
|
||||
})
|
||||
|
||||
describe('API Types', () => {
|
||||
interface TestResult {
|
||||
test_id: string
|
||||
test_name: string
|
||||
passed: boolean
|
||||
composite_score: number
|
||||
intent_accuracy: number
|
||||
faithfulness: number
|
||||
relevance: number
|
||||
coherence: number
|
||||
safety: string
|
||||
reasoning: string
|
||||
}
|
||||
|
||||
interface BQASMetrics {
|
||||
total_tests: number
|
||||
passed_tests: number
|
||||
failed_tests: number
|
||||
avg_intent_accuracy: number
|
||||
avg_faithfulness: number
|
||||
avg_relevance: number
|
||||
avg_coherence: number
|
||||
safety_pass_rate: number
|
||||
avg_composite_score: number
|
||||
scores_by_intent: Record<string, number>
|
||||
failed_test_ids: string[]
|
||||
}
|
||||
|
||||
interface TestRun {
|
||||
id: number
|
||||
timestamp: string
|
||||
git_commit: string
|
||||
golden_score: number
|
||||
synthetic_score: number
|
||||
total_tests: number
|
||||
passed_tests: number
|
||||
failed_tests: number
|
||||
duration_seconds: number
|
||||
}
|
||||
|
||||
it('should have valid TestResult structure', () => {
|
||||
const testResult: TestResult = {
|
||||
test_id: 'INT-001',
|
||||
test_name: 'Test Name',
|
||||
passed: true,
|
||||
composite_score: 4.5,
|
||||
intent_accuracy: 95,
|
||||
faithfulness: 5,
|
||||
relevance: 4,
|
||||
coherence: 4,
|
||||
safety: 'pass',
|
||||
reasoning: 'Good result',
|
||||
}
|
||||
|
||||
expect(testResult.test_id).toBe('INT-001')
|
||||
expect(testResult.passed).toBe(true)
|
||||
expect(testResult.composite_score).toBeGreaterThanOrEqual(0)
|
||||
expect(testResult.composite_score).toBeLessThanOrEqual(5)
|
||||
})
|
||||
|
||||
it('should have valid BQASMetrics structure', () => {
|
||||
const metrics: BQASMetrics = {
|
||||
total_tests: 100,
|
||||
passed_tests: 90,
|
||||
failed_tests: 10,
|
||||
avg_intent_accuracy: 85.5,
|
||||
avg_faithfulness: 4.2,
|
||||
avg_relevance: 4.0,
|
||||
avg_coherence: 4.1,
|
||||
safety_pass_rate: 0.95,
|
||||
avg_composite_score: 4.0,
|
||||
scores_by_intent: {
|
||||
student_observation: 4.5,
|
||||
worksheet_generate: 3.8,
|
||||
},
|
||||
failed_test_ids: ['INT-001', 'INT-002'],
|
||||
}
|
||||
|
||||
expect(metrics.total_tests).toBe(metrics.passed_tests + metrics.failed_tests)
|
||||
expect(metrics.safety_pass_rate).toBeGreaterThanOrEqual(0)
|
||||
expect(metrics.safety_pass_rate).toBeLessThanOrEqual(1)
|
||||
})
|
||||
|
||||
it('should have valid TestRun structure', () => {
|
||||
const testRun: TestRun = {
|
||||
id: 1,
|
||||
timestamp: '2026-01-26T10:00:00Z',
|
||||
git_commit: 'abc1234',
|
||||
golden_score: 4.2,
|
||||
synthetic_score: 3.9,
|
||||
total_tests: 100,
|
||||
passed_tests: 90,
|
||||
failed_tests: 10,
|
||||
duration_seconds: 120.5,
|
||||
}
|
||||
|
||||
expect(testRun.id).toBeGreaterThan(0)
|
||||
expect(testRun.golden_score).toBeGreaterThanOrEqual(0)
|
||||
expect(testRun.golden_score).toBeLessThanOrEqual(5)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Metric Calculations', () => {
|
||||
it('should calculate pass rate correctly', () => {
|
||||
const metrics = {
|
||||
total_tests: 100,
|
||||
passed_tests: 85,
|
||||
failed_tests: 15,
|
||||
}
|
||||
|
||||
const passRate = (metrics.passed_tests / metrics.total_tests) * 100
|
||||
expect(passRate).toBe(85)
|
||||
})
|
||||
|
||||
it('should handle zero tests gracefully', () => {
|
||||
const metrics = {
|
||||
total_tests: 0,
|
||||
passed_tests: 0,
|
||||
failed_tests: 0,
|
||||
}
|
||||
|
||||
const passRate = metrics.total_tests > 0
|
||||
? (metrics.passed_tests / metrics.total_tests) * 100
|
||||
: 0
|
||||
|
||||
expect(passRate).toBe(0)
|
||||
})
|
||||
|
||||
it('should classify trend correctly', () => {
|
||||
const classifyTrend = (scores: number[]): 'improving' | 'stable' | 'declining' | 'insufficient_data' => {
|
||||
if (scores.length < 3) return 'insufficient_data'
|
||||
|
||||
const recentAvg = scores.slice(-3).reduce((a, b) => a + b, 0) / 3
|
||||
const oldAvg = scores.slice(0, 3).reduce((a, b) => a + b, 0) / 3
|
||||
|
||||
const diff = recentAvg - oldAvg
|
||||
|
||||
if (diff > 0.1) return 'improving'
|
||||
if (diff < -0.1) return 'declining'
|
||||
return 'stable'
|
||||
}
|
||||
|
||||
// Improving trend
|
||||
expect(classifyTrend([3.0, 3.1, 3.2, 3.5, 3.8, 4.0])).toBe('improving')
|
||||
|
||||
// Declining trend
|
||||
expect(classifyTrend([4.0, 3.8, 3.5, 3.2, 3.1, 3.0])).toBe('declining')
|
||||
|
||||
// Stable trend
|
||||
expect(classifyTrend([4.0, 4.0, 4.0, 4.0, 4.0, 4.0])).toBe('stable')
|
||||
|
||||
// Insufficient data
|
||||
expect(classifyTrend([4.0, 4.0])).toBe('insufficient_data')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Score Thresholds', () => {
|
||||
it('should identify passing scores correctly', () => {
|
||||
const minScore = 3.5
|
||||
|
||||
const passingScores = [3.5, 4.0, 4.5, 5.0]
|
||||
const failingScores = [0.0, 1.0, 2.0, 3.0, 3.4]
|
||||
|
||||
passingScores.forEach(score => {
|
||||
expect(score >= minScore).toBe(true)
|
||||
})
|
||||
|
||||
failingScores.forEach(score => {
|
||||
expect(score >= minScore).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
it('should color-code scores correctly', () => {
|
||||
const getScoreColor = (score: number): string => {
|
||||
if (score >= 4) return 'emerald'
|
||||
if (score >= 3) return 'amber'
|
||||
return 'red'
|
||||
}
|
||||
|
||||
expect(getScoreColor(4.5)).toBe('emerald')
|
||||
expect(getScoreColor(4.0)).toBe('emerald')
|
||||
expect(getScoreColor(3.5)).toBe('amber')
|
||||
expect(getScoreColor(3.0)).toBe('amber')
|
||||
expect(getScoreColor(2.5)).toBe('red')
|
||||
expect(getScoreColor(0.0)).toBe('red')
|
||||
})
|
||||
})
|
||||
|
||||
describe('API URL Configuration', () => {
|
||||
it('should use correct default voice service URL', () => {
|
||||
const defaultUrl = 'http://localhost:8091'
|
||||
expect(defaultUrl).toContain('8091')
|
||||
})
|
||||
|
||||
it('should construct correct API endpoints', () => {
|
||||
const baseUrl = 'http://localhost:8091'
|
||||
|
||||
const endpoints = {
|
||||
runs: `${baseUrl}/api/v1/bqas/runs`,
|
||||
trend: `${baseUrl}/api/v1/bqas/trend`,
|
||||
latestMetrics: `${baseUrl}/api/v1/bqas/latest-metrics`,
|
||||
runGolden: `${baseUrl}/api/v1/bqas/run/golden`,
|
||||
runSynthetic: `${baseUrl}/api/v1/bqas/run/synthetic`,
|
||||
runRag: `${baseUrl}/api/v1/bqas/run/rag`,
|
||||
}
|
||||
|
||||
expect(endpoints.runs).toBe('http://localhost:8091/api/v1/bqas/runs')
|
||||
expect(endpoints.trend).toBe('http://localhost:8091/api/v1/bqas/trend')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Component Rendering Logic', () => {
|
||||
it('should format timestamps correctly', () => {
|
||||
const timestamp = '2026-01-26T10:30:00Z'
|
||||
const date = new Date(timestamp)
|
||||
|
||||
// German locale formatting
|
||||
const formatted = date.toLocaleString('de-DE')
|
||||
expect(formatted).toBeTruthy()
|
||||
})
|
||||
|
||||
it('should truncate git commits correctly', () => {
|
||||
const fullCommit = 'abc1234567890def'
|
||||
const shortCommit = fullCommit.slice(0, 7)
|
||||
|
||||
expect(shortCommit).toBe('abc1234')
|
||||
expect(shortCommit.length).toBe(7)
|
||||
})
|
||||
|
||||
it('should generate correct progress bar widths', () => {
|
||||
const getProgressWidth = (score: number, maxScore: number = 5): string => {
|
||||
return `${(score / maxScore) * 100}%`
|
||||
}
|
||||
|
||||
expect(getProgressWidth(4.0)).toBe('80%')
|
||||
expect(getProgressWidth(5.0)).toBe('100%')
|
||||
expect(getProgressWidth(2.5)).toBe('50%')
|
||||
expect(getProgressWidth(0)).toBe('0%')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Tab Navigation', () => {
|
||||
const tabs = ['overview', 'golden', 'rag', 'synthetic', 'history'] as const
|
||||
type TabId = typeof tabs[number]
|
||||
|
||||
it('should have all required tabs', () => {
|
||||
expect(tabs).toContain('overview')
|
||||
expect(tabs).toContain('golden')
|
||||
expect(tabs).toContain('rag')
|
||||
expect(tabs).toContain('synthetic')
|
||||
expect(tabs).toContain('history')
|
||||
})
|
||||
|
||||
it('should allow valid tab transitions', () => {
|
||||
const isValidTab = (tab: string): tab is TabId => {
|
||||
return tabs.includes(tab as TabId)
|
||||
}
|
||||
|
||||
expect(isValidTab('overview')).toBe(true)
|
||||
expect(isValidTab('golden')).toBe(true)
|
||||
expect(isValidTab('invalid')).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Error Handling', () => {
|
||||
it('should handle API errors gracefully', async () => {
|
||||
const mockFetch = jest.fn().mockRejectedValue(new Error('Network error'))
|
||||
|
||||
try {
|
||||
await mockFetch('http://localhost:8091/api/v1/bqas/runs')
|
||||
fail('Should have thrown an error')
|
||||
} catch (error) {
|
||||
expect(error).toBeInstanceOf(Error)
|
||||
expect((error as Error).message).toBe('Network error')
|
||||
}
|
||||
})
|
||||
|
||||
it('should provide fallback values for missing data', () => {
|
||||
const metrics = null
|
||||
|
||||
const displayScore = metrics?.avg_composite_score?.toFixed(2) || '-'
|
||||
const displayTotal = metrics?.total_tests || 0
|
||||
|
||||
expect(displayScore).toBe('-')
|
||||
expect(displayTotal).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('RAG Test Categories', () => {
|
||||
const categories = [
|
||||
{ id: 'eh_retrieval', name: 'EH Retrieval Quality' },
|
||||
{ id: 'operator_alignment', name: 'Operator Alignment' },
|
||||
{ id: 'hallucination_control', name: 'Hallucination Control' },
|
||||
{ id: 'citation_enforcement', name: 'Citation Enforcement' },
|
||||
{ id: 'privacy_compliance', name: 'Privacy/DSGVO Compliance' },
|
||||
{ id: 'namespace_isolation', name: 'Namespace Isolation' },
|
||||
]
|
||||
|
||||
it('should have all required RAG categories', () => {
|
||||
expect(categories).toHaveLength(6)
|
||||
|
||||
const categoryIds = categories.map(c => c.id)
|
||||
expect(categoryIds).toContain('eh_retrieval')
|
||||
expect(categoryIds).toContain('operator_alignment')
|
||||
expect(categoryIds).toContain('privacy_compliance')
|
||||
})
|
||||
|
||||
it('should have human-readable names for all categories', () => {
|
||||
categories.forEach(category => {
|
||||
expect(category.name).toBeTruthy()
|
||||
expect(category.name.length).toBeGreaterThan(0)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
317
website/tests/structure.test.ts
Normal file
317
website/tests/structure.test.ts
Normal file
@@ -0,0 +1,317 @@
|
||||
/**
|
||||
* Website Structure Tests
|
||||
*
|
||||
* Testet, dass alle wichtigen Seiten und Komponenten existieren.
|
||||
* Diese Tests sind statische Struktur-Tests, keine Runtime-Tests.
|
||||
*/
|
||||
|
||||
import { existsSync, readFileSync } from 'fs'
|
||||
import { join } from 'path'
|
||||
|
||||
const ROOT_DIR = join(__dirname, '..')
|
||||
const APP_DIR = join(ROOT_DIR, 'app')
|
||||
const COMPONENTS_DIR = join(ROOT_DIR, 'components')
|
||||
const LIB_DIR = join(ROOT_DIR, 'lib')
|
||||
|
||||
describe('Website Structure', () => {
|
||||
describe('App Pages', () => {
|
||||
it('has landing page (page.tsx)', () => {
|
||||
expect(existsSync(join(APP_DIR, 'page.tsx'))).toBe(true)
|
||||
})
|
||||
|
||||
it('has layout (layout.tsx)', () => {
|
||||
expect(existsSync(join(APP_DIR, 'layout.tsx'))).toBe(true)
|
||||
})
|
||||
|
||||
it('has FAQ page', () => {
|
||||
expect(existsSync(join(APP_DIR, 'faq', 'page.tsx'))).toBe(true)
|
||||
})
|
||||
|
||||
it('has success page', () => {
|
||||
expect(existsSync(join(APP_DIR, 'success', 'page.tsx'))).toBe(true)
|
||||
})
|
||||
|
||||
it('has cancel page', () => {
|
||||
expect(existsSync(join(APP_DIR, 'cancel', 'page.tsx'))).toBe(true)
|
||||
})
|
||||
|
||||
it('has admin page', () => {
|
||||
expect(existsSync(join(APP_DIR, 'admin', 'page.tsx'))).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Components', () => {
|
||||
it('has Header component', () => {
|
||||
expect(existsSync(join(COMPONENTS_DIR, 'Header.tsx'))).toBe(true)
|
||||
})
|
||||
|
||||
it('has Footer component', () => {
|
||||
expect(existsSync(join(COMPONENTS_DIR, 'Footer.tsx'))).toBe(true)
|
||||
})
|
||||
|
||||
it('has PricingSection component', () => {
|
||||
expect(existsSync(join(COMPONENTS_DIR, 'PricingSection.tsx'))).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Configuration', () => {
|
||||
it('has package.json', () => {
|
||||
expect(existsSync(join(ROOT_DIR, 'package.json'))).toBe(true)
|
||||
})
|
||||
|
||||
it('has Dockerfile', () => {
|
||||
expect(existsSync(join(ROOT_DIR, 'Dockerfile'))).toBe(true)
|
||||
})
|
||||
|
||||
it('has tailwind.config.ts', () => {
|
||||
expect(existsSync(join(ROOT_DIR, 'tailwind.config.ts'))).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Lib', () => {
|
||||
it('has content.ts', () => {
|
||||
expect(existsSync(join(LIB_DIR, 'content.ts'))).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('API Routes', () => {
|
||||
it('has content API route', () => {
|
||||
expect(existsSync(join(APP_DIR, 'api', 'content', 'route.ts'))).toBe(true)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('FAQ Page Content', () => {
|
||||
const faqPagePath = join(APP_DIR, 'faq', 'page.tsx')
|
||||
|
||||
it('contains FAQ items', () => {
|
||||
const content = readFileSync(faqPagePath, 'utf-8')
|
||||
expect(content).toContain('faqItems')
|
||||
})
|
||||
|
||||
it('has question about "Aufgabe"', () => {
|
||||
const content = readFileSync(faqPagePath, 'utf-8')
|
||||
expect(content).toContain('Was ist bei Breakpilot eine')
|
||||
})
|
||||
|
||||
it('has question about trial', () => {
|
||||
const content = readFileSync(faqPagePath, 'utf-8')
|
||||
expect(content).toContain('kostenlos testen')
|
||||
})
|
||||
|
||||
it('has question about DSGVO/data privacy', () => {
|
||||
const content = readFileSync(faqPagePath, 'utf-8')
|
||||
expect(content).toContain('DSGVO')
|
||||
})
|
||||
|
||||
it('has question about cancellation', () => {
|
||||
const content = readFileSync(faqPagePath, 'utf-8')
|
||||
expect(content).toContain('kuendigen')
|
||||
})
|
||||
|
||||
it('has at least 10 FAQ items', () => {
|
||||
const content = readFileSync(faqPagePath, 'utf-8')
|
||||
// Zaehle die Fragen (question: Eintraege)
|
||||
const matches = content.match(/question:/g)
|
||||
expect(matches).not.toBeNull()
|
||||
expect(matches!.length).toBeGreaterThanOrEqual(10)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Header Navigation', () => {
|
||||
const headerPath = join(COMPONENTS_DIR, 'Header.tsx')
|
||||
|
||||
it('has link to FAQ', () => {
|
||||
const content = readFileSync(headerPath, 'utf-8')
|
||||
expect(content).toContain('/faq')
|
||||
})
|
||||
|
||||
it('has link to pricing', () => {
|
||||
const content = readFileSync(headerPath, 'utf-8')
|
||||
expect(content).toContain('#pricing')
|
||||
})
|
||||
|
||||
it('has link to features', () => {
|
||||
const content = readFileSync(headerPath, 'utf-8')
|
||||
expect(content).toContain('#features')
|
||||
})
|
||||
|
||||
it('has mobile menu', () => {
|
||||
const content = readFileSync(headerPath, 'utf-8')
|
||||
expect(content).toContain('mobileMenuOpen')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Landing Page Content', () => {
|
||||
const landingPath = join(APP_DIR, 'page.tsx')
|
||||
|
||||
it('uses LandingContent component', () => {
|
||||
const content = readFileSync(landingPath, 'utf-8')
|
||||
expect(content).toContain('LandingContent')
|
||||
})
|
||||
|
||||
it('imports Header component', () => {
|
||||
const content = readFileSync(landingPath, 'utf-8')
|
||||
expect(content).toContain('Header')
|
||||
})
|
||||
|
||||
it('imports Footer component', () => {
|
||||
const content = readFileSync(landingPath, 'utf-8')
|
||||
expect(content).toContain('Footer')
|
||||
})
|
||||
|
||||
it('loads content from getContent', () => {
|
||||
const content = readFileSync(landingPath, 'utf-8')
|
||||
expect(content).toContain('getContent')
|
||||
})
|
||||
|
||||
it('passes content to LandingContent', () => {
|
||||
const content = readFileSync(landingPath, 'utf-8')
|
||||
expect(content).toContain('content={content}')
|
||||
})
|
||||
})
|
||||
|
||||
describe('PricingSection Content', () => {
|
||||
const pricingPath = join(COMPONENTS_DIR, 'PricingSection.tsx')
|
||||
|
||||
it('has three plans', () => {
|
||||
const content = readFileSync(pricingPath, 'utf-8')
|
||||
expect(content).toContain('basic')
|
||||
expect(content).toContain('standard')
|
||||
expect(content).toContain('premium')
|
||||
})
|
||||
|
||||
it('has trial button', () => {
|
||||
const content = readFileSync(pricingPath, 'utf-8')
|
||||
expect(content).toContain('7 Tage kostenlos')
|
||||
})
|
||||
|
||||
it('has email form modal', () => {
|
||||
const content = readFileSync(pricingPath, 'utf-8')
|
||||
expect(content).toContain('showEmailForm')
|
||||
})
|
||||
|
||||
it('calls billing service API', () => {
|
||||
const content = readFileSync(pricingPath, 'utf-8')
|
||||
expect(content).toContain('BILLING_API_URL')
|
||||
expect(content).toContain('trial/start')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Admin Page', () => {
|
||||
const adminPath = join(APP_DIR, 'admin', 'page.tsx')
|
||||
|
||||
it('is a client component', () => {
|
||||
const content = readFileSync(adminPath, 'utf-8')
|
||||
expect(content).toContain("'use client'")
|
||||
})
|
||||
|
||||
it('uses AdminLayout', () => {
|
||||
const content = readFileSync(adminPath, 'utf-8')
|
||||
expect(content).toContain('AdminLayout')
|
||||
})
|
||||
|
||||
it('has quick actions section', () => {
|
||||
const content = readFileSync(adminPath, 'utf-8')
|
||||
expect(content).toContain('quickActions')
|
||||
})
|
||||
|
||||
it('has stats display', () => {
|
||||
const content = readFileSync(adminPath, 'utf-8')
|
||||
expect(content).toContain('Stats')
|
||||
})
|
||||
|
||||
it('loads stats from API', () => {
|
||||
const content = readFileSync(adminPath, 'utf-8')
|
||||
expect(content).toContain('loadStats')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Content API Route', () => {
|
||||
const apiPath = join(APP_DIR, 'api', 'content', 'route.ts')
|
||||
|
||||
it('has GET handler', () => {
|
||||
const content = readFileSync(apiPath, 'utf-8')
|
||||
expect(content).toContain('export async function GET')
|
||||
})
|
||||
|
||||
it('has POST handler', () => {
|
||||
const content = readFileSync(apiPath, 'utf-8')
|
||||
expect(content).toContain('export async function POST')
|
||||
})
|
||||
|
||||
it('validates admin key', () => {
|
||||
const content = readFileSync(apiPath, 'utf-8')
|
||||
expect(content).toContain('x-admin-key')
|
||||
expect(content).toContain('Unauthorized')
|
||||
})
|
||||
|
||||
it('uses content lib', () => {
|
||||
const content = readFileSync(apiPath, 'utf-8')
|
||||
expect(content).toContain('getContent')
|
||||
expect(content).toContain('saveContent')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Content Library', () => {
|
||||
const contentPath = join(LIB_DIR, 'content.ts')
|
||||
|
||||
it('exports getContent function', () => {
|
||||
const content = readFileSync(contentPath, 'utf-8')
|
||||
expect(content).toContain('export function getContent')
|
||||
})
|
||||
|
||||
it('exports saveContent function', () => {
|
||||
const content = readFileSync(contentPath, 'utf-8')
|
||||
expect(content).toContain('export function saveContent')
|
||||
})
|
||||
|
||||
it('exports WebsiteContent type', () => {
|
||||
const content = readFileSync(contentPath, 'utf-8')
|
||||
expect(content).toContain('WebsiteContent')
|
||||
})
|
||||
|
||||
it('exports HeroContent type', () => {
|
||||
const content = readFileSync(contentPath, 'utf-8')
|
||||
expect(content).toContain('HeroContent')
|
||||
})
|
||||
|
||||
it('has default content', () => {
|
||||
const content = readFileSync(contentPath, 'utf-8')
|
||||
expect(content).toContain('defaultContent')
|
||||
})
|
||||
|
||||
it('stores content as JSON', () => {
|
||||
const content = readFileSync(contentPath, 'utf-8')
|
||||
expect(content).toContain('website.json')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Landing Page Uses Dynamic Content', () => {
|
||||
const landingPath = join(APP_DIR, 'page.tsx')
|
||||
const landingContentPath = join(COMPONENTS_DIR, 'LandingContent.tsx')
|
||||
|
||||
it('imports getContent', () => {
|
||||
const content = readFileSync(landingPath, 'utf-8')
|
||||
expect(content).toContain('import { getContent }')
|
||||
})
|
||||
|
||||
it('calls getContent', () => {
|
||||
const content = readFileSync(landingPath, 'utf-8')
|
||||
expect(content).toContain('getContent()')
|
||||
})
|
||||
|
||||
it('LandingContent component exists', () => {
|
||||
expect(existsSync(landingContentPath)).toBe(true)
|
||||
})
|
||||
|
||||
it('LandingContent has hero section', () => {
|
||||
const content = readFileSync(landingContentPath, 'utf-8')
|
||||
expect(content).toContain('Hero')
|
||||
})
|
||||
|
||||
it('LandingContent has pricing section', () => {
|
||||
const content = readFileSync(landingContentPath, 'utf-8')
|
||||
expect(content).toContain('PricingSection')
|
||||
})
|
||||
})
|
||||
932
website/tests/unity-bridge.test.ts
Normal file
932
website/tests/unity-bridge.test.ts
Normal file
@@ -0,0 +1,932 @@
|
||||
/**
|
||||
* Unity Bridge Tests
|
||||
*
|
||||
* Testet die Unity AI Bridge Integration:
|
||||
* - Struktur-Tests (Dateien existieren)
|
||||
* - API Route Tests (Handler-Verhalten)
|
||||
* - Content-Tests (korrekter Inhalt)
|
||||
*/
|
||||
|
||||
import { existsSync, readFileSync } from 'fs'
|
||||
import { join } from 'path'
|
||||
|
||||
const ROOT_DIR = join(__dirname, '..')
|
||||
const APP_DIR = join(ROOT_DIR, 'app')
|
||||
const COMPONENTS_DIR = join(ROOT_DIR, 'components')
|
||||
const LIB_DIR = join(ROOT_DIR, 'lib')
|
||||
|
||||
// ==============================================
|
||||
// Structure Tests
|
||||
// ==============================================
|
||||
|
||||
describe('Unity Bridge Structure', () => {
|
||||
describe('Pages', () => {
|
||||
it('has dashboard page', () => {
|
||||
expect(existsSync(join(APP_DIR, 'admin', 'unity-bridge', 'page.tsx'))).toBe(true)
|
||||
})
|
||||
|
||||
it('has wizard page', () => {
|
||||
expect(existsSync(join(APP_DIR, 'admin', 'unity-bridge', 'wizard', 'page.tsx'))).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('API Route', () => {
|
||||
it('has unity-bridge API route', () => {
|
||||
expect(existsSync(join(APP_DIR, 'api', 'admin', 'unity-bridge', 'route.ts'))).toBe(true)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// ==============================================
|
||||
// API Route Content Tests
|
||||
// ==============================================
|
||||
|
||||
describe('Unity Bridge API Route', () => {
|
||||
const apiPath = join(APP_DIR, 'api', 'admin', 'unity-bridge', 'route.ts')
|
||||
let content: string
|
||||
|
||||
beforeAll(() => {
|
||||
content = readFileSync(apiPath, 'utf-8')
|
||||
})
|
||||
|
||||
describe('GET Handler', () => {
|
||||
it('has GET export', () => {
|
||||
expect(content).toContain('export async function GET')
|
||||
})
|
||||
|
||||
it('supports status action', () => {
|
||||
expect(content).toContain("status: '/status'")
|
||||
})
|
||||
|
||||
it('supports compile action', () => {
|
||||
expect(content).toContain("compile: '/compile'")
|
||||
})
|
||||
|
||||
it('supports logs action', () => {
|
||||
expect(content).toContain("logs:")
|
||||
expect(content).toContain('/logs')
|
||||
})
|
||||
|
||||
it('supports errors action', () => {
|
||||
expect(content).toContain("errors: '/logs/errors'")
|
||||
})
|
||||
|
||||
it('supports warnings action', () => {
|
||||
expect(content).toContain("warnings: '/logs/warnings'")
|
||||
})
|
||||
|
||||
it('supports scene action', () => {
|
||||
expect(content).toContain("scene: '/scene'")
|
||||
})
|
||||
|
||||
it('supports play action', () => {
|
||||
expect(content).toContain("play: '/play'")
|
||||
})
|
||||
|
||||
it('supports stop action', () => {
|
||||
expect(content).toContain("stop: '/stop'")
|
||||
})
|
||||
|
||||
it('supports quicksetup action', () => {
|
||||
expect(content).toContain("quicksetup: '/quicksetup'")
|
||||
})
|
||||
|
||||
it('supports object endpoint with name parameter', () => {
|
||||
expect(content).toContain('object')
|
||||
expect(content).toContain('objectName')
|
||||
})
|
||||
|
||||
it('handles connection errors gracefully', () => {
|
||||
expect(content).toContain('offline')
|
||||
expect(content).toContain('503')
|
||||
})
|
||||
|
||||
it('uses correct Unity Bridge URL', () => {
|
||||
expect(content).toContain('UNITY_BRIDGE_URL')
|
||||
expect(content).toContain('http://localhost:8090')
|
||||
})
|
||||
|
||||
it('has timeout for requests', () => {
|
||||
expect(content).toContain('AbortSignal.timeout')
|
||||
expect(content).toContain('3000')
|
||||
})
|
||||
})
|
||||
|
||||
describe('POST Handler', () => {
|
||||
it('has POST export', () => {
|
||||
expect(content).toContain('export async function POST')
|
||||
})
|
||||
|
||||
it('supports diagnose action', () => {
|
||||
expect(content).toContain("action === 'diagnose'")
|
||||
expect(content).toContain('/diagnose')
|
||||
})
|
||||
|
||||
it('supports execute action', () => {
|
||||
expect(content).toContain("action === 'execute'")
|
||||
expect(content).toContain('/execute')
|
||||
})
|
||||
|
||||
it('supports clear-logs action', () => {
|
||||
expect(content).toContain("action === 'clear-logs'")
|
||||
expect(content).toContain('/logs/clear')
|
||||
})
|
||||
|
||||
it('validates POST body for execute', () => {
|
||||
expect(content).toContain('Invalid JSON body')
|
||||
expect(content).toContain('Missing "action" field')
|
||||
})
|
||||
|
||||
it('has longer timeout for diagnose', () => {
|
||||
expect(content).toContain('10000')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Error Handling', () => {
|
||||
it('returns 400 for unknown actions', () => {
|
||||
expect(content).toContain('400')
|
||||
expect(content).toContain('Unknown action')
|
||||
})
|
||||
|
||||
it('returns 503 when bridge offline', () => {
|
||||
expect(content).toContain('503')
|
||||
expect(content).toContain('offline: true')
|
||||
})
|
||||
|
||||
it('distinguishes timeout from connection refused', () => {
|
||||
expect(content).toContain('timeout')
|
||||
expect(content).toContain('aborted')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Type Definitions', () => {
|
||||
it('has BridgeStatus interface', () => {
|
||||
expect(content).toContain('interface BridgeStatus')
|
||||
})
|
||||
|
||||
it('has LogEntry interface', () => {
|
||||
expect(content).toContain('interface LogEntry')
|
||||
})
|
||||
|
||||
it('has LogsResponse interface', () => {
|
||||
expect(content).toContain('interface LogsResponse')
|
||||
})
|
||||
|
||||
it('has DiagnoseResponse interface', () => {
|
||||
expect(content).toContain('interface DiagnoseResponse')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// ==============================================
|
||||
// Dashboard Page Tests
|
||||
// ==============================================
|
||||
|
||||
describe('Unity Bridge Dashboard Page', () => {
|
||||
const dashboardPath = join(APP_DIR, 'admin', 'unity-bridge', 'page.tsx')
|
||||
let content: string
|
||||
|
||||
beforeAll(() => {
|
||||
content = readFileSync(dashboardPath, 'utf-8')
|
||||
})
|
||||
|
||||
it('is a client component', () => {
|
||||
expect(content).toContain("'use client'")
|
||||
})
|
||||
|
||||
it('uses AdminLayout', () => {
|
||||
expect(content).toContain('AdminLayout')
|
||||
expect(content).toContain("title=\"Unity AI Bridge\"")
|
||||
})
|
||||
|
||||
it('has status polling', () => {
|
||||
expect(content).toContain('setInterval')
|
||||
expect(content).toContain('5000') // 5 second interval
|
||||
})
|
||||
|
||||
it('has log polling', () => {
|
||||
expect(content).toContain('10000') // 10 second interval
|
||||
})
|
||||
|
||||
it('has status badge component', () => {
|
||||
expect(content).toContain('StatusBadge')
|
||||
})
|
||||
|
||||
it('has stat cards', () => {
|
||||
expect(content).toContain('StatCard')
|
||||
})
|
||||
|
||||
it('has console log panel', () => {
|
||||
expect(content).toContain('ConsoleLogPanel')
|
||||
})
|
||||
|
||||
it('has diagnostic panel', () => {
|
||||
expect(content).toContain('DiagnosticPanel')
|
||||
})
|
||||
|
||||
it('has quick action buttons', () => {
|
||||
expect(content).toContain('Play')
|
||||
expect(content).toContain('Stop')
|
||||
expect(content).toContain('Quick Setup')
|
||||
expect(content).toContain('Diagnose')
|
||||
})
|
||||
|
||||
it('links to wizard', () => {
|
||||
expect(content).toContain('/admin/unity-bridge/wizard')
|
||||
expect(content).toContain('Wizard')
|
||||
})
|
||||
|
||||
it('handles offline state', () => {
|
||||
expect(content).toContain('offline')
|
||||
expect(content).toContain('Offline')
|
||||
})
|
||||
|
||||
it('shows error and warning counts', () => {
|
||||
expect(content).toContain('errors')
|
||||
expect(content).toContain('warnings')
|
||||
})
|
||||
|
||||
it('shows scene information', () => {
|
||||
expect(content).toContain('scene')
|
||||
})
|
||||
|
||||
it('shows play mode status', () => {
|
||||
expect(content).toContain('is_playing')
|
||||
})
|
||||
})
|
||||
|
||||
// ==============================================
|
||||
// Wizard Page Tests
|
||||
// ==============================================
|
||||
|
||||
describe('Unity Bridge Wizard Page', () => {
|
||||
const wizardPath = join(APP_DIR, 'admin', 'unity-bridge', 'wizard', 'page.tsx')
|
||||
let content: string
|
||||
|
||||
beforeAll(() => {
|
||||
content = readFileSync(wizardPath, 'utf-8')
|
||||
})
|
||||
|
||||
it('is a client component', () => {
|
||||
expect(content).toContain("'use client'")
|
||||
})
|
||||
|
||||
it('uses AdminLayout', () => {
|
||||
expect(content).toContain('AdminLayout')
|
||||
expect(content).toContain("title=\"Unity AI Bridge Wizard\"")
|
||||
})
|
||||
|
||||
it('uses wizard components', () => {
|
||||
expect(content).toContain('WizardStepper')
|
||||
expect(content).toContain('WizardNavigation')
|
||||
expect(content).toContain('EducationCard')
|
||||
})
|
||||
|
||||
it('has 7 steps defined', () => {
|
||||
const stepMatches = content.match(/\{ id: '/g)
|
||||
expect(stepMatches).not.toBeNull()
|
||||
expect(stepMatches!.length).toBeGreaterThanOrEqual(7)
|
||||
})
|
||||
|
||||
it('has welcome step', () => {
|
||||
expect(content).toContain("id: 'welcome'")
|
||||
})
|
||||
|
||||
it('has what-is-bridge step', () => {
|
||||
expect(content).toContain("id: 'what-is-bridge'")
|
||||
})
|
||||
|
||||
it('has start-server step', () => {
|
||||
expect(content).toContain("id: 'start-server'")
|
||||
})
|
||||
|
||||
it('has api-endpoints step', () => {
|
||||
expect(content).toContain("id: 'api-endpoints'")
|
||||
})
|
||||
|
||||
it('has live-demo step', () => {
|
||||
expect(content).toContain("id: 'live-demo'")
|
||||
})
|
||||
|
||||
it('has claude-usage step', () => {
|
||||
expect(content).toContain("id: 'claude-usage'")
|
||||
})
|
||||
|
||||
it('has troubleshooting step', () => {
|
||||
expect(content).toContain("id: 'troubleshooting'")
|
||||
})
|
||||
|
||||
it('has LiveDemoPanel component', () => {
|
||||
expect(content).toContain('LiveDemoPanel')
|
||||
})
|
||||
|
||||
it('has navigation back link', () => {
|
||||
expect(content).toContain('/admin/unity-bridge')
|
||||
expect(content).toContain('Zurueck')
|
||||
})
|
||||
|
||||
it('has completion message', () => {
|
||||
expect(content).toContain('Wizard abgeschlossen')
|
||||
})
|
||||
|
||||
describe('Education Content', () => {
|
||||
it('explains what the bridge is', () => {
|
||||
expect(content).toContain('Unity AI Bridge')
|
||||
expect(content).toContain('REST API')
|
||||
expect(content).toContain('Port 8090')
|
||||
})
|
||||
|
||||
it('explains how to start the server', () => {
|
||||
expect(content).toContain('Server starten')
|
||||
expect(content).toContain('BreakpilotDrive')
|
||||
expect(content).toContain('AI Bridge')
|
||||
})
|
||||
|
||||
it('documents API endpoints', () => {
|
||||
expect(content).toContain('/status')
|
||||
expect(content).toContain('/logs')
|
||||
expect(content).toContain('/scene')
|
||||
expect(content).toContain('/play')
|
||||
expect(content).toContain('/diagnose')
|
||||
})
|
||||
|
||||
it('explains Claude integration', () => {
|
||||
expect(content).toContain('Claude')
|
||||
expect(content).toContain('UNITY_BRIDGE.md')
|
||||
})
|
||||
|
||||
it('has troubleshooting tips', () => {
|
||||
expect(content).toContain('Connection refused')
|
||||
expect(content).toContain('Port 8090')
|
||||
expect(content).toContain('lsof')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// ==============================================
|
||||
// Architecture Data Tests
|
||||
// ==============================================
|
||||
|
||||
describe('Unity Bridge Architecture Data', () => {
|
||||
const architecturePath = join(LIB_DIR, 'architecture-data.ts')
|
||||
let content: string
|
||||
|
||||
beforeAll(() => {
|
||||
content = readFileSync(architecturePath, 'utf-8')
|
||||
})
|
||||
|
||||
it('has unity-bridge service definition', () => {
|
||||
expect(content).toContain("'unity-bridge':")
|
||||
expect(content).toContain("name: 'Unity AI Bridge'")
|
||||
})
|
||||
|
||||
it('defines correct port', () => {
|
||||
expect(content).toContain('port: 8090')
|
||||
})
|
||||
|
||||
it('has module architecture definition', () => {
|
||||
expect(content).toContain("displayName: 'Unity AI Bridge'")
|
||||
})
|
||||
|
||||
it('has correct data flow', () => {
|
||||
expect(content).toContain('Admin Panel')
|
||||
expect(content).toContain('API Proxy')
|
||||
expect(content).toContain('Unity Bridge')
|
||||
expect(content).toContain('Unity Editor')
|
||||
})
|
||||
|
||||
it('has wizard configuration', () => {
|
||||
expect(content).toContain("module: 'unity-bridge'")
|
||||
expect(content).toContain('steps: 7')
|
||||
})
|
||||
})
|
||||
|
||||
// ==============================================
|
||||
// Navigation Tests
|
||||
// ==============================================
|
||||
|
||||
describe('Admin Layout Navigation', () => {
|
||||
const layoutPath = join(COMPONENTS_DIR, 'admin', 'AdminLayout.tsx')
|
||||
let content: string
|
||||
|
||||
beforeAll(() => {
|
||||
content = readFileSync(layoutPath, 'utf-8')
|
||||
})
|
||||
|
||||
it('has Unity Bridge navigation entry', () => {
|
||||
expect(content).toContain("name: 'Unity Bridge'")
|
||||
expect(content).toContain("href: '/admin/unity-bridge'")
|
||||
})
|
||||
|
||||
it('has Unity Bridge description', () => {
|
||||
expect(content).toContain('Unity Editor Steuerung')
|
||||
})
|
||||
|
||||
it('has Unity Bridge icon (lightning bolt)', () => {
|
||||
expect(content).toContain('M13 10V3L4 14h7v7l9-11h-7z')
|
||||
})
|
||||
})
|
||||
|
||||
// ==============================================
|
||||
// Streaming Tests
|
||||
// ==============================================
|
||||
|
||||
describe('Unity Bridge Streaming', () => {
|
||||
describe('API Route Streaming Endpoints', () => {
|
||||
const apiPath = join(APP_DIR, 'api', 'admin', 'unity-bridge', 'route.ts')
|
||||
let content: string
|
||||
|
||||
beforeAll(() => {
|
||||
content = readFileSync(apiPath, 'utf-8')
|
||||
})
|
||||
|
||||
it('has streaming type definitions', () => {
|
||||
expect(content).toContain('interface StreamStatusResponse')
|
||||
expect(content).toContain('interface StreamFrameResponse')
|
||||
})
|
||||
|
||||
it('supports stream-start action', () => {
|
||||
expect(content).toContain("'stream-start': '/stream/start'")
|
||||
})
|
||||
|
||||
it('supports stream-stop action', () => {
|
||||
expect(content).toContain("'stream-stop': '/stream/stop'")
|
||||
})
|
||||
|
||||
it('supports stream-frame action', () => {
|
||||
expect(content).toContain("'stream-frame': '/stream/frame'")
|
||||
})
|
||||
|
||||
it('supports stream-status action', () => {
|
||||
expect(content).toContain("'stream-status': '/stream/status'")
|
||||
})
|
||||
|
||||
it('handles screenshot endpoint specially', () => {
|
||||
expect(content).toContain("action === 'screenshot'")
|
||||
expect(content).toContain('/screenshot')
|
||||
})
|
||||
|
||||
it('returns screenshot as binary image', () => {
|
||||
expect(content).toContain("'Content-Type': 'image/jpeg'")
|
||||
expect(content).toContain('arrayBuffer')
|
||||
})
|
||||
|
||||
it('has no-cache headers for screenshot', () => {
|
||||
expect(content).toContain('no-cache')
|
||||
expect(content).toContain('must-revalidate')
|
||||
})
|
||||
|
||||
it('has longer timeout for screenshot', () => {
|
||||
// Screenshot should have 5 second timeout
|
||||
expect(content).toContain('5000')
|
||||
})
|
||||
})
|
||||
|
||||
describe('GameView Component', () => {
|
||||
const componentPath = join(COMPONENTS_DIR, 'admin', 'GameView.tsx')
|
||||
let content: string
|
||||
|
||||
beforeAll(() => {
|
||||
content = readFileSync(componentPath, 'utf-8')
|
||||
})
|
||||
|
||||
it('exists', () => {
|
||||
expect(existsSync(componentPath)).toBe(true)
|
||||
})
|
||||
|
||||
it('is a client component', () => {
|
||||
expect(content).toContain("'use client'")
|
||||
})
|
||||
|
||||
it('has streaming state management', () => {
|
||||
expect(content).toContain('isStreaming')
|
||||
expect(content).toContain('setIsStreaming')
|
||||
})
|
||||
|
||||
it('has frame data state', () => {
|
||||
expect(content).toContain('frameData')
|
||||
expect(content).toContain('setFrameData')
|
||||
})
|
||||
|
||||
it('has FPS tracking', () => {
|
||||
expect(content).toContain('fps')
|
||||
expect(content).toContain('setFps')
|
||||
})
|
||||
|
||||
it('has start streaming function', () => {
|
||||
expect(content).toContain('startStreaming')
|
||||
expect(content).toContain('stream-start')
|
||||
})
|
||||
|
||||
it('has stop streaming function', () => {
|
||||
expect(content).toContain('stopStreaming')
|
||||
expect(content).toContain('stream-stop')
|
||||
})
|
||||
|
||||
it('has screenshot capture function', () => {
|
||||
expect(content).toContain('captureScreenshot')
|
||||
expect(content).toContain('screenshot')
|
||||
})
|
||||
|
||||
it('fetches frames during streaming', () => {
|
||||
expect(content).toContain('fetchFrame')
|
||||
expect(content).toContain('stream-frame')
|
||||
})
|
||||
|
||||
it('has streaming interval of 100ms (10 FPS)', () => {
|
||||
expect(content).toContain('100')
|
||||
expect(content).toContain('10 FPS')
|
||||
})
|
||||
|
||||
it('calculates FPS every second', () => {
|
||||
expect(content).toContain('1000')
|
||||
})
|
||||
|
||||
it('displays LIVE indicator when streaming', () => {
|
||||
expect(content).toContain('LIVE')
|
||||
expect(content).toContain('animate-pulse')
|
||||
})
|
||||
|
||||
it('displays resolution info', () => {
|
||||
expect(content).toContain('Resolution')
|
||||
expect(content).toContain('1280x720')
|
||||
})
|
||||
|
||||
it('displays quality info', () => {
|
||||
expect(content).toContain('Quality')
|
||||
expect(content).toContain('75%')
|
||||
})
|
||||
|
||||
it('handles offline state', () => {
|
||||
expect(content).toContain('isUnityOnline')
|
||||
expect(content).toContain('Unity Bridge offline')
|
||||
})
|
||||
|
||||
it('has screenshot button', () => {
|
||||
expect(content).toContain('Screenshot')
|
||||
})
|
||||
|
||||
it('has stream toggle button', () => {
|
||||
expect(content).toContain('Stream')
|
||||
expect(content).toContain('Stop')
|
||||
})
|
||||
|
||||
it('shows loading overlay', () => {
|
||||
expect(content).toContain('isLoading')
|
||||
expect(content).toContain('animate-spin')
|
||||
})
|
||||
|
||||
it('uses aspect-video for game view', () => {
|
||||
expect(content).toContain('aspect-video')
|
||||
})
|
||||
|
||||
it('shows frame as image', () => {
|
||||
expect(content).toContain('<img')
|
||||
expect(content).toContain('Unity Game View')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Dashboard Integration', () => {
|
||||
const dashboardPath = join(APP_DIR, 'admin', 'unity-bridge', 'page.tsx')
|
||||
let content: string
|
||||
|
||||
beforeAll(() => {
|
||||
content = readFileSync(dashboardPath, 'utf-8')
|
||||
})
|
||||
|
||||
it('imports GameView component', () => {
|
||||
expect(content).toContain("import GameView from '@/components/admin/GameView'")
|
||||
})
|
||||
|
||||
it('renders GameView component', () => {
|
||||
expect(content).toContain('<GameView')
|
||||
})
|
||||
|
||||
it('passes isUnityOnline prop', () => {
|
||||
expect(content).toContain('isUnityOnline')
|
||||
})
|
||||
|
||||
it('passes isPlaying prop', () => {
|
||||
expect(content).toContain('isPlaying')
|
||||
})
|
||||
|
||||
it('lists streaming endpoints in API info', () => {
|
||||
expect(content).toContain('/screenshot')
|
||||
expect(content).toContain('/stream/start')
|
||||
expect(content).toContain('/stream/frame')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// ==============================================
|
||||
// Python Backend Proxy Tests
|
||||
// ==============================================
|
||||
|
||||
describe('Unity Bridge Python Backend Proxy', () => {
|
||||
const apiPath = join(APP_DIR, 'api', 'admin', 'unity-bridge', 'route.ts')
|
||||
let content: string
|
||||
|
||||
beforeAll(() => {
|
||||
content = readFileSync(apiPath, 'utf-8')
|
||||
})
|
||||
|
||||
describe('Backend Configuration', () => {
|
||||
it('has BACKEND_URL constant', () => {
|
||||
expect(content).toContain('BACKEND_URL')
|
||||
expect(content).toContain('http://localhost:8000')
|
||||
})
|
||||
|
||||
it('has BackendType type definition', () => {
|
||||
expect(content).toContain("type BackendType = 'unity' | 'python'")
|
||||
})
|
||||
|
||||
it('has EndpointConfig interface', () => {
|
||||
expect(content).toContain('interface EndpointConfig')
|
||||
})
|
||||
|
||||
it('has pythonEndpoints configuration', () => {
|
||||
expect(content).toContain('pythonEndpoints')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Unit API Endpoints', () => {
|
||||
it('supports units-list action', () => {
|
||||
expect(content).toContain("'units-list'")
|
||||
expect(content).toContain('/api/units/definitions')
|
||||
})
|
||||
|
||||
it('supports units-get action with unit_id', () => {
|
||||
expect(content).toContain("action === 'units-get'")
|
||||
expect(content).toContain('unitId')
|
||||
})
|
||||
|
||||
it('supports units-health action', () => {
|
||||
expect(content).toContain("'units-health'")
|
||||
expect(content).toContain('/api/units/health')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Analytics Endpoints', () => {
|
||||
it('supports analytics-overview action', () => {
|
||||
expect(content).toContain("'analytics-overview'")
|
||||
expect(content).toContain('/api/analytics/dashboard/overview')
|
||||
})
|
||||
|
||||
it('supports analytics-misconceptions action', () => {
|
||||
expect(content).toContain("'analytics-misconceptions'")
|
||||
expect(content).toContain('/api/analytics/misconceptions')
|
||||
})
|
||||
|
||||
it('supports analytics-learning-gain action with unit_id', () => {
|
||||
expect(content).toContain("action === 'analytics-learning-gain'")
|
||||
expect(content).toContain('/api/analytics/learning-gain')
|
||||
})
|
||||
|
||||
it('supports analytics-stops action with unit_id', () => {
|
||||
expect(content).toContain("action === 'analytics-stops'")
|
||||
expect(content).toContain('/api/analytics/unit')
|
||||
})
|
||||
|
||||
it('supports time_range parameter for analytics', () => {
|
||||
expect(content).toContain('time_range')
|
||||
expect(content).toContain('timeRange')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Teacher Dashboard Endpoints', () => {
|
||||
it('supports teacher-dashboard action', () => {
|
||||
expect(content).toContain("'teacher-dashboard'")
|
||||
expect(content).toContain('/api/teacher/dashboard')
|
||||
})
|
||||
|
||||
it('supports teacher-units action', () => {
|
||||
expect(content).toContain("'teacher-units'")
|
||||
expect(content).toContain('/api/teacher/units/available')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Content Generation Endpoints', () => {
|
||||
it('supports content-h5p action', () => {
|
||||
expect(content).toContain("action === 'content-h5p'")
|
||||
expect(content).toContain('/h5p')
|
||||
})
|
||||
|
||||
it('supports content-worksheet action', () => {
|
||||
expect(content).toContain("action === 'content-worksheet'")
|
||||
expect(content).toContain('/worksheet')
|
||||
})
|
||||
|
||||
it('supports content-pdf action', () => {
|
||||
expect(content).toContain("action === 'content-pdf'")
|
||||
expect(content).toContain('/worksheet.pdf')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Helper Functions', () => {
|
||||
it('has handlePythonBackend function', () => {
|
||||
expect(content).toContain('async function handlePythonBackend')
|
||||
})
|
||||
|
||||
it('has fetchFromPython function', () => {
|
||||
expect(content).toContain('async function fetchFromPython')
|
||||
})
|
||||
|
||||
it('has fetchPdfFromPython function', () => {
|
||||
expect(content).toContain('async function fetchPdfFromPython')
|
||||
})
|
||||
|
||||
it('fetchPdfFromPython returns PDF with correct headers', () => {
|
||||
expect(content).toContain("'Content-Type': 'application/pdf'")
|
||||
expect(content).toContain("'Content-Disposition': 'attachment")
|
||||
})
|
||||
|
||||
it('has longer timeout for PDF generation', () => {
|
||||
expect(content).toContain('15000') // 15 seconds for PDF
|
||||
})
|
||||
})
|
||||
|
||||
describe('Error Handling for Python Backend', () => {
|
||||
it('handles Python backend connection errors', () => {
|
||||
expect(content).toContain('Backend nicht erreichbar')
|
||||
})
|
||||
|
||||
it('handles Python backend timeout', () => {
|
||||
expect(content).toContain('Backend timed out')
|
||||
})
|
||||
|
||||
it('returns offline status for Python backend errors', () => {
|
||||
// Check that fetchFromPython returns offline: true
|
||||
expect(content).toContain('offline: true')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// ==============================================
|
||||
// Tab System Tests
|
||||
// ==============================================
|
||||
|
||||
describe('Unity Bridge Tab System', () => {
|
||||
const dashboardPath = join(APP_DIR, 'admin', 'unity-bridge', 'page.tsx')
|
||||
let content: string
|
||||
|
||||
beforeAll(() => {
|
||||
content = readFileSync(dashboardPath, 'utf-8')
|
||||
})
|
||||
|
||||
describe('Tab State Management', () => {
|
||||
it('has activeTab state', () => {
|
||||
expect(content).toContain('activeTab')
|
||||
expect(content).toContain('setActiveTab')
|
||||
})
|
||||
|
||||
it('default tab is editor', () => {
|
||||
expect(content).toContain("useState<TabId>('editor')")
|
||||
})
|
||||
|
||||
it('has TabId type definition', () => {
|
||||
expect(content).toContain('TabId')
|
||||
expect(content).toContain('editor')
|
||||
expect(content).toContain('units')
|
||||
expect(content).toContain('sessions')
|
||||
expect(content).toContain('analytics')
|
||||
expect(content).toContain('content')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Tab Navigation', () => {
|
||||
it('has 5 tabs', () => {
|
||||
expect(content).toContain('Editor')
|
||||
expect(content).toContain('Units')
|
||||
expect(content).toContain('Sessions')
|
||||
expect(content).toContain('Analytics')
|
||||
expect(content).toContain('Content')
|
||||
})
|
||||
|
||||
it('tabs are clickable buttons', () => {
|
||||
expect(content).toContain('onClick={() => setActiveTab')
|
||||
})
|
||||
|
||||
it('has active tab styling', () => {
|
||||
expect(content).toContain('activeTab ===')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Units Tab', () => {
|
||||
it('fetches units from backend', () => {
|
||||
expect(content).toContain('fetchUnits')
|
||||
expect(content).toContain('units-list')
|
||||
})
|
||||
|
||||
it('has units state', () => {
|
||||
expect(content).toContain('units')
|
||||
expect(content).toContain('setUnits')
|
||||
})
|
||||
|
||||
it('has UnitDefinition interface', () => {
|
||||
expect(content).toContain('interface UnitDefinition')
|
||||
})
|
||||
|
||||
it('has unit_id field', () => {
|
||||
expect(content).toContain('unit_id')
|
||||
})
|
||||
|
||||
it('has template field', () => {
|
||||
expect(content).toContain('template')
|
||||
})
|
||||
|
||||
it('has learning_objectives field', () => {
|
||||
expect(content).toContain('learning_objectives')
|
||||
})
|
||||
|
||||
it('has stops field', () => {
|
||||
expect(content).toContain('stops')
|
||||
})
|
||||
|
||||
it('can fetch unit details', () => {
|
||||
expect(content).toContain('fetchUnitDetails')
|
||||
expect(content).toContain('units-get')
|
||||
})
|
||||
|
||||
it('has selectedUnit state', () => {
|
||||
expect(content).toContain('selectedUnit')
|
||||
expect(content).toContain('setSelectedUnit')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Analytics Tab', () => {
|
||||
it('fetches analytics from backend', () => {
|
||||
expect(content).toContain('fetchAnalytics')
|
||||
expect(content).toContain('analytics-overview')
|
||||
})
|
||||
|
||||
it('has analytics state', () => {
|
||||
expect(content).toContain('analytics')
|
||||
expect(content).toContain('setAnalytics')
|
||||
})
|
||||
|
||||
it('has AnalyticsOverview interface', () => {
|
||||
expect(content).toContain('interface AnalyticsOverview')
|
||||
})
|
||||
|
||||
it('has total_sessions field', () => {
|
||||
expect(content).toContain('total_sessions')
|
||||
})
|
||||
|
||||
it('has unique_students field', () => {
|
||||
expect(content).toContain('unique_students')
|
||||
})
|
||||
|
||||
it('has avg_completion_rate field', () => {
|
||||
expect(content).toContain('avg_completion_rate')
|
||||
})
|
||||
|
||||
it('has avg_learning_gain field', () => {
|
||||
expect(content).toContain('avg_learning_gain')
|
||||
})
|
||||
|
||||
it('has most_played_units field', () => {
|
||||
expect(content).toContain('most_played_units')
|
||||
})
|
||||
|
||||
it('has struggling_concepts field', () => {
|
||||
expect(content).toContain('struggling_concepts')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Content Tab', () => {
|
||||
it('can generate H5P content', () => {
|
||||
expect(content).toContain('generateH5P')
|
||||
expect(content).toContain('content-h5p')
|
||||
})
|
||||
|
||||
it('can generate worksheet', () => {
|
||||
expect(content).toContain('generateWorksheet')
|
||||
expect(content).toContain('content-worksheet')
|
||||
})
|
||||
|
||||
it('can download PDF', () => {
|
||||
expect(content).toContain('downloadPdf')
|
||||
expect(content).toContain('content-pdf')
|
||||
})
|
||||
|
||||
it('has generatedContent state', () => {
|
||||
expect(content).toContain('generatedContent')
|
||||
expect(content).toContain('setGeneratedContent')
|
||||
})
|
||||
|
||||
it('has GeneratedContent interface', () => {
|
||||
expect(content).toContain('interface GeneratedContent')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Sessions Tab', () => {
|
||||
it('renders sessions tab content', () => {
|
||||
expect(content).toContain("activeTab === 'sessions'")
|
||||
})
|
||||
|
||||
it('has sessions placeholder or content', () => {
|
||||
// Sessions tab exists in the tab switch
|
||||
expect(content).toContain('sessions')
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user