mirror of
https://github.com/shlinkio/shlink-web-client.git
synced 2025-01-10 18:27:25 +03:00
Merge pull request #258 from acelaya-forks/feature/redux-localstorage
Feature/redux localstorage
This commit is contained in:
commit
fad0bf1c9d
40 changed files with 414 additions and 693 deletions
|
@ -19,7 +19,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|||
|
||||
#### Changed
|
||||
|
||||
* [#218](https://github.com/shlinkio/shlink-web-client/issues/218) Added back button to sections not displayed in left menu
|
||||
* [#218](https://github.com/shlinkio/shlink-web-client/issues/218) Added back button to sections not displayed in left menu.
|
||||
* [#255](https://github.com/shlinkio/shlink-web-client/issues/255) Improved how servers and settings are persisted in the local storage.
|
||||
|
||||
#### Deprecated
|
||||
|
||||
|
|
27
package-lock.json
generated
27
package-lock.json
generated
|
@ -1463,6 +1463,14 @@
|
|||
"integrity": "sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw==",
|
||||
"dev": true
|
||||
},
|
||||
"@shlinkio/redux-localstorage-simple": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@shlinkio/redux-localstorage-simple/-/redux-localstorage-simple-2.2.0.tgz",
|
||||
"integrity": "sha512-2/VggbehDAM1dOH7rT3Qjr/MTp7qQ6VeTM+Ez4JnMUPtU9OxgV9FQbKqduasLT4EZhlRUhxwBp7K6WO3gROQDA==",
|
||||
"requires": {
|
||||
"object-merge": "2.5.1"
|
||||
}
|
||||
},
|
||||
"@stryker-mutator/api": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@stryker-mutator/api/-/api-2.1.0.tgz",
|
||||
|
@ -4246,6 +4254,11 @@
|
|||
"shallow-clone": "^0.1.2"
|
||||
}
|
||||
},
|
||||
"clone-function": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/clone-function/-/clone-function-1.0.6.tgz",
|
||||
"integrity": "sha1-QoRxk3dQvKnEjsv7wW9uIy90oD0="
|
||||
},
|
||||
"clone-regexp": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/clone-regexp/-/clone-regexp-1.0.1.tgz",
|
||||
|
@ -11720,6 +11733,11 @@
|
|||
"kind-of": "^3.0.3"
|
||||
}
|
||||
},
|
||||
"object-foreach": {
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/object-foreach/-/object-foreach-0.1.2.tgz",
|
||||
"integrity": "sha1-10IcW0DjtqPvV6xiQ2jSHY+NLew="
|
||||
},
|
||||
"object-hash": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/object-hash/-/object-hash-1.3.1.tgz",
|
||||
|
@ -11744,6 +11762,15 @@
|
|||
"integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
|
||||
"dev": true
|
||||
},
|
||||
"object-merge": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/object-merge/-/object-merge-2.5.1.tgz",
|
||||
"integrity": "sha1-B36JFc446nKUeIRIxd0znjTfQic=",
|
||||
"requires": {
|
||||
"clone-function": ">=1.0.1",
|
||||
"object-foreach": ">=0.1.2"
|
||||
}
|
||||
},
|
||||
"object-visit": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz",
|
||||
|
|
|
@ -27,6 +27,7 @@
|
|||
"@fortawesome/free-regular-svg-icons": "^5.11.2",
|
||||
"@fortawesome/free-solid-svg-icons": "^5.11.2",
|
||||
"@fortawesome/react-fontawesome": "^0.1.5",
|
||||
"@shlinkio/redux-localstorage-simple": "^2.2.0",
|
||||
"array-filter": "^1.0.0",
|
||||
"array-map": "^0.0.0",
|
||||
"array-reduce": "^0.0.0",
|
||||
|
|
15
src/App.js
15
src/App.js
|
@ -1,11 +1,20 @@
|
|||
import React, { useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Route, Switch } from 'react-router-dom';
|
||||
import NotFound from './common/NotFound';
|
||||
import './App.scss';
|
||||
|
||||
const App = (MainHeader, Home, MenuLayout, CreateServer, EditServer, Settings) => ({ loadRealTimeUpdates }) => {
|
||||
const propTypes = {
|
||||
fetchServers: PropTypes.func,
|
||||
servers: PropTypes.object,
|
||||
};
|
||||
|
||||
const App = (MainHeader, Home, MenuLayout, CreateServer, EditServer, Settings) => ({ fetchServers, servers }) => {
|
||||
// On first load, try to fetch the remote servers if the list is empty
|
||||
useEffect(() => {
|
||||
loadRealTimeUpdates();
|
||||
if (Object.keys(servers).length === 0) {
|
||||
fetchServers();
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
|
@ -26,4 +35,6 @@ const App = (MainHeader, Home, MenuLayout, CreateServer, EditServer, Settings) =
|
|||
);
|
||||
};
|
||||
|
||||
App.propTypes = propTypes;
|
||||
|
||||
export default App;
|
||||
|
|
|
@ -10,9 +10,9 @@ const propTypes = {
|
|||
servers: PropTypes.object,
|
||||
};
|
||||
|
||||
const Home = ({ resetSelectedServer, servers: { list, loading } }) => {
|
||||
const servers = values(list);
|
||||
const hasServers = !isEmpty(servers);
|
||||
const Home = ({ resetSelectedServer, servers }) => {
|
||||
const serversList = values(servers);
|
||||
const hasServers = !isEmpty(serversList);
|
||||
|
||||
useEffect(() => {
|
||||
resetSelectedServer();
|
||||
|
@ -21,10 +21,9 @@ const Home = ({ resetSelectedServer, servers: { list, loading } }) => {
|
|||
return (
|
||||
<div className="home">
|
||||
<h1 className="home__title">Welcome to Shlink</h1>
|
||||
<ServersListGroup servers={servers}>
|
||||
{!loading && hasServers && <span>Please, select a server.</span>}
|
||||
{!loading && !hasServers && <span>Please, <Link to="/server/create">add a server</Link>.</span>}
|
||||
{loading && <span>Trying to load servers...</span>}
|
||||
<ServersListGroup servers={serversList}>
|
||||
{hasServers && <span>Please, select a server.</span>}
|
||||
{!hasServers && <span>Please, <Link to="/server/create">add a server</Link>.</span>}
|
||||
</ServersListGroup>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -29,7 +29,7 @@ const connect = (propsFromState, actionServiceNames = []) =>
|
|||
);
|
||||
|
||||
bottle.serviceFactory('App', App, 'MainHeader', 'Home', 'MenuLayout', 'CreateServer', 'EditServer', 'Settings');
|
||||
bottle.decorator('App', connect(null, [ 'loadRealTimeUpdates' ]));
|
||||
bottle.decorator('App', connect([ 'servers' ], [ 'fetchServers' ]));
|
||||
|
||||
provideCommonServices(bottle, connect, withRouter);
|
||||
provideShortUrlsServices(bottle, connect);
|
||||
|
|
|
@ -1,13 +1,20 @@
|
|||
import ReduxThunk from 'redux-thunk';
|
||||
import { applyMiddleware, compose, createStore } from 'redux';
|
||||
import { save, load } from '@shlinkio/redux-localstorage-simple';
|
||||
import reducers from '../reducers';
|
||||
|
||||
const composeEnhancers = process.env.NODE_ENV !== 'production' && window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__
|
||||
? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__
|
||||
: compose;
|
||||
|
||||
const store = createStore(reducers, composeEnhancers(
|
||||
applyMiddleware(ReduxThunk)
|
||||
const localStorageConfig = {
|
||||
states: [ 'settings', 'servers' ],
|
||||
namespace: 'shlink',
|
||||
namespaceSeparator: '.',
|
||||
};
|
||||
|
||||
const store = createStore(reducers, load(localStorageConfig), composeEnhancers(
|
||||
applyMiddleware(save(localStorageConfig), ReduxThunk)
|
||||
));
|
||||
|
||||
export default store;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { combineReducers } from 'redux';
|
||||
import serversReducer from '../servers/reducers/server';
|
||||
import serversReducer from '../servers/reducers/servers';
|
||||
import selectedServerReducer from '../servers/reducers/selectedServer';
|
||||
import shortUrlsListReducer from '../short-urls/reducers/shortUrlsList';
|
||||
import shortUrlsListParamsReducer from '../short-urls/reducers/shortUrlsListParams';
|
||||
|
@ -14,7 +14,7 @@ import tagsListReducer from '../tags/reducers/tagsList';
|
|||
import tagDeleteReducer from '../tags/reducers/tagDelete';
|
||||
import tagEditReducer from '../tags/reducers/tagEdit';
|
||||
import mercureInfoReducer from '../mercure/reducers/mercureInfo';
|
||||
import realTimeUpdatesReducer from '../settings/reducers/realTimeUpdates';
|
||||
import settingsReducer from '../settings/reducers/settings';
|
||||
|
||||
export default combineReducers({
|
||||
servers: serversReducer,
|
||||
|
@ -32,5 +32,5 @@ export default combineReducers({
|
|||
tagDelete: tagDeleteReducer,
|
||||
tagEdit: tagEditReducer,
|
||||
mercureInfo: mercureInfoReducer,
|
||||
realTimeUpdates: realTimeUpdatesReducer,
|
||||
settings: settingsReducer,
|
||||
});
|
||||
|
|
|
@ -4,53 +4,50 @@ import { DropdownItem, DropdownMenu, DropdownToggle, UncontrolledDropdown } from
|
|||
import PropTypes from 'prop-types';
|
||||
import { serverType } from './prop-types';
|
||||
|
||||
const ServersDropdown = (serversExporter) => class ServersDropdown extends React.Component {
|
||||
static propTypes = {
|
||||
servers: PropTypes.object,
|
||||
selectedServer: serverType,
|
||||
listServers: PropTypes.func,
|
||||
history: PropTypes.shape({
|
||||
push: PropTypes.func,
|
||||
}),
|
||||
};
|
||||
const propTypes = {
|
||||
servers: PropTypes.object,
|
||||
selectedServer: serverType,
|
||||
history: PropTypes.shape({
|
||||
push: PropTypes.func,
|
||||
}),
|
||||
};
|
||||
|
||||
renderServers = () => {
|
||||
const { servers: { list, loading }, selectedServer } = this.props;
|
||||
const servers = values(list);
|
||||
const { push } = this.props.history;
|
||||
const loadServer = (id) => push(`/server/${id}/list-short-urls/1`);
|
||||
const ServersDropdown = (serversExporter) => {
|
||||
const ServersDropdownComp = ({ servers, selectedServer, history }) => {
|
||||
const serversList = values(servers);
|
||||
const loadServer = (id) => history.push(`/server/${id}/list-short-urls/1`);
|
||||
|
||||
if (loading) {
|
||||
return <DropdownItem disabled><i>Trying to load servers...</i></DropdownItem>;
|
||||
}
|
||||
const renderServers = () => {
|
||||
if (isEmpty(serversList)) {
|
||||
return <DropdownItem disabled><i>Add a server first...</i></DropdownItem>;
|
||||
}
|
||||
|
||||
if (isEmpty(servers)) {
|
||||
return <DropdownItem disabled><i>Add a server first...</i></DropdownItem>;
|
||||
}
|
||||
return (
|
||||
<React.Fragment>
|
||||
{serversList.map(({ name, id }) => (
|
||||
<DropdownItem key={id} active={selectedServer && selectedServer.id === id} onClick={() => loadServer(id)}>
|
||||
{name}
|
||||
</DropdownItem>
|
||||
))}
|
||||
<DropdownItem divider />
|
||||
<DropdownItem className="servers-dropdown__export-item" onClick={() => serversExporter.exportServers()}>
|
||||
Export servers
|
||||
</DropdownItem>
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
{servers.map(({ name, id }) => (
|
||||
<DropdownItem key={id} active={selectedServer && selectedServer.id === id} onClick={() => loadServer(id)}>
|
||||
{name}
|
||||
</DropdownItem>
|
||||
))}
|
||||
<DropdownItem divider />
|
||||
<DropdownItem className="servers-dropdown__export-item" onClick={() => serversExporter.exportServers()}>
|
||||
Export servers
|
||||
</DropdownItem>
|
||||
</React.Fragment>
|
||||
<UncontrolledDropdown nav inNavbar>
|
||||
<DropdownToggle nav caret>Servers</DropdownToggle>
|
||||
<DropdownMenu right>{renderServers()}</DropdownMenu>
|
||||
</UncontrolledDropdown>
|
||||
);
|
||||
};
|
||||
|
||||
componentDidMount = this.props.listServers;
|
||||
ServersDropdownComp.propTypes = propTypes;
|
||||
|
||||
render = () => (
|
||||
<UncontrolledDropdown nav inNavbar>
|
||||
<DropdownToggle nav caret>Servers</DropdownToggle>
|
||||
<DropdownMenu right>{this.renderServers()}</DropdownMenu>
|
||||
</UncontrolledDropdown>
|
||||
);
|
||||
return ServersDropdownComp;
|
||||
};
|
||||
|
||||
export default ServersDropdown;
|
||||
|
|
|
@ -13,7 +13,7 @@ const propTypes = {
|
|||
};
|
||||
|
||||
export const ServerError = (DeleteServerButton) => {
|
||||
const ServerErrorComp = ({ type, servers: { list }, selectedServer }) => (
|
||||
const ServerErrorComp = ({ type, servers, selectedServer }) => (
|
||||
<div className="server-error__container flex-column">
|
||||
<div className="row w-100 mb-3 mb-md-5">
|
||||
<Message type="error">
|
||||
|
@ -27,7 +27,7 @@ export const ServerError = (DeleteServerButton) => {
|
|||
</Message>
|
||||
</div>
|
||||
|
||||
<ServersListGroup servers={Object.values(list)}>
|
||||
<ServersListGroup servers={Object.values(servers)}>
|
||||
These are the Shlink servers currently configured. Choose one of
|
||||
them or <Link to="/server/create">add a new one</Link>.
|
||||
</ServersListGroup>
|
||||
|
|
22
src/servers/reducers/remoteServers.js
Normal file
22
src/servers/reducers/remoteServers.js
Normal file
|
@ -0,0 +1,22 @@
|
|||
import { pipe, prop } from 'ramda';
|
||||
import { homepage } from '../../../package.json';
|
||||
import { createServers } from './servers';
|
||||
|
||||
const responseToServersList = pipe(
|
||||
prop('data'),
|
||||
(value) => {
|
||||
if (!Array.isArray(value)) {
|
||||
throw new Error('Value is not an array');
|
||||
}
|
||||
|
||||
return value;
|
||||
},
|
||||
);
|
||||
|
||||
export const fetchServers = ({ get }) => () => async (dispatch) => {
|
||||
const remoteList = await get(`${homepage}/servers.json`)
|
||||
.then(responseToServersList)
|
||||
.catch(() => []);
|
||||
|
||||
dispatch(createServers(remoteList));
|
||||
};
|
|
@ -25,12 +25,15 @@ const getServerVersion = memoizeWith(identity, (serverId, health) => health().th
|
|||
|
||||
export const resetSelectedServer = createAction(RESET_SELECTED_SERVER);
|
||||
|
||||
export const selectServer = ({ findServerById }, buildShlinkApiClient, loadMercureInfo) => (serverId) => async (
|
||||
dispatch
|
||||
export const selectServer = (buildShlinkApiClient, loadMercureInfo) => (serverId) => async (
|
||||
dispatch,
|
||||
getState
|
||||
) => {
|
||||
dispatch(resetSelectedServer());
|
||||
dispatch(resetShortUrlParams());
|
||||
const selectedServer = findServerById(serverId);
|
||||
|
||||
const { servers } = getState();
|
||||
const selectedServer = servers[serverId];
|
||||
|
||||
if (!selectedServer) {
|
||||
dispatch({
|
||||
|
|
|
@ -1,63 +0,0 @@
|
|||
import { handleActions } from 'redux-actions';
|
||||
import { pipe, isEmpty, assoc, map, prop } from 'ramda';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { homepage } from '../../../package.json';
|
||||
|
||||
/* eslint-disable padding-line-between-statements */
|
||||
export const FETCH_SERVERS_START = 'shlink/servers/FETCH_SERVERS_START';
|
||||
export const FETCH_SERVERS = 'shlink/servers/FETCH_SERVERS';
|
||||
/* eslint-enable padding-line-between-statements */
|
||||
|
||||
const initialState = {
|
||||
list: {},
|
||||
loading: false,
|
||||
};
|
||||
|
||||
const assocId = (server) => assoc('id', server.id || uuid(), server);
|
||||
|
||||
export default handleActions({
|
||||
[FETCH_SERVERS_START]: (state) => ({ ...state, loading: true }),
|
||||
[FETCH_SERVERS]: (state, { list }) => ({ list, loading: false }),
|
||||
}, initialState);
|
||||
|
||||
export const listServers = ({ listServers, createServers }, { get }) => () => async (dispatch) => {
|
||||
dispatch({ type: FETCH_SERVERS_START });
|
||||
const localList = listServers();
|
||||
|
||||
if (!isEmpty(localList)) {
|
||||
dispatch({ type: FETCH_SERVERS, list: localList });
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// If local list is empty, try to fetch it remotely (making sure it's an array) and calculate IDs for every server
|
||||
const getDataAsArrayWithIds = pipe(
|
||||
prop('data'),
|
||||
(value) => {
|
||||
if (!Array.isArray(value)) {
|
||||
throw new Error('Value is not an array');
|
||||
}
|
||||
|
||||
return value;
|
||||
},
|
||||
map(assocId),
|
||||
);
|
||||
const remoteList = await get(`${homepage}/servers.json`)
|
||||
.then(getDataAsArrayWithIds)
|
||||
.catch(() => []);
|
||||
|
||||
createServers(remoteList);
|
||||
dispatch({ type: FETCH_SERVERS, list: remoteList.reduce((map, server) => ({ ...map, [server.id]: server }), {}) });
|
||||
};
|
||||
|
||||
export const createServer = ({ createServer }, listServersAction) => pipe(createServer, listServersAction);
|
||||
|
||||
export const editServer = ({ editServer }, listServersAction) => pipe(editServer, listServersAction);
|
||||
|
||||
export const deleteServer = ({ deleteServer }, listServersAction) => pipe(deleteServer, listServersAction);
|
||||
|
||||
export const createServers = ({ createServers }, listServersAction) => pipe(
|
||||
map(assocId),
|
||||
createServers,
|
||||
listServersAction
|
||||
);
|
35
src/servers/reducers/servers.js
Normal file
35
src/servers/reducers/servers.js
Normal file
|
@ -0,0 +1,35 @@
|
|||
import { handleActions } from 'redux-actions';
|
||||
import { pipe, assoc, map, reduce, dissoc } from 'ramda';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
/* eslint-disable padding-line-between-statements */
|
||||
export const EDIT_SERVER = 'shlink/servers/EDIT_SERVER';
|
||||
export const DELETE_SERVER = 'shlink/servers/DELETE_SERVER';
|
||||
export const CREATE_SERVERS = 'shlink/servers/CREATE_SERVERS';
|
||||
/* eslint-enable padding-line-between-statements */
|
||||
|
||||
const initialState = {};
|
||||
|
||||
const assocId = (server) => assoc('id', server.id || uuid(), server);
|
||||
|
||||
export default handleActions({
|
||||
[CREATE_SERVERS]: (state, { newServers }) => ({ ...state, ...newServers }),
|
||||
[DELETE_SERVER]: (state, { serverId }) => dissoc(serverId, state),
|
||||
[EDIT_SERVER]: (state, { serverId, serverData }) => !state[serverId]
|
||||
? state
|
||||
: assoc(serverId, { ...state[serverId], ...serverData }, state),
|
||||
}, initialState);
|
||||
|
||||
export const createServer = (server) => createServers([ server ]);
|
||||
|
||||
const serversListToMap = reduce((acc, server) => assoc(server.id, server, acc), {});
|
||||
|
||||
export const createServers = pipe(
|
||||
map(assocId),
|
||||
serversListToMap,
|
||||
(newServers) => ({ type: CREATE_SERVERS, newServers })
|
||||
);
|
||||
|
||||
export const editServer = (serverId, serverData) => ({ type: EDIT_SERVER, serverId, serverData });
|
||||
|
||||
export const deleteServer = ({ id }) => ({ type: DELETE_SERVER, serverId: id });
|
|
@ -25,14 +25,14 @@ const saveCsv = (window, csv) => {
|
|||
};
|
||||
|
||||
export default class ServersExporter {
|
||||
constructor(serversService, window, csvjson) {
|
||||
this.serversService = serversService;
|
||||
constructor(storage, window, csvjson) {
|
||||
this.storage = storage;
|
||||
this.window = window;
|
||||
this.csvjson = csvjson;
|
||||
}
|
||||
|
||||
exportServers = async () => {
|
||||
const servers = values(this.serversService.listServers()).map(dissoc('id'));
|
||||
const servers = values(this.storage.get('servers') || {}).map(dissoc('id'));
|
||||
|
||||
try {
|
||||
const csv = this.csvjson.toCSV(servers, {
|
||||
|
|
|
@ -1,38 +0,0 @@
|
|||
import { assoc, dissoc, reduce } from 'ramda';
|
||||
|
||||
const SERVERS_STORAGE_KEY = 'servers';
|
||||
|
||||
export default class ServersService {
|
||||
constructor(storage) {
|
||||
this.storage = storage;
|
||||
}
|
||||
|
||||
listServers = () => this.storage.get(SERVERS_STORAGE_KEY) || {};
|
||||
|
||||
findServerById = (serverId) => this.listServers()[serverId];
|
||||
|
||||
createServer = (server) => this.createServers([ server ]);
|
||||
|
||||
createServers = (servers) => {
|
||||
const allServers = reduce(
|
||||
(serversObj, server) => assoc(server.id, server, serversObj),
|
||||
this.listServers(),
|
||||
servers
|
||||
);
|
||||
|
||||
this.storage.set(SERVERS_STORAGE_KEY, allServers);
|
||||
};
|
||||
|
||||
deleteServer = ({ id }) =>
|
||||
this.storage.set(SERVERS_STORAGE_KEY, dissoc(id, this.listServers()));
|
||||
|
||||
editServer = (id, serverData) => {
|
||||
const allServers = this.listServers();
|
||||
|
||||
if (!allServers[id]) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.storage.set(SERVERS_STORAGE_KEY, assoc(id, { ...allServers[id], ...serverData }, allServers));
|
||||
}
|
||||
}
|
|
@ -6,11 +6,11 @@ import DeleteServerButton from '../DeleteServerButton';
|
|||
import { EditServer } from '../EditServer';
|
||||
import ImportServersBtn from '../helpers/ImportServersBtn';
|
||||
import { resetSelectedServer, selectServer } from '../reducers/selectedServer';
|
||||
import { createServer, createServers, deleteServer, editServer, listServers } from '../reducers/server';
|
||||
import { createServer, createServers, deleteServer, editServer } from '../reducers/servers';
|
||||
import { fetchServers } from '../reducers/remoteServers';
|
||||
import ForServerVersion from '../helpers/ForServerVersion';
|
||||
import { ServerError } from '../helpers/ServerError';
|
||||
import ServersImporter from './ServersImporter';
|
||||
import ServersService from './ServersService';
|
||||
import ServersExporter from './ServersExporter';
|
||||
|
||||
const provideServices = (bottle, connect, withRouter) => {
|
||||
|
@ -23,7 +23,7 @@ const provideServices = (bottle, connect, withRouter) => {
|
|||
|
||||
bottle.serviceFactory('ServersDropdown', ServersDropdown, 'ServersExporter');
|
||||
bottle.decorator('ServersDropdown', withRouter);
|
||||
bottle.decorator('ServersDropdown', connect([ 'servers', 'selectedServer' ], [ 'listServers' ]));
|
||||
bottle.decorator('ServersDropdown', connect([ 'servers', 'selectedServer' ]));
|
||||
|
||||
bottle.serviceFactory('DeleteServerModal', () => DeleteServerModal);
|
||||
bottle.decorator('DeleteServerModal', withRouter);
|
||||
|
@ -43,16 +43,15 @@ const provideServices = (bottle, connect, withRouter) => {
|
|||
// Services
|
||||
bottle.constant('csvjson', csvjson);
|
||||
bottle.service('ServersImporter', ServersImporter, 'csvjson');
|
||||
bottle.service('ServersService', ServersService, 'Storage');
|
||||
bottle.service('ServersExporter', ServersExporter, 'ServersService', 'window', 'csvjson');
|
||||
bottle.service('ServersExporter', ServersExporter, 'Storage', 'window', 'csvjson');
|
||||
|
||||
// Actions
|
||||
bottle.serviceFactory('selectServer', selectServer, 'ServersService', 'buildShlinkApiClient', 'loadMercureInfo');
|
||||
bottle.serviceFactory('createServer', createServer, 'ServersService', 'listServers');
|
||||
bottle.serviceFactory('createServers', createServers, 'ServersService', 'listServers');
|
||||
bottle.serviceFactory('deleteServer', deleteServer, 'ServersService', 'listServers');
|
||||
bottle.serviceFactory('editServer', editServer, 'ServersService', 'listServers');
|
||||
bottle.serviceFactory('listServers', listServers, 'ServersService', 'axios');
|
||||
bottle.serviceFactory('selectServer', selectServer, 'buildShlinkApiClient', 'loadMercureInfo');
|
||||
bottle.serviceFactory('createServer', () => createServer);
|
||||
bottle.serviceFactory('createServers', () => createServers);
|
||||
bottle.serviceFactory('deleteServer', () => deleteServer);
|
||||
bottle.serviceFactory('editServer', () => editServer);
|
||||
bottle.serviceFactory('fetchServers', fetchServers, 'axios');
|
||||
|
||||
bottle.serviceFactory('resetSelectedServer', () => resetSelectedServer);
|
||||
};
|
||||
|
|
|
@ -4,14 +4,14 @@ import PropTypes from 'prop-types';
|
|||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faInfoCircle } from '@fortawesome/free-solid-svg-icons';
|
||||
import Checkbox from '../utils/Checkbox';
|
||||
import { RealTimeUpdatesType } from './reducers/realTimeUpdates';
|
||||
import { SettingsType } from './reducers/settings';
|
||||
|
||||
const propTypes = {
|
||||
realTimeUpdates: RealTimeUpdatesType,
|
||||
settings: SettingsType,
|
||||
setRealTimeUpdates: PropTypes.func,
|
||||
};
|
||||
|
||||
const RealTimeUpdates = ({ realTimeUpdates, setRealTimeUpdates }) => (
|
||||
const RealTimeUpdates = ({ settings: { realTimeUpdates }, setRealTimeUpdates }) => (
|
||||
<Card>
|
||||
<CardHeader>Real-time updates</CardHeader>
|
||||
<CardBody>
|
||||
|
|
|
@ -1,32 +0,0 @@
|
|||
import { handleActions } from 'redux-actions';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
export const LOAD_REAL_TIME_UPDATES = 'shlink/realTimeUpdates/LOAD_REAL_TIME_UPDATES';
|
||||
|
||||
export const RealTimeUpdatesType = PropTypes.shape({
|
||||
enabled: PropTypes.bool.isRequired,
|
||||
});
|
||||
|
||||
const initialState = {
|
||||
enabled: true,
|
||||
};
|
||||
|
||||
export default handleActions({
|
||||
[LOAD_REAL_TIME_UPDATES]: (state, { enabled }) => ({ ...state, enabled }),
|
||||
}, initialState);
|
||||
|
||||
export const setRealTimeUpdates = ({ updateSettings }, loadRealTimeUpdatesAction) => (enabled) => {
|
||||
updateSettings({ realTimeUpdates: { enabled } });
|
||||
|
||||
return loadRealTimeUpdatesAction();
|
||||
};
|
||||
|
||||
export const loadRealTimeUpdates = ({ loadSettings }) => () => {
|
||||
const { realTimeUpdates = {} } = loadSettings();
|
||||
const { enabled = true } = realTimeUpdates;
|
||||
|
||||
return {
|
||||
type: LOAD_REAL_TIME_UPDATES,
|
||||
enabled,
|
||||
};
|
||||
};
|
25
src/settings/reducers/settings.js
Normal file
25
src/settings/reducers/settings.js
Normal file
|
@ -0,0 +1,25 @@
|
|||
import { handleActions } from 'redux-actions';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
export const SET_REAL_TIME_UPDATES = 'shlink/realTimeUpdates/SET_REAL_TIME_UPDATES';
|
||||
|
||||
export const SettingsType = PropTypes.shape({
|
||||
realTimeUpdates: PropTypes.shape({
|
||||
enabled: PropTypes.bool.isRequired,
|
||||
}),
|
||||
});
|
||||
|
||||
const initialState = {
|
||||
realTimeUpdates: {
|
||||
enabled: true,
|
||||
},
|
||||
};
|
||||
|
||||
export default handleActions({
|
||||
[SET_REAL_TIME_UPDATES]: (state, { realTimeUpdates }) => ({ ...state, realTimeUpdates }),
|
||||
}, initialState);
|
||||
|
||||
export const setRealTimeUpdates = (enabled) => ({
|
||||
type: SET_REAL_TIME_UPDATES,
|
||||
realTimeUpdates: { enabled },
|
||||
});
|
|
@ -1,14 +0,0 @@
|
|||
const SETTINGS_STORAGE_KEY = 'settings';
|
||||
|
||||
export default class SettingsService {
|
||||
constructor(storage) {
|
||||
this.storage = storage;
|
||||
}
|
||||
|
||||
loadSettings = () => this.storage.get(SETTINGS_STORAGE_KEY) || {};
|
||||
|
||||
updateSettings = (settingsToUpdate) => this.storage.set(SETTINGS_STORAGE_KEY, {
|
||||
...this.loadSettings(),
|
||||
...settingsToUpdate,
|
||||
})
|
||||
}
|
|
@ -1,21 +1,16 @@
|
|||
import RealTimeUpdates from '../RealTimeUpdates';
|
||||
import Settings from '../Settings';
|
||||
import { loadRealTimeUpdates, setRealTimeUpdates } from '../reducers/realTimeUpdates';
|
||||
import SettingsService from './SettingsService';
|
||||
import { setRealTimeUpdates } from '../reducers/settings';
|
||||
|
||||
const provideServices = (bottle, connect) => {
|
||||
// Components
|
||||
bottle.serviceFactory('Settings', Settings, 'RealTimeUpdates');
|
||||
|
||||
bottle.serviceFactory('RealTimeUpdates', () => RealTimeUpdates);
|
||||
bottle.decorator('RealTimeUpdates', connect([ 'realTimeUpdates' ], [ 'setRealTimeUpdates' ]));
|
||||
|
||||
// Services
|
||||
bottle.service('SettingsService', SettingsService, 'Storage');
|
||||
bottle.decorator('RealTimeUpdates', connect([ 'settings' ], [ 'setRealTimeUpdates' ]));
|
||||
|
||||
// Actions
|
||||
bottle.serviceFactory('setRealTimeUpdates', setRealTimeUpdates, 'SettingsService', 'loadRealTimeUpdates');
|
||||
bottle.serviceFactory('loadRealTimeUpdates', loadRealTimeUpdates, 'SettingsService');
|
||||
bottle.serviceFactory('setRealTimeUpdates', () => setRealTimeUpdates);
|
||||
};
|
||||
|
||||
export default provideServices;
|
||||
|
|
|
@ -9,7 +9,7 @@ import SortingDropdown from '../utils/SortingDropdown';
|
|||
import { determineOrderDir } from '../utils/utils';
|
||||
import { MercureInfoType } from '../mercure/reducers/mercureInfo';
|
||||
import { bindToMercureTopic } from '../mercure/helpers';
|
||||
import { RealTimeUpdatesType } from '../settings/reducers/realTimeUpdates';
|
||||
import { SettingsType } from '../settings/reducers/settings';
|
||||
import { shortUrlType } from './reducers/shortUrlsList';
|
||||
import { shortUrlsListParamsType } from './reducers/shortUrlsListParams';
|
||||
import './ShortUrlsList.scss';
|
||||
|
@ -34,7 +34,7 @@ const propTypes = {
|
|||
createNewVisit: PropTypes.func,
|
||||
loadMercureInfo: PropTypes.func,
|
||||
mercureInfo: MercureInfoType,
|
||||
realTimeUpdates: RealTimeUpdatesType,
|
||||
settings: SettingsType,
|
||||
};
|
||||
|
||||
// FIXME Replace with typescript: (ShortUrlsRow component)
|
||||
|
@ -52,7 +52,7 @@ const ShortUrlsList = (ShortUrlsRow) => {
|
|||
createNewVisit,
|
||||
loadMercureInfo,
|
||||
mercureInfo,
|
||||
realTimeUpdates,
|
||||
settings: { realTimeUpdates },
|
||||
}) => {
|
||||
const { orderBy } = shortUrlsListParams;
|
||||
const [ order, setOrder ] = useState({
|
||||
|
|
|
@ -31,7 +31,7 @@ const provideServices = (bottle, connect) => {
|
|||
|
||||
bottle.serviceFactory('ShortUrlsList', ShortUrlsList, 'ShortUrlsRow');
|
||||
bottle.decorator('ShortUrlsList', connect(
|
||||
[ 'selectedServer', 'shortUrlsListParams', 'mercureInfo', 'realTimeUpdates' ],
|
||||
[ 'selectedServer', 'shortUrlsListParams', 'mercureInfo', 'settings' ],
|
||||
[ 'listShortUrls', 'resetShortUrlParams', 'createNewVisit', 'loadMercureInfo' ]
|
||||
));
|
||||
|
||||
|
|
|
@ -12,7 +12,7 @@ import { formatDate } from '../utils/helpers/date';
|
|||
import { useToggle } from '../utils/helpers/hooks';
|
||||
import { MercureInfoType } from '../mercure/reducers/mercureInfo';
|
||||
import { bindToMercureTopic } from '../mercure/helpers';
|
||||
import { RealTimeUpdatesType } from '../settings/reducers/realTimeUpdates';
|
||||
import { SettingsType } from '../settings/reducers/settings';
|
||||
import SortableBarGraph from './SortableBarGraph';
|
||||
import { shortUrlVisitsType } from './reducers/shortUrlVisits';
|
||||
import VisitsHeader from './VisitsHeader';
|
||||
|
@ -39,7 +39,7 @@ const propTypes = {
|
|||
createNewVisit: PropTypes.func,
|
||||
loadMercureInfo: PropTypes.func,
|
||||
mercureInfo: MercureInfoType,
|
||||
realTimeUpdates: RealTimeUpdatesType,
|
||||
settings: SettingsType,
|
||||
};
|
||||
|
||||
const highlightedVisitsToStats = (highlightedVisits, prop) => highlightedVisits.reduce((acc, highlightedVisit) => {
|
||||
|
@ -68,7 +68,7 @@ const ShortUrlVisits = ({ processStatsFromVisits, normalizeVisits }, OpenMapModa
|
|||
createNewVisit,
|
||||
loadMercureInfo,
|
||||
mercureInfo,
|
||||
realTimeUpdates,
|
||||
settings: { realTimeUpdates },
|
||||
}) => {
|
||||
const [ startDate, setStartDate ] = useState(undefined);
|
||||
const [ endDate, setEndDate ] = useState(undefined);
|
||||
|
|
|
@ -11,7 +11,7 @@ const provideServices = (bottle, connect) => {
|
|||
bottle.serviceFactory('MapModal', () => MapModal);
|
||||
bottle.serviceFactory('ShortUrlVisits', ShortUrlVisits, 'VisitsParser', 'OpenMapModalBtn');
|
||||
bottle.decorator('ShortUrlVisits', connect(
|
||||
[ 'shortUrlVisits', 'shortUrlDetail', 'mercureInfo', 'realTimeUpdates' ],
|
||||
[ 'shortUrlVisits', 'shortUrlDetail', 'mercureInfo', 'settings' ],
|
||||
[ 'getShortUrlVisits', 'getShortUrlDetail', 'cancelGetShortUrlVisits', 'createNewVisit', 'loadMercureInfo' ]
|
||||
));
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@ describe('<Home />', () => {
|
|||
let wrapped;
|
||||
const defaultProps = {
|
||||
resetSelectedServer: jest.fn(),
|
||||
servers: { loading: false, list: {} },
|
||||
servers: {},
|
||||
};
|
||||
const createComponent = (props) => {
|
||||
const actualProps = { ...defaultProps, ...props };
|
||||
|
@ -24,20 +24,12 @@ describe('<Home />', () => {
|
|||
expect(wrapped.find('Link')).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('shows message when loading servers', () => {
|
||||
const wrapped = createComponent({ servers: { loading: true } });
|
||||
const span = wrapped.find('span');
|
||||
|
||||
expect(span).toHaveLength(1);
|
||||
expect(span.text()).toContain('Trying to load servers...');
|
||||
});
|
||||
|
||||
it('Asks to select a server when not loadign and servers exist', () => {
|
||||
const list = [
|
||||
{ name: 'foo', id: '1' },
|
||||
{ name: 'bar', id: '2' },
|
||||
];
|
||||
const wrapped = createComponent({ servers: { list } });
|
||||
it('asks to select a server when servers exist', () => {
|
||||
const servers = {
|
||||
1: { name: 'foo', id: '1' },
|
||||
2: { name: 'bar', id: '2' },
|
||||
};
|
||||
const wrapped = createComponent({ servers });
|
||||
const span = wrapped.find('span');
|
||||
|
||||
expect(span).toHaveLength(1);
|
||||
|
|
|
@ -8,12 +8,9 @@ describe('<ServersDropdown />', () => {
|
|||
let wrapped;
|
||||
let ServersDropdown;
|
||||
const servers = {
|
||||
list: {
|
||||
'1a': { name: 'foo', id: 1 },
|
||||
'2b': { name: 'bar', id: 2 },
|
||||
'3c': { name: 'baz', id: 3 },
|
||||
},
|
||||
loading: false,
|
||||
'1a': { name: 'foo', id: 1 },
|
||||
'2b': { name: 'bar', id: 2 },
|
||||
'3c': { name: 'baz', id: 3 },
|
||||
};
|
||||
const history = {
|
||||
push: jest.fn(),
|
||||
|
@ -26,7 +23,7 @@ describe('<ServersDropdown />', () => {
|
|||
afterEach(() => wrapped.unmount());
|
||||
|
||||
it('contains the list of servers, the divider and the export button', () =>
|
||||
expect(wrapped.find(DropdownItem)).toHaveLength(values(servers.list).length + 2));
|
||||
expect(wrapped.find(DropdownItem)).toHaveLength(values(servers).length + 2));
|
||||
|
||||
it('contains a toggle with proper title', () =>
|
||||
expect(wrapped.find(DropdownToggle)).toHaveLength(1));
|
||||
|
@ -40,7 +37,7 @@ describe('<ServersDropdown />', () => {
|
|||
|
||||
it('shows a message when no servers exist yet', () => {
|
||||
wrapped = shallow(
|
||||
<ServersDropdown servers={{ loading: false, list: {} }} listServers={identity} history={history} />
|
||||
<ServersDropdown servers={{}} listServers={identity} history={history} />
|
||||
);
|
||||
const item = wrapped.find(DropdownItem);
|
||||
|
||||
|
@ -48,15 +45,4 @@ describe('<ServersDropdown />', () => {
|
|||
expect(item.prop('disabled')).toEqual(true);
|
||||
expect(item.find('i').text()).toEqual('Add a server first...');
|
||||
});
|
||||
|
||||
it('shows a message when loading', () => {
|
||||
wrapped = shallow(
|
||||
<ServersDropdown servers={{ loading: true, list: {} }} listServers={identity} history={history} />
|
||||
);
|
||||
const item = wrapped.find(DropdownItem);
|
||||
|
||||
expect(item).toHaveLength(1);
|
||||
expect(item.prop('disabled')).toEqual(true);
|
||||
expect(item.find('i').text()).toEqual('Trying to load servers...');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -32,7 +32,7 @@ describe('<ServerError />', () => {
|
|||
])('renders expected information for type "%s"', (type, textsToFind) => {
|
||||
wrapper = shallow(
|
||||
<BrowserRouter>
|
||||
<ServerError type={type} servers={{ list: [] }} selectedServer={selectedServer} />
|
||||
<ServerError type={type} servers={{}} selectedServer={selectedServer} />
|
||||
</BrowserRouter>
|
||||
);
|
||||
const wrapperText = wrapper.html();
|
||||
|
|
55
test/servers/reducers/remoteServers.test.js
Normal file
55
test/servers/reducers/remoteServers.test.js
Normal file
|
@ -0,0 +1,55 @@
|
|||
import { fetchServers } from '../../../src/servers/reducers/remoteServers';
|
||||
import { CREATE_SERVERS } from '../../../src/servers/reducers/servers';
|
||||
|
||||
describe('remoteServersReducer', () => {
|
||||
afterEach(jest.resetAllMocks);
|
||||
|
||||
describe('fetchServers', () => {
|
||||
const axios = { get: jest.fn() };
|
||||
const dispatch = jest.fn();
|
||||
|
||||
it.each([
|
||||
[
|
||||
Promise.resolve({
|
||||
data: [
|
||||
{
|
||||
id: '111',
|
||||
name: 'acel.me from servers.json',
|
||||
url: 'https://acel.me',
|
||||
apiKey: '07fb8a96-8059-4094-a24c-80a7d5e7e9b0',
|
||||
},
|
||||
{
|
||||
id: '222',
|
||||
name: 'Local from servers.json',
|
||||
url: 'http://localhost:8000',
|
||||
apiKey: '7a531c75-134e-4d5c-86e0-a71b7167b57a',
|
||||
},
|
||||
],
|
||||
}),
|
||||
{
|
||||
111: {
|
||||
id: '111',
|
||||
name: 'acel.me from servers.json',
|
||||
url: 'https://acel.me',
|
||||
apiKey: '07fb8a96-8059-4094-a24c-80a7d5e7e9b0',
|
||||
},
|
||||
222: {
|
||||
id: '222',
|
||||
name: 'Local from servers.json',
|
||||
url: 'http://localhost:8000',
|
||||
apiKey: '7a531c75-134e-4d5c-86e0-a71b7167b57a',
|
||||
},
|
||||
},
|
||||
],
|
||||
[ Promise.resolve('<html></html>'), {}],
|
||||
[ Promise.reject({}), {}],
|
||||
])('tries to fetch servers from remote', async (mockedValue, expectedList) => {
|
||||
axios.get.mockReturnValue(mockedValue);
|
||||
|
||||
await fetchServers(axios)()(dispatch);
|
||||
|
||||
expect(dispatch).toHaveBeenCalledWith({ type: CREATE_SERVERS, newServers: expectedList });
|
||||
expect(axios.get).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -32,9 +32,7 @@ describe('selectedServerReducer', () => {
|
|||
id: 'abc123',
|
||||
};
|
||||
const version = '1.19.0';
|
||||
const ServersServiceMock = {
|
||||
findServerById: jest.fn(() => selectedServer),
|
||||
};
|
||||
const createGetStateMock = (id) => jest.fn().mockReturnValue({ servers: { [id]: selectedServer } });
|
||||
const apiClientMock = {
|
||||
health: jest.fn(),
|
||||
};
|
||||
|
@ -49,6 +47,8 @@ describe('selectedServerReducer', () => {
|
|||
[ 'latest', MAX_FALLBACK_VERSION, 'latest' ],
|
||||
[ '%invalid_semver%', MIN_FALLBACK_VERSION, '%invalid_semver%' ],
|
||||
])('dispatches proper actions', async (serverVersion, expectedVersion, expectedPrintableVersion) => {
|
||||
const id = uuid();
|
||||
const getState = createGetStateMock(id);
|
||||
const expectedSelectedServer = {
|
||||
...selectedServer,
|
||||
version: expectedVersion,
|
||||
|
@ -57,7 +57,7 @@ describe('selectedServerReducer', () => {
|
|||
|
||||
apiClientMock.health.mockResolvedValue({ version: serverVersion });
|
||||
|
||||
await selectServer(ServersServiceMock, buildApiClient, loadMercureInfo)(uuid())(dispatch);
|
||||
await selectServer(buildApiClient, loadMercureInfo)(id)(dispatch, getState);
|
||||
|
||||
expect(dispatch).toHaveBeenCalledTimes(4);
|
||||
expect(dispatch).toHaveBeenNthCalledWith(1, { type: RESET_SELECTED_SERVER });
|
||||
|
@ -67,18 +67,23 @@ describe('selectedServerReducer', () => {
|
|||
});
|
||||
|
||||
it('invokes dependencies', async () => {
|
||||
await selectServer(ServersServiceMock, buildApiClient, loadMercureInfo)(uuid())(() => {});
|
||||
const id = uuid();
|
||||
const getState = createGetStateMock(id);
|
||||
|
||||
expect(ServersServiceMock.findServerById).toHaveBeenCalledTimes(1);
|
||||
await selectServer(buildApiClient, loadMercureInfo)(id)(() => {}, getState);
|
||||
|
||||
expect(getState).toHaveBeenCalledTimes(1);
|
||||
expect(buildApiClient).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('dispatches error when health endpoint fails', async () => {
|
||||
const id = uuid();
|
||||
const getState = createGetStateMock(id);
|
||||
const expectedSelectedServer = { ...selectedServer, serverNotReachable: true };
|
||||
|
||||
apiClientMock.health.mockRejectedValue({});
|
||||
|
||||
await selectServer(ServersServiceMock, buildApiClient, loadMercureInfo)(uuid())(dispatch);
|
||||
await selectServer(buildApiClient, loadMercureInfo)(id)(dispatch, getState);
|
||||
|
||||
expect(apiClientMock.health).toHaveBeenCalled();
|
||||
expect(dispatch).toHaveBeenNthCalledWith(3, { type: SELECT_SERVER, selectedServer: expectedSelectedServer });
|
||||
|
@ -86,13 +91,13 @@ describe('selectedServerReducer', () => {
|
|||
});
|
||||
|
||||
it('dispatches error when server is not found', async () => {
|
||||
const id = uuid();
|
||||
const getState = jest.fn(() => ({ servers: {} }));
|
||||
const expectedSelectedServer = { serverNotFound: true };
|
||||
|
||||
ServersServiceMock.findServerById.mockReturnValue(undefined);
|
||||
await selectServer(buildApiClient, loadMercureInfo)(id)(dispatch, getState);
|
||||
|
||||
await selectServer(ServersServiceMock, buildApiClient, loadMercureInfo)(uuid())(dispatch);
|
||||
|
||||
expect(ServersServiceMock.findServerById).toHaveBeenCalled();
|
||||
expect(getState).toHaveBeenCalled();
|
||||
expect(apiClientMock.health).not.toHaveBeenCalled();
|
||||
expect(dispatch).toHaveBeenNthCalledWith(3, { type: SELECT_SERVER, selectedServer: expectedSelectedServer });
|
||||
expect(loadMercureInfo).not.toHaveBeenCalled();
|
||||
|
|
|
@ -1,165 +0,0 @@
|
|||
import { values } from 'ramda';
|
||||
import reducer, {
|
||||
createServer,
|
||||
deleteServer,
|
||||
listServers,
|
||||
createServers,
|
||||
editServer,
|
||||
FETCH_SERVERS,
|
||||
FETCH_SERVERS_START,
|
||||
} from '../../../src/servers/reducers/server';
|
||||
|
||||
describe('serverReducer', () => {
|
||||
const list = {
|
||||
abc123: { id: 'abc123' },
|
||||
def456: { id: 'def456' },
|
||||
};
|
||||
const expectedFetchServersResult = { type: FETCH_SERVERS, list };
|
||||
const ServersServiceMock = {
|
||||
listServers: jest.fn(() => list),
|
||||
createServer: jest.fn(),
|
||||
editServer: jest.fn(),
|
||||
deleteServer: jest.fn(),
|
||||
createServers: jest.fn(),
|
||||
};
|
||||
|
||||
afterEach(jest.clearAllMocks);
|
||||
|
||||
describe('reducer', () => {
|
||||
it('returns servers when action is FETCH_SERVERS', () =>
|
||||
expect(reducer({}, { type: FETCH_SERVERS, list })).toEqual({ loading: false, list }));
|
||||
});
|
||||
|
||||
describe('action creators', () => {
|
||||
describe('listServers', () => {
|
||||
const axios = { get: jest.fn() };
|
||||
const dispatch = jest.fn();
|
||||
const NoListServersServiceMock = { ...ServersServiceMock, listServers: jest.fn(() => ({})) };
|
||||
|
||||
it('fetches servers from local storage when found', async () => {
|
||||
await listServers(ServersServiceMock, axios)()(dispatch);
|
||||
|
||||
expect(dispatch).toHaveBeenCalledTimes(2);
|
||||
expect(dispatch).toHaveBeenNthCalledWith(1, { type: FETCH_SERVERS_START });
|
||||
expect(dispatch).toHaveBeenNthCalledWith(2, expectedFetchServersResult);
|
||||
expect(ServersServiceMock.listServers).toHaveBeenCalledTimes(1);
|
||||
expect(ServersServiceMock.createServer).not.toHaveBeenCalled();
|
||||
expect(ServersServiceMock.editServer).not.toHaveBeenCalled();
|
||||
expect(ServersServiceMock.deleteServer).not.toHaveBeenCalled();
|
||||
expect(ServersServiceMock.createServers).not.toHaveBeenCalled();
|
||||
expect(axios.get).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it.each([
|
||||
[
|
||||
Promise.resolve({
|
||||
data: [
|
||||
{
|
||||
id: '111',
|
||||
name: 'acel.me from servers.json',
|
||||
url: 'https://acel.me',
|
||||
apiKey: '07fb8a96-8059-4094-a24c-80a7d5e7e9b0',
|
||||
},
|
||||
{
|
||||
id: '222',
|
||||
name: 'Local from servers.json',
|
||||
url: 'http://localhost:8000',
|
||||
apiKey: '7a531c75-134e-4d5c-86e0-a71b7167b57a',
|
||||
},
|
||||
],
|
||||
}),
|
||||
{
|
||||
111: {
|
||||
id: '111',
|
||||
name: 'acel.me from servers.json',
|
||||
url: 'https://acel.me',
|
||||
apiKey: '07fb8a96-8059-4094-a24c-80a7d5e7e9b0',
|
||||
},
|
||||
222: {
|
||||
id: '222',
|
||||
name: 'Local from servers.json',
|
||||
url: 'http://localhost:8000',
|
||||
apiKey: '7a531c75-134e-4d5c-86e0-a71b7167b57a',
|
||||
},
|
||||
},
|
||||
],
|
||||
[ Promise.resolve('<html></html>'), {}],
|
||||
[ Promise.reject({}), {}],
|
||||
])('tries to fetch servers from remote when not found locally', async (mockedValue, expectedList) => {
|
||||
axios.get.mockReturnValue(mockedValue);
|
||||
|
||||
await listServers(NoListServersServiceMock, axios)()(dispatch);
|
||||
|
||||
expect(dispatch).toHaveBeenCalledTimes(2);
|
||||
expect(dispatch).toHaveBeenNthCalledWith(1, { type: FETCH_SERVERS_START });
|
||||
expect(dispatch).toHaveBeenNthCalledWith(2, { type: FETCH_SERVERS, list: expectedList });
|
||||
expect(NoListServersServiceMock.listServers).toHaveBeenCalledTimes(1);
|
||||
expect(NoListServersServiceMock.createServer).not.toHaveBeenCalled();
|
||||
expect(NoListServersServiceMock.editServer).not.toHaveBeenCalled();
|
||||
expect(NoListServersServiceMock.deleteServer).not.toHaveBeenCalled();
|
||||
expect(NoListServersServiceMock.createServers).toHaveBeenCalledTimes(1);
|
||||
expect(axios.get).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createServer', () => {
|
||||
it('adds new server and then fetches servers again', () => {
|
||||
const serverToCreate = { id: 'abc123' };
|
||||
const result = createServer(ServersServiceMock, () => expectedFetchServersResult)(serverToCreate);
|
||||
|
||||
expect(result).toEqual(expectedFetchServersResult);
|
||||
expect(ServersServiceMock.listServers).not.toHaveBeenCalled();
|
||||
expect(ServersServiceMock.createServer).toHaveBeenCalledTimes(1);
|
||||
expect(ServersServiceMock.createServer).toHaveBeenCalledWith(serverToCreate);
|
||||
expect(ServersServiceMock.editServer).not.toHaveBeenCalled();
|
||||
expect(ServersServiceMock.deleteServer).not.toHaveBeenCalled();
|
||||
expect(ServersServiceMock.createServers).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('editServer', () => {
|
||||
it('edits existing server and then fetches servers again', () => {
|
||||
const serverToEdit = { name: 'edited' };
|
||||
const result = editServer(ServersServiceMock, () => expectedFetchServersResult)('123', serverToEdit);
|
||||
|
||||
expect(result).toEqual(expectedFetchServersResult);
|
||||
expect(ServersServiceMock.listServers).not.toHaveBeenCalled();
|
||||
expect(ServersServiceMock.createServer).not.toHaveBeenCalled();
|
||||
expect(ServersServiceMock.editServer).toHaveBeenCalledTimes(1);
|
||||
expect(ServersServiceMock.editServer).toHaveBeenCalledWith('123', serverToEdit);
|
||||
expect(ServersServiceMock.deleteServer).not.toHaveBeenCalled();
|
||||
expect(ServersServiceMock.createServers).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteServer', () => {
|
||||
it('deletes a server and then fetches servers again', () => {
|
||||
const serverToDelete = { id: 'abc123' };
|
||||
const result = deleteServer(ServersServiceMock, () => expectedFetchServersResult)(serverToDelete);
|
||||
|
||||
expect(result).toEqual(expectedFetchServersResult);
|
||||
expect(ServersServiceMock.listServers).not.toHaveBeenCalled();
|
||||
expect(ServersServiceMock.createServer).not.toHaveBeenCalled();
|
||||
expect(ServersServiceMock.createServers).not.toHaveBeenCalled();
|
||||
expect(ServersServiceMock.editServer).not.toHaveBeenCalled();
|
||||
expect(ServersServiceMock.deleteServer).toHaveBeenCalledTimes(1);
|
||||
expect(ServersServiceMock.deleteServer).toHaveBeenCalledWith(serverToDelete);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createServers', () => {
|
||||
it('creates multiple servers and then fetches servers again', () => {
|
||||
const serversToCreate = values(list);
|
||||
const result = createServers(ServersServiceMock, () => expectedFetchServersResult)(serversToCreate);
|
||||
|
||||
expect(result).toEqual(expectedFetchServersResult);
|
||||
expect(ServersServiceMock.listServers).not.toHaveBeenCalled();
|
||||
expect(ServersServiceMock.createServer).not.toHaveBeenCalled();
|
||||
expect(ServersServiceMock.editServer).not.toHaveBeenCalled();
|
||||
expect(ServersServiceMock.deleteServer).not.toHaveBeenCalled();
|
||||
expect(ServersServiceMock.createServers).toHaveBeenCalledTimes(1);
|
||||
expect(ServersServiceMock.createServers).toHaveBeenCalledWith(serversToCreate);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
85
test/servers/reducers/servers.test.js
Normal file
85
test/servers/reducers/servers.test.js
Normal file
|
@ -0,0 +1,85 @@
|
|||
import { values } from 'ramda';
|
||||
import reducer, {
|
||||
createServer,
|
||||
deleteServer,
|
||||
createServers,
|
||||
editServer,
|
||||
EDIT_SERVER,
|
||||
DELETE_SERVER,
|
||||
CREATE_SERVERS,
|
||||
} from '../../../src/servers/reducers/servers';
|
||||
|
||||
describe('serverReducer', () => {
|
||||
const list = {
|
||||
abc123: { id: 'abc123' },
|
||||
def456: { id: 'def456' },
|
||||
};
|
||||
|
||||
afterEach(jest.clearAllMocks);
|
||||
|
||||
describe('reducer', () => {
|
||||
it('returns edited server when action is EDIT_SERVER', () =>
|
||||
expect(reducer(
|
||||
list,
|
||||
{ type: EDIT_SERVER, serverId: 'abc123', serverData: { foo: 'foo' } },
|
||||
)).toEqual({
|
||||
abc123: { id: 'abc123', foo: 'foo' },
|
||||
def456: { id: 'def456' },
|
||||
}));
|
||||
|
||||
it('removes server when action is DELETE_SERVER', () =>
|
||||
expect(reducer(list, { type: DELETE_SERVER, serverId: 'abc123' })).toEqual({
|
||||
def456: { id: 'def456' },
|
||||
}));
|
||||
|
||||
it('appends server when action is CREATE_SERVERS', () =>
|
||||
expect(reducer(list, {
|
||||
type: CREATE_SERVERS,
|
||||
newServers: {
|
||||
ghi789: { id: 'ghi789' },
|
||||
},
|
||||
})).toEqual({
|
||||
abc123: { id: 'abc123' },
|
||||
def456: { id: 'def456' },
|
||||
ghi789: { id: 'ghi789' },
|
||||
}));
|
||||
});
|
||||
|
||||
describe('action creators', () => {
|
||||
describe('createServer', () => {
|
||||
it('returns expected action', () => {
|
||||
const serverToCreate = { id: 'abc123' };
|
||||
const result = createServer(serverToCreate);
|
||||
|
||||
expect(result).toEqual(expect.objectContaining({ type: CREATE_SERVERS }));
|
||||
});
|
||||
});
|
||||
|
||||
describe('editServer', () => {
|
||||
it('returns expected action', () => {
|
||||
const serverData = { name: 'edited' };
|
||||
const result = editServer('123', serverData);
|
||||
|
||||
expect(result).toEqual({ type: EDIT_SERVER, serverId: '123', serverData });
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteServer', () => {
|
||||
it('returns expected action', () => {
|
||||
const serverToDelete = { id: 'abc123' };
|
||||
const result = deleteServer(serverToDelete);
|
||||
|
||||
expect(result).toEqual({ type: DELETE_SERVER, serverId: 'abc123' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('createServers', () => {
|
||||
it('returns expected action', () => {
|
||||
const newServers = values(list);
|
||||
const result = createServers(newServers);
|
||||
|
||||
expect(result).toEqual(expect.objectContaining({ type: CREATE_SERVERS }));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -18,8 +18,8 @@ describe('ServersExporter', () => {
|
|||
},
|
||||
},
|
||||
});
|
||||
const serversServiceMock = {
|
||||
listServers: jest.fn(() => ({
|
||||
const storageMock = {
|
||||
get: jest.fn(() => ({
|
||||
abc123: {
|
||||
id: 'abc123',
|
||||
name: 'foo',
|
||||
|
@ -48,19 +48,15 @@ describe('ServersExporter', () => {
|
|||
global.console = { error: jest.fn() };
|
||||
global.Blob = class Blob {};
|
||||
global.URL = { createObjectURL: () => '' };
|
||||
serversServiceMock.listServers.mockReset();
|
||||
});
|
||||
afterEach(() => {
|
||||
global.console = originalConsole;
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('logs an error if something fails', () => {
|
||||
const csvjsonMock = createCsvjsonMock(true);
|
||||
const exporter = new ServersExporter(
|
||||
serversServiceMock,
|
||||
createWindowMock(),
|
||||
csvjsonMock,
|
||||
);
|
||||
const exporter = new ServersExporter(storageMock, createWindowMock(), csvjsonMock);
|
||||
|
||||
exporter.exportServers();
|
||||
|
||||
|
@ -70,30 +66,22 @@ describe('ServersExporter', () => {
|
|||
|
||||
it('makes use of msSaveBlob API when available', () => {
|
||||
const windowMock = createWindowMock();
|
||||
const exporter = new ServersExporter(
|
||||
serversServiceMock,
|
||||
windowMock,
|
||||
createCsvjsonMock(),
|
||||
);
|
||||
const exporter = new ServersExporter(storageMock, windowMock, createCsvjsonMock());
|
||||
|
||||
exporter.exportServers();
|
||||
|
||||
expect(serversServiceMock.listServers).toHaveBeenCalledTimes(1);
|
||||
expect(storageMock.get).toHaveBeenCalledTimes(1);
|
||||
expect(windowMock.navigator.msSaveBlob).toHaveBeenCalledTimes(1);
|
||||
expect(windowMock.document.createElement).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('makes use of download link API when available', () => {
|
||||
const windowMock = createWindowMock(false);
|
||||
const exporter = new ServersExporter(
|
||||
serversServiceMock,
|
||||
windowMock,
|
||||
createCsvjsonMock(),
|
||||
);
|
||||
const exporter = new ServersExporter(storageMock, windowMock, createCsvjsonMock());
|
||||
|
||||
exporter.exportServers();
|
||||
|
||||
expect(serversServiceMock.listServers).toHaveBeenCalledTimes(1);
|
||||
expect(storageMock.get).toHaveBeenCalledTimes(1);
|
||||
expect(windowMock.document.createElement).toHaveBeenCalledTimes(1);
|
||||
expect(windowMock.document.body.appendChild).toHaveBeenCalledTimes(1);
|
||||
expect(windowMock.document.body.removeChild).toHaveBeenCalledTimes(1);
|
||||
|
|
|
@ -1,130 +0,0 @@
|
|||
import ServersService from '../../../src/servers/services/ServersService';
|
||||
|
||||
describe('ServersService', () => {
|
||||
const servers = {
|
||||
abc123: { id: 'abc123' },
|
||||
def456: { id: 'def456' },
|
||||
};
|
||||
const createService = (withServers = true) => {
|
||||
const storageMock = {
|
||||
set: jest.fn(),
|
||||
get: jest.fn(() => withServers ? servers : undefined),
|
||||
};
|
||||
const service = new ServersService(storageMock);
|
||||
|
||||
return [ service, storageMock ];
|
||||
};
|
||||
|
||||
describe('listServers', () => {
|
||||
it('returns an empty object when servers are not found in storage', () => {
|
||||
const [ service, storageMock ] = createService(false);
|
||||
|
||||
const result = service.listServers();
|
||||
|
||||
expect(result).toEqual({});
|
||||
expect(storageMock.get).toHaveBeenCalledTimes(1);
|
||||
expect(storageMock.set).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('returns value from storage when found', () => {
|
||||
const [ service, storageMock ] = createService();
|
||||
|
||||
const result = service.listServers();
|
||||
|
||||
expect(result).toEqual(servers);
|
||||
expect(storageMock.get).toHaveBeenCalledTimes(1);
|
||||
expect(storageMock.set).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('findServerById', () => {
|
||||
it('returns undefined when requested server is not found', () => {
|
||||
const [ service, storageMock ] = createService();
|
||||
|
||||
const result = service.findServerById('ghi789');
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
expect(storageMock.get).toHaveBeenCalledTimes(1);
|
||||
expect(storageMock.set).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('returns server from list when found', () => {
|
||||
const [ service, storageMock ] = createService();
|
||||
|
||||
const result = service.findServerById('abc123');
|
||||
|
||||
expect(result).toEqual({ id: 'abc123' });
|
||||
expect(storageMock.get).toHaveBeenCalledTimes(1);
|
||||
expect(storageMock.set).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('createServer', () => {
|
||||
it('adds one server to the list', () => {
|
||||
const [ service, storageMock ] = createService();
|
||||
|
||||
service.createServer({ id: 'ghi789' });
|
||||
|
||||
expect(storageMock.get).toHaveBeenCalledTimes(1);
|
||||
expect(storageMock.set).toHaveBeenCalledTimes(1);
|
||||
expect(storageMock.set).toHaveBeenCalledWith(expect.anything(), {
|
||||
abc123: { id: 'abc123' },
|
||||
def456: { id: 'def456' },
|
||||
ghi789: { id: 'ghi789' },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('createServers', () => {
|
||||
it('adds multiple servers to the list', () => {
|
||||
const [ service, storageMock ] = createService();
|
||||
|
||||
service.createServers([{ id: 'ghi789' }, { id: 'jkl123' }]);
|
||||
|
||||
expect(storageMock.get).toHaveBeenCalledTimes(1);
|
||||
expect(storageMock.set).toHaveBeenCalledTimes(1);
|
||||
expect(storageMock.set).toHaveBeenCalledWith(expect.anything(), {
|
||||
abc123: { id: 'abc123' },
|
||||
def456: { id: 'def456' },
|
||||
ghi789: { id: 'ghi789' },
|
||||
jkl123: { id: 'jkl123' },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteServer', () => {
|
||||
it('removes one server from the list', () => {
|
||||
const [ service, storageMock ] = createService();
|
||||
|
||||
service.deleteServer({ id: 'abc123' });
|
||||
|
||||
expect(storageMock.get).toHaveBeenCalledTimes(1);
|
||||
expect(storageMock.set).toHaveBeenCalledTimes(1);
|
||||
expect(storageMock.set).toHaveBeenCalledWith(expect.anything(), {
|
||||
def456: { id: 'def456' },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('editServer', () => {
|
||||
it('dos nothing is provided server does not exist', () => {
|
||||
const [ service, storageMock ] = createService();
|
||||
|
||||
service.editServer('notFound', {});
|
||||
|
||||
expect(storageMock.set).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('updates the list with provided server data', () => {
|
||||
const [ service, storageMock ] = createService();
|
||||
const serverData = { name: 'foo', apiKey: 'bar' };
|
||||
|
||||
service.editServer('abc123', serverData);
|
||||
|
||||
expect(storageMock.set).toHaveBeenCalledWith(expect.anything(), {
|
||||
abc123: { id: 'abc123', ...serverData },
|
||||
def456: { id: 'def456' },
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,47 +0,0 @@
|
|||
import reducer, {
|
||||
LOAD_REAL_TIME_UPDATES,
|
||||
loadRealTimeUpdates,
|
||||
setRealTimeUpdates,
|
||||
} from '../../../src/settings/reducers/realTimeUpdates';
|
||||
|
||||
describe('realTimeUpdatesReducer', () => {
|
||||
const SettingsServiceMock = {
|
||||
updateSettings: jest.fn(),
|
||||
loadSettings: jest.fn(),
|
||||
};
|
||||
|
||||
afterEach(jest.clearAllMocks);
|
||||
|
||||
describe('reducer', () => {
|
||||
it('returns realTimeUpdates when action is LOAD_REAL_TIME_UPDATES', () => {
|
||||
expect(reducer({}, { type: LOAD_REAL_TIME_UPDATES, enabled: true })).toEqual({ enabled: true });
|
||||
});
|
||||
});
|
||||
|
||||
describe('loadRealTimeUpdates', () => {
|
||||
it.each([
|
||||
[{}, true ],
|
||||
[{ realTimeUpdates: {} }, true ],
|
||||
[{ realTimeUpdates: { enabled: true } }, true ],
|
||||
[{ realTimeUpdates: { enabled: false } }, false ],
|
||||
])('loads settings and returns LOAD_REAL_TIME_UPDATES action', (loadedSettings, expectedEnabled) => {
|
||||
SettingsServiceMock.loadSettings.mockReturnValue(loadedSettings);
|
||||
|
||||
const result = loadRealTimeUpdates(SettingsServiceMock)();
|
||||
|
||||
expect(result).toEqual({ type: LOAD_REAL_TIME_UPDATES, enabled: expectedEnabled });
|
||||
expect(SettingsServiceMock.loadSettings).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('setRealTimeUpdates', () => {
|
||||
it.each([[ true ], [ false ]])('updates settings with provided value and then loads updates again', (enabled) => {
|
||||
const loadRealTimeUpdatesAction = jest.fn();
|
||||
|
||||
setRealTimeUpdates(SettingsServiceMock, loadRealTimeUpdatesAction)(enabled);
|
||||
|
||||
expect(SettingsServiceMock.updateSettings).toHaveBeenCalledWith({ realTimeUpdates: { enabled } });
|
||||
expect(loadRealTimeUpdatesAction).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
19
test/settings/reducers/settings.test.js
Normal file
19
test/settings/reducers/settings.test.js
Normal file
|
@ -0,0 +1,19 @@
|
|||
import reducer, { SET_REAL_TIME_UPDATES, setRealTimeUpdates } from '../../../src/settings/reducers/settings';
|
||||
|
||||
describe('settingsReducer', () => {
|
||||
const realTimeUpdates = { enabled: true };
|
||||
|
||||
describe('reducer', () => {
|
||||
it('returns realTimeUpdates when action is SET_REAL_TIME_UPDATES', () => {
|
||||
expect(reducer({}, { type: SET_REAL_TIME_UPDATES, realTimeUpdates })).toEqual({ realTimeUpdates });
|
||||
});
|
||||
});
|
||||
|
||||
describe('setRealTimeUpdates', () => {
|
||||
it.each([[ true ], [ false ]])('updates settings with provided value and then loads updates again', (enabled) => {
|
||||
const result = setRealTimeUpdates(enabled);
|
||||
|
||||
expect(result).toEqual({ type: SET_REAL_TIME_UPDATES, realTimeUpdates: { enabled } });
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,46 +0,0 @@
|
|||
import SettingsService from '../../../src/settings/services/SettingsService';
|
||||
|
||||
describe('SettingsService', () => {
|
||||
const settings = { foo: 'bar' };
|
||||
const createService = (withSettings = true) => {
|
||||
const storageMock = {
|
||||
set: jest.fn(),
|
||||
get: jest.fn(() => withSettings ? settings : undefined),
|
||||
};
|
||||
const service = new SettingsService(storageMock);
|
||||
|
||||
return [ service, storageMock ];
|
||||
};
|
||||
|
||||
afterEach(jest.resetAllMocks);
|
||||
|
||||
describe('loadSettings', () => {
|
||||
it.each([
|
||||
[ false, {}],
|
||||
[ true, settings ],
|
||||
])('returns result if found in storage', (withSettings, expectedResult) => {
|
||||
const [ service, storageMock ] = createService(withSettings);
|
||||
|
||||
const result = service.loadSettings();
|
||||
|
||||
expect(result).toEqual(expectedResult);
|
||||
expect(storageMock.get).toHaveBeenCalledTimes(1);
|
||||
expect(storageMock.set).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateSettings', () => {
|
||||
it.each([
|
||||
[ false, { hi: 'goodbye' }, { hi: 'goodbye' }],
|
||||
[ true, { hi: 'goodbye' }, { foo: 'bar', hi: 'goodbye' }],
|
||||
[ true, { foo: 'goodbye' }, { foo: 'goodbye' }],
|
||||
])('appends provided data to existing settings', (withSettings, settingsToUpdate, expectedResult) => {
|
||||
const [ service, storageMock ] = createService(withSettings);
|
||||
|
||||
service.updateSettings(settingsToUpdate);
|
||||
|
||||
expect(storageMock.get).toHaveBeenCalledTimes(1);
|
||||
expect(storageMock.set).toHaveBeenCalledWith(expect.anything(), expectedResult);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -9,6 +9,7 @@ describe('<ShortUrlsList />', () => {
|
|||
const ShortUrlsRow = () => '';
|
||||
const listShortUrlsMock = jest.fn();
|
||||
const resetShortUrlParamsMock = jest.fn();
|
||||
const realTimeUpdates = { enabled: true };
|
||||
|
||||
const ShortUrlsList = shortUrlsListCreator(ShortUrlsRow);
|
||||
|
||||
|
@ -37,6 +38,7 @@ describe('<ShortUrlsList />', () => {
|
|||
]
|
||||
}
|
||||
mercureInfo={{ loading: true }}
|
||||
settings={{ realTimeUpdates }}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
|
|
@ -21,6 +21,7 @@ describe('<ShortUrlVisits />', () => {
|
|||
const history = {
|
||||
goBack: jest.fn(),
|
||||
};
|
||||
const realTimeUpdates = { enabled: true };
|
||||
|
||||
const createComponent = (shortUrlVisits) => {
|
||||
const ShortUrlVisits = createShortUrlVisits({ processStatsFromVisits, normalizeVisits: identity }, () => '');
|
||||
|
@ -36,6 +37,7 @@ describe('<ShortUrlVisits />', () => {
|
|||
shortUrlDetail={{}}
|
||||
cancelGetShortUrlVisits={identity}
|
||||
matchMedia={() => ({ matches: false })}
|
||||
settings={{ realTimeUpdates }}
|
||||
/>
|
||||
);
|
||||
|
||||
|
|
Loading…
Reference in a new issue