From 9efc432e160b429a0643c38e28140bcf42af30a7 Mon Sep 17 00:00:00 2001 From: Noah Loomans Date: Thu, 28 Jun 2018 13:45:27 +0200 Subject: Add history to redux thunk --- .eslintrc | 3 +- package.json | 1 + src/client/react/App.js | 9 +- src/client/react/components/container/Menu.js | 14 +- src/client/react/components/presentational/Menu.js | 14 +- src/client/react/index.js | 11 +- src/client/react/lib/getHistory.js | 22 ++ src/client/react/lib/url.js | 10 + src/client/react/reducers.js | 142 ------------ src/client/react/reducers.test.js | 253 --------------------- src/client/react/store/actions.js | 14 ++ src/client/react/store/reducers.js | 142 ++++++++++++ src/client/react/store/reducers.test.js | 253 +++++++++++++++++++++ 13 files changed, 462 insertions(+), 426 deletions(-) create mode 100644 src/client/react/lib/getHistory.js delete mode 100644 src/client/react/reducers.js delete mode 100644 src/client/react/reducers.test.js create mode 100644 src/client/react/store/actions.js create mode 100644 src/client/react/store/reducers.js create mode 100644 src/client/react/store/reducers.test.js diff --git a/.eslintrc b/.eslintrc index 06eeac5..dab636c 100644 --- a/.eslintrc +++ b/.eslintrc @@ -11,6 +11,7 @@ "no-underscore-dangle": ["error", { "allow": ["_test", "__REDUX_DEVTOOLS_EXTENSION_COMPOSE__"] }], "no-prototype-builtins": "off", "react/forbid-prop-types": "off", - "react/prefer-stateless-function": "off" + "react/prefer-stateless-function": "off", + "import/prefer-default-export": "off" } } diff --git a/package.json b/package.json index 81a86da..403d038 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "express": "^4.16.3", "express-handlebars": "^3.0.0", "fuzzy-search": "^2.0.1", + "history": "^4.7.2", "iconv-lite": "^0.4.17", "jsdom": "^11.6.2", "left-pad": "^1.1.1", diff --git a/src/client/react/App.js b/src/client/react/App.js index e9ff565..a5a5cbd 100644 --- a/src/client/react/App.js +++ b/src/client/react/App.js @@ -2,7 +2,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import { - BrowserRouter, + Router, Route, Switch, Redirect, @@ -16,20 +16,21 @@ import User from './components/page/User'; export default class App extends React.Component { static propTypes = { store: PropTypes.object.isRequired, + history: PropTypes.object.isRequired, } render() { - const { store } = this.props; + const { store, history } = this.props; return ( - + - + ); } diff --git a/src/client/react/components/container/Menu.js b/src/client/react/components/container/Menu.js index 0e81fde..c9e6ba3 100644 --- a/src/client/react/components/container/Menu.js +++ b/src/client/react/components/container/Menu.js @@ -19,18 +19,12 @@ */ import { connect } from 'react-redux'; -import { withRouter } from 'react-router-dom'; -import { makeSetUser, userFromMatch } from '../../lib/url'; +import { showRoomFinder } from '../../store/actions'; import Menu from '../presentational/Menu'; -const mapStateToProps = (state, { match }) => ({ - user: userFromMatch(match), +const mapDispatchToProps = dispatch => ({ + showRoomFinder: () => dispatch(showRoomFinder()), }); -const mapDispatchToProps = (dispatch, { history }) => ({ - setUser: makeSetUser(history), - showRoomFinder: () => dispatch({ type: 'ROOM_FINDER/SHOW' }), -}); - -export default withRouter(connect(mapStateToProps, mapDispatchToProps)(Menu)); +export default connect(null, mapDispatchToProps)(Menu); diff --git a/src/client/react/components/presentational/Menu.js b/src/client/react/components/presentational/Menu.js index d43c1d3..077a2e4 100644 --- a/src/client/react/components/presentational/Menu.js +++ b/src/client/react/components/presentational/Menu.js @@ -23,30 +23,18 @@ import { PropTypes } from 'prop-types'; import { Button, ButtonIcon } from 'rmwc/Button'; import { SimpleMenu, MenuItem } from 'rmwc/Menu'; import { Icon } from 'rmwc/Icon'; -import users from '../../users'; import './Menu.scss'; class Menu extends React.Component { static propTypes = { - user: PropTypes.string, - setUser: PropTypes.func.isRequired, showRoomFinder: PropTypes.func.isRequired, } - static defaultProps = { - user: null, - } - onItemSelected(index) { switch (index) { case 'room_finder': { - const { setUser, user, showRoomFinder } = this.props; - - if (user == null || users.byId[user].type !== 'r') { - // We are not currently viewing a room, correct the situation. - setUser(users.allRoomIds[0]); - } + const { showRoomFinder } = this.props; showRoomFinder(); break; diff --git a/src/client/react/index.js b/src/client/react/index.js index 650e6de..2e35594 100644 --- a/src/client/react/index.js +++ b/src/client/react/index.js @@ -27,7 +27,10 @@ import { createStore, applyMiddleware, compose as reduxCompose } from 'redux'; import thunk from 'redux-thunk'; import moment from 'moment'; -import reducer from './reducers'; +import createHistory from 'history/createBrowserHistory'; + +import makeGetHistory from './lib/getHistory'; +import reducer from './store/reducers'; import App from './App'; import './index.scss'; @@ -35,6 +38,8 @@ import './index.scss'; // number logic is used. moment.locale('nl'); +const history = createHistory(); + const compose = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || reduxCompose; const store = createStore( @@ -42,12 +47,12 @@ const store = createStore( // Redux devtools extension // https://github.com/zalmoxisus/redux-devtools-extension compose( - applyMiddleware(thunk), + applyMiddleware(thunk.withExtraArgument(makeGetHistory(history))), ), ); ReactDOM.render( - , + , document.querySelector('#root'), ); diff --git a/src/client/react/lib/getHistory.js b/src/client/react/lib/getHistory.js new file mode 100644 index 0000000..642a9a8 --- /dev/null +++ b/src/client/react/lib/getHistory.js @@ -0,0 +1,22 @@ +import { + makeSetUser, + makeSetWeek, + weekFromLocation, + userFromLocation, +} from './url'; + +export default function makeGetHistory(history) { + return function getHistory() { + const user = userFromLocation(history.location); + const week = weekFromLocation(history.location); + const setUser = makeSetUser(history); + const setWeek = makeSetWeek(history); + + return { + user, + week, + setUser, + setWeek, + }; + }; +} diff --git a/src/client/react/lib/url.js b/src/client/react/lib/url.js index 644fd74..fcd3e6a 100644 --- a/src/client/react/lib/url.js +++ b/src/client/react/lib/url.js @@ -33,6 +33,16 @@ export function userFromMatch(match) { return user; } +export function userFromLocation(location) { + const match = location.pathname.match(/^\/([stcr])\/([-0-9a-zA-Z]+)/); + if (!match) return null; + + const user = `${match[1]}/${match[2]}`; + if (!users.allIds.includes(user)) return null; + + return user; +} + export function weekFromLocation(location) { const weekStr = queryString.parse(location.search).week; diff --git a/src/client/react/reducers.js b/src/client/react/reducers.js deleted file mode 100644 index 37d26f2..0000000 --- a/src/client/react/reducers.js +++ /dev/null @@ -1,142 +0,0 @@ -/** - * Copyright (C) 2018 Noah Loomans - * - * This file is part of rooster.hetmml.nl. - * - * rooster.hetmml.nl is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * rooster.hetmml.nl is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with rooster.hetmml.nl. If not, see . - * - */ - -import getSearchResults from './lib/getSearchResults'; -import users from './users'; - -const DEFAULT_STATE = { - // results: [ - // 's/18562', - // ], - search: { - results: [], - text: '', - selected: null, - }, - isRoomFinderVisible: false, - schedules: {}, -}; - -const schedule = (state = {}, action) => { - switch (action.type) { - case 'VIEW/FETCH_SCHEDULE_REQUEST': - return { - ...state, - state: 'FETCHING', - }; - case 'VIEW/FETCH_SCHEDULE_SUCCESS': - return { - ...state, - state: 'FINISHED', - htmlStr: action.htmlStr, - }; - default: - return state; - } -}; - -function reducer(state = DEFAULT_STATE, action) { - switch (action.type) { - case 'SEARCH/SET_USER': { - const { user } = action; - - if (user == null) { - return { - ...state, - search: DEFAULT_STATE.search, - }; - } - - return { - ...state, - search: { - results: [], - text: users.byId[user].value, - selected: user, - }, - }; - } - - case 'SEARCH/INPUT_CHANGE': - return { - ...state, - search: { - results: getSearchResults(action.searchText), - text: action.searchText, - selected: null, - }, - }; - - case 'SEARCH/CHANGE_SELECTED_RESULT': { - const prevSelectedResult = state.search.selected; - const prevSelectedResultIndex = state.search.results.indexOf(prevSelectedResult); - let nextSelectedResultIndex = prevSelectedResultIndex + action.relativeChange; - - if (nextSelectedResultIndex < -1) { - nextSelectedResultIndex = state.search.results.length - 1; - } else if (nextSelectedResultIndex > state.search.results.length - 1) { - nextSelectedResultIndex = -1; - } - - const nextSelectedResult = nextSelectedResultIndex === -1 - ? null - : state.search.results[nextSelectedResultIndex]; - - return { - ...state, - search: { - ...state.search, - selected: nextSelectedResult, - }, - }; - } - - case 'ROOM_FINDER/SHOW': - return { - ...state, - isRoomFinderVisible: true, - }; - - case 'ROOM_FINDER/HIDE': - return { - ...state, - isRoomFinderVisible: false, - }; - - case 'VIEW/FETCH_SCHEDULE_REQUEST': - case 'VIEW/FETCH_SCHEDULE_SUCCESS': - return { - ...state, - schedules: { - ...state.schedules, - [`${action.user}:${action.week}`]: - schedule(state.schedules[`${action.user}:${action.week}`], action), - }, - }; - - default: - return state; - } -} - -export default reducer; -export const _test = { - DEFAULT_STATE, -}; diff --git a/src/client/react/reducers.test.js b/src/client/react/reducers.test.js deleted file mode 100644 index cd195b0..0000000 --- a/src/client/react/reducers.test.js +++ /dev/null @@ -1,253 +0,0 @@ -/** - * Copyright (C) 2018 Noah Loomans - * - * This file is part of rooster.hetmml.nl. - * - * rooster.hetmml.nl is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * rooster.hetmml.nl is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with rooster.hetmml.nl. If not, see . - * - */ - -window.USERS = [ - { type: 's', value: '18561' }, - { type: 's', value: '18562' }, - { type: 's', value: '18563' }, - { type: 's', value: '18564' }, - { type: 's', value: '18565' }, - { type: 's', value: '18566' }, - { type: 's', value: '18567' }, - { type: 's', value: '18568' }, - { type: 's', value: '18569' }, -]; - -const deepFreeze = require('deep-freeze'); -const reducer = require('./reducers').default; -const { DEFAULT_STATE } = require('./reducers')._test; - -describe('reducers', () => { - describe('search', () => { - describe('SEARCH/SET_USER', () => { - it('Resets the search state if the user is null', () => { - const prevState = { search: { foo: 'bar' } }; - const action = { type: 'SEARCH/SET_USER', user: null }; - - deepFreeze([prevState, action]); - - expect(reducer(prevState, action)).toEqual({ - search: DEFAULT_STATE.search, - }); - }); - - it('Sets all the values of that user properly', () => { - expect(reducer(undefined, { type: 'SEARCH/SET_USER', user: 's/18561' })).toEqual({ - ...DEFAULT_STATE, - search: { - results: [], - text: '18561', - selected: 's/18561', - }, - }); - }); - }); - - describe('SEARCH/INPUT_CHANGE', () => { - it('Returns no results when nothing is typed in', () => { - expect(reducer(undefined, { type: 'SEARCH/INPUT_CHANGE', searchText: '' })).toEqual({ - ...DEFAULT_STATE, - search: { - results: [], - text: '', - selected: null, - }, - }); - }); - - it('Returns no results when a space is typed in', () => { - expect(reducer(undefined, { type: 'SEARCH/INPUT_CHANGE', searchText: ' ' })).toEqual({ - ...DEFAULT_STATE, - search: { - results: [], - text: ' ', - selected: null, - }, - }); - }); - - it('Preforms a basic search, only returning four results', () => { - expect(reducer(undefined, { type: 'SEARCH/INPUT_CHANGE', searchText: '18' })).toEqual({ - ...DEFAULT_STATE, - search: { - results: [ - 's/18561', - 's/18562', - 's/18563', - 's/18564', - ], - text: '18', - selected: null, - }, - }); - }); - }); - - describe('SEARCH/CHANGE_SELECTED_RESULT', () => { - it('Does nothing when there are no results', () => { - const actionPlus = { type: 'SEARCH/CHANGE_SELECTED_RESULT', relativeChange: +1 }; - const actionMin = { type: 'SEARCH/CHANGE_SELECTED_RESULT', relativeChange: -1 }; - - deepFreeze([DEFAULT_STATE, actionPlus, actionMin]); - - const nextStatePlus = reducer(DEFAULT_STATE, actionPlus); - const nextStateMin = reducer(DEFAULT_STATE, actionMin); - expect(nextStatePlus).toEqual(DEFAULT_STATE); - expect(nextStateMin).toEqual(DEFAULT_STATE); - }); - - it('Switches to the correct selectedResult when no selected result is selected', () => { - const prevState = { - ...DEFAULT_STATE, - search: { - results: ['s/18561', 's/18562', 's/18563'], - text: '1856', - selected: null, - }, - }; - - const actionPlus = { type: 'SEARCH/CHANGE_SELECTED_RESULT', relativeChange: +1 }; - const actionMin = { type: 'SEARCH/CHANGE_SELECTED_RESULT', relativeChange: -1 }; - - deepFreeze([prevState, actionPlus, actionMin]); - - const nextStatePlus = reducer(prevState, actionPlus); - const nextStateMin = reducer(prevState, actionMin); - - expect(nextStatePlus).toEqual({ - ...prevState, - search: { - ...prevState.search, - selected: 's/18561', - }, - }); - expect(nextStateMin).toEqual({ - ...prevState, - search: { - ...prevState.search, - selected: 's/18563', - }, - }); - }); - - it('Switches to the correct selectedResult when there is a selected result selected', () => { - const prevState = { - ...DEFAULT_STATE, - search: { - results: ['s/18561', 's/18562', 's/18563'], - text: '1856', - selected: 's/18562', - }, - }; - - const actionPlus = { type: 'SEARCH/CHANGE_SELECTED_RESULT', relativeChange: +1 }; - const actionMin = { type: 'SEARCH/CHANGE_SELECTED_RESULT', relativeChange: -1 }; - - deepFreeze([prevState, actionPlus, actionMin]); - - const nextStatePlus = reducer(prevState, actionPlus); - const nextStateMin = reducer(prevState, actionMin); - - expect(nextStatePlus).toEqual({ - ...prevState, - search: { - ...prevState.search, - selected: 's/18563', - }, - }); - expect(nextStateMin).toEqual({ - ...prevState, - search: { - ...prevState.search, - selected: 's/18561', - }, - }); - }); - - it('Properly wraps around when incrementing', () => { - expect(reducer({ - ...DEFAULT_STATE, - search: { - results: ['s/18561', 's/18562', 's/18563'], - text: '1856', - selected: 's/18563', - }, - }, { type: 'SEARCH/CHANGE_SELECTED_RESULT', relativeChange: +1 })).toEqual({ - ...DEFAULT_STATE, - search: { - results: ['s/18561', 's/18562', 's/18563'], - text: '1856', - selected: null, - }, - }); - - expect(reducer({ - ...DEFAULT_STATE, - search: { - results: ['s/18561', 's/18562', 's/18563'], - text: '1856', - selected: null, - }, - }, { type: 'SEARCH/CHANGE_SELECTED_RESULT', relativeChange: +1 })).toEqual({ - ...DEFAULT_STATE, - search: { - results: ['s/18561', 's/18562', 's/18563'], - text: '1856', - selected: 's/18561', - }, - }); - }); - - it('Properly wraps around when decrementing', () => { - expect(reducer({ - ...DEFAULT_STATE, - search: { - results: ['s/18561', 's/18562', 's/18563'], - text: '1856', - selected: 's/18561', - }, - }, { type: 'SEARCH/CHANGE_SELECTED_RESULT', relativeChange: -1 })).toEqual({ - ...DEFAULT_STATE, - search: { - results: ['s/18561', 's/18562', 's/18563'], - text: '1856', - selected: null, - }, - }); - - expect(reducer({ - ...DEFAULT_STATE, - search: { - results: ['s/18561', 's/18562', 's/18563'], - text: '1856', - selected: null, - }, - }, { type: 'SEARCH/CHANGE_SELECTED_RESULT', relativeChange: -1 })).toEqual({ - ...DEFAULT_STATE, - search: { - results: ['s/18561', 's/18562', 's/18563'], - text: '1856', - selected: 's/18563', - }, - }); - }); - }); - }); -}); diff --git a/src/client/react/store/actions.js b/src/client/react/store/actions.js new file mode 100644 index 0000000..c4cc9ba --- /dev/null +++ b/src/client/react/store/actions.js @@ -0,0 +1,14 @@ +import users from '../users'; + +export function showRoomFinder() { + return (dispatch, getState, getHistory) => { + const { user, setUser } = getHistory(); + + if (user == null || users.byId[user].type !== 'r') { + // We are not currently viewing a room, correct the situation. + setUser(users.allRoomIds[0]); + } + + dispatch({ type: 'ROOM_FINDER/SHOW' }); + }; +} diff --git a/src/client/react/store/reducers.js b/src/client/react/store/reducers.js new file mode 100644 index 0000000..c2ee7e9 --- /dev/null +++ b/src/client/react/store/reducers.js @@ -0,0 +1,142 @@ +/** + * Copyright (C) 2018 Noah Loomans + * + * This file is part of rooster.hetmml.nl. + * + * rooster.hetmml.nl is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * rooster.hetmml.nl is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with rooster.hetmml.nl. If not, see . + * + */ + +import getSearchResults from '../lib/getSearchResults'; +import users from '../users'; + +const DEFAULT_STATE = { + // results: [ + // 's/18562', + // ], + search: { + results: [], + text: '', + selected: null, + }, + isRoomFinderVisible: false, + schedules: {}, +}; + +const schedule = (state = {}, action) => { + switch (action.type) { + case 'VIEW/FETCH_SCHEDULE_REQUEST': + return { + ...state, + state: 'FETCHING', + }; + case 'VIEW/FETCH_SCHEDULE_SUCCESS': + return { + ...state, + state: 'FINISHED', + htmlStr: action.htmlStr, + }; + default: + return state; + } +}; + +function reducer(state = DEFAULT_STATE, action) { + switch (action.type) { + case 'SEARCH/SET_USER': { + const { user } = action; + + if (user == null) { + return { + ...state, + search: DEFAULT_STATE.search, + }; + } + + return { + ...state, + search: { + results: [], + text: users.byId[user].value, + selected: user, + }, + }; + } + + case 'SEARCH/INPUT_CHANGE': + return { + ...state, + search: { + results: getSearchResults(action.searchText), + text: action.searchText, + selected: null, + }, + }; + + case 'SEARCH/CHANGE_SELECTED_RESULT': { + const prevSelectedResult = state.search.selected; + const prevSelectedResultIndex = state.search.results.indexOf(prevSelectedResult); + let nextSelectedResultIndex = prevSelectedResultIndex + action.relativeChange; + + if (nextSelectedResultIndex < -1) { + nextSelectedResultIndex = state.search.results.length - 1; + } else if (nextSelectedResultIndex > state.search.results.length - 1) { + nextSelectedResultIndex = -1; + } + + const nextSelectedResult = nextSelectedResultIndex === -1 + ? null + : state.search.results[nextSelectedResultIndex]; + + return { + ...state, + search: { + ...state.search, + selected: nextSelectedResult, + }, + }; + } + + case 'ROOM_FINDER/SHOW': + return { + ...state, + isRoomFinderVisible: true, + }; + + case 'ROOM_FINDER/HIDE': + return { + ...state, + isRoomFinderVisible: false, + }; + + case 'VIEW/FETCH_SCHEDULE_REQUEST': + case 'VIEW/FETCH_SCHEDULE_SUCCESS': + return { + ...state, + schedules: { + ...state.schedules, + [`${action.user}:${action.week}`]: + schedule(state.schedules[`${action.user}:${action.week}`], action), + }, + }; + + default: + return state; + } +} + +export default reducer; +export const _test = { + DEFAULT_STATE, +}; diff --git a/src/client/react/store/reducers.test.js b/src/client/react/store/reducers.test.js new file mode 100644 index 0000000..cd195b0 --- /dev/null +++ b/src/client/react/store/reducers.test.js @@ -0,0 +1,253 @@ +/** + * Copyright (C) 2018 Noah Loomans + * + * This file is part of rooster.hetmml.nl. + * + * rooster.hetmml.nl is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * rooster.hetmml.nl is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with rooster.hetmml.nl. If not, see . + * + */ + +window.USERS = [ + { type: 's', value: '18561' }, + { type: 's', value: '18562' }, + { type: 's', value: '18563' }, + { type: 's', value: '18564' }, + { type: 's', value: '18565' }, + { type: 's', value: '18566' }, + { type: 's', value: '18567' }, + { type: 's', value: '18568' }, + { type: 's', value: '18569' }, +]; + +const deepFreeze = require('deep-freeze'); +const reducer = require('./reducers').default; +const { DEFAULT_STATE } = require('./reducers')._test; + +describe('reducers', () => { + describe('search', () => { + describe('SEARCH/SET_USER', () => { + it('Resets the search state if the user is null', () => { + const prevState = { search: { foo: 'bar' } }; + const action = { type: 'SEARCH/SET_USER', user: null }; + + deepFreeze([prevState, action]); + + expect(reducer(prevState, action)).toEqual({ + search: DEFAULT_STATE.search, + }); + }); + + it('Sets all the values of that user properly', () => { + expect(reducer(undefined, { type: 'SEARCH/SET_USER', user: 's/18561' })).toEqual({ + ...DEFAULT_STATE, + search: { + results: [], + text: '18561', + selected: 's/18561', + }, + }); + }); + }); + + describe('SEARCH/INPUT_CHANGE', () => { + it('Returns no results when nothing is typed in', () => { + expect(reducer(undefined, { type: 'SEARCH/INPUT_CHANGE', searchText: '' })).toEqual({ + ...DEFAULT_STATE, + search: { + results: [], + text: '', + selected: null, + }, + }); + }); + + it('Returns no results when a space is typed in', () => { + expect(reducer(undefined, { type: 'SEARCH/INPUT_CHANGE', searchText: ' ' })).toEqual({ + ...DEFAULT_STATE, + search: { + results: [], + text: ' ', + selected: null, + }, + }); + }); + + it('Preforms a basic search, only returning four results', () => { + expect(reducer(undefined, { type: 'SEARCH/INPUT_CHANGE', searchText: '18' })).toEqual({ + ...DEFAULT_STATE, + search: { + results: [ + 's/18561', + 's/18562', + 's/18563', + 's/18564', + ], + text: '18', + selected: null, + }, + }); + }); + }); + + describe('SEARCH/CHANGE_SELECTED_RESULT', () => { + it('Does nothing when there are no results', () => { + const actionPlus = { type: 'SEARCH/CHANGE_SELECTED_RESULT', relativeChange: +1 }; + const actionMin = { type: 'SEARCH/CHANGE_SELECTED_RESULT', relativeChange: -1 }; + + deepFreeze([DEFAULT_STATE, actionPlus, actionMin]); + + const nextStatePlus = reducer(DEFAULT_STATE, actionPlus); + const nextStateMin = reducer(DEFAULT_STATE, actionMin); + expect(nextStatePlus).toEqual(DEFAULT_STATE); + expect(nextStateMin).toEqual(DEFAULT_STATE); + }); + + it('Switches to the correct selectedResult when no selected result is selected', () => { + const prevState = { + ...DEFAULT_STATE, + search: { + results: ['s/18561', 's/18562', 's/18563'], + text: '1856', + selected: null, + }, + }; + + const actionPlus = { type: 'SEARCH/CHANGE_SELECTED_RESULT', relativeChange: +1 }; + const actionMin = { type: 'SEARCH/CHANGE_SELECTED_RESULT', relativeChange: -1 }; + + deepFreeze([prevState, actionPlus, actionMin]); + + const nextStatePlus = reducer(prevState, actionPlus); + const nextStateMin = reducer(prevState, actionMin); + + expect(nextStatePlus).toEqual({ + ...prevState, + search: { + ...prevState.search, + selected: 's/18561', + }, + }); + expect(nextStateMin).toEqual({ + ...prevState, + search: { + ...prevState.search, + selected: 's/18563', + }, + }); + }); + + it('Switches to the correct selectedResult when there is a selected result selected', () => { + const prevState = { + ...DEFAULT_STATE, + search: { + results: ['s/18561', 's/18562', 's/18563'], + text: '1856', + selected: 's/18562', + }, + }; + + const actionPlus = { type: 'SEARCH/CHANGE_SELECTED_RESULT', relativeChange: +1 }; + const actionMin = { type: 'SEARCH/CHANGE_SELECTED_RESULT', relativeChange: -1 }; + + deepFreeze([prevState, actionPlus, actionMin]); + + const nextStatePlus = reducer(prevState, actionPlus); + const nextStateMin = reducer(prevState, actionMin); + + expect(nextStatePlus).toEqual({ + ...prevState, + search: { + ...prevState.search, + selected: 's/18563', + }, + }); + expect(nextStateMin).toEqual({ + ...prevState, + search: { + ...prevState.search, + selected: 's/18561', + }, + }); + }); + + it('Properly wraps around when incrementing', () => { + expect(reducer({ + ...DEFAULT_STATE, + search: { + results: ['s/18561', 's/18562', 's/18563'], + text: '1856', + selected: 's/18563', + }, + }, { type: 'SEARCH/CHANGE_SELECTED_RESULT', relativeChange: +1 })).toEqual({ + ...DEFAULT_STATE, + search: { + results: ['s/18561', 's/18562', 's/18563'], + text: '1856', + selected: null, + }, + }); + + expect(reducer({ + ...DEFAULT_STATE, + search: { + results: ['s/18561', 's/18562', 's/18563'], + text: '1856', + selected: null, + }, + }, { type: 'SEARCH/CHANGE_SELECTED_RESULT', relativeChange: +1 })).toEqual({ + ...DEFAULT_STATE, + search: { + results: ['s/18561', 's/18562', 's/18563'], + text: '1856', + selected: 's/18561', + }, + }); + }); + + it('Properly wraps around when decrementing', () => { + expect(reducer({ + ...DEFAULT_STATE, + search: { + results: ['s/18561', 's/18562', 's/18563'], + text: '1856', + selected: 's/18561', + }, + }, { type: 'SEARCH/CHANGE_SELECTED_RESULT', relativeChange: -1 })).toEqual({ + ...DEFAULT_STATE, + search: { + results: ['s/18561', 's/18562', 's/18563'], + text: '1856', + selected: null, + }, + }); + + expect(reducer({ + ...DEFAULT_STATE, + search: { + results: ['s/18561', 's/18562', 's/18563'], + text: '1856', + selected: null, + }, + }, { type: 'SEARCH/CHANGE_SELECTED_RESULT', relativeChange: -1 })).toEqual({ + ...DEFAULT_STATE, + search: { + results: ['s/18561', 's/18562', 's/18563'], + text: '1856', + selected: 's/18563', + }, + }); + }); + }); + }); +}); -- cgit v1.1