{
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',