chore: test redux components with jest
This commit is contained in:
@@ -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}
|
||||
/>,
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
}}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
50
src/reducers/configReducer.ts
Normal file
50
src/reducers/configReducer.ts
Normal 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;
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
43
src/utils/test-utils.tsx
Normal 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
45
src/utils/user.ts
Normal 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');
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user