diff --git a/package-lock.json b/package-lock.json index 7a19ee0..1d15d4a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6413,6 +6413,12 @@ "pretty-format": "22.4.0" } }, + "jest-localstorage-mock": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/jest-localstorage-mock/-/jest-localstorage-mock-2.2.0.tgz", + "integrity": "sha512-x+P0vcwr4540bCAYzTEpiD9rs+zh/QZzyiABV+MU6yM2OPwPlrrLyUx/6gValMyt6tg5lX6Z53o2rHWfUht5Xw==", + "dev": true + }, "jest-matcher-utils": { "version": "22.4.0", "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-22.4.0.tgz", diff --git a/package.json b/package.json index 8bf1391..7ab9635 100644 --- a/package.json +++ b/package.json @@ -104,5 +104,8 @@ }, "eslintConfig": { "extends": "react-app" + }, + "devDependencies": { + "jest-localstorage-mock": "^2.2.0" } } diff --git a/src/App.driver.js b/src/App.driver.js index ec83eb8..90b1f6a 100644 --- a/src/App.driver.js +++ b/src/App.driver.js @@ -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, }; }; diff --git a/src/App.js b/src/App.js index 64b9564..b5f0e78 100644 --- a/src/App.js +++ b/src/App.js @@ -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 (
- - + {shouldShowRegistration && } + {this.state.p1Name && ( + + )} {this.state.winner && (
{`${this.state.winner === 'X' ? this.state.p1Name : this.state.p2Name} won!`} + {winCount > 0 && ` He won ${winCount} times before that!`}
)} + {this.state.tie &&
It's a tie!
} + +
); } diff --git a/src/App.spec.js b/src/App.spec.js index d7a7940..02d6e69 100644 --- a/src/App.spec.js +++ b/src/App.spec.js @@ -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())); @@ -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(); + 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(); + 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(); + 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(); + 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(); + + 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(); + + expect(driver.isGameBoardVisible()).toEqual(false); + + driver.newGame(p1Name, p2Name); + + expect(driver.isGameBoardVisible()).toEqual(true); +}); diff --git a/src/Game.js b/src/Game.js index 3fbc008..1e2b8b2 100644 --- a/src/Game.js +++ b/src/Game.js @@ -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 ( -
- {p1Name} - {p2Name} +
+ + {p1Name} + + + {p2Name} + {board.map((row, rIndex) => ( @@ -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; diff --git a/src/Registration.js b/src/Registration.js index a8c9f80..b65001d 100644 --- a/src/Registration.js +++ b/src/Registration.js @@ -14,7 +14,7 @@ export default class Registration extends Component { } render() { return ( -
+
this.setState({ p1Name: el.target.value })} data-hook="p1-input" /> this.setState({ p2Name: el.target.value })} data-hook="p2-input" />