ai-code-battle/web/public/test-replay-viewer-real.html
jedarden 3ae35ea00a test(web): verify match list page renders cards with real matches
- Verified /watch/replays shows real completed matches (not just demo)
- Match cards display bot names, turn count, winner badges, map ID
- 'Watch Replay' links point to real match IDs (m_test_*)
- Curated playlists render with real data (featured, comebacks, upsets, etc.)
- Pagination/infinite scroll works via IntersectionObserver
- Mobile testing on Pixel 6 via ADB: layout responsive, touch targets usable
- Created MATCH_LIST_TEST_RESULTS.md with full verification details
- Thumbnails not implemented (clean UI without broken images due to R2 issues)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-25 11:42:47 -04:00

236 lines
9.7 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Replay Viewer Test - Real Replay</title>
<style>
body { margin: 0; padding: 20px; font-family: sans-serif; background: #0f172a; color: #e2e8f0; }
h1 { font-size: 1.2rem; margin-bottom: 10px; }
.test-container { display: flex; gap: 20px; }
.viewer-section { flex: 1; }
.results-section { width: 350px; background: #1e293b; padding: 15px; border-radius: 8px; max-height: 90vh; overflow-y: auto; }
.canvas-wrapper { background: #1e293b; border-radius: 8px; padding: 10px; }
canvas { display: block; }
.test-result { padding: 8px; margin: 5px 0; border-radius: 4px; font-size: 0.9rem; }
.pass { background: #22c55e; color: white; }
.fail { background: #ef4444; color: white; }
.info { background: #3b82f6; color: white; }
.warn { background: #f59e0b; color: white; }
.controls { margin-top: 15px; }
button { padding: 8px 16px; margin-right: 5px; background: #3b82f6; color: white; border: none; border-radius: 4px; cursor: pointer; }
button:hover { background: #2563eb; }
button:disabled { background: #475569; cursor: not-allowed; }
.replay-info { background: #334155; padding: 10px; border-radius: 4px; margin: 10px 0; font-size: 0.85rem; }
.replay-info pre { margin: 0; white-space: pre-wrap; }
</style>
</head>
<body>
<h1>Replay Viewer Test - Real Replay (m_tprjf4ij)</h1>
<div class="test-container">
<div class="viewer-section">
<div class="canvas-wrapper">
<canvas id="replay-canvas"></canvas>
</div>
<div class="controls">
<button id="play-btn">Play</button>
<button id="pause-btn">Pause</button>
<button id="next-btn">Next Turn</button>
<button id="reset-btn">Reset</button>
<span id="turn-info">Turn: 0</span>
</div>
<div class="replay-info">
<strong>Match Info:</strong>
<div id="match-details">Loading...</div>
</div>
</div>
<div class="results-section">
<h2>Test Results</h2>
<div id="test-results"></div>
</div>
</div>
<script type="module">
import { ReplayViewer } from './src/replay-viewer.ts';
const results = document.getElementById('test-results');
const canvas = document.getElementById('replay-canvas');
const turnInfo = document.getElementById('turn-info');
const playBtn = document.getElementById('play-btn');
const pauseBtn = document.getElementById('pause-btn');
const nextBtn = document.getElementById('next-btn');
const resetBtn = document.getElementById('reset-btn');
const matchDetails = document.getElementById('match-details');
let viewer;
let testsPassed = 0;
let testsFailed = 0;
let testsWarned = 0;
function addResult(name, passed, message) {
const div = document.createElement('div');
const status = passed ? 'pass' : 'fail';
div.className = `test-result ${status}`;
div.textContent = `${passed ? '✓' : '✗'} ${name}: ${message}`;
results.appendChild(div);
if (passed) testsPassed++;
else testsFailed++;
}
function addWarn(name, message) {
const div = document.createElement('div');
div.className = 'test-result warn';
div.textContent = `${name}: ${message}`;
results.appendChild(div);
testsWarned++;
}
function addInfo(message) {
const div = document.createElement('div');
div.className = 'test-result info';
div.textContent = message;
results.appendChild(div);
}
async function runTests() {
addInfo('Loading real replay from /data/real-replay.json...');
const startTime = performance.now();
try {
const response = await fetch('/data/real-replay.json');
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const replay = await response.json();
const loadTime = performance.now() - startTime;
addResult('Fetch real replay', true, `Loaded ${replay.match_id} (${(loadTime/1000).toFixed(2)}s)`);
// Test replay structure
addResult('Replay has match_id', !!replay.match_id, replay.match_id);
addResult('Replay has format_version', !!replay.format_version, replay.format_version);
addResult('Replay has players', Array.isArray(replay.players) && replay.players.length > 0, `${replay.players.length} players`);
addResult('Replay has turns', Array.isArray(replay.turns) && replay.turns.length > 0, `${replay.turns.length} turns`);
addResult('Replay has map', !!replay.map, `${replay.map.rows}x${replay.map.cols}`);
addResult('Replay has result', !!replay.result, `Winner: P${replay.result.winner}, Reason: ${replay.result.reason}`);
// Display match info
matchDetails.innerHTML = `<pre>{
Match ID: ${replay.match_id}
Players: ${replay.players.map(p => p.name).join(', ')}
Turns: ${replay.result.turns}
Winner: Player ${replay.result.winner}
Map: ${replay.map.rows}x${replay.map.cols}
Walls: ${replay.map.walls?.length || 0}
}</pre>`;
// Test map structure
if (replay.map) {
addResult('Map has valid dimensions', replay.map.rows > 0 && replay.map.cols > 0, `${replay.map.rows}x${replay.map.cols}`);
addResult('Map has walls array', Array.isArray(replay.map.walls), `${replay.map.walls?.length || 0} walls`);
addResult('Map has cores', Array.isArray(replay.map.cores), `${replay.map.cores?.length || 0} cores`);
}
// Test turn data structure
if (replay.turns && replay.turns.length > 0) {
const firstTurn = replay.turns[0];
const lastTurn = replay.turns[replay.turns.length - 1];
addResult('First turn has bots', Array.isArray(firstTurn.bots), `${firstTurn.bots?.length || 0} bots`);
addResult('First turn has energy', Array.isArray(firstTurn.energy), `${firstTurn.energy?.length || 0} energy nodes`);
addResult('Last turn has bots', Array.isArray(lastTurn.bots), `${lastTurn.bots?.length || 0} bots`);
addResult('Turns have events', replay.turns.every(t => Array.isArray(t.events)), 'All turns have events array');
}
// Test win probability
if (replay.win_prob && replay.win_prob.length > 0) {
addResult('Replay has win probability', true, `${replay.win_prob.length} data points`);
} else {
addWarn('Win probability', 'No win_prob data in replay');
}
// Test critical moments
if (replay.critical_moments && replay.critical_moments.length > 0) {
addResult('Replay has critical moments', true, `${replay.critical_moments.length} moments`);
} else {
addWarn('Critical moments', 'No critical_moments data in replay');
}
// Create viewer
addInfo('Creating ReplayViewer...');
viewer = new ReplayViewer(canvas, { cellSize: 10 });
addResult('Create ReplayViewer', true, 'Viewer instance created');
// Load replay
addInfo('Loading replay into viewer...');
viewer.loadReplay(replay);
addResult('Load replay into viewer', true, `Loaded ${replay.turns.length} turns`);
// Test viewer state
addResult('Get total turns', viewer.getTotalTurns() === replay.turns.length, `${viewer.getTotalTurns()} turns`);
addResult('Get current turn', viewer.getTurn() === 0, `Turn ${viewer.getTurn()}`);
// Test turn navigation
addInfo('Testing turn navigation...');
viewer.setTurn(10);
addResult('Set turn to 10', viewer.getTurn() === 10, `Turn ${viewer.getTurn()}`);
viewer.setTurn(0);
addResult('Reset to turn 0', viewer.getTurn() === 0, `Turn ${viewer.getTurn()}`);
// Test playback
addInfo('Testing playback controls...');
viewer.play();
addResult('Start playback', viewer.getIsPlaying(), 'Playing');
await new Promise(r => setTimeout(r, 200));
viewer.pause();
addResult('Pause playback', !viewer.getIsPlaying(), 'Paused');
// Test events
const events = viewer.getTurnEvents();
addResult('Get turn events', Array.isArray(events), `${events.length} events at turn 0`);
// Test transcript generation
const transcript = viewer.generateTranscript();
addResult('Generate transcript', Array.isArray(transcript) && transcript.length > 0, `${transcript.length} entries`);
// Test cell size adjustment
addInfo('Testing rendering...');
viewer.setTurn(Math.floor(replay.turns.length / 2));
await new Promise(r => setTimeout(r, 100));
addResult('Render mid-game turn', true, `Turn ${viewer.getTurn()} rendered`);
addInfo(`Tests completed: ${testsPassed} passed, ${testsWarned} warnings, ${testsFailed} failed`);
// Enable controls
playBtn.disabled = false;
pauseBtn.disabled = false;
nextBtn.disabled = false;
resetBtn.disabled = false;
// Set up turn change callback
viewer.onTurnChange = (turn) => {
turnInfo.textContent = `Turn: ${turn}/${viewer.getTotalTurns() - 1}`;
};
} catch (error) {
addResult('Test execution', false, error.message);
console.error('Test error:', error);
}
}
// Wire up controls
playBtn.addEventListener('click', () => viewer.play());
pauseBtn.addEventListener('click', () => viewer.pause());
nextBtn.addEventListener('click', () => {
if (viewer && viewer.getTurn() < viewer.getTotalTurns() - 1) {
viewer.setTurn(viewer.getTurn() + 1);
}
});
resetBtn.addEventListener('click', () => {
if (viewer) {
viewer.pause();
viewer.setTurn(0);
}
});
// Run tests
runTests();
</script>
</body>
</html>