Commit 2f9d7d84 authored by Mahmoud Aglan's avatar Mahmoud Aglan

test: add chess sync verification test with move+resign proof

Screenshots confirm: moves sync to opponent board, resign shows
victory screen. Both directions verified via Puppeteer + API.
Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent b3109e7a
import puppeteer from 'puppeteer';
import { mkdir } from 'fs/promises';
const BASE_URL = 'https://el3ab-player.caprover.al-arcade.com';
const AUTH_URL = 'https://safe-supabase-kong.caprover.al-arcade.com/auth/v1';
const API_URL = `${BASE_URL}/api`;
const ANON_KEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlIiwiaWF0IjoxNzM1Njg5NjAwLCJleHAiOjE4OTM0NTYwMDB9.31PF6PvP-pSrvRuQwLFptQoejR0W1A7o53lZhEbnz84';
const PLAYER1 = { email: 'testplayer1@el3ab.com', password: 'ChessTest1!', id: 'd14a27c6-2448-4319-a775-a9f94dd6de87', name: 'testplayer1', displayName: 'لاعب اختبار' };
const PLAYER2 = { email: 'test@el3ab.com', password: 'ChessTest2!', id: '37f947c0-e10b-4bef-88ec-c0aa0e8b8034', name: 'TestPlayer', displayName: 'TestPlayer' };
const wait = ms => new Promise(r => setTimeout(r, ms));
const SHOT_DIR = '/Users/mahmoudaglan/el3ab-player/screenshots/chess-sync';
async function getToken(email, password) {
const res = await fetch(`${AUTH_URL}/token?grant_type=password`, {
method: 'POST',
headers: { 'apikey': ANON_KEY, 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password })
});
const data = await res.json();
if (!data.access_token) throw new Error(`Login failed: ${JSON.stringify(data)}`);
return { token: data.access_token, refreshToken: data.refresh_token };
}
async function apiCall(token, endpoint, body) {
const res = await fetch(`${API_URL}/${endpoint}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` },
body: JSON.stringify(body)
});
return res.json();
}
async function setupPage(browser, player, auth) {
const page = await browser.newPage();
await page.setViewport({ width: 390, height: 844, deviceScaleFactor: 2 });
page.on('console', msg => {
const txt = msg.text();
if (msg.type() === 'error' && !txt.includes('favicon') && !txt.includes('ERR_'))
console.log(` [${player.name} ERR] ${txt.slice(0, 150)}`);
});
await page.goto(BASE_URL, { waitUntil: 'networkidle2', timeout: 30000 });
await page.evaluate((token, refreshToken, userId, displayName, username) => {
localStorage.setItem('el3ab_state', JSON.stringify({
auth: { token, refreshToken, userId },
player: { id: userId, display_name: displayName, username, level: 5, coins: 1000, xp: 500 },
activeWorld: 'play', audioEnabled: false, language: 'ar'
}));
}, auth.token, auth.refreshToken, player.id, player.displayName, player.name);
await page.reload({ waitUntil: 'networkidle2', timeout: 30000 });
await wait(2000);
return page;
}
async function navigateToQueue(page, playerName) {
console.log(` [${playerName}] Clicking chess tile...`);
await page.evaluate(() => document.querySelector('[data-game="chess"]').click());
await wait(1000);
console.log(` [${playerName}] Clicking أونلاين...`);
await page.evaluate(() => document.querySelector('#btn-multi').click());
await wait(1000);
console.log(` [${playerName}] Clicking time control...`);
await page.evaluate(() => {
const allEls = document.querySelectorAll('button, [class*="btn"], [role="button"]');
for (const el of allEls) {
const t = el.textContent.trim();
if (t.includes('10+0') || t.includes('10 + 0') || t.includes('سريعة') || t.includes('Rapid') || t.includes('10')) {
el.click(); return t;
}
}
const scene = document.querySelector('.scene');
if (scene) {
const btns = scene.querySelectorAll('button, [style*="cursor: pointer"]');
for (const btn of btns) { if (btn.id !== 'menu-close') { btn.click(); return btn.textContent.slice(0,30); } }
}
return null;
});
await wait(500);
}
async function run() {
await mkdir(SHOT_DIR, { recursive: true });
console.log('=== Chess SYNC Test (Move Verification) ===\n');
// 1. Auth
console.log('1. Authenticating...');
const p1Auth = await getToken(PLAYER1.email, PLAYER1.password);
const p2Auth = await getToken(PLAYER2.email, PLAYER2.password);
console.log(' ✓ Both authenticated');
// 2. Clean any existing queue/matches
console.log('\n2. Cleaning queue...');
await apiCall(p1Auth.token, 'matchmaking.php', { action: 'dequeue' });
await apiCall(p2Auth.token, 'matchmaking.php', { action: 'dequeue' });
// 3. Launch browser
console.log('\n3. Launching browser...');
const browser = await puppeteer.launch({
headless: true,
args: ['--no-sandbox', '--disable-setuid-sandbox', '--ignore-certificate-errors']
});
const page1 = await setupPage(browser, PLAYER1, p1Auth);
const page2 = await setupPage(browser, PLAYER2, p2Auth);
console.log(' ✓ Both pages loaded');
// 4. Navigate to queue
console.log('\n4. Player 1 → queue...');
await navigateToQueue(page1, PLAYER1.name);
console.log(' Player 2 → queue...');
await navigateToQueue(page2, PLAYER2.name);
// 5. Wait for match
console.log('\n5. Waiting for match (10s)...');
await wait(10000);
// Verify both in game
const p1Board = await page1.$('canvas');
const p2Board = await page2.$('canvas');
if (!p1Board || !p2Board) {
console.log(' ✗ NOT BOTH IN GAME');
const p1Html = await page1.evaluate(() => document.body.innerText.slice(0, 200));
const p2Html = await page2.evaluate(() => document.body.innerText.slice(0, 200));
console.log(` P1: ${p1Html.slice(0,100)}`);
console.log(` P2: ${p2Html.slice(0,100)}`);
await browser.close();
return;
}
console.log(' ✓ Both players in game with canvas boards');
await page1.screenshot({ path: `${SHOT_DIR}/01-start-p1.png` });
await page2.screenshot({ path: `${SHOT_DIR}/01-start-p2.png` });
// 6. Figure out who is white
// The move list at start shows just "1." — after a move it shows "1. e4"
// Also check the opponent name to identify match_id
const matchInfo = await page1.evaluate(() => {
const state = JSON.parse(localStorage.getItem('el3ab_state') || '{}');
const activeMatch = localStorage.getItem('el3ab_active_match');
return { state: state?.auth?.userId, activeMatch };
});
console.log(` P1 active match: ${matchInfo.activeMatch}`);
const matchInfo2 = await page2.evaluate(() => {
const activeMatch = localStorage.getItem('el3ab_active_match');
return { activeMatch };
});
console.log(` P2 active match: ${matchInfo2.activeMatch}`);
// Get the match ID from localStorage
let matchId = null;
try {
const parsed = JSON.parse(matchInfo.activeMatch || '{}');
matchId = parsed.matchId;
} catch(e) {}
if (!matchId) {
try {
const parsed = JSON.parse(matchInfo2.activeMatch || '{}');
matchId = parsed.matchId;
} catch(e) {}
}
console.log(` Match ID: ${matchId}`);
if (!matchId) {
console.log(' ✗ Could not find match ID');
await browser.close();
return;
}
// 7. Get match data to determine who is white
console.log('\n6. Fetching match to determine colors...');
const matchData = await apiCall(p1Auth.token, 'game.php', { action: 'get', match_id: matchId });
console.log(` White: ${matchData.white_player_id?.slice(0,8)}, Black: ${matchData.black_player_id?.slice(0,8)}`);
console.log(` P1 ID: ${PLAYER1.id.slice(0,8)}, P2 ID: ${PLAYER2.id.slice(0,8)}`);
console.log(` Current FEN: ${matchData.current_fen?.slice(0,30)}`);
console.log(` Move count: ${matchData.move_count}`);
const p1IsWhite = matchData.white_player_id === PLAYER1.id;
const whiteAuth = p1IsWhite ? p1Auth : p2Auth;
const blackAuth = p1IsWhite ? p2Auth : p1Auth;
const whitePage = p1IsWhite ? page1 : page2;
const blackPage = p1IsWhite ? page2 : page1;
const whiteName = p1IsWhite ? PLAYER1.name : PLAYER2.name;
const blackName = p1IsWhite ? PLAYER2.name : PLAYER1.name;
console.log(` White player: ${whiteName}, Black player: ${blackName}`);
// 8. Make a move as white via API (e2→e4)
console.log('\n7. Making move e4 via API as white...');
const moveResult = await apiCall(whiteAuth.token, 'game.php', {
action: 'move',
match_id: matchId,
fen: 'rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1',
move: JSON.stringify([{ from: 'e2', to: 'e4', san: 'e4', color: 'w' }]),
move_count: 1,
white_time_remaining_ms: 598000,
black_time_remaining_ms: 600000,
game_state: JSON.stringify({ last_move: { from: 'e2', to: 'e4', san: 'e4' } })
});
console.log(` API response: ${JSON.stringify(moveResult)}`);
if (moveResult.error) {
console.log(` ✗ MOVE FAILED: ${moveResult.error}`);
await browser.close();
return;
}
console.log(' ✓ Move sent to server');
// Verify move is in DB
const verifyMatch = await apiCall(blackAuth.token, 'game.php', { action: 'get', match_id: matchId });
console.log(` DB verify: move_count=${verifyMatch.move_count}, fen=${verifyMatch.current_fen?.slice(0,40)}`);
// 9. Wait for black's poll to pick it up (polls every 2s, wait 6s to be safe)
console.log('\n8. Waiting 6s for poll cycle...');
await wait(6000);
// 10. Check if black's board updated
console.log('\n9. Checking if black received the move...');
// Check move list on black's page
const blackMoveList = await blackPage.evaluate(() => {
const ml = document.querySelector('#move-list');
return ml ? ml.textContent.trim() : 'NOT FOUND';
});
console.log(` Black's move list: "${blackMoveList}"`);
// Check if clock switched (black's clock should now be active if move synced)
const blackClocks = await blackPage.evaluate(() => {
const player = document.querySelector('#clock-player');
const opponent = document.querySelector('#clock-opponent');
return {
playerText: player?.textContent,
opponentText: opponent?.textContent,
playerActive: player?.classList.contains('active'),
opponentActive: opponent?.classList.contains('active')
};
});
console.log(` Black's clocks: player=${blackClocks.playerText} (active:${blackClocks.playerActive}), opponent=${blackClocks.opponentText} (active:${blackClocks.opponentActive})`);
// Also check white's page to see their state
const whiteMoveList = await whitePage.evaluate(() => {
const ml = document.querySelector('#move-list');
return ml ? ml.textContent.trim() : 'NOT FOUND';
});
console.log(` White's move list: "${whiteMoveList}"`);
// Take screenshots
await whitePage.screenshot({ path: `${SHOT_DIR}/02-after-move-white.png` });
await blackPage.screenshot({ path: `${SHOT_DIR}/02-after-move-black.png` });
// 11. Determine if sync worked
const moveAppeared = blackMoveList.includes('e4') || blackMoveList.includes('1.');
const hasNewContent = blackMoveList !== '1.';
if (hasNewContent && blackMoveList.includes('e4')) {
console.log('\n ✓✓✓ MOVE SYNCED! Black sees e4 in move list!');
} else {
console.log(`\n ✗ MOVE DID NOT SYNC to black's UI`);
console.log(' Debugging...');
// Check console logs for errors
const blackErrors = await blackPage.evaluate(() => {
return window.__consoleErrors || [];
});
console.log(` Black console errors: ${JSON.stringify(blackErrors).slice(0, 300)}`);
// Check if polling is happening — look at network activity
const blackNetState = await blackPage.evaluate(() => {
const activeMatch = localStorage.getItem('el3ab_active_match');
return { activeMatch };
});
console.log(` Black's active match in localStorage: ${blackNetState.activeMatch}`);
// Try to intercept a poll response
console.log(' Intercepting next poll response...');
const pollResponse = await new Promise(async (resolve) => {
const timeout = setTimeout(() => resolve('TIMEOUT - no poll detected in 5s'), 5000);
blackPage.on('response', async (response) => {
const url = response.url();
if (url.includes('game.php')) {
clearTimeout(timeout);
try {
const body = await response.json();
resolve(JSON.stringify(body).slice(0, 300));
} catch(e) {
resolve(`parse error: ${e.message}`);
}
}
});
});
console.log(` Poll response: ${pollResponse}`);
}
// 12. Now test resign sync
console.log('\n10. Testing resign sync...');
const resignResult = await apiCall(blackAuth.token, 'game.php', { action: 'resign', match_id: matchId });
console.log(` Resign response: ${JSON.stringify(resignResult)}`);
await wait(4000);
// Check white's page for game-over state
const whiteAfterResign = await whitePage.evaluate(() => {
// Check if result screen appeared or if there's a game-over indicator
const body = document.body.innerText.slice(0, 300);
const hasResult = body.includes('فوز') || body.includes('خسارة') || body.includes('win') || body.includes('نتيجة');
return { hasResult, text: body.slice(0, 150) };
});
console.log(` White sees result: ${whiteAfterResign.hasResult}`);
console.log(` White page text: ${whiteAfterResign.text.slice(0, 100)}`);
await whitePage.screenshot({ path: `${SHOT_DIR}/03-after-resign-white.png` });
await blackPage.screenshot({ path: `${SHOT_DIR}/03-after-resign-black.png` });
await browser.close();
console.log(`\n Screenshots saved to: ${SHOT_DIR}/`);
console.log('\n=== Done ===');
}
run().catch(err => {
console.error('\n✗ FATAL:', err.message);
process.exit(1);
});
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment