tetris.spec.ts (16027B)
1 import { test, expect, type Page } from "@playwright/test"; 2 3 // Try common entry points until one loads successfully. 4 async function loadGame(page: Page) { 5 const candidates = [ 6 "/", 7 "/index.html", 8 "/dist/index.html", 9 "/public/index.html", 10 "/build/index.html", 11 ]; 12 13 for (const path of candidates) { 14 try { 15 const resp = await page.goto(path, { timeout: 5000 }); 16 if (resp?.ok()) return; 17 } catch { 18 continue; 19 } 20 } 21 } 22 23 // Find the game surface: canvas or a grid-like DOM container. 24 function gameBoard(page: Page) { 25 return page.locator( 26 [ 27 "canvas", 28 '[class*="board"]', 29 '[class*="grid"]', 30 '[class*="game-area"]', 31 '[class*="field"]', 32 '[id*="board"]', 33 '[id*="grid"]', 34 '[id*="game"]', 35 '[id*="field"]', 36 "table", 37 ].join(", ") 38 ); 39 } 40 41 // Click the board area to make sure it has focus, then try common 42 // start interactions in case the game waits for user action. 43 async function ensureGameStarted(page: Page) { 44 const board = gameBoard(page); 45 const count = await board.count(); 46 if (count > 0) { 47 try { 48 await board.first().click({ timeout: 2000 }); 49 } catch { 50 // click failed, continue anyway 51 } 52 } 53 54 // Some games need a key press or button click to start 55 const startButton = page.locator( 56 'button:has-text("start"), button:has-text("Start"), button:has-text("play"), button:has-text("Play"), [class*="start"], [id*="start"]' 57 ); 58 if ((await startButton.count()) > 0) { 59 try { 60 await startButton.first().click({ timeout: 2000 }); 61 } catch { 62 // ignore 63 } 64 } 65 66 // Press Enter/Space as a fallback start trigger 67 await page.keyboard.press("Enter"); 68 await page.waitForTimeout(300); 69 await page.keyboard.press("Space"); 70 await page.waitForTimeout(500); 71 } 72 73 test.describe("Tetris Game", () => { 74 test.beforeEach(async ({ page }) => { 75 await loadGame(page); 76 await page.waitForLoadState("domcontentloaded"); 77 await page.waitForTimeout(1000); 78 await ensureGameStarted(page); 79 }); 80 81 // ---- 1. Page loads without errors ---- 82 test("page loads without console errors", async ({ page }) => { 83 const errors: string[] = []; 84 page.on("pageerror", (err) => errors.push(err.message)); 85 await page.waitForTimeout(2000); 86 expect(errors).toEqual([]); 87 }); 88 89 // ---- 2. Game board is visible ---- 90 test("game board is visible", async ({ page }) => { 91 const board = gameBoard(page); 92 const count = await board.count(); 93 94 expect( 95 count, 96 "Expected a <canvas> or a container with board/grid/game/field in its class or id" 97 ).toBeGreaterThan(0); 98 99 // The board element should have meaningful dimensions 100 const box = await board.first().boundingBox(); 101 expect(box, "Game board should be visible on screen").not.toBeNull(); 102 expect(box!.width).toBeGreaterThan(50); 103 expect(box!.height).toBeGreaterThan(50); 104 }); 105 106 // ---- 3. Game starts automatically or via interaction ---- 107 test("game starts", async ({ page }) => { 108 // After beforeEach, the game should be running. Verify by checking that 109 // the page is not static: take two screenshots separated by time. 110 const shot1 = await page.screenshot(); 111 await page.waitForTimeout(2500); 112 const shot2 = await page.screenshot(); 113 114 expect( 115 Buffer.from(shot1).equals(Buffer.from(shot2)), 116 "Expected the game to show visual activity after starting" 117 ).toBe(false); 118 }); 119 120 // ---- 4. Piece falls automatically (auto-drop) ---- 121 test("piece falls automatically", async ({ page }) => { 122 // Take screenshots at intervals without pressing any keys. 123 // A falling piece should cause visual changes. 124 const shot1 = await page.screenshot(); 125 await page.waitForTimeout(2000); 126 const shot2 = await page.screenshot(); 127 await page.waitForTimeout(2000); 128 const shot3 = await page.screenshot(); 129 130 const buf1 = Buffer.from(shot1); 131 const buf2 = Buffer.from(shot2); 132 const buf3 = Buffer.from(shot3); 133 134 // At least one pair should differ (piece is moving down) 135 const changed = !buf1.equals(buf2) || !buf2.equals(buf3); 136 expect(changed, "Expected piece to fall over time without input").toBe( 137 true 138 ); 139 }); 140 141 // ---- 5. Left arrow moves piece left ---- 142 test("left arrow moves piece", async ({ page }) => { 143 const errors: string[] = []; 144 page.on("pageerror", (err) => errors.push(err.message)); 145 146 const shot1 = await page.screenshot(); 147 await page.keyboard.press("ArrowLeft"); 148 await page.waitForTimeout(200); 149 await page.keyboard.press("ArrowLeft"); 150 await page.waitForTimeout(200); 151 await page.keyboard.press("ArrowLeft"); 152 await page.waitForTimeout(300); 153 const shot2 = await page.screenshot(); 154 155 // The piece should have moved, so the screenshots should differ 156 expect( 157 Buffer.from(shot1).equals(Buffer.from(shot2)), 158 "Expected visual change after pressing left arrow" 159 ).toBe(false); 160 expect(errors).toEqual([]); 161 }); 162 163 // ---- 6. Right arrow moves piece right ---- 164 test("right arrow moves piece", async ({ page }) => { 165 const errors: string[] = []; 166 page.on("pageerror", (err) => errors.push(err.message)); 167 168 const shot1 = await page.screenshot(); 169 await page.keyboard.press("ArrowRight"); 170 await page.waitForTimeout(200); 171 await page.keyboard.press("ArrowRight"); 172 await page.waitForTimeout(200); 173 await page.keyboard.press("ArrowRight"); 174 await page.waitForTimeout(300); 175 const shot2 = await page.screenshot(); 176 177 expect( 178 Buffer.from(shot1).equals(Buffer.from(shot2)), 179 "Expected visual change after pressing right arrow" 180 ).toBe(false); 181 expect(errors).toEqual([]); 182 }); 183 184 // ---- 7. Down arrow moves piece down faster ---- 185 test("down arrow accelerates piece", async ({ page }) => { 186 const errors: string[] = []; 187 page.on("pageerror", (err) => errors.push(err.message)); 188 189 const shot1 = await page.screenshot(); 190 for (let i = 0; i < 10; i++) { 191 await page.keyboard.press("ArrowDown"); 192 await page.waitForTimeout(50); 193 } 194 await page.waitForTimeout(200); 195 const shot2 = await page.screenshot(); 196 197 expect( 198 Buffer.from(shot1).equals(Buffer.from(shot2)), 199 "Expected visual change after pressing down arrow repeatedly" 200 ).toBe(false); 201 expect(errors).toEqual([]); 202 }); 203 204 // ---- 8. Up arrow (or Z) rotates piece ---- 205 test("rotation changes the piece", async ({ page }) => { 206 const errors: string[] = []; 207 page.on("pageerror", (err) => errors.push(err.message)); 208 209 const shot1 = await page.screenshot(); 210 await page.keyboard.press("ArrowUp"); 211 await page.waitForTimeout(300); 212 const shot2 = await page.screenshot(); 213 214 expect( 215 Buffer.from(shot1).equals(Buffer.from(shot2)), 216 "Expected visual change after pressing rotate key" 217 ).toBe(false); 218 expect(errors).toEqual([]); 219 }); 220 221 // ---- 9. Space bar hard-drops piece ---- 222 test("space bar hard-drops piece", async ({ page }) => { 223 const errors: string[] = []; 224 page.on("pageerror", (err) => errors.push(err.message)); 225 226 const shot1 = await page.screenshot(); 227 await page.keyboard.press("Space"); 228 await page.waitForTimeout(500); 229 const shot2 = await page.screenshot(); 230 231 expect( 232 Buffer.from(shot1).equals(Buffer.from(shot2)), 233 "Expected visual change after pressing space (hard drop)" 234 ).toBe(false); 235 expect(errors).toEqual([]); 236 }); 237 238 // ---- 10. Pieces lock at the bottom ---- 239 test("pieces lock at the bottom", async ({ page }) => { 240 // Hard-drop several pieces and check that the bottom of the board 241 // accumulates filled cells (the visual should change cumulatively). 242 const shots: Buffer[] = []; 243 244 shots.push(Buffer.from(await page.screenshot())); 245 246 for (let i = 0; i < 3; i++) { 247 await page.keyboard.press("Space"); 248 await page.waitForTimeout(800); 249 } 250 251 shots.push(Buffer.from(await page.screenshot())); 252 253 // After 3 hard drops, the board should look different from the start 254 // because pieces have stacked up at the bottom. 255 expect( 256 shots[0].equals(shots[1]), 257 "Expected pieces to stack up at the bottom after hard drops" 258 ).toBe(false); 259 }); 260 261 // ---- 11. New piece spawns after lock ---- 262 test("new piece spawns after locking", async ({ page }) => { 263 // Hard-drop to lock a piece, then wait and verify the game is still 264 // showing activity (a new piece should be falling). 265 await page.keyboard.press("Space"); 266 await page.waitForTimeout(1000); 267 268 const shot1 = await page.screenshot(); 269 await page.waitForTimeout(2000); 270 const shot2 = await page.screenshot(); 271 272 // If a new piece spawned and is falling, the screen should change 273 expect( 274 Buffer.from(shot1).equals(Buffer.from(shot2)), 275 "Expected a new piece to spawn and fall after the previous one locked" 276 ).toBe(false); 277 }); 278 279 // ---- 12. Multiple different pieces appear ---- 280 test("multiple different pieces appear", async ({ page }) => { 281 // Play through several pieces and capture screenshots. Different piece 282 // shapes should produce visually distinct patterns. 283 const shots: Buffer[] = []; 284 285 for (let i = 0; i < 6; i++) { 286 // Move each piece to a different column so they don't overlap identically 287 if (i % 2 === 0) { 288 await page.keyboard.press("ArrowLeft"); 289 await page.waitForTimeout(100); 290 await page.keyboard.press("ArrowLeft"); 291 await page.waitForTimeout(100); 292 } else { 293 await page.keyboard.press("ArrowRight"); 294 await page.waitForTimeout(100); 295 await page.keyboard.press("ArrowRight"); 296 await page.waitForTimeout(100); 297 } 298 await page.keyboard.press("Space"); 299 await page.waitForTimeout(600); 300 shots.push(Buffer.from(await page.screenshot())); 301 } 302 303 // At least some consecutive screenshots should differ (different piece shapes) 304 let differences = 0; 305 for (let i = 1; i < shots.length; i++) { 306 if (!shots[i - 1].equals(shots[i])) differences++; 307 } 308 309 expect( 310 differences, 311 "Expected to see visual differences between consecutive pieces (different shapes)" 312 ).toBeGreaterThanOrEqual(2); 313 }); 314 315 // ---- 13. Completed line clears ---- 316 test("completed line clears", async ({ page }) => { 317 // Fill a row by dropping many pieces. Observe whether any row disappears. 318 // We can detect this by tracking the total filled area -- after a line clear, 319 // the board should have less filled content than just before the clear. 320 const pageText = async () => 321 (await page.evaluate(() => document.body.innerText)) || ""; 322 323 // Drop many pieces rapidly to fill rows 324 for (let i = 0; i < 30; i++) { 325 // Vary positions to try to complete a row 326 const moves = (i % 5) - 2; // -2, -1, 0, 1, 2 327 for (let m = 0; m < Math.abs(moves); m++) { 328 await page.keyboard.press( 329 moves < 0 ? "ArrowLeft" : "ArrowRight" 330 ); 331 await page.waitForTimeout(50); 332 } 333 await page.keyboard.press("Space"); 334 await page.waitForTimeout(300); 335 } 336 337 // Check if a score or lines counter changed (common indicators of line clears) 338 const text = await pageText(); 339 const numbers = (text.match(/\d+/g) || []).map(Number); 340 const hasNonZero = numbers.some((n) => n > 0); 341 342 // Also check visual: the board should not be completely static 343 const shot1 = await page.screenshot(); 344 await page.waitForTimeout(1000); 345 const shot2 = await page.screenshot(); 346 347 // Either: score/lines increased, or game is still active (meaning lines cleared 348 // and made room for more pieces instead of game over) 349 const stillActive = !Buffer.from(shot1).equals(Buffer.from(shot2)); 350 351 expect( 352 hasNonZero || stillActive, 353 "Expected evidence of line clearing (score > 0 or game still active after many drops)" 354 ).toBe(true); 355 }); 356 357 // ---- 14. Score increases during play ---- 358 test("score increases during play", async ({ page }) => { 359 // Look for a score display on the page 360 const getNumbers = async () => { 361 const text = (await page.evaluate(() => document.body.innerText)) || ""; 362 return (text.match(/\d+/g) || []).map(Number); 363 }; 364 365 const numbersBefore = await getNumbers(); 366 367 // Play for a while: drop several pieces 368 for (let i = 0; i < 15; i++) { 369 const offset = (i % 5) - 2; 370 for (let m = 0; m < Math.abs(offset); m++) { 371 await page.keyboard.press(offset < 0 ? "ArrowLeft" : "ArrowRight"); 372 await page.waitForTimeout(50); 373 } 374 await page.keyboard.press("Space"); 375 await page.waitForTimeout(300); 376 } 377 378 const numbersAfter = await getNumbers(); 379 380 // At least one number on the page should have increased 381 // (score, lines counter, level, etc.) 382 let anyIncreased = false; 383 const maxLen = Math.min(numbersBefore.length, numbersAfter.length); 384 for (let i = 0; i < maxLen; i++) { 385 if (numbersAfter[i] > numbersBefore[i]) { 386 anyIncreased = true; 387 break; 388 } 389 } 390 391 // Also accept if new numbers appeared 392 if (!anyIncreased && numbersAfter.length > numbersBefore.length) { 393 anyIncreased = true; 394 } 395 396 // Also accept if the max number increased 397 if (!anyIncreased) { 398 const maxBefore = numbersBefore.length > 0 ? Math.max(...numbersBefore) : 0; 399 const maxAfter = numbersAfter.length > 0 ? Math.max(...numbersAfter) : 0; 400 if (maxAfter > maxBefore) anyIncreased = true; 401 } 402 403 expect( 404 anyIncreased, 405 "Expected at least one numeric value on the page to increase during play (score, lines, level)" 406 ).toBe(true); 407 }); 408 409 // ---- 15. Game over when pieces reach top ---- 410 test("game over when pieces reach top", async ({ page }) => { 411 // Stack pieces in the center until the game ends. 412 // Drop as many pieces as possible straight down. 413 for (let i = 0; i < 50; i++) { 414 await page.keyboard.press("Space"); 415 await page.waitForTimeout(200); 416 } 417 418 await page.waitForTimeout(2000); 419 420 // After stacking to overflow, the game should show some game-over indicator: 421 // - text saying "game over", "you lose", "try again", "restart", "end" 422 // - or the game stops updating (static screen) 423 const text = ((await page.evaluate(() => document.body.innerText)) || "").toLowerCase(); 424 const gameOverText = 425 text.includes("game over") || 426 text.includes("gameover") || 427 text.includes("you lose") || 428 text.includes("try again") || 429 text.includes("restart") || 430 text.includes("play again") || 431 text.includes("ended"); 432 433 // Check if the game stopped (screen is static) 434 const shot1 = await page.screenshot(); 435 await page.waitForTimeout(2000); 436 const shot2 = await page.screenshot(); 437 const isStatic = Buffer.from(shot1).equals(Buffer.from(shot2)); 438 439 expect( 440 gameOverText || isStatic, 441 "Expected game-over text or the game to stop after stacking pieces to the top" 442 ).toBe(true); 443 }); 444 445 // ---- 16. Game runs for 30 seconds without crashing ---- 446 test("game runs for 30 seconds without crashing", async ({ page }) => { 447 const errors: string[] = []; 448 page.on("pageerror", (err) => errors.push(err.message)); 449 450 // Simulate varied gameplay for 30 seconds 451 const keys = [ 452 "ArrowLeft", 453 "ArrowRight", 454 "ArrowDown", 455 "ArrowUp", 456 "Space", 457 ]; 458 const start = Date.now(); 459 460 while (Date.now() - start < 30_000) { 461 const key = keys[Math.floor(Math.random() * keys.length)]; 462 await page.keyboard.press(key); 463 await page.waitForTimeout(150 + Math.random() * 200); 464 } 465 466 // The page should still be alive (not crashed, not blank) 467 const text = await page.evaluate(() => document.body.innerText); 468 expect(text, "Page body should not be empty after 30s of play").toBeTruthy(); 469 expect( 470 errors.length, 471 `Expected no console errors during 30s of play, got: ${errors.join("; ")}` 472 ).toBe(0); 473 }); 474 });