1983 lines
72 KiB
HTML
1983 lines
72 KiB
HTML
<!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()">×</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>
|