MM-66627 (test): VH1-8 + EC Enzyme to RTL bulk migration (#35768)

* (test): vh1-8+EC enzyme to rtl bulk migration

* address comments
This commit is contained in:
sabril
2026-04-06 11:04:56 +08:00
committed by GitHub
parent 00b2594648
commit 2b45e43e05
63 changed files with 3818 additions and 3124 deletions
@@ -78,6 +78,7 @@ const intl = useIntl();
- Add/extend RTL tests alongside the component (`*.test.tsx`). See `../tests/react_testing_utils.tsx` for helpers.
- Prefer `userEvent` and accessible queries (`getByRole`) over implementation-specific selectors.
- Avoid snapshots; assert visible behavior instead.
- **Import Rule**: Never import directly from `@testing-library/react` or `react-dom/test-utils`. All testing utilities (`act`, `screen`, `fireEvent`, `waitFor`, `waitForElementToBeRemoved`, `userEvent`, `renderWithContext`, etc.) must be imported from `tests/react_testing_utils`, which re-exports everything from `@testing-library/react` alongside project-specific helpers.
## Icons
- **Menu items and components should use Compass icon components** from `@mattermost/compass-icons/components` (e.g., `<DockWindowIcon size={18}/>`), not raw `<i className="icon icon-..."/>` elements.
@@ -1,578 +1,118 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`components/TextBox should match snapshot with additional, optional props 1`] = `
<div
className="textarea-wrapper textarea-wrapper-preview textarea-wrapper-preview--disabled"
>
<div>
<div
className="form-control custom-textarea textbox-preview-area"
onBlur={[Function]}
onKeyDown={[Function]}
onKeyPress={[MockFunction]}
tabIndex={0}
class="textarea-wrapper textarea-wrapper-preview textarea-wrapper-preview--disabled"
>
<Connect(PostMarkdown)
channelId="channelId"
imageProps={
Object {
"hideUtilities": true,
}
}
message="some test text"
/>
<div
class="form-control custom-textarea textbox-preview-area"
tabindex="0"
>
<div
data-testid="post-markdown"
>
some test text
</div>
</div>
<textarea
class="form-control custom-textarea textbox-edit-area custom-textarea--emoji-picker bad-connection"
data-testid="suggestion-box"
disabled=""
id="someid"
placeholder="placeholder text"
readonly=""
style="visibility: hidden;"
>
some test text
</textarea>
</div>
<Connect(SuggestionBox)
className="form-control custom-textarea textbox-edit-area custom-textarea--emoji-picker bad-connection"
contextId="channelId"
disabled={true}
id="someid"
inputComponent={
Object {
"$$typeof": Symbol(react.memo),
"compare": null,
"type": Object {
"$$typeof": Symbol(react.forward_ref),
"propTypes": Object {
"className": [Function],
"defaultValue": [Function],
"disabled": [Function],
"id": [Function],
"onChange": [Function],
"onHeightChange": [Function],
"onInput": [Function],
"onWidthChange": [Function],
"placeholder": [Function],
"value": [Function],
},
"render": [Function],
},
}
}
listComponent={[Function]}
listPosition="top"
onBlur={[Function]}
onChange={[Function]}
onComposition={[MockFunction]}
onHeightChange={[MockFunction]}
onKeyDown={[Function]}
onKeyPress={[MockFunction]}
onKeyUp={[Function]}
onMouseUp={[Function]}
openWhenEmpty={true}
placeholder="placeholder text"
providers={
Array [
AtMentionProvider {
"addLastViewAtToProfiles": [Function],
"autocompleteGroups": Array [
Object {
"allow_reference": true,
"create_at": 1,
"delete_at": 0,
"description": "",
"display_name": "group_display_name",
"has_syncables": false,
"id": "gid1",
"member_count": 0,
"name": "group_name",
"remote_id": "",
"scheme_admin": false,
"source": "",
"update_at": 1,
},
Object {
"allow_reference": true,
"create_at": 1,
"delete_at": 0,
"description": "",
"display_name": "group_display_name",
"has_syncables": false,
"id": "gid2",
"member_count": 0,
"name": "group_name",
"remote_id": "",
"scheme_admin": false,
"source": "",
"update_at": 1,
},
],
"autocompleteUsersInChannel": [Function],
"channelId": "channelId",
"currentUserId": "currentUserId",
"data": null,
"defaultAgent": undefined,
"disableDispatches": false,
"forceDispatch": false,
"getProfilesInChannel": [Function],
"lastCompletedWord": "",
"lastPrefixWithNoResults": "",
"latestComplete": true,
"latestPrefix": "",
"priorityProfiles": undefined,
"requestStarted": false,
"searchAssociatedGroupsForReference": [Function],
"triggerCharacter": "@",
"useChannelMentions": true,
},
ChannelMentionProvider {
"autocompleteChannels": [MockFunction],
"delayChannelAutocomplete": false,
"disableDispatches": false,
"forceDispatch": false,
"lastCompletedWord": "",
"lastPrefixTrimmed": "",
"lastPrefixWithNoResults": "",
"latestComplete": true,
"latestPrefix": "",
"requestStarted": false,
"triggerCharacter": "~",
},
EmoticonProvider {
"disableDispatches": false,
"forceDispatch": false,
"latestComplete": true,
"latestPrefix": "",
"requestStarted": false,
"triggerCharacter": ":",
},
]
}
spellCheck="true"
style={
Object {
"visibility": "hidden",
}
}
value="some test text"
/>
</div>
`;
exports[`components/TextBox should match snapshot with required props 1`] = `
<div
className="textarea-wrapper"
>
<div>
<div
className="form-control custom-textarea textbox-preview-area"
onBlur={[Function]}
onKeyDown={[Function]}
onKeyPress={[MockFunction]}
tabIndex={0}
class="textarea-wrapper"
>
<Connect(PostMarkdown)
channelId="channelId"
imageProps={
Object {
"hideUtilities": true,
}
}
message="some test text"
/>
<div
class="form-control custom-textarea textbox-preview-area"
tabindex="0"
>
<div
data-testid="post-markdown"
>
some test text
</div>
</div>
<textarea
class="form-control custom-textarea textbox-edit-area"
data-testid="suggestion-box"
id="someid"
placeholder="placeholder text"
readonly=""
style="visibility: visible;"
>
some test text
</textarea>
</div>
<Connect(SuggestionBox)
className="form-control custom-textarea textbox-edit-area"
contextId="channelId"
id="someid"
inputComponent={
Object {
"$$typeof": Symbol(react.memo),
"compare": null,
"type": Object {
"$$typeof": Symbol(react.forward_ref),
"propTypes": Object {
"className": [Function],
"defaultValue": [Function],
"disabled": [Function],
"id": [Function],
"onChange": [Function],
"onHeightChange": [Function],
"onInput": [Function],
"onWidthChange": [Function],
"placeholder": [Function],
"value": [Function],
},
"render": [Function],
},
}
}
listComponent={[Function]}
onBlur={[Function]}
onChange={[Function]}
onKeyDown={[Function]}
onKeyPress={[MockFunction]}
onKeyUp={[Function]}
onMouseUp={[Function]}
placeholder="placeholder text"
providers={
Array [
AtMentionProvider {
"addLastViewAtToProfiles": [Function],
"autocompleteGroups": Array [
Object {
"allow_reference": true,
"create_at": 1,
"delete_at": 0,
"description": "",
"display_name": "group_display_name",
"has_syncables": false,
"id": "gid1",
"member_count": 0,
"name": "group_name",
"remote_id": "",
"scheme_admin": false,
"source": "",
"update_at": 1,
},
Object {
"allow_reference": true,
"create_at": 1,
"delete_at": 0,
"description": "",
"display_name": "group_display_name",
"has_syncables": false,
"id": "gid2",
"member_count": 0,
"name": "group_name",
"remote_id": "",
"scheme_admin": false,
"source": "",
"update_at": 1,
},
],
"autocompleteUsersInChannel": [Function],
"channelId": "channelId",
"currentUserId": "currentUserId",
"data": null,
"defaultAgent": undefined,
"disableDispatches": false,
"forceDispatch": false,
"getProfilesInChannel": [Function],
"lastCompletedWord": "",
"lastPrefixWithNoResults": "",
"latestComplete": true,
"latestPrefix": "",
"priorityProfiles": undefined,
"requestStarted": false,
"searchAssociatedGroupsForReference": [Function],
"triggerCharacter": "@",
"useChannelMentions": true,
},
ChannelMentionProvider {
"autocompleteChannels": [MockFunction],
"delayChannelAutocomplete": false,
"disableDispatches": false,
"forceDispatch": false,
"lastCompletedWord": "",
"lastPrefixTrimmed": "",
"lastPrefixWithNoResults": "",
"latestComplete": true,
"latestPrefix": "",
"requestStarted": false,
"triggerCharacter": "~",
},
EmoticonProvider {
"disableDispatches": false,
"forceDispatch": false,
"latestComplete": true,
"latestPrefix": "",
"requestStarted": false,
"triggerCharacter": ":",
},
]
}
spellCheck="true"
style={
Object {
"visibility": "visible",
}
}
value="some test text"
/>
</div>
`;
exports[`components/TextBox should throw error when new property is too long 1`] = `
<div
className="textarea-wrapper"
>
<div>
<div
className="form-control custom-textarea textbox-preview-area"
onBlur={[Function]}
onKeyDown={[Function]}
onKeyPress={[MockFunction]}
tabIndex={0}
class="textarea-wrapper"
>
<Connect(PostMarkdown)
channelId="channelId"
imageProps={
Object {
"hideUtilities": true,
}
}
message="some test text that exceeds char limit"
/>
<div
class="form-control custom-textarea textbox-preview-area"
tabindex="0"
>
<div
data-testid="post-markdown"
>
some test text that exceeds char limit
</div>
</div>
<textarea
class="form-control custom-textarea textbox-edit-area"
data-testid="suggestion-box"
id="someid"
placeholder="placeholder text"
readonly=""
style="visibility: visible;"
>
some test text that exceeds char limit
</textarea>
</div>
<Connect(SuggestionBox)
className="form-control custom-textarea textbox-edit-area"
contextId="channelId"
id="someid"
inputComponent={
Object {
"$$typeof": Symbol(react.memo),
"compare": null,
"type": Object {
"$$typeof": Symbol(react.forward_ref),
"propTypes": Object {
"className": [Function],
"defaultValue": [Function],
"disabled": [Function],
"id": [Function],
"onChange": [Function],
"onHeightChange": [Function],
"onInput": [Function],
"onWidthChange": [Function],
"placeholder": [Function],
"value": [Function],
},
"render": [Function],
},
}
}
listComponent={[Function]}
onBlur={[Function]}
onChange={[Function]}
onKeyDown={[Function]}
onKeyPress={[MockFunction]}
onKeyUp={[Function]}
onMouseUp={[Function]}
placeholder="placeholder text"
providers={
Array [
AtMentionProvider {
"addLastViewAtToProfiles": [Function],
"autocompleteGroups": Array [
Object {
"allow_reference": true,
"create_at": 1,
"delete_at": 0,
"description": "",
"display_name": "group_display_name",
"has_syncables": false,
"id": "gid1",
"member_count": 0,
"name": "group_name",
"remote_id": "",
"scheme_admin": false,
"source": "",
"update_at": 1,
},
Object {
"allow_reference": true,
"create_at": 1,
"delete_at": 0,
"description": "",
"display_name": "group_display_name",
"has_syncables": false,
"id": "gid2",
"member_count": 0,
"name": "group_name",
"remote_id": "",
"scheme_admin": false,
"source": "",
"update_at": 1,
},
],
"autocompleteUsersInChannel": [Function],
"channelId": "channelId",
"currentUserId": "currentUserId",
"data": null,
"defaultAgent": undefined,
"disableDispatches": false,
"forceDispatch": false,
"getProfilesInChannel": [Function],
"lastCompletedWord": "",
"lastPrefixWithNoResults": "",
"latestComplete": true,
"latestPrefix": "",
"priorityProfiles": undefined,
"requestStarted": false,
"searchAssociatedGroupsForReference": [Function],
"triggerCharacter": "@",
"useChannelMentions": true,
},
ChannelMentionProvider {
"autocompleteChannels": [MockFunction],
"delayChannelAutocomplete": false,
"disableDispatches": false,
"forceDispatch": false,
"lastCompletedWord": "",
"lastPrefixTrimmed": "",
"lastPrefixWithNoResults": "",
"latestComplete": true,
"latestPrefix": "",
"requestStarted": false,
"triggerCharacter": "~",
},
EmoticonProvider {
"disableDispatches": false,
"forceDispatch": false,
"latestComplete": true,
"latestPrefix": "",
"requestStarted": false,
"triggerCharacter": ":",
},
]
}
spellCheck="true"
style={
Object {
"visibility": "visible",
}
}
value="some test text that exceeds char limit"
/>
</div>
`;
exports[`components/TextBox should throw error when value is too long 1`] = `
<div
className="textarea-wrapper"
>
<div>
<div
className="form-control custom-textarea textbox-preview-area"
onBlur={[Function]}
onKeyDown={[Function]}
onKeyPress={[MockFunction]}
tabIndex={0}
class="textarea-wrapper"
>
<Connect(PostMarkdown)
channelId="channelId"
imageProps={
Object {
"hideUtilities": true,
}
}
message="some test text that exceeds char limit"
/>
<div
class="form-control custom-textarea textbox-preview-area"
tabindex="0"
>
<div
data-testid="post-markdown"
>
some test text that exceeds char limit
</div>
</div>
<textarea
class="form-control custom-textarea textbox-edit-area"
data-testid="suggestion-box"
id="someid"
placeholder="placeholder text"
readonly=""
style="visibility: visible;"
>
some test text that exceeds char limit
</textarea>
</div>
<Connect(SuggestionBox)
className="form-control custom-textarea textbox-edit-area"
contextId="channelId"
id="someid"
inputComponent={
Object {
"$$typeof": Symbol(react.memo),
"compare": null,
"type": Object {
"$$typeof": Symbol(react.forward_ref),
"propTypes": Object {
"className": [Function],
"defaultValue": [Function],
"disabled": [Function],
"id": [Function],
"onChange": [Function],
"onHeightChange": [Function],
"onInput": [Function],
"onWidthChange": [Function],
"placeholder": [Function],
"value": [Function],
},
"render": [Function],
},
}
}
listComponent={[Function]}
onBlur={[Function]}
onChange={[Function]}
onKeyDown={[Function]}
onKeyPress={[MockFunction]}
onKeyUp={[Function]}
onMouseUp={[Function]}
placeholder="placeholder text"
providers={
Array [
AtMentionProvider {
"addLastViewAtToProfiles": [Function],
"autocompleteGroups": Array [
Object {
"allow_reference": true,
"create_at": 1,
"delete_at": 0,
"description": "",
"display_name": "group_display_name",
"has_syncables": false,
"id": "gid1",
"member_count": 0,
"name": "group_name",
"remote_id": "",
"scheme_admin": false,
"source": "",
"update_at": 1,
},
Object {
"allow_reference": true,
"create_at": 1,
"delete_at": 0,
"description": "",
"display_name": "group_display_name",
"has_syncables": false,
"id": "gid2",
"member_count": 0,
"name": "group_name",
"remote_id": "",
"scheme_admin": false,
"source": "",
"update_at": 1,
},
],
"autocompleteUsersInChannel": [Function],
"channelId": "channelId",
"currentUserId": "currentUserId",
"data": null,
"defaultAgent": undefined,
"disableDispatches": false,
"forceDispatch": false,
"getProfilesInChannel": [Function],
"lastCompletedWord": "",
"lastPrefixWithNoResults": "",
"latestComplete": true,
"latestPrefix": "",
"priorityProfiles": undefined,
"requestStarted": false,
"searchAssociatedGroupsForReference": [Function],
"triggerCharacter": "@",
"useChannelMentions": true,
},
ChannelMentionProvider {
"autocompleteChannels": [MockFunction],
"delayChannelAutocomplete": false,
"disableDispatches": false,
"forceDispatch": false,
"lastCompletedWord": "",
"lastPrefixTrimmed": "",
"lastPrefixWithNoResults": "",
"latestComplete": true,
"latestPrefix": "",
"requestStarted": false,
"triggerCharacter": "~",
},
EmoticonProvider {
"disableDispatches": false,
"forceDispatch": false,
"latestComplete": true,
"latestPrefix": "",
"requestStarted": false,
"triggerCharacter": ":",
},
]
}
spellCheck="true"
style={
Object {
"visibility": "visible",
}
}
value="some test text that exceeds char limit"
/>
</div>
`;
@@ -1,128 +1,97 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`components/AddUserToChannelModal should match snapshot 1`] = `
<Modal
animation={true}
aria-labelledby="addChannelModalLabel"
autoFocus={true}
backdrop={true}
bsClass="modal"
dialogClassName="a11y__modal modal--overflow"
dialogComponentClass={[Function]}
enforceFocus={true}
keyboard={true}
manager={
ModalManager {
"add": [Function],
"containers": Array [],
"data": Array [],
"handleContainerOverflow": true,
"hideSiblingNodes": true,
"isTopModal": [Function],
"modals": Array [],
"remove": [Function],
}
}
onExited={[Function]}
onHide={[Function]}
renderBackdrop={[Function]}
restoreFocus={true}
role="none"
show={true}
<body
class="modal-open"
style="padding-right: 0px;"
>
<ModalHeader
bsClass="modal-header"
closeButton={true}
closeLabel="Close"
<div
aria-hidden="true"
/>
<div
role="none"
>
<ModalTitle
bsClass="modal-title"
componentClass="h1"
id="addChannelModalLabel"
>
<MemoizedFormattedMessage
defaultMessage="Add {name} to a Channel"
id="add_user_to_channel_modal.title"
values={
Object {
"name": "Fake Person",
}
}
/>
</ModalTitle>
</ModalHeader>
<form
onSubmit={[Function]}
role="form"
>
<ModalBody
bsClass="modal-body"
componentClass="div"
<div
class="fade modal-backdrop in"
/>
<div
aria-labelledby="addChannelModalLabel"
class="fade in modal"
role="dialog"
style="display: block; padding-right: 0px;"
tabindex="-1"
>
<div
className="modal__hint"
class="a11y__modal modal--overflow modal-dialog"
>
<MemoizedFormattedMessage
defaultMessage="Type to find a channel. Use ↑↓ to browse, ↵ to select, ESC to dismiss."
id="add_user_to_channel_modal.help"
/>
<div
class="modal-content"
role="document"
>
<div
class="modal-header"
>
<button
aria-label="Close"
class="close"
type="button"
>
<span
aria-hidden="true"
>
×
</span>
<span
class="sr-only"
>
Close
</span>
</button>
<h1
class="modal-title"
id="addChannelModalLabel"
>
Add Fake Person to a Channel
</h1>
</div>
<form
role="form"
>
<div
class="modal-body"
>
<div
class="modal__hint"
>
Type to find a channel. Use ↑↓ to browse, ↵ to select, ESC to dismiss.
</div>
<div
class="pos-relative"
/>
<div />
</div>
<div
class="modal-footer"
>
<button
class="btn btn-tertiary"
type="button"
>
Cancel
</button>
<button
class="btn btn-primary"
disabled=""
id="add-user-to-channel-modal__add-button"
type="button"
>
Add
</button>
</div>
</form>
</div>
</div>
<div
className="pos-relative"
>
<Connect(SuggestionBox)
className="form-control focused"
completeOnTab={false}
listComponent={[Function]}
listPosition="bottom"
maxLength="64"
onChange={[Function]}
onItemSelected={[Function]}
openWhenEmpty={false}
providers={
Array [
SearchChannelWithPermissionsProvider {
"autocompleteChannelsForSearch": [MockFunction],
"disableDispatches": false,
"forceDispatch": false,
"latestComplete": true,
"latestPrefix": "",
"requestStarted": false,
},
]
}
value=""
/>
</div>
<div />
</ModalBody>
<ModalFooter
bsClass="modal-footer"
componentClass="div"
>
<button
className="btn btn-tertiary"
onClick={[Function]}
type="button"
>
<MemoizedFormattedMessage
defaultMessage="Cancel"
id="add_user_to_channel_modal.cancel"
/>
</button>
<button
className="btn btn-primary"
disabled={true}
id="add-user-to-channel-modal__add-button"
onClick={[Function]}
type="button"
>
<MemoizedFormattedMessage
defaultMessage="Add"
id="add_user_to_channel_modal.add"
/>
</button>
</ModalFooter>
</form>
</Modal>
</div>
</div>
</body>
`;
@@ -1,13 +1,35 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {shallow} from 'enzyme';
import React from 'react';
import AddUserToChannelModal from 'components/add_user_to_channel_modal/add_user_to_channel_modal';
import {act, renderWithContext, userEvent, waitFor} from 'tests/react_testing_utils';
import {TestHelper} from 'utils/test_helper';
let mockOnItemSelected: ((selection: any) => void) | undefined;
jest.mock('components/suggestion/suggestion_box', () => {
const react = require('react');
return {
__esModule: true,
// eslint-disable-next-line @typescript-eslint/no-unused-vars -- forwardRef arity; ref unused in mock
default: react.forwardRef((props: any, _ref: React.Ref<unknown>) => {
mockOnItemSelected = props.onItemSelected;
return null;
}),
};
});
jest.mock('components/suggestion/search_channel_with_permissions_provider', () =>
jest.fn().mockImplementation(() => ({disableDispatches: false})),
);
function getAddButton() {
return document.querySelector('#add-user-to-channel-modal__add-button') as HTMLButtonElement;
}
describe('components/AddUserToChannelModal', () => {
const baseProps = {
channelMembers: {},
@@ -24,52 +46,101 @@ describe('components/AddUserToChannelModal', () => {
},
};
beforeEach(() => {
mockOnItemSelected = undefined;
});
async function selectChannel(channelId = 'someChannelId', displayName = 'channelName') {
await act(async () => {
mockOnItemSelected!({channel: TestHelper.getChannelMock({id: channelId, display_name: displayName})});
});
}
it('should match snapshot', () => {
const wrapper = shallow(
const {baseElement} = renderWithContext(
<AddUserToChannelModal {...baseProps}/>,
);
expect(wrapper.find('#add-user-to-channel-modal__add-button').props().disabled).toBe(true);
expect(wrapper.find('#add-user-to-channel-modal__user-is-member').exists()).toBe(false);
expect(wrapper.find('#add-user-to-channel-modal__invite-error').exists()).toBe(false);
expect(wrapper).toMatchSnapshot();
expect(getAddButton().disabled).toBe(true);
expect(document.querySelector('#add-user-to-channel-modal__user-is-member')).toBeNull();
expect(document.querySelector('#add-user-to-channel-modal__invite-error')).toBeNull();
expect(baseElement).toMatchSnapshot();
});
it('should enable the add button when a channel is selected', () => {
const wrapper = shallow(
it('should enable the add button when a channel is selected', async () => {
renderWithContext(
<AddUserToChannelModal {...baseProps}/>,
);
wrapper.setState({selectedChannelId: 'someChannelId'});
expect(wrapper.find('#add-user-to-channel-modal__add-button').props().disabled).toBe(false);
expect(wrapper.find('#add-user-to-channel-modal__invite-error').exists()).toBe(false);
await selectChannel();
await waitFor(() => {
expect(getAddButton().disabled).toBe(false);
});
expect(document.querySelector('#add-user-to-channel-modal__invite-error')).toBeNull();
});
it('should show invite error when an error message is captured', () => {
const wrapper = shallow(
<AddUserToChannelModal {...baseProps}/>,
it('should show invite error when an error message is captured', async () => {
const props = {
...baseProps,
actions: {
...baseProps.actions,
addChannelMember: jest.fn().mockResolvedValue({error: {message: 'some error'}}),
},
};
renderWithContext(
<AddUserToChannelModal {...props}/>,
);
wrapper.setState({submitError: 'some error'});
expect(wrapper.find('#add-user-to-channel-modal__add-button').props().disabled).toBe(true);
expect(wrapper.find('#add-user-to-channel-modal__invite-error').exists()).toBe(true);
});
await selectChannel();
it('should disable add button when membership is being checked', () => {
const wrapper = shallow(
<AddUserToChannelModal {...baseProps}/>,
);
wrapper.setState({
selectedChannelId: 'someChannelId',
checkingForMembership: true,
await waitFor(() => {
expect(getAddButton().disabled).toBe(false);
});
expect(wrapper.find('#add-user-to-channel-modal__add-button').props().disabled).toBe(true);
await userEvent.click(getAddButton());
await waitFor(() => {
expect(document.querySelector('#add-user-to-channel-modal__invite-error')).not.toBeNull();
});
});
it('should display error message if user is a member of the selected channel', () => {
const props = {...baseProps,
it('should disable add button when membership is being checked', async () => {
let resolveGetChannelMember: (value: any) => void;
const getChannelMemberPromise = new Promise<Record<string, never>>((resolve) => {
resolveGetChannelMember = resolve;
});
const props = {
...baseProps,
actions: {
...baseProps.actions,
getChannelMember: jest.fn(() => getChannelMemberPromise),
},
};
renderWithContext(
<AddUserToChannelModal {...props}/>,
);
// Select a channel - membership check starts
act(() => {
mockOnItemSelected!({channel: TestHelper.getChannelMock({id: 'someChannelId', display_name: 'channelName'})});
});
// While checking membership, button should be disabled
expect(getAddButton().disabled).toBe(true);
// Resolve the membership check
await act(async () => {
resolveGetChannelMember!({} as Record<string, never>);
});
});
it('should display error message if user is a member of the selected channel', async () => {
const props = {
...baseProps,
channelMembers: {
someChannelId: {
someUserId: TestHelper.getChannelMembershipMock({}),
@@ -77,37 +148,63 @@ describe('components/AddUserToChannelModal', () => {
},
};
const wrapper = shallow(
renderWithContext(
<AddUserToChannelModal {...props}/>,
);
wrapper.setState({selectedChannelId: 'someChannelId'});
expect(wrapper.find('#add-user-to-channel-modal__add-button').props().disabled).toBe(true);
expect(wrapper.find('#add-user-to-channel-modal__user-is-member').exists()).toBe(true);
await selectChannel();
await waitFor(() => {
expect(getAddButton().disabled).toBe(true);
expect(document.querySelector('#add-user-to-channel-modal__user-is-member')).not.toBeNull();
});
});
it('should disable the add button when saving', () => {
const wrapper = shallow(
<AddUserToChannelModal {...baseProps}/>,
it('should disable the add button when saving', async () => {
let resolveAddChannelMember: (value: any) => void;
const addChannelMemberPromise = new Promise<Record<string, never>>((resolve) => {
resolveAddChannelMember = resolve;
});
const props = {
...baseProps,
actions: {
...baseProps.actions,
addChannelMember: jest.fn(() => addChannelMemberPromise),
},
};
renderWithContext(
<AddUserToChannelModal {...props}/>,
);
wrapper.setState({
selectedChannelId: 'someChannelId',
saving: true,
await selectChannel();
await waitFor(() => {
expect(getAddButton().disabled).toBe(false);
});
await userEvent.click(getAddButton());
// While saving, button should be disabled
expect(getAddButton().disabled).toBe(true);
// Clean up
await act(async () => {
resolveAddChannelMember!({} as Record<string, never>);
});
expect(wrapper.find('#add-user-to-channel-modal__add-button').props().disabled).toBe(true);
});
describe('didSelectChannel', () => {
it('should fetch the selected user\'s membership for the selected channel', () => {
it('should fetch the selected user\'s membership for the selected channel', async () => {
const props = {...baseProps};
const wrapper = shallow<AddUserToChannelModal>(
renderWithContext(
<AddUserToChannelModal {...props}/>,
);
const selection = {channel: TestHelper.getChannelMock({id: 'someChannelId', display_name: 'channelName'})};
wrapper.instance().didSelectChannel(selection);
await selectChannel();
expect(props.actions.getChannelMember).toHaveBeenCalledWith('someChannelId', 'someUserId');
});
@@ -123,45 +220,48 @@ describe('components/AddUserToChannelModal', () => {
},
};
const wrapper = shallow<AddUserToChannelModal>(
renderWithContext(
<AddUserToChannelModal {...props}/>,
);
expect(wrapper.state().text).toEqual('');
expect(wrapper.state().checkingForMembership).toEqual(false);
expect(wrapper.state().selectedChannelId).toEqual(null);
expect(wrapper.state().submitError).toEqual('');
// Initially, add button should be disabled (no channel selected)
expect(getAddButton().disabled).toBe(true);
const selection = {channel: TestHelper.getChannelMock({id: 'someChannelId', display_name: 'channelName'})};
wrapper.setState({submitError: 'some pre-existing error'});
// Select a channel
act(() => {
mockOnItemSelected!({channel: TestHelper.getChannelMock({id: 'someChannelId', display_name: 'channelName'})});
});
wrapper.instance().didSelectChannel(selection);
expect(wrapper.state().text).toEqual('channelName');
expect(wrapper.state().checkingForMembership).toEqual(true);
expect(wrapper.state().selectedChannelId).toEqual('someChannelId');
expect(wrapper.state().submitError).toEqual('');
// During membership check, button should be disabled
expect(getAddButton().disabled).toBe(true);
await promise;
expect(wrapper.state().checkingForMembership).toEqual(false);
// Wait for membership check
await act(async () => {
await promise;
});
// After membership check, button should be enabled
await waitFor(() => {
expect(getAddButton().disabled).toBe(false);
});
});
});
describe('handleSubmit', () => {
it('should do nothing if no channel is selected', () => {
it('should do nothing if no channel is selected', async () => {
const props = {...baseProps};
const wrapper = shallow<AddUserToChannelModal>(
renderWithContext(
<AddUserToChannelModal {...props}/>,
);
const event: any = {stopPropagation: jest.fn(), preventDefault: jest.fn()};
wrapper.instance().handleSubmit(event);
expect(wrapper.state().saving).toBe(false);
await userEvent.click(getAddButton());
expect(props.actions.addChannelMember).not.toHaveBeenCalled();
});
it('should do nothing if user is a member of the selected channel', () => {
const props = {...baseProps,
it('should do nothing if user is a member of the selected channel', async () => {
const props = {
...baseProps,
channelMembers: {
someChannelId: {
someUserId: TestHelper.getChannelMembershipMock({}),
@@ -169,32 +269,35 @@ describe('components/AddUserToChannelModal', () => {
},
};
const wrapper = shallow<AddUserToChannelModal>(
renderWithContext(
<AddUserToChannelModal {...props}/>,
);
wrapper.setState({selectedChannelId: 'someChannelId'});
const event: any = {stopPropagation: jest.fn(), preventDefault: jest.fn()};
wrapper.instance().handleSubmit(event);
expect(wrapper.state().saving).toBe(false);
await selectChannel();
await userEvent.click(getAddButton());
expect(props.actions.addChannelMember).not.toHaveBeenCalled();
});
it('should submit if user is not a member of the selected channel', () => {
const props = {...baseProps,
it('should submit if user is not a member of the selected channel', async () => {
const props = {
...baseProps,
channelMembers: {
someChannelId: {},
},
};
const wrapper = shallow<AddUserToChannelModal>(
renderWithContext(
<AddUserToChannelModal {...props}/>,
);
wrapper.setState({selectedChannelId: 'someChannelId'});
const event: any = {stopPropagation: jest.fn(), preventDefault: jest.fn()};
wrapper.instance().handleSubmit(event);
expect(wrapper.state().saving).toBe(true);
await selectChannel();
await waitFor(() => {
expect(getAddButton().disabled).toBe(false);
});
await userEvent.click(getAddButton());
expect(props.actions.addChannelMember).toHaveBeenCalled();
});
@@ -204,51 +307,57 @@ describe('components/AddUserToChannelModal', () => {
...baseProps,
actions: {
...baseProps.actions,
addChannelMember: () => promise,
addChannelMember: jest.fn(() => promise),
},
};
const wrapper = shallow<AddUserToChannelModal>(
renderWithContext(
<AddUserToChannelModal {...props}/>,
);
expect(wrapper.state().show).toBe(true);
expect(wrapper.state().saving).toBe(false);
wrapper.setState({selectedChannelId: 'someChannelId'});
await selectChannel();
const event: any = {stopPropagation: jest.fn(), preventDefault: jest.fn()};
wrapper.instance().handleSubmit(event);
expect(wrapper.state().show).toBe(true);
expect(wrapper.state().saving).toBe(true);
await waitFor(() => {
expect(getAddButton().disabled).toBe(false);
});
await promise;
expect(wrapper.state().submitError).toEqual('');
expect(wrapper.state().show).toBe(false);
await userEvent.click(getAddButton());
// After successful save, modal should close (no error)
await waitFor(() => {
expect(document.querySelector('#add-user-to-channel-modal__invite-error')).toBeNull();
});
});
test('should match state when save fails', async () => {
const promise = Promise.resolve({error: new Error('some error')});
const errorMessage = 'some error';
const promise = Promise.resolve({error: new Error(errorMessage)});
const props = {
...baseProps,
actions: {
...baseProps.actions,
addChannelMember: () => promise,
addChannelMember: jest.fn(() => promise),
},
};
const wrapper = shallow<AddUserToChannelModal>(
renderWithContext(
<AddUserToChannelModal {...props}/>,
);
expect(wrapper.state().show).toBe(true);
wrapper.setState({selectedChannelId: 'someChannelId'});
await selectChannel();
const event: any = {stopPropagation: jest.fn(), preventDefault: jest.fn()};
wrapper.instance().handleSubmit(event);
await waitFor(() => {
expect(getAddButton().disabled).toBe(false);
});
await promise;
expect(wrapper.state().submitError).toEqual('some error');
expect(wrapper.state().show).toBe(true);
await userEvent.click(getAddButton());
// After failed save, error should be shown
await waitFor(() => {
const inviteError = document.querySelector('#add-user-to-channel-modal__invite-error');
expect(inviteError).not.toBeNull();
expect(inviteError).toHaveTextContent(errorMessage);
});
});
});
});
@@ -7,9 +7,16 @@ import {General} from 'mattermost-redux/constants';
import ManageTeamsModal from 'components/admin_console/manage_teams_modal/manage_teams_modal';
import {renderWithContext, screen, waitFor} from 'tests/react_testing_utils';
import {renderWithContext, runPostRenderAct, screen, waitFor} from 'tests/react_testing_utils';
import {TestHelper} from 'utils/test_helper';
/**
* `runPostRenderAct` rounds (each `await Promise.resolve()`): (1) passive effects run so `useEffect`
* starts `loadTeamsAndTeamMembers`; (2) mocked `actions.getTeamMembersForUser` resolves →
* `getTeamMembers` → `setTeamMembers`; (3) mocked `actions.getTeamsForUser` resolves → `setTeams`.
*/
const MANAGE_TEAMS_MODAL_ASYNC_ROUNDS = 3;
describe('ManageTeamsModal', () => {
const baseProps = {
locale: General.DEFAULT_LOCALE,
@@ -35,31 +42,35 @@ describe('ManageTeamsModal', () => {
});
test('should match snapshot init', async () => {
const {baseElement} = renderWithContext(<ManageTeamsModal {...baseProps}/>);
const {baseElement} = renderWithContext(
<ManageTeamsModal {...baseProps}/>,
);
await waitFor(() => {
expect(baseProps.actions.getTeamMembersForUser).toHaveBeenCalledTimes(1);
expect(baseProps.actions.getTeamsForUser).toHaveBeenCalledTimes(1);
});
await runPostRenderAct(MANAGE_TEAMS_MODAL_ASYNC_ROUNDS);
expect(baseProps.actions.getTeamMembersForUser).toHaveBeenCalledTimes(1);
expect(baseProps.actions.getTeamsForUser).toHaveBeenCalledTimes(1);
expect(screen.getByText('Manage Teams')).toBeInTheDocument();
expect(screen.getByText('@currentUsername')).toBeInTheDocument();
expect(screen.getByText('currentUser@test.com')).toBeInTheDocument();
expect(baseElement).toMatchSnapshot();
});
test('should call api calls on mount', async () => {
renderWithContext(<ManageTeamsModal {...baseProps}/>);
renderWithContext(
<ManageTeamsModal {...baseProps}/>,
);
await runPostRenderAct(MANAGE_TEAMS_MODAL_ASYNC_ROUNDS);
await waitFor(() => {
expect(baseProps.actions.getTeamMembersForUser).toHaveBeenCalledTimes(1);
expect(baseProps.actions.getTeamMembersForUser).toHaveBeenCalledWith(baseProps.user.id);
expect(baseProps.actions.getTeamsForUser).toHaveBeenCalledTimes(1);
expect(baseProps.actions.getTeamsForUser).toHaveBeenCalledWith(baseProps.user.id);
});
expect(baseProps.actions.getTeamMembersForUser).toHaveBeenCalledTimes(1);
expect(baseProps.actions.getTeamMembersForUser).toHaveBeenCalledWith(baseProps.user.id);
expect(baseProps.actions.getTeamsForUser).toHaveBeenCalledTimes(1);
expect(baseProps.actions.getTeamsForUser).toHaveBeenCalledWith(baseProps.user.id);
});
test('should save data in state from api calls', async () => {
test('should show team from api data after load', async () => {
const mockTeamData = TestHelper.getTeamMock({
id: '123test',
name: 'testTeam',
@@ -79,10 +90,15 @@ describe('ManageTeamsModal', () => {
},
};
renderWithContext(<ManageTeamsModal {...props}/>);
renderWithContext(
<ManageTeamsModal {...props}/>,
);
await waitFor(() => {
expect(screen.getByText(mockTeamData.display_name)).toBeInTheDocument();
});
// Non-system-admin users get ManageTeamsDropdown; default role label for plain membership.
expect(screen.getByText('Team Member')).toBeInTheDocument();
});
});
@@ -1,7 +1,6 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {act} from '@testing-library/react';
import React from 'react';
import type {AppField} from '@mattermost/types/apps';
@@ -218,69 +217,57 @@ describe('components/apps_form/apps_form_field/AppsFormField', () => {
expect(reactSelect).toBeInTheDocument();
});
it('should render user select field', async () => {
it('should render user select field', () => {
const userField: AppField = {
name: 'user-field',
type: AppFieldTypes.USER,
label: 'User Field',
};
let container: HTMLElement;
await act(async () => {
const result = renderWithContext(
<AppsFormField
{...baseProps}
field={userField}
/>,
);
container = result.container;
});
const {container} = renderWithContext(
<AppsFormField
{...baseProps}
field={userField}
/>,
);
const reactSelect = container!.querySelector('.react-select');
const reactSelect = container.querySelector('.react-select');
expect(reactSelect).toBeInTheDocument();
});
it('should render channel select field', async () => {
it('should render channel select field', () => {
const channelField: AppField = {
name: 'channel-field',
type: AppFieldTypes.CHANNEL,
label: 'Channel Field',
};
let container: HTMLElement;
await act(async () => {
const result = renderWithContext(
<AppsFormField
{...baseProps}
field={channelField}
/>,
);
container = result.container;
});
const {container} = renderWithContext(
<AppsFormField
{...baseProps}
field={channelField}
/>,
);
const reactSelect = container!.querySelector('.react-select');
const reactSelect = container.querySelector('.react-select');
expect(reactSelect).toBeInTheDocument();
});
it('should render dynamic select field', async () => {
it('should render dynamic select field', () => {
const dynamicSelectField: AppField = {
name: 'dynamic-select-field',
type: AppFieldTypes.DYNAMIC_SELECT,
label: 'Dynamic Select Field',
};
let container: HTMLElement;
await act(async () => {
const result = renderWithContext(
<AppsFormField
{...baseProps}
field={dynamicSelectField}
/>,
);
container = result.container;
});
const {container} = renderWithContext(
<AppsFormField
{...baseProps}
field={dynamicSelectField}
/>,
);
const reactSelect = container!.querySelector('.react-select');
const reactSelect = container.querySelector('.react-select');
expect(reactSelect).toBeInTheDocument();
});
@@ -1,40 +1,51 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`AudioVideoPreview should match snapshot without children 1`] = `
<video
controls={true}
data-setup="{}"
height={480}
key="file_id"
width={640}
>
<source
src="/api/v4/files/file_id"
/>
</video>
<div>
<video
controls=""
data-setup="{}"
height="480"
width="640"
>
<source
src="/api/v4/files/file_id"
/>
</video>
</div>
`;
exports[`AudioVideoPreview should match snapshot, cannot play 1`] = `
<Connect(Component)
fileInfo={
Object {
"archived": false,
"channel_id": "channel_id",
"clientId": "client_id",
"create_at": 1,
"delete_at": 1,
"extension": "mov",
"has_preview_image": true,
"height": 200,
"id": "file_id",
"mime_type": "mime_type",
"name": "name",
"size": 1,
"update_at": 1,
"user_id": "user_id",
"width": 350,
}
}
fileUrl="/api/v4/files/file_id"
/>
<div>
<div
class="file-details__container"
>
<a
class="file-details__preview"
href="/api/v4/files/file_id"
>
<span
class="file-details__preview-helper"
/>
<img
alt="file preview"
src=""
/>
</a>
<div
class="file-details"
>
<div
class="file-details__name"
>
name
</div>
<div
class="file-details__info"
>
File type MOV, Size 1B
</div>
</div>
</div>
</div>
`;
@@ -1,9 +1,9 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {shallow} from 'enzyme';
import React from 'react';
import {fireEvent, renderWithContext} from 'tests/react_testing_utils';
import {TestHelper} from 'utils/test_helper';
import AudioVideoPreview from './audio_video_preview';
@@ -19,17 +19,21 @@ describe('AudioVideoPreview', () => {
};
test('should match snapshot without children', () => {
const wrapper = shallow(
const {container} = renderWithContext(
<AudioVideoPreview {...baseProps}/>,
);
expect(wrapper).toMatchSnapshot();
expect(container).toMatchSnapshot();
});
test('should match snapshot, cannot play', () => {
const wrapper = shallow(
const {container} = renderWithContext(
<AudioVideoPreview {...baseProps}/>,
);
wrapper.setState({canPlay: false});
expect(wrapper).toMatchSnapshot();
// Trigger the error handler to set canPlay to false; fireEvent.error dispatches the <source> error (media load failure).
const source = container.querySelector('source')!;
fireEvent.error(source);
expect(container).toMatchSnapshot();
});
});
@@ -624,9 +624,7 @@ describe('components/channel_invite_modal', () => {
profilesFromRecentDMs: [users[1]],
};
await act(async () => {
renderWithContext(<ChannelInviteModal {...props}/>);
});
renderWithContext(<ChannelInviteModal {...props}/>);
// Wait for the API call to complete and state to update
await act(async () => {
@@ -657,9 +655,7 @@ describe('components/channel_invite_modal', () => {
profilesFromRecentDMs: [users[1]],
};
await act(async () => {
renderWithContext(<ChannelInviteModal {...props}/>);
});
renderWithContext(<ChannelInviteModal {...props}/>);
const input = screen.getByRole('combobox', {name: /search for people/i});
await userEvent.type(input, 'user');
@@ -687,11 +683,9 @@ describe('components/channel_invite_modal', () => {
};
// Render the component
await act(async () => {
renderWithContext(
<ChannelInviteModal {...props}/>,
);
});
renderWithContext(
<ChannelInviteModal {...props}/>,
);
// Reset the mock after component mount to ignore initial data loading
getProfilesNotInChannelMock.mockClear();
@@ -770,9 +764,7 @@ describe('components/channel_invite_modal', () => {
profilesNotInCurrentChannel: [users[0]],
};
await act(async () => {
renderWithContext(<ChannelInviteModal {...props}/>);
});
renderWithContext(<ChannelInviteModal {...props}/>);
const input = screen.getByRole('combobox', {name: /search for people/i});
await userEvent.type(input, '@');
@@ -819,9 +811,7 @@ describe('components/channel_invite_modal', () => {
profilesNotInCurrentChannel: [users[0]],
};
await act(async () => {
renderWithContext(<ChannelInviteModal {...props}/>);
});
renderWithContext(<ChannelInviteModal {...props}/>);
// Wait for the API call to complete and state to update
await act(async () => {
@@ -853,9 +843,7 @@ describe('components/channel_invite_modal', () => {
channel: {...channel, policy_enforced: true},
};
await act(async () => {
renderWithContext(<ChannelInviteModal {...props}/>);
});
renderWithContext(<ChannelInviteModal {...props}/>);
// Wait for the API call to complete
await act(async () => {
@@ -886,9 +874,7 @@ describe('components/channel_invite_modal', () => {
includeUsers: {[users[1].id]: users[1]}, // Contaminated data
};
await act(async () => {
renderWithContext(<ChannelInviteModal {...props}/>);
});
renderWithContext(<ChannelInviteModal {...props}/>);
// Wait for the API call to complete and state to update
await act(async () => {
@@ -1,9 +1,6 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {shallow} from 'enzyme';
import React from 'react';
import CenterChannel from './center_channel';
import type {OwnProps} from './index';
@@ -29,21 +26,27 @@ describe('components/channel_layout/CenterChannel', () => {
},
};
test('should call update returnTo on props change', () => {
const wrapper = shallow(<CenterChannel {...props}/>);
let state = {returnTo: '', lastReturnTo: ''};
expect(wrapper.state('returnTo')).toBe('');
// Simulate initial render with pathname '/some'
let derived = CenterChannel.getDerivedStateFromProps(props as any, state);
state = {...state, ...derived};
expect(state.returnTo).toBe('');
wrapper.setProps({
location: {
pathname: '/pl/path',
},
});
expect(wrapper.state('returnTo')).toBe('/some');
wrapper.setProps({
location: {
pathname: '/pl/path1',
},
});
expect(wrapper.state('returnTo')).toBe('/pl/path');
// Simulate props change to '/pl/path'
derived = CenterChannel.getDerivedStateFromProps(
{...props, location: {pathname: '/pl/path'}} as any,
state,
);
state = {...state, ...derived};
expect(state.returnTo).toBe('/some');
// Simulate props change to '/pl/path1'
derived = CenterChannel.getDerivedStateFromProps(
{...props, location: {pathname: '/pl/path1'}} as any,
state,
);
state = {...state, ...derived};
expect(state.returnTo).toBe('/pl/path');
});
});
@@ -1,7 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {shallow} from 'enzyme';
import {render} from '@testing-library/react';
import type {History} from 'history';
import React from 'react';
@@ -9,11 +9,20 @@ import {getHistory} from 'utils/browser_history';
import ChannelIdentifierRouter from './channel_identifier_router';
jest.mock('components/channel_view/index', () => ({
__esModule: true,
default: () => null,
}));
const mockReplace = jest.fn();
jest.mock('utils/browser_history', () => ({
getHistory: () => ({replace: mockReplace}),
}));
jest.useFakeTimers({legacyFakeTimers: true});
describe('components/channel_layout/CenterChannel', () => {
describe('components/channel_layout/ChannelIdentifierRouter', () => {
const baseProps = {
match: {
isExact: false,
params: {
@@ -24,20 +33,23 @@ describe('components/channel_layout/CenterChannel', () => {
path: '/team/channel/identifier',
url: '/team/channel/identifier',
},
actions: {
onChannelByIdentifierEnter: jest.fn(),
},
history: [] as unknown as History,
};
beforeEach(() => {
jest.clearAllMocks();
});
test('should call onChannelByIdentifierEnter on props change', () => {
const wrapper = shallow(<ChannelIdentifierRouter {...baseProps}/>);
const instance = wrapper.instance();
const {rerender} = render(<ChannelIdentifierRouter {...baseProps}/>);
expect(baseProps.actions.onChannelByIdentifierEnter).toHaveBeenCalledTimes(1);
expect(baseProps.actions.onChannelByIdentifierEnter).toHaveBeenLastCalledWith(baseProps);
const props2 = {
...baseProps,
match: {
isExact: false,
params: {
@@ -49,18 +61,11 @@ describe('components/channel_layout/CenterChannel', () => {
url: '/team2/channel/identifier2',
},
};
wrapper.setProps(props2);
rerender(<ChannelIdentifierRouter {...props2}/>);
// expect(propsTest.match).toEqual(props2.match);
//Should clear the timeout if url is changed
expect(clearTimeout).toHaveBeenCalledWith((instance as any).replaceUrlTimeout);
expect(clearTimeout).toHaveBeenCalled();
expect(baseProps.actions.onChannelByIdentifierEnter).toHaveBeenCalledTimes(2);
expect(baseProps.actions.onChannelByIdentifierEnter).toHaveBeenLastCalledWith({
...baseProps,
match: props2.match,
actions: baseProps.actions,
});
expect(baseProps.actions.onChannelByIdentifierEnter).toHaveBeenLastCalledWith(props2);
});
test('should call browserHistory.replace if it is permalink after timer', () => {
@@ -78,7 +83,7 @@ describe('components/channel_layout/CenterChannel', () => {
url: '/team/channel/identifier/abcd',
},
};
shallow(<ChannelIdentifierRouter {...props}/>);
render(<ChannelIdentifierRouter {...props}/>);
jest.runOnlyPendingTimers();
expect(getHistory().replace).toHaveBeenLastCalledWith('/team/channel/identifier');
});
@@ -99,8 +104,8 @@ describe('components/channel_layout/CenterChannel', () => {
},
};
const wrapper = shallow(<ChannelIdentifierRouter {...baseProps}/>);
wrapper.setProps(props);
const {rerender} = render(<ChannelIdentifierRouter {...baseProps}/>);
rerender(<ChannelIdentifierRouter {...props}/>);
jest.runOnlyPendingTimers();
expect(getHistory().replace).toHaveBeenLastCalledWith('/team1/channel/identifier1');
@@ -1,120 +1,118 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`components/channel_view Should match snapshot if channel is archived 1`] = `
<div
className="app__content"
id="app-content"
>
<FileUploadOverlay
id="centerChannelFileDropOverlay"
overlayType="center"
/>
<ChannelHeader />
<ChannelBanner
channelId="channelId"
/>
<DeferredRenderWrapper
channelId="channelId"
/>
<div>
<div
className="post-create__container"
id="post-create"
class="app__content"
id="app-content"
>
<div
className="channel-archived__message"
id="channelArchivedMessage"
data-testid="FileUploadOverlay"
/>
<div
data-testid="ChannelHeader"
/>
<div
data-testid="ChannelBanner"
/>
<div
data-channel-id="channelId"
data-testid="deferred-post-view"
/>
<div
class="post-create__container"
id="post-create"
>
<MemoizedFormattedMessage
defaultMessage="You are viewing an <b>archived channel</b>. New messages cannot be posted."
id="channelView.archivedChannel"
values={
Object {
"b": [Function],
}
}
/>
<button
className="btn btn-primary channel-archived__close-btn"
onClick={[Function]}
<div
class="channel-archived__message"
id="channelArchivedMessage"
>
<MemoizedFormattedMessage
defaultMessage="Close Channel"
id="center_panel.archived.closeChannel"
/>
</button>
You are viewing an
<b>
archived channel
</b>
. New messages cannot be posted.
<button
class="btn btn-primary channel-archived__close-btn"
>
Close Channel
</button>
</div>
</div>
</div>
</div>
`;
exports[`components/channel_view Should match snapshot if channel is deactivated 1`] = `
<div
className="app__content"
id="app-content"
>
<FileUploadOverlay
id="centerChannelFileDropOverlay"
overlayType="center"
/>
<ChannelHeader />
<ChannelBanner
channelId="channelId"
/>
<DeferredRenderWrapper
channelId="channelId"
/>
<div>
<div
className="post-create__container AdvancedTextEditor__ctr"
id="post-create"
class="app__content"
id="app-content"
>
<div
className="channel-archived__message"
data-testid="FileUploadOverlay"
/>
<div
data-testid="ChannelHeader"
/>
<div
data-testid="ChannelBanner"
/>
<div
data-channel-id="channelId"
data-testid="deferred-post-view"
/>
<div
class="post-create__container AdvancedTextEditor__ctr"
id="post-create"
>
<MemoizedFormattedMessage
defaultMessage="You are viewing an archived channel with a <b>deactivated user</b>. New messages cannot be posted."
id="channelView.archivedChannelWithDeactivatedUser"
values={
Object {
"b": [Function],
}
}
/>
<button
className="btn btn-primary channel-archived__close-btn"
onClick={[Function]}
<div
class="channel-archived__message"
>
<MemoizedFormattedMessage
defaultMessage="Close Channel"
id="center_panel.archived.closeChannel"
/>
</button>
You are viewing an archived channel with a
<b>
deactivated user
</b>
. New messages cannot be posted.
<button
class="btn btn-primary channel-archived__close-btn"
>
Close Channel
</button>
</div>
</div>
</div>
</div>
`;
exports[`components/channel_view Should match snapshot with base props 1`] = `
<div
className="app__content"
id="app-content"
>
<FileUploadOverlay
id="centerChannelFileDropOverlay"
overlayType="center"
/>
<ChannelHeader />
<ChannelBanner
channelId="channelId"
/>
<DeferredRenderWrapper
channelId="channelId"
/>
<div>
<div
className="post-create__container AdvancedTextEditor__ctr"
data-testid="post-create"
id="post-create"
class="app__content"
id="app-content"
>
<AdvancedCreatePost />
<div
data-testid="FileUploadOverlay"
/>
<div
data-testid="ChannelHeader"
/>
<div
data-testid="ChannelBanner"
/>
<div
data-channel-id="channelId"
data-testid="deferred-post-view"
/>
<div
class="post-create__container AdvancedTextEditor__ctr"
data-testid="post-create"
id="post-create"
>
<div
data-testid="AdvancedCreatePost"
/>
</div>
</div>
</div>
`;
@@ -1,12 +1,44 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {shallow} from 'enzyme';
import React from 'react';
import {renderWithContext, screen} from 'tests/react_testing_utils';
import ChannelView from './channel_view';
import type {Props} from './channel_view';
jest.mock('components/async_load', () => ({
makeAsyncComponent: (name: string) => {
const Component = () => <div data-testid={name}/>;
Component.displayName = name;
return Component;
},
}));
jest.mock('components/deferComponentRender', () => {
return jest.fn(() => {
return function DeferredPostView(props: any) {
return (
<div
data-testid='deferred-post-view'
{...(props.focusedPostId ? {'data-focused-post-id': props.focusedPostId} : {})}
data-channel-id={props.channelId}
/>
);
};
});
});
jest.mock('components/post_view', () => () => <div data-testid='post-view'/>);
jest.mock('client/web_websocket_client', () => ({
__esModule: true,
default: {updateActiveChannel: jest.fn()},
}));
jest.mock('./input_loading', () => () => <div data-testid='input-loading'/>);
describe('components/channel_view', () => {
const baseProps: Props = {
channelId: 'channelId',
@@ -32,50 +64,73 @@ describe('components/channel_view', () => {
};
it('Should match snapshot with base props', () => {
const wrapper = shallow(<ChannelView {...baseProps}/>);
expect(wrapper).toMatchSnapshot();
const {container} = renderWithContext(<ChannelView {...baseProps}/>);
expect(container).toMatchSnapshot();
});
it('Should match snapshot if channel is archived', () => {
const wrapper = shallow(
const {container} = renderWithContext(
<ChannelView
{...baseProps}
channelIsArchived={true}
/>,
);
expect(wrapper).toMatchSnapshot();
expect(container).toMatchSnapshot();
});
it('Should match snapshot if channel is deactivated', () => {
const wrapper = shallow(
const {container} = renderWithContext(
<ChannelView
{...baseProps}
deactivatedChannel={true}
/>,
);
expect(wrapper).toMatchSnapshot();
expect(container).toMatchSnapshot();
});
it('Should have focusedPostId state based on props', () => {
const wrapper = shallow(<ChannelView {...baseProps}/>);
expect(wrapper.state('focusedPostId')).toEqual(undefined);
const {rerender} = renderWithContext(<ChannelView {...baseProps}/>);
wrapper.setProps({channelId: 'newChannelId', match: {url: '/team/channel/channelId/postId', params: {postid: 'postid'}}});
expect(wrapper.state('focusedPostId')).toEqual('postid');
wrapper.setProps({channelId: 'newChannelId', match: {url: '/team/channel/channelId/postId1', params: {postid: 'postid1'}}});
expect(wrapper.state('focusedPostId')).toEqual('postid1');
// Initially no focusedPostId
expect(screen.getByTestId('deferred-post-view')).not.toHaveAttribute('data-focused-post-id');
// Rerender with postid
rerender(
<ChannelView
{...baseProps}
channelId='newChannelId'
match={{url: '/team/channel/channelId/postId', params: {postid: 'postid'}} as Props['match']}
/>,
);
expect(screen.getByTestId('deferred-post-view')).toHaveAttribute('data-focused-post-id', 'postid');
// Rerender with different postid
rerender(
<ChannelView
{...baseProps}
channelId='newChannelId'
match={{url: '/team/channel/channelId/postId1', params: {postid: 'postid1'}} as Props['match']}
/>,
);
expect(screen.getByTestId('deferred-post-view')).toHaveAttribute('data-focused-post-id', 'postid1');
});
it('should call fetchRecentMentions on componentDidUpdate', () => {
const wrapper = shallow(
const {rerender} = renderWithContext(
<ChannelView
{...baseProps}
canRestrictDirectMessage={true}
restrictDirectMessage={undefined as any}
/>,
);
const instance = wrapper.instance() as ChannelView;
wrapper.setProps({channelId: 'newChannelId'});
expect(instance.props.fetchIsRestrictedDM).toHaveBeenCalledTimes(1);
rerender(
<ChannelView
{...baseProps}
canRestrictDirectMessage={true}
restrictDirectMessage={undefined as any}
channelId='newChannelId'
/>,
);
expect(baseProps.fetchIsRestrictedDM).toHaveBeenCalledTimes(1);
});
});
@@ -1,17 +1,7 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`/components/common/hocs/cloud/with_use_get_usage_deltas should pass the useGetUsageDeltas 1`] = `
<Component>
<mockConstructor
usageDeltas={
Object {
"teams": Object {
"active": -1,
},
}
}
>
<div />
</mockConstructor>
</Component>
<div>
<div />
</div>
`;
@@ -1,10 +1,11 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {mount} from 'enzyme';
import React from 'react';
import type {ComponentType} from 'react';
import {renderWithContext} from 'tests/react_testing_utils';
import withGetCloudSubscription from './with_get_cloud_subscription';
describe('/components/common/hocs/with_get_cloud_subcription', () => {
@@ -22,7 +23,7 @@ describe('/components/common/hocs/with_get_cloud_subcription', () => {
const getCloudSubscriptionSpy = jest.spyOn(actions, 'getCloudSubscription');
mount(
renderWithContext(
<EnhancedComponent
isCloud={true}
actions={actions}
@@ -42,7 +43,7 @@ describe('/components/common/hocs/with_get_cloud_subcription', () => {
const getCloudSubscriptionSpy = jest.spyOn(actions, 'getCloudSubscription');
mount(
renderWithContext(
<EnhancedComponent
isCloud={false}
actions={actions}
@@ -62,7 +63,7 @@ describe('/components/common/hocs/with_get_cloud_subcription', () => {
const getCloudSubscriptionSpy = jest.spyOn(actions, 'getCloudSubscription');
mount(
renderWithContext(
<EnhancedComponent
isCloud={true}
actions={actions}
@@ -1,10 +1,11 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {mount} from 'enzyme';
import React from 'react';
import type {ComponentType} from 'react';
import {renderWithContext} from 'tests/react_testing_utils';
import withUseGetUsageDeltas from './with_use_get_usage_deltas';
jest.mock('components/common/hooks/useGetUsageDeltas', () => jest.fn(() => ({
@@ -19,10 +20,10 @@ describe('/components/common/hocs/cloud/with_use_get_usage_deltas', () => {
test('should pass the useGetUsageDeltas', () => {
const WrappedComponent = withUseGetUsageDeltas(TestComponent);
const wrapper = mount(
const {container} = renderWithContext(
<WrappedComponent/>,
);
expect(wrapper).toMatchSnapshot();
expect(container).toMatchSnapshot();
});
});
@@ -1,13 +1,13 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {shallow} from 'enzyme';
import React from 'react';
import type {ChannelType} from '@mattermost/types/channels';
import {loadProfilesForSidebar} from 'actions/user_actions';
import {renderWithContext, runPostRenderAct} from 'tests/react_testing_utils';
import {TestHelper} from 'utils/test_helper';
import DataPrefetch from './data_prefetch';
@@ -71,28 +71,30 @@ describe('/components/data_prefetch', () => {
beforeEach(() => {
mockQueue.splice(0, mockQueue.length);
jest.clearAllMocks();
});
test('should fetch posts for current channel on first channel load', async () => {
const props = defaultProps;
const wrapper = shallow<DataPrefetch>(
const props = {...defaultProps};
const {rerender} = renderWithContext(
<DataPrefetch {...props}/>,
);
const instance = wrapper.instance();
instance.prefetchPosts = jest.fn();
// Change channels and wait for async componentDidUpdate to resolve
wrapper.setProps({currentChannelId: 'currentChannelId'});
await Promise.resolve(true);
rerender(
<DataPrefetch
{...props}
currentChannelId='currentChannelId'
/>,
);
await runPostRenderAct();
expect(mockQueue).toHaveLength(1);
expect(instance.prefetchPosts).not.toHaveBeenCalled();
// Manually run queued tasks
mockQueue.shift()!();
expect(instance.prefetchPosts).toHaveBeenCalledTimes(1);
expect(instance.prefetchPosts).toHaveBeenCalledWith('currentChannelId');
expect(props.actions.prefetchChannelPosts).toHaveBeenCalledTimes(1);
expect(props.actions.prefetchChannelPosts).toHaveBeenCalledWith('currentChannelId', undefined);
});
test('should fetch profiles for sidebar on sidebar load', async () => {
@@ -100,20 +102,30 @@ describe('/components/data_prefetch', () => {
...defaultProps,
sidebarLoaded: false,
};
const wrapper = shallow<DataPrefetch>(
const {rerender} = renderWithContext(
<DataPrefetch {...props}/>,
);
expect(loadProfilesForSidebar).not.toHaveBeenCalled();
// Finish loading the sidebar
wrapper.setProps({sidebarLoaded: true});
await Promise.resolve(true);
rerender(
<DataPrefetch
{...props}
sidebarLoaded={true}
/>,
);
await runPostRenderAct();
expect(loadProfilesForSidebar).toHaveBeenCalledTimes(1);
// Reload the sidebar
wrapper.setProps({sidebarLoaded: true});
// Reload the sidebar - should not call again
rerender(
<DataPrefetch
{...props}
sidebarLoaded={true}
/>,
);
await Promise.resolve(true);
expect(loadProfilesForSidebar).toHaveBeenCalledTimes(1);
@@ -128,38 +140,39 @@ describe('/components/data_prefetch', () => {
3: [],
},
};
const wrapper = shallow<DataPrefetch>(
const {rerender} = renderWithContext(
<DataPrefetch {...props}/>,
);
const instance = wrapper.instance();
instance.prefetchPosts = jest.fn();
wrapper.setProps({currentChannelId: 'currentChannelId'});
await Promise.resolve(true);
rerender(
<DataPrefetch
{...props}
currentChannelId='currentChannelId'
/>,
);
await runPostRenderAct();
expect(mockQueue).toHaveLength(5); // current channel, mentioned channels, unread channels
expect(instance.prefetchPosts).not.toHaveBeenCalled();
mockQueue.shift()!();
expect(instance.prefetchPosts).toHaveBeenCalledTimes(1);
expect(instance.prefetchPosts).toHaveBeenCalledWith('currentChannelId');
expect(props.actions.prefetchChannelPosts).toHaveBeenCalledTimes(1);
expect(props.actions.prefetchChannelPosts).toHaveBeenCalledWith('currentChannelId', undefined);
mockQueue.shift()!();
expect(instance.prefetchPosts).toHaveBeenCalledTimes(2);
expect(instance.prefetchPosts).toHaveBeenCalledWith('mentionChannel0');
expect(props.actions.prefetchChannelPosts).toHaveBeenCalledTimes(2);
expect(props.actions.prefetchChannelPosts).toHaveBeenCalledWith('mentionChannel0', undefined);
mockQueue.shift()!();
expect(instance.prefetchPosts).toHaveBeenCalledTimes(3);
expect(instance.prefetchPosts).toHaveBeenCalledWith('mentionChannel1');
expect(props.actions.prefetchChannelPosts).toHaveBeenCalledTimes(3);
expect(props.actions.prefetchChannelPosts).toHaveBeenCalledWith('mentionChannel1', undefined);
mockQueue.shift()!();
expect(instance.prefetchPosts).toHaveBeenCalledTimes(4);
expect(instance.prefetchPosts).toHaveBeenCalledWith('unreadChannel0');
expect(props.actions.prefetchChannelPosts).toHaveBeenCalledTimes(4);
expect(props.actions.prefetchChannelPosts).toHaveBeenCalledWith('unreadChannel0', undefined);
mockQueue.shift()!();
expect(instance.prefetchPosts).toHaveBeenCalledTimes(5);
expect(instance.prefetchPosts).toHaveBeenCalledWith('unreadChannel1');
expect(props.actions.prefetchChannelPosts).toHaveBeenCalledTimes(5);
expect(props.actions.prefetchChannelPosts).toHaveBeenCalledWith('unreadChannel1', undefined);
});
test('should cancel fetch and requeue channels when prefetch queue changes', async () => {
@@ -170,33 +183,40 @@ describe('/components/data_prefetch', () => {
2: ['unreadChannel0', 'unreadChannel1', 'unreadChannel2'],
},
};
const wrapper = shallow<DataPrefetch>(
const {rerender} = renderWithContext(
<DataPrefetch {...props}/>,
);
const instance = wrapper.instance();
instance.prefetchPosts = jest.fn();
wrapper.setProps({currentChannelId: 'currentChannelId'});
await Promise.resolve(true);
rerender(
<DataPrefetch
{...props}
currentChannelId='currentChannelId'
/>,
);
await runPostRenderAct();
expect(mockQueue).toHaveLength(4);
expect(instance.prefetchPosts).not.toHaveBeenCalled();
mockQueue.shift()!();
expect(instance.prefetchPosts).toHaveBeenCalledTimes(1);
expect(instance.prefetchPosts).toHaveBeenCalledWith('currentChannelId');
expect(props.actions.prefetchChannelPosts).toHaveBeenCalledTimes(1);
expect(props.actions.prefetchChannelPosts).toHaveBeenCalledWith('currentChannelId', undefined);
mockQueue.shift()!();
expect(instance.prefetchPosts).toHaveBeenCalledTimes(2);
expect(instance.prefetchPosts).toHaveBeenCalledWith('unreadChannel0');
expect(props.actions.prefetchChannelPosts).toHaveBeenCalledTimes(2);
expect(props.actions.prefetchChannelPosts).toHaveBeenCalledWith('unreadChannel0', undefined);
wrapper.setProps({
prefetchQueueObj: {
1: ['mentionChannel0', 'mentionChannel1'],
2: ['unreadChannel2', 'unreadChannel3'],
},
});
const newPrefetchQueueObj = {
1: ['mentionChannel0', 'mentionChannel1'],
2: ['unreadChannel2', 'unreadChannel3'],
};
rerender(
<DataPrefetch
{...props}
currentChannelId='currentChannelId'
prefetchQueueObj={newPrefetchQueueObj}
/>,
);
// Check queue has been cleared and wait for async componentDidUpdate to complete
expect(mockQueue).toHaveLength(0);
@@ -205,20 +225,20 @@ describe('/components/data_prefetch', () => {
expect(mockQueue).toHaveLength(4);
mockQueue.shift()!();
expect(instance.prefetchPosts).toHaveBeenCalledTimes(3);
expect(instance.prefetchPosts).toHaveBeenCalledWith('mentionChannel0');
expect(props.actions.prefetchChannelPosts).toHaveBeenCalledTimes(3);
expect(props.actions.prefetchChannelPosts).toHaveBeenCalledWith('mentionChannel0', undefined);
mockQueue.shift()!();
expect(instance.prefetchPosts).toHaveBeenCalledTimes(4);
expect(instance.prefetchPosts).toHaveBeenCalledWith('mentionChannel1');
expect(props.actions.prefetchChannelPosts).toHaveBeenCalledTimes(4);
expect(props.actions.prefetchChannelPosts).toHaveBeenCalledWith('mentionChannel1', undefined);
mockQueue.shift()!();
expect(instance.prefetchPosts).toHaveBeenCalledTimes(5);
expect(instance.prefetchPosts).toHaveBeenCalledWith('unreadChannel2');
expect(props.actions.prefetchChannelPosts).toHaveBeenCalledTimes(5);
expect(props.actions.prefetchChannelPosts).toHaveBeenCalledWith('unreadChannel2', undefined);
mockQueue.shift()!();
expect(instance.prefetchPosts).toHaveBeenCalledTimes(6);
expect(instance.prefetchPosts).toHaveBeenCalledWith('unreadChannel3');
expect(props.actions.prefetchChannelPosts).toHaveBeenCalledTimes(6);
expect(props.actions.prefetchChannelPosts).toHaveBeenCalledWith('unreadChannel3', undefined);
});
test('should skip making request for posts if a request was made', async () => {
@@ -232,25 +252,27 @@ describe('/components/data_prefetch', () => {
unreadChannel: 'success',
},
};
const wrapper = shallow<DataPrefetch>(
const {rerender} = renderWithContext(
<DataPrefetch {...props}/>,
);
const instance = wrapper.instance();
instance.prefetchPosts = jest.fn();
wrapper.setProps({currentChannelId: 'currentChannelId'});
await Promise.resolve(true);
rerender(
<DataPrefetch
{...props}
currentChannelId='currentChannelId'
/>,
);
await runPostRenderAct();
expect(mockQueue).toHaveLength(2);
mockQueue.shift()!();
expect(instance.prefetchPosts).toHaveBeenCalledTimes(1);
expect(instance.prefetchPosts).toHaveBeenCalledWith('currentChannelId');
expect(props.actions.prefetchChannelPosts).toHaveBeenCalledTimes(1);
expect(props.actions.prefetchChannelPosts).toHaveBeenCalledWith('currentChannelId', undefined);
mockQueue.shift()!();
expect(instance.prefetchPosts).toHaveBeenCalledTimes(2);
expect(instance.prefetchPosts).toHaveBeenCalledWith('mentionChannel');
expect(props.actions.prefetchChannelPosts).toHaveBeenCalledTimes(2);
expect(props.actions.prefetchChannelPosts).toHaveBeenCalledWith('mentionChannel', undefined);
});
test('should add delay if last post is made in last min', async () => {
@@ -280,12 +302,17 @@ describe('/components/data_prefetch', () => {
last_root_post_at: 12345,
}],
};
const wrapper = shallow(
const {rerender} = renderWithContext(
<DataPrefetch {...props}/>,
);
wrapper.setProps({currentChannelId: 'currentChannelId'});
await Promise.resolve(true);
rerender(
<DataPrefetch
{...props}
currentChannelId='currentChannelId'
/>,
);
await runPostRenderAct();
expect(mockQueue).toHaveLength(2);
@@ -327,12 +354,17 @@ describe('/components/data_prefetch', () => {
last_root_post_at: 12345,
}],
};
const wrapper = shallow<DataPrefetch>(
const {rerender} = renderWithContext(
<DataPrefetch {...props}/>,
);
wrapper.setProps({currentChannelId: 'currentChannelId'});
await Promise.resolve(true);
rerender(
<DataPrefetch
{...props}
currentChannelId='currentChannelId'
/>,
);
await runPostRenderAct();
expect(mockQueue).toHaveLength(2);
@@ -354,26 +386,37 @@ describe('/components/data_prefetch', () => {
sidebarLoaded: false,
};
let wrapper = shallow<DataPrefetch>(
const {rerender, unmount} = renderWithContext(
<DataPrefetch {...props}/>,
);
wrapper.setProps({});
// Rerender with same props - no sidebar loaded
rerender(<DataPrefetch {...props}/>);
expect(loadProfilesForSidebar).not.toHaveBeenCalled();
unmount();
// With current channel loaded first
wrapper = shallow<DataPrefetch>(
const {rerender: rerender2} = renderWithContext(
<DataPrefetch {...props}/>,
);
wrapper.setProps({
currentChannelId: 'channel',
});
rerender2(
<DataPrefetch
{...props}
currentChannelId='channel'
/>,
);
expect(loadProfilesForSidebar).not.toHaveBeenCalled();
wrapper.setProps({
sidebarLoaded: true,
});
rerender2(
<DataPrefetch
{...props}
currentChannelId='channel'
sidebarLoaded={true}
/>,
);
expect(loadProfilesForSidebar).toHaveBeenCalled();
});
@@ -1,24 +1,16 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {shallow} from 'enzyme';
import React from 'react';
import {Modal} from 'react-bootstrap';
import {Provider} from 'react-redux';
import type {DialogElement as TDialogElement} from '@mattermost/types/integrations';
import {mountWithIntl} from 'tests/helpers/intl-test-helper';
import mockStore from 'tests/test_store';
import {renderWithContext, screen, userEvent, waitFor} from 'tests/react_testing_utils';
import EmojiMap from 'utils/emoji_map';
import type {Props} from './interactive_dialog';
import InteractiveDialog from './interactive_dialog';
const submitEvent = {
preventDefault: jest.fn(),
} as unknown as React.FormEvent<HTMLFormElement>;
describe('components/interactive_dialog/InteractiveDialog', () => {
const baseProps: Props = {
url: 'http://example.com',
@@ -47,23 +39,33 @@ describe('components/interactive_dialog/InteractiveDialog', () => {
lookupInteractiveDialog: jest.fn(),
},
};
const wrapper = shallow<InteractiveDialog>(<InteractiveDialog {...props}/>);
renderWithContext(<InteractiveDialog {...props}/>);
await wrapper.instance().handleSubmit(submitEvent);
const submitButton = screen.getByText('Yes');
await userEvent.click(submitButton);
const expected = (
<div className='error-text'>
{'This is an error.'}
</div>
);
expect(wrapper.find(Modal.Footer).containsMatchingElement(expected)).toBe(true);
await waitFor(() => {
expect(screen.getByText('This is an error.')).toBeInTheDocument();
});
const errorElement = screen.getByText('This is an error.');
expect(errorElement).toHaveClass('error-text');
});
test('should not appear when submit does not return an error', async () => {
const wrapper = shallow<InteractiveDialog>(<InteractiveDialog {...baseProps}/>);
await wrapper.instance().handleSubmit(submitEvent);
const props = {
...baseProps,
actions: {
submitInteractiveDialog: jest.fn().mockResolvedValue({}),
lookupInteractiveDialog: jest.fn(),
},
};
renderWithContext(<InteractiveDialog {...props}/>);
expect(wrapper.find(Modal.Footer).exists('.error-text')).toBe(false);
const submitButton = screen.getByText('Yes');
await userEvent.click(submitButton);
expect(document.querySelector('.error-text')).not.toBeInTheDocument();
});
});
@@ -88,20 +90,14 @@ describe('components/interactive_dialog/InteractiveDialog', () => {
max_length: 0,
};
const {elements, ...rest} = baseProps;
elements?.push(selectElement);
const props = {
...rest,
elements,
...baseProps,
elements: [selectElement],
};
const store = mockStore({});
const wrapper = mountWithIntl(
<Provider store={store}>
<InteractiveDialog {...props}/>
</Provider>,
);
expect(wrapper.find(Modal.Body).find('input').find({defaultValue: 'Option3'}).exists()).toBe(true);
renderWithContext(<InteractiveDialog {...props}/>);
expect(screen.getByDisplayValue('Option3')).toBeInTheDocument();
});
});
@@ -120,14 +116,6 @@ describe('components/interactive_dialog/InteractiveDialog', () => {
max_length: 0,
options: [],
};
const {elements, ...rest} = baseProps;
const props = {
...rest,
elements: [
...elements || [],
element,
],
};
const testCases = [
{description: 'no default', expectedChecked: false},
@@ -140,19 +128,26 @@ describe('components/interactive_dialog/InteractiveDialog', () => {
];
testCases.forEach((testCase) => test(`should interpret ${testCase.description}`, () => {
const testElement = {...element};
if (testCase.default === undefined) {
delete (element as any).default;
delete (testElement as any).default;
} else {
(element as any).default = testCase.default;
(testElement as any).default = testCase.default;
}
const store = mockStore({});
const wrapper = mountWithIntl(
<Provider store={store}>
<InteractiveDialog {...props}/>
</Provider>,
);
expect(wrapper.find(Modal.Body).find('input').find({checked: testCase.expectedChecked}).exists()).toBe(true);
const props = {
...baseProps,
elements: [testElement],
};
renderWithContext(<InteractiveDialog {...props}/>);
const checkbox = screen.getByRole('checkbox');
if (testCase.expectedChecked) {
expect(checkbox).toBeChecked();
} else {
expect(checkbox).not.toBeChecked();
}
}));
});
});
@@ -1,18 +1,11 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {shallow} from 'enzyme';
import React from 'react';
import deepFreeze from 'mattermost-redux/utils/deep_freeze';
import AlertIcon from 'components/widgets/icons/alert_icon';
import EmailIcon from 'components/widgets/icons/mail_icon';
import BotTag from 'components/widgets/tag/bot_tag';
import GuestTag from 'components/widgets/tag/guest_tag';
import Avatar from 'components/widgets/users/avatar';
import {mountWithIntl} from 'tests/helpers/intl-test-helper';
import {renderWithContext, screen} from 'tests/react_testing_utils';
import ResultTable from './result_table';
import type {Props} from './result_table';
@@ -75,8 +68,8 @@ describe('ResultTable', () => {
email: 'aa@aa.aa',
reason: {id: 'some_reason', defaultMessage: 'some reason'},
}];
const wrapper = shallow(<ResultTable {...props}/>);
expect(wrapper.find(EmailIcon).length).toBe(1);
const {container} = renderWithContext(<ResultTable {...props}/>);
expect(container.querySelector('.mail-icon')).toBeInTheDocument();
});
test('unsent invites render as unsent invites', () => {
@@ -84,8 +77,8 @@ describe('ResultTable', () => {
text: '@incomplete_userna',
reason: {id: 'incomplete_user', defaultMessage: 'This was not a complete user'},
}];
const wrapper = shallow(<ResultTable {...props}/>);
expect(wrapper.find(AlertIcon).length).toBe(1);
const {container} = renderWithContext(<ResultTable {...props}/>);
expect(container.querySelector('.alert-icon')).toBeInTheDocument();
});
test('user invites render as users', () => {
@@ -93,10 +86,10 @@ describe('ResultTable', () => {
user: defaultUser,
reason: {id: 'success', defaultMessage: 'added successfully'},
}];
const wrapper = shallow(<ResultTable {...props}/>);
expect(wrapper.find(Avatar).length).toBe(1);
expect(wrapper.find(BotTag).length).toBe(0);
expect(wrapper.find(GuestTag).length).toBe(0);
const {container} = renderWithContext(<ResultTable {...props}/>);
expect(container.querySelector('.Avatar')).toBeInTheDocument();
expect(screen.queryByText('BOT')).not.toBeInTheDocument();
expect(screen.queryByText('GUEST')).not.toBeInTheDocument();
});
test('bots render as bots', () => {
@@ -107,10 +100,10 @@ describe('ResultTable', () => {
},
reason: {id: 'success', defaultMessage: 'added successfully'},
}];
const wrapper = shallow(<ResultTable {...props}/>);
expect(wrapper.find(Avatar).length).toBe(1);
expect(wrapper.find(BotTag).length).toBe(1);
expect(wrapper.find(GuestTag).length).toBe(0);
const {container} = renderWithContext(<ResultTable {...props}/>);
expect(container.querySelector('.Avatar')).toBeInTheDocument();
expect(screen.getByText('BOT')).toBeInTheDocument();
expect(screen.queryByText('GUEST')).not.toBeInTheDocument();
});
test('guests render as guests', () => {
@@ -121,21 +114,21 @@ describe('ResultTable', () => {
},
reason: {id: 'success', defaultMessage: 'added successfully'},
}];
const wrapper = shallow(<ResultTable {...props}/>);
expect(wrapper.find(Avatar).length).toBe(1);
expect(wrapper.find(BotTag).length).toBe(0);
expect(wrapper.find(GuestTag).length).toBe(1);
const {container} = renderWithContext(<ResultTable {...props}/>);
expect(container.querySelector('.Avatar')).toBeInTheDocument();
expect(screen.queryByText('BOT')).not.toBeInTheDocument();
expect(screen.getByText('GUEST')).toBeInTheDocument();
});
test('renders success banner when invites were sent', () => {
props.sent = true;
const wrapper = mountWithIntl(<ResultTable {...props}/>);
expect(wrapper.find('h2').at(0).text()).toContain('Successful Invites');
renderWithContext(<ResultTable {...props}/>);
expect(screen.getByText('Successful Invites')).toBeInTheDocument();
});
test('renders not sent banner when invites were not sent', () => {
props.sent = false;
const wrapper = mountWithIntl(<ResultTable {...props}/>);
expect(wrapper.find('h2').at(0).text()).toContain('Invitations Not Sent');
renderWithContext(<ResultTable {...props}/>);
expect(screen.getByText('Invitations Not Sent')).toBeInTheDocument();
});
});
@@ -6,7 +6,7 @@ import React from 'react';
import LatexInline from 'components/latex_inline/latex_inline';
import {withIntl} from 'tests/helpers/intl-test-helper';
import {act, renderWithContext} from 'tests/react_testing_utils';
import {renderWithContext, waitFor} from 'tests/react_testing_utils';
describe('components/LatexInline', () => {
const defaultProps = {
@@ -15,27 +15,20 @@ describe('components/LatexInline', () => {
};
test('should match snapshot', async () => {
let container;
await act(async () => {
const result = renderWithContext(withIntl(<LatexInline {...defaultProps}/>));
container = result.container;
const {container} = renderWithContext(withIntl(<LatexInline {...defaultProps}/>));
await waitFor(() => {
expect(container.querySelector('[data-testid="latex-enabled"]')).toBeInTheDocument();
});
expect(container).toMatchSnapshot();
});
test('latex is disabled', async () => {
test('latex is disabled', () => {
const props = {
...defaultProps,
enableInlineLatex: false,
};
let container;
await act(async () => {
const result = renderWithContext(withIntl(<LatexInline {...props}/>));
container = result.container;
});
const {container} = renderWithContext(withIntl(<LatexInline {...props}/>));
expect(container).toMatchSnapshot();
});
@@ -45,11 +38,9 @@ describe('components/LatexInline', () => {
enableInlineLatex: true,
};
let container;
await act(async () => {
const result = renderWithContext(withIntl(<LatexInline {...props}/>));
container = result.container;
const {container} = renderWithContext(withIntl(<LatexInline {...props}/>));
await waitFor(() => {
expect(container.querySelector('[data-testid="latex-enabled"]')).toBeInTheDocument();
});
expect(container).toMatchSnapshot();
});
@@ -1,39 +1,41 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`components/MarkdownImageExpand should match snapshot for collapsed embeds 1`] = `
<div
className="markdown-image-expand "
>
<button
className="markdown-image-expand__expand-button"
onClick={[Function]}
type="button"
<div>
<div
class="markdown-image-expand "
>
<span
className="icon icon-menu-right markdown-image-expand__expand-icon"
/>
<span
className="markdown-image-expand__alt-text"
<button
class="markdown-image-expand__expand-button"
type="button"
>
Some alt text
</span>
</button>
<span
class="icon icon-menu-right markdown-image-expand__expand-icon"
/>
<span
class="markdown-image-expand__alt-text"
>
Some alt text
</span>
</button>
</div>
</div>
`;
exports[`components/MarkdownImageExpand should match snapshot for expanded embeds 1`] = `
<div
className="markdown-image-expand markdown-image-expand--expanded"
>
<button
className="markdown-image-expand__collapse-button"
onClick={[Function]}
type="button"
<div>
<div
class="markdown-image-expand markdown-image-expand--expanded"
>
<span
className="icon icon-menu-down"
/>
</button>
An image to expand
<button
class="markdown-image-expand__collapse-button"
type="button"
>
<span
class="icon icon-menu-down"
/>
</button>
An image to expand
</div>
</div>
`;
@@ -1,16 +1,17 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {shallow} from 'enzyme';
import React from 'react';
import {renderWithContext, userEvent} from 'tests/react_testing_utils';
import MarkdownImageExpand from './markdown_image_expand';
describe('components/MarkdownImageExpand', () => {
it('should match snapshot for collapsed embeds', () => {
const toggleHandler = jest.fn();
const imageCollapseHandler = jest.fn();
const wrapper = shallow(
const {container} = renderWithContext(
<MarkdownImageExpand
alt={'Some alt text'}
postId={'abc'}
@@ -21,13 +22,13 @@ describe('components/MarkdownImageExpand', () => {
>{'An image to expand'}</MarkdownImageExpand>,
);
expect(wrapper).toMatchSnapshot();
expect(container).toMatchSnapshot();
});
it('should match snapshot for expanded embeds', () => {
const toggleHandler = jest.fn();
const imageCollapseHandler = jest.fn();
const wrapper = shallow(
const {container} = renderWithContext(
<MarkdownImageExpand
alt={'Some alt text'}
postId={'abc'}
@@ -38,13 +39,13 @@ describe('components/MarkdownImageExpand', () => {
>{'An image to expand'}</MarkdownImageExpand>,
);
expect(wrapper).toMatchSnapshot();
expect(container).toMatchSnapshot();
});
it('should emit toggle action on collapse button click', () => {
it('should emit toggle action on collapse button click', async () => {
const toggleHandler = jest.fn();
const imageCollapseHandler = jest.fn();
const wrapper = shallow(
const {container} = renderWithContext(
<MarkdownImageExpand
alt={'Some alt text'}
postId={'abc'}
@@ -55,15 +56,16 @@ describe('components/MarkdownImageExpand', () => {
>{'An image to expand'}</MarkdownImageExpand>,
);
wrapper.find('.markdown-image-expand__collapse-button').simulate('click');
const collapseButton = container.querySelector('.markdown-image-expand__collapse-button')!;
await userEvent.click(collapseButton);
expect(imageCollapseHandler).toHaveBeenCalled();
});
it('should emit toggle action on expand button click', () => {
it('should emit toggle action on expand button click', async () => {
const toggleHandler = jest.fn();
const imageCollapseHandler = jest.fn();
const wrapper = shallow(
const {container} = renderWithContext(
<MarkdownImageExpand
alt={'Some alt text'}
postId={'abc'}
@@ -74,7 +76,8 @@ describe('components/MarkdownImageExpand', () => {
>{'An image to expand'}</MarkdownImageExpand>,
);
wrapper.find('.markdown-image-expand__expand-button').simulate('click');
const expandButton = container.querySelector('.markdown-image-expand__expand-button')!;
await userEvent.click(expandButton);
expect(imageCollapseHandler).toHaveBeenCalled();
});
@@ -1,31 +1,3 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`components/ModalController component should match snapshot without any modals 1`] = `
<Provider
store={
Object {
"clearActions": [Function],
"dispatch": [Function],
"getActions": [Function],
"getState": [Function],
"replaceReducer": [Function],
"subscribe": [Function],
}
}
>
<Connect(ModalController)>
<ModalController
actions={
Object {
"closeModal": [Function],
}
}
modals={
Object {
"modalState": Object {},
}
}
/>
</Connect(ModalController)>
</Provider>
`;
exports[`components/ModalController component should match snapshot without any modals 1`] = `<div />`;
@@ -1,14 +1,12 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {mount} from 'enzyme';
import React from 'react';
import {Modal} from 'react-bootstrap';
import {Provider} from 'react-redux';
import {closeModal} from 'actions/views/modals';
import mockStore from 'tests/test_store';
import {renderWithContext, act, userEvent} from 'tests/react_testing_utils';
import ModalController from '.';
@@ -59,16 +57,14 @@ describe('components/ModalController', () => {
},
};
const store = mockStore(state);
const wrapper = mount(
<Provider store={store}>
<ModalController/>
</Provider>,
const {container} = renderWithContext(
<ModalController/>,
state,
{useMockedStore: true},
);
expect(wrapper).toMatchSnapshot();
expect(wrapper.find('ModalController > *').length).toBe(0);
expect(container).toMatchSnapshot();
expect(container.childElementCount).toBe(0);
expect(document.getElementsByClassName('modal-dialog').length).toBeFalsy();
});
@@ -87,18 +83,16 @@ describe('components/ModalController', () => {
},
};
const store = mockStore(state);
mount(
<Provider store={store}>
<ModalController/>
</Provider>,
renderWithContext(
<ModalController/>,
state,
{useMockedStore: true},
);
expect(document.getElementsByClassName('modal-dialog').length).toBe(1);
});
test('should pass onExited to modal to allow a modal to remove itself', () => {
test('should pass onExited to modal to allow a modal to remove itself', async () => {
const state = {
views: {
modals: {
@@ -113,26 +107,30 @@ describe('components/ModalController', () => {
},
};
const store = mockStore(state);
const wrapper = mount(
<Provider store={store}>
<ModalController/>
</Provider>,
const {store} = renderWithContext(
<ModalController/>,
state,
{useMockedStore: true},
);
expect(wrapper.find(TestModal).exists()).toBe(true);
expect(wrapper.find(TestModal).prop('onExited')).toBeDefined();
expect(wrapper.find(Modal).prop('onExited')).toBeDefined();
// Verify the modal is rendered
expect(document.getElementsByClassName('modal-dialog').length).toBe(1);
wrapper.find(TestModal).prop('onExited')!();
// Click the close button to trigger modal close flow (onHide -> setState show:false -> onExited)
const closeButton = document.querySelector('.close') as HTMLElement;
await userEvent.click(closeButton);
expect(store.getActions()).toEqual([
// Wait for the modal's exit transition to complete and fire onExited
await act(async () => {
await new Promise((resolve) => setTimeout(resolve, 500));
});
expect((store as unknown as {getActions: () => unknown[]}).getActions()).toEqual([
closeModal(modalId),
]);
});
test('should call a provided onExited in addition to removing the modal', () => {
test('should call a provided onExited in addition to removing the modal', async () => {
const onExited = jest.fn();
const state = {
@@ -151,21 +149,25 @@ describe('components/ModalController', () => {
},
};
const store = mockStore(state);
const wrapper = mount(
<Provider store={store}>
<ModalController/>
</Provider>,
renderWithContext(
<ModalController/>,
state,
{useMockedStore: true},
);
expect(wrapper.find(TestModal).exists()).toBe(true);
expect(wrapper.find(TestModal).prop('onExited')).toBeDefined();
expect(wrapper.find(Modal).prop('onExited')).toBeDefined();
// Verify the modal is rendered
expect(document.getElementsByClassName('modal-dialog').length).toBe(1);
expect(onExited).not.toHaveBeenCalled();
wrapper.find(TestModal).prop('onExited')!();
// Click the close button to trigger modal close flow (onHide -> setState show:false -> onExited)
const closeButton = document.querySelector('.close') as HTMLElement;
await userEvent.click(closeButton);
// Wait for the modal's exit transition to complete and fire onExited
await act(async () => {
await new Promise((resolve) => setTimeout(resolve, 500));
});
expect(onExited).toHaveBeenCalled();
});
@@ -1,49 +1,12 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`components/PermalinkView should match snapshot 1`] = `
<div
className="app__content"
id="app-content"
/>
`;
exports[`components/PermalinkView should match snapshot with archived channel 1`] = `
<Memo(PermalinkView)
actions={
Object {
"focusPost": [MockFunction] {
"calls": Array [
Array [
"post_id",
"return_to",
"current_user",
],
],
"results": Array [
Object {
"type": "return",
"value": undefined,
},
],
},
}
}
channelId="channel_id"
channelIsArchived={true}
currentUserId="current_user"
match={
Object {
"params": Object {
"postid": "post_id",
},
}
}
returnTo="return_to"
teamName="team_name"
>
<div>
<div
className="app__content"
class="app__content"
id="app-content"
/>
</Memo(PermalinkView)>
</div>
`;
exports[`components/PermalinkView should match snapshot with archived channel 1`] = `<div />`;
@@ -1,7 +1,6 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {shallow} from 'enzyme';
import nock from 'nock';
import React from 'react';
import type {ComponentProps} from 'react';
@@ -18,8 +17,7 @@ import {focusPost} from 'components/permalink_view/actions';
import PermalinkView from 'components/permalink_view/permalink_view';
import TestHelper from 'packages/mattermost-redux/test/test_helper';
import {mountWithIntl} from 'tests/helpers/intl-test-helper';
import {act, waitFor} from 'tests/react_testing_utils';
import {renderWithContext, waitFor} from 'tests/react_testing_utils';
import mockStore from 'tests/test_store';
import {getHistory} from 'utils/browser_history';
import {joinPrivateChannelPrompt} from 'utils/channel_utils';
@@ -104,15 +102,15 @@ describe('components/PermalinkView', () => {
};
test('should match snapshot', async () => {
const wrapper = shallow(
const {container} = renderWithContext(
<PermalinkView {...baseProps}/>,
);
expect(wrapper).toMatchSnapshot();
expect(container).toMatchSnapshot();
});
test('should call baseProps.actions.focusPost on doPermalinkEvent', async () => {
mountWithIntl(
renderWithContext(
<PermalinkView {...baseProps}/>,
);
@@ -123,15 +121,18 @@ describe('components/PermalinkView', () => {
});
test('should call baseProps.actions.focusPost when postid changes', async () => {
const wrapper = mountWithIntl(
const {rerender} = renderWithContext(
<PermalinkView {...baseProps}/>,
);
const newPostid = `${baseProps.match.params.postid}_new`;
act(() => {
wrapper.setProps({...baseProps, match: {params: {postid: newPostid}}} as any);
});
rerender(
<PermalinkView
{...baseProps}
match={{params: {postid: newPostid}} as match<{postid: string}>}
/>,
);
await waitFor(() => {
expect(baseProps.actions.focusPost).toHaveBeenCalledTimes(2);
@@ -142,13 +143,13 @@ describe('components/PermalinkView', () => {
test('should match snapshot with archived channel', async () => {
const props = {...baseProps, channelIsArchived: true};
const wrapper = mountWithIntl(
const {container} = renderWithContext(
<PermalinkView {...props}/>,
);
await waitFor(() => {});
expect(wrapper).toMatchSnapshot();
expect(container).toMatchSnapshot();
});
describe('actions', () => {
@@ -1,60 +1,42 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`components/post_view/embedded_bindings/embedded_binding should match snapshot 1`] = `
<div
className="attachment"
onClick={[Function]}
>
<div>
<div
className="attachment__content"
class="attachment"
>
<div
className="clearfix attachment__container attachment__container--"
class="attachment__content"
>
<h1
className="attachment__title"
<div
class="clearfix attachment__container attachment__container--"
>
<Connect(Markdown)
message="some text"
options={
Object {
"mentionHighlight": false,
"renderer": LinkOnlyRenderer {
"options": Object {},
},
}
}
postId="post_id"
/>
</h1>
<div>
<div
className="attachment__body attachment__body--no_thumb"
<h1
class="attachment__title"
>
<Connect(ShowMore)
isAttachmentText={true}
maxHeight={200}
text="some title"
some text
</h1>
<div>
<div
class="attachment__body attachment__body--no_thumb"
>
<Connect(Markdown)
imageProps={
Object {
"onImageHeightChanged": [Function],
"onImageLoaded": [Function],
}
}
message="some title"
postId="post_id"
/>
</Connect(ShowMore)>
<div
class="post-message post-message--collapsed"
>
<div
class="post-message__text-container"
style="max-height: 200px;"
>
<p>
some title
</p>
</div>
</div>
</div>
<div
style="clear: both;"
/>
</div>
<div
style={
Object {
"clear": "both",
}
}
/>
</div>
</div>
</div>
@@ -62,60 +44,42 @@ exports[`components/post_view/embedded_bindings/embedded_binding should match sn
`;
exports[`components/post_view/embedded_bindings/embedded_binding should match snapshot when the attachment has a link in the title 1`] = `
<div
className="attachment"
onClick={[Function]}
>
<div>
<div
className="attachment__content"
class="attachment"
>
<div
className="clearfix attachment__container attachment__container--"
class="attachment__content"
>
<h1
className="attachment__title"
<div
class="clearfix attachment__container attachment__container--"
>
<Connect(Markdown)
message="some text"
options={
Object {
"mentionHighlight": false,
"renderer": LinkOnlyRenderer {
"options": Object {},
},
}
}
postId="post_id"
/>
</h1>
<div>
<div
className="attachment__body attachment__body--no_thumb"
<h1
class="attachment__title"
>
<Connect(ShowMore)
isAttachmentText={true}
maxHeight={200}
text="some title"
some text
</h1>
<div>
<div
class="attachment__body attachment__body--no_thumb"
>
<Connect(Markdown)
imageProps={
Object {
"onImageHeightChanged": [Function],
"onImageLoaded": [Function],
}
}
message="some title"
postId="post_id"
/>
</Connect(ShowMore)>
<div
class="post-message post-message--collapsed"
>
<div
class="post-message__text-container"
style="max-height: 200px;"
>
<p>
some title
</p>
</div>
</div>
</div>
<div
style="clear: both;"
/>
</div>
<div
style={
Object {
"clear": "both",
}
}
/>
</div>
</div>
</div>
@@ -123,60 +87,42 @@ exports[`components/post_view/embedded_bindings/embedded_binding should match sn
`;
exports[`components/post_view/embedded_bindings/embedded_binding should match snapshot when the attachment has an emoji in the title 1`] = `
<div
className="attachment"
onClick={[Function]}
>
<div>
<div
className="attachment__content"
class="attachment"
>
<div
className="clearfix attachment__container attachment__container--"
class="attachment__content"
>
<h1
className="attachment__title"
<div
class="clearfix attachment__container attachment__container--"
>
<Connect(Markdown)
message="some text"
options={
Object {
"mentionHighlight": false,
"renderer": LinkOnlyRenderer {
"options": Object {},
},
}
}
postId="post_id"
/>
</h1>
<div>
<div
className="attachment__body attachment__body--no_thumb"
<h1
class="attachment__title"
>
<Connect(ShowMore)
isAttachmentText={true}
maxHeight={200}
text="some title"
some text
</h1>
<div>
<div
class="attachment__body attachment__body--no_thumb"
>
<Connect(Markdown)
imageProps={
Object {
"onImageHeightChanged": [Function],
"onImageLoaded": [Function],
}
}
message="some title"
postId="post_id"
/>
</Connect(ShowMore)>
<div
class="post-message post-message--collapsed"
>
<div
class="post-message__text-container"
style="max-height: 200px;"
>
<p>
some title
</p>
</div>
</div>
</div>
<div
style="clear: both;"
/>
</div>
<div
style={
Object {
"clear": "both",
}
}
/>
</div>
</div>
</div>
@@ -184,60 +130,42 @@ exports[`components/post_view/embedded_bindings/embedded_binding should match sn
`;
exports[`components/post_view/embedded_bindings/embedded_binding should match snapshot when the attachment hasn't any emojis in the title 1`] = `
<div
className="attachment"
onClick={[Function]}
>
<div>
<div
className="attachment__content"
class="attachment"
>
<div
className="clearfix attachment__container attachment__container--"
class="attachment__content"
>
<h1
className="attachment__title"
<div
class="clearfix attachment__container attachment__container--"
>
<Connect(Markdown)
message="some text"
options={
Object {
"mentionHighlight": false,
"renderer": LinkOnlyRenderer {
"options": Object {},
},
}
}
postId="post_id"
/>
</h1>
<div>
<div
className="attachment__body attachment__body--no_thumb"
<h1
class="attachment__title"
>
<Connect(ShowMore)
isAttachmentText={true}
maxHeight={200}
text="some title"
some text
</h1>
<div>
<div
class="attachment__body attachment__body--no_thumb"
>
<Connect(Markdown)
imageProps={
Object {
"onImageHeightChanged": [Function],
"onImageLoaded": [Function],
}
}
message="some title"
postId="post_id"
/>
</Connect(ShowMore)>
<div
class="post-message post-message--collapsed"
>
<div
class="post-message__text-container"
style="max-height: 200px;"
>
<p>
some title
</p>
</div>
</div>
</div>
<div
style="clear: both;"
/>
</div>
<div
style={
Object {
"clear": "both",
}
}
/>
</div>
</div>
</div>
@@ -1,13 +1,14 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {shallow} from 'enzyme';
import React from 'react';
import type {AppBinding} from '@mattermost/types/apps';
import type {MessageAttachment as MessageAttachmentType} from '@mattermost/types/message_attachments';
import type {Post} from '@mattermost/types/posts';
import {renderWithContext} from 'tests/react_testing_utils';
import EmbeddedBinding from './embedded_binding';
describe('components/post_view/embedded_bindings/embedded_binding', () => {
@@ -29,9 +30,21 @@ describe('components/post_view/embedded_bindings/embedded_binding', () => {
currentRelativeTeamUrl: 'dummy_team',
};
const initialState = {
entities: {
general: {config: {}},
users: {profiles: {}},
groups: {myGroups: []},
emojis: {},
channels: {},
teams: {teams: {}},
preferences: {myPreferences: {}},
},
};
test('should match snapshot', () => {
const wrapper = shallow(<EmbeddedBinding {...baseProps}/>);
expect(wrapper).toMatchSnapshot();
const {container} = renderWithContext(<EmbeddedBinding {...baseProps}/>, initialState);
expect(container).toMatchSnapshot();
});
test('should match snapshot when the attachment has an emoji in the title', () => {
@@ -42,9 +55,9 @@ describe('components/post_view/embedded_bindings/embedded_binding', () => {
} as MessageAttachmentType,
};
const wrapper = shallow(<EmbeddedBinding {...props}/>);
const {container} = renderWithContext(<EmbeddedBinding {...props}/>, initialState);
expect(wrapper).toMatchSnapshot();
expect(container).toMatchSnapshot();
});
test('should match snapshot when the attachment hasn\'t any emojis in the title', () => {
@@ -55,9 +68,9 @@ describe('components/post_view/embedded_bindings/embedded_binding', () => {
} as MessageAttachmentType,
};
const wrapper = shallow(<EmbeddedBinding {...props}/>);
const {container} = renderWithContext(<EmbeddedBinding {...props}/>, initialState);
expect(wrapper).toMatchSnapshot();
expect(container).toMatchSnapshot();
});
test('should match snapshot when the attachment has a link in the title', () => {
@@ -68,8 +81,8 @@ describe('components/post_view/embedded_bindings/embedded_binding', () => {
} as MessageAttachmentType,
};
const wrapper = shallow(<EmbeddedBinding {...props}/>);
const {container} = renderWithContext(<EmbeddedBinding {...props}/>, initialState);
expect(wrapper).toMatchSnapshot();
expect(container).toMatchSnapshot();
});
});
@@ -1,15 +1,36 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {shallow} from 'enzyme';
import React from 'react';
import type {AppBinding, AppCallResponse} from '@mattermost/types/apps';
import type {Post} from '@mattermost/types/posts';
import {shallowWithIntl} from 'tests/helpers/intl-test-helper';
import {act, renderWithContext, screen} from 'tests/react_testing_utils';
import SelectBinding, {RawSelectBinding} from './select_binding';
import SelectBinding from './select_binding';
type MockOnSelected = ((selected: {text: string; value: string}) => void) | undefined;
let mockOnSelected: MockOnSelected;
jest.mock('components/autocomplete_selector', () => {
return {
__esModule: true,
default: (props: any) => {
mockOnSelected = props.onSelected;
return (
<div data-testid='autoCompleteSelector'>
<input
placeholder={props.placeholder}
value={props.value || ''}
readOnly={true}
/>
</div>
);
},
};
});
describe('components/post_view/embedded_bindings/select_binding', () => {
const post = {
@@ -73,16 +94,30 @@ describe('components/post_view/embedded_bindings/select_binding', () => {
},
};
const intl = {
formatMessage: (message: {id: string; defaultMessage: string}) => {
return message.defaultMessage;
const initialState = {
entities: {
general: {config: {}},
users: {profiles: {}},
groups: {myGroups: []},
emojis: {},
channels: {},
teams: {teams: {}},
preferences: {myPreferences: {}},
},
} as any;
};
beforeEach(() => {
mockOnSelected = undefined;
});
test('should start with nothing selected', () => {
const wrapper = shallowWithIntl(<SelectBinding {...baseProps}/>);
renderWithContext(<SelectBinding {...baseProps}/>, initialState);
expect(wrapper.state()).toMatchObject({});
const selector = screen.getByTestId('autoCompleteSelector');
expect(selector).toBeInTheDocument();
const input = selector.querySelector('input');
expect(input).toHaveValue('');
});
describe('handleSelected', () => {
@@ -109,14 +144,15 @@ describe('components/post_view/embedded_bindings/select_binding', () => {
postEphemeralCallResponseForPost: jest.fn(),
openAppsModal: jest.fn(),
},
intl,
};
const wrapper = shallow<RawSelectBinding>(<RawSelectBinding {...props}/>);
renderWithContext(<SelectBinding {...props}/>, initialState);
await wrapper.instance().handleSelected({
text: 'Option 1',
value: 'option1',
await act(async () => {
mockOnSelected!({
text: 'Option 1',
value: 'option1',
});
});
expect(props.actions.getChannel).toHaveBeenCalledWith('some_channel_id');
@@ -167,14 +203,15 @@ describe('components/post_view/embedded_bindings/select_binding', () => {
postEphemeralCallResponseForPost: jest.fn(),
openAppsModal: jest.fn(),
},
intl,
};
const wrapper = shallow<RawSelectBinding>(<RawSelectBinding {...props}/>);
renderWithContext(<SelectBinding {...props}/>, initialState);
await wrapper.instance().handleSelected({
text: 'Option 1',
value: 'option1',
await act(async () => {
mockOnSelected!({
text: 'Option 1',
value: 'option1',
});
});
expect(props.actions.postEphemeralCallResponseForPost).toHaveBeenCalledWith(errorResponse, 'The error', post);
@@ -1,76 +1,42 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`components/post_view/post_list snapshot for loading when there are no posts 1`] = `
<div
className="post-list-holder-by-time"
>
<div>
<div
className="post-list__table"
class="post-list-holder-by-time"
>
<div
className="post-list__content"
id="virtualizedPostListContent"
class="post-list__table"
>
<PostList
actions={
Object {
"canLoadMorePosts": [Function],
"changeUnreadChunkTimeStamp": [MockFunction],
"loadNewerPosts": [Function],
"loadOlderPosts": [Function],
"toggleShouldStartFromBottomWhenUnread": [MockFunction],
"updateNewMessagesAtInChannel": [MockFunction],
}
}
atLatestPost={false}
autoRetryEnable={true}
channelId="fake-id"
isChannelAutotranslated={false}
isMobileView={false}
lastViewedAt={1532345226632}
loadingNewerPosts={false}
loadingOlderPosts={false}
postListIds={Array []}
shouldStartFromBottomWhenUnread={false}
/>
<div
class="post-list__content"
id="virtualizedPostListContent"
>
<div
data-testid="virt-post-list"
/>
</div>
</div>
</div>
</div>
`;
exports[`components/post_view/post_list snapshot with couple of posts 1`] = `
<div
className="post-list-holder-by-time"
>
<div>
<div
className="post-list__table"
class="post-list-holder-by-time"
>
<div
className="post-list__content"
id="virtualizedPostListContent"
class="post-list__table"
>
<PostList
actions={
Object {
"canLoadMorePosts": [Function],
"changeUnreadChunkTimeStamp": [MockFunction],
"loadNewerPosts": [Function],
"loadOlderPosts": [Function],
"toggleShouldStartFromBottomWhenUnread": [MockFunction],
"updateNewMessagesAtInChannel": [MockFunction],
}
}
atLatestPost={false}
autoRetryEnable={true}
channelId="fake-id"
isChannelAutotranslated={false}
isMobileView={false}
lastViewedAt={1532345226632}
loadingNewerPosts={false}
loadingOlderPosts={false}
postListIds={Array []}
shouldStartFromBottomWhenUnread={false}
/>
<div
class="post-list__content"
id="virtualizedPostListContent"
>
<div
data-testid="virt-post-list"
/>
</div>
</div>
</div>
</div>
@@ -1,15 +1,43 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {shallow} from 'enzyme';
import React from 'react';
import VirtPostList from 'components/post_view/post_list_virtualized/post_list_virtualized';
import {renderWithContext, act, runPostRenderAct} from 'tests/react_testing_utils';
import {PostRequestTypes} from 'utils/constants';
import PostList, {MAX_EXTRA_PAGES_LOADED} from './post_list';
let lastVirtPostListProps: any = null;
jest.mock('components/post_view/post_list_virtualized/post_list_virtualized', () => ({
__esModule: true,
default: (props: any) => {
lastVirtPostListProps = props;
return <div data-testid='virt-post-list'/>;
},
}));
jest.mock('components/loading_screen', () => ({
__esModule: true,
default: ({centered}: {centered?: boolean}) => (
<div
data-testid='loading-screen'
data-centered={centered}
>{'Loading'}</div>
),
}));
jest.mock('actions/telemetry_actions.jsx', () => ({
clearMarks: jest.fn(),
mark: jest.fn(),
}));
jest.mock('utils/performance_telemetry', () => ({
Mark: {},
Measure: {},
measureAndReport: jest.fn(),
}));
const actionsProp = {
loadPostsAround: jest.fn().mockImplementation(() => Promise.resolve({atLatestMessage: true, atOldestmessage: true})),
loadUnreads: jest.fn().mockImplementation(() => Promise.resolve({atLatestMessage: true, atOldestmessage: true})),
@@ -51,163 +79,288 @@ const baseProps = {
};
describe('components/post_view/post_list', () => {
beforeEach(() => {
jest.clearAllMocks();
lastVirtPostListProps = null;
});
it('snapshot for loading when there are no posts', () => {
const wrapper = shallow(
const {container} = renderWithContext(
<PostList {...{...baseProps, postListIds: []}}/>,
);
expect(wrapper).toMatchSnapshot();
expect(container).toMatchSnapshot();
});
it('snapshot with couple of posts', () => {
const postIds = createFakePosIds(2);
const wrapper = shallow(
const {container} = renderWithContext(
<PostList {...{...baseProps, postListIds: postIds}}/>,
);
expect(wrapper).toMatchSnapshot();
expect(container).toMatchSnapshot();
});
it('Should call postsOnLoad', async () => {
const emptyPostList: string[] = [];
const wrapper = shallow<PostList>(
<PostList {...{...baseProps, postListIds: emptyPostList}}/>,
const ref = React.createRef<PostList>();
renderWithContext(
<PostList
ref={ref}
{...{...baseProps, postListIds: emptyPostList}}
/>,
);
expect(actionsProp.loadUnreads).toHaveBeenCalledWith(baseProps.channelId);
await wrapper.instance().postsOnLoad('undefined');
expect(wrapper.state('loadingNewerPosts')).toBe(false);
expect(wrapper.state('loadingOlderPosts')).toBe(false);
await act(async () => {
await ref.current!.postsOnLoad('undefined');
});
expect(ref.current!.state.loadingNewerPosts).toBe(false);
expect(ref.current!.state.loadingOlderPosts).toBe(false);
});
it('Should not call loadUnreads if isPrefetchingInProcess is true', async () => {
const emptyPostList: string[] = [];
shallow(<PostList {...{...baseProps, postListIds: emptyPostList, isPrefetchingInProcess: true}}/>);
renderWithContext(
<PostList
{...{...baseProps, postListIds: emptyPostList, isPrefetchingInProcess: true}}
/>,
);
expect(actionsProp.loadUnreads).not.toHaveBeenCalledWith(baseProps.channelId);
});
it('Should call for before and afterPosts', async () => {
const postIds = createFakePosIds(2);
const wrapper = shallow<PostList>(
<PostList {...{...baseProps, postListIds: postIds}}/>,
const ref = React.createRef<PostList>();
renderWithContext(
<PostList
ref={ref}
{...{...baseProps, postListIds: postIds}}
/>,
);
wrapper.find(VirtPostList).prop('actions').loadOlderPosts();
expect(wrapper.state('loadingOlderPosts')).toEqual(true);
expect(actionsProp.loadPosts).toHaveBeenCalledWith({channelId: baseProps.channelId, postId: postIds[postIds.length - 1], type: PostRequestTypes.BEFORE_ID, perPage: 30});
await wrapper.instance().callLoadPosts('undefined', 'undefined', undefined, 30);
expect(wrapper.state('loadingOlderPosts')).toBe(false);
await runPostRenderAct();
wrapper.find(VirtPostList).prop('actions').loadNewerPosts();
expect(wrapper.state('loadingNewerPosts')).toEqual(true);
// Use deferred promise to capture intermediate loading state
let resolveOlderLoadPosts!: (value: any) => void;
actionsProp.loadPosts.mockImplementationOnce(() => new Promise((resolve) => {
resolveOlderLoadPosts = resolve;
}));
await act(async () => {
lastVirtPostListProps.actions.loadOlderPosts();
});
expect(ref.current!.state.loadingOlderPosts).toEqual(true);
expect(actionsProp.loadPosts).toHaveBeenCalledWith({channelId: baseProps.channelId, postId: postIds[postIds.length - 1], type: PostRequestTypes.BEFORE_ID, perPage: 30});
await act(async () => {
resolveOlderLoadPosts({moreToLoad: false});
});
expect(ref.current!.state.loadingOlderPosts).toBe(false);
let resolveNewerLoadPosts!: (value: any) => void;
actionsProp.loadPosts.mockImplementationOnce(() => new Promise((resolve) => {
resolveNewerLoadPosts = resolve;
}));
await act(async () => {
lastVirtPostListProps.actions.loadNewerPosts();
});
expect(ref.current!.state.loadingNewerPosts).toEqual(true);
expect(actionsProp.loadPosts).toHaveBeenCalledWith({channelId: baseProps.channelId, postId: postIds[0], type: PostRequestTypes.AFTER_ID, perPage: 30});
await wrapper.instance().callLoadPosts('undefined', 'undefined', undefined, 30);
expect(wrapper.state('loadingNewerPosts')).toBe(false);
await act(async () => {
resolveNewerLoadPosts({moreToLoad: false});
});
expect(ref.current!.state.loadingNewerPosts).toBe(false);
});
it('VirtPostList Should have formattedPostIds as prop', async () => {
const postIds = createFakePosIds(2);
const wrapper = shallow(
renderWithContext(
<PostList {...{...baseProps, postListIds: postIds}}/>,
);
const formattedPostIds = wrapper.find(VirtPostList).prop('postListIds');
expect(formattedPostIds).toEqual([]);
expect(lastVirtPostListProps.postListIds).toEqual([]);
});
it('getOldestVisiblePostId and getLatestVisiblePostId should return based on postListIds', async () => {
const postIds = createFakePosIds(10);
const formattedPostIds = ['1', '2'];
const wrapper = shallow<PostList>(
<PostList {...{...baseProps, postListIds: postIds, formattedPostIds}}/>,
const ref = React.createRef<PostList>();
renderWithContext(
<PostList
ref={ref}
{...{...baseProps, postListIds: postIds, formattedPostIds}}
/>,
);
const instance = wrapper.instance();
expect(instance.getOldestVisiblePostId()).toEqual('123410');
expect(instance.getLatestVisiblePostId()).toEqual('12341');
expect(ref.current!.getOldestVisiblePostId()).toEqual('123410');
expect(ref.current!.getLatestVisiblePostId()).toEqual('12341');
});
it('Should call for permalink posts', async () => {
const focusedPostId = 'new';
const wrapper = shallow(
<PostList {...{...baseProps, focusedPostId}}/>,
const ref = React.createRef<PostList>();
renderWithContext(
<PostList
ref={ref}
{...{...baseProps, focusedPostId}}
/>,
);
expect(actionsProp.loadPostsAround).toHaveBeenCalledWith(baseProps.channelId, focusedPostId);
await actionsProp.loadPostsAround();
expect(wrapper.state('loadingOlderPosts')).toBe(false);
expect(wrapper.state('loadingNewerPosts')).toBe(false);
await act(async () => {
await actionsProp.loadPostsAround();
});
expect(ref.current!.state.loadingOlderPosts).toBe(false);
expect(ref.current!.state.loadingNewerPosts).toBe(false);
});
it('Should call for loadLatestPosts', async () => {
const wrapper = shallow(
<PostList {...{...baseProps, postListIds: [], isFirstLoad: false}}/>,
const ref = React.createRef<PostList>();
renderWithContext(
<PostList
ref={ref}
{...{...baseProps, postListIds: [], isFirstLoad: false}}
/>,
);
expect(actionsProp.loadLatestPosts).toHaveBeenCalledWith(baseProps.channelId);
await actionsProp.loadLatestPosts();
expect(wrapper.state('loadingOlderPosts')).toBe(false);
expect(wrapper.state('loadingNewerPosts')).toBe(false);
await act(async () => {
await actionsProp.loadLatestPosts();
});
expect(ref.current!.state.loadingOlderPosts).toBe(false);
expect(ref.current!.state.loadingNewerPosts).toBe(false);
});
describe('getPostsSince', () => {
test('should call getPostsSince on channel switch', () => {
test('should call getPostsSince on channel switch', async () => {
const postIds = createFakePosIds(2);
shallow(<PostList {...{...baseProps, isFirstLoad: false, postListIds: postIds, latestPostTimeStamp: 1234}}/>);
renderWithContext(
<PostList
{...{...baseProps, isFirstLoad: false, postListIds: postIds, latestPostTimeStamp: 1234}}
/>,
);
expect(actionsProp.syncPostsInChannel).toHaveBeenCalledWith(baseProps.channelId, 1234, false);
});
});
describe('canLoadMorePosts', () => {
test('Should not call loadLatestPosts if postListIds is empty', async () => {
const wrapper = shallow(<PostList {...{...baseProps, isFirstLoad: false, postListIds: []}}/>);
const ref = React.createRef<PostList>();
renderWithContext(
<PostList
ref={ref}
{...{...baseProps, isFirstLoad: false, postListIds: []}}
/>,
);
expect(actionsProp.loadLatestPosts).toHaveBeenCalledWith(baseProps.channelId);
await actionsProp.loadLatestPosts();
expect(wrapper.state('loadingOlderPosts')).toBe(false);
expect(wrapper.state('loadingNewerPosts')).toBe(false);
await act(async () => {
await actionsProp.loadLatestPosts();
});
expect(ref.current!.state.loadingOlderPosts).toBe(false);
expect(ref.current!.state.loadingNewerPosts).toBe(false);
});
test('Should not call loadPosts if olderPosts or newerPosts are loading', async () => {
const postIds = createFakePosIds(2);
const wrapper = shallow(<PostList {...{...baseProps, isFirstLoad: false, postListIds: postIds}}/>);
wrapper.setState({loadingOlderPosts: true});
wrapper.find(VirtPostList).prop('actions').canLoadMorePosts(undefined);
const ref = React.createRef<PostList>();
renderWithContext(
<PostList
ref={ref}
{...{...baseProps, isFirstLoad: false, postListIds: postIds}}
/>,
);
act(() => {
ref.current!.setState({loadingOlderPosts: true});
});
await act(async () => {
lastVirtPostListProps.actions.canLoadMorePosts(undefined);
});
expect(actionsProp.loadPosts).not.toHaveBeenCalled();
wrapper.setState({loadingOlderPosts: false});
wrapper.setState({loadingNewerPosts: true});
wrapper.find(VirtPostList).prop('actions').canLoadMorePosts(undefined);
act(() => {
ref.current!.setState({loadingOlderPosts: false});
});
act(() => {
ref.current!.setState({loadingNewerPosts: true});
});
await act(async () => {
lastVirtPostListProps.actions.canLoadMorePosts(undefined);
});
expect(actionsProp.loadPosts).not.toHaveBeenCalled();
});
test('Should not call loadPosts if there were more than MAX_EXTRA_PAGES_LOADED', async () => {
const postIds = createFakePosIds(2);
const wrapper = shallow<PostList>(<PostList {...{...baseProps, isFirstLoad: false, postListIds: postIds}}/>);
wrapper.instance().extraPagesLoaded = MAX_EXTRA_PAGES_LOADED + 1;
wrapper.find(VirtPostList).prop('actions').canLoadMorePosts(undefined);
const ref = React.createRef<PostList>();
renderWithContext(
<PostList
ref={ref}
{...{...baseProps, isFirstLoad: false, postListIds: postIds}}
/>,
);
ref.current!.extraPagesLoaded = MAX_EXTRA_PAGES_LOADED + 1;
await act(async () => {
lastVirtPostListProps.actions.canLoadMorePosts(undefined);
});
expect(actionsProp.loadPosts).not.toHaveBeenCalled();
});
test('Should call getPostsBefore if not all older posts are loaded', async () => {
const postIds = createFakePosIds(2);
const wrapper = shallow(<PostList {...{...baseProps, isFirstLoad: false, postListIds: postIds}}/>);
wrapper.setProps({atOldestPost: false});
wrapper.find(VirtPostList).prop('actions').canLoadMorePosts(undefined);
const ref = React.createRef<PostList>();
const {rerender} = renderWithContext(
<PostList
ref={ref}
{...{...baseProps, isFirstLoad: false, postListIds: postIds}}
/>,
);
await act(async () => {});
rerender(
<PostList
ref={ref}
{...{...baseProps, isFirstLoad: false, postListIds: postIds, atOldestPost: false}}
/>,
);
await act(async () => {
lastVirtPostListProps.actions.canLoadMorePosts(undefined);
});
expect(actionsProp.loadPosts).toHaveBeenCalledWith({channelId: baseProps.channelId, postId: postIds[postIds.length - 1], type: PostRequestTypes.BEFORE_ID, perPage: 200});
});
test('Should call getPostsAfter if all older posts are loaded and not newerPosts', async () => {
const postIds = createFakePosIds(2);
const wrapper = shallow(<PostList {...{...baseProps, isFirstLoad: false, postListIds: postIds}}/>);
wrapper.setProps({atOldestPost: true});
wrapper.find(VirtPostList).prop('actions').canLoadMorePosts(undefined);
const ref = React.createRef<PostList>();
const {rerender} = renderWithContext(
<PostList
ref={ref}
{...{...baseProps, isFirstLoad: false, postListIds: postIds}}
/>,
);
await act(async () => {});
rerender(
<PostList
ref={ref}
{...{...baseProps, isFirstLoad: false, postListIds: postIds, atOldestPost: true}}
/>,
);
await act(async () => {
lastVirtPostListProps.actions.canLoadMorePosts(undefined);
});
expect(actionsProp.loadPosts).toHaveBeenCalledWith({channelId: baseProps.channelId, postId: postIds[0], type: PostRequestTypes.AFTER_ID, perPage: 30});
});
test('Should call getPostsAfter canLoadMorePosts is requested with AFTER_ID', async () => {
const postIds = createFakePosIds(2);
const wrapper = shallow(<PostList {...{...baseProps, isFirstLoad: false, postListIds: postIds}}/>);
wrapper.find(VirtPostList).prop('actions').canLoadMorePosts(PostRequestTypes.AFTER_ID);
renderWithContext(
<PostList
{...{...baseProps, isFirstLoad: false, postListIds: postIds}}
/>,
);
await act(async () => {
lastVirtPostListProps.actions.canLoadMorePosts(PostRequestTypes.AFTER_ID);
});
expect(actionsProp.loadPosts).toHaveBeenCalledWith({channelId: baseProps.channelId, postId: postIds[0], type: PostRequestTypes.AFTER_ID, perPage: 30});
});
});
@@ -215,7 +368,14 @@ describe('components/post_view/post_list', () => {
describe('Auto retry of load more posts', () => {
test('Should retry loadPosts on failure of loadPosts', async () => {
const postIds = createFakePosIds(2);
const loadPosts = jest.fn().mockImplementation(() => Promise.resolve({moreToLoad: true, error: {}}));
// Use deferred first call to capture intermediate loading state
let resolveFirstCall!: (value: any) => void;
const loadPosts = jest.fn().
mockImplementationOnce(() => new Promise((resolve) => {
resolveFirstCall = resolve;
})).
mockImplementation(() => Promise.resolve({moreToLoad: true, error: {}}));
const props = {
...baseProps,
postListIds: postIds,
@@ -225,17 +385,31 @@ describe('components/post_view/post_list', () => {
},
};
const wrapper = shallow(
<PostList {...props}/>,
const ref = React.createRef<PostList>();
renderWithContext(
<PostList
ref={ref}
{...props}
/>,
);
wrapper.find(VirtPostList).prop('actions').loadOlderPosts();
expect(wrapper.state('loadingOlderPosts')).toEqual(true);
await runPostRenderAct();
await act(async () => {
lastVirtPostListProps.actions.loadOlderPosts();
});
expect(ref.current!.state.loadingOlderPosts).toEqual(true);
expect(loadPosts).toHaveBeenCalledTimes(1);
expect(loadPosts).toHaveBeenCalledWith({channelId: baseProps.channelId, postId: postIds[postIds.length - 1], type: PostRequestTypes.BEFORE_ID, perPage: 30});
await loadPosts();
expect(wrapper.state('loadingOlderPosts')).toBe(false);
expect(loadPosts).toHaveBeenCalledTimes(3);
// Resolve first call with error to trigger retries
await act(async () => {
resolveFirstCall({moreToLoad: true, error: {}});
});
expect(ref.current!.state.loadingOlderPosts).toBe(false);
// 1 original + 3 retries = 4 total calls
expect(loadPosts).toHaveBeenCalledTimes(4);
});
});
@@ -243,21 +417,31 @@ describe('components/post_view/post_list', () => {
test('Should call markChannelAsReadAndViewed on postsOnLoad', async () => {
const emptyPostList: string[] = [];
const wrapper = shallow<PostList>(
<PostList {...{...baseProps, postListIds: emptyPostList}}/>,
const ref = React.createRef<PostList>();
renderWithContext(
<PostList
ref={ref}
{...{...baseProps, postListIds: emptyPostList}}
/>,
);
await wrapper.instance().postsOnLoad('undefined');
await act(async () => {
await ref.current!.postsOnLoad('undefined');
});
expect(actionsProp.markChannelAsRead).toHaveBeenCalledWith(baseProps.channelId);
});
test('Should not call markChannelAsReadAndViewed as it is a permalink', async () => {
const emptyPostList: string[] = [];
const focusedPostId = 'new';
shallow(
<PostList {...{...baseProps, postListIds: emptyPostList, focusedPostId}}/>,
renderWithContext(
<PostList
{...{...baseProps, postListIds: emptyPostList, focusedPostId}}
/>,
);
await actionsProp.loadPostsAround();
await act(async () => {
await actionsProp.loadPostsAround();
});
expect(actionsProp.markChannelAsRead).not.toHaveBeenCalled();
});
});
@@ -275,12 +459,14 @@ describe('components/post_view/post_list', () => {
},
};
const wrapper = shallow(
renderWithContext(
<PostList {...props}/>,
);
// Trigger user scroll up
wrapper.find(VirtPostList).prop('actions').loadOlderPosts();
// Trigger user scroll up (async setState + loadPosts; wrap in act, not render)
await act(async () => {
await lastVirtPostListProps.actions.loadOlderPosts();
});
expect(loadPosts).toHaveBeenCalledWith({
channelId: baseProps.channelId,
@@ -303,12 +489,14 @@ describe('components/post_view/post_list', () => {
},
};
const wrapper = shallow(
renderWithContext(
<PostList {...props}/>,
);
// Trigger auto-loading via canLoadMorePosts
await wrapper.find(VirtPostList).prop('actions').canLoadMorePosts(PostRequestTypes.BEFORE_ID);
await act(async () => {
await lastVirtPostListProps.actions.canLoadMorePosts(PostRequestTypes.BEFORE_ID);
});
expect(loadPosts).toHaveBeenCalledWith({
channelId: baseProps.channelId,
@@ -330,12 +518,13 @@ describe('components/post_view/post_list', () => {
},
};
const wrapper = shallow(
renderWithContext(
<PostList {...props}/>,
);
// Trigger user scroll down
wrapper.find(VirtPostList).prop('actions').loadNewerPosts();
await act(async () => {
await lastVirtPostListProps.actions.loadNewerPosts();
});
expect(loadPosts).toHaveBeenCalledWith({
channelId: baseProps.channelId,
@@ -358,12 +547,13 @@ describe('components/post_view/post_list', () => {
},
};
const wrapper = shallow(
renderWithContext(
<PostList {...props}/>,
);
// Trigger auto-loading via canLoadMorePosts
wrapper.find(VirtPostList).prop('actions').canLoadMorePosts(PostRequestTypes.BEFORE_ID);
await act(async () => {
await lastVirtPostListProps.actions.canLoadMorePosts(PostRequestTypes.BEFORE_ID);
});
// Should use auto-load page size (200 posts)
expect(loadPosts).toHaveBeenCalledWith(expect.objectContaining({
@@ -1,140 +1,127 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`components/post_view/post_list_row should have class hideAnimation for NEWER_MESSAGES_LOADER if loadingNewerPosts is false 1`] = `
<div
className="loading-screen"
>
<div>
<div
className="loading__content hideAnimation"
class="loading-screen"
>
<div
className="round round-1"
/>
<div
className="round round-2"
/>
<div
className="round round-3"
/>
class="loading__content hideAnimation"
>
<div
class="round round-1"
/>
<div
class="round round-2"
/>
<div
class="round round-3"
/>
</div>
</div>
</div>
`;
exports[`components/post_view/post_list_row should have class hideAnimation for OLDER_MESSAGES_LOADER if loadingOlderPosts is false 1`] = `
<div
className="loading-screen"
>
<div>
<div
className="loading__content hideAnimation"
class="loading-screen"
>
<div
className="round round-1"
/>
<div
className="round round-2"
/>
<div
className="round round-3"
/>
class="loading__content hideAnimation"
>
<div
class="round round-1"
/>
<div
class="round round-2"
/>
<div
class="round round-3"
/>
</div>
</div>
</div>
`;
exports[`components/post_view/post_list_row should render channel intro message 1`] = `<Connect(ChannelIntroMessage) />`;
exports[`components/post_view/post_list_row should render channel intro message 1`] = `
<div>
<div
data-testid="channel-intro-message"
>
ChannelIntroMessage
</div>
</div>
`;
exports[`components/post_view/post_list_row should render combined post 1`] = `
<Connect(Connect(ErrorBoundary(PostComponent)))
combinedId="user-activity-1234-5678"
isChannelAutotranslated={false}
isLastPost={false}
location="CENTER"
previousPostId="abcd"
shouldHighlight={false}
togglePostMenu={[MockFunction]}
/>
<div>
<div
data-testid="combined-user-activity"
>
CombinedUserActivityPost: user-activity-1234-5678
</div>
</div>
`;
exports[`components/post_view/post_list_row should render date line 1`] = `
<Memo(DateSeparator)
date={1553106600000}
key="1553106600000"
/>
<div>
<div
data-testid="date-separator"
>
DateSeparator: 1553106600000
</div>
</div>
`;
exports[`components/post_view/post_list_row should render manual load messages trigger 1`] = `
<button
className="more-messages-text theme style--none color--link"
onClick={[MockFunction]}
>
<MemoizedFormattedMessage
defaultMessage="Load more messages"
id="posts_view.loadMore"
/>
</button>
<div>
<button
class="more-messages-text theme style--none color--link"
>
Load more messages
</button>
</div>
`;
exports[`components/post_view/post_list_row should render more messages loading indicator 1`] = `
<div
className="loading-screen"
>
<div>
<div
className="loading__content"
class="loading-screen"
>
<div
className="round round-1"
/>
<div
className="round round-2"
/>
<div
className="round round-3"
/>
class="loading__content"
>
<div
class="round round-1"
/>
<div
class="round round-2"
/>
<div
class="round round-3"
/>
</div>
</div>
</div>
`;
exports[`components/post_view/post_list_row should render new messages line 1`] = `
<Memo(NewMessageSeparator)
channelId="channel_id_1"
newMessagesSeparatorActions={Array []}
separatorId="start-of-new-messages-1553106600000"
/>
<div>
<div
data-testid="new-message-separator"
>
NewMessageSeparator: start-of-new-messages-1553106600000
</div>
</div>
`;
exports[`components/post_view/post_list_row should render post 1`] = `
<Connect(ErrorBoundary(PostComponent))
isChannelAutotranslated={false}
isLastPost={false}
location="CENTER"
post={
Object {
"channel_id": "",
"create_at": 0,
"delete_at": 0,
"edit_at": 0,
"hashtags": "",
"id": "post_id_1",
"is_pinned": false,
"message": "post message",
"metadata": Object {
"embeds": Array [],
"emojis": Array [],
"files": Array [],
"images": Object {},
"reactions": Array [],
},
"original_id": "",
"pending_post_id": "",
"props": Object {},
"reply_count": 0,
"root_id": "",
"type": "system_add_remove",
"update_at": 0,
"user_id": "user_id",
}
}
previousPostId="abcd"
shouldHighlight={false}
togglePostMenu={[MockFunction]}
/>
<div>
<div
data-testid="post"
>
Post: post_id_1
</div>
</div>
`;
@@ -1,7 +1,6 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {shallow} from 'enzyme';
import React from 'react';
import type {ChannelType} from '@mattermost/types/channels';
@@ -9,17 +8,42 @@ import type {CloudUsage} from '@mattermost/types/cloud';
import * as PostListUtils from 'mattermost-redux/utils/post_list';
import Post from 'components/post';
import ChannelIntroMessage from 'components/post_view/channel_intro_message/';
import CombinedUserActivityPost from 'components/post_view/combined_user_activity_post';
import DateSeparator from 'components/post_view/date_separator';
import NewMessageSeparator from 'components/post_view/new_message_separator/new_message_separator';
import {renderWithContext, screen, userEvent} from 'tests/react_testing_utils';
import {PostListRowListIds} from 'utils/constants';
import {TestHelper} from 'utils/test_helper';
import PostListRow from './post_list_row';
jest.mock('components/post', () => ({
__esModule: true,
default: (props: any) => <div data-testid='post'>{`Post: ${props.post?.id}`}</div>,
}));
jest.mock('components/post_view/channel_intro_message/', () => ({
__esModule: true,
default: () => <div data-testid='channel-intro-message'>{'ChannelIntroMessage'}</div>,
}));
jest.mock('components/post_view/combined_user_activity_post', () => ({
__esModule: true,
default: (props: any) => <div data-testid='combined-user-activity'>{`CombinedUserActivityPost: ${props.combinedId}`}</div>,
}));
jest.mock('components/post_view/date_separator', () => ({
__esModule: true,
default: (props: any) => <div data-testid='date-separator'>{`DateSeparator: ${props.date}`}</div>,
}));
jest.mock('components/post_view/new_message_separator/new_message_separator', () => ({
__esModule: true,
default: (props: any) => <div data-testid='new-message-separator'>{`NewMessageSeparator: ${props.separatorId}`}</div>,
}));
jest.mock('components/center_message_lock', () => ({
__esModule: true,
default: () => <div data-testid='center-message-lock'>{'CenterMessageLock'}</div>,
}));
describe('components/post_view/post_list_row', () => {
const defaultProps = {
listId: '1234',
@@ -52,13 +76,13 @@ describe('components/post_view/post_list_row', () => {
listId,
loadingOlderPosts: true,
};
const wrapper = shallow(
const {container} = renderWithContext(
<PostListRow {...props}/>,
);
expect(wrapper).toMatchSnapshot();
expect(container).toMatchSnapshot();
});
test('should render manual load messages trigger', () => {
test('should render manual load messages trigger', async () => {
const listId = PostListRowListIds.LOAD_OLDER_MESSAGES_TRIGGER;
const loadOlderPosts = jest.fn();
const props = {
@@ -66,11 +90,11 @@ describe('components/post_view/post_list_row', () => {
listId,
loadOlderPosts,
};
const wrapper = shallow(
const {container} = renderWithContext(
<PostListRow {...props}/>,
);
expect(wrapper).toMatchSnapshot();
wrapper.prop('onClick')();
expect(container).toMatchSnapshot();
await userEvent.click(screen.getByRole('button'));
expect(loadOlderPosts).toHaveBeenCalledTimes(1);
});
@@ -99,11 +123,11 @@ describe('components/post_view/post_list_row', () => {
listId,
};
const wrapper = shallow(
const {container} = renderWithContext(
<PostListRow {...props}/>,
);
expect(wrapper).toMatchSnapshot();
expect(wrapper.find(ChannelIntroMessage).exists()).toBe(true);
expect(container).toMatchSnapshot();
expect(screen.getByTestId('channel-intro-message')).toBeInTheDocument();
});
test('should render new messages line', () => {
@@ -112,11 +136,11 @@ describe('components/post_view/post_list_row', () => {
...defaultProps,
listId,
};
const wrapper = shallow(
const {container} = renderWithContext(
<PostListRow {...props}/>,
);
expect(wrapper).toMatchSnapshot();
expect(wrapper.find(NewMessageSeparator).exists()).toBe(true);
expect(container).toMatchSnapshot();
expect(screen.getByTestId('new-message-separator')).toBeInTheDocument();
});
test('should render date line', () => {
@@ -125,11 +149,11 @@ describe('components/post_view/post_list_row', () => {
...defaultProps,
listId,
};
const wrapper = shallow(
const {container} = renderWithContext(
<PostListRow {...props}/>,
);
expect(wrapper).toMatchSnapshot();
expect(wrapper.find(DateSeparator).exists()).toBe(true);
expect(container).toMatchSnapshot();
expect(screen.getByTestId('date-separator')).toBeInTheDocument();
});
test('should render combined post', () => {
@@ -139,11 +163,11 @@ describe('components/post_view/post_list_row', () => {
listId: `${PostListUtils.COMBINED_USER_ACTIVITY}1234-5678`,
previousListId: 'abcd',
};
const wrapper = shallow(
const {container} = renderWithContext(
<PostListRow {...props}/>,
);
expect(wrapper).toMatchSnapshot();
expect(wrapper.find(CombinedUserActivityPost).exists()).toBe(true);
expect(container).toMatchSnapshot();
expect(screen.getByTestId('combined-user-activity')).toBeInTheDocument();
});
test('should render post', () => {
@@ -153,11 +177,11 @@ describe('components/post_view/post_list_row', () => {
listId: '1234',
previousListId: 'abcd',
};
const wrapper = shallow(
const {container} = renderWithContext(
<PostListRow {...props}/>,
);
expect(wrapper).toMatchSnapshot();
expect(wrapper.find(Post).exists()).toBe(true);
expect(container).toMatchSnapshot();
expect(screen.getByTestId('post')).toBeInTheDocument();
});
test('should have class hideAnimation for OLDER_MESSAGES_LOADER if loadingOlderPosts is false', () => {
@@ -167,10 +191,10 @@ describe('components/post_view/post_list_row', () => {
listId,
loadingOlderPosts: false,
};
const wrapper = shallow(
const {container} = renderWithContext(
<PostListRow {...props}/>,
);
expect(wrapper).toMatchSnapshot();
expect(container).toMatchSnapshot();
});
test('should have class hideAnimation for NEWER_MESSAGES_LOADER if loadingNewerPosts is false', () => {
@@ -180,9 +204,9 @@ describe('components/post_view/post_list_row', () => {
listId,
loadingNewerPosts: false,
};
const wrapper = shallow(
const {container} = renderWithContext(
<PostListRow {...props}/>,
);
expect(wrapper).toMatchSnapshot();
expect(container).toMatchSnapshot();
});
});
@@ -208,32 +208,28 @@ describe('ProductNoticesModal', () => {
Date.now = jest.fn().mockReturnValue(1599868800000);
// Simulate socket disconnect
await act(async () => {
rerender(
<ProductNoticesModal
ref={ref}
{...baseProps}
socketStatus={{
...baseProps.socketStatus,
connected: false,
}}
/>,
);
});
rerender(
<ProductNoticesModal
ref={ref}
{...baseProps}
socketStatus={{
...baseProps.socketStatus,
connected: false,
}}
/>,
);
// Simulate socket reconnect
await act(async () => {
rerender(
<ProductNoticesModal
ref={ref}
{...baseProps}
socketStatus={{
...baseProps.socketStatus,
connected: true,
}}
/>,
);
});
rerender(
<ProductNoticesModal
ref={ref}
{...baseProps}
socketStatus={{
...baseProps.socketStatus,
connected: true,
}}
/>,
);
expect(baseProps.actions.getInProductNotices).toHaveBeenCalledWith(baseProps.currentTeamId, 'web', baseProps.version);
expect(baseProps.actions.getInProductNotices).toHaveBeenCalledTimes(2);
@@ -257,32 +253,28 @@ describe('ProductNoticesModal', () => {
Date.now = jest.fn().mockReturnValue(1599760196593);
// Simulate socket disconnect
await act(async () => {
rerender(
<ProductNoticesModal
ref={ref}
{...baseProps}
socketStatus={{
...baseProps.socketStatus,
connected: false,
}}
/>,
);
});
rerender(
<ProductNoticesModal
ref={ref}
{...baseProps}
socketStatus={{
...baseProps.socketStatus,
connected: false,
}}
/>,
);
// Simulate socket reconnect on the same day
await act(async () => {
rerender(
<ProductNoticesModal
ref={ref}
{...baseProps}
socketStatus={{
...baseProps.socketStatus,
connected: true,
}}
/>,
);
});
rerender(
<ProductNoticesModal
ref={ref}
{...baseProps}
socketStatus={{
...baseProps.socketStatus,
connected: true,
}}
/>,
);
expect(baseProps.actions.getInProductNotices).toHaveBeenCalledTimes(1);
});
@@ -1,38 +1,19 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`components/RhsThread should match snapshot 1`] = `
<div
className="sidebar-right__body"
id="rhsContainer"
>
<Connect(injectIntl(RhsHeaderPost))
channel={
Object {
"create_at": 0,
"creator_id": "",
"delete_at": 0,
"display_name": "",
"group_constrained": false,
"header": "",
"id": "channel_id",
"last_post_at": 0,
"last_root_post_at": 0,
"name": "",
"purpose": "",
"scheme_id": "",
"status": "",
"team_id": "team_id",
"teammate_id": "",
"type": "O",
"update_at": 0,
}
}
rootPostId="id"
/>
<Connect(ThreadViewer)
isThreadView={false}
rootPostId="id"
useRelativeTimestamp={true}
/>
<div>
<div
class="sidebar-right__body"
id="rhsContainer"
>
<div
data-root-post-id="id"
data-testid="rhs-header-post"
/>
<div
data-root-post-id="id"
data-testid="thread-viewer"
/>
</div>
</div>
`;
@@ -1,24 +1,30 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {shallow} from 'enzyme';
import React from 'react';
import type {Channel} from '@mattermost/types/channels';
import type {Post} from '@mattermost/types/posts';
import type {UserProfile} from '@mattermost/types/users';
import {renderWithContext} from 'tests/react_testing_utils';
import {TestHelper} from 'utils/test_helper';
import RhsThread from './rhs_thread';
const mockDispatch = jest.fn();
let mockState: any;
jest.mock('react-redux', () => ({
...jest.requireActual('react-redux') as typeof import('react-redux'),
useSelector: (selector: (state: typeof mockState) => unknown) => selector(mockState),
useDispatch: () => mockDispatch,
jest.mock('components/rhs_header_post', () => (props: any) => (
<div
data-testid='rhs-header-post'
data-root-post-id={props.rootPostId}
/>
));
jest.mock('components/threading/thread_viewer', () => (props: any) => (
<div
data-testid='thread-viewer'
data-root-post-id={props.rootPostId}
/>
));
jest.mock('actions/views/rhs', () => ({
closeRightHandSide: jest.fn(() => ({type: 'CLOSE_RHS'})),
}));
describe('components/RhsThread', () => {
@@ -39,34 +45,18 @@ describe('components/RhsThread', () => {
status: '',
});
const actions = {
removePost: jest.fn(),
selectPostCard: jest.fn(),
getPostThread: jest.fn(),
};
const directTeammate: UserProfile = TestHelper.getUserMock();
const currentTeam = TestHelper.getTeamMock();
const baseProps = {
posts: [post],
selected: post,
channel,
currentUserId: 'user_id',
previewCollapsed: 'false',
previewEnabled: true,
socketConnectionStatus: true,
actions,
directTeammate,
currentTeam,
fromSuppressed: false,
};
test('should match snapshot', () => {
const wrapper = shallow(
const {container} = renderWithContext(
<RhsThread {...baseProps}/>,
);
expect(wrapper).toMatchSnapshot();
expect(container).toMatchSnapshot();
});
});
@@ -1,47 +1,38 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`CommandSuggestion should match snapshot 1`] = `
<SuggestionContainer
aria-describedby="test-suggestion-description"
aria-labelledby="test-suggestion-label"
id="test-suggestion"
isSelection={true}
item={
Object {
"Complete": "/invite",
"Description": "Invite a user to a channel",
"Hint": "@[username] ~[channel]",
"IconData": "",
"Suggestion": "/invite",
}
}
matchedPretext=""
onClick={[MockFunction]}
onMouseMove={[MockFunction]}
term="/"
>
<div
className="slash-command__icon"
>
<span>
/
</span>
</div>
<div
className="slash-command__info"
<div>
<li
aria-describedby="test-suggestion-description"
aria-labelledby="test-suggestion-label"
class="suggestion-list__item suggestion--selected"
id="test-suggestion"
role="option"
tabindex="-1"
>
<div
className="slash-command__title"
id="test-suggestion-label"
class="slash-command__icon"
>
invite @[username] ~[channel]
<span>
/
</span>
</div>
<div
className="slash-command__desc"
id="test-suggestion-description"
class="slash-command__info"
>
Invite a user to a channel
<div
class="slash-command__title"
id="test-suggestion-label"
>
invite @[username] ~[channel]
</div>
<div
class="slash-command__desc"
id="test-suggestion-description"
>
Invite a user to a channel
</div>
</div>
</div>
</SuggestionContainer>
</li>
</div>
`;
@@ -1,13 +1,13 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {shallow} from 'enzyme';
import React from 'react';
import type {AutocompleteSuggestion} from '@mattermost/types/integrations';
import {Client4} from 'mattermost-redux/client';
import {renderWithContext} from 'tests/react_testing_utils';
import * as UserAgent from 'utils/user_agent';
import CommandProvider, {commandsGroup, CommandSuggestion} from './command_provider';
@@ -32,12 +32,14 @@ describe('CommandSuggestion', () => {
};
test('should match snapshot', () => {
const wrapper = shallow(
const {container} = renderWithContext(
<CommandSuggestion {...baseProps}/>,
);
expect(wrapper).toMatchSnapshot();
expect(wrapper.find('.slash-command__title').first().text()).toEqual('invite @[username] ~[channel]');
expect(container).toMatchSnapshot();
const titleElement = container.querySelector('.slash-command__title');
expect(titleElement?.textContent).toEqual('invite @[username] ~[channel]');
});
});
+69 -12
View File
@@ -1,13 +1,64 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {shallow} from 'enzyme';
import React from 'react';
import Textbox, {type Props} from 'components/textbox/textbox';
import {renderWithContext} from 'tests/react_testing_utils';
import {TestHelper} from 'utils/test_helper';
jest.mock('components/suggestion/suggestion_box', () => {
// Use require inside the factory: jest hoists mocks before imports run, and babel-plugin-jest-hoist
// forbids non-mock-prefixed outer variables (e.g. React). Cast so forwardRef accepts generics (TS2347).
const react = require('react') as typeof import('react'); // eslint-disable-line @typescript-eslint/no-var-requires, global-require
const MockSuggestionBox = react.forwardRef<HTMLTextAreaElement, any>((props: any, ref: any) => (
<textarea
ref={ref}
id={props.id}
className={props.className}
placeholder={props.placeholder}
value={props.value}
disabled={props.disabled}
data-testid='suggestion-box'
readOnly={true}
style={props.style}
/>
));
MockSuggestionBox.displayName = 'SuggestionBox';
return {__esModule: true, default: MockSuggestionBox};
});
jest.mock('components/post_markdown', () => {
return {
__esModule: true,
default: (props: any) => <div data-testid='post-markdown'>{props.message}</div>,
};
});
jest.mock('components/suggestion/suggestion_list', () => {
return {
__esModule: true,
default: () => null,
};
});
jest.mock('components/suggestion/at_mention_provider', () => {
return jest.fn().mockImplementation(() => ({setProps: jest.fn()}));
});
jest.mock('components/suggestion/channel_mention_provider', () => {
return jest.fn().mockImplementation(() => ({setProps: jest.fn()}));
});
jest.mock('components/suggestion/command_provider/command_provider', () => {
return jest.fn().mockImplementation(() => ({setProps: jest.fn()}));
});
jest.mock('components/suggestion/command_provider/app_provider', () => {
return jest.fn().mockImplementation(() => ({setProps: jest.fn()}));
});
jest.mock('components/suggestion/emoticon_provider', () => {
return jest.fn().mockImplementation(() => ({}));
});
jest.mock('components/remove_flagged_message_confirmation_modal/remove_flagged_message_confirmation_modal', () => {
return jest.fn(() => <div data-testid='keep-remove-flagged-message-confirmation-modal'>{'KeepRemoveFlaggedMessageConfirmationModal Mock'}</div>);
});
@@ -52,10 +103,10 @@ describe('components/TextBox', () => {
supportsCommands: false,
};
const wrapper = shallow(
const {container} = renderWithContext(
<Textbox {...props}/>,
);
expect(wrapper).toMatchSnapshot();
expect(container).toMatchSnapshot();
});
test('should match snapshot with additional, optional props', () => {
@@ -85,10 +136,10 @@ describe('components/TextBox', () => {
openWhenEmpty: true,
};
const wrapper = shallow(
const {container} = renderWithContext(
<Textbox {...props}/>,
);
expect(wrapper).toMatchSnapshot();
expect(container).toMatchSnapshot();
});
test('should throw error when value is too long', () => {
@@ -112,12 +163,12 @@ describe('components/TextBox', () => {
handlePostError,
};
const wrapper = shallow(
const {container} = renderWithContext(
<Textbox {...props}/>,
);
expect(gotError).toEqual(true);
expect(wrapper).toMatchSnapshot();
expect(container).toMatchSnapshot();
});
test('should throw error when new property is too long', () => {
@@ -141,14 +192,20 @@ describe('components/TextBox', () => {
handlePostError,
};
const wrapper = shallow(
const {container, rerender} = renderWithContext(
<Textbox {...props}/>,
);
wrapper.setProps({value: 'some test text that exceeds char limit'});
wrapper.update();
expect(gotError).toEqual(true);
const newProps = {
...props,
value: 'some test text that exceeds char limit',
};
expect(wrapper).toMatchSnapshot();
rerender(
<Textbox {...newProps}/>,
);
expect(gotError).toEqual(true);
expect(container).toMatchSnapshot();
});
});
@@ -1,110 +1,113 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`components/threading/global_threads/thread_list should match snapshot 1`] = `
<div
className="ThreadList"
id="threads-list-container"
tabIndex={0}
>
<Header
heading={
<body>
<div>
<div
class="ThreadList"
id="threads-list-container"
tabindex="0"
>
<div
aria-label="Filter visible threads"
aria-orientation="horizontal"
className="tab-buttons-list"
role="tablist"
class="Header"
id="tutorial-threads-mobile-header"
>
<div
className="tab-button-wrapper"
class="left"
>
<Memo(Button)
aria-controls="threads-list"
aria-selected={true}
className="Button___large Margined"
id="threads-list-filter-none"
isActive={true}
onClick={[Function]}
onKeyDown={[Function]}
role="tab"
tabIndex={0}
<div
aria-label="Filter visible threads"
aria-orientation="horizontal"
class="tab-buttons-list"
role="tablist"
>
<Memo(MemoizedFormattedMessage)
defaultMessage="Followed threads"
id="globalThreads.heading"
/>
</Memo(Button)>
<div
class="tab-button-wrapper"
>
<button
aria-controls="threads-list"
aria-selected="true"
class="Button Button___transparent is-active Button___large Margined"
id="threads-list-filter-none"
role="tab"
tabindex="0"
>
<span
class="Button_label"
>
Followed threads
</span>
</button>
</div>
<div
class="tab-button-wrapper"
id="threads-list-unread-button"
>
<button
aria-controls="threads-list"
aria-selected="false"
class="Button Button___transparent Button___large Margined"
id="threads-list-filter-unread"
role="tab"
tabindex="-1"
>
<span
class="Button_label"
>
Unreads
<span
class="dot"
/>
</span>
</button>
</div>
</div>
</div>
<div
className="tab-button-wrapper"
id="threads-list-unread-button"
class="spacer"
/>
<div
class="right-anchor"
>
<Memo(Button)
aria-controls="threads-list"
aria-selected={false}
className="Button___large Margined"
hasDot={true}
id="threads-list-filter-unread"
isActive={false}
onClick={[Function]}
onKeyDown={[Function]}
role="tab"
tabIndex={-1}
>
<Memo(MemoizedFormattedMessage)
defaultMessage="Unreads"
id="threading.filters.unreads"
/>
</Memo(Button)>
</div>
</div>
}
id="tutorial-threads-mobile-header"
right={
<div
className="right-anchor"
>
<WithTooltip
title="Mark all threads as read"
>
<Memo(Button)
<button
aria-label="Mark all threads as read"
className="Button___large Button___icon"
class="Button Button___transparent Button___large Button___icon"
id="threads-list__mark-all-as-read"
marginTop={true}
onClick={[Function]}
>
<span
className="icon"
class="Button_label margin_top"
>
<PlaylistCheckIcon
size={18}
/>
<span
class="icon"
>
<svg
fill="currentColor"
height="18"
version="1.1"
viewBox="0 0 24 24"
width="18"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M14,10H2V12H14V10M14,6H2V8H14V6M2,16H10V14H2V16M21.5,11.5L23,13L16,20L11.5,15.5L13,14L16,17L21.5,11.5Z"
/>
</svg>
</span>
</span>
</Memo(Button)>
</WithTooltip>
</button>
</div>
</div>
}
/>
<div
className="threads"
data-testid="threads_list"
id="threads-list"
role="tabpanel"
>
<Memo(VirtualizedThreadList)
addNoMoreResultsItem={false}
ids={
Array [
"1",
"2",
"3",
]
}
isLoading={false}
key="threads_list_"
loadMoreItems={[Function]}
total={0}
/>
<div
class="threads"
data-testid="threads_list"
id="threads-list"
role="tabpanel"
>
<div
data-testid="virtualized-thread-list"
/>
</div>
</div>
</div>
</div>
</body>
`;
@@ -1,7 +1,37 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`components/threading/global_threads/thread_list/virtualized_thread_list should match snapshot 1`] = `
<AutoSizer>
<Component />
</AutoSizer>
<body>
<div>
<div>
<div
class="virtualized-thread-list"
style="position: relative; height: 500px; width: 300px; overflow: auto; -webkit-overflow-scrolling: touch; will-change: auto; direction: ltr;"
>
<div
style="height: 399px; width: 100%;"
>
<div
data-testid="thread-row-1"
style="position: absolute; left: 0px; top: 0px; height: 133px; width: 100%;"
>
1
</div>
<div
data-testid="thread-row-2"
style="position: absolute; left: 0px; top: 133px; height: 133px; width: 100%;"
>
2
</div>
<div
data-testid="thread-row-3"
style="position: absolute; left: 0px; top: 266px; height: 133px; width: 100%;"
>
3
</div>
</div>
</div>
</div>
</div>
</body>
`;
@@ -1,57 +1,204 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`components/threading/global_threads/thread_list/virtualized_thread_list_row should match snapshot 1`] = `
<Connect(Component)
isFirstThreadInList={false}
isSelected={false}
key="2"
style={Object {}}
threadId="2"
/>
<body>
<div>
<div
data-selected="false"
data-testid="thread-item-2"
/>
</div>
</body>
`;
exports[`components/threading/global_threads/thread_list/virtualized_thread_list_row should support item loading indicator 1`] = `
<LoadingScreen
message={<React.Fragment />}
style={Object {}}
/>
<body>
<div>
<div
class="loading-screen"
style="position: relative;"
>
<div
class="loading__content"
>
<p />
<div
class="round round-1"
/>
<div
class="round round-2"
/>
<div
class="round round-3"
/>
</div>
</div>
</div>
</body>
`;
exports[`components/threading/global_threads/thread_list/virtualized_thread_list_row should support item search guidance 1`] = `
<NoResultsIndicator
iconGraphic={
<SearchSVG
className="no-results__icon"
/>
}
layout={1}
style={
Object {
"background": "rgba(var(--center-channel-color-rgb), 0.04)",
"padding": "16px 16px 16px 24px",
}
}
subtitle={
<Memo(MemoizedFormattedMessage)
defaultMessage="If youre looking for older conversations, try searching with {searchShortcut}"
id="globalThreads.searchGuidance.subtitle"
values={
Object {
"searchShortcut": <SearchShortcut
className="thread-no-results-subtitle-shortcut"
variant="contrast"
/>,
}
}
/>
}
subtitleClassName="thread-no-results-subtitle"
title={
<Memo(MemoizedFormattedMessage)
defaultMessage="Thats the end of the list"
id="globalThreads.searchGuidance.title"
/>
}
titleClassName="thread-no-results-title"
/>
<body>
<div>
<div
class="no-results__wrapper horizontal-layout"
style="padding: 16px 16px 16px 24px; background: rgba(var(--center-channel-color-rgb), 0.04);"
>
<span
class="no-results__icon"
>
<svg
aria-label="Search Icon"
height="99"
role="img"
viewBox="0 0 123 99"
width="123"
>
<g>
<rect
fill="var(--button-bg)"
fill-opacity="0.12"
height="31"
rx="3.75"
width="107"
x="0.255005"
y="32"
/>
<rect
fill="var(--button-bg)"
fill-opacity="0.12"
height="27"
rx="3.75"
width="76"
x="46.255"
/>
<rect
fill="var(--button-bg)"
fill-opacity="0.12"
height="27"
rx="3.75"
width="76"
x="46.255"
y="67"
/>
<path
d="M62.5814 6.60593C70.262 6.55738 77.3232 8.54646 83.755 12.5732C86.3956 14.2264 88.9301 16.223 91.3579 18.5631C94.3589 21.6787 96.7911 24.9905 98.653 28.5M62.5813 87.8542C53.6129 87.7863 45.4964 85.1063 38.255 79.7911C36.7377 78.6774 35.2587 77.4479 33.8185 76.1025C30.0784 72.0903 27.2249 67.8234 25.255 63.299M21.9572 47.2301C21.964 41.8688 22.9467 36.7921 24.9051 32C26.8843 27.1573 29.8599 22.6053 33.8322 18.3439M101.949 37C102.786 40.2642 103.206 43.6739 103.206 47.2301C103.2 51.5308 102.585 55.6211 101.358 59.5M100.495 32.5C100.769 33.1979 101.022 33.903 101.255 34.6153"
fill="none"
stroke="var(--center-channel-color)"
stroke-opacity="0.48"
/>
<path
d="M82.6551 5.72396C90.4991 9.4225 96.7179 14.8934 101.302 22.1318C103.184 25.1036 104.79 28.3733 106.12 31.9406"
fill="none"
stroke="var(--center-channel-color)"
stroke-opacity="0.48"
/>
<rect
fill="var(--center-channel-color)"
fill-opacity="0.48"
height="7.45395"
rx="1"
transform="rotate(45 89.8878 69.3516)"
width="33.9752"
x="89.8878"
y="69.3516"
/>
<rect
fill="none"
height="6.45395"
rx="0.5"
stroke="var(--center-channel-color)"
stroke-opacity="0.8"
transform="rotate(45 89.8878 70.0587)"
width="32.9752"
x="89.8878"
y="70.0587"
/>
<path
d="M62.5706 11.2232C72.1732 11.1625 80.6832 14.6952 88.0764 21.8213C95.0689 29.0809 98.5773 37.5423 98.5773 47.2299C98.5652 56.9053 95.0689 65.3789 88.0764 72.6385C80.6832 79.7646 72.1975 83.2973 62.5706 83.2366C52.9559 83.1637 44.4459 79.7039 37.077 72.8206C30.0724 65.3061 26.5761 56.7839 26.564 47.2299C26.5761 37.6759 30.0845 29.1416 37.0892 21.627C44.4702 14.7559 52.968 11.2839 62.5706 11.2232Z"
fill="var(--center-channel-bg)"
/>
<path
d="M62.5738 11.7232C72.0335 11.6634 80.418 15.1368 87.7227 22.1748C94.6257 29.3444 98.0773 37.6809 98.0773 47.2299C98.0652 56.7673 94.6251 65.1159 87.7228 72.2849C80.418 79.323 72.0576 82.7964 62.5738 82.7366C53.0959 82.6646 44.7093 79.2606 37.431 72.467C30.5135 65.0406 27.0759 56.6419 27.064 47.2294C27.0762 37.8176 30.5255 29.4072 37.4428 21.9809C44.7342 15.1984 53.109 11.783 62.5738 11.7232Z"
fill="none"
stroke="var(--center-channel-color)"
stroke-opacity="0.8"
/>
<path
clip-rule="evenodd"
d="M88.076 21.8213C80.6829 14.6952 72.1729 11.1625 62.5703 11.2232C52.9677 11.2839 44.4698 14.7559 37.0888 21.627C30.0841 29.1416 26.5757 37.6759 26.5636 47.2299C26.5757 56.7839 30.072 65.3061 37.0767 72.8206C44.4455 79.7039 52.9555 83.1638 62.5703 83.2366C72.1971 83.2973 80.6829 79.7646 88.076 72.6385C95.0685 65.3789 98.5648 56.9053 98.577 47.2299C98.577 37.5423 95.0685 29.0809 88.076 21.8213ZM84.5637 25.3086C78.1852 19.1606 70.8432 16.1128 62.5586 16.1652C54.274 16.2175 46.9424 19.213 40.5745 25.1411C34.5312 31.6242 31.5043 38.9872 31.4939 47.2299C31.5043 55.4726 34.5207 62.8251 40.564 69.3083C46.9215 75.2468 54.2635 78.2318 62.5586 78.2947C70.8642 78.347 78.1852 75.2992 84.5637 69.1512C90.5965 62.888 93.6129 55.5774 93.6233 47.2299C93.6233 38.872 90.5965 31.5719 84.5637 25.3086Z"
fill="var(--center-channel-bg)"
fill-rule="evenodd"
/>
<path
clip-rule="evenodd"
d="M88.076 21.8213C80.6828 14.6952 72.1728 11.1625 62.5702 11.2232C52.9676 11.2839 44.4697 14.7559 37.0887 21.627C30.0841 29.1416 26.5757 37.6759 26.5635 47.2299C26.5757 56.7839 30.0719 65.3061 37.0766 72.8206C44.4455 79.7039 52.9555 83.1638 62.5702 83.2366C72.1971 83.2973 80.6828 79.7646 88.076 72.6385C95.0685 65.3789 98.5648 56.9053 98.5769 47.2299C98.5769 37.5423 95.0685 29.0809 88.076 21.8213ZM84.5636 25.3086C78.1852 19.1606 70.8432 16.1128 62.5585 16.1652C54.2739 16.2175 46.9424 19.213 40.5744 25.1411C34.5312 31.6242 31.5043 38.9872 31.4938 47.2299C31.5043 55.4726 34.5207 62.8251 40.564 69.3083C46.9214 75.2468 54.2634 78.2318 62.5585 78.2947C70.8641 78.347 78.1852 75.2992 84.5636 69.1512C90.5964 62.888 93.6128 55.5774 93.6233 47.2299C93.6233 38.872 90.5964 31.5719 84.5636 25.3086Z"
fill="var(--center-channel-color)"
fill-opacity="0.8"
fill-rule="evenodd"
/>
<path
d="M62.5736 11.7232C72.0333 11.6634 80.4179 15.1369 87.7225 22.1748C94.6255 29.3444 98.0771 37.681 98.0771 47.2299C98.065 56.7674 94.625 65.1159 87.7226 72.2849C80.4178 79.3231 72.0575 82.7964 62.5736 82.7366C53.0957 82.6647 44.7092 79.2607 37.4308 72.4671C30.5133 65.0407 27.0758 56.642 27.0638 47.2294C27.076 37.8176 30.5254 29.4072 37.4426 21.9809C44.734 15.1984 53.1088 11.783 62.5736 11.7232Z"
fill="none"
stroke="var(--center-channel-color)"
stroke-opacity="0.8"
/>
<circle
cx="51.255"
cy="36"
fill="var(--center-channel-color)"
fill-opacity="0.32"
r="9"
/>
<path
d="M43.255 56H85.255"
fill="none"
stroke="var(--center-channel-color)"
stroke-linecap="round"
stroke-opacity="0.8"
/>
<path
d="M43.255 63H65.255"
fill="none"
stroke="var(--center-channel-color)"
stroke-linecap="round"
stroke-opacity="0.8"
/>
<path
d="M43.255 50H60.255"
fill="none"
stroke="var(--center-channel-color)"
stroke-linecap="round"
stroke-opacity="0.8"
/>
<path
d="M65.255 50H80.255"
fill="none"
stroke="var(--center-channel-color)"
stroke-linecap="round"
stroke-opacity="0.8"
/>
</g>
</svg>
</span>
<div
class="no-results__text-container"
>
<h3
class="no-results__title thread-no-results-title"
>
Thats the end of the list
</h3>
<div
class="no-results__subtitle thread-no-results-subtitle"
>
If youre looking for older conversations, try searching with
<span>
Ctrl+F
</span>
</div>
</div>
</div>
</div>
</body>
`;
@@ -1,7 +1,6 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {shallow} from 'enzyme';
import React from 'react';
import type {ComponentProps} from 'react';
@@ -9,15 +8,11 @@ import {getThreadsForCurrentTeam} from 'mattermost-redux/actions/threads';
import {openModal} from 'actions/views/modals';
import Header from 'components/widgets/header';
import {renderWithContext, screen, userEvent} from 'tests/react_testing_utils';
import {WindowSizes} from 'utils/constants';
import {TestHelper} from 'utils/test_helper';
import ThreadList, {ThreadFilter} from './thread_list';
import VirtualizedThreadList from './virtualized_thread_list';
import Button from '../../common/button';
jest.mock('mattermost-redux/actions/threads');
jest.mock('actions/views/modals');
@@ -27,6 +22,7 @@ const mockRouting = {
currentTeamId: 'tid',
goToInChannel: jest.fn(),
select: jest.fn(),
clear: jest.fn(),
};
jest.mock('../../hooks', () => {
return {
@@ -34,6 +30,14 @@ jest.mock('../../hooks', () => {
};
});
let capturedVTLProps: any = {};
jest.mock('./virtualized_thread_list', () => {
return function MockVirtualizedThreadList(props: any) {
capturedVTLProps = props;
return <div data-testid='virtualized-thread-list'/>;
};
});
const mockDispatch = jest.fn();
let mockState: any;
@@ -87,56 +91,54 @@ describe('components/threading/global_threads/thread_list', () => {
},
},
};
capturedVTLProps = {};
mockDispatch.mockClear();
});
test('should match snapshot', () => {
const wrapper = shallow(
const {baseElement} = renderWithContext(
<ThreadList {...props}/>,
);
expect(wrapper).toMatchSnapshot();
expect(baseElement).toMatchSnapshot();
});
test('should support filter:all', () => {
const wrapper = shallow(
test('should support filter:all', async () => {
renderWithContext(
<ThreadList {...props}/>,
);
wrapper.find(Header).shallow().find(Button).first().shallow().simulate('click');
await userEvent.click(screen.getByRole('tab', {name: 'Followed threads'}));
expect(props.setFilter).toHaveBeenCalledWith('');
});
test('should support filter:unread', () => {
const wrapper = shallow(
test('should support filter:unread', async () => {
renderWithContext(
<ThreadList {...props}/>,
);
wrapper.find(Header).shallow().find(Button).find({hasDot: true}).simulate('click');
await userEvent.click(screen.getByRole('tab', {name: 'Unreads'}));
expect(props.setFilter).toHaveBeenCalledWith('unread');
});
test('should support openModal', () => {
const wrapper = shallow(
test('should support openModal', async () => {
renderWithContext(
<ThreadList {...props}/>,
);
wrapper.find(Header).shallow().find(Button).find({id: 'threads-list__mark-all-as-read'}).simulate('click');
await userEvent.click(screen.getByLabelText('Mark all threads as read'));
expect(openModal).toHaveBeenCalledTimes(1);
});
test('should support getThreads', async () => {
const setState = jest.fn();
const useStateSpy = jest.spyOn(React, 'useState');
useStateSpy.mockImplementation((init = false) => [init, setState]);
const wrapper = shallow(
renderWithContext(
<ThreadList {...props}/>,
);
const handleLoadMoreItems = wrapper.find(VirtualizedThreadList).prop('loadMoreItems');
const handleLoadMoreItems = capturedVTLProps.loadMoreItems;
const loadMoreItems = await handleLoadMoreItems(2, 3);
expect(loadMoreItems).toEqual({data: true});
expect(getThreadsForCurrentTeam).toHaveBeenCalledWith({unread: false, before: '2'});
expect(setState.mock.calls).toEqual([[true], [false], [true]]);
});
});
@@ -1,12 +1,32 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {shallow} from 'enzyme';
import React from 'react';
import type {ComponentProps} from 'react';
import {renderWithContext} from 'tests/react_testing_utils';
import VirtualizedThreadList from './virtualized_thread_list';
jest.mock('react-virtualized-auto-sizer', () => {
return function MockAutoSizer({children}: {children: (size: {height: number; width: number}) => React.ReactNode}) {
return <div>{children({height: 500, width: 300})}</div>;
};
});
jest.mock('./virtualized_thread_list_row', () => {
return function MockRow({index, style, data}: any) {
return (
<div
style={style}
data-testid={`thread-row-${data.ids[index]}`}
>
{data.ids[index]}
</div>
);
};
});
describe('components/threading/global_threads/thread_list/virtualized_thread_list', () => {
let props: ComponentProps<typeof VirtualizedThreadList>;
let loadMoreItems: (startIndex: number, stopIndex: number) => Promise<any>;
@@ -25,8 +45,7 @@ describe('components/threading/global_threads/thread_list/virtualized_thread_lis
});
test('should match snapshot', () => {
const wrapper = shallow(<VirtualizedThreadList {...props}/>);
expect(wrapper).toMatchSnapshot();
const {baseElement} = renderWithContext(<VirtualizedThreadList {...props}/>);
expect(baseElement).toMatchSnapshot();
});
});
@@ -1,14 +1,30 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {shallow} from 'enzyme';
import React from 'react';
import type {ComponentProps} from 'react';
import {renderWithContext} from 'tests/react_testing_utils';
import {Constants} from 'utils/constants';
import Row from './virtualized_thread_list_row';
jest.mock('../thread_item', () => {
return function MockThreadItem(props: any) {
return (
<div
data-testid={`thread-item-${props.threadId}`}
data-selected={props.isSelected}
style={props.style}
/>
);
};
});
jest.mock('components/search_shortcut/search_shortcut', () => ({
SearchShortcut: () => <span>{'Ctrl+F'}</span>,
}));
describe('components/threading/global_threads/thread_list/virtualized_thread_list_row', () => {
let props: ComponentProps<typeof Row>;
@@ -24,30 +40,29 @@ describe('components/threading/global_threads/thread_list/virtualized_thread_lis
});
test('should match snapshot', () => {
const wrapper = shallow(<Row {...props}/>);
expect(wrapper).toMatchSnapshot();
const {baseElement} = renderWithContext(<Row {...props}/>);
expect(baseElement).toMatchSnapshot();
});
test('should support item loading indicator', () => {
const wrapper = shallow(
const {baseElement} = renderWithContext(
<Row
{...props}
data={{ids: [...props.data.ids, Constants.THREADS_LOADING_INDICATOR_ITEM_ID], selectedThreadId: undefined}}
index={3}
/>);
expect(wrapper).toMatchSnapshot();
/>,
);
expect(baseElement).toMatchSnapshot();
});
test('should support item search guidance ', () => {
const wrapper = shallow(
const {baseElement} = renderWithContext(
<Row
{...props}
data={{ids: [...props.data.ids, Constants.THREADS_NO_RESULTS_ITEM_ID], selectedThreadId: undefined}}
index={3}
/>);
expect(wrapper).toMatchSnapshot();
/>,
);
expect(baseElement).toMatchSnapshot();
});
});
@@ -1,56 +1,64 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`components/threading/global_threads/thread_pane should match snapshot 1`] = `
<div
className="ThreadPane"
id="thread-pane-container"
>
<Header
className="ThreadPane___header"
heading={
<React.Fragment>
<Memo(Button)
className="Button___icon Button___large back"
onClick={[Function]}
<div>
<div
class="ThreadPane"
id="thread-pane-container"
>
<div
class="Header ThreadPane___header"
>
<div
class="left"
>
<button
class="Button Button___transparent Button___icon Button___large back"
>
<i
className="icon icon-arrow-back-ios"
/>
</Memo(Button)>
<span
class="Button_label"
>
<i
class="icon icon-arrow-back-ios"
/>
</span>
</button>
<h3>
<span
className="separated"
class="separated"
>
Thread
</span>
<Memo(Button)
allowTextOverflow={true}
className="separated"
onClick={[Function]}
<button
class="Button Button___transparent allowTextOverflow separated"
>
Team name
</Memo(Button)>
<span
class="Button_label"
>
Team name
</span>
</button>
</h3>
</React.Fragment>
}
right={
<React.Fragment>
<Memo(FollowButton)
isFollowing={true}
onClick={[Function]}
/>
<PopoutButton
onClick={[Function]}
/>
<Memo(ThreadMenu)
hasUnreads={false}
idPrefix="thread-menu"
isFollowing={true}
threadId="1y8hpek81byspd4enyk9mp1ncw"
unreadTimestamp={1611786714912}
/>
</React.Fragment>
}
/>
</div>
<div
class="spacer"
/>
<button
class="Button Button___transparent is-active FollowButton"
>
<span
class="Button_label"
>
Following
</span>
</button>
<button>
Popout
</button>
<div
data-testid="thread-menu"
/>
</div>
</div>
</div>
`;
@@ -1,7 +1,6 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {shallow} from 'enzyme';
import React from 'react';
import type {ComponentProps} from 'react';
@@ -9,15 +8,15 @@ import type {UserProfile} from '@mattermost/types/users';
import {setThreadFollow} from 'mattermost-redux/actions/threads';
import Button from 'components/threading/common/button';
import FollowButton from 'components/threading/common/follow_button';
import Header from 'components/widgets/header';
import TestHelper from 'packages/mattermost-redux/test/test_helper';
import {renderWithContext, screen, userEvent} from 'tests/react_testing_utils';
import ThreadPane from './thread_pane';
jest.mock('mattermost-redux/actions/threads');
jest.mock('mattermost-redux/actions/threads', () => ({
...jest.requireActual('mattermost-redux/actions/threads'),
setThreadFollow: jest.fn(() => ({type: 'MOCK_SET_THREAD_FOLLOW'})),
}));
const mockRouting = {
params: {
@@ -34,20 +33,28 @@ jest.mock('../../hooks', () => {
};
});
const mockDispatch = jest.fn();
let mockState: any;
jest.mock('components/popout_button', () => ({
__esModule: true,
default: ({onClick}: {onClick: () => void}) => (
<button onClick={onClick}>{'Popout'}</button>
),
}));
jest.mock('react-redux', () => ({
...jest.requireActual('react-redux') as typeof import('react-redux'),
useSelector: (selector: (state: typeof mockState) => unknown) => selector(mockState),
useDispatch: () => mockDispatch,
jest.mock('../thread_menu', () => ({
__esModule: true,
default: ({children}: {children: React.ReactNode}) => (
<div data-testid='thread-menu'>{children}</div>
),
}));
describe('components/threading/global_threads/thread_pane', () => {
let props: ComponentProps<typeof ThreadPane>;
let mockThread: typeof props['thread'];
let initialState: any;
beforeEach(() => {
jest.clearAllMocks();
mockThread = {
id: '1y8hpek81byspd4enyk9mp1ncw',
unread_replies: 0,
@@ -67,7 +74,7 @@ describe('components/threading/global_threads/thread_pane', () => {
const profiles: Record<string, UserProfile> = {};
profiles[user1.id] = user1;
mockState = {
initialState = {
entities: {
general: {
config: {},
@@ -104,48 +111,53 @@ describe('components/threading/global_threads/thread_pane', () => {
});
test('should match snapshot', () => {
const wrapper = shallow(
const {container} = renderWithContext(
<ThreadPane {...props}/>,
initialState,
);
expect(wrapper).toMatchSnapshot();
expect(container).toMatchSnapshot();
});
test('should support follow', () => {
test('should support follow', async () => {
props.thread.is_following = false;
const wrapper = shallow(
renderWithContext(
<ThreadPane {...props}/>,
initialState,
);
wrapper.find(Header).shallow().find(FollowButton).shallow().simulate('click');
await userEvent.click(screen.getByText('Follow'));
expect(setThreadFollow).toHaveBeenCalledWith(mockRouting.currentUserId, mockRouting.currentTeamId, mockThread.id, true);
expect(mockDispatch).toHaveBeenCalledTimes(1);
});
test('should support unfollow', () => {
test('should support unfollow', async () => {
props.thread.is_following = true;
const wrapper = shallow(
renderWithContext(
<ThreadPane {...props}/>,
initialState,
);
wrapper.find(Header).shallow().find(FollowButton).shallow().simulate('click');
await userEvent.click(screen.getByText('Following'));
expect(setThreadFollow).toHaveBeenCalledWith(mockRouting.currentUserId, mockRouting.currentTeamId, mockThread.id, false);
expect(mockDispatch).toHaveBeenCalledTimes(1);
});
test('should support openInChannel', () => {
const wrapper = shallow(
test('should support openInChannel', async () => {
renderWithContext(
<ThreadPane {...props}/>,
initialState,
);
wrapper.find(Header).shallow().find('h3').find(Button).simulate('click');
await userEvent.click(screen.getByText('Team name'));
expect(mockRouting.goToInChannel).toHaveBeenCalledWith('1y8hpek81byspd4enyk9mp1ncw');
});
test('should support go back to list', () => {
const wrapper = shallow(
test('should support go back to list', async () => {
renderWithContext(
<ThreadPane {...props}/>,
initialState,
);
wrapper.find(Header).shallow().find(Button).find('.back').simulate('click');
const backButton = document.querySelector('.back') as HTMLElement;
expect(backButton).toBeInTheDocument();
await userEvent.click(backButton);
expect(mockRouting.select).toHaveBeenCalledWith();
});
});
@@ -1,58 +1,20 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`components/threading/ThreadViewer should match snapshot 1`] = `
<Fragment>
<div>
<div
className="ThreadViewer"
class="ThreadViewer"
>
<div
className="post-right-comments-container"
class="post-right-comments-container"
>
<FileUploadOverlay
id="threadView"
overlayType="right"
<div
data-testid="file-upload-overlay"
/>
<DeferredRenderWrapper
channelId="channel_id"
isThreadView={false}
key="id"
onCardClick={[Function]}
postIds={
Array [
"id",
]
}
selected={
Object {
"channel_id": "channel_id",
"create_at": 1502715365009,
"delete_at": 0,
"edit_at": 0,
"hashtags": "",
"id": "id",
"is_following": true,
"is_pinned": false,
"message": "post message",
"metadata": Object {
"embeds": Array [],
"emojis": Array [],
"files": Array [],
"images": Object {},
"reactions": Array [],
},
"original_id": "",
"pending_post_id": "",
"props": Object {},
"reply_count": 3,
"root_id": "",
"type": "system_add_remove",
"update_at": 1502715372443,
"user_id": "user_id",
}
}
useRelativeTimestamp={false}
<div
data-testid="virtualized-thread-viewer"
/>
</div>
</div>
</Fragment>
</div>
`;
@@ -1,7 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {shallow} from 'enzyme';
import {render, waitFor} from '@testing-library/react';
import React from 'react';
import type {Channel} from '@mattermost/types/channels';
@@ -16,6 +16,23 @@ import type {FakePost} from 'types/store/rhs';
import ThreadViewer from './thread_viewer';
import type {Props} from './thread_viewer';
jest.mock('components/deferComponentRender', () => (component: any) => component);
jest.mock('../virtualized_thread_viewer', () => () => (
<div data-testid='virtualized-thread-viewer'/>
));
jest.mock('components/file_upload_overlay', () => () => (
<div data-testid='file-upload-overlay'/>
));
jest.mock('client/web_websocket_client', () => ({
__esModule: true,
default: {
updateActiveThread: jest.fn(),
},
}));
describe('components/threading/ThreadViewer', () => {
const post: Post = TestHelper.getPostMock({
channel_id: 'channel_id',
@@ -91,24 +108,46 @@ describe('components/threading/ThreadViewer', () => {
test('should match snapshot', async () => {
const reset = fakeDate(new Date(1502715365000));
const wrapper = shallow(
const {container} = render(
<ThreadViewer {...baseProps}/>,
);
await new Promise((resolve) => setTimeout(resolve));
expect(wrapper).toMatchSnapshot();
await waitFor(() => {
expect(actions.getPostThread).toHaveBeenCalled();
});
expect(container).toMatchSnapshot();
reset();
});
test('should make api call to get thread posts on socket reconnect', () => {
const wrapper = shallow(
test('should make api call to get thread posts on socket reconnect', async () => {
const {rerender} = render(
<ThreadViewer {...baseProps}/>,
);
wrapper.setProps({socketConnectionStatus: false});
wrapper.setProps({socketConnectionStatus: true});
// ThreadViewer calls getPostThread(rootId, !reconnected, lastUpdateAt) from onInit(reconnected).
// Initial mount: onInit(false) → fetchThreads true. After reconnect: onInit(true) → fetchThreads false.
await waitFor(() => {
expect(actions.getPostThread).toHaveBeenCalledWith(post.id, true, 1234);
});
actions.getPostThread.mockClear();
return expect(actions.getPostThread).toHaveBeenCalledWith(post.id, true, 1234);
rerender(
<ThreadViewer
{...baseProps}
socketConnectionStatus={false}
/>,
);
rerender(
<ThreadViewer
{...baseProps}
socketConnectionStatus={true}
/>,
);
await waitFor(() => {
expect(actions.getPostThread).toHaveBeenCalledTimes(1);
expect(actions.getPostThread).toHaveBeenCalledWith(post.id, false, 1234);
});
});
test('should not break if root post is a fake post', () => {
@@ -118,7 +157,7 @@ describe('components/threading/ThreadViewer', () => {
};
expect(() => {
shallow(<ThreadViewer {...props}/>);
render(<ThreadViewer {...props}/>);
}).not.toThrow("Cannot read property 'reply_count' of undefined");
});
@@ -130,28 +169,25 @@ describe('components/threading/ThreadViewer', () => {
};
expect(() => {
shallow(<ThreadViewer {...props}/>);
render(<ThreadViewer {...props}/>);
}).not.toThrow("Cannot read property 'reply_count' of undefined");
});
test('should call fetchThread when no thread on mount', (done) => {
test('should call fetchThread when no thread on mount', async () => {
const {actions} = baseProps;
shallow(
render(
<ThreadViewer
{...baseProps}
isCollapsedThreadsEnabled={true}
/>,
);
expect.assertions(3);
process.nextTick(() => {
expect(actions.updateThreadLastOpened).not.toHaveBeenCalled();
expect(actions.updateThreadRead).not.toHaveBeenCalled();
await waitFor(() => {
expect(actions.getThread).toHaveBeenCalledWith('user_id', 'team_id', 'id', true);
done();
});
expect(actions.updateThreadLastOpened).not.toHaveBeenCalled();
expect(actions.updateThreadRead).not.toHaveBeenCalled();
});
test('should call updateThreadLastOpened on mount', () => {
@@ -163,7 +199,7 @@ describe('components/threading/ThreadViewer', () => {
last_reply_at: 32,
} as UserThread;
shallow(
render(
<ThreadViewer
{...baseProps}
userThread={userThread}
@@ -171,7 +207,6 @@ describe('components/threading/ThreadViewer', () => {
/>,
);
expect.assertions(3);
expect(actions.updateThreadLastOpened).toHaveBeenCalledWith('id', 42);
expect(actions.updateThreadRead).not.toHaveBeenCalled();
expect(actions.getThread).not.toHaveBeenCalled();
@@ -186,7 +221,7 @@ describe('components/threading/ThreadViewer', () => {
last_reply_at: 142,
} as UserThread;
shallow(
render(
<ThreadViewer
{...baseProps}
userThread={userThread}
@@ -194,13 +229,12 @@ describe('components/threading/ThreadViewer', () => {
/>,
);
expect.assertions(3);
expect(actions.updateThreadLastOpened).toHaveBeenCalledWith('id', 42);
expect(actions.updateThreadRead).toHaveBeenCalledWith('user_id', 'team_id', 'id', 400);
expect(actions.getThread).not.toHaveBeenCalled();
});
test('should call updateThreadLastOpened and updateThreadRead upon thread id change', (done) => {
test('should call updateThreadLastOpened and updateThreadRead upon thread id change', async () => {
jest.useRealTimers();
const dateNowOrig = Date.now;
Date.now = () => new Date(400).getMilliseconds();
@@ -212,34 +246,48 @@ describe('components/threading/ThreadViewer', () => {
last_reply_at: 142,
} as UserThread;
const wrapper = shallow(
const {rerender} = render(
<ThreadViewer
{...baseProps}
isCollapsedThreadsEnabled={true}
/>,
);
expect.assertions(6);
process.nextTick(() => {
expect(actions.updateThreadLastOpened).not.toHaveBeenCalled();
expect(actions.updateThreadRead).not.toHaveBeenCalled();
await waitFor(() => {
expect(actions.getThread).toHaveBeenCalled();
jest.resetAllMocks();
wrapper.setProps({userThread});
expect(actions.updateThreadLastOpened).toHaveBeenCalledWith('id', 42);
expect(actions.updateThreadRead).toHaveBeenCalledWith('user_id', 'team_id', 'id', 400);
expect(actions.getThread).not.toHaveBeenCalled();
Date.now = dateNowOrig;
done();
});
expect(actions.updateThreadLastOpened).not.toHaveBeenCalled();
expect(actions.updateThreadRead).not.toHaveBeenCalled();
jest.resetAllMocks();
jest.mocked(baseProps.actions.getPostThread).mockResolvedValue({
data: {
order: ['post1', 'post2', 'post3'],
posts: {
post1: TestHelper.getPostMock({id: 'post1', update_at: 1000}),
post2: TestHelper.getPostMock({id: 'post2', update_at: 2000}),
post3: TestHelper.getPostMock({id: 'post3', update_at: 1500}),
},
},
});
rerender(
<ThreadViewer
{...baseProps}
isCollapsedThreadsEnabled={true}
userThread={userThread}
/>,
);
expect(actions.updateThreadLastOpened).toHaveBeenCalledWith('id', 42);
expect(actions.updateThreadRead).toHaveBeenCalledWith('user_id', 'team_id', 'id', 400);
expect(actions.getThread).not.toHaveBeenCalled();
Date.now = dateNowOrig;
});
test('should call fetchRHSAppsBindings on mount if appsEnabled', () => {
const {actions} = baseProps;
shallow(
render(
<ThreadViewer
{...baseProps}
/>,
@@ -251,7 +299,7 @@ describe('components/threading/ThreadViewer', () => {
test('should not call fetchRHSAppsBindings on mount if not appsEnabled', () => {
const {actions} = baseProps;
shallow(
render(
<ThreadViewer
{...baseProps}
appsEnabled={false}
@@ -264,17 +312,17 @@ describe('components/threading/ThreadViewer', () => {
test('should update thread with highest update_at value when lastUpdateAt is 0', async () => {
const {actions} = baseProps;
shallow(
render(
<ThreadViewer
{...baseProps}
lastUpdateAt={0} // Set lastUpdateAt to 0
/>,
);
await new Promise(process.nextTick);
// Verify getPostThread was called with lastUpdateAt = 0
expect(actions.getPostThread).toHaveBeenCalledWith(post.id, true, 0);
await waitFor(() => {
// Verify getPostThread was called with lastUpdateAt = 0
expect(actions.getPostThread).toHaveBeenCalledWith(post.id, true, 0);
});
// Verify updateThreadLastUpdateAt was called with the highest update_at value
expect(actions.updateThreadLastUpdateAt).toHaveBeenCalledWith(post.id, 2000);
@@ -293,15 +341,15 @@ describe('components/threading/ThreadViewer', () => {
},
});
shallow(
render(
<ThreadViewer
{...baseProps}
/>,
);
await new Promise(process.nextTick);
// Verify updateThreadLastUpdateAt was called with the highest update_at value
expect(actions.updateThreadLastUpdateAt).toHaveBeenCalledWith(post.id, 9000);
await waitFor(() => {
// Verify updateThreadLastUpdateAt was called with the highest update_at value
expect(actions.updateThreadLastUpdateAt).toHaveBeenCalledWith(post.id, 9000);
});
});
});
@@ -1,7 +1,6 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {shallow} from 'enzyme';
import type {ComponentProps} from 'react';
import React from 'react';
@@ -11,10 +10,30 @@ import type {DeepPartial} from '@mattermost/types/utilities';
import {Permissions} from 'mattermost-redux/constants';
import {renderWithContext} from 'tests/react_testing_utils';
import {TestHelper} from 'utils/test_helper';
import VirtualizedThreadViewer from './virtualized_thread_viewer';
const mockScrollToItem = jest.fn();
jest.mock('components/dynamic_virtualized_list', () => {
const ReactMock = require('react');
return {
DynamicVirtualizedList: ReactMock.forwardRef((props: any, ref: any) => {
ReactMock.useImperativeHandle(ref, () => ({
scrollToItem: mockScrollToItem,
}));
return <div data-testid='virtualized-list'/>;
}),
};
});
jest.mock('./create_comment', () => () => <div data-testid='create-comment'/>);
jest.mock('./thread_viewer_row', () => () => <div data-testid='thread-viewer-row'/>);
jest.mock('components/new_replies_banner', () => () => null);
jest.mock('components/post_view/floating_timestamp', () => () => null);
type Props = ComponentProps<typeof VirtualizedThreadViewer>;
function getBasePropsAndState(): [Props, DeepPartial<GlobalState>] {
const channel = TestHelper.getChannelMock();
@@ -73,75 +92,76 @@ function getBasePropsAndState(): [Props, DeepPartial<GlobalState>] {
}
describe('components/threading/VirtualizedThreadViewer', () => {
const [baseProps] = getBasePropsAndState();
const [baseProps, baseState] = getBasePropsAndState();
beforeEach(() => {
mockScrollToItem.mockClear();
});
test('should scroll to the bottom when the current user makes a new post in the thread', () => {
const scrollToBottom = jest.fn();
const wrapper = shallow(
const {rerender} = renderWithContext(
<VirtualizedThreadViewer {...baseProps}/>,
baseState,
);
const instance = wrapper.instance() as VirtualizedThreadViewer;
instance.scrollToBottom = scrollToBottom;
expect(scrollToBottom).not.toHaveBeenCalled();
wrapper.setProps({
lastPost:
{
mockScrollToItem.mockClear();
rerender(
<VirtualizedThreadViewer
{...baseProps}
lastPost={TestHelper.getPostMock({
id: 'newpost',
root_id: baseProps.selected.id,
user_id: 'user_id',
},
});
expect(scrollToBottom).toHaveBeenCalled();
});
test('should not scroll to the bottom when another user makes a new post in the thread', () => {
const scrollToBottom = jest.fn();
const wrapper = shallow(
<VirtualizedThreadViewer {...baseProps}/>,
);
const instance = wrapper.instance() as VirtualizedThreadViewer;
instance.scrollToBottom = scrollToBottom;
expect(scrollToBottom).not.toHaveBeenCalled();
wrapper.setProps({
lastPost:
{
id: 'newpost',
root_id: baseProps.selected.id,
user_id: 'other_user_id',
},
});
expect(scrollToBottom).not.toHaveBeenCalled();
});
test('should not scroll to the bottom when there is a highlighted reply', () => {
const scrollToBottom = jest.fn();
const wrapper = shallow(
<VirtualizedThreadViewer
{...baseProps}
})}
/>,
);
const instance = wrapper.instance() as VirtualizedThreadViewer;
instance.scrollToBottom = scrollToBottom;
expect(mockScrollToItem).toHaveBeenCalledWith(0, 'end', undefined);
});
wrapper.setProps({
lastPost:
{
test('should not scroll to the bottom when another user makes a new post in the thread', () => {
const {rerender} = renderWithContext(
<VirtualizedThreadViewer {...baseProps}/>,
baseState,
);
mockScrollToItem.mockClear();
rerender(
<VirtualizedThreadViewer
{...baseProps}
lastPost={TestHelper.getPostMock({
id: 'newpost',
root_id: baseProps.selected.id,
user_id: 'other_user_id',
})}
/>,
);
expect(mockScrollToItem).not.toHaveBeenCalled();
});
test('should not scroll to the bottom when there is a highlighted reply', () => {
const {rerender} = renderWithContext(
<VirtualizedThreadViewer {...baseProps}/>,
baseState,
);
mockScrollToItem.mockClear();
rerender(
<VirtualizedThreadViewer
{...baseProps}
lastPost={TestHelper.getPostMock({
id: 'newpost',
root_id: baseProps.selected.id,
user_id: 'user_id',
},
highlightedPostId: '42',
});
})}
highlightedPostId='42'
/>,
);
expect(scrollToBottom).not.toHaveBeenCalled();
expect(mockScrollToItem).not.toHaveBeenCalledWith(0, 'end', undefined);
});
});
@@ -1,17 +1,15 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import type {ShallowWrapper} from 'enzyme';
import React from 'react';
import type {ComponentProps} from 'react';
import type {ChannelType} from '@mattermost/types/channels';
import type {TeamType} from '@mattermost/types/teams';
import UnreadsStatusHandler from 'components/unreads_status_handler/unreads_status_handler';
import type {UnreadsStatusHandlerClass} from 'components/unreads_status_handler/unreads_status_handler';
import UnreadsStatusHandler, {UnreadsStatusHandlerClass} from 'components/unreads_status_handler/unreads_status_handler';
import {shallowWithIntl} from 'tests/helpers/intl-test-helper';
import {renderWithContext} from 'tests/react_testing_utils';
import {Constants} from 'utils/constants';
import {TestHelper} from 'utils/test_helper';
import {isChrome, isFirefox} from 'utils/user_agent';
@@ -51,70 +49,78 @@ describe('components/UnreadsStatusHandler', () => {
};
test('set correctly the title when needed', () => {
const wrapper = shallowWithIntl(
<UnreadsStatusHandler {...defaultProps}/>,
) as unknown as ShallowWrapper<Props, any, UnreadsStatusHandlerClass>;
const instance = wrapper.instance();
instance.updateTitle();
instance.componentDidUpdate = jest.fn();
instance.render = jest.fn();
// Render with slightly different prop to trigger componentDidUpdate on first rerender
const {rerender} = renderWithContext(
<UnreadsStatusHandler
{...defaultProps}
unreadStatus={true}
/>,
);
// Track cumulative props like Enzyme's setProps
let currentProps: any = {...defaultProps};
rerender(<UnreadsStatusHandler {...currentProps}/>);
expect(document.title).toBe('Public test 1 - Test team display name Test site');
wrapper.setProps({
siteName: undefined,
});
instance.updateTitle();
currentProps = {...currentProps, siteName: undefined};
rerender(<UnreadsStatusHandler {...currentProps}/>);
expect(document.title).toBe('Public test 1 - Test team display name');
wrapper.setProps({
currentProps = {
...currentProps,
currentChannel: {id: '1', type: Constants.DM_CHANNEL} as Props['currentChannel'],
currentTeammate: {display_name: 'teammate'} as Props['currentTeammate'],
});
instance.updateTitle();
};
rerender(<UnreadsStatusHandler {...currentProps}/>);
expect(document.title).toBe('teammate - Test team display name');
wrapper.setProps({
unreadStatus: 3,
});
instance.updateTitle();
currentProps = {...currentProps, unreadStatus: 3};
rerender(<UnreadsStatusHandler {...currentProps}/>);
expect(document.title).toBe('(3) teammate - Test team display name');
wrapper.setProps({
currentProps = {
...currentProps,
currentChannel: {} as Props['currentChannel'],
currentTeammate: {} as Props['currentTeammate']});
instance.updateTitle();
currentTeammate: {} as Props['currentTeammate'],
};
rerender(<UnreadsStatusHandler {...currentProps}/>);
expect(document.title).toBe('Mattermost - Join a team');
wrapper.setProps({
currentProps = {
...currentProps,
inDrafts: false,
inScheduledPosts: true,
unreadStatus: 0,
});
instance.updateTitle();
};
rerender(<UnreadsStatusHandler {...currentProps}/>);
expect(document.title).toBe('Scheduled - Test team display name');
wrapper.setProps({
currentProps = {
...currentProps,
inDrafts: false,
inScheduledPosts: true,
unreadStatus: 10,
});
instance.updateTitle();
};
rerender(<UnreadsStatusHandler {...currentProps}/>);
expect(document.title).toBe('(10) Scheduled - Test team display name');
wrapper.setProps({
currentProps = {
...currentProps,
inDrafts: true,
inScheduledPosts: false,
unreadStatus: 0,
});
instance.updateTitle();
};
rerender(<UnreadsStatusHandler {...currentProps}/>);
expect(document.title).toBe('Drafts - Test team display name');
wrapper.setProps({
currentProps = {
...currentProps,
inDrafts: true,
inScheduledPosts: false,
unreadStatus: 10,
});
instance.updateTitle();
};
rerender(<UnreadsStatusHandler {...currentProps}/>);
expect(document.title).toBe('(10) Drafts - Test team display name');
});
@@ -123,76 +129,106 @@ describe('components/UnreadsStatusHandler', () => {
// supported, hence we need to show * in title on mentions
(isFirefox as jest.Mock).mockImplementation(() => false);
(isChrome as jest.Mock).mockImplementation(() => false);
const wrapper = shallowWithIntl(
<UnreadsStatusHandler {...defaultProps}/>,
) as unknown as ShallowWrapper<Props, any, UnreadsStatusHandlerClass>;
const instance = wrapper.instance();
wrapper.setProps({
siteName: undefined,
});
wrapper.setProps({
const {rerender} = renderWithContext(
<UnreadsStatusHandler {...defaultProps}/>,
);
let currentProps: any = {...defaultProps, siteName: undefined};
currentProps = {
...currentProps,
currentChannel: {id: '1', type: Constants.DM_CHANNEL} as Props['currentChannel'],
currentTeammate: {display_name: 'teammate'} as Props['currentTeammate'],
});
wrapper.setProps({
unreadStatus: 3,
});
instance.updateTitle();
};
currentProps = {...currentProps, unreadStatus: 3};
rerender(<UnreadsStatusHandler {...currentProps}/>);
expect(document.title).toBe('(3) * teammate - Test team display name');
});
test('should display correct favicon', () => {
const link = document.createElement('link');
link.rel = 'icon';
document.head.appendChild(link);
const sizes = ['16x16', '24x24', '32x32', '64x64', '96x96'];
sizes.forEach((size) => {
const link = document.createElement('link');
link.rel = 'icon';
link.setAttribute('sizes', size);
document.head.appendChild(link);
});
const wrapper = shallowWithIntl(
(isFirefox as jest.Mock).mockReturnValue(true);
// Spy on getBadgeStatus to verify the correct badge status is computed
// (updateFavicon is an arrow function so we can't spy on it directly)
const getBadgeStatusSpy = jest.spyOn(UnreadsStatusHandlerClass.prototype, 'getBadgeStatus');
const {rerender} = renderWithContext(
<UnreadsStatusHandler {...defaultProps}/>,
) as unknown as ShallowWrapper<Props, any, UnreadsStatusHandlerClass>;
const instance = wrapper.instance();
instance.updateFavicon = jest.fn();
);
wrapper.setProps({
unreadStatus: 3,
});
expect(instance.updateFavicon).toHaveBeenLastCalledWith('Mention');
rerender(
<UnreadsStatusHandler
{...defaultProps}
unreadStatus={3}
/>,
);
expect(getBadgeStatusSpy).toHaveLastReturnedWith('Mention');
wrapper.setProps({
unreadStatus: true,
});
expect(instance.updateFavicon).toHaveBeenLastCalledWith('Unread');
rerender(
<UnreadsStatusHandler
{...defaultProps}
unreadStatus={true}
/>,
);
expect(getBadgeStatusSpy).toHaveLastReturnedWith('Unread');
wrapper.setProps({
unreadStatus: false,
rerender(
<UnreadsStatusHandler
{...defaultProps}
unreadStatus={false}
/>,
);
expect(getBadgeStatusSpy).toHaveLastReturnedWith('None');
getBadgeStatusSpy.mockRestore();
// Clean up
sizes.forEach((size) => {
const link = document.querySelector(`link[rel="icon"][sizes="${size}"]`);
if (link) {
link.remove();
}
});
expect(instance.updateFavicon).toHaveBeenLastCalledWith('None');
});
test('should display correct title when in drafts', () => {
const wrapper = shallowWithIntl(
const {rerender} = renderWithContext(
<UnreadsStatusHandler {...defaultProps}/>,
);
rerender(
<UnreadsStatusHandler
{...defaultProps}
inDrafts={true}
currentChannel={undefined}
siteName={undefined}
/>,
) as unknown as ShallowWrapper<Props, any, UnreadsStatusHandlerClass>;
wrapper.instance().updateTitle();
);
expect(document.title).toBe('Drafts - Test team display name');
});
test('should display correct title when in scheduled posts tab', () => {
const wrapper = shallowWithIntl(
const {rerender} = renderWithContext(
<UnreadsStatusHandler {...defaultProps}/>,
);
rerender(
<UnreadsStatusHandler
{...defaultProps}
inScheduledPosts={true}
currentChannel={undefined}
siteName={undefined}
/>,
) as unknown as ShallowWrapper<Props, any, UnreadsStatusHandlerClass>;
wrapper.instance().updateTitle();
);
expect(document.title).toBe('Scheduled - Test team display name');
});
@@ -9,7 +9,7 @@ import type {UserProfile} from '@mattermost/types/users';
import {General} from 'mattermost-redux/constants';
import {displayUsername} from 'mattermost-redux/utils/user_utils';
import {renderWithContext, act, userEvent} from 'tests/react_testing_utils';
import {renderWithContext, userEvent} from 'tests/react_testing_utils';
import {TestHelper} from 'utils/test_helper';
import GroupMemberList from './group_member_list';
@@ -89,32 +89,26 @@ describe('component/user_group_popover/group_member_list', () => {
},
};
test('should match snapshot', async () => {
let baseElement: HTMLElement;
await act(async () => {
({baseElement} = renderWithContext(
<GroupMemberList
{...baseProps}
/>,
initialState as any,
));
});
test('should match snapshot', () => {
const {baseElement} = renderWithContext(
<GroupMemberList
{...baseProps}
/>,
initialState as any,
);
expect(baseElement!).toMatchSnapshot();
expect(baseElement).toMatchSnapshot();
});
test('should open dms', async () => {
let container: HTMLElement;
await act(async () => {
({container} = renderWithContext(
<GroupMemberList
{...baseProps}
/>,
initialState as any,
));
});
const {container} = renderWithContext(
<GroupMemberList
{...baseProps}
/>,
initialState as any,
);
const dmButton = container!.querySelector('.group-member-list_dm-button');
const dmButton = container.querySelector('.group-member-list_dm-button');
if (dmButton) {
await userEvent.click(dmButton);
}
@@ -122,17 +116,14 @@ describe('component/user_group_popover/group_member_list', () => {
});
test('should show user overlay and hide', async () => {
let container: HTMLElement;
await act(async () => {
({container} = renderWithContext(
<GroupMemberList
{...baseProps}
/>,
initialState as any,
));
});
const {container} = renderWithContext(
<GroupMemberList
{...baseProps}
/>,
initialState as any,
);
const listItem = container!.querySelector('.group-member-list_item');
const listItem = container.querySelector('.group-member-list_item');
if (listItem) {
await userEvent.click(listItem);
}
@@ -7,7 +7,7 @@ import React from 'react';
import type {Group} from '@mattermost/types/groups';
import type {UserProfile} from '@mattermost/types/users';
import {renderWithContext, act} from 'tests/react_testing_utils';
import {renderWithContext} from 'tests/react_testing_utils';
import {TestHelper} from 'utils/test_helper';
import UserGroupPopover from './user_group_popover';
@@ -84,44 +84,35 @@ describe('component/user_group_popover', () => {
returnFocus: jest.fn(),
};
test('should match snapshot', async () => {
let baseElement: HTMLElement;
await act(async () => {
({baseElement} = renderWithContext(
<UserGroupPopover
{...baseProps}
/>,
initialState as any,
));
});
expect(baseElement!).toMatchSnapshot();
test('should match snapshot', () => {
const {baseElement} = renderWithContext(
<UserGroupPopover
{...baseProps}
/>,
initialState as any,
);
expect(baseElement).toMatchSnapshot();
});
test('should not show search bar', async () => {
let container: HTMLElement;
await act(async () => {
({container} = renderWithContext(
<UserGroupPopover
{...baseProps}
group={group2}
/>,
initialState as any,
));
});
const {container} = renderWithContext(
<UserGroupPopover
{...baseProps}
group={group2}
/>,
initialState as any,
);
expect(container!.querySelector('.user-group-popover_search-bar')).not.toBeInTheDocument();
expect(container.querySelector('.user-group-popover_search-bar')).not.toBeInTheDocument();
});
test('should show users', async () => {
let container: HTMLElement;
await act(async () => {
({container} = renderWithContext(
<UserGroupPopover
{...baseProps}
/>,
initialState as any,
));
});
expect(container!.querySelectorAll('.group-member-list_item').length).toBeGreaterThan(0);
const {container} = renderWithContext(
<UserGroupPopover
{...baseProps}
/>,
initialState as any,
);
expect(container.querySelectorAll('.group-member-list_item').length).toBeGreaterThan(0);
});
});
@@ -3,7 +3,7 @@
import React from 'react';
import {renderWithContext, screen, act} from 'tests/react_testing_utils';
import {renderWithContext, screen} from 'tests/react_testing_utils';
import {CloudProducts, LicenseSkus} from 'utils/constants';
import ADLDAPUpsellBanner from './ad_ldap_upsell_banner';
@@ -81,12 +81,10 @@ describe('component/user_groups_modal/ad_ldap_upsell_banner', () => {
},
};
await act(async () => {
renderWithContext(
<ADLDAPUpsellBanner/>,
state,
);
});
renderWithContext(
<ADLDAPUpsellBanner/>,
state,
);
expect(screen.getByText('AD/LDAP group sync creates groups faster')).toBeInTheDocument();
expect(screen.getByText('Contact sales to use')).toBeInTheDocument();
@@ -137,12 +135,10 @@ describe('component/user_groups_modal/ad_ldap_upsell_banner', () => {
},
};
await act(async () => {
renderWithContext(
<ADLDAPUpsellBanner/>,
state,
);
});
renderWithContext(
<ADLDAPUpsellBanner/>,
state,
);
expect(screen.queryByText('AD/LDAP group sync creates groups faster')).not.toBeInTheDocument();
});
@@ -1,27 +1,34 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`components/MenuItemToggleModalRedux should match snapshot with extra text 1`] = `
<Fragment>
<ToggleModalButton
className="MenuItem__with-help"
dialogProps={
Object {
"test": "test",
}
}
dialogType={[MockFunction]}
modalId="test"
exports[`components/MenuItemToggleModalRedux should match snapshot 1`] = `
<div>
<button
class="style--none "
>
<span
className="MenuItem__primary-text"
class="MenuItem__primary-text"
>
Whatever
</span>
</button>
</div>
`;
exports[`components/MenuItemToggleModalRedux should match snapshot with extra text 1`] = `
<div>
<button
class="style--none MenuItem__with-help"
>
<span
class="MenuItem__primary-text"
>
Whatever
</span>
<span
className="MenuItem__help-text"
class="MenuItem__help-text"
>
Extra text
</span>
</ToggleModalButton>
</Fragment>
</button>
</div>
`;
@@ -1,14 +1,15 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {shallow} from 'enzyme';
import React from 'react';
import {renderWithContext} from 'tests/react_testing_utils';
import {MenuItemToggleModalReduxImpl} from './menu_item_toggle_modal_redux';
describe('components/MenuItemToggleModalRedux', () => {
test('should match snapshot', () => {
const wrapper = shallow(
const {container} = renderWithContext(
<MenuItemToggleModalReduxImpl
modalId='test'
dialogType={jest.fn()}
@@ -17,30 +18,11 @@ describe('components/MenuItemToggleModalRedux', () => {
/>,
);
expect(wrapper).toMatchInlineSnapshot(`
<Fragment>
<ToggleModalButton
className=""
dialogProps={
Object {
"test": "test",
}
}
dialogType={[MockFunction]}
modalId="test"
>
<span
className="MenuItem__primary-text"
>
Whatever
</span>
</ToggleModalButton>
</Fragment>
`);
expect(container).toMatchSnapshot();
});
test('should match snapshot with extra text', () => {
const wrapper = shallow(
const {container} = renderWithContext(
<MenuItemToggleModalReduxImpl
modalId='test'
dialogType={jest.fn()}
@@ -50,6 +32,6 @@ describe('components/MenuItemToggleModalRedux', () => {
/>,
);
expect(wrapper).toMatchSnapshot();
expect(container).toMatchSnapshot();
});
});
@@ -1,76 +1,158 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`components/submenu_modal should match snapshot 1`] = `
<Modal
animation={true}
autoFocus={true}
backdrop={true}
bsClass="modal"
dialogClassName="SubMenuModal a11y__modal mobile-sub-menu"
dialogComponentClass={[Function]}
enforceFocus={false}
id="submenuModal"
keyboard={true}
manager={
ModalManager {
"add": [Function],
"containers": Array [],
"data": Array [],
"handleContainerOverflow": true,
"hideSiblingNodes": true,
"isTopModal": [Function],
"modals": Array [],
"remove": [Function],
}
}
onExited={[MockFunction]}
onHide={[Function]}
renderBackdrop={[Function]}
restoreFocus={true}
role="none"
show={true}
<body
class="modal-open"
style="padding-right: 0px;"
>
<ModalBody
bsClass="modal-body"
componentClass="div"
data-testid="SubMenuModalBody"
onClick={[Function]}
<div
aria-hidden="true"
/>
<div
role="none"
>
<MenuWrapper
animationComponent={[Function]}
className=""
<div
class="fade modal-backdrop in"
/>
<div
class="fade in modal"
id="submenuModal"
role="dialog"
style="display: block; padding-right: 0px;"
tabindex="-1"
>
<Menu
ariaLabel="mobile submenu"
openLeft={true}
<div
class="SubMenuModal a11y__modal mobile-sub-menu modal-dialog"
>
<injectIntl(SubMenuItem)
action={[MockFunction]}
id="A"
key="A"
root={false}
text="Text A"
/>
<injectIntl(SubMenuItem)
action={[MockFunction]}
id="B"
key="B"
root={false}
subMenu={
Array [
Object {
"action": [MockFunction],
"direction": "left",
"id": "C",
"text": "Text C",
},
]
}
text="Text B"
/>
</Menu>
<div />
</MenuWrapper>
</ModalBody>
</Modal>
<div
class="modal-content"
role="document"
>
<div
class="modal-body"
data-testid="SubMenuModalBody"
>
<div
class="MenuWrapper "
>
<div
aria-label="mobile submenu"
class="a11y__popup Menu"
>
<ul
class="Menu__content dropdown-menu openLeft"
role="menu"
>
<li
class="SubMenuItem MenuItem"
id="A_menuitem"
role="menuitem"
>
<div
class=""
id="A"
role="button"
tabindex="0"
>
<div
class="flex"
>
Text A
<span
class="selected"
/>
<span
aria-label="submenu icon"
class="fa fa-angle-right SubMenu__icon-right-empty"
id="channelHeaderDropdownIconRight_A"
/>
</div>
<ul
class="a11y__popup Menu dropdown-menu SubMenu"
id="A_submenu"
style="visibility: hidden; top: unset; right: 100%;"
/>
</div>
</li>
<li
aria-controls="B_submenu"
aria-expanded="false"
aria-haspopup="menu"
class="SubMenuItem MenuItem"
id="B_menuitem"
role="menuitem"
>
<div
class=""
id="B"
role="button"
tabindex="0"
>
<div
class="flex"
>
Text B
<span
class="selected"
/>
<span
aria-label="submenu icon"
class="fa fa-angle-right SubMenu__icon-right"
id="channelHeaderDropdownIconRight_B"
/>
</div>
<ul
class="a11y__popup Menu dropdown-menu SubMenu"
id="B_submenu"
style="visibility: hidden; top: unset; right: 100%;"
>
<span
class="SubMenuItemContainer"
tabindex="0"
>
<li
class="SubMenuItem MenuItem"
id="C_menuitem"
role="menuitem"
>
<div
aria-label="Text C not selected"
class=""
id="C"
role="button"
tabindex="1"
>
<div
class="flex"
>
Text C
<span
class="selected"
/>
<span
aria-label="submenu icon"
class="fa fa-angle-right SubMenu__icon-right-empty"
id="channelHeaderDropdownIconRight_C"
/>
</div>
<ul
class="a11y__popup Menu dropdown-menu SubMenu"
id="C_submenu"
style="visibility: hidden; top: unset; right: 100%;"
/>
</div>
</li>
</span>
</ul>
</div>
</li>
</ul>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</body>
`;
@@ -1,12 +1,9 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {waitForElementToBeRemoved} from '@testing-library/react';
import {shallow} from 'enzyme';
import React from 'react';
import {Modal} from 'react-bootstrap';
import {renderWithContext, screen, userEvent} from 'tests/react_testing_utils';
import {renderWithContext, screen, userEvent, waitFor, waitForElementToBeRemoved} from 'tests/react_testing_utils';
import SubMenuModal from './submenu_modal';
@@ -50,10 +47,10 @@ describe('components/submenu_modal', () => {
};
test('should match snapshot', () => {
const wrapper = shallow(
const {baseElement} = renderWithContext(
<SubMenuModal {...baseProps}/>,
);
expect(wrapper).toMatchSnapshot();
expect(baseElement).toMatchSnapshot();
});
test('should hide on modal body click', async () => {
@@ -91,12 +88,16 @@ describe('components/submenu_modal', () => {
expect(action3).toHaveBeenCalledTimes(1);
});
test('should have called props.onExited when Modal.onExited is called', () => {
const wrapper = shallow(
test('should have called props.onExited when Modal.onExited is called', async () => {
renderWithContext(
<SubMenuModal {...baseProps}/>,
);
wrapper.find(Modal).props().onExited!(document.createElement('div'));
expect(baseProps.onExited).toHaveBeenCalledTimes(1);
// Trigger modal close by clicking the body
await userEvent.click(screen.getByTestId('SubMenuModalBody'));
await waitFor(() => {
expect(baseProps.onExited).toHaveBeenCalledTimes(1);
});
});
});
@@ -1,13 +1,26 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {shallow} from 'enzyme';
import React from 'react';
import Textbox from 'components/textbox';
import {renderWithContext} from 'tests/react_testing_utils';
import PluginTextbox from '.';
const mockTextboxProps = jest.fn();
jest.mock('components/textbox', () => {
// Use require inside the factory: jest hoists mocks before imports run, and babel-plugin-jest-hoist
// forbids non-mock-prefixed outer variables (e.g. React). Cast so forwardRef is typed.
const react = require('react') as typeof import('react'); // eslint-disable-line @typescript-eslint/no-var-requires, global-require
// eslint-disable-next-line @typescript-eslint/no-unused-vars -- forwardRef arity; ref unused in mock
const MockTextbox = react.forwardRef((props: unknown, _ref: unknown) => {
mockTextboxProps(props);
return null;
});
MockTextbox.displayName = 'Textbox';
return {__esModule: true, default: MockTextbox};
});
describe('PluginTextbox', () => {
const baseProps = {
id: 'id',
@@ -33,13 +46,18 @@ describe('PluginTextbox', () => {
};
test('should rename suggestionListStyle to suggestionListPosition', () => {
const props: React.ComponentProps<typeof PluginTextbox> = {
const props = {
...baseProps,
suggestionListStyle: 'bottom',
suggestionListStyle: 'bottom' as const,
};
const wrapper = shallow(<PluginTextbox {...props}/>);
expect(wrapper.find(Textbox).prop('suggestionListPosition')).toEqual('bottom');
expect(wrapper.find(Textbox).prop('suggestionListStyle')).toBeUndefined();
renderWithContext(<PluginTextbox {...props}/>);
expect(mockTextboxProps).toHaveBeenCalledWith(
expect.objectContaining({suggestionListPosition: 'bottom'}),
);
expect(mockTextboxProps).toHaveBeenCalledWith(
expect.not.objectContaining({suggestionListStyle: expect.anything()}),
);
});
});
@@ -200,6 +200,29 @@ const Providers = ({children, store, history, options}: RenderStateProps) => {
);
};
/**
* After `render` / `renderWithContext`, runs an async `act` boundary so updates from mount effects
* (e.g. promise chains) can commit. Does not wrap the render call itself.
*
* @param microtaskRounds How many times to `await Promise.resolve()` inside `act`, yielding to the
* microtask queue between each. Increase above the default when mocked thunks resolve in sequence
* (e.g. effect + multiple `await`s) so `setState` runs inside `act` and React does not warn.
* @default 1
*/
export function runPostRenderAct(microtaskRounds: number = 1) {
const rounds = Math.max(1, microtaskRounds);
return act(async () => {
const drainMicrotasks = async (remaining: number): Promise<void> => {
if (remaining <= 0) {
return;
}
await Promise.resolve();
await drainMicrotasks(remaining - 1);
};
await drainMicrotasks(rounds);
});
}
/**
* A helper to use when an Enzyme test needs to wait for async code to run in a component before generating a snapshot.
*
@@ -1,273 +1,413 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`messageHtmlToComponent At mention 1`] = `
<p>
<span
className="mention--highlight"
>
<div>
<p>
<span
data-mention="joram"
class="mention--highlight"
>
<Memo(Connect(Component))
disableGroupHighlight={false}
disableHighlight={false}
mentionName="joram"
<span
data-mention="joram"
>
@joram
</Memo(Connect(Component))>
</span>
</span>
</span>
</p>
</p>
</div>
`;
exports[`messageHtmlToComponent At mention 2`] = `
<p>
<span
data-mention="joram"
>
<Memo(Connect(Component))
disableGroupHighlight={false}
disableHighlight={true}
mentionName="joram"
<div>
<p>
<span
data-mention="joram"
>
@joram
</Memo(Connect(Component))>
</span>
</p>
</span>
</p>
</div>
`;
exports[`messageHtmlToComponent At mention with group highlight disabled 1`] = `
<p>
<span
data-mention="developers"
>
<Memo(Connect(Component))
disableGroupHighlight={false}
disableHighlight={false}
mentionName="developers"
<div>
<p>
<span
data-mention="developers"
>
@developers
</Memo(Connect(Component))>
</span>
</p>
</span>
</p>
</div>
`;
exports[`messageHtmlToComponent At mention with group highlight disabled 2`] = `
<p>
<span
data-mention="developers"
>
<Memo(Connect(Component))
disableGroupHighlight={true}
disableHighlight={false}
mentionName="developers"
<div>
<p>
<span
data-mention="developers"
>
@developers
</Memo(Connect(Component))>
</span>
</p>
</span>
</p>
</div>
`;
exports[`messageHtmlToComponent Inline markdown image 1`] = `
<div
className="markdown-inline-img__container"
>
<Memo(Connect(MarkdownImage))
alt="Mattermost"
className="markdown-inline-img"
imageIsLink={false}
postId="post_id"
postType="system_header_change"
src="/images/icon.png"
/>
and a
<a
className="theme markdown__link"
href="http://link"
rel="noreferrer"
target="_blank"
<div>
<div
class="markdown-inline-img__container"
>
link
</a>
<div
class="file-preview__button"
style="display: inline-block;"
>
<figure
class="image-loaded-container"
>
<img
alt="Mattermost"
aria-label="file thumbnail"
class="markdown-inline-img markdown-inline-img--scaled-down-loading"
src="/images/icon.png"
tabindex="0"
/>
</figure>
</div>
and a
<a
class="theme markdown__link"
href="http://link"
rel="noreferrer"
target="_blank"
>
link
</a>
</div>
</div>
`;
exports[`messageHtmlToComponent Inline markdown image where image is link 1`] = `
<div
className="markdown-inline-img__container"
>
<a
className="theme markdown__link"
href="http://images/icon.png"
rel="noreferrer"
target="_blank"
<div>
<div
class="markdown-inline-img__container"
>
<Memo(Connect(MarkdownImage))
alt="Mattermost"
className="markdown-inline-img"
imageIsLink={true}
postId="post_id"
postType="system_header_change"
src="images/icon.png"
/>
</a>
<a
class="theme markdown__link"
href="http://images/icon.png"
rel="noreferrer"
target="_blank"
>
<div
class="file-preview__button"
style="display: inline-block;"
>
<figure
class="image-loaded-container"
>
<img
alt="Mattermost"
aria-label="file thumbnail"
class="markdown-inline-img markdown-inline-img--scaled-down-loading"
src="images/icon.png"
tabindex="0"
/>
</figure>
</div>
</a>
</div>
</div>
`;
exports[`messageHtmlToComponent Remote at mention 1`] = `
<p>
<span
className="mention--highlight"
>
<div>
<p>
<span
data-mention="joram"
class="mention--highlight"
>
<Memo(Connect(Component))
disableGroupHighlight={false}
disableHighlight={false}
mentionName="joram"
<span
data-mention="joram"
>
@joram
</Memo(Connect(Component))>
</span>
</span>
</span>
</p>
</p>
</div>
`;
exports[`messageHtmlToComponent Remote at mention 2`] = `
<p>
<span
data-mention="joram"
>
<Memo(Connect(Component))
disableGroupHighlight={false}
disableHighlight={true}
mentionName="joram"
<div>
<p>
<span
data-mention="joram"
>
@joram
</Memo(Connect(Component))>
</span>
</p>
</span>
</p>
</div>
`;
exports[`messageHtmlToComponent citation links should not render normal links as InlineEntityLink 1`] = `
<p>
<a
className="theme markdown__link"
href="http://localhost:8065/team/pl/postid"
rel="noreferrer"
target="_blank"
>
post
</a>
</p>
<div>
<p>
<a
class="theme markdown__link"
href="http://localhost:8065/team/pl/postid"
rel="noreferrer"
target="_blank"
>
post
</a>
</p>
</div>
`;
exports[`messageHtmlToComponent citation links should render citation link using InlineEntityLink 1`] = `
<p>
<InlineEntityLink
text={
Array [
"post",
]
}
url="http://localhost:8065/team/pl/postid?view=citation"
/>
</p>
<div>
<p>
<a
aria-label="Go to post"
class="inline-entity-link"
href="http://localhost:8065/team/pl/postid?view=citation"
>
<svg
fill="currentColor"
height="14"
version="1.1"
viewBox="0 0 24 24"
width="14"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M10.59,13.41C11,13.8 11,14.44 10.59,14.83C10.2,15.22 9.56,15.22 9.17,14.83C7.22,12.88 7.22,9.71 9.17,7.76V7.76L12.71,4.22C14.66,2.27 17.83,2.27 19.78,4.22C21.73,6.17 21.73,9.34 19.78,11.29L18.29,12.78C18.3,11.96 18.17,11.14 17.89,10.36L18.36,9.88C19.54,8.71 19.54,6.81 18.36,5.64C17.19,4.46 15.29,4.46 14.12,5.64L10.59,9.17C9.41,10.34 9.41,12.24 10.59,13.41M13.41,9.17C13.8,8.78 14.44,8.78 14.83,9.17C16.78,11.12 16.78,14.29 14.83,16.24V16.24L11.29,19.78C9.34,21.73 6.17,21.73 4.22,19.78C2.27,17.83 2.27,14.66 4.22,12.71L5.71,11.22C5.7,12.04 5.83,12.86 6.11,13.65L5.64,14.12C4.46,15.29 4.46,17.19 5.64,18.36C6.81,19.54 8.71,19.54 9.88,18.36L13.41,14.83C14.59,13.66 14.59,11.76 13.41,10.59C13,10.2 13,9.56 13.41,9.17Z"
/>
</svg>
</a>
</p>
</div>
`;
exports[`messageHtmlToComponent html 1`] = `
<CodeBlock
code="<div>This is a html div</div>"
language="html"
searchedContent=""
/>
<div>
<div
class="post-code"
>
<div
class="post-code__overlay"
>
<span
aria-label="Copy code"
class="post-code__clipboard"
role="button"
>
<i
class="icon icon-content-copy"
/>
</span>
<span
class="post-code__language"
>
HTML, XML
</span>
</div>
<div
class="hljs"
>
<div
class="post-code__line-numbers"
>
1
</div>
<code>
&lt;div&gt;This is a html div&lt;/div&gt;
</code>
</div>
</div>
</div>
`;
exports[`messageHtmlToComponent latex 1`] = `
Array [
<div>
<p>
This is some latex!
</p>,
"
",
<Memo(Connect(Component))
content="x^2 + y^2 = z^2"
/>,
<Memo(Connect(Component))
content="F_m - 2 = F_0 F_1 \\\\dots F_{m-1}"
/>,
</p>
<div
class="post-code"
>
<div
class="post-code__overlay"
>
<span
aria-label="Copy code"
class="post-code__clipboard"
role="button"
>
<i
class="icon icon-content-copy"
/>
</span>
<span
class="post-code__language"
>
LaTeX
</span>
</div>
<div
class="hljs"
>
<div
class="post-code__line-numbers"
>
1
</div>
<code>
x^2 + y^2 = z^2
</code>
</div>
</div>
<div
class="post-code"
>
<div
class="post-code__overlay"
>
<span
aria-label="Copy code"
class="post-code__clipboard"
role="button"
>
<i
class="icon icon-content-copy"
/>
</span>
<span
class="post-code__language"
>
LaTeX
</span>
</div>
<div
class="hljs"
>
<div
class="post-code__line-numbers"
>
1
</div>
<code>
F_m - 2 = F_0 F_1 \\dots F_{m-1}
</code>
</div>
</div>
<p>
That was some latex!
</p>,
]
</p>
</div>
`;
exports[`messageHtmlToComponent link with enabled a tooltip plugin 1`] = `
<p>
lorem ipsum
<PluginLinkTooltip
nodeAttributes={
Object {
"className": "theme markdown__link",
"href": "http://www.dolor.com",
"rel": "noreferrer",
"target": "_blank",
}
}
>
www.dolor.com
</PluginLinkTooltip>
sit amet
</p>
<div>
<p>
lorem ipsum
<a
class="theme markdown__link"
href="http://www.dolor.com"
rel="noreferrer"
target="_blank"
>
www.dolor.com
</a>
sit amet
</p>
</div>
`;
exports[`messageHtmlToComponent link without enabled tooltip plugins 1`] = `
<p>
lorem ipsum
<a
className="theme markdown__link"
href="http://www.dolor.com"
rel="noreferrer"
target="_blank"
>
www.dolor.com
</a>
sit amet
</p>
<div>
<p>
lorem ipsum
<a
class="theme markdown__link"
href="http://www.dolor.com"
rel="noreferrer"
target="_blank"
>
www.dolor.com
</a>
sit amet
</p>
</div>
`;
exports[`messageHtmlToComponent plain text 1`] = `
<p>
Hello, world!
</p>
<div>
<p>
Hello, world!
</p>
</div>
`;
exports[`messageHtmlToComponent typescript 1`] = `
<CodeBlock
code="const myFunction = () => {
<div>
<div
class="post-code"
>
<div
class="post-code__overlay"
>
<span
aria-label="Copy code"
class="post-code__clipboard"
role="button"
>
<i
class="icon icon-content-copy"
/>
</span>
<span
class="post-code__language"
>
TypeScript
</span>
</div>
<div
class="hljs"
>
<div
class="post-code__line-numbers"
>
1
2
3
</div>
<code>
const myFunction = () =&gt; {
console.log('This is a meaningful function');
};"
language="typescript"
searchedContent=""
/>
};
</code>
</div>
</div>
</div>
`;
exports[`messageHtmlToComponent typescript 2`] = `
<p>
Text before typescript codeblock
<div>
<p>
Text before typescript codeblock
<span
className="codespan__pre-wrap"
>
<code>
typescript
<span
class="codespan__pre-wrap"
>
<code>
typescript
const myFunction = () =&gt; {
console.log('This is a test function');
};
</code>
</span>
</code>
</span>
text after typescript block
</p>
</p>
</div>
`;
@@ -1,13 +1,9 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {shallow} from 'enzyme';
import React from 'react';
import type {AnchorHTMLAttributes} from 'react';
import AtMention from 'components/at_mention';
import InlineEntityLink from 'components/inline_entity_link';
import MarkdownImage from 'components/markdown_image';
import {renderWithContext, screen} from 'tests/react_testing_utils';
import Constants from 'utils/constants';
import EmojiMap from 'utils/emoji_map';
@@ -21,7 +17,8 @@ describe('messageHtmlToComponent', () => {
const input = 'Hello, world!';
const html = TextFormatting.formatText(input, {}, emptyEmojiMap);
expect(messageHtmlToComponent(html)).toMatchSnapshot();
const {container} = renderWithContext(<>{messageHtmlToComponent(html)}</>);
expect(container).toMatchSnapshot();
});
test('latex', () => {
@@ -37,7 +34,8 @@ F_m - 2 = F_0 F_1 \\dots F_{m-1}
That was some latex!`;
const html = TextFormatting.formatText(input, {}, emptyEmojiMap);
expect(messageHtmlToComponent(html)).toMatchSnapshot();
const {container} = renderWithContext(<>{messageHtmlToComponent(html)}</>);
expect(container).toMatchSnapshot();
});
test('typescript', () => {
@@ -49,7 +47,8 @@ const myFunction = () => {
`;
const html = TextFormatting.formatText(input, {}, emptyEmojiMap);
expect(messageHtmlToComponent(html, {postId: 'randompostid'})).toMatchSnapshot();
const {container} = renderWithContext(<>{messageHtmlToComponent(html, {postId: 'randompostid'})}</>);
expect(container).toMatchSnapshot();
});
test('html', () => {
@@ -59,21 +58,24 @@ const myFunction = () => {
`;
const html = TextFormatting.formatText(input, {}, emptyEmojiMap);
expect(messageHtmlToComponent(html, {postId: 'randompostid'})).toMatchSnapshot();
const {container} = renderWithContext(<>{messageHtmlToComponent(html, {postId: 'randompostid'})}</>);
expect(container).toMatchSnapshot();
});
test('link without enabled tooltip plugins', () => {
const input = 'lorem ipsum www.dolor.com sit amet';
const html = TextFormatting.formatText(input, {}, emptyEmojiMap);
expect(messageHtmlToComponent(html)).toMatchSnapshot();
const {container} = renderWithContext(<>{messageHtmlToComponent(html)}</>);
expect(container).toMatchSnapshot();
});
test('link with enabled a tooltip plugin', () => {
const input = 'lorem ipsum www.dolor.com sit amet';
const html = TextFormatting.formatText(input, {}, emptyEmojiMap);
expect(messageHtmlToComponent(html, {hasPluginTooltips: true})).toMatchSnapshot();
const {container} = renderWithContext(<>{messageHtmlToComponent(html, {hasPluginTooltips: true})}</>);
expect(container).toMatchSnapshot();
});
test('Inline markdown image', () => {
@@ -85,8 +87,12 @@ const myFunction = () => {
postId: 'post_id',
postType: Constants.PostTypes.HEADER_CHANGE,
});
expect(component).toMatchSnapshot();
expect(shallow(component).find(MarkdownImage).prop('imageIsLink')).toBe(false);
const {container} = renderWithContext(<>{component}</>);
expect(container).toMatchSnapshot();
// imageIsLink=false means the img is NOT wrapped in an <a> tag
const img = container.querySelector('img');
expect(img?.closest('a')).toBeNull();
});
test('Inline markdown image where image is link', () => {
@@ -98,8 +104,12 @@ const myFunction = () => {
postId: 'post_id',
postType: Constants.PostTypes.HEADER_CHANGE,
});
expect(component).toMatchSnapshot();
expect(shallow(component).find(MarkdownImage).prop('imageIsLink')).toBe(true);
const {container} = renderWithContext(<>{component}</>);
expect(container).toMatchSnapshot();
// imageIsLink=true means the img IS wrapped in an <a> tag
const img = container.querySelector('img');
expect(img?.closest('a')).not.toBeNull();
});
test('At mention', () => {
@@ -107,16 +117,24 @@ const myFunction = () => {
let html = TextFormatting.formatText('@joram', options, emptyEmojiMap);
let component = messageHtmlToComponent(html, {mentionHighlight: true});
expect(component).toMatchSnapshot();
expect(shallow(component).find(AtMention).prop('disableHighlight')).toBe(false);
const {container, unmount} = renderWithContext(<>{component}</>);
expect(container).toMatchSnapshot();
// When mentionHighlight is true, disableHighlight is false - the mention--highlight class wraps the mention
expect(container.querySelector('.mention--highlight')).toBeInTheDocument();
unmount();
options.mentionHighlight = false;
html = TextFormatting.formatText('@joram', options, emptyEmojiMap);
component = messageHtmlToComponent(html, {mentionHighlight: false});
expect(component).toMatchSnapshot();
expect(shallow(component).find(AtMention).prop('disableHighlight')).toBe(true);
const {container: container2} = renderWithContext(<>{component}</>);
expect(container2).toMatchSnapshot();
// When mentionHighlight is false, disableHighlight is true - no mention--highlight class
expect(container2.querySelector('.mention--highlight')).not.toBeInTheDocument();
});
test('At mention with group highlight disabled', () => {
@@ -124,16 +142,18 @@ const myFunction = () => {
let html = TextFormatting.formatText('@developers', options, emptyEmojiMap);
let component = messageHtmlToComponent(html, {disableGroupHighlight: false});
expect(component).toMatchSnapshot();
expect(shallow(component).find(AtMention).prop('disableGroupHighlight')).toBe(false);
const {container, unmount} = renderWithContext(<>{component}</>);
expect(container).toMatchSnapshot();
unmount();
options.disableGroupHighlight = true;
html = TextFormatting.formatText('@developers', options, emptyEmojiMap);
component = messageHtmlToComponent(html, {disableGroupHighlight: true});
expect(component).toMatchSnapshot();
expect(shallow(component).find(AtMention).prop('disableGroupHighlight')).toBe(true);
const {container: container2} = renderWithContext(<>{component}</>);
expect(container2).toMatchSnapshot();
});
test('Remote at mention', () => {
@@ -141,16 +161,24 @@ const myFunction = () => {
let html = TextFormatting.formatText('@joram', options, emptyEmojiMap);
let component = messageHtmlToComponent(html, {mentionHighlight: true});
expect(component).toMatchSnapshot();
expect(shallow(component).find(AtMention).prop('disableHighlight')).toBe(false);
const {container, unmount} = renderWithContext(<>{component}</>);
expect(container).toMatchSnapshot();
// When mentionHighlight is true, disableHighlight is false - the mention--highlight class wraps the mention
expect(container.querySelector('.mention--highlight')).toBeInTheDocument();
unmount();
options.mentionHighlight = false;
html = TextFormatting.formatText('@joram', options, emptyEmojiMap);
component = messageHtmlToComponent(html, {mentionHighlight: false});
expect(component).toMatchSnapshot();
expect(shallow(component).find(AtMention).prop('disableHighlight')).toBe(true);
const {container: container2} = renderWithContext(<>{component}</>);
expect(container2).toMatchSnapshot();
// When mentionHighlight is false, disableHighlight is true - no mention--highlight class
expect(container2.querySelector('.mention--highlight')).not.toBeInTheDocument();
});
test('typescript', () => {
@@ -164,7 +192,8 @@ const myFunction = () => {
const html = TextFormatting.formatText(input, {}, emptyEmojiMap);
expect(messageHtmlToComponent(html)).toMatchSnapshot();
const {container} = renderWithContext(<>{messageHtmlToComponent(html)}</>);
expect(container).toMatchSnapshot();
});
describe('citation links', () => {
@@ -173,11 +202,13 @@ const myFunction = () => {
const html = TextFormatting.formatText(input, {}, emptyEmojiMap);
const component = messageHtmlToComponent(html);
expect(component).toMatchSnapshot();
const {container} = renderWithContext(<>{component}</>);
expect(container).toMatchSnapshot();
const wrapper = shallow(component);
expect(wrapper.find(InlineEntityLink).exists()).toBe(true);
expect(wrapper.find(InlineEntityLink).prop('url')).toBe('http://localhost:8065/team/pl/postid?view=citation');
// InlineEntityLink renders an anchor with the citation URL and class 'inline-entity-link'
const link = container.querySelector('a.inline-entity-link');
expect(link).toBeInTheDocument();
expect(link?.getAttribute('href')).toBe('http://localhost:8065/team/pl/postid?view=citation');
});
test('should not render normal links as InlineEntityLink', () => {
@@ -185,11 +216,12 @@ const myFunction = () => {
const html = TextFormatting.formatText(input, {}, emptyEmojiMap);
const component = messageHtmlToComponent(html);
expect(component).toMatchSnapshot();
const {container} = renderWithContext(<>{component}</>);
expect(container).toMatchSnapshot();
const wrapper = shallow(component);
expect(wrapper.find(InlineEntityLink).exists()).toBe(false);
expect(wrapper.find('a').exists()).toBe(true);
// Normal links should NOT have the inline-entity-link class
expect(container.querySelector('a.inline-entity-link')).not.toBeInTheDocument();
expect(container.querySelector('a')).toBeInTheDocument();
});
});