Skip to content

Commit

Permalink
MM-61198: react-hooks-testing-library and renderHookWithContext (matt…
Browse files Browse the repository at this point in the history
  • Loading branch information
calebroseland authored Oct 29, 2024
1 parent da2a84e commit 3ac1c98
Show file tree
Hide file tree
Showing 4 changed files with 147 additions and 20 deletions.
1 change: 1 addition & 0 deletions webapp/channels/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@
"@stylistic/stylelint-plugin": "2.1.0",
"@testing-library/jest-dom": "5.16.4",
"@testing-library/react": "12.1.4",
"@testing-library/react-hooks": "8.0.1",
"@testing-library/user-event": "13.5.0",
"@types/bootstrap": "4.5.0",
"@types/country-list": "2.1.0",
Expand Down
36 changes: 36 additions & 0 deletions webapp/channels/src/components/threading/hooks.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.

import type {DeepPartial} from '@mattermost/types/utilities';

import {renderHookWithContext} from 'tests/react_testing_utils';
import {TestHelper} from 'utils/test_helper';

import type {GlobalState} from 'types/store';

import {useThreadRouting} from './hooks';

describe('components/threading/hooks', () => {
const mockUser = TestHelper.getUserMock();
const mockTeam = TestHelper.getTeamMock();

const mockState: DeepPartial<GlobalState> = {
entities: {
users: {
currentUserId: mockUser.id,
},
teams: {
currentTeamId: mockTeam.id,
},
},
};

describe('useThreadRouting', () => {
test('should indicate current team and user', () => {
const {result} = renderHookWithContext(() => useThreadRouting(), mockState);

expect(result.current.currentUserId).toBe(mockUser.id);
expect(result.current.currentTeamId).toBe(mockTeam.id);
});
});
});
83 changes: 63 additions & 20 deletions webapp/channels/src/tests/react_testing_utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// See LICENSE.txt for license information.

import {render} from '@testing-library/react';
import {renderHook} from '@testing-library/react-hooks';
import userEvent from '@testing-library/user-event';
import type {History} from 'history';
import {createBrowserHistory} from 'history';
Expand Down Expand Up @@ -54,28 +55,14 @@ export const renderWithContext = (
store: testStore,
};

// This should wrap the component in roughly the same providers used in App and RootProvider
function WrapComponent(props: {children: React.ReactElement}) {
// Every time this is called, these values should be updated from `renderState`
return (
<Provider store={renderState.store}>
<Router history={renderState.history}>
<IntlProvider
locale={renderState.options.locale}
messages={renderState.options.intlMessages}
>
<WebSocketContext.Provider value={WebSocketClient}>
{props.children}
</WebSocketContext.Provider>
</IntlProvider>
</Router>
</Provider>
);
}

replaceGlobalStore(() => renderState.store);

const results = render(component, {wrapper: WrapComponent});
const results = render(component, {
wrapper: ({children}) => {
// Every time this is called, these values should be updated from `renderState`
return <Providers {...renderState}>{children}</Providers>;
},
});

return {
...results,
Expand Down Expand Up @@ -106,6 +93,36 @@ export const renderWithContext = (
};
};

export const renderHookWithContext = <TProps, TResult>(
callback: (props: TProps) => TResult,
initialState: DeepPartial<GlobalState> = {},
partialOptions?: FullContextOptions,
) => {
const options = {
intlMessages: partialOptions?.intlMessages,
locale: partialOptions?.locale ?? 'en',
useMockedStore: partialOptions?.useMockedStore ?? false,
};

const testStore = configureOrMockStore(initialState, options.useMockedStore, partialOptions?.pluginReducers);

// Store these in an object so that they can be maintained through rerenders
const renderState = {
callback,
history: partialOptions?.history ?? createBrowserHistory(),
options,
store: testStore,
};
replaceGlobalStore(() => renderState.store);

return renderHook(callback, {
wrapper: ({children}) => {
// Every time this is called, these values should be updated from `renderState`
return <Providers {...renderState}>{children}</Providers>;
},
});
};

function configureOrMockStore<T>(initialState: DeepPartial<T>, useMockedStore: boolean, extraReducersKeys?: string[]) {
let testReducers;
if (extraReducersKeys) {
Expand Down Expand Up @@ -133,3 +150,29 @@ function replaceGlobalStore(getStore: () => any) {
// This may stop working if getStore starts to return new results
jest.spyOn(globalStore, 'subscribe').mockImplementation((...args) => getStore().subscribe(...args));
}

type Opts = {
intlMessages: Record<string, string> | undefined;
locale: string;
useMockedStore: boolean;
}

type RenderStateProps = {children: React.ReactNode; store: any; history: History<unknown>; options: Opts}

// This should wrap the component in roughly the same providers used in App and RootProvider
const Providers = ({children, store, history, options}: RenderStateProps) => {
return (
<Provider store={store}>
<Router history={history}>
<IntlProvider
locale={options.locale}
messages={options.intlMessages}
>
<WebSocketContext.Provider value={WebSocketClient}>
{children}
</WebSocketContext.Provider>
</IntlProvider>
</Router>
</Provider>
);
};
47 changes: 47 additions & 0 deletions webapp/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 3ac1c98

Please sign in to comment.