Implement tests around redux game logic and add pre-commit hook

This commit is contained in:
2019-10-16 13:02:53 +02:00
parent 3d05b0b06a
commit ebb4f744b9
7 changed files with 160 additions and 9 deletions
+11 -3
View File
@@ -3,7 +3,6 @@
"version": "0.1.0",
"private": true,
"dependencies": {
"@types/jest": "24.0.18",
"@types/node": "12.7.12",
"@types/react": "16.9.5",
"@types/react-dom": "16.9.1",
@@ -19,8 +18,15 @@
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
"test-nowatch": "CI=true react-scripts test --env=jsdom",
"eject": "react-scripts eject",
"lint": "eslint --ext .js,.tsx,.ts,.tsx src/"
},
"pre-commit": [
"lint",
"test-nowatch",
"build"
],
"eslintConfig": {
"extends": "react-app"
},
@@ -37,6 +43,7 @@
]
},
"devDependencies": {
"@types/jest": "^24.0.18",
"@types/react-redux": "^7.1.4",
"@types/redux": "^3.6.0",
"@types/styled-components": "^4.1.19",
@@ -48,6 +55,7 @@
"eslint-plugin-jsx-a11y": "^6.2.3",
"eslint-plugin-react": "^7.16.0",
"eslint-plugin-react-hooks": "^1.7.0",
"jest": "^24.9.0"
"jest": "^24.9.0",
"pre-commit": "^1.2.2"
}
}
+6 -2
View File
@@ -21,6 +21,7 @@ const StyledApp = styled.div`
interface StateProps {
currentPlayer: Player;
winner: Winner;
}
interface ActionProps {
@@ -29,10 +30,12 @@ interface ActionProps {
type Props = StateProps & ActionProps;
const App: React.FC<Props> = ({ currentPlayer, startGame }) => (
const App: React.FC<Props> = ({ currentPlayer, startGame, winner }) => (
<StyledApp>
<GlobalStyle />
<h1>{currentPlayer}</h1>
<h1>
{currentPlayer} - {winner}
</h1>
<button type="button" onClick={startGame}>
Reset Game
</button>
@@ -42,6 +45,7 @@ const App: React.FC<Props> = ({ currentPlayer, startGame }) => (
const mapStateToProps = (state: GameState): StateProps => ({
currentPlayer: state.currentPlayer,
winner: state.winner,
});
const mapActionsToProps = {
+6
View File
@@ -0,0 +1,6 @@
describe('My Connected React-Redux Component', () => {
it('should render with given state from Redux store', () => {
});
it('should dispatch an action on button click', () => {
});
});
+4
View File
@@ -0,0 +1,4 @@
describe('My Connected React-Redux Component', () => {
it('should render with given state from Redux store', () => {});
it('should dispatch an action on button click', () => {});
});
+3 -3
View File
@@ -1,7 +1,7 @@
import { Board, Cell, Cells, empty, Rows } from './Board';
import { Player, Winner, tie, undetermined } from './Player';
export interface IBoardSummary {
export interface BoardSummary {
horizontal: Rows;
vertical: Rows;
diagonal: Rows;
@@ -52,8 +52,8 @@ export const getWinnerFromLines = (lines: Rows): Winner => {
return undetermined;
};
export const getBoardSummary = (board: Board): IBoardSummary => {
const summary: IBoardSummary = {
export const getBoardSummary = (board: Board): BoardSummary => {
const summary: BoardSummary = {
horizontal: [[], [], []],
vertical: [[], [], []],
diagonal: [[], []],
+129
View File
@@ -0,0 +1,129 @@
import { reducer, GameState, actions } from './game';
import { Player, tie, undetermined } from '../lib/Player';
import { empty, createEmptyBoard } from '../lib/Board';
import { getBoardSummary, BoardSummary } from '../lib/scoring';
jest.mock('../lib/scoring');
const mockedGetBoardSummary = <jest.Mock<BoardSummary>>getBoardSummary;
describe('reducer with name', () => {
describe('START_GAME', () => {
it('must reset the game state', () => {
const state: GameState = {
currentPlayer: Player.O,
activeGame: false,
board: [],
winner: tie,
};
const action = actions.startGame();
const updatedState = reducer(state, action);
expect(updatedState).toEqual({
board: [[empty, empty, empty], [empty, empty, empty], [empty, empty, empty]],
winner: undetermined,
activeGame: true,
currentPlayer: Player.X,
});
});
});
describe('END_GAME', () => {
it('must set the activeGame flag to false', () => {
const state: GameState = {
currentPlayer: Player.O,
activeGame: true,
board: [],
winner: tie,
};
const action = actions.endGame();
const updatedState = reducer(state, action);
expect(updatedState).toEqual({ ...state, activeGame: false });
});
});
describe('NEXT_PLAYER', () => {
it('must toggle the player everytime NEXT_PLAYER is called', () => {
const state: GameState = {
currentPlayer: Player.O,
activeGame: true,
board: [],
winner: tie,
};
const action = actions.nextPlayer();
let updatedState = reducer(state, action);
expect(updatedState.currentPlayer).toEqual(Player.X);
updatedState = reducer(updatedState, action);
expect(updatedState.currentPlayer).toEqual(Player.O);
updatedState = reducer(updatedState, action);
expect(updatedState.currentPlayer).toEqual(Player.X);
});
it('must not toggle the player when the game is not active anymore', () => {
const state: GameState = {
currentPlayer: Player.O,
activeGame: false,
board: [],
winner: tie,
};
const action = actions.nextPlayer();
let updatedState = reducer(state, action);
expect(updatedState.currentPlayer).toEqual(Player.O);
updatedState = reducer(updatedState, action);
expect(updatedState.currentPlayer).toEqual(Player.O);
updatedState = reducer(updatedState, action);
expect(updatedState.currentPlayer).toEqual(Player.O);
});
});
describe('UPDATE_BOARD', () => {
it('must mark an empty cell with the specified player', () => {
const state: GameState = {
currentPlayer: Player.O,
activeGame: true,
board: createEmptyBoard(),
winner: tie,
};
const action = actions.updateBoard(2, 1, Player.O);
const updatedState = reducer(state, action);
expect(updatedState).toEqual({
...state,
board: [[empty, empty, empty], [empty, empty, empty], [empty, Player.O, empty]],
});
});
it('must NOT mark a cell when it is not empty', () => {
const state: GameState = {
currentPlayer: Player.O,
activeGame: true,
board: [[empty, empty, empty], [empty, empty, empty], [empty, Player.O, empty]],
winner: tie,
};
const action = actions.updateBoard(2, 1, Player.X);
const updatedState = reducer(state, action);
expect(updatedState).toEqual({
...state,
board: [[empty, empty, empty], [empty, empty, empty], [empty, Player.O, empty]],
});
});
it('must NOT mark a cell when the game is not active anymore', () => {
const state: GameState = {
currentPlayer: Player.O,
activeGame: false,
board: [[empty, empty, empty], [empty, empty, empty], [empty, Player.O, empty]],
winner: tie,
};
const action = actions.updateBoard(2, 1, Player.X);
const updatedState = reducer(state, action);
expect(updatedState).toEqual(state);
});
});
describe('DETERMINE_WINNER', () => {
it('must use the external scroring logic to decide if there is a winner', () => {
const state: GameState = {
currentPlayer: Player.O,
activeGame: true,
board: [],
winner: undetermined,
};
mockedGetBoardSummary.mockReturnValueOnce({ horizontal: [], vertical: [], diagonal: [], winner: Player.X });
const action = actions.determineWinner();
const updatedState = reducer(state, action);
expect(updatedState).toEqual({ ...state, winner: Player.X });
});
});
});
+1 -1
View File
@@ -61,7 +61,7 @@ const defaultState: IGameState = {
export const reducer: Reducer<IGameState, Action> = (state: IGameState = defaultState, action: Action): IGameState => {
if (action.type === START_GAME) {
return { ...state, board: createEmptyBoard(), activeGame: true };
return { ...state, board: createEmptyBoard(), activeGame: true, winner: undetermined, currentPlayer: Player.X };
}
if (action.type === END_GAME) {
return { ...state, activeGame: false };