Implement all component and redux tests

This commit is contained in:
2019-10-16 18:28:17 +02:00
parent ebb4f744b9
commit a97964db55
9 changed files with 204 additions and 18 deletions

View File

@@ -48,6 +48,10 @@ module.exports = {
'no-underscore-dangle': 0,
'no-unused-vars': 0,
'object-curly-newline': 0,
'import/no-extraneous-dependencies': [
'error',
{ devDependencies: ['**/*.spec.ts', '**/*.spec.tsx', '**/*.spec.js', '**/*.spec.jsx'] },
],
'@typescript-eslint/no-unused-vars': [
'error',
{

View File

@@ -45,7 +45,9 @@
"devDependencies": {
"@types/jest": "^24.0.18",
"@types/react-redux": "^7.1.4",
"@types/react-test-renderer": "^16.9.1",
"@types/redux": "^3.6.0",
"@types/redux-mock-store": "^1.0.1",
"@types/styled-components": "^4.1.19",
"@typescript-eslint/eslint-plugin": "^2.3.3",
"@typescript-eslint/parser": "^2.3.3",
@@ -56,6 +58,8 @@
"eslint-plugin-react": "^7.16.0",
"eslint-plugin-react-hooks": "^1.7.0",
"jest": "^24.9.0",
"pre-commit": "^1.2.2"
"pre-commit": "^1.2.2",
"react-test-renderer": "^16.10.2",
"redux-mock-store": "^1.5.3"
}
}

58
src/App.spec.tsx Normal file
View File

@@ -0,0 +1,58 @@
import React from 'react';
import configureStore from 'redux-mock-store';
import { Middleware, Store } from 'redux';
import { Provider } from 'react-redux';
import renderer from 'react-test-renderer';
import { GameState, actions } from './redux/game';
import { Player, undetermined, tie } from './lib/Player';
import { createEmptyBoard } from './lib/Board';
import App from './App';
const middlewares: Middleware[] = [];
const mockStore = configureStore(middlewares);
const WrappedApp = (store: Store): React.ReactElement => (
<Provider store={store}>
<App />
</Provider>
);
describe('App component', () => {
it('The header must show the current player and winner', () => {
const initialState: GameState = {
board: createEmptyBoard(),
activeGame: false,
winner: undetermined,
currentPlayer: Player.X,
};
/* Test with first player / winner combination */
let store = mockStore(initialState);
let component = renderer.create(WrappedApp(store));
let h1 = component.root.findByType('h1');
expect(h1.children.join('')).toEqual('X - undetermined');
/* Test with second player / winner combination */
store = mockStore({ ...initialState, winner: tie, currentPlayer: Player.O });
component = renderer.create(WrappedApp(store));
h1 = component.root.findByType('h1');
expect(h1.children.join('')).toEqual('O - tie');
});
it('must call the new game action when the start game button is clicked', () => {
const initialState: GameState = {
board: createEmptyBoard(),
activeGame: false,
winner: undetermined,
currentPlayer: Player.X,
};
const store = mockStore(initialState);
const component = renderer.create(WrappedApp(store));
/* Click the new game button */
component.root.findByType('button').props.onClick();
const calledActions = store.getActions();
expect(calledActions.length).toEqual(1);
expect(calledActions[0]).toEqual(actions.startGame());
});
});

View File

@@ -1,9 +0,0 @@
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
it.skip('renders without crashing', () => {
const div = document.createElement('div');
ReactDOM.render(<App />, div);
ReactDOM.unmountComponentAtNode(div);
});

View File

@@ -1,6 +1,59 @@
describe('My Connected React-Redux Component', () => {
it('should render with given state from Redux store', () => {
import React from 'react';
import configureStore from 'redux-mock-store';
import { Middleware, Store } from 'redux';
import { Provider } from 'react-redux';
import renderer from 'react-test-renderer';
import { GameState, actions } from '../redux/game';
import { Player, undetermined } from '../lib/Player';
import { createEmptyBoard, empty } from '../lib/Board';
import Board from './Board';
const middlewares: Middleware[] = [];
const mockStore = configureStore(middlewares);
const WrappedBoard = (store: Store): React.ReactElement => (
<Provider store={store}>
<Board />
</Provider>
);
describe('Board component', () => {
it('must call the MAKE_MOVE action when a cell is clicked and the game is active', () => {
const initialState: GameState = {
board: createEmptyBoard(),
activeGame: false,
winner: undetermined,
currentPlayer: Player.X,
};
const store = mockStore(initialState);
const component = renderer.create(WrappedBoard(store));
/* Click on cell 8 (row 2, cell 2) */
component.root.findAllByType('td')[8].props.onClick();
const calledActions = store.getActions();
expect(calledActions.length).toEqual(1);
expect(calledActions[0]).toEqual(actions.makeMove(2, 2, Player.X));
});
it('should dispatch an action on button click', () => {
it('must render the board according to the redux state', () => {
const board = [[Player.X, Player.O, Player.X], [Player.O, Player.X, Player.O], [empty, Player.X, Player.O]];
const initialState: GameState = {
board,
activeGame: false,
winner: undetermined,
currentPlayer: Player.X,
};
const store = mockStore(initialState);
const component = renderer.create(WrappedBoard(store));
/* Get all the table cells */
const tds = component.root.findAllByType('td');
/* Check all the table cells against the values of the board */
board
.reduce((cells, row) => cells.concat(row), [])
.forEach((cell, index) => {
expect(tds[index].children[0]).toEqual(cell === empty ? '' : cell);
});
});
});

View File

@@ -87,7 +87,7 @@ export const getBoardSummary = (board: Board): BoardSummary => {
} else if (result.filter((lineResult) => lineResult === Player.O).length > 0) {
summary.winner = Player.O;
} else {
summary.winner = undetermined;
throw new Error('Unsupported scoring case');
}
return summary;
};

View File

@@ -1,4 +1,5 @@
import { reducer, GameState, actions } from './game';
import { Action, Dispatch, MiddlewareAPI } from 'redux';
import { reducer, GameState, actions, middleware } from './game';
import { Player, tie, undetermined } from '../lib/Player';
import { empty, createEmptyBoard } from '../lib/Board';
import { getBoardSummary, BoardSummary } from '../lib/scoring';
@@ -126,4 +127,69 @@ describe('reducer with name', () => {
expect(updatedState).toEqual({ ...state, winner: Player.X });
});
});
describe('MAKE_MOVE', () => {
it('should update the board, check if there is a winner and switch to the next player', () => {
const state: GameState = {
currentPlayer: Player.O,
activeGame: true,
board: [],
winner: undetermined,
};
const makeMoveAction = actions.makeMove(1, 2, Player.O);
const dispatch = jest.fn();
const middlewareAPI: MiddlewareAPI<Dispatch, GameState> = {
dispatch,
getState: (): GameState => state,
};
const next = ((action: Action<any>): Action<any> => action) as Dispatch;
middleware(middlewareAPI)(next)(makeMoveAction);
expect(dispatch.mock.calls.length).toEqual(3);
expect(dispatch.mock.calls[0][0]).toEqual(actions.updateBoard(1, 2, Player.O));
expect(dispatch.mock.calls[1][0]).toEqual(actions.determineWinner());
expect(dispatch.mock.calls[2][0]).toEqual(actions.nextPlayer());
});
it('should update the board, check if there is a winner and if there is a winner end the game', () => {
const state: GameState = {
currentPlayer: Player.O,
activeGame: true,
board: [],
winner: Player.O,
};
const makeMoveAction = actions.makeMove(1, 2, Player.O);
const dispatch = jest.fn();
const middlewareAPI: MiddlewareAPI<Dispatch, GameState> = {
dispatch,
getState: (): GameState => state,
};
const next = ((action: Action<any>): Action<any> => action) as Dispatch;
middleware(middlewareAPI)(next)(makeMoveAction);
expect(dispatch.mock.calls.length).toEqual(3);
expect(dispatch.mock.calls[0][0]).toEqual(actions.updateBoard(1, 2, Player.O));
expect(dispatch.mock.calls[1][0]).toEqual(actions.determineWinner());
expect(dispatch.mock.calls[2][0]).toEqual(actions.endGame());
});
it('should not do anything if the game is not active', () => {
const state: GameState = {
currentPlayer: Player.O,
activeGame: false,
board: [],
winner: undetermined,
};
const makeMoveAction = actions.makeMove(1, 2, Player.O);
const dispatch = jest.fn();
const middlewareAPI: MiddlewareAPI<Dispatch, GameState> = {
dispatch,
getState: (): GameState => state,
};
const next = ((action: Action<any>): Action<any> => action) as Dispatch;
middleware(middlewareAPI)(next)(makeMoveAction);
expect(dispatch.mock.calls.length).toEqual(0);
});
});
});

View File

@@ -52,14 +52,14 @@ export interface GameState {
export type IGameState = GameState;
const defaultState: IGameState = {
export const initialState: IGameState = {
currentPlayer: Player.X,
activeGame: false,
board: createEmptyBoard(),
winner: undetermined,
};
export const reducer: Reducer<IGameState, Action> = (state: IGameState = defaultState, action: Action): IGameState => {
export const reducer: Reducer<IGameState, Action> = (state: IGameState = initialState, action: Action): IGameState => {
if (action.type === START_GAME) {
return { ...state, board: createEmptyBoard(), activeGame: true, winner: undetermined, currentPlayer: Player.X };
}
@@ -109,11 +109,12 @@ export const middleware = ({ dispatch, getState }: MiddlewareAPI<Dispatch<Action
}
const { rowIndex, cellIndex, currentPlayer } = (action as MakeMoveAction).payload;
dispatch(actions.updateBoard(rowIndex, cellIndex, currentPlayer));
dispatch(actions.nextPlayer());
dispatch(actions.determineWinner());
const { winner } = getState();
if (winner !== undetermined) {
dispatch(actions.endGame());
} else {
dispatch(actions.nextPlayer());
}
}
};

9
src/redux/store.spec.ts Normal file
View File

@@ -0,0 +1,9 @@
import { createStore } from './store';
import { initialState } from './game';
describe('createStore', () => {
it('must return the correct initial state', () => {
const store = createStore();
expect(store.getState()).toEqual(initialState);
});
});