├── src
├── symbols
│ └── symbols.js
├── index.css
├── actions
│ └── actions.js
├── components
│ ├── BlankSymbol.test.js
│ ├── App.test.js
│ ├── App.js
│ ├── OSymbol.js
│ ├── BlankSymbol.js
│ ├── XSymbol.js
│ ├── Result.test.js
│ ├── App.css
│ ├── Result.js
│ ├── Board.test.js
│ └── Board.js
├── index.js
├── reducers
│ ├── gameReducer.test.js
│ └── gameReducer.js
├── logic
│ ├── logic.js
│ └── logic.test.js
├── logo.svg
└── registerServiceWorker.js
├── public
├── favicon.ico
├── manifest.json
└── index.html
├── .gitignore
├── README.md
├── package.json
└── .travis.yml
/src/symbols/symbols.js:
--------------------------------------------------------------------------------
1 | export const X = 'x';
2 | export const O = 'o';
3 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/belfz/tic-tac-react/HEAD/public/favicon.ico
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | padding: 0;
4 | font-family: sans-serif;
5 | }
6 |
7 | * {
8 | box-sizing: border-box;
9 | }
10 |
--------------------------------------------------------------------------------
/src/actions/actions.js:
--------------------------------------------------------------------------------
1 | export const addSymbol = (row, position, symbol) => ({
2 | type: 'ADD_SYMBOL',
3 | symbol,
4 | row,
5 | position
6 | });
7 |
8 | export const startAgain = () => ({
9 | type: 'START_AGAIN'
10 | });
11 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See http://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | node_modules
5 |
6 | # testing
7 | coverage
8 |
9 | # production
10 | build
11 |
12 | # misc
13 | .DS_Store
14 | .env
15 | npm-debug.log
16 |
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "Tic-Tac-React",
3 | "name": "Tic-Tac-React",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "192x192",
8 | "type": "image/png"
9 | }
10 | ],
11 | "start_url": "./index.html",
12 | "display": "standalone",
13 | "theme_color": "#000000",
14 | "background_color": "#ffffff"
15 | }
16 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Tic-tac-react
2 |
3 | [](https://travis-ci.org/belfz/tic-tac-react)
4 |
5 | A tic-tac-toe game (for two players) implemented with react and redux.
6 | [Click here to play!](https://belfz.github.io/tic-tac-react/)
7 |
8 | This project was bootstrapped with [Create React App](https://github.com/facebookincubator/create-react-app).
9 |
--------------------------------------------------------------------------------
/src/components/BlankSymbol.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { shallow } from 'enzyme';
3 | import BlankSymbol from './BlankSymbol';
4 |
5 | it('Should call a passed addSymbol callback when it is clicked', () => {
6 | const addSymbol = jest.fn();
7 | const wrapper = shallow();
8 | wrapper.simulate('click');
9 | expect(addSymbol.mock.calls.length).toBe(1);
10 | });
11 |
--------------------------------------------------------------------------------
/src/components/App.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { shallow } from 'enzyme';
3 | import App from './App';
4 | import Result from './Result';
5 | import Board from './Board';
6 |
7 | it('Should render an App component with Result and Board components', () => {
8 | const wrapper = shallow().dive();
9 | expect(wrapper.find(Result).length).toBe(1);
10 | expect(wrapper.find(Board).length).toBe(1);
11 | });
12 |
--------------------------------------------------------------------------------
/src/components/App.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Board from './Board';
3 | import Result from './Result';
4 | import styled from 'styled-components';
5 | import './App.css';
6 |
7 | const App = ({className}) => {
8 | return (
9 |
10 |
11 |
12 |
13 | );
14 | }
15 |
16 | export default styled(App)`
17 | font-family: Courier New, Courier, monospace;
18 | margin: 0 auto;
19 | width: 200px;
20 | `;
21 |
--------------------------------------------------------------------------------
/src/components/OSymbol.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { Symbol } from './BlankSymbol';
4 |
5 | const OSymbol = (props) => {
6 | return (
7 |
8 |
11 |
12 | );
13 | };
14 |
15 | OSymbol.propTypes = {
16 | position: PropTypes.number.isRequired
17 | };
18 |
19 | export default OSymbol;
20 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import {Provider} from 'react-redux';
4 | import {createStore} from 'redux';
5 | import { initialState, gameReducer } from './reducers/gameReducer';
6 | import App from './components/App';
7 | import registerServiceWorker from './registerServiceWorker';
8 | import './index.css';
9 |
10 | const store = createStore(gameReducer, initialState);
11 |
12 | ReactDOM.render(
13 |
14 |
15 | ,
16 | document.getElementById('root')
17 | );
18 |
19 | registerServiceWorker();
20 |
--------------------------------------------------------------------------------
/src/components/BlankSymbol.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import styled from 'styled-components';
4 |
5 | export const Symbol = styled.div`
6 | background-color: white;
7 | border: 1px solid black;
8 | height: 60px;
9 | margin: 1px;
10 | transition: background-color .5s ease;
11 | width: 60px;
12 | `;
13 |
14 | const BlankSymbol = (props) => {
15 | return props.addSymbol(props.turn)}>;
16 | };
17 |
18 | BlankSymbol.propTypes = {
19 | addSymbol: PropTypes.func.isRequired
20 | };
21 |
22 | export default BlankSymbol;
23 |
--------------------------------------------------------------------------------
/src/components/XSymbol.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { Symbol } from './BlankSymbol';
4 |
5 | const XSymbol = (props) => {
6 | return (
7 |
8 |
12 |
13 | );
14 | };
15 |
16 | XSymbol.propTypes = {
17 | position: PropTypes.number.isRequired
18 | };
19 |
20 | export default XSymbol;
21 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "tic-tac-react",
3 | "version": "0.1.0",
4 | "private": true,
5 | "homepage": "http://belfz.github.io/tic-tac-react",
6 | "devDependencies": {
7 | "enzyme": "^2.7.0",
8 | "gh-pages": "^0.12.0",
9 | "react-addons-test-utils": "^15.4.2",
10 | "react-scripts": "1.0.17"
11 | },
12 | "dependencies": {
13 | "lodash": "^4.17.4",
14 | "prop-types": "^15.6.0",
15 | "react": "^15.4.2",
16 | "react-dom": "^15.4.2",
17 | "react-redux": "^5.0.1",
18 | "redux": "^3.6.0",
19 | "styled-components": "^2.3.3"
20 | },
21 | "scripts": {
22 | "start": "react-scripts start",
23 | "build": "react-scripts build",
24 | "deploy": "npm run build&&gh-pages -d build",
25 | "test": "react-scripts test --env=jsdom",
26 | "eject": "react-scripts eject"
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/components/Result.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { shallow } from 'enzyme';
3 | import { PureResult as Result } from './Result';
4 |
5 | it('Should render the Result component with message about current turn', () => {
6 | const wrapper = shallow();
7 | expect(wrapper.find('p').node.props.children).toEqual('It\'s O\'s turn.');
8 | });
9 |
10 | it('Should render the Result component with message about winning symbol', () => {
11 | const wrapper = shallow();
12 | expect(wrapper.find('p').node.props.children).toEqual('Yay! X won!');
13 | });
14 |
15 | it('Should render the Result component with message about the draw', () => {
16 | const wrapper = shallow();
17 | expect(wrapper.find('p').node.props.children).toEqual('We have a draw!');
18 | });
19 |
--------------------------------------------------------------------------------
/src/components/App.css:
--------------------------------------------------------------------------------
1 | .board.won-row0 .row0 .symbol,
2 | .board.won-row1 .row1 .symbol,
3 | .board.won-row2 .row2 .symbol,
4 | .board.won-column0 .symbol.column0,
5 | .board.won-column1 .symbol.column1,
6 | .board.won-column2 .symbol.column2,
7 | .board.won-leftSlant .row0 .symbol.column0,
8 | .board.won-leftSlant .row1 .symbol.column1,
9 | .board.won-leftSlant .row2 .symbol.column2,
10 | .board.won-rightSlant .row0 .symbol.column2,
11 | .board.won-rightSlant .row1 .symbol.column1,
12 | .board.won-rightSlant .row2 .symbol.column0 {
13 | background-color: mediumseagreen;
14 | }
15 |
16 | .board.draw .symbol {
17 | background-color: palevioletred;
18 | }
19 |
20 | .row {
21 | display: flex;
22 | }
23 |
24 |
25 | @keyframes symbol-appear {
26 | from {
27 | transform: scale(1.9);
28 | }
29 | to {
30 | transform: scale(1);
31 | }
32 | }
33 |
34 | svg {
35 | animation-name: symbol-appear;
36 | animation-duration: .15s;
37 | }
38 |
--------------------------------------------------------------------------------
/src/components/Result.js:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react';
2 | import PropTypes from 'prop-types';
3 | import {connect} from 'react-redux';
4 |
5 | class Result extends Component {
6 | render () {
7 | let result = '';
8 | if (this.props.turn) {
9 | result = `It's ${this.props.turn.toUpperCase()}'s turn.`;
10 | }
11 | if (this.props.won) {
12 | result = `Yay! ${this.props.won.toUpperCase()} won!`
13 | } else if (this.props.draw) {
14 | result = 'We have a draw!';
15 | }
16 | return (
17 |
18 |
19 | {result}
20 |
21 |
22 | );
23 | }
24 | }
25 |
26 | Result.propTypes = {
27 | won: PropTypes.string,
28 | turn: PropTypes.string.isRequired,
29 | draw: PropTypes.bool.isRequired
30 | };
31 |
32 | export default connect(
33 | ({won, turn, draw}) => ({
34 | won, turn, draw
35 | })
36 | )(Result);
37 |
38 | export {Result as PureResult};
39 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - 8.9.1
4 | before_script:
5 | - git config --global user.email "marcinbar1@gmail.com"
6 | - git config --global user.name "belfz"
7 | - git remote rm origin
8 | - git remote add origin https://user:${GH_TOKEN}@github.com/belfz/tic-tac-react.git
9 | after_success:
10 | - npm run deploy
11 | env:
12 | global:
13 | secure: LEgabKoDNd/LklOyTSwNpnS58uMMLFCF808dpaf1csUmSGsUIBxOXY8OT36ZTVH5dLI3LLTiUT+6rgZY/FqlmVgnL1m6f5GKyh1V6g2AWEOB1OJ0rVyzcpgbsB5G5zsBmfMUXr4LKbdnI05WtPH9i1lgUFk1D54v0+xFAnwGgkVkgufyLwI0ZQg590sKfnW3nOGMOfB6yhDnUALsJ0IdphxJGGGE8FGx/O3C2ecR593PfQYiGjj+DNBwcqC5zmt2JMSzxmVVuuloCF5GhvRoV7wZ/tmRzK3Rzz3cSbQ+TVpS78QOoijWaDp75fEVp2qE7jxqLGGRLAGAjm+urMc+0JJK9TKf1kGsNR94ll1ZqflomZJy5sDP+MvKHlMx8a3ZswfEVqOA5CHXCzrml2Ki+B2yHw97qU3mxMfT+LA+tWFxZzNz2sytDMzhtSoUlAzHCLL/ci5I0tDhkf7f7zMMwF6dmM4ZUBKVd9VtwKuZ/necX1r+F8L3ger7mTPfpzHJUfzUSfELECQwUfOgWHhR8OSTrPRV5z7jt/vvtsBm+/Qj8h9ryumvMDpkkxHpXJrMq0gtLK1z4rxigUCRUkxzE+0QNkaGSPTpYJHInozxNA+9qjJwaUyZU7rrAwNu5GVDaiZVHkHjLjtH5/YuvFwNvs5iFwRpHBrRd8g7euz6x+k=
14 |
--------------------------------------------------------------------------------
/src/reducers/gameReducer.test.js:
--------------------------------------------------------------------------------
1 | import { initialState, gameReducer } from './gameReducer';
2 | import { X, O } from '../symbols/symbols';
3 |
4 | it('Should add a symbol at given position and change turn', () => {
5 | const state = {
6 | board: {
7 | 0: ['', '', ''],
8 | 1: ['', '', ''],
9 | 2: ['', '', '']
10 | },
11 | won: undefined,
12 | wonLine: undefined,
13 | draw: false,
14 | turn: O
15 | };
16 | const nextState = gameReducer(state, {type: 'ADD_SYMBOL', symbol: O, row: 0, position: 0});
17 | expect(nextState.board[0]).toEqual([O, '', '']);
18 | expect(nextState.turn).toEqual(X);
19 | });
20 |
21 | it('Should set "won" symbol when a winning line is set', () => {
22 | const state = {
23 | board: {
24 | 0: [X, O, ''],
25 | 1: ['', X, ''],
26 | 2: [O, '', '']
27 | },
28 | won: undefined,
29 | wonLine: undefined,
30 | draw: false,
31 | turn: X
32 | };
33 | const nextState = gameReducer(state, {type: 'ADD_SYMBOL', symbol: X, row: 2, position: 2});
34 | expect(nextState.won).toEqual(X);
35 | });
36 |
37 | it('Should reset the state to initial', () => {
38 | const state = {
39 | board: {
40 | 0: [X, O, ''],
41 | 1: ['', X, ''],
42 | 2: [O, '', '']
43 | },
44 | won: undefined,
45 | wonLine: undefined,
46 | draw: false,
47 | turn: X
48 | };
49 | const nextState = gameReducer(state, {type: 'START_AGAIN'});
50 | expect(nextState).toEqual(initialState);
51 | });
52 |
--------------------------------------------------------------------------------
/src/reducers/gameReducer.js:
--------------------------------------------------------------------------------
1 | import { X, O } from '../symbols/symbols';
2 | import { resultForSymbol } from '../logic/logic';
3 | import * as _ from 'lodash';
4 |
5 | export const initialState = {
6 | board: {
7 | 0: ['', '', ''],
8 | 1: ['', '', ''],
9 | 2: ['', '', '']
10 | },
11 | won: undefined,
12 | wonLine: undefined,
13 | draw: false,
14 | turn: O
15 | };
16 |
17 | export const gameReducer = (state, action) => {
18 | switch (action.type) {
19 | case 'ADD_SYMBOL':
20 | const {symbol, row, position} = action;
21 | const newState = _.cloneDeep(state);
22 | newState.board[row][position] = symbol;
23 |
24 | const xResult = resultForSymbol(X, newState.board);
25 | const oResult = resultForSymbol(O, newState.board);
26 |
27 | if (xResult.won) {
28 | newState.won = X;
29 | newState.wonLine = xResult.line;
30 | }
31 |
32 | if (oResult.won) {
33 | newState.won = O;
34 | newState.wonLine = oResult.line;
35 | }
36 |
37 | if (!newState.won) {
38 | newState.turn = newState.turn === O ? X : O;
39 | }
40 |
41 | const boardIsFull = [
42 | ...newState.board[0],
43 | ...newState.board[1],
44 | ...newState.board[2]
45 | ]
46 | .filter(symbol => symbol !== '')
47 | .length === 9;
48 |
49 | if (boardIsFull && !newState.won) {
50 | newState.draw = true;
51 | }
52 |
53 | return newState;
54 | case 'START_AGAIN':
55 | return initialState;
56 | default:
57 | return state;
58 | }
59 | };
60 |
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
22 | React App
23 |
24 |
25 |
26 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/src/logic/logic.js:
--------------------------------------------------------------------------------
1 | const countInRow = (symbol, row) => row.filter(el => el === symbol).length;
2 | const hasWonInRow = (symbol, row) => countInRow(symbol, row) === 3;
3 | export const hasThreatInRow = (symbol, row) => countInRow(symbol, row) === 2;
4 |
5 | const countInColumn = (symbol, colNumber, ...rows) => rows.map(row => row[colNumber]).filter(el => el === symbol).length;
6 | const hasWonInColumn = (symbol, colNumber, ...rows) => countInColumn(symbol, colNumber, ...rows) === 3;
7 | export const hasThreatInColumn = (symbol, colNumber, ...rows) => countInColumn(symbol, colNumber, ...rows) === 2;
8 |
9 | const countInLeftSlant = (symbol, ...rows) => {
10 | const [row0, row1, row2] = rows;
11 | return [row0[0], row1[1], row2[2]].filter(el => el === symbol).length;
12 | };
13 | const hasWonInLeftSlant = (symbol, ...rows) => countInLeftSlant(symbol, ...rows) === 3;
14 | export const hasThreatInLeftSlant = (symbol, ...rows) => countInLeftSlant(symbol, ...rows) === 2;
15 |
16 | const countInRightSlant = (symbol, ...rows) => {
17 | const [row0, row1, row2] = rows;
18 | return [row0[2], row1[1], row2[0]].filter(el => el === symbol).length;
19 | };
20 | const hasWonInRightSlant = (symbol, ...rows) => countInRightSlant(symbol, ...rows) === 3;
21 | export const hasThreatInRightSlant = (symbol, ...rows) => countInRightSlant(symbol, ...rows) === 2;
22 |
23 | export const resultForSymbol = (symbol, board) => {
24 | const rows = Object.keys(board).map(row => board[row]);
25 | return [
26 | {line: 'row0', won: hasWonInRow(symbol, board[0])},
27 | {line: 'row1', won: hasWonInRow(symbol, board[1])},
28 | {line: 'row2', won: hasWonInRow(symbol, board[2])},
29 | {line: 'column0', won: hasWonInColumn(symbol, 0, ...rows)},
30 | {line: 'column1', won: hasWonInColumn(symbol, 1, ...rows)},
31 | {line: 'column2', won: hasWonInColumn(symbol, 2, ...rows)},
32 | {line: 'leftSlant', won: hasWonInLeftSlant(symbol, ...rows)},
33 | {line: 'rightSlant', won: hasWonInRightSlant(symbol, ...rows)}
34 | ]
35 | .reduce((answer, nextCheck) => {
36 | return nextCheck.won ? nextCheck : answer;
37 | }, {won: false});
38 | };
39 |
--------------------------------------------------------------------------------
/src/components/Board.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { shallow } from 'enzyme';
3 | import { X, O } from '../symbols/symbols';
4 | import BlankSymbol from './BlankSymbol';
5 | import XSymbol from './XSymbol';
6 | import OSymbol from './OSymbol';
7 | import { PureBoard as Board } from './Board';
8 |
9 | const board = {
10 | 0: [X, O, ''],
11 | 1: ['', X, O],
12 | 2: [X, X, O]
13 | };
14 |
15 | it('Should render Board with symbols', () => {
16 | const startAgain = jest.fn();
17 | const addSymbol = jest.fn();
18 | const wrapper = shallow();
19 | expect(wrapper.find(XSymbol).length).toBe(4);
20 | expect(wrapper.find(OSymbol).length).toBe(3);
21 | expect(wrapper.find(BlankSymbol).length).toBe(2);
22 | });
23 |
24 | it('Should not display a "start again" text when neither won and there was no draw', () => {
25 | const startAgain = jest.fn();
26 | const addSymbol = jest.fn();
27 | const wrapper = shallow();
28 | expect(wrapper.find('p.startAgain').length).toBe(0);
29 | });
30 |
31 | it('Should display a "start again" text when one symbol won', () => {
32 | const startAgain = jest.fn();
33 | const addSymbol = jest.fn();
34 | const wrapper = shallow();
35 | expect(wrapper.find('p.startAgain').length).toBe(1);
36 | });
37 |
38 | it('Should display a "start again" text when there was a draw', () => {
39 | const startAgain = jest.fn();
40 | const addSymbol = jest.fn();
41 | const wrapper = shallow();
42 | expect(wrapper.find('p.startAgain').length).toBe(1);
43 | });
44 |
45 | it('Should call a passed callback when clicked upon the "start again"', () => {
46 | const startAgain = jest.fn();
47 | const addSymbol = jest.fn();
48 | const wrapper = shallow();
49 | wrapper.find('p.startAgain').simulate('click');
50 | expect(startAgain.mock.calls.length).toBe(1);
51 | });
52 |
--------------------------------------------------------------------------------
/src/logo.svg:
--------------------------------------------------------------------------------
1 |
8 |
--------------------------------------------------------------------------------
/src/components/Board.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 | import BlankSymbol from './BlankSymbol';
4 | import XSymbol from './XSymbol';
5 | import OSymbol from './OSymbol';
6 | import { X, O } from '../symbols/symbols';
7 | import { addSymbol, startAgain } from '../actions/actions';
8 | import { connect } from 'react-redux';
9 |
10 | class Board extends Component {
11 | addSymbol (rowIndex, position, symbol) {
12 | !this.props.won && this.props.addSymbol(rowIndex, position, symbol);
13 | }
14 |
15 | getSymbol(rowIndex, position, symbol) {
16 | if (symbol === X) {
17 | return ;
18 | }
19 | if (symbol === O) {
20 | return ;
21 | }
22 | return ;
23 | }
24 |
25 | render() {
26 | const wonClass = this.props.won ? ` won-${this.props.wonLine}` : '';
27 | const drawClass = this.props.draw ? ' draw' : '';
28 | const boardClass = 'board' + wonClass + drawClass;
29 | return (
30 |
31 | {
32 | Object.keys(this.props.board)
33 | .map(rowIndex => {
34 | return (
35 |
36 | {
37 | this.props.board[rowIndex].map((symbol, positon) => {
38 | return this.getSymbol(rowIndex, positon, symbol);
39 | })
40 | }
41 |
42 | );
43 | })
44 | }
45 | {
46 | this.props.won || this.props.draw ?
47 |
48 | Click to start again!
49 |
: false
50 | }
51 |
52 | );
53 | }
54 | }
55 |
56 | Board.propTypes = {
57 | board: PropTypes.object.isRequired,
58 | turn: PropTypes.string.isRequired,
59 | won: PropTypes.string,
60 | draw: PropTypes.bool.isRequired,
61 | wonLine: PropTypes.string,
62 | addSymbol: PropTypes.func.isRequired,
63 | startAgain: PropTypes.func.isRequired
64 | };
65 |
66 | export default connect(
67 | ({board, turn, won, draw, wonLine}) => ({
68 | board, turn, won, draw, wonLine
69 | }),
70 | (dispatch) => {
71 | return {
72 | addSymbol (rowIndex, position, symbol) {
73 | dispatch(addSymbol(rowIndex, position, symbol));
74 | },
75 | startAgain () {
76 | dispatch(startAgain());
77 | }
78 | };
79 | }
80 | )(Board);
81 |
82 | export {Board as PureBoard};
83 |
--------------------------------------------------------------------------------
/src/logic/logic.test.js:
--------------------------------------------------------------------------------
1 | import { resultForSymbol } from './logic';
2 | import { X, O } from '../symbols/symbols';
3 |
4 | it('Should indicate no winning result', () => {
5 | const board = {
6 | 0: ['', X, ''],
7 | 1: [O, '', O],
8 | 2: [X, '', '']
9 | };
10 | const xResult = resultForSymbol(X, board);
11 | const oResult = resultForSymbol(O, board);
12 | expect(xResult.won).toBe(false);
13 | expect(oResult.won).toBe(false);
14 | });
15 |
16 | it('Should indicate X as a winner in row0', () => {
17 | const board = {
18 | 0: [X, X, X],
19 | 1: ['', O, ''],
20 | 2: ['', '', O]
21 | };
22 | const xResult = resultForSymbol(X, board);
23 | const oResult = resultForSymbol(O, board);
24 | expect(xResult.won).toBe(true);
25 | expect(xResult.line).toBe('row0');
26 | expect(oResult.won).toBe(false);
27 | });
28 |
29 | it('Should indicate X as a winner in row1', () => {
30 | const board = {
31 | 0: ['', '', O],
32 | 1: [X, X, X],
33 | 2: ['', '', O]
34 | };
35 | const xResult = resultForSymbol(X, board);
36 | const oResult = resultForSymbol(O, board);
37 | expect(xResult.won).toBe(true);
38 | expect(xResult.line).toBe('row1');
39 | expect(oResult.won).toBe(false);
40 | });
41 |
42 | it('Should indicate X as a winner in row2', () => {
43 | const board = {
44 | 0: ['', O, ''],
45 | 1: ['', O, ''],
46 | 2: [X, X, X]
47 | };
48 | const xResult = resultForSymbol(X, board);
49 | const oResult = resultForSymbol(O, board);
50 | expect(xResult.won).toBe(true);
51 | expect(xResult.line).toBe('row2');
52 | expect(oResult.won).toBe(false);
53 | });
54 |
55 | it('Should indicate O as a winner in column0', () => {
56 | const board = {
57 | 0: [O, X, X],
58 | 1: [O, '', ''],
59 | 2: [O, '', X]
60 | };
61 | const xResult = resultForSymbol(X, board);
62 | const oResult = resultForSymbol(O, board);
63 | expect(xResult.won).toBe(false);
64 | expect(oResult.won).toBe(true);
65 | expect(oResult.line).toBe('column0');
66 | });
67 |
68 | it('Should indicate O as a winner in column1', () => {
69 | const board = {
70 | 0: ['', O, X],
71 | 1: ['', O, ''],
72 | 2: [X, O, X]
73 | };
74 | const xResult = resultForSymbol(X, board);
75 | const oResult = resultForSymbol(O, board);
76 | expect(xResult.won).toBe(false);
77 | expect(oResult.won).toBe(true);
78 | expect(oResult.line).toBe('column1');
79 | });
80 |
81 | it('Should indicate O as a winner in column2', () => {
82 | const board = {
83 | 0: ['', X, O],
84 | 1: [X, '', O],
85 | 2: [X, '', O]
86 | };
87 | const xResult = resultForSymbol(X, board);
88 | const oResult = resultForSymbol(O, board);
89 | expect(xResult.won).toBe(false);
90 | expect(oResult.won).toBe(true);
91 | expect(oResult.line).toBe('column2');
92 | });
93 |
94 | it('Should indicate O as a winner in leftSlant', () => {
95 | const board = {
96 | 0: [O, X, X],
97 | 1: ['', O, ''],
98 | 2: [X, '', O]
99 | };
100 | const xResult = resultForSymbol(X, board);
101 | const oResult = resultForSymbol(O, board);
102 | expect(xResult.won).toBe(false);
103 | expect(oResult.won).toBe(true);
104 | expect(oResult.line).toBe('leftSlant');
105 | });
106 |
107 | it('Should indicate X as a winner in rightSlant', () => {
108 | const board = {
109 | 0: ['', O, X],
110 | 1: ['', X, ''],
111 | 2: [X, '', O]
112 | };
113 | const xResult = resultForSymbol(X, board);
114 | const oResult = resultForSymbol(O, board);
115 | expect(xResult.won).toBe(true);
116 | expect(xResult.line).toBe('rightSlant');
117 | expect(oResult.won).toBe(false);
118 | });
119 |
--------------------------------------------------------------------------------
/src/registerServiceWorker.js:
--------------------------------------------------------------------------------
1 | // In production, we register a service worker to serve assets from local cache.
2 |
3 | // This lets the app load faster on subsequent visits in production, and gives
4 | // it offline capabilities. However, it also means that developers (and users)
5 | // will only see deployed updates on the "N+1" visit to a page, since previously
6 | // cached resources are updated in the background.
7 |
8 | // To learn more about the benefits of this model, read https://goo.gl/KwvDNy.
9 | // This link also includes instructions on opting out of this behavior.
10 |
11 | const isLocalhost = Boolean(
12 | window.location.hostname === 'localhost' ||
13 | // [::1] is the IPv6 localhost address.
14 | window.location.hostname === '[::1]' ||
15 | // 127.0.0.1/8 is considered localhost for IPv4.
16 | window.location.hostname.match(
17 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
18 | )
19 | );
20 |
21 | export default function register() {
22 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
23 | // The URL constructor is available in all browsers that support SW.
24 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location);
25 | if (publicUrl.origin !== window.location.origin) {
26 | // Our service worker won't work if PUBLIC_URL is on a different origin
27 | // from what our page is served on. This might happen if a CDN is used to
28 | // serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374
29 | return;
30 | }
31 |
32 | window.addEventListener('load', () => {
33 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
34 |
35 | if (isLocalhost) {
36 | // This is running on localhost. Lets check if a service worker still exists or not.
37 | checkValidServiceWorker(swUrl);
38 | } else {
39 | // Is not local host. Just register service worker
40 | registerValidSW(swUrl);
41 | }
42 | });
43 | }
44 | }
45 |
46 | function registerValidSW(swUrl) {
47 | navigator.serviceWorker
48 | .register(swUrl)
49 | .then(registration => {
50 | registration.onupdatefound = () => {
51 | const installingWorker = registration.installing;
52 | installingWorker.onstatechange = () => {
53 | if (installingWorker.state === 'installed') {
54 | if (navigator.serviceWorker.controller) {
55 | // At this point, the old content will have been purged and
56 | // the fresh content will have been added to the cache.
57 | // It's the perfect time to display a "New content is
58 | // available; please refresh." message in your web app.
59 | console.log('New content is available; please refresh.');
60 | } else {
61 | // At this point, everything has been precached.
62 | // It's the perfect time to display a
63 | // "Content is cached for offline use." message.
64 | console.log('Content is cached for offline use.');
65 | }
66 | }
67 | };
68 | };
69 | })
70 | .catch(error => {
71 | console.error('Error during service worker registration:', error);
72 | });
73 | }
74 |
75 | function checkValidServiceWorker(swUrl) {
76 | // Check if the service worker can be found. If it can't reload the page.
77 | fetch(swUrl)
78 | .then(response => {
79 | // Ensure service worker exists, and that we really are getting a JS file.
80 | if (
81 | response.status === 404 ||
82 | response.headers.get('content-type').indexOf('javascript') === -1
83 | ) {
84 | // No service worker found. Probably a different app. Reload the page.
85 | navigator.serviceWorker.ready.then(registration => {
86 | registration.unregister().then(() => {
87 | window.location.reload();
88 | });
89 | });
90 | } else {
91 | // Service worker found. Proceed as normal.
92 | registerValidSW(swUrl);
93 | }
94 | })
95 | .catch(() => {
96 | console.log(
97 | 'No internet connection found. App is running in offline mode.'
98 | );
99 | });
100 | }
101 |
102 | export function unregister() {
103 | if ('serviceWorker' in navigator) {
104 | navigator.serviceWorker.ready.then(registration => {
105 | registration.unregister();
106 | });
107 | }
108 | }
109 |
--------------------------------------------------------------------------------