Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Workshop tasks #6

Open
wants to merge 12 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -104,5 +104,8 @@
},
"eslintConfig": {
"extends": "react-app"
},
"devDependencies": {
"jest-localstorage-mock": "^2.2.0"
}
}
5 changes: 5 additions & 0 deletions src/App.driver.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ const appDriver = () => {
.at(index)
.text(),
getWinnerMessage: () => wrapper.find('[data-hook="winner-message"]').text(),
getTieMessage: () => wrapper.find('[data-hook="tie-message"]').text(),
isP1NameHasClass: klass => wrapper.find('[data-hook="p1-name"]').hasClass(klass),
isP2NameHasClass: klass => wrapper.find('[data-hook="p2-name"]').hasClass(klass),
isRegistrationVisible: () => wrapper.find('[data-hook="registration"]').length > 0,
isGameBoardVisible: () => wrapper.find('[data-hook="game-board"]').length > 0,
};
};

Expand Down
65 changes: 56 additions & 9 deletions src/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,36 +13,83 @@ class App extends React.Component {
board: [['', '', ''], ['', '', ''], ['', '', '']],
winner: '',
currentPlayer: 'X',
p1Wins: 0,
p2Wins: 0,
};
}
onNewGame = ({ p1Name, p2Name }) => {
this.setState({ p1Name, p2Name });
const p1Wins = localStorage.getItem(p1Name);
const p2Wins = localStorage.getItem(p2Name);

this.setState({
p1Name,
p2Name,
p1Wins: p1Wins ? parseInt(p1Wins, 10) : 0,
p2Wins: p2Wins ? parseInt(p2Wins, 10) : 0,
});
};

isCellAlreadySet(rIndex, cIndex) {
const { board } = this.state;
return board[rIndex][cIndex] === 'X' || board[rIndex][cIndex] === 'O';
}

handleCellClick = (rIndex, cIndex) => {
if (this.isCellAlreadySet(rIndex, cIndex)) {
return;
}

const board = this.state.board.map(row => [...row]);
board[rIndex][cIndex] = this.state.currentPlayer;
if (gameStatus(board) === this.state.currentPlayer) {
this.setState({ winner: this.state.currentPlayer });

if (this.state.currentPlayer === 'X') {
localStorage.setItem(this.state.p1Name, this.state.p1Wins + 1);
} else {
localStorage.setItem(this.state.p2Name, this.state.p2Wins + 1);
}
}
const nextPlayer = this.state.currentPlayer === 'X' ? 'O' : 'X';
this.setState({ board, currentPlayer: nextPlayer });
this.setState({ board, currentPlayer: nextPlayer, tie: gameStatus(board) === 'tie' });
};

saveGame = () => {
localStorage.setItem('gameState', JSON.stringify(this.state));
};

loadGame = () => {
this.setState(JSON.parse(localStorage.getItem('gameState')));
};

render() {
const shouldShowRegistration = !this.state.p1Name || this.state.tie || this.state.winner;
const winCount = this.state.winner === 'X' ? this.state.p1Wins : this.state.p2Wins;
return (
<div className="App">
<Registration onNewGame={this.onNewGame} />
<Game
onCellClicked={this.handleCellClick}
board={this.state.board}
p1Name={this.state.p1Name}
p2Name={this.state.p2Name}
/>
{shouldShowRegistration && <Registration onNewGame={this.onNewGame} />}
{this.state.p1Name && (
<Game
onCellClicked={this.handleCellClick}
board={this.state.board}
p1Name={this.state.p1Name}
p2Name={this.state.p2Name}
currentPlayer={this.state.currentPlayer}
/>
)}
{this.state.winner && (
<div data-hook="winner-message">
{`${this.state.winner === 'X' ? this.state.p1Name : this.state.p2Name} won!`}
{winCount > 0 && ` He won ${winCount} times before that!`}
</div>
)}
{this.state.tie && <div data-hook="tie-message">It&apos;s a tie!</div>}
<button data-hook="save-game" onClick={this.saveGame}>
Save game!
</button>
<button data-hook="load-game" onClick={this.loadGame}>
Load game!
</button>
</div>
);
}
Expand Down
83 changes: 83 additions & 0 deletions src/App.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,14 @@ import App from './App';
import appDriver from './App.driver';

configure({ adapter: new Adapter() });

beforeAll(() => {
global.localStorage = {
getItem: () => null,
setItem: () => null,
clear: () => null,
};
});
let driver;
beforeEach(() => (driver = appDriver()));

Expand Down Expand Up @@ -38,3 +46,78 @@ test('"O" should win the game', () => {
driver.clickACellAt(2);
expect(driver.getWinnerMessage()).toBe(`${p2Name} won!`);
});

test("User can't change cell status once it's set", () => {
const p1Name = 'Yaniv';
const p2Name = 'Computer';
driver.render(<App />);
driver.newGame(p1Name, p2Name);
driver.clickACellAt(0);
driver.clickACellAt(0);
expect(driver.getACellAt(0)).toBe('X');
});

test('should display tie message', () => {
const p1Name = 'Yaniv';
const p2Name = 'Computer';
driver.render(<App />);
driver.newGame(p1Name, p2Name);

driver.clickACellAt(1); // x
driver.clickACellAt(2); // o
driver.clickACellAt(3); // x
driver.clickACellAt(5); // o
driver.clickACellAt(4); // x
driver.clickACellAt(7); // o
driver.clickACellAt(6); // x
driver.clickACellAt(0); // o
driver.clickACellAt(8); // x

expect(driver.getTieMessage()).toBe("It's a tie!");
});

test('on init should apply class to current player', () => {
const p1Name = 'Yaniv';
const p2Name = 'Computer';
driver.render(<App />);
driver.newGame(p1Name, p2Name);

expect(driver.isP1NameHasClass('current-player')).toEqual(true);
expect(driver.isP2NameHasClass('current-player')).toEqual(false);
});

test('should switch current player class after init', () => {
const p1Name = 'Yaniv';
const p2Name = 'Computer';
driver.render(<App />);
driver.newGame(p1Name, p2Name);

driver.clickACellAt(1);

expect(driver.isP1NameHasClass('current-player')).toEqual(false);
expect(driver.isP2NameHasClass('current-player')).toEqual(true);
});

test('should hide registration after game start', () => {
const p1Name = 'Yaniv';
const p2Name = 'Computer';
driver.render(<App />);

expect(driver.isRegistrationVisible()).toEqual(true);

driver.newGame(p1Name, p2Name);

expect(driver.isRegistrationVisible()).toEqual(false);
});

test('should hide game board before game start', () => {
const p1Name = 'Yaniv';
const p2Name = 'Computer';
driver.render(<App />);

expect(driver.isGameBoardVisible()).toEqual(false);

driver.newGame(p1Name, p2Name);

expect(driver.isGameBoardVisible()).toEqual(true);
});
13 changes: 9 additions & 4 deletions src/Game.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
import React from 'react';
import PropTypes from 'prop-types';

const Game = ({ p1Name, p2Name, board, onCellClicked }) => {
const Game = ({ p1Name, p2Name, board, onCellClicked, currentPlayer }) => {
return (
<div>
<span data-hook="p1-name">{p1Name}</span>
<span data-hook="p2-name">{p2Name}</span>
<div data-hook="game-board">
<span data-hook="p1-name" className={currentPlayer === 'X' ? 'current-player' : ''}>
{p1Name}
</span>
<span data-hook="p2-name" className={currentPlayer === 'O' ? 'current-player' : ''}>
{p2Name}
</span>
<table role="grid">
<tbody>
{board.map((row, rIndex) => (
Expand Down Expand Up @@ -33,5 +37,6 @@ Game.propTypes = {
p2Name: PropTypes.string.isRequired,
board: PropTypes.arrayOf(PropTypes.arrayOf(PropTypes.string)).isRequired,
onCellClicked: PropTypes.func.isRequired,
currentPlayer: PropTypes.oneOf(['X', 'O']).isRequired,
};
export default Game;
2 changes: 1 addition & 1 deletion src/Registration.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export default class Registration extends Component {
}
render() {
return (
<div>
<div data-hook="registration">
<input onChange={el => this.setState({ p1Name: el.target.value })} data-hook="p1-input" />
<input onChange={el => this.setState({ p2Name: el.target.value })} data-hook="p2-input" />
<button
Expand Down
25 changes: 24 additions & 1 deletion src/gameService.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,32 @@
const checkLine = (board, symbol) => board.some(row => row.every(cell => cell === symbol));

const checkCol = (board, symbol) =>
[0, 1, 2].some(colIndex => board.every(row => row[colIndex] === symbol));

const checkLeftDiagonal = (board, symbol) =>
board[0][0] === symbol && board[1][1] === symbol && board[2][2] === symbol;

const checkRightDiagonal = (board, symbol) =>
board[0][2] === symbol && board[1][1] === symbol && board[2][0] === symbol;

const allCellsTaken = board => board.every(row => row.every(cell => cell === 'X' || cell === 'O'));

export const gameStatus = board => {
const isWin = symbol => board[0].every(cell => cell === symbol);
const isWin = symbol =>
checkLine(board, symbol) ||
checkCol(board, symbol) ||
checkLeftDiagonal(board, symbol) ||
checkRightDiagonal(board, symbol);

if (isWin('X')) {
return 'X';
}

if (isWin('O')) {
return 'O';
}

if (allCellsTaken(board)) {
return 'tie';
}
};
42 changes: 41 additions & 1 deletion src/gameService.unit.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,46 @@
const { gameStatus } = require('./gameService');

test('X should win', () => {
test('X should win on top line', () => {
const board = [['X', 'X', 'X'], ['', '', ''], ['', '', '']];
expect(gameStatus(board)).toBe('X');
});

test('X should win on middle line', () => {
const board = [['', '', ''], ['X', 'X', 'X'], ['', '', '']];
expect(gameStatus(board)).toBe('X');
});

test('X should win on bottom line', () => {
const board = [['', '', ''], ['', '', ''], ['X', 'X', 'X']];
expect(gameStatus(board)).toBe('X');
});

test('X should win on left col', () => {
const board = [['X', '', ''], ['X', '', ''], ['X', '', '']];
expect(gameStatus(board)).toBe('X');
});

test('X should win on middle col', () => {
const board = [['', 'X', ''], ['', 'X', ''], ['', 'X', '']];
expect(gameStatus(board)).toBe('X');
});

test('X should win on right col', () => {
const board = [['', '', 'X'], ['', '', 'X'], ['', '', 'X']];
expect(gameStatus(board)).toBe('X');
});

test('X should win on left diagonal', () => {
const board = [['X', '', ''], ['', 'X', ''], ['', '', 'X']];
expect(gameStatus(board)).toBe('X');
});

test('X should win on right diagonal', () => {
const board = [['', '', 'X'], ['', 'X', ''], ['X', '', '']];
expect(gameStatus(board)).toBe('X');
});

test("when all cells are taking it's a tie", () => {
const board = [['O', 'X', 'O'], ['X', 'X', 'O'], ['X', 'O', 'X']];
expect(gameStatus(board)).toBe('tie');
});
5 changes: 5 additions & 0 deletions src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,8 @@ body {
padding: 0;
font-family: sans-serif;
}

.current-player {
border-color: red;
color: green;
}
5 changes: 5 additions & 0 deletions test/App.driver.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ const appDriver = page => ({
page.$$eval('[data-hook="cell"]', (cells, i) => cells[i].innerText, index),
getWinnerMessage: () => page.$eval('[data-hook="winner-message"]', el => el.innerText),
hasWinner: async () => !!await page.$('[data-hook="winner-message"]'),
clickSaveGame: () => page.$eval('[data-hook="save-game"]', el => el.click()),
clickLoadGame: () => page.$eval('[data-hook="load-game"]', el => el.click()),
getGameStateFromLocalStorage: () => page.evaluate(() => localStorage.getItem('gameState')),
setGameStateFromLocalStorage: gameState =>
page.evaluate(state => localStorage.setItem('gameState', JSON.stringify(state)), gameState),
});

module.exports = appDriver;
Loading