Implement all component and redux tests
This commit is contained in:
@@ -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',
|
||||
{
|
||||
|
||||
@@ -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
58
src/App.spec.tsx
Normal 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());
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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
9
src/redux/store.spec.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user