chore: test redux components with jest

This commit is contained in:
Sebastian Velez
2023-01-13 18:36:39 -05:00
parent 6dfa40e026
commit 5486a69305
12 changed files with 193 additions and 119 deletions

View File

@@ -1,12 +1,11 @@
import React from 'react';
import { render } from '@testing-library/react';
import { renderWithProviders } from '../utils/test-utils';
import MainList from './MainList';
type onClick = (event: React.MouseEvent<HTMLAnchorElement, MouseEvent>) => void;
describe('MainList', () => {
let disableLogging: onClick;
let enableLogging: onClick;
let loggedIn: boolean;
let loggingEnabled: boolean;
let logoutUser: onClick;
let totalTimeLoggedToday: string;
@@ -14,17 +13,15 @@ describe('MainList', () => {
disableLogging = jest.fn();
enableLogging = jest.fn();
loggingEnabled = false;
loggedIn = false;
logoutUser = jest.fn();
totalTimeLoggedToday = '1/1/1999';
});
it('should render properly', () => {
const { container } = render(
const { container } = renderWithProviders(
<MainList
disableLogging={disableLogging}
enableLogging={enableLogging}
loggingEnabled={loggingEnabled}
loggedIn={loggedIn}
logoutUser={logoutUser}
totalTimeLoggedToday={totalTimeLoggedToday}
/>,

View File

@@ -1,9 +1,11 @@
import React from 'react';
import { useSelector } from 'react-redux';
import { ReduxSelector } from '../types/store';
import { User } from '../types/user';
export interface MainListProps {
disableLogging: (event: React.MouseEvent<HTMLAnchorElement, MouseEvent>) => void;
enableLogging: (event: React.MouseEvent<HTMLAnchorElement, MouseEvent>) => void;
loggedIn: boolean;
loggingEnabled: boolean;
logoutUser: (event: React.MouseEvent<HTMLAnchorElement, MouseEvent>) => void;
totalTimeLoggedToday?: string;
@@ -15,14 +17,17 @@ const openOptionsPage = async (): Promise<void> => {
export default function MainList({
disableLogging,
enableLogging,
loggedIn,
loggingEnabled,
logoutUser,
totalTimeLoggedToday,
}: MainListProps): JSX.Element {
const user: User | undefined = useSelector(
(selector: ReduxSelector) => selector.currentUser.user,
);
return (
<div>
{loggedIn && (
{user && (
<div className="row">
<div className="col-xs-12">
<blockquote>
@@ -34,7 +39,7 @@ export default function MainList({
</div>
</div>
)}
{loggingEnabled && loggedIn && (
{loggingEnabled && user && (
<div className="row">
<div className="col-xs-12">
<p>
@@ -45,7 +50,7 @@ export default function MainList({
</div>
</div>
)}
{!loggingEnabled && loggedIn && (
{!loggingEnabled && user && (
<div className="row">
<div className="col-xs-12">
<p>
@@ -61,7 +66,7 @@ export default function MainList({
<i className="fa fa-fw fa-cogs"></i>
Options
</a>
{loggedIn && (
{user && (
<div>
<a href="#" className="list-group-item" onClick={logoutUser}>
<i className="fa fa-fw fa-sign-out"></i>
@@ -69,7 +74,7 @@ export default function MainList({
</a>
</div>
)}
{!loggedIn && (
{!user && (
<a
target="_blank"
rel="noreferrer"

View File

@@ -1,13 +1,12 @@
import React, { useEffect, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { setValue } from '../reducers/apiKey';
import { configLogout, setApiKey } from '../reducers/configReducer';
import { userLogout } from '../reducers/currentUser';
import { ReduxSelector } from '../types/store';
import { User } from '../types/user';
import config from '../config/config';
import apiKeyInvalid from '../utils/apiKey';
import WakaTimeCore from '../core/WakaTimeCore';
import { setUser } from '../reducers/currentUser';
import changeExtensionState from '../utils/changeExtensionState';
import { fetchUserData } from '../utils/user';
export default function NavBar(): JSX.Element {
const [state, setState] = useState({
@@ -158,15 +157,11 @@ export default function NavBar(): JSX.Element {
if (state.apiKeyError === '' && state.apiKey !== '') {
setState({ ...state, loading: true });
await browser.storage.sync.set({ apiKey: state.apiKey });
dispatch(setValue(state.apiKey));
dispatch(configLogout());
dispatch(userLogout());
dispatch(setApiKey(state.apiKey));
try {
const data = await WakaTimeCore.checkAuth(state.apiKey);
dispatch(setUser(data));
} catch (err: unknown) {
dispatch(setUser(undefined));
await changeExtensionState('notSignedIn');
}
await fetchUserData(state.apiKey, dispatch);
setState({ ...state, loading: false });
}
}}

View File

@@ -1,69 +1,28 @@
import React, { useEffect, useState } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { setValue } from '../reducers/apiKey';
import { ReduxSelector } from '../types/store';
import { setUser } from '../reducers/currentUser';
import WakaTimeCore from '../core/WakaTimeCore';
import { ApiKeyReducer, ReduxSelector } from '../types/store';
import config from '../config/config';
import { fetchUserData } from '../utils/user';
import changeExtensionState from '../utils/changeExtensionState';
import NavBar from './NavBar';
import MainList from './MainList';
const API_KEY = 'waka_3766d693-bff3-4c63-8bf5-b439f3e12301';
export default function WakaTime(): JSX.Element {
const dispatch = useDispatch();
const defaultState = {
loggedIn: false,
loggingEnabled: config.loggingEnabled,
totalTimeLoggedToday: '0 minutes',
};
const [state, setState] = useState(defaultState);
const apiKeyFromRedux: string = useSelector((selector: ReduxSelector) => selector.apiKey.value);
const fetchUserData = async (): Promise<void> => {
// await browser.storage.sync.set({ apiKey: API_KEY });
let apiKey = '';
if (!apiKeyFromRedux) {
const storage = await browser.storage.sync.get({
apiKey: config.apiKey,
});
apiKey = storage.apiKey as string;
dispatch(setValue(apiKey));
}
if (!apiKey) {
await changeExtensionState('notSignedIn');
}
try {
const data = await WakaTimeCore.checkAuth(apiKey);
dispatch(setUser(data));
const items = await browser.storage.sync.get({ loggingEnabled: config.loggingEnabled });
if (items.loggingEnabled === true) {
await changeExtensionState('allGood');
} else {
await changeExtensionState('notLogging');
}
const totalTimeLoggedToday = await WakaTimeCore.getTotalTimeLoggedToday(apiKey);
setState({
...state,
loggedIn: true,
loggingEnabled: items.loggingEnabled as boolean,
totalTimeLoggedToday: totalTimeLoggedToday.text,
});
await WakaTimeCore.recordHeartbeat();
} catch (err: unknown) {
await changeExtensionState('notSignedIn');
}
};
const {
apiKey: apiKeyFromRedux,
loggingEnabled,
totalTimeLoggedToday,
}: ApiKeyReducer = useSelector((selector: ReduxSelector) => selector.config);
useEffect(() => {
fetchUserData();
fetchUserData(apiKeyFromRedux, dispatch);
}, []);
const disableLogging = async () => {
@@ -109,10 +68,9 @@ export default function WakaTime(): JSX.Element {
<MainList
disableLogging={disableLogging}
enableLogging={enableLogging}
loggingEnabled={state.loggingEnabled}
totalTimeLoggedToday={state.totalTimeLoggedToday}
loggingEnabled={loggingEnabled}
totalTimeLoggedToday={totalTimeLoggedToday}
logoutUser={logoutUser}
loggedIn={state.loggedIn}
/>
</div>
</div>

View File

@@ -1,25 +0,0 @@
import { createSlice } from '@reduxjs/toolkit';
interface setValueAction {
payload: string;
type: string;
}
export interface ApiKey {
value: string;
}
export const initialState: ApiKey = { value: '' };
const apiKeySlice = createSlice({
initialState,
name: 'spiKey',
reducers: {
setValue: (state, action: setValueAction) => {
state.value = action.payload;
},
},
});
export const actions = apiKeySlice.actions;
export const { setValue } = apiKeySlice.actions;
export default apiKeySlice.reducer;

View File

@@ -0,0 +1,50 @@
import { createSlice } from '@reduxjs/toolkit';
import config from '../config/config';
import { ApiKeyReducer } from '../types/store';
interface SetApiKeyAction {
payload: string;
type: string;
}
interface SetLoggingEnabledAction {
payload: boolean;
type: string;
}
interface SetTotalTimeLoggedTodayAction {
payload: string;
type: string;
}
export const initialConfigState: ApiKeyReducer = {
apiKey: '',
loggingEnabled: config.loggingEnabled,
totalTimeLoggedToday: '0 minutes',
};
const apiKeySlice = createSlice({
initialState: initialConfigState,
name: 'configReducer',
reducers: {
configLogout: (state) => {
state.apiKey = '';
state.loggingEnabled = config.loggingEnabled;
state.totalTimeLoggedToday = '0 minutes';
},
setApiKey: (state, action: SetApiKeyAction) => {
state.apiKey = action.payload;
},
setLoggingEnabled: (state, action: SetLoggingEnabledAction) => {
state.loggingEnabled = action.payload;
},
setTotalTimeLoggedToday: (state, action: SetTotalTimeLoggedTodayAction) => {
state.totalTimeLoggedToday = action.payload;
},
},
});
export const actions = apiKeySlice.actions;
export const { configLogout, setApiKey, setLoggingEnabled, setTotalTimeLoggedToday } =
apiKeySlice.actions;
export default apiKeySlice.reducer;

View File

@@ -39,9 +39,12 @@ const currentUser = createSlice({
setUser: (state, action: setUserAction) => {
state.user = action.payload;
},
userLogout: (state) => {
state.user = undefined;
},
},
});
export const actions = currentUser.actions;
export const { setUser } = currentUser.actions;
export const { setUser, userLogout } = currentUser.actions;
export default currentUser.reducer;

View File

@@ -1,22 +1,25 @@
import { configureStore, Store } from '@reduxjs/toolkit';
import { configureStore, Store, combineReducers } from '@reduxjs/toolkit';
import { logger } from 'redux-logger';
import { reduxBatch } from '@manaflair/redux-batch';
import devToolsEnhancer from 'remote-redux-devtools';
import currentUserReducer, { initialState as InitalCurrentUser } from '../reducers/currentUser';
import apiKeyReducer from '../reducers/apiKey';
import configReducer, { initialConfigState } from '../reducers/configReducer';
import isProd from '../utils/isProd';
import { CurrentUser } from '../types/user';
export interface RootState {
currentUser: CurrentUser;
}
// Create the root reducer separately so we can extract the RootState type
const rootReducer = combineReducers({
config: configReducer,
currentUser: currentUserReducer,
});
export type RootState = ReturnType<typeof rootReducer>;
const preloadedState: RootState = {
config: initialConfigState,
currentUser: InitalCurrentUser,
};
export type RootStore = Store<RootState>;
export default (appName: string): RootStore => {
export default (appName: string): Store<RootState> => {
const enhancers = [];
enhancers.push(reduxBatch);
if (!isProd()) {
@@ -29,10 +32,7 @@ export default (appName: string): RootStore => {
enhancers,
middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(logger),
preloadedState,
reducer: {
apiKey: apiKeyReducer,
currentUser: currentUserReducer,
},
reducer: rootReducer,
});
return store;

View File

@@ -1,10 +1,12 @@
import { CurrentUser } from './user';
export interface ApiKeyReducer {
value: string;
apiKey: string;
loggingEnabled: boolean;
totalTimeLoggedToday: string;
}
export interface ReduxSelector {
apiKey: ApiKeyReducer;
config: ApiKeyReducer;
currentUser: CurrentUser;
}

View File

@@ -1,12 +1,13 @@
import { RootStore } from '../stores/createStore';
import { Store } from '@reduxjs/toolkit';
import { RootState } from '../stores/createStore';
import { fetchCurrentUser } from '../reducers/currentUser';
import { ReduxSelector } from '../types/store';
type unsub = () => void;
export default (store: RootStore) =>
export default (store: Store<RootState>) =>
(time: number): unsub => {
const fetchUser = () => {
const apiKey: string = (store.getState() as ReduxSelector).apiKey.value;
const apiKey: string = (store.getState() as ReduxSelector).config.apiKey;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
store.dispatch(fetchCurrentUser(apiKey));

43
src/utils/test-utils.tsx Normal file
View File

@@ -0,0 +1,43 @@
import React, { PropsWithChildren } from 'react';
import { render } from '@testing-library/react';
import type { RenderOptions } from '@testing-library/react';
import { combineReducers, configureStore, Store } from '@reduxjs/toolkit';
import type { PreloadedState } from '@reduxjs/toolkit';
import { Provider } from 'react-redux';
import { RootState } from '../stores/createStore';
// As a basic setup, import your same slice reducers
import userReducer, { initialState as InitalCurrentUser } from '../reducers/currentUser';
import configReducer, { initialConfigState } from '../reducers/configReducer';
// This type interface extends the default options for render from RTL, as well
// as allows the user to specify other things such as initialState, store.
interface ExtendedRenderOptions extends Omit<RenderOptions, 'queries'> {
preloadedState?: PreloadedState<RootState>;
store?: Store<RootState>;
}
const rootReducer = combineReducers({
config: configReducer,
currentUser: userReducer,
});
export function renderWithProviders(
ui: React.ReactElement,
{
preloadedState = {
config: initialConfigState,
currentUser: InitalCurrentUser,
},
// Automatically create a store instance if no store was passed in
store = configureStore({ preloadedState, reducer: rootReducer }),
...renderOptions
}: ExtendedRenderOptions = {},
): any {
function Wrapper({ children }: PropsWithChildren<Record<string, unknown>>): JSX.Element {
return <Provider store={store}>{children}</Provider>;
}
// Return an object with the store and all of RTL's query functions
return { store, ...render(ui, { wrapper: Wrapper, ...renderOptions }) };
}

45
src/utils/user.ts Normal file
View File

@@ -0,0 +1,45 @@
import { AnyAction, Dispatch } from '@reduxjs/toolkit';
import { setApiKey, setLoggingEnabled, setTotalTimeLoggedToday } from '../reducers/configReducer';
import config from '../config/config';
import WakaTimeCore from '../core/WakaTimeCore';
import { setUser } from '../reducers/currentUser';
import changeExtensionState from './changeExtensionState';
export const fetchUserData = async (
apiKey: string,
dispatch: Dispatch<AnyAction>,
): Promise<void> => {
if (!apiKey) {
const storage = await browser.storage.sync.get({
apiKey: config.apiKey,
});
apiKey = storage.apiKey as string;
dispatch(setApiKey(apiKey));
}
if (!apiKey) {
await changeExtensionState('notSignedIn');
}
try {
const [data, totalTimeLoggedTodayResponse, items] = await Promise.all([
WakaTimeCore.checkAuth(apiKey),
WakaTimeCore.getTotalTimeLoggedToday(apiKey),
browser.storage.sync.get({ loggingEnabled: config.loggingEnabled }),
]);
dispatch(setUser(data));
if (items.loggingEnabled === true) {
await changeExtensionState('allGood');
} else {
await changeExtensionState('notLogging');
}
dispatch(setLoggingEnabled(items.loggingEnabled as boolean));
dispatch(setTotalTimeLoggedToday(totalTimeLoggedTodayResponse.text));
await WakaTimeCore.recordHeartbeat();
} catch (err: unknown) {
await changeExtensionState('notSignedIn');
}
};