diff --git a/webapp/channels/src/components/external_link/external_link.test.tsx b/webapp/channels/src/components/external_link/external_link.test.tsx index 67e237286d..753b48edb5 100644 --- a/webapp/channels/src/components/external_link/external_link.test.tsx +++ b/webapp/channels/src/components/external_link/external_link.test.tsx @@ -59,12 +59,11 @@ describe('components/external_link', () => { }, }, }; - const store: GlobalState = JSON.parse(JSON.stringify(state)); renderWithIntlAndStore( {'Click Me'} , - store, + state, ); expect(screen.queryByText('Click Me')).toHaveAttribute( @@ -86,12 +85,11 @@ describe('components/external_link', () => { }, }, }; - const store: GlobalState = JSON.parse(JSON.stringify(state)); renderWithIntlAndStore( {'Click Me'} , - store, + state, ); expect(screen.queryByText('Click Me')).toHaveAttribute( @@ -113,12 +111,11 @@ describe('components/external_link', () => { }, }, }; - const store: GlobalState = JSON.parse(JSON.stringify(state)); renderWithIntlAndStore( {'Click Me'} , - store, + state, ); expect(screen.queryByText('Click Me')).not.toHaveAttribute( @@ -140,14 +137,13 @@ describe('components/external_link', () => { }, }, }; - const store: GlobalState = JSON.parse(JSON.stringify(state)); renderWithIntlAndStore( {'Click Me'}, - store, + state, ); expect(screen.queryByText('Click Me')).toHaveAttribute( @@ -175,14 +171,13 @@ describe('components/external_link', () => { }, }, }; - const store: GlobalState = JSON.parse(JSON.stringify(state)); renderWithIntlAndStore( {'Click Me'} , - store, + state, ); expect(screen.queryByText('Click Me')).toHaveAttribute( diff --git a/webapp/channels/src/components/post/post_component.test.tsx b/webapp/channels/src/components/post/post_component.test.tsx new file mode 100644 index 0000000000..8bba229665 --- /dev/null +++ b/webapp/channels/src/components/post/post_component.test.tsx @@ -0,0 +1,131 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {screen} from '@testing-library/react'; +import React from 'react'; + +import {DeepPartial} from '@mattermost/types/utilities'; + +import {renderWithFullContext} from 'tests/react_testing_utils'; + +import {GlobalState} from 'types/store'; + +import {Locations} from 'utils/constants'; +import {TestHelper} from 'utils/test_helper'; + +import PostComponent from './post_component'; +import mergeObjects from 'packages/mattermost-redux/test/merge_objects'; + +describe('PostComponent', () => { + const baseProps = { + center: false, + currentTeam: TestHelper.getTeamMock(), + currentUserId: 'currentUserId', + displayName: '', + isBot: false, + isFlagged: false, + isMobileView: false, + isPostAcknowledgementsEnabled: false, + isPostPriorityEnabled: false, + location: Locations.CENTER, + post: TestHelper.getPostMock(), + recentEmojis: [], + actions: { + markPostAsUnread: jest.fn(), + emitShortcutReactToLastPostFrom: jest.fn(), + setActionsMenuInitialisationState: jest.fn(), + selectPost: jest.fn(), + selectPostFromRightHandSideSearch: jest.fn(), + removePost: jest.fn(), + closeRightHandSide: jest.fn(), + selectPostCard: jest.fn(), + setRhsExpanded: jest.fn(), + }, + }; + + describe('reactions', () => { + const baseState: DeepPartial = { + entities: { + posts: { + reactions: { + [baseProps.post.id]: { + [`${baseProps.currentUserId}-taco`]: TestHelper.getReactionMock({emoji_name: 'taco'}), + }, + }, + }, + }, + }; + + test('should show reactions in the center channel', () => { + renderWithFullContext( + , + baseState, + ); + + expect(screen.getByLabelText('reactions')).toBeInTheDocument(); + }); + + test('should show reactions in thread view', () => { + const state = mergeObjects(baseState, { + views: { + rhs: { + selectedPostId: baseProps.post.id, + }, + }, + }); + + const {rerender} = renderWithFullContext( + , + state, + ); + + expect(screen.getByLabelText('reactions')).toBeInTheDocument(); + + rerender( + , + ); + + expect(screen.getByLabelText('reactions')).toBeInTheDocument(); + }); + + test('should show only show reactions in search results with pinned/saved posts visible', () => { + const {rerender} = renderWithFullContext( + , + baseState, + ); + + expect(screen.queryByLabelText('reactions')).not.toBeInTheDocument(); + + rerender( + , + ); + + expect(screen.getByLabelText('reactions')).toBeInTheDocument(); + + rerender( + , + ); + + expect(screen.getByLabelText('reactions')).toBeInTheDocument(); + }); + }); +}); diff --git a/webapp/channels/src/components/post/post_component.tsx b/webapp/channels/src/components/post/post_component.tsx index 9fba63cdc4..6a63f95bb9 100644 --- a/webapp/channels/src/components/post/post_component.tsx +++ b/webapp/channels/src/components/post/post_component.tsx @@ -480,7 +480,7 @@ const PostComponent = (props: Props): JSX.Element => { ) : null; const currentPostDay = getDateForUnixTicks(post.create_at); const channelDisplayName = getChannelName(); - const showReactions = props.location !== Locations.SEARCH && !props.isPinnedPosts && !props.isFlaggedPosts; + const showReactions = props.location !== Locations.SEARCH || props.isPinnedPosts || props.isFlaggedPosts; const getTestId = () => { let idPrefix: string; diff --git a/webapp/channels/src/components/post/user_profile.tsx b/webapp/channels/src/components/post/user_profile.tsx index ca2d0ea41e..03f1280ef3 100644 --- a/webapp/channels/src/components/post/user_profile.tsx +++ b/webapp/channels/src/components/post/user_profile.tsx @@ -4,7 +4,7 @@ import React, {ReactNode} from 'react'; import {FormattedMessage} from 'react-intl'; -import Constants, {Locations} from 'utils/constants'; +import {Locations} from 'utils/constants'; import {fromAutoResponder, isFromWebhook} from 'utils/post_utils'; import Tag from 'components/widgets/tag/tag'; @@ -17,7 +17,6 @@ import {Post} from '@mattermost/types/posts'; type Props = { post: Post; compactDisplay?: boolean; - currentUserId: string; colorizeUsernames?: boolean; enablePostUsernameOverride?: boolean; isConsecutivePost?: boolean; @@ -142,7 +141,6 @@ const PostUserProfile = (props: Props): JSX.Element | null => { /> } userId={post.user_id} - overwriteImage={Constants.SYSTEM_MESSAGE_PROFILE_IMAGE} disablePopover={true} channelId={post.channel_id} colorize={colorize} diff --git a/webapp/channels/src/components/post_view/commented_on/__snapshots__/commented_on.test.tsx.snap b/webapp/channels/src/components/post_view/commented_on/__snapshots__/commented_on.test.tsx.snap index c34af41f1c..844456614d 100644 --- a/webapp/channels/src/components/post_view/commented_on/__snapshots__/commented_on.test.tsx.snap +++ b/webapp/channels/src/components/post_view/commented_on/__snapshots__/commented_on.test.tsx.snap @@ -21,7 +21,6 @@ exports[`components/post_view/CommentedOn should match snapshot 1`] = ` hasMention={true} hideStatus={false} isRHS={false} - overwriteImage="" overwriteName="" userId="" /> @@ -60,7 +59,6 @@ exports[`components/post_view/CommentedOn should match snapshot 2`] = ` hasMention={true} hideStatus={false} isRHS={false} - overwriteImage="" overwriteName="" userId="" /> @@ -99,7 +97,6 @@ exports[`components/post_view/CommentedOn should match snapshot 3`] = ` hasMention={true} hideStatus={false} isRHS={false} - overwriteImage="" overwriteName="" userId="" /> @@ -140,7 +137,6 @@ exports[`components/post_view/CommentedOn should match snapshots for post with p hasMention={true} hideStatus={false} isRHS={false} - overwriteImage="" overwriteName="" userId="" /> @@ -179,7 +175,6 @@ exports[`components/post_view/CommentedOn should match snapshots for post with p hasMention={true} hideStatus={false} isRHS={false} - overwriteImage="" overwriteName="" userId="" /> @@ -218,7 +213,6 @@ exports[`components/post_view/CommentedOn should match snapshots for post with p hasMention={true} hideStatus={false} isRHS={false} - overwriteImage="" overwriteName="" userId="" /> @@ -257,7 +251,6 @@ exports[`components/post_view/CommentedOn should match snapshots for post with p hasMention={true} hideStatus={false} isRHS={false} - overwriteImage="" overwriteName="" userId="" /> diff --git a/webapp/channels/src/components/self_hosted_purchases/self_hosted_expansion_modal/index.test.tsx b/webapp/channels/src/components/self_hosted_purchases/self_hosted_expansion_modal/index.test.tsx index 2915d932de..89642776ee 100644 --- a/webapp/channels/src/components/self_hosted_purchases/self_hosted_expansion_modal/index.test.tsx +++ b/webapp/channels/src/components/self_hosted_purchases/self_hosted_expansion_modal/index.test.tsx @@ -17,6 +17,7 @@ import {DeepPartial} from '@mattermost/types/utilities'; import SelfHostedExpansionModal, {makeInitialState, canSubmit, FormState} from './'; import moment from 'moment-timezone'; +import mergeObjects from 'packages/mattermost-redux/test/merge_objects'; interface MockCardInputProps { onCardInputChange: (event: {complete: boolean}) => void; @@ -345,13 +346,19 @@ describe('SelfHostedExpansionModal RHS Card', () => { }); it('New seats input cannot be less than 1', () => { - if (initialState.entities?.users?.filteredStats?.total_users_count) { - initialState.entities.users.filteredStats.total_users_count = 50; - } + const state = mergeObjects(initialState, { + entities: { + users: { + filteredStats: { + total_users_count: 50, + }, + }, + }, + }); const expectedAddNewSeats = '1'; - renderWithIntlAndStore(
, initialState); + renderWithIntlAndStore(
, state); fillForm(defaultSuccessForm); // Try to set a negative value. diff --git a/webapp/channels/src/components/user_profile/user_profile.tsx b/webapp/channels/src/components/user_profile/user_profile.tsx index 689557b2d1..770ca0c54d 100644 --- a/webapp/channels/src/components/user_profile/user_profile.tsx +++ b/webapp/channels/src/components/user_profile/user_profile.tsx @@ -35,7 +35,6 @@ export type UserProfileProps = { hasMention?: boolean; hideStatus?: boolean; isRHS?: boolean; - overwriteImage?: React.ReactNode; channelId?: string; theme?: Theme; } @@ -49,7 +48,6 @@ export default class UserProfile extends PureComponent { hasMention: false, hideStatus: false, isRHS: false, - overwriteImage: '', overwriteName: '', colorize: false, }; diff --git a/webapp/channels/src/images/logo_compact.png b/webapp/channels/src/images/logo_compact.png deleted file mode 100644 index e826e10f2f..0000000000 Binary files a/webapp/channels/src/images/logo_compact.png and /dev/null differ diff --git a/webapp/channels/src/tests/react_testing_utils.test.tsx b/webapp/channels/src/tests/react_testing_utils.test.tsx new file mode 100644 index 0000000000..2bdf23e2f9 --- /dev/null +++ b/webapp/channels/src/tests/react_testing_utils.test.tsx @@ -0,0 +1,244 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {screen} from '@testing-library/react'; +import React from 'react'; +import {FormattedMessage, useIntl} from 'react-intl'; +import {connect, useSelector} from 'react-redux'; +import {Link, Route} from 'react-router-dom'; + +import {getUser} from 'mattermost-redux/selectors/entities/users'; +import mergeObjects from 'packages/mattermost-redux/test/merge_objects'; + +import {GlobalState} from 'types/store'; + +import {TestHelper} from 'utils/test_helper'; + +import {renderWithFullContext} from './react_testing_utils'; + +describe('renderWithFullContext', () => { + test('should be able to render anything', () => { + const TestComponent = () => { + return
{'Anything'}
; + }; + + renderWithFullContext( + , + {}, + ); + + expect(screen.getByText('Anything')).toBeInTheDocument(); + }); + + test('should be able to render react-intl components', () => { + const TestComponent = () => { + return ( + + ); + }; + + renderWithFullContext( + , + ); + + expect(screen.getByText('Build Number:')).toBeInTheDocument(); + }); + + test('should be able to render components using react-intl hooks', () => { + const TestComponent = () => { + const intl = useIntl(); + + return
{intl.formatMessage({id: 'about.hash', defaultMessage: 'Build Hash:'})}
; + }; + + renderWithFullContext( + , + ); + + expect(screen.getByText('Build Hash:')).toBeInTheDocument(); + }); + + test('should be able to render react-router components', () => { + const RouteComponent = () => { + return
{'this is the route component'}
; + }; + const TestComponent = () => { + return ( +
+ + {'Test Link'} +
+ ); + }; + + renderWithFullContext( + , + ); + + expect(screen.getByText('this is the route component')).toBeInTheDocument(); + expect(screen.getByRole('link', {name: 'Test Link'})).toBeInTheDocument(); + }); + + test('should be able to render components that use connect to access the Redux store', () => { + const UnconnectedTestComponent = (props: {numProfiles: number}) => { + return
{`There are ${props.numProfiles} users loaded`}
; + }; + const TestComponent = connect((state: GlobalState) => ({ + numProfiles: Object.keys(state.entities.users.profiles).length, + }))(UnconnectedTestComponent); + + renderWithFullContext( + , + ); + + expect(screen.getByText('There are 0 users loaded')).toBeInTheDocument(); + }); + + test('should be able to render components that use hooks to access the Redux store', () => { + const TestComponent = () => { + const numProfiles = useSelector((state: GlobalState) => Object.keys(state.entities.users.profiles).length); + return
{`There are ${numProfiles} users loaded`}
; + }; + + renderWithFullContext( + , + ); + + expect(screen.getByText('There are 0 users loaded')).toBeInTheDocument(); + }); + + test('should be able to rerender components without losing context', () => { + const TestComponent = (props: {appTitle: string}) => { + return ( + + ); + }; + + const {rerender} = renderWithFullContext( + , + ); + + expect(screen.getByText('About Mattermost')).toBeInTheDocument(); + + rerender( + , + ); + + expect(screen.getByText('About Mattermots')).toBeInTheDocument(); + }); + + test('should be able to inject store state and replace it later', () => { + const initialState = { + entities: { + users: { + profiles: { + user1: TestHelper.getUserMock({id: 'user1', username: 'Alpha'}), + user2: TestHelper.getUserMock({id: 'user2', username: 'Bravo'}), + }, + }, + }, + }; + + const TestComponent = () => { + const user1 = useSelector((state: GlobalState) => getUser(state, 'user1')); + const user2 = useSelector((state: GlobalState) => getUser(state, 'user2')); + + return
{`User1 is ${user1.username} and User2 is ${user2.username}!`}
; + }; + + const {replaceStoreState} = renderWithFullContext( + , + initialState, + ); + + expect(screen.getByText('User1 is Alpha and User2 is Bravo!')).toBeInTheDocument(); + + replaceStoreState(mergeObjects(initialState, { + entities: { + users: { + profiles: { + user1: {username: 'Charlie'}, + }, + }, + }, + })); + + expect(screen.getByText('User1 is Charlie and User2 is Bravo!')).toBeInTheDocument(); + + replaceStoreState(mergeObjects(initialState, { + entities: { + users: { + profiles: { + user2: {username: 'Delta'}, + }, + }, + }, + })); + + // Since this replaces the state, user1's username goes back to the initial value + expect(screen.getByText('User1 is Alpha and User2 is Delta!')).toBeInTheDocument(); + }); + + test('should be able to update store state', () => { + const initialState = { + entities: { + users: { + profiles: { + user1: TestHelper.getUserMock({id: 'user1', username: 'Echo'}), + user2: TestHelper.getUserMock({id: 'user2', username: 'Foxtrot'}), + }, + }, + }, + }; + + const TestComponent = () => { + const user1 = useSelector((state: GlobalState) => getUser(state, 'user1')); + const user2 = useSelector((state: GlobalState) => getUser(state, 'user2')); + + return
{`User1 is ${user1.username} and User2 is ${user2.username}!`}
; + }; + + const {updateStoreState} = renderWithFullContext( + , + initialState, + ); + + expect(screen.getByText('User1 is Echo and User2 is Foxtrot!')).toBeInTheDocument(); + + updateStoreState({ + entities: { + users: { + profiles: { + user1: {username: 'Golf'}, + }, + }, + }, + }); + + expect(screen.getByText('User1 is Golf and User2 is Foxtrot!')).toBeInTheDocument(); + + updateStoreState({ + entities: { + users: { + profiles: { + user2: {username: 'Hotel'}, + }, + }, + }, + }); + + expect(screen.getByText('User1 is Golf and User2 is Hotel!')).toBeInTheDocument(); + }); +}); diff --git a/webapp/channels/src/tests/react_testing_utils.tsx b/webapp/channels/src/tests/react_testing_utils.tsx index 25888652a2..08e39150d8 100644 --- a/webapp/channels/src/tests/react_testing_utils.tsx +++ b/webapp/channels/src/tests/react_testing_utils.tsx @@ -1,22 +1,32 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import React from 'react'; import {render} from '@testing-library/react'; -import {Provider} from 'react-redux'; - +import {createBrowserHistory} from 'history'; +import React from 'react'; import {IntlProvider} from 'react-intl'; +import {Provider} from 'react-redux'; +import {Router} from 'react-router-dom'; + +import {DeepPartial} from '@mattermost/types/utilities'; + +import configureStore from 'store'; import mockStore from 'tests/test_store'; -import {GlobalState} from '@mattermost/types/store'; -import {DeepPartial} from '@mattermost/types/utilities'; + +import {GlobalState} from 'types/store'; +import mergeObjects from 'packages/mattermost-redux/test/merge_objects'; export const renderWithIntl = (component: React.ReactNode | React.ReactNodeArray, locale = 'en') => { return render({component}); }; export const renderWithIntlAndStore = (component: React.ReactNode | React.ReactNodeArray, initialState: DeepPartial, locale = 'en') => { - const store = mockStore(initialState); + // We use a redux-mock-store store for testing, but we set up a real store to ensure the initial state is complete + const realStore = configureStore(initialState); + + const store = mockStore(realStore.getState()); + return render( @@ -25,3 +35,66 @@ export const renderWithIntlAndStore = (component: React.ReactNode | React.ReactN , ); }; + +export const renderWithFullContext = (component: React.ReactNode | React.ReactNodeArray, initialState: DeepPartial = {}, locale = 'en') => { + // We use a redux-mock-store store for testing, but we set up a real store to ensure the initial state is complete + const testState = configureStore(initialState).getState(); + + // Store these in an object so that they can be maintained through rerenders + const renderState = { + component, + history: createBrowserHistory(), + locale, + state: testState, + store: mockStore(testState), + }; + + // This should wrap the component in roughly the same providers used in App and RootProvider + function wrapComponent() { + // Every time this is called, these values should be updated from `renderState` + return ( + + + + {renderState.component} + + + + ); + } + + const results = render(wrapComponent()); + + return { + ...results, + rerender: (newComponent: React.ReactNode | React.ReactNodeArray) => { + renderState.component = newComponent; + + results.rerender(wrapComponent()); + }, + + /** + * Rerenders the component after replacing the entire store state with the provided one. + */ + replaceStoreState: (newInitialState: DeepPartial) => { + const newTestState = configureStore(newInitialState).getState(); + renderState.state = newTestState; + renderState.store = mockStore(newTestState); + + results.rerender(wrapComponent()); + }, + + /** + * Rerenders the component after merging the current store state with the provided one. + */ + updateStoreState: (stateDiff: DeepPartial) => { + const newTestState = mergeObjects(renderState.state, stateDiff); + renderState.state = newTestState; + renderState.store = mockStore(newTestState); + + results.rerender(wrapComponent()); + }, + }; +}; diff --git a/webapp/channels/src/utils/constants.tsx b/webapp/channels/src/utils/constants.tsx index 7c8a60340a..93a10c261d 100644 --- a/webapp/channels/src/utils/constants.tsx +++ b/webapp/channels/src/utils/constants.tsx @@ -18,7 +18,6 @@ import pdfIcon from 'images/icons/pdf.svg'; import pptIcon from 'images/icons/ppt.svg'; import videoIcon from 'images/icons/video.svg'; import wordIcon from 'images/icons/word.svg'; -import logoImage from 'images/logo_compact.png'; import githubIcon from 'images/themes/code_themes/github.png'; import monokaiIcon from 'images/themes/code_themes/monokai.png'; import solarizedDarkIcon from 'images/themes/code_themes/solarized-dark.png'; @@ -1508,7 +1507,6 @@ export const Constants = { MENTION_NAME_PADDING_LEFT: 2.4, AVATAR_WIDTH: 24, AUTO_RESPONDER: 'system_auto_responder', - SYSTEM_MESSAGE_PROFILE_IMAGE: logoImage, RESERVED_TEAM_NAMES: [ 'signup', 'login',