Improve unit tests with mocking, edge cases, and better coverage

- Add deterministic mocking for Math.random() in garbage tests
- Add edge case tests for piece positioning at boundaries
- Add tests for negative Y positions (spawn area)
- Improve lockPieceToBoard tests with proper I piece shape handling
- Add performance tests for checkBoardOverflow and createEmptyBoard
- Add tests for multiple garbage rows and board overflow scenarios
- Fix test expectations to match actual game logic behavior
- Total tests increased from 57 to 105
This commit is contained in:
2026-03-21 15:22:11 +00:00
parent 6d8ee7dda8
commit 1af068923b
2 changed files with 536 additions and 1 deletions
+242 -1
View File
@@ -7,7 +7,8 @@ const {
createEmptyBoard,
getPieceFromType,
checkBoardOverflow,
addGarbageRow
addGarbageRow,
lockPieceToBoard
} = require('../game-logic');
// Mock player object
@@ -64,6 +65,33 @@ function sendGarbage(lobby, sender, rowsCleared) {
return garbageLog;
}
// Simulate lockPiece with elimination check (from index.js)
function lockPiece(player) {
if (!player.currentPiece) return 0;
// Lock piece into board
player.board = lockPieceToBoard(player.currentPiece, player.board);
// Check board overflow for elimination
if (checkBoardOverflow(player.board)) {
player.eliminated = true;
return 0;
}
// Clear rows
let rowsCleared = 0;
for (let row = BOARD_HEIGHT - 1; row >= 0; row--) {
if (player.board[row].every(cell => cell !== 0)) {
player.board.splice(row, 1);
player.board.unshift(Array(BOARD_WIDTH).fill(0));
rowsCleared++;
row++;
}
}
return rowsCleared;
}
describe('addGarbageToPlayer', () => {
test('adds exactly 1 garbage row per call', () => {
const player = createMockPlayer('Player1', 5);
@@ -111,6 +139,28 @@ describe('addGarbageToPlayer', () => {
expect(player.eliminated).toBe(false);
expect(player.currentPiece.y).toBe(5); // 10 - 5 = 5
});
test('handles player with no current piece', () => {
const player = createMockPlayer('Player1', null);
addGarbageToPlayer(player, 'Enemy');
expect(player.currentPiece).toBe(null);
expect(player.garbageReceived.length).toBe(1);
});
test('multiple garbage from different senders tracked separately', () => {
const player = createMockPlayer('Player1', 10);
addGarbageToPlayer(player, 'Enemy1');
addGarbageToPlayer(player, 'Enemy2');
addGarbageToPlayer(player, 'Enemy1');
expect(player.garbageReceived).toHaveLength(3);
expect(player.garbageReceived[0].sender).toBe('Enemy1');
expect(player.garbageReceived[1].sender).toBe('Enemy2');
expect(player.garbageReceived[2].sender).toBe('Enemy1');
});
});
describe('sendGarbage', () => {
@@ -171,6 +221,106 @@ describe('sendGarbage', () => {
// Total garbage sent should equal rows cleared
expect(target1.garbageReceived.length + target2.garbageReceived.length).toBe(4);
});
test('garbage distribution is deterministic with mocked random', () => {
const sender = createMockPlayer('Sender', 5);
const target1 = createMockPlayer('Target1', 10);
const target2 = createMockPlayer('Target2', 10);
const lobby = createMockLobby([sender, target1, target2]);
// Mock Math.random to always pick first opponent (index 0)
const originalRandom = Math.random;
Math.random = () => 0.1;
sendGarbage(lobby, sender, 4);
Math.random = originalRandom;
// All garbage should go to target1 (first in array)
expect(target1.garbageReceived.length).toBe(4);
expect(target2.garbageReceived.length).toBe(0);
});
test('1 row cleared sends 1 garbage row', () => {
const sender = createMockPlayer('Sender', 5);
const target = createMockPlayer('Target', 10);
const lobby = createMockLobby([sender, target]);
const garbageLog = sendGarbage(lobby, sender, 1);
expect(garbageLog.length).toBe(1);
expect(target.garbageReceived.length).toBe(1);
});
test('2 rows cleared sends 2 garbage rows', () => {
const sender = createMockPlayer('Sender', 5);
const target = createMockPlayer('Target', 10);
const lobby = createMockLobby([sender, target]);
const garbageLog = sendGarbage(lobby, sender, 2);
expect(garbageLog.length).toBe(2);
});
test('4 rows (tetris) sends 4 garbage rows', () => {
const sender = createMockPlayer('Sender', 5);
const target = createMockPlayer('Target', 10);
const lobby = createMockLobby([sender, target]);
const garbageLog = sendGarbage(lobby, sender, 4);
expect(garbageLog.length).toBe(4);
});
});
describe('lockPiece elimination', () => {
test('piece locking does not eliminate when board not overflowed', () => {
const player = createMockPlayer('Player1', 10);
const rowsCleared = lockPiece(player);
expect(player.eliminated).toBe(false);
expect(rowsCleared).toBe(0);
});
test('piece locking eliminates when board overflowed', () => {
const player = createMockPlayer('Player1', 0);
// Fill top 2 rows with garbage
player.board[0].fill(GARBAGE_COLOR);
player.board[0][5] = 0; // Gap
lockPiece(player);
expect(player.eliminated).toBe(true);
});
test('piece at y=0 eliminates when it locks into top rows', () => {
const player = createMockPlayer('Player1', 0);
// I piece at y=0 locks into rows 0,1,2,3
// This causes overflow since row 0 and 1 will have blocks
lockPiece(player);
expect(player.eliminated).toBe(true);
});
test('piece at y=5 does not eliminate if top rows empty', () => {
const player = createMockPlayer('Player1', 5);
// Top rows are empty, piece locks at rows 5,6,7,8
lockPiece(player);
expect(player.eliminated).toBe(false);
});
test('piece locks and does not clear rows when no complete rows', () => {
const player = createMockPlayer('Player1', 10);
// Piece locks at rows 10,11,12,13 - no complete rows
const rowsCleared = lockPiece(player);
expect(rowsCleared).toBe(0);
});
});
describe('checkBoardOverflow', () => {
@@ -203,6 +353,20 @@ describe('checkBoardOverflow', () => {
board[18].fill(GARBAGE_COLOR);
expect(checkBoardOverflow(board)).toBe(false);
});
test('returns true when both row 0 and row 1 have blocks', () => {
const board = createEmptyBoard();
board[0][3] = '#ff0000';
board[1][7] = '#ff0000';
expect(checkBoardOverflow(board)).toBe(true);
});
test('returns true when row 0 has garbage with gap', () => {
const board = createEmptyBoard();
board[0].fill(GARBAGE_COLOR);
board[0][5] = 0; // Gap in row 0
expect(checkBoardOverflow(board)).toBe(true);
});
});
describe('Garbage Elimination Bug Fix', () => {
@@ -263,4 +427,81 @@ describe('Garbage Elimination Bug Fix', () => {
expect(target.eliminated).toBe(false);
expect(target.garbageReceived.length).toBe(4);
});
test('elimination only happens when piece locks with overflow', () => {
// Complete scenario: garbage received, piece locks, then elimination checked
const player = createMockPlayer('Player1', 0); // Piece at spawn
// Receive 1 garbage row - player NOT eliminated
addGarbageToPlayer(player, 'Enemy');
expect(player.eliminated).toBe(false);
// Fill top row to simulate overflow
player.board[0].fill(GARBAGE_COLOR);
player.board[0][5] = 0;
// Now lock piece - player eliminated
lockPiece(player);
expect(player.eliminated).toBe(true);
});
test('player with piece above board survives garbage', () => {
// Edge case: piece is already above visible area (negative y)
// Should still not be eliminated by garbage alone
const player = createMockPlayer('Player1', 2);
// Push piece 5 rows up (to y=-3)
for (let i = 0; i < 5; i++) {
addGarbageToPlayer(player, 'Enemy');
}
// Piece is at y=-3, but player NOT eliminated
expect(player.eliminated).toBe(false);
expect(player.currentPiece.y).toBe(-3);
});
});
describe('edge cases', () => {
test('player with piece at y=0 receives garbage', () => {
const player = createMockPlayer('Player1', 0);
addGarbageToPlayer(player, 'Enemy');
expect(player.currentPiece.y).toBe(-1);
expect(player.eliminated).toBe(false);
});
test('18 garbage rows pushes piece well above board', () => {
const player = createMockPlayer('Player1', 10);
for (let i = 0; i < 18; i++) {
addGarbageToPlayer(player, 'Enemy');
}
expect(player.currentPiece.y).toBe(-8);
// Board overflowed but player not eliminated yet (only on lock)
expect(player.eliminated).toBe(false);
});
test('garbage with no opponents does not crash', () => {
const sender = createMockPlayer('Sender', 5);
const lobby = createMockLobby([sender]);
const garbageLog = sendGarbage(lobby, sender, 4);
expect(garbageLog.length).toBe(0);
});
test('zero rows cleared sends no garbage', () => {
const sender = createMockPlayer('Sender', 5);
const target = createMockPlayer('Target', 10);
const lobby = createMockLobby([sender, target]);
const garbageLog = sendGarbage(lobby, sender, 0);
expect(garbageLog.length).toBe(0);
expect(target.garbageReceived.length).toBe(0);
});
});