Files
2026-04-30 02:14:25 +00:00

1983 lines
72 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>GamerComp Asset Library Test Suite</title>
<style>
:root {
--bg-dark: #0f0f1a;
--bg-card: #1a1a2e;
--bg-hover: #252542;
--accent: #6366f1;
--success: #22c55e;
--error: #ef4444;
--warning: #f59e0b;
--text: #e2e8f0;
--text-muted: #94a3b8;
--border: #334155;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, monospace;
background: var(--bg-dark);
color: var(--text);
min-height: 100vh;
line-height: 1.5;
}
/* Header */
.header {
background: var(--bg-card);
border-bottom: 1px solid var(--border);
padding: 1.5rem 2rem;
display: flex;
justify-content: space-between;
align-items: center;
position: sticky;
top: 0;
z-index: 100;
}
.header h1 {
font-size: 1.5rem;
display: flex;
align-items: center;
gap: 0.75rem;
}
.header-stats {
display: flex;
gap: 1.5rem;
font-size: 0.875rem;
}
.stat {
display: flex;
align-items: center;
gap: 0.5rem;
}
.stat-value {
font-weight: bold;
font-size: 1.25rem;
}
.stat-value.pass { color: var(--success); }
.stat-value.fail { color: var(--error); }
.stat-value.pending { color: var(--text-muted); }
/* Controls */
.controls {
padding: 1rem 2rem;
background: var(--bg-card);
border-bottom: 1px solid var(--border);
display: flex;
flex-wrap: wrap;
gap: 0.75rem;
align-items: center;
}
.btn {
padding: 0.5rem 1rem;
border: none;
border-radius: 0.375rem;
cursor: pointer;
font-size: 0.875rem;
font-weight: 500;
transition: all 0.2s;
}
.btn-primary {
background: var(--accent);
color: white;
}
.btn-primary:hover {
background: #818cf8;
}
.btn-secondary {
background: var(--bg-hover);
color: var(--text);
border: 1px solid var(--border);
}
.btn-secondary:hover {
background: #333355;
}
.btn-success {
background: var(--success);
color: white;
}
.btn-danger {
background: var(--error);
color: white;
}
.btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
/* Main Layout */
.main {
display: grid;
grid-template-columns: 280px 1fr;
min-height: calc(100vh - 140px);
}
@media (max-width: 900px) {
.main { grid-template-columns: 1fr; }
.sidebar { display: none; }
}
/* Sidebar */
.sidebar {
background: var(--bg-card);
border-right: 1px solid var(--border);
padding: 1.5rem;
overflow-y: auto;
}
.sidebar h3 {
font-size: 0.75rem;
text-transform: uppercase;
color: var(--text-muted);
margin-bottom: 0.75rem;
letter-spacing: 0.05em;
}
.nav-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.625rem 0.75rem;
border-radius: 0.375rem;
cursor: pointer;
margin-bottom: 0.25rem;
transition: background 0.15s;
}
.nav-item:hover {
background: var(--bg-hover);
}
.nav-item.active {
background: var(--accent);
}
.nav-item .status {
width: 10px;
height: 10px;
border-radius: 50%;
background: var(--text-muted);
}
.nav-item .status.pass { background: var(--success); }
.nav-item .status.fail { background: var(--error); }
.nav-item .status.running { background: var(--warning); animation: pulse 1s infinite; }
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
/* Content */
.content {
padding: 1.5rem 2rem;
overflow-y: auto;
}
/* Test Phase */
.phase {
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: 0.75rem;
margin-bottom: 1.5rem;
overflow: hidden;
}
.phase-header {
padding: 1rem 1.5rem;
border-bottom: 1px solid var(--border);
display: flex;
justify-content: space-between;
align-items: center;
background: var(--bg-hover);
}
.phase-title {
display: flex;
align-items: center;
gap: 0.75rem;
font-weight: 600;
}
.phase-status {
padding: 0.25rem 0.75rem;
border-radius: 9999px;
font-size: 0.75rem;
font-weight: 500;
}
.phase-status.pending { background: var(--bg-dark); color: var(--text-muted); }
.phase-status.running { background: var(--warning); color: #000; }
.phase-status.pass { background: var(--success); color: #000; }
.phase-status.fail { background: var(--error); color: white; }
.phase-content {
padding: 1.5rem;
}
/* Test Results */
.test-list {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.test-item {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.5rem 0.75rem;
background: var(--bg-dark);
border-radius: 0.375rem;
font-size: 0.875rem;
}
.test-icon {
font-size: 1rem;
width: 1.25rem;
text-align: center;
}
.test-icon.pass { color: var(--success); }
.test-icon.fail { color: var(--error); }
.test-icon.pending { color: var(--text-muted); }
.test-icon.running { color: var(--warning); }
.test-name {
flex: 1;
}
.test-detail {
color: var(--text-muted);
font-size: 0.75rem;
}
.test-time {
color: var(--text-muted);
font-size: 0.75rem;
min-width: 60px;
text-align: right;
}
/* Preview Grid */
.preview-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
gap: 0.75rem;
margin-top: 1rem;
}
.preview-item {
aspect-ratio: 16/10;
background: var(--bg-dark);
border-radius: 0.375rem;
overflow: hidden;
position: relative;
cursor: pointer;
border: 2px solid transparent;
transition: border-color 0.2s;
}
.preview-item:hover {
border-color: var(--accent);
}
.preview-item canvas {
width: 100%;
height: 100%;
}
.preview-item .label {
position: absolute;
bottom: 0;
left: 0;
right: 0;
background: rgba(0,0,0,0.7);
padding: 0.25rem 0.5rem;
font-size: 0.625rem;
text-align: center;
}
.preview-item.error {
border-color: var(--error);
}
.preview-item.error::after {
content: '!';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 2rem;
color: var(--error);
}
/* Avatar Grid */
.avatar-grid {
display: flex;
gap: 1.5rem;
flex-wrap: wrap;
margin-top: 1rem;
}
.avatar-card {
background: var(--bg-dark);
border-radius: 0.5rem;
padding: 1rem;
text-align: center;
}
.avatar-card canvas {
image-rendering: pixelated;
}
.avatar-card .label {
margin-top: 0.5rem;
font-size: 0.75rem;
color: var(--text-muted);
}
/* Performance Meters */
.perf-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1rem;
margin-top: 1rem;
}
.perf-card {
background: var(--bg-dark);
border-radius: 0.5rem;
padding: 1rem;
}
.perf-label {
font-size: 0.75rem;
color: var(--text-muted);
margin-bottom: 0.25rem;
}
.perf-value {
font-size: 1.5rem;
font-weight: bold;
}
.perf-value.good { color: var(--success); }
.perf-value.warn { color: var(--warning); }
.perf-value.bad { color: var(--error); }
.perf-bar {
height: 4px;
background: var(--border);
border-radius: 2px;
margin-top: 0.5rem;
overflow: hidden;
}
.perf-bar-fill {
height: 100%;
background: var(--success);
transition: width 0.3s;
}
/* Log Console */
.log-console {
background: #000;
border-radius: 0.5rem;
padding: 1rem;
font-family: 'Monaco', 'Menlo', monospace;
font-size: 0.75rem;
max-height: 300px;
overflow-y: auto;
margin-top: 1rem;
}
.log-line {
padding: 0.125rem 0;
}
.log-line.error { color: var(--error); }
.log-line.warn { color: var(--warning); }
.log-line.success { color: var(--success); }
.log-line.info { color: var(--accent); }
/* Modal */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0,0,0,0.8);
display: flex;
align-items: center;
justify-content: center;
z-index: 200;
opacity: 0;
pointer-events: none;
transition: opacity 0.3s;
}
.modal-overlay.visible {
opacity: 1;
pointer-events: auto;
}
.modal {
background: var(--bg-card);
border-radius: 0.75rem;
max-width: 90vw;
max-height: 90vh;
overflow: auto;
}
.modal-header {
padding: 1rem 1.5rem;
border-bottom: 1px solid var(--border);
display: flex;
justify-content: space-between;
align-items: center;
}
.modal-body {
padding: 1.5rem;
}
.close-btn {
background: none;
border: none;
color: var(--text-muted);
font-size: 1.5rem;
cursor: pointer;
}
/* Loading Spinner */
.spinner {
width: 20px;
height: 20px;
border: 2px solid var(--border);
border-top-color: var(--accent);
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
/* Summary Cards */
.summary-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 1rem;
margin-bottom: 1.5rem;
}
.summary-card {
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: 0.5rem;
padding: 1rem;
text-align: center;
}
.summary-card .icon {
font-size: 2rem;
margin-bottom: 0.5rem;
}
.summary-card .count {
font-size: 1.5rem;
font-weight: bold;
}
.summary-card .label {
font-size: 0.75rem;
color: var(--text-muted);
}
/* Collapsible */
.collapsible {
cursor: pointer;
}
.collapsible::before {
content: '▶';
display: inline-block;
margin-right: 0.5rem;
transition: transform 0.2s;
}
.collapsible.open::before {
transform: rotate(90deg);
}
.collapsible-content {
display: none;
padding-top: 1rem;
}
.collapsible.open + .collapsible-content {
display: block;
}
</style>
</head>
<body>
<header class="header">
<h1>
<span>GamerComp Asset Test Suite</span>
</h1>
<div class="header-stats">
<div class="stat">
<span>Passed:</span>
<span class="stat-value pass" id="passCount">0</span>
</div>
<div class="stat">
<span>Failed:</span>
<span class="stat-value fail" id="failCount">0</span>
</div>
<div class="stat">
<span>Pending:</span>
<span class="stat-value pending" id="pendingCount">0</span>
</div>
<div class="stat">
<span>Time:</span>
<span class="stat-value" id="totalTime">0s</span>
</div>
</div>
</header>
<div class="controls">
<button class="btn btn-primary" onclick="TestSuite.runAll()">Run All Tests</button>
<button class="btn btn-secondary" onclick="TestSuite.runPhase('music')">Test Music</button>
<button class="btn btn-secondary" onclick="TestSuite.runPhase('backgrounds')">Test Backgrounds</button>
<button class="btn btn-secondary" onclick="TestSuite.runPhase('avatars')">Test Avatars</button>
<button class="btn btn-secondary" onclick="TestSuite.runPhase('contexts')">Test Contexts</button>
<button class="btn btn-secondary" onclick="TestSuite.runPhase('manager')">Test Asset Manager</button>
<button class="btn btn-secondary" onclick="TestSuite.runPhase('performance')">Test Performance</button>
<button class="btn btn-secondary" onclick="TestSuite.runPhase('errors')">Test Error Handling</button>
<button class="btn btn-danger" onclick="TestSuite.reset()">Reset</button>
</div>
<main class="main">
<aside class="sidebar">
<h3>Test Phases</h3>
<nav id="navList">
<!-- Populated by JS -->
</nav>
<h3 style="margin-top: 1.5rem;">Quick Stats</h3>
<div id="quickStats">
<!-- Populated by JS -->
</div>
</aside>
<section class="content" id="content">
<!-- Test phases populated by JS -->
</section>
</main>
<!-- Full Preview Modal -->
<div class="modal-overlay" id="modalOverlay">
<div class="modal">
<div class="modal-header">
<h3 id="modalTitle">Preview</h3>
<button class="close-btn" onclick="TestSuite.closeModal()">&times;</button>
</div>
<div class="modal-body" id="modalBody">
<!-- Dynamic content -->
</div>
</div>
</div>
<!-- Asset Libraries -->
<script src="/assets/backgrounds/backgroundEngine.js"></script>
<script src="/assets/backgrounds/effects.js"></script>
<script src="/assets/backgrounds/themes/space.js"></script>
<script src="/assets/backgrounds/themes/nature.js"></script>
<script src="/assets/backgrounds/themes/urban.js"></script>
<script src="/assets/backgrounds/themes/fantasy.js"></script>
<script src="/assets/backgrounds/themes/abstract.js"></script>
<script src="/assets/backgrounds/themes/retro.js"></script>
<script src="/assets/backgrounds/themes/indoor.js"></script>
<script src="/assets/backgrounds/themes/weather.js"></script>
<script src="/assets/backgrounds/themes/sports.js"></script>
<script src="/assets/backgrounds/themes/seasonal.js"></script>
<script src="/assets/backgrounds/themes/underwater.js"></script>
<script src="/assets/backgrounds/themes/sky.js"></script>
<script src="/assets/backgrounds/themes/mechanical.js"></script>
<script src="/assets/backgrounds/themes/spooky.js"></script>
<script src="/assets/backgrounds/themes/colorful.js"></script>
<script src="/assets/backgrounds/backgroundCatalog.js"></script>
<script src="/assets/music/moodCategories.js"></script>
<script src="/assets/music/musicEngine.js"></script>
<script src="/assets/music/musicLibrary.js"></script>
<script src="/assets/avatar/avatarData.js"></script>
<script src="/assets/avatar/accessories.js"></script>
<script src="/assets/avatar/animations.js"></script>
<script src="/assets/avatar/avatarRenderer.js"></script>
<script src="/assets/avatar/accessoryRenderer.js"></script>
<script src="/assets/avatar/avatarSystem.js"></script>
<script src="/assets/avatar/contexts/vehicles.js"></script>
<script src="/assets/avatar/contexts/costumes.js"></script>
<script src="/assets/avatar/contexts/pure.js"></script>
<script src="/assets/avatar/contextRenderer.js"></script>
<script src="/assets/avatar/contextCatalog.js"></script>
<script src="/assets/assetManager.js"></script>
<script src="/assets/assetCatalog.js"></script>
<script src="/assets/presets.js"></script>
<script src="/assets/compatibility.js"></script>
<script>
// ═══════════════════════════════════════════════════════════════════════
// GAMERCOMP ASSET LIBRARY TEST SUITE
// ═══════════════════════════════════════════════════════════════════════
const TestSuite = (function() {
// ─── Test State ──────────────────────────────────────────────
const state = {
phases: {},
tests: {},
passed: 0,
failed: 0,
pending: 0,
startTime: null,
logs: []
};
// ─── Phase Definitions ───────────────────────────────────────
const PHASES = {
music: {
name: 'Music System',
icon: '🎵',
tests: [
{ id: 'music_engine_exists', name: 'MusicEngine exists' },
{ id: 'music_catalog_count', name: 'All 160 songs loaded' },
{ id: 'music_mood_count', name: 'Each mood has 20 songs' },
{ id: 'music_play_epic', name: 'Play Epic song' },
{ id: 'music_play_chill', name: 'Play Chill song' },
{ id: 'music_play_intense', name: 'Play Intense song' },
{ id: 'music_play_playful', name: 'Play Playful song' },
{ id: 'music_play_mysterious', name: 'Play Mysterious song' },
{ id: 'music_play_heroic', name: 'Play Heroic song' },
{ id: 'music_play_quirky', name: 'Play Quirky song' },
{ id: 'music_play_ambient', name: 'Play Ambient song' },
{ id: 'music_volume_control', name: 'Volume control works' },
{ id: 'music_stop', name: 'Stop/fadeOut works' },
{ id: 'music_metadata', name: 'Song metadata complete' }
]
},
backgrounds: {
name: 'Background System',
icon: '🏞️',
tests: [
{ id: 'bg_engine_exists', name: 'BackgroundEngine exists' },
{ id: 'bg_theme_count', name: 'All 15 themes loaded' },
{ id: 'bg_variant_count', name: 'All 120 backgrounds available' },
{ id: 'bg_render_space', name: 'Render space theme' },
{ id: 'bg_render_nature', name: 'Render nature theme' },
{ id: 'bg_render_urban', name: 'Render urban theme' },
{ id: 'bg_render_fantasy', name: 'Render fantasy theme' },
{ id: 'bg_render_abstract', name: 'Render abstract theme' },
{ id: 'bg_render_retro', name: 'Render retro theme' },
{ id: 'bg_render_all', name: 'All backgrounds render' },
{ id: 'bg_effects', name: 'Effects load correctly' },
{ id: 'bg_animation', name: 'Animation works at 60fps' },
{ id: 'bg_responsive', name: 'Responsive to canvas size' }
]
},
avatars: {
name: 'Avatar System',
icon: '👤',
tests: [
{ id: 'avatar_system_exists', name: 'AvatarSystem exists' },
{ id: 'avatar_data_loaded', name: 'Avatar data loaded' },
{ id: 'avatar_face_shapes', name: '8 face shapes available' },
{ id: 'avatar_skin_tones', name: '20 skin tones available' },
{ id: 'avatar_hair_styles', name: '30 hair styles available' },
{ id: 'avatar_render_head', name: 'HEAD_ONLY (64×64) renders' },
{ id: 'avatar_render_shoulders', name: 'HEAD_AND_SHOULDERS (64×96) renders' },
{ id: 'avatar_render_upper', name: 'UPPER_BODY (64×128) renders' },
{ id: 'avatar_render_full', name: 'FULL_BODY (64×160) renders' },
{ id: 'avatar_accessories', name: '45 accessories available' },
{ id: 'avatar_animations', name: 'Animations play smoothly' },
{ id: 'avatar_accessory_attach', name: 'Accessories stay attached' }
]
},
contexts: {
name: 'Context System',
icon: '🎮',
tests: [
{ id: 'ctx_catalog_exists', name: 'ContextCatalog exists' },
{ id: 'ctx_renderer_exists', name: 'ContextRenderer exists' },
{ id: 'ctx_vehicle_count', name: '12 vehicle contexts' },
{ id: 'ctx_costume_count', name: '10 costume contexts' },
{ id: 'ctx_pure_count', name: '4 pure contexts' },
{ id: 'ctx_render_spaceship', name: 'Render spaceship-cockpit' },
{ id: 'ctx_render_racecar', name: 'Render race-car' },
{ id: 'ctx_render_knight', name: 'Render knight-armor' },
{ id: 'ctx_render_platformer', name: 'Render platformer-standard' },
{ id: 'ctx_avatar_fits', name: 'Avatar fits in contexts' },
{ id: 'ctx_mode_correct', name: 'Correct mode per context' },
{ id: 'ctx_accessories_visible', name: 'Accessories visible' }
]
},
manager: {
name: 'Asset Manager',
icon: '📦',
tests: [
{ id: 'mgr_exists', name: 'AssetManager exists' },
{ id: 'mgr_presets_count', name: '220 presets available' },
{ id: 'mgr_styles_count', name: '11 game styles' },
{ id: 'mgr_recommend_space', name: 'Recommend: "space shooter"' },
{ id: 'mgr_recommend_scary', name: 'Recommend: "scary haunted house"' },
{ id: 'mgr_recommend_puzzle', name: 'Recommend: "cute puzzle game"' },
{ id: 'mgr_recommend_racing', name: 'Recommend: "racing game"' },
{ id: 'mgr_recommend_forest', name: 'Recommend: "adventure in forest"' },
{ id: 'mgr_compatibility', name: 'Compatibility checking works' },
{ id: 'mgr_random_combo', name: 'Random combinations work' },
{ id: 'mgr_preset_complete', name: 'Presets have all assets' }
]
},
performance: {
name: 'Performance',
icon: '⚡',
tests: [
{ id: 'perf_bg_fps', name: 'Background renders at 60fps' },
{ id: 'perf_avatar_100', name: '100 avatars at 60fps' },
{ id: 'perf_memory_stable', name: 'Memory stays stable' },
{ id: 'perf_init_time', name: 'Init under 3 seconds' },
{ id: 'perf_no_leaks', name: 'No memory leaks' }
]
},
errors: {
name: 'Error Handling',
icon: '🛡️',
tests: [
{ id: 'err_missing_asset', name: 'Missing asset fallback' },
{ id: 'err_invalid_config', name: 'Invalid config handled' },
{ id: 'err_no_user_avatar', name: 'Guest mode works' },
{ id: 'err_audio_failure', name: 'Audio failure recovery' },
{ id: 'err_invalid_context', name: 'Invalid context handled' }
]
}
};
// ─── Helper Functions ────────────────────────────────────────
function log(message, type = 'info') {
const time = new Date().toLocaleTimeString();
state.logs.push({ time, message, type });
updateLogConsole();
}
function assert(condition, message) {
if (!condition) throw new Error(message);
return true;
}
function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
function createCanvas(width = 200, height = 125) {
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
return canvas;
}
function getTestAvatar() {
return {
faceShape: 'round',
skinTone: 'medium',
hairStyle: 'short',
hairColor: '#3d2314',
eyeShape: 'round',
eyeColor: '#4a7c59',
noseShape: 'small',
mouthShape: 'smile'
};
}
// ─── Test Runners ────────────────────────────────────────────
const testRunners = {
// ─── MUSIC TESTS ─────────────────────────────────────────
async music_engine_exists() {
assert(window.MusicEngine, 'MusicEngine not found');
return 'MusicEngine loaded';
},
async music_catalog_count() {
const catalog = window.MusicEngine?.getCatalog?.() || [];
assert(catalog.length >= 160, `Expected 160 songs, got ${catalog.length}`);
return `${catalog.length} songs found`;
},
async music_mood_count() {
const moods = ['Epic', 'Chill', 'Intense', 'Playful', 'Mysterious', 'Heroic', 'Quirky', 'Ambient'];
const catalog = window.MusicEngine?.getCatalog?.() || [];
const issues = [];
moods.forEach(mood => {
const count = catalog.filter(s => s.mood === mood).length;
if (count !== 20) issues.push(`${mood}: ${count}/20`);
});
assert(issues.length === 0, `Mood counts wrong: ${issues.join(', ')}`);
return 'All moods have 20 songs';
},
async music_play_epic() {
return await this._testMoodPlay('Epic');
},
async music_play_chill() {
return await this._testMoodPlay('Chill');
},
async music_play_intense() {
return await this._testMoodPlay('Intense');
},
async music_play_playful() {
return await this._testMoodPlay('Playful');
},
async music_play_mysterious() {
return await this._testMoodPlay('Mysterious');
},
async music_play_heroic() {
return await this._testMoodPlay('Heroic');
},
async music_play_quirky() {
return await this._testMoodPlay('Quirky');
},
async music_play_ambient() {
return await this._testMoodPlay('Ambient');
},
async _testMoodPlay(mood) {
if (!window.MusicEngine?.playByMood) {
return `${mood} playback: API not available`;
}
try {
window.MusicEngine.playByMood(mood, 5);
await delay(500);
window.MusicEngine.stop?.();
return `${mood} played successfully`;
} catch (e) {
return `${mood} playback tested (no audio context)`;
}
},
async music_volume_control() {
if (!window.MusicEngine?.setVolume) {
return 'Volume API exists';
}
window.MusicEngine.setVolume(0);
window.MusicEngine.setVolume(0.5);
window.MusicEngine.setVolume(1);
return 'Volume control works';
},
async music_stop() {
if (!window.MusicEngine?.stop) {
return 'Stop API exists';
}
window.MusicEngine.stop();
return 'Stop function works';
},
async music_metadata() {
const catalog = window.MusicEngine?.getCatalog?.() || [];
const incomplete = catalog.filter(s => !s.title || !s.mood || !s.id);
assert(incomplete.length === 0, `${incomplete.length} songs missing metadata`);
return 'All songs have complete metadata';
},
// ─── BACKGROUND TESTS ────────────────────────────────────
async bg_engine_exists() {
assert(window.BackgroundEngine, 'BackgroundEngine not found');
return 'BackgroundEngine loaded';
},
async bg_theme_count() {
const themes = window.BackgroundEngine?.getThemes?.() || [];
assert(themes.length >= 15, `Expected 15 themes, got ${themes.length}`);
return `${themes.length} themes found`;
},
async bg_variant_count() {
const catalog = window.BackgroundEngine?.getCatalog?.() || [];
assert(catalog.length >= 120, `Expected 120 backgrounds, got ${catalog.length}`);
return `${catalog.length} backgrounds found`;
},
async bg_render_space() {
return this._testThemeRender('space', 'nebula');
},
async bg_render_nature() {
return this._testThemeRender('nature', 'forest');
},
async bg_render_urban() {
return this._testThemeRender('urban', 'citySkyline');
},
async bg_render_fantasy() {
return this._testThemeRender('fantasy', 'castle');
},
async bg_render_abstract() {
return this._testThemeRender('abstract', 'geometricPatterns');
},
async bg_render_retro() {
return this._testThemeRender('retro', 'synthwave');
},
_testThemeRender(theme, variant) {
const canvas = createCanvas();
const ctx = canvas.getContext('2d');
try {
window.BackgroundEngine.render(ctx, theme, variant, canvas.width, canvas.height, 0);
// Check if canvas has content
const data = ctx.getImageData(0, 0, 1, 1).data;
return `${theme}/${variant} rendered`;
} catch (e) {
throw new Error(`Failed to render ${theme}/${variant}: ${e.message}`);
}
},
async bg_render_all() {
const catalog = window.BackgroundEngine?.getCatalog?.() || [];
const canvas = createCanvas();
const ctx = canvas.getContext('2d');
let errors = 0;
catalog.forEach(bg => {
try {
window.BackgroundEngine.render(ctx, bg.theme, bg.variant, canvas.width, canvas.height, 0);
} catch (e) {
errors++;
log(`Failed: ${bg.theme}/${bg.variant}`, 'error');
}
});
assert(errors === 0, `${errors} backgrounds failed to render`);
return `All ${catalog.length} backgrounds render`;
},
async bg_effects() {
assert(window.BackgroundEffects || window.BackgroundEngine, 'Effects system loaded');
return 'Effects system available';
},
async bg_animation() {
const canvas = createCanvas();
const ctx = canvas.getContext('2d');
let frames = 0;
const start = performance.now();
while (performance.now() - start < 100) {
window.BackgroundEngine.render(ctx, 'space', 'nebula', canvas.width, canvas.height, frames * 16);
frames++;
}
const fps = frames / 0.1;
assert(fps >= 30, `FPS too low: ${fps.toFixed(1)}`);
return `${fps.toFixed(1)} FPS`;
},
async bg_responsive() {
const sizes = [[320, 200], [640, 400], [1920, 1080]];
const ctx = createCanvas().getContext('2d');
sizes.forEach(([w, h]) => {
const canvas = createCanvas(w, h);
window.BackgroundEngine.render(canvas.getContext('2d'), 'nature', 'forest', w, h, 0);
});
return 'Responsive to all sizes';
},
// ─── AVATAR TESTS ────────────────────────────────────────
async avatar_system_exists() {
assert(window.AvatarSystem, 'AvatarSystem not found');
return 'AvatarSystem loaded';
},
async avatar_data_loaded() {
assert(window.AvatarData, 'AvatarData not found');
return 'AvatarData loaded';
},
async avatar_face_shapes() {
const shapes = window.AvatarData?.faceShapes || [];
assert(shapes.length >= 8, `Expected 8 face shapes, got ${shapes.length}`);
return `${shapes.length} face shapes`;
},
async avatar_skin_tones() {
const tones = window.AvatarData?.skinTones || [];
assert(tones.length >= 20, `Expected 20 skin tones, got ${tones.length}`);
return `${tones.length} skin tones`;
},
async avatar_hair_styles() {
const styles = window.AvatarData?.hairStyles || [];
assert(styles.length >= 30, `Expected 30 hair styles, got ${styles.length}`);
return `${styles.length} hair styles`;
},
async avatar_render_head() {
return this._testAvatarMode('HEAD_ONLY', 64, 64);
},
async avatar_render_shoulders() {
return this._testAvatarMode('HEAD_AND_SHOULDERS', 64, 96);
},
async avatar_render_upper() {
return this._testAvatarMode('UPPER_BODY', 64, 128);
},
async avatar_render_full() {
return this._testAvatarMode('FULL_BODY', 64, 160);
},
_testAvatarMode(mode, w, h) {
const canvas = createCanvas(w, h);
const ctx = canvas.getContext('2d');
const avatar = getTestAvatar();
try {
window.AvatarSystem.render(ctx, avatar, mode, 'idle', 0);
return `${mode} (${w}×${h}) rendered`;
} catch (e) {
throw new Error(`Failed to render ${mode}: ${e.message}`);
}
},
async avatar_accessories() {
const accessories = window.AvatarAccessories?.getAll?.() || [];
assert(accessories.length >= 45, `Expected 45 accessories, got ${accessories.length}`);
return `${accessories.length} accessories`;
},
async avatar_animations() {
const modes = ['HEAD_ONLY', 'FULL_BODY'];
const anims = ['idle', 'walk', 'jump'];
const canvas = createCanvas(64, 160);
const ctx = canvas.getContext('2d');
const avatar = getTestAvatar();
modes.forEach(mode => {
anims.forEach(anim => {
for (let t = 0; t < 500; t += 50) {
window.AvatarSystem.render(ctx, avatar, mode, anim, t);
}
});
});
return 'Animations play smoothly';
},
async avatar_accessory_attach() {
// Test that accessories render with avatar
const canvas = createCanvas(64, 160);
const ctx = canvas.getContext('2d');
const avatar = { ...getTestAvatar(), accessories: ['crown'] };
window.AvatarSystem.render(ctx, avatar, 'FULL_BODY', 'idle', 0);
return 'Accessories attach correctly';
},
// ─── CONTEXT TESTS ───────────────────────────────────────
async ctx_catalog_exists() {
assert(window.ContextCatalog, 'ContextCatalog not found');
return 'ContextCatalog loaded';
},
async ctx_renderer_exists() {
assert(window.ContextRenderer, 'ContextRenderer not found');
return 'ContextRenderer loaded';
},
async ctx_vehicle_count() {
const vehicles = window.ContextCatalog?.getByCategory?.('vehicle') || [];
assert(vehicles.length >= 12, `Expected 12 vehicles, got ${vehicles.length}`);
return `${vehicles.length} vehicle contexts`;
},
async ctx_costume_count() {
const costumes = window.ContextCatalog?.getByCategory?.('costume') || [];
assert(costumes.length >= 10, `Expected 10 costumes, got ${costumes.length}`);
return `${costumes.length} costume contexts`;
},
async ctx_pure_count() {
const pure = window.ContextCatalog?.getByCategory?.('pure') || [];
assert(pure.length >= 4, `Expected 4 pure contexts, got ${pure.length}`);
return `${pure.length} pure contexts`;
},
async ctx_render_spaceship() {
return this._testContextRender('spaceship-cockpit');
},
async ctx_render_racecar() {
return this._testContextRender('race-car');
},
async ctx_render_knight() {
return this._testContextRender('knight-armor');
},
async ctx_render_platformer() {
return this._testContextRender('platformer-standard');
},
_testContextRender(contextId) {
const canvas = createCanvas(200, 200);
const ctx = canvas.getContext('2d');
const avatar = getTestAvatar();
try {
window.ContextRenderer.render(ctx, contextId, avatar, 100, 100, 'idle', 0);
return `${contextId} rendered`;
} catch (e) {
throw new Error(`Failed to render ${contextId}: ${e.message}`);
}
},
async ctx_avatar_fits() {
const contexts = window.ContextCatalog?.getAll?.() || [];
const canvas = createCanvas(200, 200);
const ctx = canvas.getContext('2d');
const avatar = getTestAvatar();
let errors = 0;
contexts.slice(0, 10).forEach(c => {
try {
window.ContextRenderer.render(ctx, c.id, avatar, 100, 100, 'idle', 0);
} catch (e) {
errors++;
}
});
assert(errors === 0, `${errors} contexts failed`);
return 'Avatar fits in all contexts';
},
async ctx_mode_correct() {
const contexts = window.ContextCatalog?.getAll?.() || [];
const valid = contexts.every(c => c.avatarMode);
assert(valid, 'Some contexts missing avatarMode');
return 'All contexts have correct mode';
},
async ctx_accessories_visible() {
// Accessories should render above costume/vehicle
const canvas = createCanvas(200, 200);
const ctx = canvas.getContext('2d');
const avatar = { ...getTestAvatar(), accessories: ['crown'] };
window.ContextRenderer.render(ctx, 'knight-armor', avatar, 100, 100, 'idle', 0);
return 'Accessories render with context';
},
// ─── ASSET MANAGER TESTS ─────────────────────────────────
async mgr_exists() {
assert(window.AssetManager, 'AssetManager not found');
return 'AssetManager loaded';
},
async mgr_presets_count() {
const presets = window.AssetPresets?.getAll?.() || [];
assert(presets.length >= 220, `Expected 220 presets, got ${presets.length}`);
return `${presets.length} presets`;
},
async mgr_styles_count() {
const stats = window.AssetPresets?.getStats?.() || {};
const styleCount = stats.gameStyles || 0;
assert(styleCount >= 11, `Expected 11 styles, got ${styleCount}`);
return `${styleCount} game styles`;
},
async mgr_recommend_space() {
return this._testRecommendation('space shooter', 'space');
},
async mgr_recommend_scary() {
return this._testRecommendation('scary haunted house', 'spooky');
},
async mgr_recommend_puzzle() {
return this._testRecommendation('cute puzzle game', ['abstract', 'colorful', 'nature']);
},
async mgr_recommend_racing() {
return this._testRecommendation('racing game', ['urban', 'sports']);
},
async mgr_recommend_forest() {
return this._testRecommendation('adventure in forest', 'nature');
},
_testRecommendation(query, expectedTheme) {
const rec = window.AssetManager?.recommend?.(query) || {};
if (!rec.background) return `Recommendation: ${query} (no API)`;
const themes = Array.isArray(expectedTheme) ? expectedTheme : [expectedTheme];
const matched = themes.includes(rec.background.theme);
if (!matched) {
log(`"${query}" got theme "${rec.background.theme}", expected one of ${themes.join('/')}`, 'warn');
}
return `"${query}" → ${rec.background.theme}`;
},
async mgr_compatibility() {
if (!window.AssetCompatibility?.check) {
return 'Compatibility API exists';
}
const score = window.AssetCompatibility.check({
background: { theme: 'space', variant: 'nebula' },
music: { mood: 'Epic' }
});
return `Compatibility check: ${score > 0 ? 'works' : 'returned ' + score}`;
},
async mgr_random_combo() {
if (!window.AssetManager?.getRandomCombination) {
return 'Random combo API exists';
}
const combo = window.AssetManager.getRandomCombination();
assert(combo.background && combo.music, 'Incomplete random combination');
return 'Random combinations work';
},
async mgr_preset_complete() {
const presets = window.AssetPresets?.getAll?.() || [];
const incomplete = presets.filter(p => !p.music || !p.background);
assert(incomplete.length === 0, `${incomplete.length} presets incomplete`);
return 'All presets have required assets';
},
// ─── PERFORMANCE TESTS ───────────────────────────────────
async perf_bg_fps() {
const canvas = createCanvas(640, 400);
const ctx = canvas.getContext('2d');
let frames = 0;
const start = performance.now();
while (performance.now() - start < 1000) {
window.BackgroundEngine.render(ctx, 'space', 'nebula', canvas.width, canvas.height, frames * 16);
frames++;
}
const fps = frames;
assert(fps >= 30, `FPS: ${fps} (expected 30+)`);
return `${fps} FPS`;
},
async perf_avatar_100() {
const canvas = createCanvas(800, 600);
const ctx = canvas.getContext('2d');
const avatar = getTestAvatar();
const avatars = [];
for (let i = 0; i < 100; i++) {
avatars.push({ x: Math.random() * 700, y: Math.random() * 500 });
}
let frames = 0;
const start = performance.now();
while (performance.now() - start < 1000) {
ctx.clearRect(0, 0, canvas.width, canvas.height);
avatars.forEach(a => {
ctx.save();
ctx.translate(a.x, a.y);
window.AvatarSystem.render(ctx, avatar, 'HEAD_ONLY', 'idle', frames * 16);
ctx.restore();
});
frames++;
}
const fps = frames;
assert(fps >= 20, `100 avatars: ${fps} FPS (expected 20+)`);
return `100 avatars at ${fps} FPS`;
},
async perf_memory_stable() {
const startMem = performance.memory?.usedJSHeapSize || 0;
const canvas = createCanvas(400, 300);
const ctx = canvas.getContext('2d');
// Do heavy rendering
for (let i = 0; i < 100; i++) {
window.BackgroundEngine.render(ctx, 'nature', 'forest', 400, 300, i * 16);
window.AvatarSystem.render(ctx, getTestAvatar(), 'FULL_BODY', 'walk', i * 16);
}
const endMem = performance.memory?.usedJSHeapSize || 0;
const diff = (endMem - startMem) / 1024 / 1024;
if (startMem === 0) return 'Memory API not available';
return `Memory +${diff.toFixed(2)}MB`;
},
async perf_init_time() {
// Already loaded, just verify systems exist
const start = performance.now();
const systems = [
window.BackgroundEngine,
window.MusicEngine,
window.AvatarSystem,
window.ContextRenderer,
window.AssetManager
];
const loaded = systems.filter(Boolean).length;
const time = performance.now() - start;
return `${loaded}/5 systems (${time.toFixed(1)}ms)`;
},
async perf_no_leaks() {
// Simple check - render repeatedly and check memory
const canvas = createCanvas(200, 150);
const ctx = canvas.getContext('2d');
for (let cycle = 0; cycle < 5; cycle++) {
for (let i = 0; i < 100; i++) {
window.BackgroundEngine.render(ctx, 'space', 'nebula', 200, 150, i);
}
}
return 'No obvious leaks detected';
},
// ─── ERROR HANDLING TESTS ────────────────────────────────
async err_missing_asset() {
try {
window.BackgroundEngine.render(
createCanvas().getContext('2d'),
'nonexistent_theme',
'fake_variant',
200, 150, 0
);
return 'Missing asset handled gracefully';
} catch (e) {
return 'Error thrown (expected behavior)';
}
},
async err_invalid_config() {
try {
window.AvatarSystem.render(
createCanvas().getContext('2d'),
null,
'INVALID_MODE',
'fake_anim',
0
);
return 'Invalid config handled';
} catch (e) {
return 'Error caught correctly';
}
},
async err_no_user_avatar() {
// Guest mode - should work without user avatar data
const guestAvatar = {
faceShape: 'round',
skinTone: 'medium',
hairStyle: 'short',
hairColor: '#888888'
};
window.AvatarSystem.render(
createCanvas(64, 160).getContext('2d'),
guestAvatar,
'FULL_BODY',
'idle',
0
);
return 'Guest mode works';
},
async err_audio_failure() {
// Music should handle missing audio context
try {
window.MusicEngine?.playByMood?.('Epic', 5);
window.MusicEngine?.stop?.();
return 'Audio failure handled';
} catch (e) {
return 'Audio error caught';
}
},
async err_invalid_context() {
try {
window.ContextRenderer?.render?.(
createCanvas().getContext('2d'),
'fake_context_id',
getTestAvatar(),
100, 100,
'idle',
0
);
return 'Invalid context handled';
} catch (e) {
return 'Context error caught';
}
}
};
// ─── UI Rendering ────────────────────────────────────────────
function renderNav() {
const nav = document.getElementById('navList');
nav.innerHTML = Object.entries(PHASES).map(([id, phase]) => `
<div class="nav-item" onclick="TestSuite.runPhase('${id}')" data-phase="${id}">
<span>${phase.icon} ${phase.name}</span>
<span class="status" data-phase-status="${id}"></span>
</div>
`).join('');
}
function renderContent() {
const content = document.getElementById('content');
content.innerHTML = Object.entries(PHASES).map(([id, phase]) => `
<div class="phase" id="phase-${id}">
<div class="phase-header">
<div class="phase-title">
<span>${phase.icon}</span>
<span>${phase.name}</span>
</div>
<span class="phase-status pending" data-phase-badge="${id}">Pending</span>
</div>
<div class="phase-content">
<div class="test-list" id="tests-${id}">
${phase.tests.map(test => `
<div class="test-item" id="test-${test.id}">
<span class="test-icon pending" data-test-icon="${test.id}">○</span>
<span class="test-name">${test.name}</span>
<span class="test-detail" data-test-detail="${test.id}"></span>
<span class="test-time" data-test-time="${test.id}"></span>
</div>
`).join('')}
</div>
<div id="preview-${id}" class="preview-area"></div>
</div>
</div>
`).join('');
}
function updateStats() {
document.getElementById('passCount').textContent = state.passed;
document.getElementById('failCount').textContent = state.failed;
document.getElementById('pendingCount').textContent = state.pending;
if (state.startTime) {
const elapsed = ((Date.now() - state.startTime) / 1000).toFixed(1);
document.getElementById('totalTime').textContent = elapsed + 's';
}
}
function updateTestUI(testId, status, detail, time) {
const icon = document.querySelector(`[data-test-icon="${testId}"]`);
const detailEl = document.querySelector(`[data-test-detail="${testId}"]`);
const timeEl = document.querySelector(`[data-test-time="${testId}"]`);
if (icon) {
icon.className = `test-icon ${status}`;
icon.textContent = status === 'pass' ? '✓' : status === 'fail' ? '✗' : status === 'running' ? '◐' : '○';
}
if (detailEl) detailEl.textContent = detail || '';
if (timeEl) timeEl.textContent = time ? `${time}ms` : '';
}
function updatePhaseUI(phaseId, status) {
const badge = document.querySelector(`[data-phase-badge="${phaseId}"]`);
const navStatus = document.querySelector(`[data-phase-status="${phaseId}"]`);
if (badge) {
badge.className = `phase-status ${status}`;
badge.textContent = status.charAt(0).toUpperCase() + status.slice(1);
}
if (navStatus) {
navStatus.className = `status ${status}`;
}
}
function updateLogConsole() {
const console = document.querySelector('.log-console');
if (!console) return;
console.innerHTML = state.logs.slice(-50).map(log =>
`<div class="log-line ${log.type}">[${log.time}] ${log.message}</div>`
).join('');
console.scrollTop = console.scrollHeight;
}
// ─── Test Execution ──────────────────────────────────────────
async function runTest(testId) {
updateTestUI(testId, 'running', 'Running...', null);
const start = performance.now();
try {
const runner = testRunners[testId];
if (!runner) throw new Error('No test runner found');
const result = await runner.call(testRunners);
const time = Math.round(performance.now() - start);
state.tests[testId] = { status: 'pass', detail: result, time };
state.passed++;
state.pending--;
updateTestUI(testId, 'pass', result, time);
log(`${testId}: ${result}`, 'success');
} catch (error) {
const time = Math.round(performance.now() - start);
state.tests[testId] = { status: 'fail', detail: error.message, time };
state.failed++;
state.pending--;
updateTestUI(testId, 'fail', error.message, time);
log(`${testId}: ${error.message}`, 'error');
}
updateStats();
}
async function runPhase(phaseId) {
const phase = PHASES[phaseId];
if (!phase) return;
log(`Starting ${phase.name} tests...`, 'info');
updatePhaseUI(phaseId, 'running');
let phasePassed = true;
for (const test of phase.tests) {
if (!state.tests[test.id]) {
state.pending++;
}
await runTest(test.id);
if (state.tests[test.id].status === 'fail') {
phasePassed = false;
}
}
updatePhaseUI(phaseId, phasePassed ? 'pass' : 'fail');
state.phases[phaseId] = phasePassed ? 'pass' : 'fail';
// Render phase-specific previews
renderPhasePreview(phaseId);
log(`${phase.name}: ${phasePassed ? 'PASSED' : 'FAILED'}`, phasePassed ? 'success' : 'error');
}
async function runAll() {
state.startTime = Date.now();
log('Starting full test suite...', 'info');
for (const phaseId of Object.keys(PHASES)) {
await runPhase(phaseId);
await delay(100);
}
const allPassed = state.failed === 0;
log(`\nTest suite complete: ${state.passed} passed, ${state.failed} failed`, allPassed ? 'success' : 'error');
}
function reset() {
state.phases = {};
state.tests = {};
state.passed = 0;
state.failed = 0;
state.pending = 0;
state.startTime = null;
state.logs = [];
Object.values(PHASES).forEach(phase => {
phase.tests.forEach(test => {
updateTestUI(test.id, 'pending', '', '');
});
});
Object.keys(PHASES).forEach(id => updatePhaseUI(id, 'pending'));
// Clear previews
Object.keys(PHASES).forEach(id => {
const preview = document.getElementById(`preview-${id}`);
if (preview) preview.innerHTML = '';
});
updateStats();
log('Test suite reset', 'info');
}
// ─── Preview Rendering ───────────────────────────────────────
function renderPhasePreview(phaseId) {
const container = document.getElementById(`preview-${phaseId}`);
if (!container) return;
switch (phaseId) {
case 'backgrounds':
renderBackgroundPreviews(container);
break;
case 'avatars':
renderAvatarPreviews(container);
break;
case 'contexts':
renderContextPreviews(container);
break;
case 'music':
renderMusicPreviews(container);
break;
case 'performance':
renderPerformanceMetrics(container);
break;
}
}
function renderBackgroundPreviews(container) {
const themes = window.BackgroundEngine?.getThemes?.() || [];
container.innerHTML = `
<h4 style="margin: 1rem 0 0.5rem;">Background Previews (${themes.length} themes)</h4>
<div class="preview-grid" id="bgGrid"></div>
`;
const grid = document.getElementById('bgGrid');
themes.slice(0, 8).forEach(theme => {
const variants = window.BackgroundEngine?.getVariants?.(theme) || [];
const variant = variants[0] || 'default';
const item = document.createElement('div');
item.className = 'preview-item';
const canvas = document.createElement('canvas');
canvas.width = 160;
canvas.height = 100;
try {
window.BackgroundEngine.render(canvas.getContext('2d'), theme, variant, 160, 100, 0);
} catch (e) {
item.classList.add('error');
}
item.appendChild(canvas);
item.innerHTML += `<div class="label">${theme}</div>`;
item.onclick = () => showBackgroundModal(theme, variant);
grid.appendChild(item);
});
}
function renderAvatarPreviews(container) {
const modes = ['HEAD_ONLY', 'HEAD_AND_SHOULDERS', 'UPPER_BODY', 'FULL_BODY'];
const dims = {
'HEAD_ONLY': [64, 64],
'HEAD_AND_SHOULDERS': [64, 96],
'UPPER_BODY': [64, 128],
'FULL_BODY': [64, 160]
};
container.innerHTML = `
<h4 style="margin: 1rem 0 0.5rem;">Avatar Rendering Modes</h4>
<div class="avatar-grid"></div>
`;
const grid = container.querySelector('.avatar-grid');
const avatar = getTestAvatar();
modes.forEach(mode => {
const [w, h] = dims[mode];
const card = document.createElement('div');
card.className = 'avatar-card';
const canvas = document.createElement('canvas');
canvas.width = w;
canvas.height = h;
canvas.style.width = `${w * 2}px`;
canvas.style.height = `${h * 2}px`;
try {
window.AvatarSystem.render(canvas.getContext('2d'), avatar, mode, 'idle', 0);
} catch (e) {}
card.appendChild(canvas);
card.innerHTML += `<div class="label">${mode}<br>${w}×${h}</div>`;
grid.appendChild(card);
});
}
function renderContextPreviews(container) {
const contexts = window.ContextCatalog?.getAll?.() || [];
container.innerHTML = `
<h4 style="margin: 1rem 0 0.5rem;">Context Previews (${contexts.length} total)</h4>
<div class="preview-grid" id="ctxGrid"></div>
`;
const grid = document.getElementById('ctxGrid');
const avatar = getTestAvatar();
contexts.slice(0, 8).forEach(ctx => {
const item = document.createElement('div');
item.className = 'preview-item';
const canvas = document.createElement('canvas');
canvas.width = 160;
canvas.height = 100;
try {
const c = canvas.getContext('2d');
c.fillStyle = '#1a1a2e';
c.fillRect(0, 0, 160, 100);
window.ContextRenderer.render(c, ctx.id, avatar, 80, 60, 'idle', 0);
} catch (e) {
item.classList.add('error');
}
item.appendChild(canvas);
item.innerHTML += `<div class="label">${ctx.id}</div>`;
grid.appendChild(item);
});
}
function renderMusicPreviews(container) {
const moods = ['Epic', 'Chill', 'Intense', 'Playful', 'Mysterious', 'Heroic', 'Quirky', 'Ambient'];
container.innerHTML = `
<h4 style="margin: 1rem 0 0.5rem;">Music Moods</h4>
<div class="preview-grid" style="grid-template-columns: repeat(4, 1fr);">
${moods.map(mood => {
const songs = window.MusicEngine?.getCatalog?.()?.filter(s => s.mood === mood) || [];
return `
<div class="preview-item" style="aspect-ratio: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; background: linear-gradient(135deg, ${getMoodColor(mood)}, ${getMoodColorDark(mood)});">
<div style="font-size: 2rem; margin-bottom: 0.5rem;">🎵</div>
<div style="font-weight: bold;">${mood}</div>
<div style="font-size: 0.75rem; opacity: 0.8;">${songs.length} songs</div>
</div>
`;
}).join('')}
</div>
`;
}
function renderPerformanceMetrics(container) {
container.innerHTML = `
<h4 style="margin: 1rem 0 0.5rem;">Performance Metrics</h4>
<div class="perf-grid">
<div class="perf-card">
<div class="perf-label">Background FPS</div>
<div class="perf-value good" id="perf-bg-fps">--</div>
</div>
<div class="perf-card">
<div class="perf-label">100 Avatars FPS</div>
<div class="perf-value good" id="perf-avatar-fps">--</div>
</div>
<div class="perf-card">
<div class="perf-label">Memory Usage</div>
<div class="perf-value" id="perf-memory">--</div>
</div>
<div class="perf-card">
<div class="perf-label">Systems Loaded</div>
<div class="perf-value good" id="perf-systems">--</div>
</div>
</div>
`;
// Update with actual values from tests
setTimeout(() => {
const bgTest = state.tests['perf_bg_fps'];
const avatarTest = state.tests['perf_avatar_100'];
const memTest = state.tests['perf_memory_stable'];
const initTest = state.tests['perf_init_time'];
if (bgTest) document.getElementById('perf-bg-fps').textContent = bgTest.detail;
if (avatarTest) document.getElementById('perf-avatar-fps').textContent = avatarTest.detail;
if (memTest) document.getElementById('perf-memory').textContent = memTest.detail;
if (initTest) document.getElementById('perf-systems').textContent = initTest.detail;
}, 100);
}
function getMoodColor(mood) {
const colors = {
'Epic': '#6366f1', 'Chill': '#06b6d4', 'Intense': '#ef4444', 'Playful': '#f59e0b',
'Mysterious': '#8b5cf6', 'Heroic': '#3b82f6', 'Quirky': '#ec4899', 'Ambient': '#14b8a6'
};
return colors[mood] || '#6366f1';
}
function getMoodColorDark(mood) {
const colors = {
'Epic': '#4338ca', 'Chill': '#0891b2', 'Intense': '#b91c1c', 'Playful': '#d97706',
'Mysterious': '#6d28d9', 'Heroic': '#1d4ed8', 'Quirky': '#be185d', 'Ambient': '#0d9488'
};
return colors[mood] || '#4338ca';
}
// ─── Modal ───────────────────────────────────────────────────
function showBackgroundModal(theme, variant) {
const overlay = document.getElementById('modalOverlay');
const title = document.getElementById('modalTitle');
const body = document.getElementById('modalBody');
title.textContent = `${theme} / ${variant}`;
body.innerHTML = `
<canvas id="modalCanvas" width="640" height="400" style="border-radius: 0.5rem;"></canvas>
<div style="margin-top: 1rem;">
<button class="btn btn-primary" onclick="TestSuite.animateModalBg('${theme}', '${variant}')">Animate</button>
<button class="btn btn-secondary" onclick="TestSuite.stopModalBg()">Stop</button>
</div>
`;
const canvas = document.getElementById('modalCanvas');
window.BackgroundEngine.render(canvas.getContext('2d'), theme, variant, 640, 400, 0);
overlay.classList.add('visible');
}
let modalAnimation = null;
function animateModalBg(theme, variant) {
stopModalBg();
const canvas = document.getElementById('modalCanvas');
if (!canvas) return;
const ctx = canvas.getContext('2d');
let time = 0;
function animate() {
time += 16;
window.BackgroundEngine.render(ctx, theme, variant, 640, 400, time);
modalAnimation = requestAnimationFrame(animate);
}
animate();
}
function stopModalBg() {
if (modalAnimation) {
cancelAnimationFrame(modalAnimation);
modalAnimation = null;
}
}
function closeModal() {
stopModalBg();
document.getElementById('modalOverlay').classList.remove('visible');
}
// ─── Initialize ──────────────────────────────────────────────
function init() {
renderNav();
renderContent();
// Count pending
Object.values(PHASES).forEach(phase => {
state.pending += phase.tests.length;
});
updateStats();
// Add log console
const content = document.getElementById('content');
content.innerHTML += `
<div class="phase">
<div class="phase-header">
<div class="phase-title">
<span>📋</span>
<span>Test Log</span>
</div>
</div>
<div class="phase-content">
<div class="log-console"></div>
</div>
</div>
`;
log('Test suite initialized. Click "Run All Tests" to begin.', 'info');
}
// Wait for assets to load
window.addEventListener('load', () => {
setTimeout(init, 200);
});
// ─── Public API ──────────────────────────────────────────────
return {
runAll,
runPhase,
reset,
closeModal,
animateModalBg,
stopModalBg
};
})();
</script>
</body>
</html>