feat: split Install/UnitTest stages, add game.js tests with 100% coverage

21 tests covering win/loss/draw/score/reset. lcov report fed to SonarQube.
Pipeline: Install → Unit Test → Scan Code Quality → Build & Push.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-02 16:10:27 +07:00
parent 93dea76004
commit 4d5657f259
4 changed files with 843 additions and 17 deletions

157
test/game.test.js Normal file
View File

@@ -0,0 +1,157 @@
/**
* @jest-environment jsdom
*/
function setupDOM() {
document.body.innerHTML = `
<div id="status">Player X's turn</div>
<div id="board">
${Array.from({ length: 9 }, (_, i) => `<div class="cell" data-index="${i}"></div>`).join('')}
</div>
<span id="score-x">0</span>
<span id="score-draw">0</span>
<span id="score-o">0</span>
<button id="reset">New Game</button>
`;
}
function click(index) {
document.querySelectorAll('.cell')[index].dispatchEvent(new MouseEvent('click', { bubbles: true }));
}
function reset() {
document.getElementById('reset').dispatchEvent(new MouseEvent('click', { bubbles: true }));
}
beforeEach(() => {
setupDOM();
jest.resetModules();
require('../public/game.js');
});
describe('initial state', () => {
test('all cells empty', () => {
document.querySelectorAll('.cell').forEach(c => expect(c.textContent).toBe(''));
});
test('status shows X turn', () => {
expect(document.getElementById('status').textContent).toBe("Player X's turn");
});
test('scores start at 0', () => {
expect(document.getElementById('score-x').textContent).toBe('0');
expect(document.getElementById('score-o').textContent).toBe('0');
expect(document.getElementById('score-draw').textContent).toBe('0');
});
});
describe('turn alternation', () => {
test('alternates X and O after each move', () => {
click(0);
expect(document.getElementById('status').textContent).toBe("Player O's turn");
click(1);
expect(document.getElementById('status').textContent).toBe("Player X's turn");
});
test('cell shows correct player mark', () => {
click(0);
expect(document.querySelectorAll('.cell')[0].textContent).toBe('X');
click(1);
expect(document.querySelectorAll('.cell')[1].textContent).toBe('O');
});
test('clicking taken cell does nothing', () => {
click(0);
click(0);
expect(document.getElementById('status').textContent).toBe("Player O's turn");
});
});
describe('X wins', () => {
// X: 0,1,2 O: 3,4
beforeEach(() => { click(0); click(3); click(1); click(4); click(2); });
test('status shows X wins', () => {
expect(document.getElementById('status').textContent).toBe('Player X wins!');
});
test('winning cells get winner class', () => {
const cells = document.querySelectorAll('.cell');
expect(cells[0].classList.contains('winner')).toBe(true);
expect(cells[1].classList.contains('winner')).toBe(true);
expect(cells[2].classList.contains('winner')).toBe(true);
});
test('X score increments', () => {
expect(document.getElementById('score-x').textContent).toBe('1');
});
test('no moves accepted after win', () => {
click(5);
expect(document.querySelectorAll('.cell')[5].textContent).toBe('');
});
});
describe('O wins', () => {
// X: 0,6,7 O: 3,4,5
beforeEach(() => { click(0); click(3); click(6); click(4); click(7); click(5); });
test('status shows O wins', () => {
expect(document.getElementById('status').textContent).toBe('Player O wins!');
});
test('O score increments', () => {
expect(document.getElementById('score-o').textContent).toBe('1');
});
});
describe('draw', () => {
// X: 0,2,5,6,7 O: 1,3,4,8 — fills board, no winner
beforeEach(() => {
click(0); click(1); click(2); click(3); click(5); click(4); click(6); click(8); click(7);
});
test('status shows draw', () => {
expect(document.getElementById('status').textContent).toBe("It's a draw!");
});
test('draw score increments', () => {
expect(document.getElementById('score-draw').textContent).toBe('1');
});
});
describe('reset', () => {
test('clears board after game', () => {
click(0); click(1);
reset();
document.querySelectorAll('.cell').forEach(c => expect(c.textContent).toBe(''));
});
test('resets status to X turn', () => {
click(0);
reset();
expect(document.getElementById('status').textContent).toBe("Player X's turn");
});
test('score persists across reset', () => {
click(0); click(3); click(1); click(4); click(2);
reset();
expect(document.getElementById('score-x').textContent).toBe('1');
});
test('allows play after reset', () => {
click(0); click(1); click(2); click(3); click(4); click(5); click(6); click(7); click(8);
reset();
click(0);
expect(document.querySelectorAll('.cell')[0].textContent).toBe('X');
});
});
describe('score accumulates across games', () => {
test('multiple wins tracked', () => {
click(0); click(3); click(1); click(4); click(2);
reset();
click(0); click(3); click(1); click(4); click(2);
expect(document.getElementById('score-x').textContent).toBe('2');
});
});