MM-52297 Fix reactions disappearing with search open and add testing utilities (#23510)
* MM-52297 Fix reactions disappearing when search is open * Add unit tests and extra utilities * Fix typing issue
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
00ba62b300
Коммит
789f84af90
244
webapp/channels/src/tests/react_testing_utils.test.tsx
Обычный файл
244
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 <div>{'Anything'}</div>;
|
||||
};
|
||||
|
||||
renderWithFullContext(
|
||||
<TestComponent/>,
|
||||
{},
|
||||
);
|
||||
|
||||
expect(screen.getByText('Anything')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should be able to render react-intl components', () => {
|
||||
const TestComponent = () => {
|
||||
return (
|
||||
<FormattedMessage
|
||||
id='about.buildnumber'
|
||||
defaultMessage='Build Number:'
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
renderWithFullContext(
|
||||
<TestComponent/>,
|
||||
);
|
||||
|
||||
expect(screen.getByText('Build Number:')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should be able to render components using react-intl hooks', () => {
|
||||
const TestComponent = () => {
|
||||
const intl = useIntl();
|
||||
|
||||
return <div>{intl.formatMessage({id: 'about.hash', defaultMessage: 'Build Hash:'})}</div>;
|
||||
};
|
||||
|
||||
renderWithFullContext(
|
||||
<TestComponent/>,
|
||||
);
|
||||
|
||||
expect(screen.getByText('Build Hash:')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should be able to render react-router components', () => {
|
||||
const RouteComponent = () => {
|
||||
return <div>{'this is the route component'}</div>;
|
||||
};
|
||||
const TestComponent = () => {
|
||||
return (
|
||||
<div>
|
||||
<Route
|
||||
path={''}
|
||||
component={RouteComponent}
|
||||
/>
|
||||
<Link to={'/another_page'}>{'Test Link'}</Link>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
renderWithFullContext(
|
||||
<TestComponent/>,
|
||||
);
|
||||
|
||||
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 <div>{`There are ${props.numProfiles} users loaded`}</div>;
|
||||
};
|
||||
const TestComponent = connect((state: GlobalState) => ({
|
||||
numProfiles: Object.keys(state.entities.users.profiles).length,
|
||||
}))(UnconnectedTestComponent);
|
||||
|
||||
renderWithFullContext(
|
||||
<TestComponent/>,
|
||||
);
|
||||
|
||||
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 <div>{`There are ${numProfiles} users loaded`}</div>;
|
||||
};
|
||||
|
||||
renderWithFullContext(
|
||||
<TestComponent/>,
|
||||
);
|
||||
|
||||
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 (
|
||||
<FormattedMessage
|
||||
id='about.title'
|
||||
defaultMessage='About {appTitle}'
|
||||
values={{
|
||||
appTitle: props.appTitle,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const {rerender} = renderWithFullContext(
|
||||
<TestComponent appTitle='Mattermost'/>,
|
||||
);
|
||||
|
||||
expect(screen.getByText('About Mattermost')).toBeInTheDocument();
|
||||
|
||||
rerender(
|
||||
<TestComponent appTitle='Mattermots'/>,
|
||||
);
|
||||
|
||||
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 <div>{`User1 is ${user1.username} and User2 is ${user2.username}!`}</div>;
|
||||
};
|
||||
|
||||
const {replaceStoreState} = renderWithFullContext(
|
||||
<TestComponent/>,
|
||||
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 <div>{`User1 is ${user1.username} and User2 is ${user2.username}!`}</div>;
|
||||
};
|
||||
|
||||
const {updateStoreState} = renderWithFullContext(
|
||||
<TestComponent/>,
|
||||
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();
|
||||
});
|
||||
});
|
||||
@@ -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(<IntlProvider locale={locale}>{component}</IntlProvider>);
|
||||
};
|
||||
|
||||
export const renderWithIntlAndStore = (component: React.ReactNode | React.ReactNodeArray, initialState: DeepPartial<GlobalState>, 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(
|
||||
<IntlProvider locale={locale}>
|
||||
<Provider store={store}>
|
||||
@@ -25,3 +35,66 @@ export const renderWithIntlAndStore = (component: React.ReactNode | React.ReactN
|
||||
</IntlProvider>,
|
||||
);
|
||||
};
|
||||
|
||||
export const renderWithFullContext = (component: React.ReactNode | React.ReactNodeArray, initialState: DeepPartial<GlobalState> = {}, 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 (
|
||||
<Provider store={renderState.store}>
|
||||
<Router history={renderState.history}>
|
||||
<IntlProvider
|
||||
locale={renderState.locale}
|
||||
>
|
||||
{renderState.component}
|
||||
</IntlProvider>
|
||||
</Router>
|
||||
</Provider>
|
||||
);
|
||||
}
|
||||
|
||||
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<GlobalState>) => {
|
||||
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<GlobalState>) => {
|
||||
const newTestState = mergeObjects(renderState.state, stateDiff);
|
||||
renderState.state = newTestState;
|
||||
renderState.store = mockStore(newTestState);
|
||||
|
||||
results.rerender(wrapComponent());
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
Ссылка в новой задаче
Block a user