From 3ac1c9821b82ca53413f9ca5d3fd6b8fb6c72980 Mon Sep 17 00:00:00 2001 From: Caleb Roseland Date: Tue, 29 Oct 2024 08:56:22 -0500 Subject: [PATCH] MM-61198: react-hooks-testing-library and renderHookWithContext (#28886) --- webapp/channels/package.json | 1 + .../src/components/threading/hooks.test.ts | 36 ++++++++ .../src/tests/react_testing_utils.tsx | 83 ++++++++++++++----- webapp/package-lock.json | 47 +++++++++++ 4 files changed, 147 insertions(+), 20 deletions(-) create mode 100644 webapp/channels/src/components/threading/hooks.test.ts diff --git a/webapp/channels/package.json b/webapp/channels/package.json index 09f773e3cf..c1368541f1 100644 --- a/webapp/channels/package.json +++ b/webapp/channels/package.json @@ -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", diff --git a/webapp/channels/src/components/threading/hooks.test.ts b/webapp/channels/src/components/threading/hooks.test.ts new file mode 100644 index 0000000000..8212b84754 --- /dev/null +++ b/webapp/channels/src/components/threading/hooks.test.ts @@ -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 = { + 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); + }); + }); +}); diff --git a/webapp/channels/src/tests/react_testing_utils.tsx b/webapp/channels/src/tests/react_testing_utils.tsx index ecd682f6ef..8693222b55 100644 --- a/webapp/channels/src/tests/react_testing_utils.tsx +++ b/webapp/channels/src/tests/react_testing_utils.tsx @@ -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'; @@ -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 ( - - - - - {props.children} - - - - - ); - } - 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 {children}; + }, + }); return { ...results, @@ -106,6 +93,36 @@ export const renderWithContext = ( }; }; +export const renderHookWithContext = ( + callback: (props: TProps) => TResult, + initialState: DeepPartial = {}, + 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 {children}; + }, + }); +}; + function configureOrMockStore(initialState: DeepPartial, useMockedStore: boolean, extraReducersKeys?: string[]) { let testReducers; if (extraReducersKeys) { @@ -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 | undefined; + locale: string; + useMockedStore: boolean; +} + +type RenderStateProps = {children: React.ReactNode; store: any; history: History; 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 ( + + + + + {children} + + + + + ); +}; diff --git a/webapp/package-lock.json b/webapp/package-lock.json index a2fd4074d5..1afe9a6374 100644 --- a/webapp/package-lock.json +++ b/webapp/package-lock.json @@ -159,6 +159,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", @@ -5169,6 +5170,36 @@ "react-dom": "*" } }, + "node_modules/@testing-library/react-hooks": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/@testing-library/react-hooks/-/react-hooks-8.0.1.tgz", + "integrity": "sha512-Aqhl2IVmLt8IovEVarNDFuJDVWVvhnr9/GCU6UUnrYXwgDFF9h2L2o2P9KBni1AST5sT6riAyoukFLyjQUgD/g==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.12.5", + "react-error-boundary": "^3.1.0" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "@types/react": "^16.9.0 || ^17.0.0", + "react": "^16.9.0 || ^17.0.0", + "react-dom": "^16.9.0 || ^17.0.0", + "react-test-renderer": "^16.9.0 || ^17.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "react-dom": { + "optional": true + }, + "react-test-renderer": { + "optional": true + } + } + }, "node_modules/@testing-library/user-event": { "version": "13.5.0", "dev": true, @@ -19236,6 +19267,22 @@ "react": "17.0.2" } }, + "node_modules/react-error-boundary": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-3.1.4.tgz", + "integrity": "sha512-uM9uPzZJTF6wRQORmSrvOIgt4lJ9MC1sNgEOj2XGsDTRE4kmpWxg7ENK9EWNKJRMAOY9z0MuF4yIfl6gp4sotA==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "engines": { + "node": ">=10", + "npm": ">=6" + }, + "peerDependencies": { + "react": ">=16.13.1" + } + }, "node_modules/react-fast-compare": { "version": "3.2.2", "license": "MIT"