MM-67498 Bulk migrate batches M13-20 from Enzyme to RTL (#35207)

* test: bulk migrate batches m13-20 from enzyme to rtl

* remove commented test block

---------

Co-authored-by: Mattermost Build <build@mattermost.com>
This commit is contained in:
sabril 2026-02-12 08:24:08 +08:00 committed by GitHub
parent 61b7fc1594
commit d09ab7173e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
67 changed files with 15541 additions and 11497 deletions

View file

@ -0,0 +1,70 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`components/widgets/settings/AutocompleteSelector check snapshot with value prop and changing focus 1`] = `
<div>
<div
class="form-group"
data-testid="autoCompleteSelector"
>
<label
class="control-label "
>
some label
</label>
<div
class=""
>
<input
class="form-control"
value="value from prop"
/>
</div>
</div>
</div>
`;
exports[`components/widgets/settings/AutocompleteSelector check snapshot with value prop and changing focus 2`] = `
<div>
<div
class="form-group"
data-testid="autoCompleteSelector"
>
<label
class="control-label "
>
some label
</label>
<div
class=""
>
<input
class="form-control"
value="value from input"
/>
</div>
</div>
</div>
`;
exports[`components/widgets/settings/AutocompleteSelector render component with required props 1`] = `
<div>
<div
class="form-group"
data-testid="autoCompleteSelector"
>
<label
class="control-label "
>
some label
</label>
<div
class=""
>
<input
class="form-control"
value="some value"
/>
</div>
</div>
</div>
`;

View file

@ -1,7 +1,3 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`components/actions_menu/ActionsMenu on mobile view should match snapshot 1`] = `
<ContextConsumer>
<Component />
</ContextConsumer>
`;
exports[`components/actions_menu/ActionsMenu on mobile view should match snapshot 1`] = `<div />`;

View file

@ -1,12 +1,12 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {shallow} from 'enzyme';
import React from 'react';
import ActionsMenu from 'components/actions_menu/actions_menu';
import type {Props} from 'components/actions_menu/actions_menu';
import {renderWithContext} from 'tests/react_testing_utils';
import {TestHelper} from 'utils/test_helper';
jest.mock('utils/utils', () => {
@ -46,10 +46,10 @@ describe('components/actions_menu/ActionsMenu on mobile view', () => {
pluginMenuItemComponents: [],
};
const wrapper = shallow(
const {container} = renderWithContext(
<ActionsMenu {...baseProps}/>,
);
expect(wrapper).toMatchSnapshot();
expect(container).toMatchSnapshot();
});
});

View file

@ -1,14 +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 {act, render, screen, userEvent} from 'tests/react_testing_utils';
import AutocompleteSelector from './autocomplete_selector';
let mockOnItemSelected: ((selected: any) => void) | undefined;
jest.mock('components/suggestion/suggestion_box', () => {
const ReactMod = require('react');
const MockSuggestionBox = ReactMod.forwardRef(function MockSuggestionBox(props: any, ref: any) {
mockOnItemSelected = props.onItemSelected;
return (
<input
ref={ref}
className={props.className || 'form-control'}
value={props.value || ''}
onChange={(e: any) => props.onChange?.({target: e.target})}
onFocus={props.onFocus}
onBlur={props.onBlur}
placeholder={props.placeholder}
/>
);
});
return {__esModule: true, default: MockSuggestionBox};
});
describe('components/widgets/settings/AutocompleteSelector', () => {
test('render component with required props', () => {
const wrapper = shallow(
const {container} = render(
<AutocompleteSelector
id='string.id'
label='some label'
@ -16,43 +38,11 @@ describe('components/widgets/settings/AutocompleteSelector', () => {
providers={[]}
/>,
);
expect(wrapper).toMatchInlineSnapshot(`
<div
className="form-group"
data-testid="autoCompleteSelector"
>
<label
className="control-label "
>
some label
</label>
<div
className=""
>
<Connect(SuggestionBox)
className="form-control"
completeOnTab={true}
containerClass="select-suggestion-container"
listComponent={[Function]}
listPosition="top"
onBlur={[Function]}
onChange={[Function]}
onFocus={[Function]}
onItemSelected={[Function]}
openOnFocus={true}
openWhenEmpty={true}
providers={Array []}
renderNoResults={true}
replaceAllInputOnSelect={true}
value="some value"
/>
</div>
</div>
`);
expect(container).toMatchSnapshot();
});
test('check snapshot with value prop and changing focus', () => {
const wrapper = shallow<AutocompleteSelector>(
test('check snapshot with value prop and changing focus', async () => {
const {container} = render(
<AutocompleteSelector
providers={[]}
label='some label'
@ -60,83 +50,28 @@ describe('components/widgets/settings/AutocompleteSelector', () => {
/>,
);
wrapper.instance().onBlur();
const input = screen.getByRole('textbox');
expect(wrapper).toMatchInlineSnapshot(`
<div
className="form-group"
data-testid="autoCompleteSelector"
>
<label
className="control-label "
>
some label
</label>
<div
className=""
>
<Connect(SuggestionBox)
className="form-control"
completeOnTab={true}
containerClass="select-suggestion-container"
listComponent={[Function]}
listPosition="top"
onBlur={[Function]}
onChange={[Function]}
onFocus={[Function]}
onItemSelected={[Function]}
openOnFocus={true}
openWhenEmpty={true}
providers={Array []}
renderNoResults={true}
replaceAllInputOnSelect={true}
value="value from prop"
/>
</div>
</div>
`);
// Initially not focused, shows prop value
expect(input).toHaveValue('value from prop');
expect(container).toMatchSnapshot();
wrapper.instance().onChange(({target: {value: 'value from input'} as HTMLInputElement}));
wrapper.instance().onFocus();
// Focus the input and type a new value
await userEvent.click(input);
await userEvent.clear(input);
await userEvent.type(input, 'value from input');
expect(wrapper).toMatchInlineSnapshot(`
<div
className="form-group"
data-testid="autoCompleteSelector"
>
<label
className="control-label "
>
some label
</label>
<div
className=""
>
<Connect(SuggestionBox)
className="form-control"
completeOnTab={true}
containerClass="select-suggestion-container"
listComponent={[Function]}
listPosition="top"
onBlur={[Function]}
onChange={[Function]}
onFocus={[Function]}
onItemSelected={[Function]}
openOnFocus={true}
openWhenEmpty={true}
providers={Array []}
renderNoResults={true}
replaceAllInputOnSelect={true}
value="value from input"
/>
</div>
</div>
`);
expect(input).toHaveValue('value from input');
expect(container).toMatchSnapshot();
// Blur the input - value should revert to prop value
await userEvent.tab();
expect(input).toHaveValue('value from prop');
});
test('onSelected', () => {
const onSelected = jest.fn();
const wrapper = shallow<AutocompleteSelector>(
render(
<AutocompleteSelector
label='some label'
value='some value'
@ -146,7 +81,9 @@ describe('components/widgets/settings/AutocompleteSelector', () => {
);
const selected = {text: 'sometext', value: 'somevalue', id: '', username: '', display_name: ''};
wrapper.instance().handleSelected(selected);
act(() => {
mockOnItemSelected!(selected);
});
expect(onSelected).toHaveBeenCalledTimes(1);
expect(onSelected).toHaveBeenCalledWith(selected);

View file

@ -1,43 +1,35 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`EmojiPage should render without crashing 1`] = `
<div
className="backstage-content emoji-list"
>
<div>
<div
className="backstage-header"
class="backstage-content emoji-list"
>
<h1>
<MemoizedFormattedMessage
defaultMessage="Custom Emoji"
id="emoji_list.header"
/>
</h1>
<Memo(AnyTeamPermissionGate)
permissions={
Array [
"create_emojis",
]
}
<div
class="backstage-header"
>
<Link
className="add-link"
to="/team/emoji/add"
<h1>
Custom Emoji
</h1>
<div
data-testid="permission-gate"
>
<button
className="btn btn-primary"
type="button"
<a
class="add-link"
href="/team/emoji/add"
>
<MemoizedFormattedMessage
defaultMessage="Add Custom Emoji"
id="emoji_list.add"
/>
</button>
</Link>
</Memo(AnyTeamPermissionGate)>
<button
class="btn btn-primary"
type="button"
>
Add Custom Emoji
</button>
</a>
</div>
</div>
<div
data-testid="emoji-list"
/>
</div>
<Connect(EmojiList)
scrollToTop={[MockFunction]}
/>
</div>
`;

View file

@ -1,20 +1,26 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import type {ShallowWrapper} from 'enzyme';
import {shallow} from 'enzyme';
import React from 'react';
import {Link} from 'react-router-dom';
import AnyTeamPermissionGate from 'components/permissions_gates/any_team_permission_gate';
import {renderWithContext, screen} from 'tests/react_testing_utils';
import EmojiList from './emoji_list';
import EmojiPage from './emoji_page';
jest.mock('utils/utils', () => ({
localizeMessage: jest.fn().mockReturnValue('Custom Emoji'),
}));
jest.mock('./emoji_list', () => ({
__esModule: true,
default: () => <div data-testid='emoji-list'/>,
}));
jest.mock('components/permissions_gates/any_team_permission_gate', () => ({
__esModule: true,
default: ({children}: {children: React.ReactNode}) => <div data-testid='permission-gate'>{children}</div>,
}));
describe('EmojiPage', () => {
const mockLoadRolesIfNeeded = jest.fn();
const mockScrollToTop = jest.fn();
@ -30,27 +36,31 @@ describe('EmojiPage', () => {
};
it('should render without crashing', () => {
const wrapper: ShallowWrapper = shallow(<EmojiPage {...defaultProps}/>);
expect(wrapper).toMatchSnapshot();
const {container} = renderWithContext(<EmojiPage {...defaultProps}/>);
expect(container).toMatchSnapshot();
});
it('should render the emoji list and the add button with permission', () => {
const wrapper: ShallowWrapper = shallow(<EmojiPage {...defaultProps}/>);
expect(wrapper.find(EmojiList).exists()).toBe(true);
expect(wrapper.find(AnyTeamPermissionGate).exists()).toBe(true);
expect(wrapper.find(Link).prop('to')).toBe('/team/emoji/add');
renderWithContext(<EmojiPage {...defaultProps}/>);
expect(screen.getByTestId('emoji-list')).toBeInTheDocument();
expect(screen.getByTestId('permission-gate')).toBeInTheDocument();
expect(screen.getByRole('link')).toHaveAttribute('href', '/team/emoji/add');
});
it('should not render the add button if permission is not granted', () => {
const wrapper: ShallowWrapper = shallow(
<EmojiPage {...defaultProps}/>,
).setProps({teamName: '', actions: {loadRolesIfNeeded: mockLoadRolesIfNeeded}});
expect(wrapper.find(AnyTeamPermissionGate).exists()).toBe(true);
expect(wrapper.find(Link).exists()).toBe(true); // Update this to match your permission setup
renderWithContext(
<EmojiPage
{...defaultProps}
teamName=''
actions={{loadRolesIfNeeded: mockLoadRolesIfNeeded}}
/>,
);
expect(screen.getByTestId('permission-gate')).toBeInTheDocument();
expect(screen.getByRole('link')).toBeInTheDocument();
});
it('should render EmojiList component', () => {
const wrapper = shallow(<EmojiPage {...defaultProps}/>);
expect(wrapper.find(EmojiList).exists()).toBe(true);
renderWithContext(<EmojiPage {...defaultProps}/>);
expect(screen.getByTestId('emoji-list')).toBeInTheDocument();
});
});

View file

@ -1,69 +1,59 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`components/emoji_picker/components/EmojiPickerHeader handleEmojiPickerClose, should have called props.handleEmojiPickerClose 1`] = `
<div
className="emoji-picker__header"
>
<button
className="close emoji-picker__header-close-button"
onClick={[MockFunction]}
type="button"
<div>
<div
class="emoji-picker__header"
>
<span
aria-hidden="true"
<button
class="close emoji-picker__header-close-button"
type="button"
>
×
</span>
<span
className="sr-only"
<span
aria-hidden="true"
>
×
</span>
<span
class="sr-only"
>
Close
</span>
</button>
<h4
class="modal-title emoji-picker__header-title"
>
<MemoizedFormattedMessage
defaultMessage="Close"
id="emoji_picker.close"
/>
</span>
</button>
<h4
className="modal-title emoji-picker__header-title"
>
<MemoizedFormattedMessage
defaultMessage="Emoji Picker"
id="emoji_picker.header"
/>
</h4>
Emoji Picker
</h4>
</div>
</div>
`;
exports[`components/emoji_picker/components/EmojiPickerHeader should match snapshot, 1`] = `
<div
className="emoji-picker__header"
>
<button
className="close emoji-picker__header-close-button"
onClick={[MockFunction]}
type="button"
<div>
<div
class="emoji-picker__header"
>
<span
aria-hidden="true"
<button
class="close emoji-picker__header-close-button"
type="button"
>
×
</span>
<span
className="sr-only"
<span
aria-hidden="true"
>
×
</span>
<span
class="sr-only"
>
Close
</span>
</button>
<h4
class="modal-title emoji-picker__header-title"
>
<MemoizedFormattedMessage
defaultMessage="Close"
id="emoji_picker.close"
/>
</span>
</button>
<h4
className="modal-title emoji-picker__header-title"
>
<MemoizedFormattedMessage
defaultMessage="Emoji Picker"
id="emoji_picker.header"
/>
</h4>
Emoji Picker
</h4>
</div>
</div>
`;

View file

@ -1,35 +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 EmojiPickerHeader from 'components/emoji_picker/components/emoji_picker_header';
import {renderWithContext, screen, userEvent} from 'tests/react_testing_utils';
describe('components/emoji_picker/components/EmojiPickerHeader', () => {
test('should match snapshot, ', () => {
const props = {
handleEmojiPickerClose: jest.fn(),
};
const wrapper = shallow(
const {container} = renderWithContext(
<EmojiPickerHeader {...props}/>,
);
expect(wrapper).toMatchSnapshot();
expect(container).toMatchSnapshot();
});
test('handleEmojiPickerClose, should have called props.handleEmojiPickerClose', () => {
test('handleEmojiPickerClose, should have called props.handleEmojiPickerClose', async () => {
const props = {
handleEmojiPickerClose: jest.fn(),
};
const wrapper = shallow(
const {container} = renderWithContext(
<EmojiPickerHeader {...props}/>,
);
expect(wrapper).toMatchSnapshot();
expect(container).toMatchSnapshot();
wrapper.find('button').first().simulate('click');
await userEvent.click(screen.getAllByRole('button')[0]);
expect(props.handleEmojiPickerClose).toHaveBeenCalledTimes(1);
});
});

View file

@ -1,97 +1,126 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`components/ProductBranding should fallback to ProductChannelsIcon when string icon name is not found in glyphMap 1`] = `
<ProductBrandingContainer
tabIndex={-1}
>
<ProductChannelsIcon
size={24}
/>
<h1
className="sr-only"
<div>
<span
class="ProductBrandingContainer-fBgRMk bpZokb"
tabindex="-1"
>
InvalidProduct
</h1>
<ProductBrandingHeading>
InvalidProduct
</ProductBrandingHeading>
</ProductBrandingContainer>
<svg
data-testid="ProductChannelsIcon"
size="24"
/>
<h1
class="sr-only"
>
InvalidProduct
</h1>
<span
class="ProductBrandingHeading-cfoqHj gJNxBU"
>
InvalidProduct
</span>
</span>
</div>
`;
exports[`components/ProductBranding should render a React element icon when switcherIcon is a React node 1`] = `
<ProductBrandingContainer
tabIndex={-1}
>
<svg
data-testid="custom-icon"
<div>
<span
class="ProductBrandingContainer-fBgRMk bpZokb"
tabindex="-1"
>
<circle
cx="12"
cy="12"
r="10"
/>
</svg>
<h1
className="sr-only"
>
CustomProduct
</h1>
<ProductBrandingHeading>
CustomProduct
</ProductBrandingHeading>
</ProductBrandingContainer>
<svg
data-testid="custom-icon"
>
<circle
cx="12"
cy="12"
r="10"
/>
</svg>
<h1
class="sr-only"
>
CustomProduct
</h1>
<span
class="ProductBrandingHeading-cfoqHj gJNxBU"
>
CustomProduct
</span>
</span>
</div>
`;
exports[`components/ProductBranding should show correct icon glyph when we are on Boards 1`] = `
<ProductBrandingContainer
tabIndex={-1}
>
<ProductBoardsIcon
size={24}
/>
<h1
className="sr-only"
<div>
<span
class="ProductBrandingContainer-fBgRMk bpZokb"
tabindex="-1"
>
Boards
</h1>
<ProductBrandingHeading>
Boards
</ProductBrandingHeading>
</ProductBrandingContainer>
<svg
data-testid="ProductChannelsIcon"
size="24"
/>
<h1
class="sr-only"
>
Boards
</h1>
<span
class="ProductBrandingHeading-cfoqHj gJNxBU"
>
Boards
</span>
</span>
</div>
`;
exports[`components/ProductBranding should show correct icon glyph when we are on Channels 1`] = `
<ProductBrandingContainer
tabIndex={-1}
>
<ProductChannelsIcon
size={24}
/>
<h1
className="sr-only"
<div>
<span
class="ProductBrandingContainer-fBgRMk bpZokb"
tabindex="-1"
>
Channels
</h1>
<ProductBrandingHeading>
Channels
</ProductBrandingHeading>
</ProductBrandingContainer>
<svg
data-testid="ProductChannelsIcon"
size="24"
/>
<h1
class="sr-only"
>
Channels
</h1>
<span
class="ProductBrandingHeading-cfoqHj gJNxBU"
>
Channels
</span>
</span>
</div>
`;
exports[`components/ProductBranding should show correct icon glyph when we are on Playbooks 1`] = `
<ProductBrandingContainer
tabIndex={-1}
>
<ProductPlaybooksIcon
size={24}
/>
<h1
className="sr-only"
<div>
<span
class="ProductBrandingContainer-fBgRMk bpZokb"
tabindex="-1"
>
Playbooks
</h1>
<ProductBrandingHeading>
Playbooks
</ProductBrandingHeading>
</ProductBrandingContainer>
<svg
data-testid="ProductChannelsIcon"
size="24"
/>
<h1
class="sr-only"
>
Playbooks
</h1>
<span
class="ProductBrandingHeading-cfoqHj gJNxBU"
>
Playbooks
</span>
</span>
</div>
`;

View file

@ -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 {renderWithContext, screen} from 'tests/react_testing_utils';
import {TopLevelProducts} from 'utils/constants';
import * as productUtils from 'utils/products';
import {TestHelper} from 'utils/test_helper';
@ -12,37 +12,50 @@ import type {ProductComponent} from 'types/store/plugins';
import ProductBranding from './product_branding';
jest.mock('@mattermost/compass-icons/components', () => {
const actual = jest.requireActual('@mattermost/compass-icons/components');
return {
...actual,
ProductChannelsIcon: (props: any) => (
<svg
data-testid='ProductChannelsIcon'
{...props}
/>
),
};
});
describe('components/ProductBranding', () => {
test('should show correct icon glyph when we are on Channels', () => {
const currentProductSpy = jest.spyOn(productUtils, 'useCurrentProduct');
currentProductSpy.mockReturnValue(null);
const wrapper = shallow(
const {container} = renderWithContext(
<ProductBranding/>,
);
expect(wrapper).toMatchSnapshot();
expect(container).toMatchSnapshot();
});
test('should show correct icon glyph when we are on Playbooks', () => {
const currentProductSpy = jest.spyOn(productUtils, 'useCurrentProduct');
currentProductSpy.mockReturnValue(TestHelper.makeProduct(TopLevelProducts.PLAYBOOKS));
const wrapper = shallow(
const {container} = renderWithContext(
<ProductBranding/>,
);
expect(wrapper).toMatchSnapshot();
expect(container).toMatchSnapshot();
});
test('should show correct icon glyph when we are on Boards', () => {
const currentProductSpy = jest.spyOn(productUtils, 'useCurrentProduct');
currentProductSpy.mockReturnValue(TestHelper.makeProduct(TopLevelProducts.BOARDS));
const wrapper = shallow(
const {container} = renderWithContext(
<ProductBranding/>,
);
expect(wrapper).toMatchSnapshot();
expect(container).toMatchSnapshot();
});
test('should render a React element icon when switcherIcon is a React node', () => {
@ -62,12 +75,12 @@ describe('components/ProductBranding', () => {
};
currentProductSpy.mockReturnValue(productWithReactIcon);
const wrapper = shallow(
const {container} = renderWithContext(
<ProductBranding/>,
);
expect(wrapper).toMatchSnapshot();
expect(wrapper.find('[data-testid="custom-icon"]').exists()).toBe(true);
expect(container).toMatchSnapshot();
expect(screen.getByTestId('custom-icon')).toBeInTheDocument();
});
test('should fallback to ProductChannelsIcon when string icon name is not found in glyphMap', () => {
@ -78,11 +91,11 @@ describe('components/ProductBranding', () => {
};
currentProductSpy.mockReturnValue(productWithInvalidIcon);
const wrapper = shallow(
const {container} = renderWithContext(
<ProductBranding/>,
);
expect(wrapper).toMatchSnapshot();
expect(wrapper.find('ProductChannelsIcon').exists()).toBe(true);
expect(container).toMatchSnapshot();
expect(screen.getByTestId('ProductChannelsIcon')).toBeInTheDocument();
});
});

View file

@ -1,28 +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 * as reactRedux from 'react-redux';
import mockStore from 'tests/test_store';
import {renderWithContext, screen} from 'tests/react_testing_utils';
import {TopLevelProducts} from 'utils/constants';
import * as productUtils from 'utils/products';
import {TestHelper} from 'utils/test_helper';
import ProductBranding from './product_branding';
import ProductBrandingFreeEdition from './product_branding_team_edition';
import ProductMenu, {ProductMenuButton, ProductMenuContainer} from './product_menu';
import ProductMenuItem from './product_menu_item';
import ProductMenuList from './product_menu_list';
import ProductMenu from './product_menu';
jest.mock('./product_branding', () => {
return function MockProductBranding() {
return <div data-testid='product-branding'/>;
};
});
jest.mock('./product_branding_team_edition', () => {
return function MockProductBrandingFreeEdition() {
return <div data-testid='product-branding-free-edition'/>;
};
});
jest.mock('./product_menu_list', () => {
return function MockProductMenuList() {
return <div data-testid='product-menu-list'/>;
};
});
jest.mock('components/onboarding_tasks', () => ({
OnboardingTaskCategory: 'onboardingTask',
OnboardingTasksName: {VISIT_SYSTEM_CONSOLE: 'visit_system_console'},
TaskNameMapToSteps: {visit_system_console: {FINISHED: 999}},
useHandleOnBoardingTaskData: () => jest.fn(),
}));
const spyProduct = jest.spyOn(productUtils, 'useCurrentProductId');
spyProduct.mockReturnValue(null);
describe('components/global/product_switcher', () => {
const useDispatchMock = jest.spyOn(reactRedux, 'useDispatch');
const useSelectorMock = jest.spyOn(reactRedux, 'useSelector');
beforeEach(() => {
const products = [
TestHelper.makeProduct(TopLevelProducts.BOARDS),
@ -30,101 +46,90 @@ describe('components/global/product_switcher', () => {
];
const spyProducts = jest.spyOn(productUtils, 'useProducts');
spyProducts.mockReturnValue(products);
useDispatchMock.mockClear();
useSelectorMock.mockClear();
});
it('should match snapshot', () => {
const state = {
views: {
productMenu: {
switcherOpen: false,
const baseState = {
entities: {
general: {
license: {
IsLicensed: 'true',
},
},
};
useSelectorMock.mockReturnValue(true);
useSelectorMock.mockReturnValueOnce(true);
useSelectorMock.mockReturnValueOnce({IsLicensed: 'true'});
const store = mockStore(state);
const dummyDispatch = jest.fn();
useDispatchMock.mockReturnValue(dummyDispatch);
const wrapper = shallow(<ProductMenu/>, {
wrappingComponent: reactRedux.Provider,
wrappingComponentProps: {store},
});
},
views: {
productMenu: {
switcherOpen: false,
},
},
};
expect(wrapper).toMatchSnapshot();
it('should match snapshot', () => {
const {container} = renderWithContext(
<ProductMenu/>,
baseState,
);
expect(container).toMatchSnapshot();
});
it('should match snapshot without license', () => {
const state = {
views: {
productMenu: {
switcherOpen: false,
...baseState,
entities: {
...baseState.entities,
general: {
...baseState.entities.general,
license: {
IsLicensed: 'false',
},
},
},
};
useSelectorMock.mockReturnValue(true);
useSelectorMock.mockReturnValueOnce(true);
useSelectorMock.mockReturnValueOnce({IsLicensed: 'false'});
const store = mockStore(state);
const dummyDispatch = jest.fn();
useDispatchMock.mockReturnValue(dummyDispatch);
const wrapper = shallow(<ProductMenu/>, {
wrappingComponent: reactRedux.Provider,
wrappingComponentProps: {store},
});
expect(wrapper).toMatchSnapshot();
const {container} = renderWithContext(
<ProductMenu/>,
state,
);
expect(container).toMatchSnapshot();
});
it('should render once when there are no top level products available', () => {
const state = {
users: {
currentUserId: 'test_id',
},
...baseState,
views: {
...baseState.views,
productMenu: {
switcherOpen: true,
},
},
};
const store = mockStore(state);
const dummyDispatch = jest.fn();
useDispatchMock.mockReturnValue(dummyDispatch);
useSelectorMock.mockReturnValue(true);
useSelectorMock.mockReturnValueOnce(true);
useSelectorMock.mockReturnValueOnce({IsLicensed: 'true'});
const wrapper = shallow(<ProductMenu/>, {
wrappingComponent: reactRedux.Provider,
wrappingComponentProps: {store},
});
const {container} = renderWithContext(
<ProductMenu/>,
state,
);
const spyProducts = jest.spyOn(productUtils, 'useProducts');
spyProducts.mockReturnValue([]);
expect(wrapper.find(ProductMenuItem).at(0)).toHaveLength(1);
expect(wrapper).toMatchSnapshot();
const menuItems = screen.getAllByRole('menuitem');
expect(menuItems.length).toBeGreaterThanOrEqual(1);
expect(menuItems.at(0)).toBeDefined();
expect(container).toMatchSnapshot();
});
it('should render the correct amount of times when there are products available', () => {
const state = {
...baseState,
views: {
...baseState.views,
productMenu: {
switcherOpen: true,
},
},
};
const store = mockStore(state);
const dummyDispatch = jest.fn();
useDispatchMock.mockReturnValue(dummyDispatch);
useSelectorMock.mockReturnValue(true);
useSelectorMock.mockReturnValueOnce(true);
useSelectorMock.mockReturnValueOnce({IsLicensed: 'true'});
const wrapper = shallow(<ProductMenu/>, {
wrappingComponent: reactRedux.Provider,
wrappingComponentProps: {store},
});
const products = [
TestHelper.makeProduct(TopLevelProducts.BOARDS),
TestHelper.makeProduct(TopLevelProducts.PLAYBOOKS),
@ -133,172 +138,172 @@ describe('components/global/product_switcher', () => {
const spyProducts = jest.spyOn(productUtils, 'useProducts');
spyProducts.mockReturnValue(products);
expect(wrapper.find(ProductMenuItem)).toHaveLength(3);
expect(wrapper).toMatchSnapshot();
const {container} = renderWithContext(
<ProductMenu/>,
state,
);
// Channels + 2 products
expect(screen.getAllByRole('menuitem')).toHaveLength(3);
expect(container).toMatchSnapshot();
});
it('should have an active button state when the switcher menu is open', () => {
const state = {
...baseState,
views: {
...baseState.views,
productMenu: {
switcherOpen: true,
},
},
};
const store = mockStore(state);
const dummyDispatch = jest.fn();
useDispatchMock.mockReturnValue(dummyDispatch);
useSelectorMock.mockReturnValue(true);
useSelectorMock.mockReturnValueOnce(true);
useSelectorMock.mockReturnValueOnce({IsLicensed: 'true'});
const wrapper = shallow(<ProductMenu/>, {
wrappingComponent: reactRedux.Provider,
wrappingComponentProps: {store},
});
const setState = jest.fn();
const useStateSpy = jest.spyOn(React, 'useState');
useStateSpy.mockImplementation(() => [false, setState]);
const {container} = renderWithContext(
<ProductMenu/>,
state,
);
wrapper.find(ProductMenuContainer).simulate('click');
expect(wrapper.find(ProductMenuButton).props()['aria-expanded']).toEqual(true);
expect(wrapper).toMatchSnapshot();
const button = screen.getByRole('button', {name: 'Product switch menu'});
expect(button).toHaveAttribute('aria-expanded', 'true');
expect(container).toMatchSnapshot();
});
it('should match snapshot with product switcher menu', () => {
const state = {
...baseState,
views: {
...baseState.views,
productMenu: {
switcherOpen: true,
},
},
};
const store = mockStore(state);
const dummyDispatch = jest.fn();
useDispatchMock.mockReturnValue(dummyDispatch);
useSelectorMock.mockReturnValue(true);
useSelectorMock.mockReturnValueOnce(true);
useSelectorMock.mockReturnValueOnce({IsLicensed: 'true'});
const wrapper = shallow(<ProductMenu/>, {
wrappingComponent: reactRedux.Provider,
wrappingComponentProps: {store},
});
expect(wrapper.find(ProductMenuList)).toHaveLength(1);
expect(wrapper).toMatchSnapshot();
const {container} = renderWithContext(
<ProductMenu/>,
state,
);
expect(screen.getByTestId('product-menu-list')).toBeInTheDocument();
expect(container).toMatchSnapshot();
});
it('should render ProductBrandingFreeEdition for Entry license', () => {
const state = {
views: {
productMenu: {
switcherOpen: false,
...baseState,
entities: {
...baseState.entities,
general: {
...baseState.entities.general,
license: {
IsLicensed: 'true',
SkuShortName: 'entry',
},
},
},
};
const store = mockStore(state);
const dummyDispatch = jest.fn();
useDispatchMock.mockReturnValue(dummyDispatch);
useSelectorMock.mockReturnValue(true);
useSelectorMock.mockReturnValueOnce(true);
useSelectorMock.mockReturnValueOnce({IsLicensed: 'true', SkuShortName: 'entry'});
const wrapper = shallow(<ProductMenu/>, {
wrappingComponent: reactRedux.Provider,
wrappingComponentProps: {store},
});
expect(wrapper.find(ProductBrandingFreeEdition)).toHaveLength(1);
expect(wrapper.find(ProductBranding)).toHaveLength(0);
renderWithContext(
<ProductMenu/>,
state,
);
expect(screen.getByTestId('product-branding-free-edition')).toBeInTheDocument();
expect(screen.queryByTestId('product-branding')).not.toBeInTheDocument();
});
it('should render ProductBrandingFreeEdition for unlicensed', () => {
const state = {
views: {
productMenu: {
switcherOpen: false,
...baseState,
entities: {
...baseState.entities,
general: {
...baseState.entities.general,
license: {
IsLicensed: 'false',
},
},
},
};
const store = mockStore(state);
const dummyDispatch = jest.fn();
useDispatchMock.mockReturnValue(dummyDispatch);
useSelectorMock.mockReturnValue(true);
useSelectorMock.mockReturnValueOnce(true);
useSelectorMock.mockReturnValueOnce({IsLicensed: 'false'});
const wrapper = shallow(<ProductMenu/>, {
wrappingComponent: reactRedux.Provider,
wrappingComponentProps: {store},
});
expect(wrapper.find(ProductBrandingFreeEdition)).toHaveLength(1);
expect(wrapper.find(ProductBranding)).toHaveLength(0);
renderWithContext(
<ProductMenu/>,
state,
);
expect(screen.getByTestId('product-branding-free-edition')).toBeInTheDocument();
expect(screen.queryByTestId('product-branding')).not.toBeInTheDocument();
});
it('should render ProductBranding for Professional license', () => {
const state = {
views: {
productMenu: {
switcherOpen: false,
...baseState,
entities: {
...baseState.entities,
general: {
...baseState.entities.general,
license: {
IsLicensed: 'true',
SkuShortName: 'professional',
},
},
},
};
const store = mockStore(state);
const dummyDispatch = jest.fn();
useDispatchMock.mockReturnValue(dummyDispatch);
useSelectorMock.mockReturnValue(true);
useSelectorMock.mockReturnValueOnce(true);
useSelectorMock.mockReturnValueOnce({IsLicensed: 'true', SkuShortName: 'professional'});
const wrapper = shallow(<ProductMenu/>, {
wrappingComponent: reactRedux.Provider,
wrappingComponentProps: {store},
});
expect(wrapper.find(ProductBranding)).toHaveLength(1);
expect(wrapper.find(ProductBrandingFreeEdition)).toHaveLength(0);
renderWithContext(
<ProductMenu/>,
state,
);
expect(screen.getByTestId('product-branding')).toBeInTheDocument();
expect(screen.queryByTestId('product-branding-free-edition')).not.toBeInTheDocument();
});
it('should render ProductBranding for Enterprise license', () => {
const state = {
views: {
productMenu: {
switcherOpen: false,
...baseState,
entities: {
...baseState.entities,
general: {
...baseState.entities.general,
license: {
IsLicensed: 'true',
SkuShortName: 'enterprise',
},
},
},
};
const store = mockStore(state);
const dummyDispatch = jest.fn();
useDispatchMock.mockReturnValue(dummyDispatch);
useSelectorMock.mockReturnValue(true);
useSelectorMock.mockReturnValueOnce(true);
useSelectorMock.mockReturnValueOnce({IsLicensed: 'true', SkuShortName: 'enterprise'});
const wrapper = shallow(<ProductMenu/>, {
wrappingComponent: reactRedux.Provider,
wrappingComponentProps: {store},
});
expect(wrapper.find(ProductBranding)).toHaveLength(1);
expect(wrapper.find(ProductBrandingFreeEdition)).toHaveLength(0);
renderWithContext(
<ProductMenu/>,
state,
);
expect(screen.getByTestId('product-branding')).toBeInTheDocument();
expect(screen.queryByTestId('product-branding-free-edition')).not.toBeInTheDocument();
});
it('should match snapshot for Entry license', () => {
const state = {
views: {
productMenu: {
switcherOpen: false,
...baseState,
entities: {
...baseState.entities,
general: {
...baseState.entities.general,
license: {
IsLicensed: 'true',
SkuShortName: 'entry',
},
},
},
};
const store = mockStore(state);
const dummyDispatch = jest.fn();
useDispatchMock.mockReturnValue(dummyDispatch);
useSelectorMock.mockReturnValue(true);
useSelectorMock.mockReturnValueOnce(true);
useSelectorMock.mockReturnValueOnce({IsLicensed: 'true', SkuShortName: 'entry'});
const wrapper = shallow(<ProductMenu/>, {
wrappingComponent: reactRedux.Provider,
wrappingComponentProps: {store},
});
expect(wrapper).toMatchSnapshot();
const {container} = renderWithContext(
<ProductMenu/>,
state,
);
expect(container).toMatchSnapshot();
});
});

View file

@ -1,32 +1,29 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {shallow} from 'enzyme';
import React from 'react';
import * as reactRedux from 'react-redux';
import type {UserProfile} from '@mattermost/types/users';
import type {DeepPartial} from '@mattermost/types/utilities';
import MenuGroup from 'components/widgets/menu/menu_group';
import {renderWithContext, screen, userEvent} from 'tests/react_testing_utils';
import {TestHelper} from 'utils/test_helper';
import type {GlobalState} from 'types/store';
import ProductMenuList from './product_menu_list';
import type {Props as ProductMenuListProps} from './product_menu_list';
jest.mock('react-redux', () => ({
...jest.requireActual('react-redux'),
useSelector: jest.fn(),
jest.mock('components/widgets/menu/menu_items/menu_cloud_trial', () => () => null);
jest.mock('components/widgets/menu/menu_items/menu_item_cloud_limit', () => () => null);
jest.mock('components/permissions_gates/system_permission_gate', () => ({children}: {children: React.ReactNode}) => <>{children}</>);
jest.mock('components/permissions_gates/team_permission_gate', () => ({children}: {children: React.ReactNode}) => <>{children}</>);
jest.mock('components/onboarding_tasks', () => ({
VisitSystemConsoleTour: () => null,
}));
jest.mock('components/widgets/menu/menu_items/restricted_indicator', () => () => <div data-testid='RestrictedIndicator'/>);
describe('components/global/product_switcher_menu', () => {
let useSelectorMock: jest.SpyInstance;
const getMenuWrapper = (props: ProductMenuListProps) => {
const wrapper = shallow(<ProductMenuList {...props}/>);
return wrapper.find(MenuGroup).shallow();
};
const user = TestHelper.getUserMock({
id: 'test-user-id',
username: 'username',
@ -59,15 +56,40 @@ describe('components/global/product_switcher_menu', () => {
},
};
beforeEach(() => {
useSelectorMock = jest.spyOn(reactRedux, 'useSelector');
useSelectorMock.mockReturnValue(true);
});
const adminState: DeepPartial<GlobalState> = {
entities: {
users: {
currentUserId: 'test-user-id',
profiles: {
'test-user-id': {
id: 'test-user-id',
username: 'username',
roles: 'system_admin system_user',
} as UserProfile,
},
},
},
};
const nonAdminState: DeepPartial<GlobalState> = {
entities: {
users: {
currentUserId: 'test-user-id',
profiles: {
'test-user-id': {
id: 'test-user-id',
username: 'username',
roles: 'system_user',
} as UserProfile,
},
},
},
};
test('should match snapshot with id', () => {
const props = {...defaultProps, id: 'product-switcher-menu-test'};
const wrapper = shallow(<ProductMenuList {...props}/>);
expect(wrapper).toMatchSnapshot();
const {container} = renderWithContext(<ProductMenuList {...props}/>, adminState);
expect(container).toMatchSnapshot();
});
test('should not render if the user is not logged in', () => {
@ -75,8 +97,8 @@ describe('components/global/product_switcher_menu', () => {
...defaultProps,
currentUser: undefined as unknown as UserProfile,
};
const wrapper = shallow(<ProductMenuList {...props}/>);
expect(wrapper.type()).toEqual(null);
const {container} = renderWithContext(<ProductMenuList {...props}/>, adminState);
expect(container.firstChild).toBeNull();
});
test('should match snapshot with most of the thing enabled', () => {
@ -90,8 +112,8 @@ describe('components/global/product_switcher_menu', () => {
canManageIntegrations: true,
enablePluginMarketplace: true,
};
const wrapper = shallow(<ProductMenuList {...props}/>);
expect(wrapper).toMatchSnapshot();
const {container} = renderWithContext(<ProductMenuList {...props}/>, adminState);
expect(container).toMatchSnapshot();
});
test('should match userGroups snapshot with cloud free', () => {
@ -101,8 +123,8 @@ describe('components/global/product_switcher_menu', () => {
isStarterFree: true,
isFreeTrial: false,
};
const wrapper = shallow(<ProductMenuList {...props}/>);
expect(wrapper.find('#userGroups')).toMatchSnapshot();
const {container} = renderWithContext(<ProductMenuList {...props}/>, adminState);
expect(container.querySelector('#userGroups')).toMatchSnapshot();
});
test('should match userGroups snapshot with cloud free trial', () => {
@ -112,8 +134,8 @@ describe('components/global/product_switcher_menu', () => {
isStarterFree: false,
isFreeTrial: true,
};
const wrapper = shallow(<ProductMenuList {...props}/>);
expect(wrapper.find('#userGroups')).toMatchSnapshot();
const {container} = renderWithContext(<ProductMenuList {...props}/>, adminState);
expect(container.querySelector('#userGroups')).toMatchSnapshot();
});
test('should match userGroups snapshot with EnableCustomGroups config', () => {
@ -123,8 +145,8 @@ describe('components/global/product_switcher_menu', () => {
isStarterFree: false,
isFreeTrial: false,
};
const wrapper = shallow(<ProductMenuList {...props}/>);
expect(wrapper.find('#userGroups')).toMatchSnapshot();
const {container} = renderWithContext(<ProductMenuList {...props}/>, adminState);
expect(container.querySelector('#userGroups')).toMatchSnapshot();
});
test('user groups button is disabled for free', () => {
@ -134,21 +156,19 @@ describe('components/global/product_switcher_menu', () => {
isStarterFree: true,
isFreeTrial: false,
};
const wrapper = getMenuWrapper(props);
expect(wrapper.find('#userGroups').prop('disabled')).toBe(true);
const {container} = renderWithContext(<ProductMenuList {...props}/>, adminState);
expect(container.querySelector('#userGroups button')).toBeDisabled();
});
test('should hide RestrictedIndicator if user is not admin', () => {
useSelectorMock.mockReturnValueOnce(false);
const props = {
...defaultProps,
isStarterFree: true,
};
const wrapper = shallow(<ProductMenuList {...props}/>);
const {container} = renderWithContext(<ProductMenuList {...props}/>, nonAdminState);
expect(wrapper.find('RestrictedIndicator').exists()).toBe(false);
expect(container.querySelector('[data-testid="RestrictedIndicator"]')).toBeNull();
});
describe('should show integrations', () => {
@ -157,8 +177,8 @@ describe('components/global/product_switcher_menu', () => {
...defaultProps,
enableIncomingWebhooks: true,
};
const wrapper = shallow(<ProductMenuList {...props}/>);
expect(wrapper.find('#integrations').prop('show')).toBe(true);
const {container} = renderWithContext(<ProductMenuList {...props}/>, adminState);
expect(container.querySelector('#integrations')).not.toBeNull();
});
it('when outgoing webhooks enabled', () => {
@ -166,8 +186,8 @@ describe('components/global/product_switcher_menu', () => {
...defaultProps,
enableOutgoingWebhooks: true,
};
const wrapper = shallow(<ProductMenuList {...props}/>);
expect(wrapper.find('#integrations').prop('show')).toBe(true);
const {container} = renderWithContext(<ProductMenuList {...props}/>, adminState);
expect(container.querySelector('#integrations')).not.toBeNull();
});
it('when slash commands enabled', () => {
@ -175,8 +195,8 @@ describe('components/global/product_switcher_menu', () => {
...defaultProps,
enableCommands: true,
};
const wrapper = getMenuWrapper(props);
expect(wrapper.find('#integrations').prop('show')).toBe(true);
const {container} = renderWithContext(<ProductMenuList {...props}/>, adminState);
expect(container.querySelector('#integrations')).not.toBeNull();
});
it('when oauth providers enabled', () => {
@ -184,8 +204,8 @@ describe('components/global/product_switcher_menu', () => {
...defaultProps,
enableOAuthServiceProvider: true,
};
const wrapper = getMenuWrapper(props);
expect(wrapper.find('#integrations').prop('show')).toBe(true);
const {container} = renderWithContext(<ProductMenuList {...props}/>, adminState);
expect(container.querySelector('#integrations')).not.toBeNull();
});
it('when can manage system bots', () => {
@ -193,8 +213,8 @@ describe('components/global/product_switcher_menu', () => {
...defaultProps,
canManageSystemBots: true,
};
const wrapper = getMenuWrapper(props);
expect(wrapper.find('#integrations').prop('show')).toBe(true);
const {container} = renderWithContext(<ProductMenuList {...props}/>, adminState);
expect(container.querySelector('#integrations')).not.toBeNull();
});
it('unless cannot manage integrations', () => {
@ -203,18 +223,19 @@ describe('components/global/product_switcher_menu', () => {
canManageIntegrations: false,
enableCommands: true,
};
const wrapper = getMenuWrapper(props);
expect(wrapper.find('#integrations').prop('show')).toBe(false);
const {container} = renderWithContext(<ProductMenuList {...props}/>, adminState);
expect(container.querySelector('#integrations')).toBeNull();
});
it('should show integrations modal', () => {
it('should show integrations modal', async () => {
const props = {
...defaultProps,
enableIncomingWebhooks: true,
teamName: 'test-team',
};
const wrapper = getMenuWrapper(props);
wrapper.find('#integrations').simulate('click');
expect(wrapper).toMatchSnapshot();
const {container} = renderWithContext(<ProductMenuList {...props}/>, adminState);
await userEvent.click(screen.getByText('Integrations'));
expect(container).toMatchSnapshot();
});
});
});

View file

@ -1,39 +1,16 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`components/global/AtMentionsButton should match snapshot 1`] = `
<WithTooltip
title={
<React.Fragment>
<Memo(MemoizedFormattedMessage)
defaultMessage="Recent mentions"
id="channel_header.recentMentions"
/>
<Memo(KeyboardShortcutSequence)
hideDescription={true}
isInsideTooltip={true}
shortcut={
Object {
"default": Object {
"defaultMessage": "Recent mentions: Ctrl|Shift|M",
"id": "shortcuts.nav.recent_mentions",
},
"mac": Object {
"defaultMessage": "Recent mentions: ⌘|Shift|M",
"id": "shortcuts.nav.recent_mentions.mac",
},
}
}
/>
</React.Fragment>
}
>
<HeaderIconButton
<div>
<button
aria-controls="searchContainer"
aria-expanded={false}
aria-expanded="false"
aria-label="Recent mentions"
icon="at"
onClick={[Function]}
toggled={false}
/>
</WithTooltip>
class="HeaderIconButton"
>
<i
class="icon-at"
/>
</button>
</div>
`;

View file

@ -1,44 +1,45 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {shallow} from 'enzyme';
import React from 'react';
import IconButton from 'components/global_header/header_icon_button';
import {showMentions} from 'actions/views/rhs';
import {renderWithContext, screen, userEvent} from 'tests/react_testing_utils';
import type {GlobalState} from 'types/store';
import AtMentionsButton from './at_mentions_button';
const mockDispatch = jest.fn();
let mockState: GlobalState;
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('actions/views/rhs', () => ({
closeRightHandSide: jest.fn(() => ({type: 'MOCK_CLOSE_RHS'})),
showMentions: jest.fn(() => ({type: 'MOCK_SHOW_MENTIONS'})),
}));
describe('components/global/AtMentionsButton', () => {
beforeEach(() => {
mockState = {views: {rhs: {isSidebarOpen: true}}} as GlobalState;
});
const initialState = {
views: {
rhs: {
isSidebarOpen: true,
},
},
} as unknown as GlobalState;
test('should match snapshot', () => {
const wrapper = shallow(
const {container} = renderWithContext(
<AtMentionsButton/>,
initialState,
);
expect(wrapper).toMatchSnapshot();
expect(container).toMatchSnapshot();
});
test('should show active mentions', () => {
const wrapper = shallow(
test('should show active mentions', async () => {
renderWithContext(
<AtMentionsButton/>,
initialState,
);
wrapper.find(IconButton).simulate('click', {
preventDefault: jest.fn(),
});
expect(mockDispatch).toHaveBeenCalledTimes(1);
await userEvent.click(screen.getByRole('button', {name: 'Recent mentions'}));
expect(showMentions).toHaveBeenCalledTimes(1);
});
});

View file

@ -1,23 +1,26 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {mount} from 'enzyme';
import React from 'react';
import * as reactRedux from 'react-redux';
import * as cloudActions from 'mattermost-redux/actions/cloud';
import mockStore from 'tests/test_store';
import {renderWithContext, screen} from 'tests/react_testing_utils';
import {CloudProducts} from 'utils/constants';
import PlanUpgradeButton from './index';
describe('components/global/PlanUpgradeButton', () => {
const useDispatchMock = jest.spyOn(reactRedux, 'useDispatch');
jest.mock('components/common/hooks/useOpenPricingModal', () => ({
__esModule: true,
default: () => ({openPricingModal: jest.fn(), isAirGapped: false}),
}));
describe('components/global/PlanUpgradeButton', () => {
beforeEach(() => {
useDispatchMock.mockClear();
jest.spyOn(cloudActions, 'getCloudSubscription').mockReturnValue({type: 'MOCK_GET_CLOUD_SUBSCRIPTION'} as any);
jest.spyOn(cloudActions, 'getCloudProducts').mockReturnValue({type: 'MOCK_GET_CLOUD_PRODUCTS'} as any);
});
const initialState = {
entities: {
general: {
@ -49,28 +52,19 @@ describe('components/global/PlanUpgradeButton', () => {
},
},
};
it('should show Upgrade button in global header for admin users, cloud free subscription', () => {
const state = {
...initialState,
};
it('should show Upgrade button in global header for admin users, cloud free subscription', () => {
const cloudSubscriptionSpy = jest.spyOn(cloudActions, 'getCloudSubscription');
const cloudProductsSpy = jest.spyOn(cloudActions, 'getCloudProducts');
const store = mockStore(state);
const dummyDispatch = jest.fn();
useDispatchMock.mockReturnValue(dummyDispatch);
const wrapper = mount(
<reactRedux.Provider store={store}>
<PlanUpgradeButton/>
</reactRedux.Provider>,
renderWithContext(
<PlanUpgradeButton/>,
initialState,
);
expect(cloudSubscriptionSpy).toHaveBeenCalledTimes(1);
expect(cloudProductsSpy).toHaveBeenCalledTimes(1);
expect(wrapper.find('#UpgradeButton').exists()).toEqual(true);
expect(screen.getByRole('button', {name: 'View plans'})).toBeInTheDocument();
});
it('should show Upgrade button in global header for admin users, cloud and enterprise trial subscription', () => {
@ -89,18 +83,12 @@ describe('components/global/PlanUpgradeButton', () => {
},
};
const store = mockStore(state);
const dummyDispatch = jest.fn();
useDispatchMock.mockReturnValue(dummyDispatch);
const wrapper = mount(
<reactRedux.Provider store={store}>
<PlanUpgradeButton/>
</reactRedux.Provider>,
renderWithContext(
<PlanUpgradeButton/>,
state,
);
expect(wrapper.find('#UpgradeButton').exists()).toEqual(true);
expect(screen.getByRole('button', {name: 'View plans'})).toBeInTheDocument();
});
it('should not show for cloud enterprise non-trial', () => {
@ -119,18 +107,12 @@ describe('components/global/PlanUpgradeButton', () => {
},
};
const store = mockStore(state);
const dummyDispatch = jest.fn();
useDispatchMock.mockReturnValue(dummyDispatch);
const wrapper = mount(
<reactRedux.Provider store={store}>
<PlanUpgradeButton/>
</reactRedux.Provider>,
renderWithContext(
<PlanUpgradeButton/>,
state,
);
expect(wrapper.find('#UpgradeButton').exists()).toEqual(false);
expect(screen.queryByRole('button', {name: 'View plans'})).not.toBeInTheDocument();
});
it('should not show for cloud professional product', () => {
@ -149,18 +131,12 @@ describe('components/global/PlanUpgradeButton', () => {
},
};
const store = mockStore(state);
const dummyDispatch = jest.fn();
useDispatchMock.mockReturnValue(dummyDispatch);
const wrapper = mount(
<reactRedux.Provider store={store}>
<PlanUpgradeButton/>
</reactRedux.Provider>,
renderWithContext(
<PlanUpgradeButton/>,
state,
);
expect(wrapper.find('#UpgradeButton').exists()).toEqual(false);
expect(screen.queryByRole('button', {name: 'View plans'})).not.toBeInTheDocument();
});
it('should not show Upgrade button in global header for non admin cloud users', () => {
@ -172,18 +148,12 @@ describe('components/global/PlanUpgradeButton', () => {
},
};
const store = mockStore(state);
const dummyDispatch = jest.fn();
useDispatchMock.mockReturnValue(dummyDispatch);
const wrapper = mount(
<reactRedux.Provider store={store}>
<PlanUpgradeButton/>
</reactRedux.Provider>,
renderWithContext(
<PlanUpgradeButton/>,
state,
);
expect(wrapper.find('#UpgradeButton').exists()).toEqual(false);
expect(screen.queryByRole('button', {name: 'View plans'})).not.toBeInTheDocument();
});
it('should not show Upgrade button in global header for non admin self hosted users', () => {
@ -195,22 +165,16 @@ describe('components/global/PlanUpgradeButton', () => {
},
};
state.entities.general.license = {
IsLicensed: 'false', // starter
IsLicensed: 'false',
Cloud: 'false',
};
const store = mockStore(state);
const dummyDispatch = jest.fn();
useDispatchMock.mockReturnValue(dummyDispatch);
const wrapper = mount(
<reactRedux.Provider store={store}>
<PlanUpgradeButton/>
</reactRedux.Provider>,
renderWithContext(
<PlanUpgradeButton/>,
state,
);
expect(wrapper.find('#UpgradeButton').exists()).toEqual(false);
expect(screen.queryByRole('button', {name: 'View plans'})).not.toBeInTheDocument();
});
it('should not show Upgrade button in global header for non enterprise edition self hosted users', () => {
@ -231,18 +195,12 @@ describe('components/global/PlanUpgradeButton', () => {
BuildEnterpriseReady: 'false',
};
const store = mockStore(state);
const dummyDispatch = jest.fn();
useDispatchMock.mockReturnValue(dummyDispatch);
const wrapper = mount(
<reactRedux.Provider store={store}>
<PlanUpgradeButton/>
</reactRedux.Provider>,
renderWithContext(
<PlanUpgradeButton/>,
state,
);
expect(wrapper.find('#UpgradeButton').exists()).toEqual(false);
expect(screen.queryByRole('button', {name: 'View plans'})).not.toBeInTheDocument();
});
it('should NOT show Upgrade button in global header for self hosted non trial and licensed', () => {
@ -256,19 +214,13 @@ describe('components/global/PlanUpgradeButton', () => {
const cloudSubscriptionSpy = jest.spyOn(cloudActions, 'getCloudSubscription');
const cloudProductsSpy = jest.spyOn(cloudActions, 'getCloudProducts');
const store = mockStore(state);
const dummyDispatch = jest.fn();
useDispatchMock.mockReturnValue(dummyDispatch);
const wrapper = mount(
<reactRedux.Provider store={store}>
<PlanUpgradeButton/>
</reactRedux.Provider>,
renderWithContext(
<PlanUpgradeButton/>,
state,
);
expect(cloudSubscriptionSpy).toHaveBeenCalledTimes(0); // no calls to cloud endpoints for non cloud
expect(cloudSubscriptionSpy).toHaveBeenCalledTimes(0);
expect(cloudProductsSpy).toHaveBeenCalledTimes(0);
expect(wrapper.find('#UpgradeButton').exists()).toEqual(false);
expect(screen.queryByRole('button', {name: 'View plans'})).not.toBeInTheDocument();
});
});

View file

@ -1,21 +1,16 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`components/global/AtMentionsButton should match snapshot 1`] = `
<WithTooltip
title={
<Memo(MemoizedFormattedMessage)
defaultMessage="Saved messages"
id="channel_header.flagged"
/>
}
>
<HeaderIconButton
<div>
<button
aria-controls="searchContainer"
aria-expanded={false}
aria-expanded="false"
aria-label="Saved messages"
icon="bookmark-outline"
onClick={[Function]}
toggled={false}
/>
</WithTooltip>
class="HeaderIconButton"
>
<i
class="icon-bookmark-outline"
/>
</button>
</div>
`;

View file

@ -1,44 +1,45 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {shallow} from 'enzyme';
import React from 'react';
import IconButton from 'components/global_header/header_icon_button';
import {showFlaggedPosts} from 'actions/views/rhs';
import {renderWithContext, screen, userEvent} from 'tests/react_testing_utils';
import type {GlobalState} from 'types/store';
import SavedPostsButton from './saved_posts_button';
const mockDispatch = jest.fn();
let mockState: GlobalState;
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('actions/views/rhs', () => ({
closeRightHandSide: jest.fn(() => ({type: 'MOCK_CLOSE_RHS'})),
showFlaggedPosts: jest.fn(() => ({type: 'MOCK_SHOW_FLAGGED_POSTS'})),
}));
describe('components/global/AtMentionsButton', () => {
beforeEach(() => {
mockState = {views: {rhs: {isSidebarOpen: true}}} as GlobalState;
});
const initialState = {
views: {
rhs: {
isSidebarOpen: true,
},
},
} as unknown as GlobalState;
test('should match snapshot', () => {
const wrapper = shallow(
const {container} = renderWithContext(
<SavedPostsButton/>,
initialState,
);
expect(wrapper).toMatchSnapshot();
expect(container).toMatchSnapshot();
});
test('should show active mentions', () => {
const wrapper = shallow(
test('should show active mentions', async () => {
renderWithContext(
<SavedPostsButton/>,
initialState,
);
wrapper.find(IconButton).simulate('click', {
preventDefault: jest.fn(),
});
expect(mockDispatch).toHaveBeenCalledTimes(1);
await userEvent.click(screen.getByRole('button', {name: 'Saved messages'}));
expect(showFlaggedPosts).toHaveBeenCalledTimes(1);
});
});

View file

@ -1,45 +1,45 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`components/DialogIntroductionText should not fail on empty value 1`] = `
<DialogIntroductionText
emojiMap={
EmojiMap {
"customEmojis": Map {},
"customEmojisArray": Array [],
}
}
id="testblankvalue"
value=""
>
<div>
<span
dangerouslySetInnerHTML={
Object {
"__html": "",
}
}
id="testblankvalue"
/>
</DialogIntroductionText>
</div>
`;
exports[`components/DialogIntroductionText should render message with supported values 1`] = `
<DialogIntroductionText
emojiMap={
EmojiMap {
"customEmojis": Map {},
"customEmojisArray": Array [],
}
}
id="testsupported"
value="**bold** *italic* [link](https://mattermost.com/) <br/> [link target blank](!https://mattermost.com/)"
>
<div>
<span
dangerouslySetInnerHTML={
Object {
"__html": "<p><strong>bold</strong> <em>italic</em> <a class=\\"theme markdown__link\\" href=\\"https://mattermost.com/\\" rel=\\"noreferrer\\" target=\\"_blank\\">link</a> &lt;br/&gt; <a class=\\"theme markdown__link\\" href=\\"!https://mattermost.com/\\" rel=\\"noreferrer\\" target=\\"_blank\\">link target blank</a></p>",
}
}
id="testsupported"
/>
</DialogIntroductionText>
>
<p>
<strong>
bold
</strong>
<em>
italic
</em>
<a
class="theme markdown__link"
href="https://mattermost.com/"
rel="noreferrer"
target="_blank"
>
link
</a>
&lt;br/&gt;
<a
class="theme markdown__link"
href="!https://mattermost.com/"
rel="noreferrer"
target="_blank"
>
link target blank
</a>
</p>
</span>
</div>
`;

View file

@ -1,11 +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 RadioSetting from 'components/widgets/settings/radio_setting';
import TextSetting from 'components/widgets/settings/text_setting';
import {render, screen} from 'tests/react_testing_utils';
import DialogElement from './dialog_element';
@ -23,29 +21,29 @@ describe('components/interactive_dialog/DialogElement', () => {
};
it('type textarea', () => {
const wrapper = shallow(
render(
<DialogElement
{...baseDialogProps}
type='textarea'
/>,
);
expect(wrapper.find(TextSetting).dive().find('textarea').exists()).toBe(true);
expect(document.querySelector('textarea')).toBeInTheDocument();
});
it('subtype blank', () => {
const wrapper = shallow(
render(
<DialogElement
{...baseDialogProps}
subtype=''
/>,
);
expect(wrapper.find(TextSetting).props().type).toEqual('text');
expect(screen.getByTestId('testinginput')).toHaveAttribute('type', 'text');
});
describe('subtype number', () => {
test('value is 0', () => {
const wrapper = shallow(
render(
<DialogElement
{...baseDialogProps}
type='text'
@ -53,11 +51,11 @@ describe('components/interactive_dialog/DialogElement', () => {
value={0}
/>,
);
expect(wrapper.find(TextSetting).props().value).toEqual(0);
expect(screen.getByTestId('testingnumber')).toHaveValue(0);
});
test('value is 123', () => {
const wrapper = shallow(
render(
<DialogElement
{...baseDialogProps}
type='text'
@ -65,28 +63,28 @@ describe('components/interactive_dialog/DialogElement', () => {
value={123}
/>,
);
expect(wrapper.find(TextSetting).props().value).toEqual(123);
expect(screen.getByTestId('testingnumber')).toHaveValue(123);
});
});
it('subtype email', () => {
const wrapper = shallow(
render(
<DialogElement
{...baseDialogProps}
subtype='email'
/>,
);
expect(wrapper.find(TextSetting).props().type).toEqual('email');
expect(screen.getByTestId('testingemail')).toHaveAttribute('type', 'email');
});
it('subtype password', () => {
const wrapper = shallow(
render(
<DialogElement
{...baseDialogProps}
subtype='password'
/>,
);
expect(wrapper.find(TextSetting).props().type).toEqual('password');
expect(screen.getByTestId('testingpassword')).toHaveAttribute('type', 'password');
});
describe('radioSetting', () => {
@ -96,7 +94,7 @@ describe('components/interactive_dialog/DialogElement', () => {
];
test('RadioSetting is rendered when type is radio', () => {
const wrapper = shallow(
render(
<DialogElement
{...baseDialogProps}
type='radio'
@ -104,11 +102,11 @@ describe('components/interactive_dialog/DialogElement', () => {
/>,
);
expect(wrapper.find(RadioSetting).exists()).toBe(true);
expect(screen.getAllByRole('radio')).toHaveLength(2);
});
test('RadioSetting is rendered when options are null', () => {
const wrapper = shallow(
render(
<DialogElement
{...baseDialogProps}
type='radio'
@ -116,11 +114,11 @@ describe('components/interactive_dialog/DialogElement', () => {
/>,
);
expect(wrapper.find(RadioSetting).exists()).toBe(true);
expect(screen.getByTestId('testing')).toBeInTheDocument();
});
test('RadioSetting is rendered when options are null and value is null', () => {
const wrapper = shallow(
render(
<DialogElement
{...baseDialogProps}
type='radio'
@ -129,11 +127,11 @@ describe('components/interactive_dialog/DialogElement', () => {
/>,
);
expect(wrapper.find(RadioSetting).exists()).toBe(true);
expect(screen.getByTestId('testing')).toBeInTheDocument();
});
test('RadioSetting is rendered when options are null and value is not null', () => {
const wrapper = shallow(
render(
<DialogElement
{...baseDialogProps}
type='radio'
@ -142,11 +140,11 @@ describe('components/interactive_dialog/DialogElement', () => {
/>,
);
expect(wrapper.find(RadioSetting).exists()).toBe(true);
expect(screen.getByTestId('testing')).toBeInTheDocument();
});
test('RadioSetting is rendered when value is not one of the options', () => {
const wrapper = shallow(
render(
<DialogElement
{...baseDialogProps}
type='radio'
@ -155,23 +153,25 @@ describe('components/interactive_dialog/DialogElement', () => {
/>,
);
expect(wrapper.find(RadioSetting).exists()).toBe(true);
expect(screen.getByTestId('testing')).toBeInTheDocument();
});
test('No default value is selected from the radio button list', () => {
const wrapper = shallow(
render(
<DialogElement
{...baseDialogProps}
type='radio'
options={radioOptions}
/>,
);
const instance = wrapper.instance() as DialogElement;
expect(instance.props.value).toBeUndefined();
const radios = screen.getAllByRole('radio');
radios.forEach((radio) => {
expect(radio).not.toBeChecked();
});
});
test('The default value can be specified from the list', () => {
const wrapper = shallow(
render(
<DialogElement
{...baseDialogProps}
type='radio'
@ -179,7 +179,7 @@ describe('components/interactive_dialog/DialogElement', () => {
value={radioOptions[1].value}
/>,
);
expect(wrapper.find({options: radioOptions, value: radioOptions[1].value}).exists()).toBe(true);
expect(screen.getByRole('radio', {name: radioOptions[1].text})).toBeChecked();
});
});
});

View file

@ -1,9 +1,9 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {mount} from 'enzyme';
import React from 'react';
import {render} from 'tests/react_testing_utils';
import EmojiMap from 'utils/emoji_map';
import DialogIntroductionText from './dialog_introduction_text';
@ -17,8 +17,8 @@ describe('components/DialogIntroductionText', () => {
value: '**bold** *italic* [link](https://mattermost.com/) <br/> [link target blank](!https://mattermost.com/)',
emojiMap,
};
const wrapper = mount(<DialogIntroductionText {...descriptor}/>);
expect(wrapper).toMatchSnapshot();
const {container} = render(<DialogIntroductionText {...descriptor}/>);
expect(container).toMatchSnapshot();
});
test('should not fail on empty value', () => {
@ -27,7 +27,7 @@ describe('components/DialogIntroductionText', () => {
value: '',
emojiMap,
};
const wrapper = mount(<DialogIntroductionText {...descriptor}/>);
expect(wrapper).toMatchSnapshot();
const {container} = render(<DialogIntroductionText {...descriptor}/>);
expect(container).toMatchSnapshot();
});
});

View file

@ -1,118 +1,86 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`components/learn_more_trial_modal/learn_more_trial_modal_step should match snapshot 1`] = `
<ContextProvider
value={
Object {
"getServerState": undefined,
"identityFunctionCheck": "once",
"stabilityCheck": "once",
"store": Object {
"clearActions": [Function],
"dispatch": [Function],
"getActions": [Function],
"getState": [Function],
"replaceReducer": [Function],
"subscribe": [Function],
},
"subscription": Object {
"addNestedSub": [Function],
"getListeners": [Function],
"handleChangeWrapper": [Function],
"isSubscribed": [Function],
"notifyNestedSubs": [Function],
"trySubscribe": [Function],
"tryUnsubscribe": [Function],
},
}
}
>
<LearnMoreTrialModalStep
buttonLabel="button"
description="Step description"
id="stepId"
svgElement={<svg />}
svgWrapperClassName="stepClassname"
title="Step title"
/>
</ContextProvider>
<div>
<div
class="LearnMoreTrialModalStep slide-container"
id="learnMoreTrialModalStep-stepId"
>
<div
class="stepClassname svg-wrapper"
>
<svg />
</div>
<div
class="title"
>
Step title
</div>
<div
class="description"
>
Step description
</div>
</div>
</div>
`;
exports[`components/learn_more_trial_modal/learn_more_trial_modal_step should match snapshot when loaded in cloud workspace 1`] = `
<ContextProvider
value={
Object {
"getServerState": undefined,
"identityFunctionCheck": "once",
"stabilityCheck": "once",
"store": Object {
"clearActions": [Function],
"dispatch": [Function],
"getActions": [Function],
"getState": [Function],
"replaceReducer": [Function],
"subscribe": [Function],
},
"subscription": Object {
"addNestedSub": [Function],
"getListeners": [Function],
"handleChangeWrapper": [Function],
"isSubscribed": [Function],
"notifyNestedSubs": [Function],
"trySubscribe": [Function],
"tryUnsubscribe": [Function],
},
}
}
>
<LearnMoreTrialModalStep
bottomLeftMessage="Step bottom message"
buttonLabel="button"
description="Step description"
id="stepId"
isCloud={true}
svgElement={<svg />}
svgWrapperClassName="stepClassname"
title="Step title"
/>
</ContextProvider>
<div>
<div
class="LearnMoreTrialModalStep slide-container"
id="learnMoreTrialModalStep-stepId"
>
<div
class="stepClassname svg-wrapper"
>
<svg />
</div>
<div
class="title"
>
Step title
</div>
<div
class="description"
>
Step description
</div>
<div
class="bottom-text-left-message"
>
Step bottom message
</div>
</div>
</div>
`;
exports[`components/learn_more_trial_modal/learn_more_trial_modal_step should match snapshot with optional params 1`] = `
<ContextProvider
value={
Object {
"getServerState": undefined,
"identityFunctionCheck": "once",
"stabilityCheck": "once",
"store": Object {
"clearActions": [Function],
"dispatch": [Function],
"getActions": [Function],
"getState": [Function],
"replaceReducer": [Function],
"subscribe": [Function],
},
"subscription": Object {
"addNestedSub": [Function],
"getListeners": [Function],
"handleChangeWrapper": [Function],
"isSubscribed": [Function],
"notifyNestedSubs": [Function],
"trySubscribe": [Function],
"tryUnsubscribe": [Function],
},
}
}
>
<LearnMoreTrialModalStep
bottomLeftMessage="Step bottom message"
buttonLabel="button"
description="Step description"
id="stepId"
svgElement={<svg />}
svgWrapperClassName="stepClassname"
title="Step title"
/>
</ContextProvider>
<div>
<div
class="LearnMoreTrialModalStep slide-container"
id="learnMoreTrialModalStep-stepId"
>
<div
class="stepClassname svg-wrapper"
>
<svg />
</div>
<div
class="title"
>
Step title
</div>
<div
class="description"
>
Step description
</div>
<div
class="bottom-text-left-message"
>
Step bottom message
</div>
</div>
</div>
`;

View file

@ -1,35 +1,12 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`components/learn_more_trial_modal/start_trial_btn should match snapshot 1`] = `
<ContextProvider
value={
Object {
"getServerState": undefined,
"identityFunctionCheck": "once",
"stabilityCheck": "once",
"store": Object {
"clearActions": [Function],
"dispatch": [Function],
"getActions": [Function],
"getState": [Function],
"replaceReducer": [Function],
"subscribe": [Function],
},
"subscription": Object {
"addNestedSub": [Function],
"getListeners": [Function],
"handleChangeWrapper": [Function],
"isSubscribed": [Function],
"notifyNestedSubs": [Function],
"trySubscribe": [Function],
"tryUnsubscribe": [Function],
},
}
}
>
<StartTrialBtn
message="Start trial"
onClick={[MockFunction]}
/>
</ContextProvider>
<div>
<a
class="btn btn-secondary"
id="start_trial_btn"
>
Start trial
</a>
</div>
`;

View file

@ -1,17 +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 {Provider} from 'react-redux';
import {GenericModal} from '@mattermost/components';
import Carousel from 'components/common/carousel/carousel';
import LearnMoreTrialModal from 'components/learn_more_trial_modal/learn_more_trial_modal';
import {mountWithIntl} from 'tests/helpers/intl-test-helper';
import mockStore from 'tests/test_store';
import {renderWithContext, screen, userEvent, waitFor} from 'tests/react_testing_utils';
jest.mock('components/common/hooks/useOpenStartTrialFormModal', () => ({
__esModule: true,
default: () => jest.fn(),
}));
describe('components/learn_more_trial_modal/learn_more_trial_modal', () => {
// required state to mount using the provider
@ -59,101 +58,86 @@ describe('components/learn_more_trial_modal/learn_more_trial_modal', () => {
onExited: jest.fn(),
};
const store = mockStore(state);
test('should match snapshot', () => {
const wrapper = shallow(
<Provider store={store}>
<LearnMoreTrialModal {...props}/>
</Provider>,
const {baseElement} = renderWithContext(
<LearnMoreTrialModal {...props}/>,
state,
);
expect(wrapper).toMatchSnapshot();
expect(baseElement).toMatchSnapshot();
});
test('should show the learn more about trial modal carousel slides', () => {
const wrapper = mountWithIntl(
<Provider store={store}>
<LearnMoreTrialModal {...props}/>
</Provider>,
renderWithContext(
<LearnMoreTrialModal {...props}/>,
state,
);
expect(wrapper.find('LearnMoreTrialModal').find('Carousel')).toHaveLength(1);
expect(document.querySelector('#learnMoreTrialModalCarousel')).not.toBeNull();
});
test('should call on close', () => {
test('should call on close', async () => {
const mockOnClose = jest.fn();
const wrapper = mountWithIntl(
<Provider store={store}>
<LearnMoreTrialModal
{...props}
onClose={mockOnClose}
/>
</Provider>,
renderWithContext(
<LearnMoreTrialModal
{...props}
onClose={mockOnClose}
/>,
state,
);
wrapper.find(GenericModal).props().onExited?.();
await userEvent.click(screen.getByLabelText('Close'));
expect(mockOnClose).toHaveBeenCalled();
await waitFor(() => {
expect(mockOnClose).toHaveBeenCalled();
});
});
test('should call on exited', () => {
test('should call on exited', async () => {
const mockOnExited = jest.fn();
const wrapper = mountWithIntl(
<Provider store={store}>
<LearnMoreTrialModal
{...props}
onExited={mockOnExited}
/>
</Provider>,
renderWithContext(
<LearnMoreTrialModal
{...props}
onExited={mockOnExited}
/>,
state,
);
wrapper.find(GenericModal).props().onExited?.();
await userEvent.click(screen.getByLabelText('Close'));
expect(mockOnExited).toHaveBeenCalled();
await waitFor(() => {
expect(mockOnExited).toHaveBeenCalled();
});
});
test('should move the slides when clicking carousel next and prev buttons', () => {
const wrapper = mountWithIntl(
<Provider store={store}>
<LearnMoreTrialModal
{...props}
/>
</Provider>,
test('should move the slides when clicking carousel next and prev buttons', async () => {
renderWithContext(
<LearnMoreTrialModal
{...props}
/>,
state,
);
// validate the value of the first slide
let activeSlide = wrapper.find(Carousel).find('.slide.active-anim');
let activeSlideId = activeSlide.find('LearnMoreTrialModalStep').props().id;
expect(document.querySelector('.slide.active-anim #learnMoreTrialModalStep-useSso')).not.toBeNull();
expect(activeSlideId).toBe('useSso');
const nextButton = wrapper.find(Carousel).find('CarouselButton div.chevron-right');
const prevButton = wrapper.find(Carousel).find('CarouselButton div.chevron-left');
const nextButton = document.querySelector('.chevron-right')!;
const prevButton = document.querySelector('.chevron-left')!;
// move to the second slide
nextButton.simulate('click');
await userEvent.click(nextButton);
activeSlide = wrapper.find(Carousel).find('.slide.active-anim');
activeSlideId = activeSlide.find('LearnMoreTrialModalStep').props().id;
expect(activeSlideId).toBe('ldap');
expect(document.querySelector('.slide.active-anim #learnMoreTrialModalStep-ldap')).not.toBeNull();
// move to the third slide
nextButton.simulate('click');
await userEvent.click(nextButton);
activeSlide = wrapper.find(Carousel).find('.slide.active-anim');
activeSlideId = activeSlide.find('LearnMoreTrialModalStep').props().id;
expect(activeSlideId).toBe('systemConsole');
expect(document.querySelector('.slide.active-anim #learnMoreTrialModalStep-systemConsole')).not.toBeNull();
// move back to the second slide
prevButton.simulate('click');
await userEvent.click(prevButton);
activeSlide = wrapper.find(Carousel).find('.slide.active-anim');
activeSlideId = activeSlide.find('LearnMoreTrialModalStep').props().id;
expect(activeSlideId).toBe('ldap');
expect(document.querySelector('.slide.active-anim #learnMoreTrialModalStep-ldap')).not.toBeNull();
});
test('should have the self hosted request trial button cloud free is disabled', () => {
@ -170,18 +154,15 @@ describe('components/learn_more_trial_modal/learn_more_trial_modal', () => {
},
},
};
const nonCloudStore = mockStore(nonCloudState);
const wrapper = mountWithIntl(
<Provider store={nonCloudStore}>
<LearnMoreTrialModal
{...props}
/>
</Provider>,
renderWithContext(
<LearnMoreTrialModal
{...props}
/>,
nonCloudState,
);
// validate the cloud start trial button is not present
const selfHostedRequestTrialButton = wrapper.find('StartTrialBtn');
expect(selfHostedRequestTrialButton).toHaveLength(1);
expect(document.querySelector('#start_trial_btn')).not.toBeNull();
});
});

View file

@ -1,13 +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 {Provider} from 'react-redux';
import LearnMoreTrialModalStep from 'components/learn_more_trial_modal/learn_more_trial_modal_step';
import mockStore from 'tests/test_store';
import {renderWithContext} from 'tests/react_testing_utils';
describe('components/learn_more_trial_modal/learn_more_trial_modal_step', () => {
const props = {
@ -43,42 +41,37 @@ describe('components/learn_more_trial_modal/learn_more_trial_modal_step', () =>
},
};
const store = mockStore(state);
test('should match snapshot', () => {
const wrapper = shallow(
<Provider store={store}>
<LearnMoreTrialModalStep {...props}/>
</Provider>,
const {container} = renderWithContext(
<LearnMoreTrialModalStep {...props}/>,
state,
);
expect(wrapper).toMatchSnapshot();
expect(container).toMatchSnapshot();
});
test('should match snapshot with optional params', () => {
const wrapper = shallow(
<Provider store={store}>
<LearnMoreTrialModalStep
{...props}
bottomLeftMessage='Step bottom message'
/>
</Provider>,
const {container} = renderWithContext(
<LearnMoreTrialModalStep
{...props}
bottomLeftMessage='Step bottom message'
/>,
state,
);
expect(wrapper).toMatchSnapshot();
expect(container).toMatchSnapshot();
});
test('should match snapshot when loaded in cloud workspace', () => {
const cloudProps = {...props, isCloud: true};
const wrapper = shallow(
<Provider store={store}>
<LearnMoreTrialModalStep
{...cloudProps}
bottomLeftMessage='Step bottom message'
/>
</Provider>,
const {container} = renderWithContext(
<LearnMoreTrialModalStep
{...cloudProps}
bottomLeftMessage='Step bottom message'
/>,
state,
);
expect(wrapper).toMatchSnapshot();
expect(container).toMatchSnapshot();
});
});

View file

@ -1,15 +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 {Provider} from 'react-redux';
import StartTrialBtn from 'components/learn_more_trial_modal/start_trial_btn';
import {mountWithIntl} from 'tests/helpers/intl-test-helper';
import {act} from 'tests/react_testing_utils';
import mockStore from 'tests/test_store';
import {renderWithContext, screen, userEvent} from 'tests/react_testing_utils';
jest.mock('mattermost-redux/actions/general', () => ({
...jest.requireActual('mattermost-redux/actions/general'),
@ -28,6 +24,11 @@ jest.mock('actions/admin_actions', () => ({
},
}));
jest.mock('components/common/hooks/useOpenStartTrialFormModal', () => ({
__esModule: true,
default: () => jest.fn(),
}));
describe('components/learn_more_trial_modal/start_trial_btn', () => {
const state = {
entities: {
@ -62,38 +63,31 @@ describe('components/learn_more_trial_modal/start_trial_btn', () => {
},
};
const store = mockStore(state);
const props = {
onClick: jest.fn(),
message: 'Start trial',
};
test('should match snapshot', () => {
const wrapper = shallow(
<Provider store={store}>
<StartTrialBtn {...props}/>
</Provider>,
const {container} = renderWithContext(
<StartTrialBtn {...props}/>,
state,
);
expect(wrapper).toMatchSnapshot();
expect(container).toMatchSnapshot();
});
test('should handle on click', async () => {
const mockOnClick = jest.fn();
// Mount the component
const wrapper = mountWithIntl(
<Provider store={store}>
<StartTrialBtn
{...props}
onClick={mockOnClick}
/>
</Provider>,
const {container} = renderWithContext(
<StartTrialBtn
{...props}
onClick={mockOnClick}
/>,
state,
);
act(() => {
wrapper.find('.btn-secondary').simulate('click');
});
await userEvent.click(container.querySelector('.btn-secondary')!);
expect(mockOnClick).toHaveBeenCalled();
});
@ -101,44 +95,17 @@ describe('components/learn_more_trial_modal/start_trial_btn', () => {
test('should handle on click when rendered as button', async () => {
const mockOnClick = jest.fn();
// Mount the component
const wrapper = mountWithIntl(
<Provider store={store}>
<StartTrialBtn
{...props}
renderAsButton={true}
onClick={mockOnClick}
/>
</Provider>,
renderWithContext(
<StartTrialBtn
{...props}
renderAsButton={true}
onClick={mockOnClick}
/>,
state,
);
act(() => {
wrapper.find('button').simulate('click');
});
await userEvent.click(screen.getByRole('button'));
expect(mockOnClick).toHaveBeenCalled();
});
// test('does not show success for embargoed countries', async () => {
// const mockOnClick = jest.fn();
// const clonedState = JSON.parse(JSON.stringify(state));
// clonedState.entities.admin.analytics.TOTAL_USERS = 451;
// // Mount the component
// const wrapper = mountWithIntl(
// <Provider store={mockStore(clonedState)}>
// <StartTrialBtn
// {...props}
// onClick={mockOnClick}
// />
// </Provider>,
// );
// act(() => {
// wrapper.find('.start-trial-btn').simulate('click');
// });
// expect(mockOnClick).not.toHaveBeenCalled();
// });
});

View file

@ -2,32 +2,23 @@
exports[`components/mfa/components/Confirm should match snapshot 1`] = `
<div>
<form
className="form-group"
onKeyPress={[Function]}
onSubmit={[Function]}
>
<strong>
<MemoizedFormattedMessage
defaultMessage="Set up complete!"
id="mfa.confirm.complete"
/>
</strong>
<p>
<MemoizedFormattedMessage
defaultMessage="Your account is now secure. Next time you sign in, you will be asked to enter a code from your authenticator app on your phone."
id="mfa.confirm.secure"
/>
</p>
<button
className="btn btn-primary"
type="submit"
<div>
<form
class="form-group"
>
<MemoizedFormattedMessage
defaultMessage="Okay"
id="mfa.confirm.okay"
/>
</button>
</form>
<strong>
Set up complete!
</strong>
<p>
Your account is now secure. Next time you sign in, you will be asked to enter a code from your authenticator app on your phone.
</p>
<button
class="btn btn-primary"
type="submit"
>
Okay
</button>
</form>
</div>
</div>
`;

View file

@ -1,14 +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 {redirectUserToDefaultTeam} from 'actions/global_actions';
import Confirm from 'components/mfa/confirm';
import {mountWithIntl} from 'tests/helpers/intl-test-helper';
import {renderWithContext, screen, userEvent} from 'tests/react_testing_utils';
import Constants from 'utils/constants';
jest.mock('actions/global_actions', () => ({
@ -23,13 +22,14 @@ describe('components/mfa/components/Confirm', () => {
});
test('should match snapshot', () => {
const wrapper = shallow(<Confirm/>);
expect(wrapper).toMatchSnapshot();
const {container} = renderWithContext(<Confirm/>);
expect(container).toMatchSnapshot();
});
test('should submit on form submit', () => {
const wrapper = mountWithIntl(<Confirm/>);
wrapper.find('form').simulate('submit');
test('should submit on form submit', async () => {
renderWithContext(<Confirm/>);
await userEvent.click(screen.getByRole('button', {name: 'Okay'}));
expect(redirectUserToDefaultTeam).toHaveBeenCalled();
});
@ -42,7 +42,7 @@ describe('components/mfa/components/Confirm', () => {
map[event] = callback;
});
mountWithIntl(<Confirm/>);
renderWithContext(<Confirm/>);
const event = {
preventDefault: jest.fn(),

View file

@ -2,84 +2,55 @@
exports[`components/mfa/setup should match snapshot without required text 1`] = `
<div>
<form
className="form-group"
onSubmit={[Function]}
>
<p>
<MemoizedFormattedMessage
defaultMessage="1. Scan the QR code below using an authenticator app of your choice, such as Google Authenticator, Microsoft Authenticator app, or 1Password."
id="mfa.setup.step1"
/>
</p>
<p>
<MemoizedFormattedMessage
defaultMessage="Alternatively, enter the secret key displayed below into the authenticator app manually."
id="mfa.setup.step2_secret"
/>
</p>
<div
className="form-group"
<div>
<form
class="form-group"
>
<p>
1. Scan the QR code below using an authenticator app of your choice, such as Google Authenticator, Microsoft Authenticator app, or 1Password.
</p>
<p>
Alternatively, enter the secret key displayed below into the authenticator app manually.
</p>
<div
className="col-sm-12"
class="form-group"
>
<img
alt="qr code image"
src="data:image/png;base64,"
style={
Object {
"maxHeight": 170,
}
}
/>
<div
class="col-sm-12"
>
<img
alt="qr code image"
src="data:image/png;base64,"
style="max-height: 170px;"
/>
</div>
</div>
</div>
<br />
<div
className="form-group"
>
<p
className="col-sm-12"
<br />
<div
class="form-group"
>
<MemoizedFormattedMessage
defaultMessage="Secret: {secret}"
id="mfa.setup.secret"
values={
Object {
"secret": "",
}
}
<p
class="col-sm-12"
>
Secret:
</p>
</div>
<p>
2. Enter the code generated by the authenticator app in the field below.
</p>
<p>
<input
class="form-control"
placeholder="MFA Code"
/>
</p>
</div>
<p>
<MemoizedFormattedMessage
defaultMessage="2. Enter the code generated by the authenticator app in the field below."
id="mfa.setup.step3_code"
/>
</p>
<p>
<LocalizedPlaceholderInput
autoFocus={true}
className="form-control"
placeholder={
Object {
"defaultMessage": "MFA Code",
"id": "mfa.setup.code",
}
}
/>
</p>
<button
className="btn btn-primary"
type="submit"
>
<MemoizedFormattedMessage
defaultMessage="Save"
id="mfa.setup.save"
/>
</button>
</form>
<button
class="btn btn-primary"
type="submit"
>
Save
</button>
</form>
</div>
</div>
`;

View file

@ -1,13 +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 Setup from 'components/mfa/setup/setup';
import {mountWithIntl} from 'tests/helpers/intl-test-helper';
import {act, waitFor} from 'tests/react_testing_utils';
import {renderWithContext, screen, userEvent, waitFor} from 'tests/react_testing_utils';
import {TestHelper} from 'utils/test_helper';
jest.mock('actions/global_actions', () => ({
@ -32,12 +30,12 @@ describe('components/mfa/setup', () => {
};
test('should match snapshot without required text', async () => {
const wrapper = shallow<Setup>(
const {container} = renderWithContext(
<Setup {...baseProps}/>,
);
expect(wrapper).toMatchSnapshot();
const requiredText = wrapper.find('#mfa.setup.required_mfa');
expect(requiredText).not.toBeFalsy();
expect(container).toMatchSnapshot();
const requiredText = screen.queryByText(/Multi-factor authentication is required/);
expect(requiredText).not.toBeInTheDocument();
});
test('should match snapshot with required text', async () => {
@ -46,30 +44,32 @@ describe('components/mfa/setup', () => {
enforceMultifactorAuthentication: true,
};
const wrapper = shallow<Setup>(
renderWithContext(
<Setup {...props}/>,
);
const requiredText = wrapper.find('#mfa.setup.required_mfa');
const requiredText = screen.queryByText(/Multi-factor authentication is required/);
expect(requiredText).toBeDefined();
});
test('should set state after calling component did mount', async () => {
const wrapper = shallow<Setup>(
renderWithContext(
<Setup {...baseProps}/>,
);
expect(generateMfaSecret).toHaveBeenCalled();
await wrapper.instance().componentDidMount();
expect(wrapper.state('secret')).toEqual('generated secret');
expect(wrapper.state('qrCode')).toEqual('qrcode');
await waitFor(() => {
expect(screen.getByText(/generated secret/)).toBeInTheDocument();
});
});
test('should call activateMfa on submission', async () => {
const wrapper = mountWithIntl(
renderWithContext(
<Setup {...baseProps}/>,
);
(wrapper.instance() as Setup).input.current!.value = 'testcodeinput';
wrapper.find('form').simulate('submit', {preventDefault: () => {}});
const input = screen.getByPlaceholderText('MFA Code');
await userEvent.type(input, 'testcodeinput');
await userEvent.click(screen.getByRole('button', {name: 'Save'}));
await waitFor(() => {
expect(baseProps.actions.activateMfa).toHaveBeenCalledWith('testcodeinput');
@ -77,16 +77,15 @@ describe('components/mfa/setup', () => {
});
test('should focus input when code is empty', async () => {
const wrapper = mountWithIntl(
renderWithContext(
<Setup {...baseProps}/>,
);
const input = wrapper.find('input').getDOMNode() as HTMLInputElement;
const focusSpy = jest.spyOn(input, 'focus');
const input = screen.getByPlaceholderText('MFA Code');
wrapper.find('form').simulate('submit', {preventDefault: () => {}});
await userEvent.click(screen.getByRole('button', {name: 'Save'}));
await waitFor(() => {
expect(focusSpy).toHaveBeenCalled();
expect(input).toHaveFocus();
});
});
@ -104,19 +103,16 @@ describe('components/mfa/setup', () => {
},
};
const wrapper = mountWithIntl(
renderWithContext(
<Setup {...props}/>,
);
const input = wrapper.find('input').getDOMNode() as HTMLInputElement;
const focusSpy = jest.spyOn(input, 'focus');
const input = screen.getByPlaceholderText('MFA Code');
act(() => {
(wrapper.instance() as Setup).input.current!.value = 'invalidcode';
wrapper.find('form').simulate('submit', {preventDefault: () => {}});
});
await userEvent.type(input, 'invalidcode');
await userEvent.click(screen.getByRole('button', {name: 'Save'}));
await waitFor(() => {
expect(focusSpy).toHaveBeenCalled();
expect(input).toHaveFocus();
});
});
});

View file

@ -1,528 +1,489 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`components/multiselect/multiselect MultiSelectList should match snapshot when custom no option message is defined 1`] = `
<Fragment>
<div>
<div
className="filtered-user-list"
class="filtered-user-list"
>
<div
className="filter-row"
class="filter-row"
>
<div
className="multi-select__container react-select"
class="multi-select__container react-select"
>
<ForwardRef
aria-invalid={false}
className=""
classNamePrefix="react-select-auto react-select"
components={
Object {
"IndicatorsContainer": [Function],
"Menu": [Function],
"MultiValueLabel": [Function],
"MultiValueRemove": [Function],
}
}
getOptionLabel={[Function]}
getOptionValue={[Function]}
id="selectItems"
inputValue=""
isClearable={false}
isMulti={true}
menuIsOpen={false}
onBlur={[Function]}
onChange={[Function]}
onInputChange={[Function]}
onKeyDown={[Function]}
openMenuOnFocus={false}
options={
Array [
Object {
"id": "0",
"label": "0",
"value": "0",
},
Object {
"id": "1",
"label": "1",
"value": "1",
},
Object {
"id": "2",
"label": "2",
"value": "2",
},
Object {
"id": "3",
"label": "3",
"value": "3",
},
Object {
"id": "4",
"label": "4",
"value": "4",
},
Object {
"id": "5",
"label": "5",
"value": "5",
},
Object {
"id": "6",
"label": "6",
"value": "6",
},
Object {
"id": "7",
"label": "7",
"value": "7",
},
]
}
styles={
Object {
"container": [Function],
}
}
value={
Array [
Object {
"id": "id",
"label": "label",
"value": "value",
},
]
}
/>
<SaveButton
disabled={false}
id="saveItems"
onClick={[Function]}
saving={false}
/>
</div>
<div
className="multi-select__help"
id="multiSelectHelpMemberInfo"
>
<MemoizedFormattedMessage
defaultMessage="{memberOptions, number} of {totalCount, number} members"
id="multiselect.numMembers"
values={
Object {
"memberOptions": 5,
"totalCount": 8,
}
}
/>
</div>
</div>
<MultiSelectList
ariaLabelRenderer={[Function]}
customNoOptionsMessage={
<div
className="custom-no-options-message"
class="css-1dedlln"
id="selectItems"
>
<span
class="css-1f43avz-a11yText-A11yText"
id="react-select-5-live-region"
/>
<span
aria-atomic="false"
aria-live="polite"
aria-relevant="additions text"
class="css-1f43avz-a11yText-A11yText"
role="log"
/>
<div
class="react-select-auto react-select__control css-13cymwt-control"
>
<div
class="react-select-auto react-select__value-container react-select-auto react-select__value-container--is-multi react-select-auto react-select__value-container--has-value css-1dyz3mf"
>
<div
class="react-select-auto react-select__multi-value css-1p3m7a8-multiValue"
>
<div
class="react-select__padded-component"
>
<span>
label
</span>
</div>
<div
aria-label="Remove label"
class="react-select-auto react-select__multi-value__remove css-v7duua"
role="button"
>
<svg
aria-hidden="true"
class="css-tj5bde-Svg"
focusable="false"
height="14"
viewBox="0 0 20 20"
width="14"
>
<path
d="M14.348 14.849c-0.469 0.469-1.229 0.469-1.697 0l-2.651-3.030-2.651 3.029c-0.469 0.469-1.229 0.469-1.697 0-0.469-0.469-0.469-1.229 0-1.697l2.758-3.15-2.759-3.152c-0.469-0.469-0.469-1.228 0-1.697s1.228-0.469 1.697 0l2.652 3.031 2.651-3.031c0.469-0.469 1.228-0.469 1.697 0s0.469 1.229 0 1.697l-2.758 3.152 2.758 3.15c0.469 0.469 0.469 1.229 0 1.698z"
/>
</svg>
</div>
</div>
<div
class="react-select-auto react-select__input-container css-19bb58m"
data-value=""
>
<input
aria-activedescendant=""
aria-autocomplete="list"
aria-expanded="false"
aria-haspopup="true"
aria-invalid="false"
autocapitalize="none"
autocomplete="off"
autocorrect="off"
class="react-select-auto react-select__input"
id="react-select-5-input"
role="combobox"
spellcheck="false"
style="color: inherit; background: 0px; opacity: 1; width: 100%; grid-area: 1 / 2; min-width: 2px; border: 0px; margin: 0px; outline: 0; padding: 0px;"
tabindex="0"
type="text"
value=""
/>
</div>
</div>
</div>
</div>
<button
class="btn btn-primary "
data-testid="saveSetting"
id="saveItems"
type="submit"
>
<span>
No matches found
Save
</span>
</div>
}
onAction={[Function]}
onAdd={[Function]}
onSelect={[Function]}
optionRenderer={[Function]}
options={
Array [
Object {
"id": "0",
"label": "0",
"value": "0",
},
Object {
"id": "1",
"label": "1",
"value": "1",
},
Object {
"id": "2",
"label": "2",
"value": "2",
},
Object {
"id": "3",
"label": "3",
"value": "3",
},
Object {
"id": "4",
"label": "4",
"value": "4",
},
]
}
perPage={50}
query=""
/>
</button>
</div>
<div
class="multi-select__help"
id="multiSelectHelpMemberInfo"
>
5 of 8 members
</div>
</div>
<div
className="multi-select__help"
aria-live="polite"
class="multi-select__wrapper"
>
<div
class="more-modal__list"
>
<div
aria-live="polite"
class="sr-only"
role="status"
>
5 results found for your search.
</div>
<div
aria-atomic="true"
aria-live="polite"
class="sr-only"
/>
<div
class="more-modal__options"
id="multiSelectList"
role="presentation"
>
<div>
0
</div>
<div>
1
</div>
<div>
2
</div>
<div>
3
</div>
<div>
4
</div>
</div>
</div>
</div>
<div
class="multi-select__help"
id="multiSelectMessageNote"
/>
<div
className="filter-controls"
class="filter-controls"
>
<button
className="btn btn-sm btn-tertiary filter-control filter-control__next"
onClick={[Function]}
class="btn btn-sm btn-tertiary filter-control filter-control__next"
>
<MemoizedFormattedMessage
defaultMessage="Next"
id="filtered_user_list.next"
/>
Next
</button>
</div>
</div>
</Fragment>
</div>
`;
exports[`components/multiselect/multiselect should match snapshot 1`] = `
<Fragment>
<div>
<div
className="filtered-user-list"
class="filtered-user-list"
>
<div
className="filter-row"
class="filter-row"
>
<div
className="multi-select__container react-select"
class="multi-select__container react-select"
>
<ForwardRef
aria-invalid={false}
className=""
classNamePrefix="react-select-auto react-select"
components={
Object {
"IndicatorsContainer": [Function],
"Menu": [Function],
"MultiValueLabel": [Function],
"MultiValueRemove": [Function],
}
}
getOptionLabel={[Function]}
getOptionValue={[Function]}
<div
class="css-1dedlln"
id="selectItems"
inputValue=""
isClearable={false}
isMulti={true}
menuIsOpen={false}
onBlur={[Function]}
onChange={[Function]}
onInputChange={[Function]}
onKeyDown={[Function]}
openMenuOnFocus={false}
options={
Array [
Object {
"id": "0",
"label": "0",
"value": "0",
},
Object {
"id": "1",
"label": "1",
"value": "1",
},
Object {
"id": "2",
"label": "2",
"value": "2",
},
Object {
"id": "3",
"label": "3",
"value": "3",
},
Object {
"id": "4",
"label": "4",
"value": "4",
},
Object {
"id": "5",
"label": "5",
"value": "5",
},
Object {
"id": "6",
"label": "6",
"value": "6",
},
Object {
"id": "7",
"label": "7",
"value": "7",
},
]
}
styles={
Object {
"container": [Function],
}
}
value={
Array [
Object {
"id": "id",
"label": "label",
"value": "value",
},
]
}
/>
<SaveButton
disabled={false}
>
<span
class="css-1f43avz-a11yText-A11yText"
id="react-select-2-live-region"
/>
<span
aria-atomic="false"
aria-live="polite"
aria-relevant="additions text"
class="css-1f43avz-a11yText-A11yText"
role="log"
/>
<div
class="react-select-auto react-select__control css-13cymwt-control"
>
<div
class="react-select-auto react-select__value-container react-select-auto react-select__value-container--is-multi react-select-auto react-select__value-container--has-value css-1dyz3mf"
>
<div
class="react-select-auto react-select__multi-value css-1p3m7a8-multiValue"
>
<div
class="react-select__padded-component"
>
<span>
label
</span>
</div>
<div
aria-label="Remove label"
class="react-select-auto react-select__multi-value__remove css-v7duua"
role="button"
>
<svg
aria-hidden="true"
class="css-tj5bde-Svg"
focusable="false"
height="14"
viewBox="0 0 20 20"
width="14"
>
<path
d="M14.348 14.849c-0.469 0.469-1.229 0.469-1.697 0l-2.651-3.030-2.651 3.029c-0.469 0.469-1.229 0.469-1.697 0-0.469-0.469-0.469-1.229 0-1.697l2.758-3.15-2.759-3.152c-0.469-0.469-0.469-1.228 0-1.697s1.228-0.469 1.697 0l2.652 3.031 2.651-3.031c0.469-0.469 1.228-0.469 1.697 0s0.469 1.229 0 1.697l-2.758 3.152 2.758 3.15c0.469 0.469 0.469 1.229 0 1.698z"
/>
</svg>
</div>
</div>
<div
class="react-select-auto react-select__input-container css-19bb58m"
data-value=""
>
<input
aria-activedescendant=""
aria-autocomplete="list"
aria-expanded="false"
aria-haspopup="true"
aria-invalid="false"
autocapitalize="none"
autocomplete="off"
autocorrect="off"
class="react-select-auto react-select__input"
id="react-select-2-input"
role="combobox"
spellcheck="false"
style="color: inherit; background: 0px; opacity: 1; width: 100%; grid-area: 1 / 2; min-width: 2px; border: 0px; margin: 0px; outline: 0; padding: 0px;"
tabindex="0"
type="text"
value=""
/>
</div>
</div>
</div>
</div>
<button
class="btn btn-primary "
data-testid="saveSetting"
id="saveItems"
onClick={[Function]}
saving={false}
/>
type="submit"
>
<span>
Save
</span>
</button>
</div>
<div
className="multi-select__help"
class="multi-select__help"
id="multiSelectHelpMemberInfo"
>
<MemoizedFormattedMessage
defaultMessage="{memberOptions, number} of {totalCount, number} members"
id="multiselect.numMembers"
values={
Object {
"memberOptions": 5,
"totalCount": 8,
}
}
/>
5 of 8 members
</div>
</div>
<MultiSelectList
ariaLabelRenderer={[Function]}
onAction={[Function]}
onAdd={[Function]}
onSelect={[Function]}
optionRenderer={[Function]}
options={
Array [
Object {
"id": "0",
"label": "0",
"value": "0",
},
Object {
"id": "1",
"label": "1",
"value": "1",
},
Object {
"id": "2",
"label": "2",
"value": "2",
},
Object {
"id": "3",
"label": "3",
"value": "3",
},
Object {
"id": "4",
"label": "4",
"value": "4",
},
]
}
perPage={50}
query=""
/>
<div
className="multi-select__help"
aria-live="polite"
class="multi-select__wrapper"
>
<div
class="more-modal__list"
>
<div
aria-live="polite"
class="sr-only"
role="status"
>
5 results found for your search.
</div>
<div
aria-atomic="true"
aria-live="polite"
class="sr-only"
/>
<div
class="more-modal__options"
id="multiSelectList"
role="presentation"
>
<div>
0
</div>
<div>
1
</div>
<div>
2
</div>
<div>
3
</div>
<div>
4
</div>
</div>
</div>
</div>
<div
class="multi-select__help"
id="multiSelectMessageNote"
/>
<div
className="filter-controls"
class="filter-controls"
>
<button
className="btn btn-sm btn-tertiary filter-control filter-control__next"
onClick={[Function]}
class="btn btn-sm btn-tertiary filter-control filter-control__next"
>
<MemoizedFormattedMessage
defaultMessage="Next"
id="filtered_user_list.next"
/>
Next
</button>
</div>
</div>
</Fragment>
</div>
`;
exports[`components/multiselect/multiselect should match snapshot for page 2 1`] = `
<Fragment>
<div>
<div
className="filtered-user-list"
class="filtered-user-list"
>
<div
className="filter-row"
class="filter-row"
>
<div
className="multi-select__container react-select"
class="multi-select__container react-select"
>
<ForwardRef
aria-invalid={false}
className=""
classNamePrefix="react-select-auto react-select"
components={
Object {
"IndicatorsContainer": [Function],
"Menu": [Function],
"MultiValueLabel": [Function],
"MultiValueRemove": [Function],
}
}
getOptionLabel={[Function]}
getOptionValue={[Function]}
<div
class="css-1dedlln"
id="selectItems"
inputValue=""
isClearable={false}
isMulti={true}
menuIsOpen={false}
onBlur={[Function]}
onChange={[Function]}
onInputChange={[Function]}
onKeyDown={[Function]}
openMenuOnFocus={false}
options={
Array [
Object {
"id": "0",
"label": "0",
"value": "0",
},
Object {
"id": "1",
"label": "1",
"value": "1",
},
Object {
"id": "2",
"label": "2",
"value": "2",
},
Object {
"id": "3",
"label": "3",
"value": "3",
},
Object {
"id": "4",
"label": "4",
"value": "4",
},
Object {
"id": "5",
"label": "5",
"value": "5",
},
Object {
"id": "6",
"label": "6",
"value": "6",
},
Object {
"id": "7",
"label": "7",
"value": "7",
},
]
}
styles={
Object {
"container": [Function],
}
}
value={
Array [
Object {
"id": "id",
"label": "label",
"value": "value",
},
]
}
/>
<SaveButton
disabled={false}
>
<span
class="css-1f43avz-a11yText-A11yText"
id="react-select-3-live-region"
/>
<span
aria-atomic="false"
aria-live="polite"
aria-relevant="additions text"
class="css-1f43avz-a11yText-A11yText"
role="log"
/>
<div
class="react-select-auto react-select__control css-13cymwt-control"
>
<div
class="react-select-auto react-select__value-container react-select-auto react-select__value-container--is-multi react-select-auto react-select__value-container--has-value css-1dyz3mf"
>
<div
class="react-select-auto react-select__multi-value css-1p3m7a8-multiValue"
>
<div
class="react-select__padded-component"
>
<span>
label
</span>
</div>
<div
aria-label="Remove label"
class="react-select-auto react-select__multi-value__remove css-v7duua"
role="button"
>
<svg
aria-hidden="true"
class="css-tj5bde-Svg"
focusable="false"
height="14"
viewBox="0 0 20 20"
width="14"
>
<path
d="M14.348 14.849c-0.469 0.469-1.229 0.469-1.697 0l-2.651-3.030-2.651 3.029c-0.469 0.469-1.229 0.469-1.697 0-0.469-0.469-0.469-1.229 0-1.697l2.758-3.15-2.759-3.152c-0.469-0.469-0.469-1.228 0-1.697s1.228-0.469 1.697 0l2.652 3.031 2.651-3.031c0.469-0.469 1.228-0.469 1.697 0s0.469 1.229 0 1.697l-2.758 3.152 2.758 3.15c0.469 0.469 0.469 1.229 0 1.698z"
/>
</svg>
</div>
</div>
<div
class="react-select-auto react-select__input-container css-19bb58m"
data-value=""
>
<input
aria-activedescendant=""
aria-autocomplete="list"
aria-expanded="false"
aria-haspopup="true"
aria-invalid="false"
autocapitalize="none"
autocomplete="off"
autocorrect="off"
class="react-select-auto react-select__input"
id="react-select-3-input"
role="combobox"
spellcheck="false"
style="color: inherit; background: 0px; opacity: 1; width: 100%; grid-area: 1 / 2; min-width: 2px; border: 0px; margin: 0px; outline: 0; padding: 0px;"
tabindex="0"
type="text"
value=""
/>
</div>
</div>
</div>
</div>
<button
class="btn btn-primary "
data-testid="saveSetting"
id="saveItems"
onClick={[Function]}
saving={false}
/>
type="submit"
>
<span>
Save
</span>
</button>
</div>
<div
className="multi-select__help"
class="multi-select__help"
id="multiSelectHelpMemberInfo"
>
<MemoizedFormattedMessage
defaultMessage="{memberOptions, number} of {totalCount, number} members"
id="multiselect.numMembers"
values={
Object {
"memberOptions": 3,
"totalCount": 8,
}
}
/>
3 of 8 members
</div>
</div>
<MultiSelectList
ariaLabelRenderer={[Function]}
onAction={[Function]}
onAdd={[Function]}
onSelect={[Function]}
optionRenderer={[Function]}
options={
Array [
Object {
"id": "5",
"label": "5",
"value": "5",
},
Object {
"id": "6",
"label": "6",
"value": "6",
},
Object {
"id": "7",
"label": "7",
"value": "7",
},
]
}
perPage={50}
query=""
/>
<div
className="multi-select__help"
aria-live="polite"
class="multi-select__wrapper"
>
<div
class="more-modal__list"
>
<div
aria-live="polite"
class="sr-only"
role="status"
>
3 results found for your search.
</div>
<div
aria-atomic="true"
aria-live="polite"
class="sr-only"
>
5
</div>
<div
class="more-modal__options"
id="multiSelectList"
role="presentation"
>
<div>
5
</div>
<div>
6
</div>
<div>
7
</div>
</div>
</div>
</div>
<div
class="multi-select__help"
id="multiSelectMessageNote"
/>
<div
className="filter-controls"
class="filter-controls"
>
<button
className="btn btn-sm btn-tertiary filter-control filter-control__prev"
onClick={[Function]}
class="btn btn-sm btn-tertiary filter-control filter-control__prev"
>
<MemoizedFormattedMessage
defaultMessage="Previous"
id="filtered_user_list.prev"
/>
Previous
</button>
</div>
</div>
</Fragment>
</div>
`;

View file

@ -1,73 +1,70 @@
// 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 {IntlShape} from 'react-intl';
import {createIntl} from 'react-intl';
import {mountWithIntl} from 'tests/helpers/intl-test-helper';
import {renderWithContext, screen, userEvent} from 'tests/react_testing_utils';
import MultiSelect from './multiselect';
import type {Value} from './multiselect';
import MultiSelectList from './multiselect_list';
import type {Props as MultiSelectProps} from './multiselect_list';
const element = () => <div/>;
describe('components/multiselect/multiselect', () => {
const intl = createIntl({locale: 'en'});
const totalCount = 8;
const optionsNumber = 8;
const users = [];
const users: Value[] = [];
for (let i = 0; i < optionsNumber; i++) {
users.push({id: `${i}`, label: `${i}`, value: `${i}`});
}
const baseProps = {
ariaLabelRenderer: element as any,
ariaLabelRenderer: (option: Value) => option?.label ?? '',
handleAdd: jest.fn(),
handleDelete: jest.fn(),
handleInput: jest.fn(),
handleSubmit: jest.fn(),
intl: {} as IntlShape,
optionRenderer: element,
intl,
optionRenderer: (option: Value) => <div key={option.id}>{option.label}</div>,
options: users,
perPage: 5,
saving: false,
totalCount,
users,
valueRenderer: element as any,
valueRenderer: (props: {data: Value}) => <span>{props.data.label}</span>,
values: [{id: 'id', label: 'label', value: 'value'}],
valueWithImage: false,
focusOnLoad: false,
};
test('should match snapshot', () => {
const wrapper = shallow(
const {container} = renderWithContext(
<MultiSelect
{...baseProps}
/>,
);
expect(wrapper).toMatchSnapshot();
expect(container).toMatchSnapshot();
});
test('should match snapshot for page 2', () => {
const wrapper = shallow(
test('should match snapshot for page 2', async () => {
const {container} = renderWithContext(
<MultiSelect
{...baseProps}
/>,
);
wrapper.find('.filter-control__next').simulate('click');
wrapper.update();
expect(wrapper.state('page')).toEqual(1);
expect(wrapper).toMatchSnapshot();
await userEvent.click(screen.getByText('Next'));
expect(container).toMatchSnapshot();
});
test('MultiSelectList should match state on next page', () => {
test('MultiSelectList should match state on next page', async () => {
const renderOption: MultiSelectProps<Value>['optionRenderer'] = (option, isSelected, onAdd, onMouseMove) => {
return (
<p
key={option.id}
className={isSelected ? 'option--selected' : ''}
onClick={() => onAdd(option)}
onMouseMove={() => onMouseMove(option)}
>
@ -80,7 +77,7 @@ describe('components/multiselect/multiselect', () => {
return props.data.value;
};
const wrapper = mountWithIntl(
renderWithContext(
<MultiSelect
{...baseProps}
optionRenderer={renderOption}
@ -88,9 +85,14 @@ describe('components/multiselect/multiselect', () => {
/>,
);
expect(wrapper.find(MultiSelectList).state('selected')).toEqual(-1);
wrapper.find('.filter-control__next').simulate('click');
expect(wrapper.find(MultiSelectList).state('selected')).toEqual(0);
// Initially no option should be selected (selected = -1)
expect(document.querySelector('.option--selected')).not.toBeInTheDocument();
// Click next page
await userEvent.click(screen.getByText('Next'));
// After clicking next, the first option on the new page should be selected (selected = 0)
expect(document.querySelector('.option--selected')).toBeInTheDocument();
});
test('MultiSelectList should match snapshot when custom no option message is defined', () => {
@ -100,19 +102,19 @@ describe('components/multiselect/multiselect', () => {
</div>
);
const wrapper = shallow(
const {container} = renderWithContext(
<MultiSelect
{...baseProps}
customNoOptionsMessage={customNoOptionsMessage}
/>,
);
expect(wrapper).toMatchSnapshot();
expect(container).toMatchSnapshot();
});
test('Back button should be customizable', () => {
test('Back button should be customizable', async () => {
const handleBackButtonClick = jest.fn();
const wrapper = mountWithIntl(
renderWithContext(
<MultiSelect
{...baseProps}
backButtonClick={handleBackButtonClick}
@ -122,11 +124,11 @@ describe('components/multiselect/multiselect', () => {
/>,
);
const backButton = wrapper.find('div.multi-select__footer button.tertiary-button');
const backButton = screen.getByRole('button', {name: 'Cancel'});
backButton.simulate('click');
await userEvent.click(backButton);
expect(backButton).toHaveLength(1);
expect(backButton).toBeInTheDocument();
expect(handleBackButtonClick).toHaveBeenCalled();
});

View file

@ -1,18 +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 {fireEvent, renderWithContext} from 'tests/react_testing_utils';
import type {Value} from './multiselect';
import MultiSelectList from './multiselect_list';
import type {Props as MultiSelectProps} from './multiselect_list';
const element = () => <div/>;
describe('components/multiselect/multiselect', () => {
const optionsNumber = 8;
const users = [];
const users: Value[] = [];
for (let i = 0; i < optionsNumber; i++) {
users.push({id: `${i}`, label: `${i}`, value: `${i}`});
}
@ -28,11 +27,11 @@ describe('components/multiselect/multiselect', () => {
} as any;
const baseProps = {
ariaLabelRenderer: element as any,
ariaLabelRenderer: (() => <div/>) as any,
loading: false,
onAdd: jest.fn(),
onSelect: jest.fn(),
optionRenderer: element,
optionRenderer: (() => <div/>) as any,
selectedItemRef,
options: users,
};
@ -42,7 +41,6 @@ describe('components/multiselect/multiselect', () => {
return (
<p
key={option.id}
ref={isSelected ? selectedItemRef : option.id}
onClick={() => onAdd(option)}
onMouseMove={() => onMouseMove(option)}
>
@ -51,23 +49,22 @@ describe('components/multiselect/multiselect', () => {
);
};
const wrapper = shallow(
renderWithContext(
<MultiSelectList
{...baseProps}
optionRenderer={renderOption}
/>,
);
(wrapper.instance() as any).listRef = {
current: {
getBoundingClientRect: jest.fn(() => ({
bottom: 50,
top: 50,
})),
},
} as any;
const listEl = document.getElementById('multiSelectList')!;
jest.spyOn(listEl, 'getBoundingClientRect').mockReturnValue({
bottom: 50,
top: 50,
} as DOMRect);
// fireEvent on document used because userEvent.keyboard requires element focus
fireEvent.keyDown(document, {key: 'ArrowDown'});
wrapper.setState({selected: 1});
expect(selectedItemRef.current.scrollIntoView).toHaveBeenCalledWith(false);
});
@ -76,7 +73,6 @@ describe('components/multiselect/multiselect', () => {
return (
<p
key={option.id}
ref={isSelected ? selectedItemRef : option.id}
onClick={() => onAdd(option)}
onMouseMove={() => onMouseMove(option)}
>
@ -85,23 +81,22 @@ describe('components/multiselect/multiselect', () => {
);
};
const wrapper = shallow(
renderWithContext(
<MultiSelectList
{...baseProps}
optionRenderer={renderOption}
/>,
);
(wrapper.instance() as any).listRef = {
current: {
getBoundingClientRect: jest.fn(() => ({
bottom: 200,
top: 60,
})),
},
} as any;
const listEl = document.getElementById('multiSelectList')!;
jest.spyOn(listEl, 'getBoundingClientRect').mockReturnValue({
bottom: 200,
top: 60,
} as DOMRect);
// fireEvent on document used because userEvent.keyboard requires element focus
fireEvent.keyDown(document, {key: 'ArrowDown'});
wrapper.setState({selected: 1});
expect(selectedItemRef.current.scrollIntoView).toHaveBeenCalledWith(true);
});
});

View file

@ -1,89 +1,81 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`components/onboarding_tasklist/onboarding_tasklist_completed.tsx should match snapshot 1`] = `
<Fragment>
<CSSTransition
classNames="fade"
in={true}
timeout={150}
<div>
<div
class="CompletedWrapper-byXeYZ caqIuy"
>
<CompletedWrapper>
<img
alt="completed tasks image"
src=""
/>
<h2>
<MemoizedFormattedMessage
defaultMessage="Well done. Youve completed all of the tasks!"
id="onboardingTask.checklist.completed_title"
/>
</h2>
<span
className="completed-subtitle"
>
<MemoizedFormattedMessage
defaultMessage="We hope Mattermost is more familiar now."
id="onboardingTask.checklist.completed_subtitle"
/>
<img
alt="completed tasks image"
src=""
/>
<h2>
Well done. Youve completed all of the tasks!
</h2>
<span
class="completed-subtitle"
>
We hope Mattermost is more familiar now.
</span>
<span
class="start-trial-text"
>
Interested in our higher-security features?
<br />
Start your free Enterprise trial now!
</span>
<a
class="btn btn-secondary"
id="start_trial_btn"
>
Start trial
</a>
<button
class="no-thanks-link style-link"
>
No, thanks
</button>
<div
class="download-apps"
>
<span>
Now that youre all set up,
<a
href="https://mattermost.com/download?utm_source=mattermost&utm_medium=in-product&utm_content=onboarding_tasklist_completed&uid=&sid=&edition=team&server_version=#desktop"
location="onboarding_tasklist_completed"
rel="noopener noreferrer"
target="_blank"
>
download our apps.
</a>
</span>
<span
className="start-trial-text"
>
<MemoizedFormattedMessage
defaultMessage="Interested in our higher-security features?"
id="onboardingTask.checklist.higher_security_features"
/>
<br />
<MemoizedFormattedMessage
defaultMessage="Start your free Enterprise trial now!"
id="onboardingTask.checklist.start_enterprise_now"
/>
</div>
<div
class="disclaimer"
>
<span>
By clicking “Start trial”, I agree to the
<a
href="https://mattermost.com/pl/software-and-services-license-agreement?utm_source=mattermost&utm_medium=in-product&utm_content=onboarding_tasklist_completed&uid=&sid=&edition=team&server_version="
location="onboarding_tasklist_completed"
rel="noopener noreferrer"
target="_blank"
>
Mattermost Software Evaluation Agreement
</a>
,
<a
href="https://mattermost.com/pl/privacy-policy/?utm_source=mattermost&utm_medium=in-product&utm_content=onboarding_tasklist_completed&uid=&sid=&edition=team&server_version="
location="onboarding_tasklist_completed"
rel="noopener noreferrer"
target="_blank"
>
privacy policy
</a>
and receiving product emails.
</span>
<StartTrialBtn
onClick={[MockFunction]}
/>
<button
className="no-thanks-link style-link"
onClick={[MockFunction]}
>
<MemoizedFormattedMessage
defaultMessage="No, thanks"
id="onboardingTask.checklist.no_thanks"
/>
</button>
<div
className="download-apps"
>
<span>
<MemoizedFormattedMessage
defaultMessage="Now that youre all set up, <link>download our apps.</link>"
id="onboardingTask.checklist.downloads"
values={
Object {
"link": [Function],
}
}
/>
</span>
</div>
<div
className="disclaimer"
>
<span>
<MemoizedFormattedMessage
defaultMessage="By clicking “Start trial”, I agree to the <linkEvaluation>Mattermost Software Evaluation Agreement</linkEvaluation>, <linkPrivacy>privacy policy</linkPrivacy> and receiving product emails."
id="onboardingTask.checklist.disclaimer"
values={
Object {
"linkEvaluation": [Function],
"linkPrivacy": [Function],
}
}
/>
</span>
</div>
</CompletedWrapper>
</CSSTransition>
</Fragment>
</div>
</div>
</div>
`;

View file

@ -1,21 +1,24 @@
// 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 Completed from './onboarding_tasklist_completed';
let mockState: any;
const mockDispatch = jest.fn();
const dismissMockFn = jest.fn();
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('mattermost-redux/actions/admin', () => ({
...jest.requireActual('mattermost-redux/actions/admin'),
getPrevTrialLicense: () => ({type: 'MOCK_GET_PREV_TRIAL_LICENSE'}),
}));
jest.mock('components/common/hooks/useOpenStartTrialFormModal', () => ({
__esModule: true,
default: () => jest.fn(),
}));
const dismissMockFn = jest.fn();
describe('components/onboarding_tasklist/onboarding_tasklist_completed.tsx', () => {
const props = {
dismissAction: dismissMockFn,
@ -23,51 +26,45 @@ describe('components/onboarding_tasklist/onboarding_tasklist_completed.tsx', ()
isFirstAdmin: true,
};
beforeEach(() => {
mockState = {
entities: {
admin: {
prevTrialLicense: {
IsLicensed: 'false',
},
},
general: {
license: {
IsLicensed: 'false',
},
},
cloud: {
subscription: {
product_id: 'prod_professional',
is_free_trial: 'false',
trial_end_at: 1,
},
const initialState = {
entities: {
admin: {
prevTrialLicense: {
IsLicensed: 'false',
},
},
};
});
afterEach(() => {
jest.restoreAllMocks();
});
general: {
license: {
IsLicensed: 'false',
},
},
cloud: {
subscription: {
product_id: 'prod_professional',
is_free_trial: 'false',
trial_end_at: 1,
},
},
},
};
test('should match snapshot', () => {
const wrapper = shallow(<Completed {...props}/>);
expect(wrapper).toMatchSnapshot();
const {container} = renderWithContext(<Completed {...props}/>, initialState);
expect(container).toMatchSnapshot();
});
test('finds the completed subtitle', () => {
const wrapper = shallow(<Completed {...props}/>);
expect(wrapper.find('.completed-subtitle')).toHaveLength(1);
const {container} = renderWithContext(<Completed {...props}/>, initialState);
expect(container.querySelectorAll('.completed-subtitle')).toHaveLength(1);
});
test('displays the no thanks option to close the onboarding list', () => {
const wrapper = shallow(<Completed {...props}/>);
const noThanksLink = wrapper.find('.no-thanks-link');
test('displays the no thanks option to close the onboarding list', async () => {
const {container} = renderWithContext(<Completed {...props}/>, initialState);
const noThanksLink = container.querySelectorAll('.no-thanks-link');
expect(noThanksLink).toHaveLength(1);
// calls the dissmiss function on click
noThanksLink.simulate('click');
await userEvent.click(noThanksLink[0]);
expect(dismissMockFn).toHaveBeenCalledTimes(1);
});
});

View file

@ -1,11 +1,9 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {mount} from 'enzyme';
import React from 'react';
import {Provider} from 'react-redux';
import configureStore from 'store';
import {renderWithContext, screen} from 'tests/react_testing_utils';
import {useTasksList} from './onboarding_tasks_manager';
@ -49,49 +47,44 @@ describe('onboarding tasks manager', () => {
};
it('Places all the elements (6 ignoring plugins) when user is first admin or admin', () => {
const store = configureStore(initialState);
const wrapper = mount(
<Provider store={store}>
<WrapperComponent/>
</Provider>,
renderWithContext(
<WrapperComponent/>,
initialState,
);
expect(wrapper.find('li')).toHaveLength(6);
expect(screen.getAllByRole('listitem')).toHaveLength(6);
// find the visit system console and start_trial
expect(wrapper.findWhere((node) => node.key() === 'visit_system_console')).toHaveLength(1);
expect(wrapper.findWhere((node) => node.key() === 'start_trial')).toHaveLength(1);
expect(screen.getByText('visit_system_console')).toBeInTheDocument();
expect(screen.getByText('start_trial')).toBeInTheDocument();
});
it('Removes start_trial and visit_system_console when user is end user', () => {
const endUserState = {...initialState, entities: {...initialState.entities, users: {...initialState.entities.users, currentUserId: user2}}};
const store = configureStore(endUserState);
const wrapper = mount(
<Provider store={store}>
<WrapperComponent/>
</Provider>,
renderWithContext(
<WrapperComponent/>,
endUserState,
);
expect(wrapper.find('li')).toHaveLength(4);
expect(screen.getAllByRole('listitem')).toHaveLength(4);
// verify visit_system_console and start_trial were removed
expect(wrapper.findWhere((node) => node.key() === 'visit_system_console')).toHaveLength(0);
expect(wrapper.findWhere((node) => node.key() === 'start_trial')).toHaveLength(0);
expect(screen.queryByText('visit_system_console')).not.toBeInTheDocument();
expect(screen.queryByText('start_trial')).not.toBeInTheDocument();
});
it('Removes invite people task item when user is GUEST user', () => {
const endUserState = {...initialState, entities: {...initialState.entities, users: {...initialState.entities.users, currentUserId: user3}}};
const store = configureStore(endUserState);
const wrapper = mount(
<Provider store={store}>
<WrapperComponent/>
</Provider>,
renderWithContext(
<WrapperComponent/>,
endUserState,
);
expect(wrapper.find('li')).toHaveLength(3);
expect(screen.getAllByRole('listitem')).toHaveLength(3);
// verify visit_system_console and start_trial were removed
expect(wrapper.findWhere((node) => node.key() === 'invite_people')).toHaveLength(0);
expect(screen.queryByText('invite_people')).not.toBeInTheDocument();
});
});

View file

@ -1,313 +1,151 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`PostAttachmentOpenGraph should match snapshot 1`] = `
<Provider
store={
Object {
"clearActions": [Function],
"dispatch": [Function],
"getActions": [Function],
"getState": [Function],
"replaceReducer": [Function],
"subscribe": [Function],
}
}
>
<Connect(PostAttachmentOpenGraph)
actions={
Object {
"editPost": [MockFunction],
}
}
currentUserId="1234"
link="http://mattermost.com"
openGraphData={
Object {
"description": "description",
"images": Array [
Object {
"secure_url": "",
"url": "http://mattermost.com/OpenGraphImage.jpg",
},
],
"site_name": "Mattermost",
"title": "Mattermost Private Cloud Messaging",
}
}
post={
Object {
"channel_id": "channel_id",
"create_at": 1,
"id": "post_id_1",
"message": "https://mattermost.com",
"metadata": Object {
"images": Object {
"http://mattermost.com/OpenGraphImage.jpg": Object {
"format": "png",
"frameCount": 0,
"height": 100,
"width": 100,
},
},
},
"root_id": "root_id",
}
}
postId=""
toggleEmbedVisibility={[MockFunction]}
<div>
<a
class="PostAttachmentOpenGraph"
href="https://www.mattermost.com/?utm_source=mattermost&utm_medium=in-product&utm_content=post_attachment_opengraph&uid=user-1&sid=&edition=team&server_version="
location="post_attachment_opengraph"
rel="noopener noreferrer"
role="link"
target="_blank"
title="Mattermost | Open Source Collaboration for Developers"
>
<PostAttachmentOpenGraph
actions={
Object {
"editPost": [Function],
}
}
currentUserId="user-1"
enableLinkPreviews={true}
imageCollapsed={false}
link="http://mattermost.com"
post={
Object {
"channel_id": "channel_id",
"create_at": 1,
"id": "post_id_1",
"message": "https://mattermost.com",
"metadata": Object {
"images": Object {
"http://mattermost.com/OpenGraphImage.jpg": Object {
"format": "png",
"frameCount": 0,
"height": 100,
"width": 100,
},
},
},
"root_id": "root_id",
}
}
postId=""
previewEnabled={true}
toggleEmbedVisibility={[MockFunction]}
/>
</Connect(PostAttachmentOpenGraph)>
</Provider>
<div
class="PostAttachmentOpenGraph__body"
>
<span
class="sitename"
>
Mattermost.com
</span>
<span
class="title"
>
Mattermost | Open Source Collaboration for Developers
</span>
<span
class="description"
>
Mattermost is a secure, open source platform for communication, collaboration, and workflow orchestration across tools and teams.
</span>
</div>
<div
class="PostAttachmentOpenGraph__image large"
>
<div
class="AutoHeight"
style="transition-property: height; transition-duration: 250ms; transition-timing-function: ease; width: 100%; height: auto; overflow: visible;"
>
<div>
<button
class="preview-toggle style--none"
>
<svg
fill="currentColor"
height="18"
version="1.1"
viewBox="0 0 24 24"
width="18"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M7,10L12,15L17,10H7Z"
/>
</svg>
</button>
<figure>
<img
alt="Mattermost | Open Source Collaboration for Developers"
src="/api/v4/image?url=http%3A%2F%2Flocalhost%3A8065%2Fapi%2Fv4%2Fimage%3Furl%3Dhttp%253A%252F%252Fmattermo%E2%80%A6t.com%252Fwp-content%252Fuploads%252F2021%252F09%252FHomepage%25402x.png"
/>
</figure>
</div>
</div>
</div>
</a>
</div>
`;
exports[`PostAttachmentOpenGraphBody should match snapshot 1`] = `
<Memo()
description="test-description"
isInPermalink={false}
sitename="test-sitename"
title="test-title"
>
<div>
<div
className="PostAttachmentOpenGraph__body"
class="PostAttachmentOpenGraph__body"
>
<span
className="sitename"
class="sitename"
>
test-sitename
</span>
<span
className="title"
class="title"
>
test-title
</span>
<span
className="description"
class="description"
>
test-description
</span>
</div>
</Memo()>
</div>
`;
exports[`PostAttachmentOpenGraphBody should match snapshot 2`] = `
<Memo()
description="test_description"
isInPermalink={false}
siteName="test_sitename"
title="test_title"
>
<div>
<div
className="PostAttachmentOpenGraph__body"
class="PostAttachmentOpenGraph__body"
>
<span
className="title"
class="title"
>
test_title
</span>
<span
className="description"
class="description"
>
test_description
</span>
</div>
</Memo()>
</div>
`;
exports[`PostAttachmentOpenGraphImage should match snapshot 1`] = `
<Provider
store={
Object {
"clearActions": [Function],
"dispatch": [Function],
"getActions": [Function],
"getState": [Function],
"replaceReducer": [Function],
"subscribe": [Function],
}
}
>
<Memo()
imageMetadata={
Object {
"format": "png",
"frameCount": 0,
"height": 1256,
"secure_url": "http://localhost:8065/api/v4/image?url=http%3A%2F%2Fmattermo…t.com%2Fwp-content%2Fuploads%2F2021%2F09%2FHomepage%402x.png",
"type": "image/png",
"url": "",
"width": 2400,
}
}
isEmbedVisible={true}
isInPermalink={false}
title="test_image"
toggleEmbedVisibility={[MockFunction]}
<div>
<div
class="PostAttachmentOpenGraph__image large"
>
<div
className="PostAttachmentOpenGraph__image large"
class="AutoHeight"
style="transition-property: height; transition-duration: 250ms; transition-timing-function: ease; width: 100%; height: auto; overflow: visible;"
>
<AutoHeightSwitcher
showSlot={1}
slot1={
<Memo(Connect(Component))
imageMetadata={
Object {
"format": "png",
"frameCount": 0,
"height": 1256,
"secure_url": "http://localhost:8065/api/v4/image?url=http%3A%2F%2Fmattermo…t.com%2Fwp-content%2Fuploads%2F2021%2F09%2FHomepage%402x.png",
"type": "image/png",
"url": "",
"width": 2400,
}
}
src="http://localhost:8065/api/v4/image?url=http%3A%2F%2Fmattermo…t.com%2Fwp-content%2Fuploads%2F2021%2F09%2FHomepage%402x.png"
>
[Function]
</Memo(Connect(Component))>
}
slot2={
<button
className="preview-toggle style--none"
onClick={[Function]}
>
<MenuDownIcon
color="currentColor"
size={18}
/>
</button>
}
>
<Transition
appear={false}
enter={true}
exit={true}
in={false}
mountOnEnter={false}
onEnter={[Function]}
onEntered={[Function]}
onEntering={[Function]}
onExit={[Function]}
onExited={[Function]}
onExiting={[Function]}
timeout={250}
unmountOnExit={false}
<div>
<button
class="preview-toggle style--none"
>
<div
className="AutoHeight"
style={
Object {
"height": "auto",
"overflow": "visible",
"transitionDuration": "250ms",
"transitionProperty": "height",
"transitionTimingFunction": "ease",
"width": "100%",
}
}
<svg
fill="currentColor"
height="18"
version="1.1"
viewBox="0 0 24 24"
width="18"
xmlns="http://www.w3.org/2000/svg"
>
<div>
<Connect(Component)
imageMetadata={
Object {
"format": "png",
"frameCount": 0,
"height": 1256,
"secure_url": "http://localhost:8065/api/v4/image?url=http%3A%2F%2Fmattermo…t.com%2Fwp-content%2Fuploads%2F2021%2F09%2FHomepage%402x.png",
"type": "image/png",
"url": "",
"width": 2400,
}
}
src="http://localhost:8065/api/v4/image?url=http%3A%2F%2Fmattermo…t.com%2Fwp-content%2Fuploads%2F2021%2F09%2FHomepage%402x.png"
>
<Memo(ExternalImage)
dispatch={[Function]}
enableSVGs={true}
hasImageProxy={true}
imageMetadata={
Object {
"format": "png",
"frameCount": 0,
"height": 1256,
"secure_url": "http://localhost:8065/api/v4/image?url=http%3A%2F%2Fmattermo…t.com%2Fwp-content%2Fuploads%2F2021%2F09%2FHomepage%402x.png",
"type": "image/png",
"url": "",
"width": 2400,
}
}
src="http://localhost:8065/api/v4/image?url=http%3A%2F%2Fmattermo…t.com%2Fwp-content%2Fuploads%2F2021%2F09%2FHomepage%402x.png"
>
<button
className="preview-toggle style--none"
onClick={[Function]}
>
<MenuDownIcon
color="currentColor"
size={18}
>
<svg
fill="currentColor"
height={18}
version="1.1"
viewBox="0 0 24 24"
width={18}
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M7,10L12,15L17,10H7Z"
/>
</svg>
</MenuDownIcon>
</button>
<figure>
<img
alt="test_image"
src="/api/v4/image?url=http%3A%2F%2Flocalhost%3A8065%2Fapi%2Fv4%2Fimage%3Furl%3Dhttp%253A%252F%252Fmattermo%E2%80%A6t.com%252Fwp-content%252Fuploads%252F2021%252F09%252FHomepage%25402x.png"
/>
</figure>
</Memo(ExternalImage)>
</Connect(Component)>
</div>
</div>
</Transition>
</AutoHeightSwitcher>
<path
d="M7,10L12,15L17,10H7Z"
/>
</svg>
</button>
<figure>
<img
alt="test_image"
src="/api/v4/image?url=http%3A%2F%2Flocalhost%3A8065%2Fapi%2Fv4%2Fimage%3Furl%3Dhttp%253A%252F%252Fmattermo%E2%80%A6t.com%252Fwp-content%252Fuploads%252F2021%252F09%252FHomepage%25402x.png"
/>
</figure>
</div>
</div>
</Memo()>
</Provider>
</div>
</div>
`;
exports[`PostAttachmentOpenGraphImage should not render when used in Permalink 1`] = `Object {}`;
exports[`PostAttachmentOpenGraphImage should not render when used in Permalink 1`] = `<div />`;

View file

@ -1,17 +1,13 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {mount} from 'enzyme';
import cloneDeep from 'lodash/cloneDeep';
import set from 'lodash/set';
import React from 'react';
import {Provider} from 'react-redux';
import type {OpenGraphMetadata, Post} from '@mattermost/types/posts';
import {getPreferenceKey} from 'mattermost-redux/utils/preference_utils';
import mockStore from 'tests/test_store';
import {render, renderWithContext} from 'tests/react_testing_utils';
import {Preferences} from 'utils/constants';
import {getBestImage, getIsLargeImage, PostAttachmentOpenGraphImage, PostAttachmentOpenGraphBody} from './post_attachment_opengraph';
@ -91,18 +87,9 @@ describe('PostAttachmentOpenGraph', () => {
const baseProps = {
post,
postId: '',
link: 'http://mattermost.com',
postId: 'post_id_1',
link: openGraphData.url,
currentUserId: '1234',
openGraphData: {
description: 'description',
images: [{
secure_url: '',
url: imageUrl,
}],
site_name: 'Mattermost',
title: 'Mattermost Private Cloud Messaging',
},
toggleEmbedVisibility,
actions: {
editPost: jest.fn(),
@ -110,60 +97,72 @@ describe('PostAttachmentOpenGraph', () => {
};
test('should match snapshot', () => {
const store = mockStore(initialState);
const wrapper = mount(
<Provider store={store}>
<PostAttachmentOpenGraph {...baseProps}/>
</Provider>,
const {container} = renderWithContext(
<PostAttachmentOpenGraph {...baseProps}/>,
initialState,
);
expect(wrapper).toMatchSnapshot();
expect(container).toMatchSnapshot();
});
test('should render nothing without any data', () => {
const state = cloneDeep(initialState);
set(state, 'entities.posts.openGraph', {});
const store = mockStore(state);
const wrapper = mount(
<Provider store={store}>
<PostAttachmentOpenGraph {...baseProps}/>
</Provider>,
const {container} = renderWithContext(
<PostAttachmentOpenGraph {...baseProps}/>,
{
...initialState,
entities: {
...initialState.entities,
posts: {
...initialState.entities.posts,
openGraph: {},
},
},
},
);
expect(wrapper).toEqual({});
expect(container).toBeEmptyDOMElement();
});
test('should render nothing when link previews are disabled on the server', () => {
const state = cloneDeep(initialState);
set(state, 'entities.config.EnableLinkPreviews', 'false');
const store = mockStore(state);
const wrapper = mount(
<Provider store={store}>
<PostAttachmentOpenGraph {...baseProps}/>
</Provider>,
const {container} = renderWithContext(
<PostAttachmentOpenGraph {...baseProps}/>,
{
...initialState,
entities: {
...initialState.entities,
general: {
...initialState.entities.general,
config: {
...initialState.entities.general.config,
EnableLinkPreviews: 'false',
},
},
},
},
);
expect(wrapper).toEqual({});
expect(container).toBeEmptyDOMElement();
});
test('should render nothing when link previews are disabled by the user', () => {
const state = cloneDeep(initialState);
set(state, `entities.preferences.EnableLinkPreviews["${preferenceKeys.LINK_PREVIEW_DISPLAY}"]`, 'false');
const store = mockStore(state);
const wrapper = mount(
<Provider store={store}>
<PostAttachmentOpenGraph {...baseProps}/>
</Provider>,
const {container} = renderWithContext(
<PostAttachmentOpenGraph {...baseProps}/>,
{
...initialState,
entities: {
...initialState.entities,
preferences: {
...initialState.entities.preferences,
myPreferences: {
...initialState.entities.preferences.myPreferences,
[preferenceKeys.LINK_PREVIEW_DISPLAY]: {value: 'false'},
},
},
},
},
);
expect(wrapper).toEqual({});
expect(container).toBeEmptyDOMElement();
});
});
@ -176,62 +175,72 @@ describe('PostAttachmentOpenGraphBody', () => {
};
test('should match snapshot', () => {
const wrapper = mount(<PostAttachmentOpenGraphBody {...baseProps}/>);
const {container} = render(<PostAttachmentOpenGraphBody {...baseProps}/>);
expect(wrapper).toMatchSnapshot();
expect(container).toMatchSnapshot();
});
test('should not render without title', () => {
const props = cloneDeep(baseProps);
set(props, 'title', '');
const props = {
...baseProps,
title: '',
};
const wrapper = mount(<PostAttachmentOpenGraphBody {...props}/>);
const {container} = render(<PostAttachmentOpenGraphBody {...props}/>);
expect(wrapper).toEqual({});
expect(container).toBeEmptyDOMElement();
});
test('should add extra class for permalink view', () => {
const props = cloneDeep(baseProps);
set(props, 'isInPermalink', true);
const props = {
...baseProps,
isInPermalink: true,
};
const wrapper = mount(<PostAttachmentOpenGraphBody {...props}/>);
const {container} = render(<PostAttachmentOpenGraphBody {...props}/>);
expect(wrapper.find('.isInPermalink').exists()).toBe(true);
expect(wrapper.find('.sitename').exists()).toBe(false);
expect(container.querySelector('.isInPermalink')).toBeInTheDocument();
expect(container.querySelector('.sitename')).not.toBeInTheDocument();
});
test('should render without sitename', () => {
const props = cloneDeep(baseProps);
set(props, 'sitename', '');
const props = {
...baseProps,
sitename: '',
};
const wrapper = mount(<PostAttachmentOpenGraphBody {...props}/>);
const {container} = render(<PostAttachmentOpenGraphBody {...props}/>);
expect(wrapper.find('.sitename').exists()).toBe(false);
expect(wrapper.find('.title').exists()).toBe(true);
expect(wrapper.find('.description').exists()).toBe(true);
expect(container.querySelector('.sitename')).not.toBeInTheDocument();
expect(container.querySelector('.title')).toBeInTheDocument();
expect(container.querySelector('.description')).toBeInTheDocument();
});
test('should render without description', () => {
const props = cloneDeep(baseProps);
set(props, 'description', '');
const props = {
...baseProps,
description: '',
};
const wrapper = mount(<PostAttachmentOpenGraphBody {...props}/>);
const {container} = render(<PostAttachmentOpenGraphBody {...props}/>);
expect(wrapper.find('.sitename').exists()).toBe(true);
expect(wrapper.find('.title').exists()).toBe(true);
expect(wrapper.find('.description').exists()).toBe(false);
expect(container.querySelector('.sitename')).toBeInTheDocument();
expect(container.querySelector('.title')).toBeInTheDocument();
expect(container.querySelector('.description')).not.toBeInTheDocument();
});
test('should render with title only', () => {
const props = cloneDeep(baseProps);
set(props, 'sitename', '');
set(props, 'description', '');
const props = {
...baseProps,
sitename: '',
description: '',
};
const wrapper = mount(<PostAttachmentOpenGraphBody {...props}/>);
const {container} = render(<PostAttachmentOpenGraphBody {...props}/>);
expect(wrapper.find('.sitename').exists()).toBe(false);
expect(wrapper.find('.title').exists()).toBe(true);
expect(wrapper.find('.description').exists()).toBe(false);
expect(container.querySelector('.sitename')).not.toBeInTheDocument();
expect(container.querySelector('.title')).toBeInTheDocument();
expect(container.querySelector('.description')).not.toBeInTheDocument();
});
});
@ -245,62 +254,57 @@ describe('PostAttachmentOpenGraphImage', () => {
};
test('should match snapshot', () => {
const store = mockStore(initialState);
const wrapper = mount(
<Provider store={store}>
<PostAttachmentOpenGraphImage {...baseProps}/>
</Provider>,
const {container} = renderWithContext(
<PostAttachmentOpenGraphImage {...baseProps}/>,
initialState,
);
expect(wrapper).toMatchSnapshot();
expect(container).toMatchSnapshot();
});
test('should not render when used in Permalink', () => {
const props = cloneDeep(baseProps);
set(props, 'isInPermalink', true);
const props = {
...baseProps,
isInPermalink: true,
};
const store = mockStore(initialState);
const wrapper = mount(
<Provider store={store}>
<PostAttachmentOpenGraphImage {...props}/>
</Provider>,
const {container} = renderWithContext(
<PostAttachmentOpenGraphImage {...props}/>,
initialState,
);
expect(wrapper).toMatchSnapshot({});
expect(container).toMatchSnapshot();
});
test('should render a large image with toggle', () => {
const store = mockStore(initialState);
const wrapper = mount(
<Provider store={store}>
<PostAttachmentOpenGraphImage {...baseProps}/>
</Provider>,
const {container} = renderWithContext(
<PostAttachmentOpenGraphImage {...baseProps}/>,
initialState,
);
expect(wrapper.find('.PostAttachmentOpenGraph__image').exists()).toBe(true);
expect(wrapper.find('.PostAttachmentOpenGraph__image.large').exists()).toBe(true);
expect(wrapper.find('.PostAttachmentOpenGraph__image .preview-toggle').exists()).toBe(true);
expect(container.querySelector('.PostAttachmentOpenGraph__image')).toBeInTheDocument();
expect(container.querySelector('.PostAttachmentOpenGraph__image.large')).toBeInTheDocument();
expect(container.querySelector('.PostAttachmentOpenGraph__image .preview-toggle')).toBeInTheDocument();
});
test('should render a small image without toggle', () => {
const props = cloneDeep(baseProps);
set(props, 'imageMetadata.height', 90);
set(props, 'imageMetadata.width', 120);
const props = {
...baseProps,
imageMetadata: {
...baseProps.imageMetadata!,
height: 90,
width: 120,
},
};
const store = mockStore(initialState);
const wrapper = mount(
<Provider store={store}>
<PostAttachmentOpenGraphImage {...props}/>
</Provider>,
const {container} = renderWithContext(
<PostAttachmentOpenGraphImage {...props}/>,
initialState,
);
expect(wrapper.find('.PostAttachmentOpenGraph__image').exists()).toBe(true);
expect(wrapper.find('.PostAttachmentOpenGraph__image.large').exists()).toBe(false);
expect(wrapper.find('.PostAttachmentOpenGraph__image .preview-toggle').exists()).toBe(false);
expect(container.querySelector('.PostAttachmentOpenGraph__image')).toBeInTheDocument();
expect(container.querySelector('.PostAttachmentOpenGraph__image.large')).not.toBeInTheDocument();
expect(container.querySelector('.PostAttachmentOpenGraph__image .preview-toggle')).not.toBeInTheDocument();
});
});
@ -313,24 +317,26 @@ describe('PostAttachmentOpenGraphBody', () => {
};
test('should match snapshot', () => {
const wrapper = mount(
const {container} = render(
<PostAttachmentOpenGraphBody {...baseProps}/>,
);
expect(wrapper).toMatchSnapshot();
expect(container).toMatchSnapshot();
});
describe('permalink preview', () => {
const props = {
...baseProps,
isInPermalink: true,
};
test('should add extra class for permalink view', () => {
const props = {
...baseProps,
isInPermalink: true,
};
const wrapper = mount(
<PostAttachmentOpenGraphBody {...props}/>,
);
const {container} = render(
<PostAttachmentOpenGraphBody {...props}/>,
);
expect(wrapper.find('.isInPermalink').exists()).toBe(true);
expect(container.querySelector('.isInPermalink')).toBeInTheDocument();
});
});
});

View file

@ -2,372 +2,202 @@
exports[`PostBodyAdditionalContent with a YouTube video should not render content when isEmbedVisible is false 1`] = `
<div>
<button
aria-label="Toggle Embed Visibility"
className="style--none post__embed-visibility color--link pull-left"
data-expanded={false}
key="toggle"
onClick={[Function]}
/>
<span>
some children
</span>
<div>
<button
aria-label="Toggle Embed Visibility"
class="style--none post__embed-visibility color--link pull-left"
data-expanded="false"
/>
<span>
some children
</span>
</div>
</div>
`;
exports[`PostBodyAdditionalContent with a YouTube video should render correctly 1`] = `
<div>
<button
aria-label="Toggle Embed Visibility"
className="style--none post__embed-visibility color--link pull-left"
data-expanded={true}
key="toggle"
onClick={[Function]}
/>
<span>
some children
</span>
<Connect(YoutubeVideo)
link="https://www.youtube.com/watch?v=d-YO3v-wJts"
postId="post_id_1"
show={true}
/>
<div>
<button
aria-label="Toggle Embed Visibility"
class="style--none post__embed-visibility color--link pull-left"
data-expanded="true"
/>
<span>
some children
</span>
<div
data-testid="youtube-video"
/>
</div>
</div>
`;
exports[`PostBodyAdditionalContent with a YouTube video should render the toggle after a message containing more than just a link 1`] = `
<div>
<span>
some children
</span>
<button
aria-label="Toggle Embed Visibility"
className="style--none post__embed-visibility color--link "
data-expanded={true}
key="toggle"
onClick={[Function]}
/>
<Connect(YoutubeVideo)
link="https://www.youtube.com/watch?v=d-YO3v-wJts"
postId="post_id_1"
show={true}
/>
<div>
<span>
some children
</span>
<button
aria-label="Toggle Embed Visibility"
class="style--none post__embed-visibility color--link "
data-expanded="true"
/>
<div
data-testid="youtube-video"
/>
</div>
</div>
`;
exports[`PostBodyAdditionalContent with a message attachment should render correctly 1`] = `
<div>
<span>
some children
</span>
<Memo(MessageAttachmentList)
attachments={Array []}
postId="post_id_1"
/>
<div>
<span>
some children
</span>
<div
data-testid="message-attachment-list"
/>
</div>
</div>
`;
exports[`PostBodyAdditionalContent with a normal link Should render nothing if the plugin matches but isEmbedVisible is false 1`] = `
<div>
<span>
some children
</span>
<div>
<span>
some children
</span>
</div>
</div>
`;
exports[`PostBodyAdditionalContent with a normal link Should render nothing if the registered plugins don't match 1`] = `
<div>
<span>
some children
</span>
<div>
<span>
some children
</span>
</div>
</div>
`;
exports[`PostBodyAdditionalContent with a normal link Should render the plugin component if it matches and is not toggeable 1`] = `
<div>
<span>
some children
</span>
<EmbedMP3
embed={
Object {
"type": "link",
"url": "https://example.com/song.mp3",
}
}
webSocketClient={
WebSocketClient {
"closeCallback": null,
"closeListeners": Set {},
"config": Object {
"clientPingInterval": 30000,
"maxWebSocketFails": 7,
"maxWebSocketRetryTime": 300000,
"minWebSocketRetryTime": 3000,
"newWebSocketFn": [Function],
"reconnectJitterRange": 2000,
},
"conn": null,
"connectFailCount": 0,
"connectionId": "",
"errorCallback": null,
"errorListeners": Set {},
"eventCallback": null,
"firstConnectCallback": null,
"firstConnectListeners": Set {},
"lastErrCode": null,
"messageListeners": Set {},
"missedEventCallback": null,
"missedMessageListeners": Set {},
"offlineHandler": null,
"onlineHandler": null,
"pingInterval": null,
"postedAck": false,
"reconnectCallback": null,
"reconnectListeners": Set {},
"reconnectTimeout": null,
"responseCallbacks": Object {},
"responseSequence": 1,
"serverHostname": "",
"serverSequence": 0,
"waitingForPong": false,
}
}
/>
<div>
<span>
some children
</span>
<div
data-testid="embed-mp3"
/>
</div>
</div>
`;
exports[`PostBodyAdditionalContent with a normal link Should render the plugin component if it matches and is toggeable 1`] = `
<div>
<button
aria-label="Toggle Embed Visibility"
className="style--none post__embed-visibility color--link pull-left"
data-expanded={true}
key="toggle"
onClick={[Function]}
/>
<span>
some children
</span>
<EmbedMP3
embed={
Object {
"type": "link",
"url": "https://example.com/song.mp3",
}
}
webSocketClient={
WebSocketClient {
"closeCallback": null,
"closeListeners": Set {},
"config": Object {
"clientPingInterval": 30000,
"maxWebSocketFails": 7,
"maxWebSocketRetryTime": 300000,
"minWebSocketRetryTime": 3000,
"newWebSocketFn": [Function],
"reconnectJitterRange": 2000,
},
"conn": null,
"connectFailCount": 0,
"connectionId": "",
"errorCallback": null,
"errorListeners": Set {},
"eventCallback": null,
"firstConnectCallback": null,
"firstConnectListeners": Set {},
"lastErrCode": null,
"messageListeners": Set {},
"missedEventCallback": null,
"missedMessageListeners": Set {},
"offlineHandler": null,
"onlineHandler": null,
"pingInterval": null,
"postedAck": false,
"reconnectCallback": null,
"reconnectListeners": Set {},
"reconnectTimeout": null,
"responseCallbacks": Object {},
"responseSequence": 1,
"serverHostname": "",
"serverSequence": 0,
"waitingForPong": false,
}
}
/>
<div>
<button
aria-label="Toggle Embed Visibility"
class="style--none post__embed-visibility color--link pull-left"
data-expanded="true"
/>
<span>
some children
</span>
<div
data-testid="embed-mp3"
/>
</div>
</div>
`;
exports[`PostBodyAdditionalContent with a permalinklink Render permalink preview 1`] = `
<div>
<span>
some children
</span>
<Connect(PostMessagePreview)
handleFileDropdownOpened={[MockFunction]}
metadata={
Object {
"channel_display_name": "channel1",
"channel_id": "channel_id",
"channel_type": "O",
"post_id": "post_id123",
"team_name": "core",
}
}
/>
<div>
<span>
some children
</span>
<div
data-testid="post-message-preview"
/>
</div>
</div>
`;
exports[`PostBodyAdditionalContent with a permalinklink Render permalink preview with no data 1`] = `
<div>
<span>
some children
</span>
<div>
<span>
some children
</span>
</div>
</div>
`;
exports[`PostBodyAdditionalContent with an image preview should render correctly 1`] = `
<div>
<button
aria-label="Toggle Embed Visibility"
className="style--none post__embed-visibility color--link pull-left"
data-expanded={true}
key="toggle"
onClick={[Function]}
/>
<span>
some children
</span>
<Connect(PostImage)
imageMetadata={Object {}}
link="https://example.com/image.png"
post={
Object {
"channel_id": "channel_id",
"create_at": 1,
"id": "post_id_1",
"message": "https://example.com/image.png",
"metadata": Object {
"embeds": Array [
Object {
"type": "image",
"url": "https://example.com/image.png",
},
],
"emojis": Array [],
"files": Array [],
"images": Object {
"https://example.com/image.png": Object {},
},
"reactions": Array [],
},
"root_id": "root_id",
}
}
/>
<div>
<button
aria-label="Toggle Embed Visibility"
class="style--none post__embed-visibility color--link pull-left"
data-expanded="true"
/>
<span>
some children
</span>
<div
data-image-metadata="present"
data-testid="post-image"
/>
</div>
</div>
`;
exports[`PostBodyAdditionalContent with an image preview should render the toggle after a message containing more than just a link 1`] = `
<div>
<span>
some children
</span>
<button
aria-label="Toggle Embed Visibility"
className="style--none post__embed-visibility color--link "
data-expanded={true}
key="toggle"
onClick={[Function]}
/>
<Connect(PostImage)
imageMetadata={Object {}}
link="https://example.com/image.png"
post={
Object {
"channel_id": "channel_id",
"create_at": 1,
"id": "post_id_1",
"message": "This is an image: https://example.com/image.png",
"metadata": Object {
"embeds": Array [
Object {
"type": "image",
"url": "https://example.com/image.png",
},
],
"emojis": Array [],
"files": Array [],
"images": Object {
"https://example.com/image.png": Object {},
},
"reactions": Array [],
},
"root_id": "root_id",
}
}
/>
<div>
<span>
some children
</span>
<button
aria-label="Toggle Embed Visibility"
class="style--none post__embed-visibility color--link "
data-expanded="true"
/>
<div
data-image-metadata="present"
data-testid="post-image"
/>
</div>
</div>
`;
exports[`PostBodyAdditionalContent with an opengraph preview should render correctly 1`] = `
<div>
<span>
some children
</span>
<Connect(PostAttachmentOpenGraph)
isEmbedVisible={true}
link="https://example.com/image.png"
post={
Object {
"channel_id": "channel_id",
"create_at": 1,
"id": "post_id_1",
"message": "https://example.com/image.png",
"metadata": Object {
"embeds": Array [
Object {
"type": "opengraph",
"url": "https://example.com/image.png",
},
],
},
"root_id": "root_id",
}
}
postId="post_id_1"
toggleEmbedVisibility={[Function]}
/>
<div>
<span>
some children
</span>
<div
data-testid="post-attachment-opengraph"
/>
</div>
</div>
`;
exports[`PostBodyAdditionalContent with an opengraph preview should render the toggle after a message containing more than just a link 1`] = `
<div>
<span>
some children
</span>
<Connect(PostAttachmentOpenGraph)
isEmbedVisible={true}
link="https://example.com/image.png"
post={
Object {
"channel_id": "channel_id",
"create_at": 1,
"id": "post_id_1",
"message": "This is a link: https://example.com/image.png",
"metadata": Object {
"embeds": Array [
Object {
"type": "opengraph",
"url": "https://example.com/image.png",
},
],
},
"root_id": "root_id",
}
}
postId="post_id_1"
toggleEmbedVisibility={[Function]}
/>
<div>
<span>
some children
</span>
<div
data-testid="post-attachment-opengraph"
/>
</div>
</div>
`;

View file

@ -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 {
@ -13,10 +12,7 @@ import type {
import {getEmbedFromMetadata} from 'mattermost-redux/utils/post_utils';
import MessageAttachmentList from 'components/post_view/message_attachments/message_attachment_list';
import PostAttachmentOpenGraph from 'components/post_view/post_attachment_opengraph';
import PostImageComponent from 'components/post_view/post_image';
import YoutubeVideo from 'components/youtube_video';
import {render, screen, userEvent} from 'tests/react_testing_utils';
import PostBodyAdditionalContent from './post_body_additional_content';
import type {Props} from './post_body_additional_content';
@ -29,6 +25,52 @@ jest.mock('mattermost-redux/utils/post_utils', () => {
};
});
jest.mock('components/post_view/post_image', () => ({
__esModule: true,
default: jest.fn((props: any) => (
<div
data-testid='post-image'
data-image-metadata={props.imageMetadata ? 'present' : 'absent'}
/>
)),
}));
jest.mock('components/post_view/message_attachments/message_attachment_list', () => ({
__esModule: true,
default: jest.fn(() => (
<div data-testid='message-attachment-list'/>
)),
}));
jest.mock('components/post_view/post_attachment_opengraph', () => ({
__esModule: true,
default: jest.fn(() => (
<div data-testid='post-attachment-opengraph'/>
)),
}));
jest.mock('components/youtube_video', () => {
const MockYoutubeVideo: any = jest.fn(() => (
<div data-testid='youtube-video'/>
));
MockYoutubeVideo.isYoutubeLink = (link: string) => {
return (/^https?:\/\/((www\.)?youtube\.com|youtu\.be)\/.+/).test(link);
};
return {__esModule: true, default: MockYoutubeVideo};
});
jest.mock('components/post_view/post_message_preview', () => ({
__esModule: true,
default: jest.fn(() => (
<div data-testid='post-message-preview'/>
)),
}));
jest.mock('client/web_websocket_client', () => ({
__esModule: true,
default: {},
}));
describe('PostBodyAdditionalContent', () => {
const baseProps: Props = {
children: <span>{'some children'}</span>,
@ -73,11 +115,10 @@ describe('PostBodyAdditionalContent', () => {
};
test('should render correctly', () => {
const wrapper = shallow(<PostBodyAdditionalContent {...imageBaseProps}/>);
const {container} = render(<PostBodyAdditionalContent {...imageBaseProps}/>);
expect(wrapper).toMatchSnapshot();
expect(wrapper.find(PostImageComponent).exists()).toBe(true);
expect(wrapper.find(PostImageComponent).prop('imageMetadata')).toBe(imageMetadata);
expect(container).toMatchSnapshot();
expect(screen.getByTestId('post-image')).toBeInTheDocument();
});
test('should render the toggle after a message containing more than just a link', () => {
@ -89,9 +130,9 @@ describe('PostBodyAdditionalContent', () => {
},
};
const wrapper = shallow(<PostBodyAdditionalContent {...props}/>);
const {container} = render(<PostBodyAdditionalContent {...props}/>);
expect(wrapper).toMatchSnapshot();
expect(container).toMatchSnapshot();
});
test('should not render content when isEmbedVisible is false', () => {
@ -100,9 +141,9 @@ describe('PostBodyAdditionalContent', () => {
isEmbedVisible: false,
};
const wrapper = shallow(<PostBodyAdditionalContent {...props}/>);
render(<PostBodyAdditionalContent {...props}/>);
expect(wrapper.find(PostImageComponent).exists()).toBe(false);
expect(screen.queryByTestId('post-image')).not.toBeInTheDocument();
});
});
@ -125,11 +166,10 @@ describe('PostBodyAdditionalContent', () => {
};
test('should render correctly', () => {
const wrapper = shallow(<PostBodyAdditionalContent {...messageAttachmentBaseProps}/>);
const {container} = render(<PostBodyAdditionalContent {...messageAttachmentBaseProps}/>);
expect(wrapper).toMatchSnapshot();
expect(wrapper.find(MessageAttachmentList).exists()).toBe(true);
expect(wrapper.find(MessageAttachmentList).prop('attachments')).toBe(attachments);
expect(container).toMatchSnapshot();
expect(screen.getByTestId('message-attachment-list')).toBeInTheDocument();
});
test('should render content when isEmbedVisible is false', () => {
@ -138,9 +178,9 @@ describe('PostBodyAdditionalContent', () => {
isEmbedVisible: false,
};
const wrapper = shallow(<PostBodyAdditionalContent {...props}/>);
render(<PostBodyAdditionalContent {...props}/>);
expect(wrapper.find(MessageAttachmentList).exists()).toBe(true);
expect(screen.getByTestId('message-attachment-list')).toBeInTheDocument();
});
});
@ -162,10 +202,10 @@ describe('PostBodyAdditionalContent', () => {
};
test('should render correctly', () => {
const wrapper = shallow(<PostBodyAdditionalContent {...ogBaseProps}/>);
const {container} = render(<PostBodyAdditionalContent {...ogBaseProps}/>);
expect(wrapper.find(PostAttachmentOpenGraph).exists()).toBe(true);
expect(wrapper).toMatchSnapshot();
expect(screen.getByTestId('post-attachment-opengraph')).toBeInTheDocument();
expect(container).toMatchSnapshot();
});
test('should render the toggle after a message containing more than just a link', () => {
@ -177,9 +217,9 @@ describe('PostBodyAdditionalContent', () => {
},
};
const wrapper = shallow(<PostBodyAdditionalContent {...props}/>);
const {container} = render(<PostBodyAdditionalContent {...props}/>);
expect(wrapper).toMatchSnapshot();
expect(container).toMatchSnapshot();
});
test('should render content when isEmbedVisible is false', () => {
@ -188,9 +228,9 @@ describe('PostBodyAdditionalContent', () => {
isEmbedVisible: false,
};
const wrapper = shallow(<PostBodyAdditionalContent {...props}/>);
render(<PostBodyAdditionalContent {...props}/>);
expect(wrapper.find(PostAttachmentOpenGraph).exists()).toBe(true);
expect(screen.getByTestId('post-attachment-opengraph')).toBeInTheDocument();
});
});
@ -212,10 +252,10 @@ describe('PostBodyAdditionalContent', () => {
};
test('should render correctly', () => {
const wrapper = shallow(<PostBodyAdditionalContent {...youtubeBaseProps}/>);
const {container} = render(<PostBodyAdditionalContent {...youtubeBaseProps}/>);
expect(wrapper.find(YoutubeVideo).exists()).toBe(true);
expect(wrapper).toMatchSnapshot();
expect(screen.getByTestId('youtube-video')).toBeInTheDocument();
expect(container).toMatchSnapshot();
});
test('should render the toggle after a message containing more than just a link', () => {
@ -227,9 +267,9 @@ describe('PostBodyAdditionalContent', () => {
},
};
const wrapper = shallow(<PostBodyAdditionalContent {...props}/>);
const {container} = render(<PostBodyAdditionalContent {...props}/>);
expect(wrapper).toMatchSnapshot();
expect(container).toMatchSnapshot();
});
test('should not render content when isEmbedVisible is false', () => {
@ -238,17 +278,17 @@ describe('PostBodyAdditionalContent', () => {
isEmbedVisible: false,
};
const wrapper = shallow(<PostBodyAdditionalContent {...props}/>);
const {container} = render(<PostBodyAdditionalContent {...props}/>);
expect(wrapper.find(YoutubeVideo).exists()).toBe(false);
expect(wrapper).toMatchSnapshot();
expect(screen.queryByTestId('youtube-video')).not.toBeInTheDocument();
expect(container).toMatchSnapshot();
});
});
describe('with a normal link', () => {
const mp3Url = 'https://example.com/song.mp3';
const EmbedMP3 = () => <></>;
const EmbedMP3 = () => <div data-testid='embed-mp3'/>;
const linkBaseProps = {
...baseProps,
@ -278,9 +318,9 @@ describe('PostBodyAdditionalContent', () => {
],
};
const wrapper = shallow(<PostBodyAdditionalContent {...props}/>);
expect(wrapper.find(EmbedMP3).exists()).toBe(false);
expect(wrapper).toMatchSnapshot();
const {container} = render(<PostBodyAdditionalContent {...props}/>);
expect(screen.queryByTestId('embed-mp3')).not.toBeInTheDocument();
expect(container).toMatchSnapshot();
});
test('Should render the plugin component if it matches and is toggeable', () => {
@ -297,10 +337,10 @@ describe('PostBodyAdditionalContent', () => {
],
};
const wrapper = shallow(<PostBodyAdditionalContent {...props}/>);
expect(wrapper.find(EmbedMP3).exists()).toBe(true);
expect(wrapper.find('button.post__embed-visibility').exists()).toBe(true);
expect(wrapper).toMatchSnapshot();
const {container} = render(<PostBodyAdditionalContent {...props}/>);
expect(screen.getByTestId('embed-mp3')).toBeInTheDocument();
expect(container.querySelector('button.post__embed-visibility')).toBeInTheDocument();
expect(container).toMatchSnapshot();
});
test('Should render the plugin component if it matches and is not toggeable', () => {
@ -317,10 +357,10 @@ describe('PostBodyAdditionalContent', () => {
],
};
const wrapper = shallow(<PostBodyAdditionalContent {...props}/>);
expect(wrapper.find(EmbedMP3).exists()).toBe(true);
expect(wrapper.find('button.post__embed-visibility').exists()).toBe(false);
expect(wrapper).toMatchSnapshot();
const {container} = render(<PostBodyAdditionalContent {...props}/>);
expect(screen.getByTestId('embed-mp3')).toBeInTheDocument();
expect(container.querySelector('button.post__embed-visibility')).not.toBeInTheDocument();
expect(container).toMatchSnapshot();
});
test('Should render nothing if the plugin matches but isEmbedVisible is false', () => {
@ -338,16 +378,38 @@ describe('PostBodyAdditionalContent', () => {
isEmbedVisible: false,
};
const wrapper = shallow(<PostBodyAdditionalContent {...props}/>);
expect(wrapper.find(EmbedMP3).exists()).toBe(false);
expect(wrapper).toMatchSnapshot();
const {container} = render(<PostBodyAdditionalContent {...props}/>);
expect(screen.queryByTestId('embed-mp3')).not.toBeInTheDocument();
expect(container).toMatchSnapshot();
});
});
test('should call toggleEmbedVisibility with post id', () => {
const wrapper = shallow<PostBodyAdditionalContent>(<PostBodyAdditionalContent {...baseProps}/>);
test('should call toggleEmbedVisibility with post id', async () => {
const imageUrl = 'https://example.com/image.png';
const props = {
...baseProps,
post: {
...baseProps.post,
message: imageUrl,
metadata: {
embeds: [{
type: 'image',
url: imageUrl,
}],
images: {
[imageUrl]: {} as PostImage,
},
emojis: [],
files: [],
reactions: [],
} as PostMetadata,
},
};
wrapper.instance().toggleEmbedVisibility();
render(<PostBodyAdditionalContent {...props}/>);
const toggleButton = screen.getByRole('button', {name: 'Toggle Embed Visibility'});
await userEvent.click(toggleButton);
expect(baseProps.actions.toggleEmbedVisibility).toHaveBeenCalledTimes(1);
expect(baseProps.actions.toggleEmbedVisibility).toHaveBeenCalledWith('post_id_1');
@ -367,8 +429,7 @@ describe('PostBodyAdditionalContent', () => {
},
};
const wrapper = shallow<PostBodyAdditionalContent>(<PostBodyAdditionalContent {...props}/>);
wrapper.instance().getEmbed();
render(<PostBodyAdditionalContent {...props}/>);
expect(getEmbedFromMetadata).toHaveBeenCalledWith(metadata);
});
@ -402,8 +463,8 @@ describe('PostBodyAdditionalContent', () => {
};
test('Render permalink preview', () => {
const wrapper = shallow(<PostBodyAdditionalContent {...permalinkBaseProps}/>);
expect(wrapper).toMatchSnapshot();
const {container} = render(<PostBodyAdditionalContent {...permalinkBaseProps}/>);
expect(container).toMatchSnapshot();
});
test('Render permalink preview with no data', () => {
@ -421,8 +482,8 @@ describe('PostBodyAdditionalContent', () => {
},
};
const wrapper = shallow(<PostBodyAdditionalContent {...props}/>);
expect(wrapper).toMatchSnapshot();
const {container} = render(<PostBodyAdditionalContent {...props}/>);
expect(container).toMatchSnapshot();
});
});
});

View file

@ -1,275 +1,195 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`components/post_view/Reaction should apply read-only class if user does not have permission to add reaction 1`] = `
<Connect(ReactionTooltip)
canAddReactions={false}
canRemoveReactions={true}
currentUserReacted={false}
emojiName="smile"
onShow={[Function]}
reactions={
Array [
Object {
"create_at": 0,
"emoji_name": ":smile:",
"post_id": "post_id",
"user_id": "user_id_2",
},
Object {
"create_at": 0,
"emoji_name": ":smile:",
"post_id": "post_id",
"user_id": "user_id_3",
},
]
}
>
<button
aria-label="2 users reacted with :smile:"
className="Reaction Reaction--unreacted Reaction--read-only"
id="postReaction-post_id-smile"
onClick={[Function]}
<div>
<div
data-testid="reaction-tooltip"
>
<span
className="d-flex align-items-center"
<button
aria-label="2 users reacted with :smile:"
class="Reaction Reaction--unreacted Reaction--read-only"
id="postReaction-post_id-smile"
>
<img
alt=""
aria-hidden={true}
className="Reaction__emoji emoticon"
src="emoji_image_url"
/>
<span
className="Reaction__count"
class="d-flex align-items-center"
>
<img
alt=""
aria-hidden="true"
class="Reaction__emoji emoticon"
src="emoji_image_url"
/>
<span
className="Reaction__number"
class="Reaction__count"
>
<span
className="Reaction__number--display"
class="Reaction__number"
>
2
</span>
<span
className="Reaction__number--unreacted"
onAnimationEnd={[Function]}
>
2
</span>
<span
className="Reaction__number--reacted"
>
3
<span
class="Reaction__number--display"
>
2
</span>
<span
class="Reaction__number--unreacted"
>
2
</span>
<span
class="Reaction__number--reacted"
>
3
</span>
</span>
</span>
</span>
</span>
</button>
</Connect(ReactionTooltip)>
</button>
</div>
</div>
`;
exports[`components/post_view/Reaction should apply read-only class if user does not have permission to remove reaction 1`] = `
<Connect(ReactionTooltip)
canAddReactions={true}
canRemoveReactions={false}
currentUserReacted={true}
emojiName="smile"
onShow={[Function]}
reactions={
Array [
Object {
"create_at": 0,
"emoji_name": ":smile:",
"post_id": "post_id",
"user_id": "user_id_2",
},
Object {
"create_at": 0,
"emoji_name": ":smile:",
"post_id": "post_id",
"user_id": "user_id_3",
},
]
}
>
<button
aria-label="2 users reacted with :smile:"
className="Reaction Reaction--reacted Reaction--read-only"
id="postReaction-post_id-smile"
onClick={[Function]}
<div>
<div
data-testid="reaction-tooltip"
>
<span
className="d-flex align-items-center"
<button
aria-label="2 users reacted with :smile:"
class="Reaction Reaction--reacted Reaction--read-only"
id="postReaction-post_id-smile"
>
<img
alt=""
aria-hidden={true}
className="Reaction__emoji emoticon"
src="emoji_image_url"
/>
<span
className="Reaction__count"
class="d-flex align-items-center"
>
<img
alt=""
aria-hidden="true"
class="Reaction__emoji emoticon"
src="emoji_image_url"
/>
<span
className="Reaction__number"
class="Reaction__count"
>
<span
className="Reaction__number--display"
class="Reaction__number"
>
2
</span>
<span
className="Reaction__number--unreacted"
onAnimationEnd={[Function]}
>
1
</span>
<span
className="Reaction__number--reacted"
>
2
<span
class="Reaction__number--display"
>
2
</span>
<span
class="Reaction__number--unreacted"
>
1
</span>
<span
class="Reaction__number--reacted"
>
2
</span>
</span>
</span>
</span>
</span>
</button>
</Connect(ReactionTooltip)>
</button>
</div>
</div>
`;
exports[`components/post_view/Reaction should match snapshot 1`] = `
<Connect(ReactionTooltip)
canAddReactions={true}
canRemoveReactions={true}
currentUserReacted={false}
emojiName="smile"
onShow={[Function]}
reactions={
Array [
Object {
"create_at": 0,
"emoji_name": ":smile:",
"post_id": "post_id",
"user_id": "user_id_2",
},
Object {
"create_at": 0,
"emoji_name": ":smile:",
"post_id": "post_id",
"user_id": "user_id_3",
},
]
}
>
<button
aria-label="2 users reacted with :smile:. Click to add."
className="Reaction Reaction--unreacted "
id="postReaction-post_id-smile"
onClick={[Function]}
<div>
<div
data-testid="reaction-tooltip"
>
<span
className="d-flex align-items-center"
<button
aria-label="2 users reacted with :smile:. Click to add."
class="Reaction Reaction--unreacted "
id="postReaction-post_id-smile"
>
<img
alt=""
aria-hidden={true}
className="Reaction__emoji emoticon"
src="emoji_image_url"
/>
<span
className="Reaction__count"
class="d-flex align-items-center"
>
<img
alt=""
aria-hidden="true"
class="Reaction__emoji emoticon"
src="emoji_image_url"
/>
<span
className="Reaction__number"
class="Reaction__count"
>
<span
className="Reaction__number--display"
class="Reaction__number"
>
2
</span>
<span
className="Reaction__number--unreacted"
onAnimationEnd={[Function]}
>
2
</span>
<span
className="Reaction__number--reacted"
>
3
<span
class="Reaction__number--display"
>
2
</span>
<span
class="Reaction__number--unreacted"
>
2
</span>
<span
class="Reaction__number--reacted"
>
3
</span>
</span>
</span>
</span>
</span>
</button>
</Connect(ReactionTooltip)>
</button>
</div>
</div>
`;
exports[`components/post_view/Reaction should match snapshot when a current user reacted to a post 1`] = `
<Connect(ReactionTooltip)
canAddReactions={true}
canRemoveReactions={true}
currentUserReacted={true}
emojiName="smile"
onShow={[Function]}
reactions={
Array [
Object {
"create_at": 0,
"emoji_name": ":cry:",
"post_id": "post_id",
"user_id": "user_id_1",
},
Object {
"create_at": 0,
"emoji_name": ":smile:",
"post_id": "post_id",
"user_id": "user_id_3",
},
]
}
>
<button
aria-label="2 users reacted with :smile:. Click to remove."
className="Reaction Reaction--reacted "
id="postReaction-post_id-smile"
onClick={[Function]}
<div>
<div
data-testid="reaction-tooltip"
>
<span
className="d-flex align-items-center"
<button
aria-label="2 users reacted with :smile:. Click to remove."
class="Reaction Reaction--reacted "
id="postReaction-post_id-smile"
>
<img
alt=""
aria-hidden={true}
className="Reaction__emoji emoticon"
src="emoji_image_url"
/>
<span
className="Reaction__count"
class="d-flex align-items-center"
>
<img
alt=""
aria-hidden="true"
class="Reaction__emoji emoticon"
src="emoji_image_url"
/>
<span
className="Reaction__number"
class="Reaction__count"
>
<span
className="Reaction__number--display"
class="Reaction__number"
>
2
</span>
<span
className="Reaction__number--unreacted"
onAnimationEnd={[Function]}
>
1
</span>
<span
className="Reaction__number--reacted"
>
2
<span
class="Reaction__number--display"
>
2
</span>
<span
class="Reaction__number--unreacted"
>
1
</span>
<span
class="Reaction__number--reacted"
>
2
</span>
</span>
</span>
</span>
</span>
</button>
</Connect(ReactionTooltip)>
</button>
</div>
</div>
`;
exports[`components/post_view/Reaction should return null/empty if no emojiImageUrl 1`] = `""`;
exports[`components/post_view/Reaction should return null/empty if no emojiImageUrl 1`] = `<div />`;

View file

@ -1,18 +1,28 @@
// 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 {Reaction as ReactionType} from '@mattermost/types/reactions';
import {Reaction as ReactionClass} from 'components/post_view/reaction/reaction';
import {getIntl} from 'utils/i18n';
import {renderWithContext, screen, userEvent} from 'tests/react_testing_utils';
import {TestHelper} from 'utils/test_helper';
import ReactionComponent from './reaction';
jest.mock('./reaction_tooltip', () => {
return {
__esModule: true,
default: ({children, onShow}: any) => (
<div
data-testid='reaction-tooltip'
onMouseEnter={onShow}
>{children}</div>
),
};
});
describe('components/post_view/Reaction', () => {
const intl = getIntl();
const post = TestHelper.getPostMock({
id: 'post_id',
});
@ -43,12 +53,11 @@ describe('components/post_view/Reaction', () => {
reactions,
emojiImageUrl: 'emoji_image_url',
actions,
intl,
};
test('should match snapshot', () => {
const wrapper = shallow<ReactionClass>(<ReactionClass {...baseProps}/>);
expect(wrapper).toMatchSnapshot();
const {container} = renderWithContext(<ReactionComponent {...baseProps}/>);
expect(container).toMatchSnapshot();
});
test('should match snapshot when a current user reacted to a post', () => {
@ -67,20 +76,20 @@ describe('components/post_view/Reaction', () => {
currentUserReacted: true,
reactions: newReactions,
};
const wrapper = shallow<ReactionClass>(<ReactionClass {...props}/>);
expect(wrapper).toMatchSnapshot();
const {container} = renderWithContext(<ReactionComponent {...props}/>);
expect(container).toMatchSnapshot();
});
test('should return null/empty if no emojiImageUrl', () => {
const props = {...baseProps, emojiImageUrl: ''};
const wrapper = shallow<ReactionClass>(<ReactionClass {...props}/>);
expect(wrapper).toMatchSnapshot();
const {container} = renderWithContext(<ReactionComponent {...props}/>);
expect(container).toMatchSnapshot();
});
test('should apply read-only class if user does not have permission to add reaction', () => {
const props = {...baseProps, canAddReactions: false};
const wrapper = shallow<ReactionClass>(<ReactionClass {...props}/>);
expect(wrapper).toMatchSnapshot();
const {container} = renderWithContext(<ReactionComponent {...props}/>);
expect(container).toMatchSnapshot();
});
test('should apply read-only class if user does not have permission to remove reaction', () => {
@ -89,13 +98,15 @@ describe('components/post_view/Reaction', () => {
canRemoveReactions: false,
currentUserReacted: true,
};
const wrapper = shallow<ReactionClass>(<ReactionClass {...props}/>);
expect(wrapper).toMatchSnapshot();
const {container} = renderWithContext(<ReactionComponent {...props}/>);
expect(container).toMatchSnapshot();
});
test('should have called actions.getMissingProfilesByIds when loadMissingProfiles is called', () => {
const wrapper = shallow<ReactionClass>(<ReactionClass {...baseProps}/>);
wrapper.instance().loadMissingProfiles();
test('should have called actions.getMissingProfilesByIds when loadMissingProfiles is called', async () => {
renderWithContext(<ReactionComponent {...baseProps}/>);
const tooltipTrigger = screen.getByTestId('reaction-tooltip');
await userEvent.hover(tooltipTrigger);
expect(actions.getMissingProfilesByIds).toHaveBeenCalledTimes(1);
expect(actions.getMissingProfilesByIds).toHaveBeenCalledWith([reactions[0].user_id, reactions[1].user_id]);

View file

@ -1,83 +1,19 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`components/ReactionList should render nothing when no reactions 1`] = `""`;
exports[`components/ReactionList should render nothing when no reactions 1`] = `<div />`;
exports[`components/ReactionList should render when there are reactions 1`] = `
<div
aria-label="reactions"
className="post-reaction-list"
>
<Connect(injectIntl(Reaction))
emojiName="expressionless"
key="expressionless"
post={
Object {
"channel_id": "",
"create_at": 0,
"delete_at": 0,
"edit_at": 0,
"hashtags": "",
"id": "post_id",
"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",
}
}
reactions={
Array [
Object {
"create_at": 1542994995740,
"emoji_name": "expressionless",
"post_id": "xbqfo5qb4bb4ffmj9hqfji6fiw",
"user_id": "1rj9fokoeffrigu7sk5uc8aiih",
},
]
}
/>
<AddReactionButton
onEmojiClick={[Function]}
post={
Object {
"channel_id": "",
"create_at": 0,
"delete_at": 0,
"edit_at": 0,
"hashtags": "",
"id": "post_id",
"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",
}
}
teamId="teamId"
/>
<div>
<div
aria-label="reactions"
class="post-reaction-list"
>
<div
data-testid="reaction-expressionless"
/>
<div
data-testid="add-reaction-button"
/>
</div>
</div>
`;

View file

@ -1,15 +1,31 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {shallow} from 'enzyme';
import React from 'react';
import Reaction from 'components/post_view/reaction';
import {render, screen} from 'tests/react_testing_utils';
import {TestHelper} from 'utils/test_helper';
import ReactionList from './reaction_list';
jest.mock('components/post_view/reaction', () => {
return {
__esModule: true,
default: (props: any) => (
<div data-testid={`reaction-${props.emojiName}`}/>
),
};
});
jest.mock('./add_reaction_button', () => {
return {
__esModule: true,
default: () => (
<div data-testid='add-reaction-button'/>
),
};
});
describe('components/ReactionList', () => {
const reaction = {
user_id: '1rj9fokoeffrigu7sk5uc8aiih',
@ -44,19 +60,19 @@ describe('components/ReactionList', () => {
reactions: {},
};
const wrapper = shallow<ReactionList>(
const {container} = render(
<ReactionList {...props}/>,
);
expect(wrapper).toMatchSnapshot();
expect(container).toMatchSnapshot();
});
test('should render when there are reactions', () => {
const wrapper = shallow<ReactionList>(
const {container} = render(
<ReactionList {...baseProps}/>,
);
expect(wrapper).toMatchSnapshot();
expect(container).toMatchSnapshot();
});
/*
@ -91,14 +107,16 @@ describe('components/ReactionList', () => {
},
};
const wrapper = shallow<ReactionList>(
const {rerender} = render(
<ReactionList {...propsA}/>,
);
const firstRender = wrapper.find(Reaction).map((node) => node.key);
const firstRender = screen.getAllByTestId(/^reaction-/).map((el) => el.getAttribute('data-testid'));
wrapper.setProps(propsB);
rerender(
<ReactionList {...propsB}/>,
);
const secondRender = wrapper.find(Reaction).map((node) => node.key);
const secondRender = screen.getAllByTestId(/^reaction-/).map((el) => el.getAttribute('data-testid'));
expect(firstRender.length).toBe(2);
expect(firstRender).toEqual(secondRender);

View file

@ -127,8 +127,10 @@ describe('SuggestionBox', () => {
expect(providerSpy).toHaveBeenCalledTimes(2);
});
expect(screen.queryByRole('listbox')).toBeVisible();
expect(screen.getByText('Suggestion: testwordstestwords')).toBeVisible();
await waitFor(() => {
expect(screen.queryByRole('listbox')).toBeVisible();
expect(screen.getByText('Suggestion: testwordstestwords')).toBeVisible();
});
// Clearing the textbox hides all suggestions
await userEvent.clear(screen.getByPlaceholderText('test input'));

View file

@ -1,88 +1,136 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`components/user_settings/advanced/JoinLeaveSection should match snapshot 1`] = `
<SettingItemMin
section="joinLeave"
title={
<Memo(MemoizedFormattedMessage)
defaultMessage="Enable Join/Leave Messages"
id="user.settings.advance.joinLeaveTitle"
/>
}
updateSection={[Function]}
/>
<div>
<div
class="section-min"
>
<div
class="section-min__header"
>
<h4
class="section-min__title"
id="joinLeaveTitle"
>
Enable Join/Leave Messages
</h4>
<button
aria-expanded="false"
aria-labelledby="joinLeaveTitle joinLeaveEdit"
class="color--link style--none section-min__edit"
id="joinLeaveEdit"
>
<i
aria-hidden="true"
class="icon-pencil-outline"
title="Edit Icon"
/>
Edit
</button>
</div>
<div
class="section-min__describe"
id="joinLeaveDesc"
>
<span>
On
</span>
</div>
</div>
</div>
`;
exports[`components/user_settings/advanced/JoinLeaveSection should match snapshot 2`] = `
<Memo(SettingItemMax)
inputs={
Array [
<fieldset>
<legend
className="form-legend hidden-label"
<div>
<section
class="section-max form-horizontal "
>
<h4
class="col-sm-12 section-title"
id="settingTitle"
>
Enable Join/Leave Messages
</h4>
<div
class="sectionContent col-sm-10 col-sm-offset-2"
>
<div
class="setting-list"
tabindex="-1"
>
<div
class="setting-list-item"
>
<Memo(MemoizedFormattedMessage)
defaultMessage="Enable Join/Leave Messages"
id="user.settings.advance.joinLeaveTitle"
<fieldset>
<legend
class="form-legend hidden-label"
>
Enable Join/Leave Messages
</legend>
<div
class="radio"
>
<label>
<input
checked=""
class="a11y--active a11y--focused"
id="joinLeaveOn"
name="joinLeave"
type="radio"
value="true"
/>
On
</label>
<br />
</div>
<div
class="radio"
>
<label>
<input
id="joinLeaveOff"
name="joinLeave"
type="radio"
value="false"
/>
Off
</label>
<br />
</div>
<div
class="mt-5"
>
When "On", System Messages saying a user has joined or left a channel will be visible. When "Off", the System Messages about joining or leaving a channel will be hidden. A message will still show up when you are added to a channel, so you can receive a notification.
</div>
</fieldset>
</div>
<div
class="setting-list-item"
>
<hr />
<div
role="alert"
/>
</legend>
<div
className="radio"
>
<label>
<input
checked={true}
id="joinLeaveOn"
name="joinLeave"
onChange={[Function]}
type="radio"
value="true"
/>
<Memo(MemoizedFormattedMessage)
defaultMessage="On"
id="user.settings.advance.on"
/>
</label>
<br />
<button
class="btn btn-primary "
data-testid="saveSetting"
id="saveSetting"
type="submit"
>
<span>
Save
</span>
</button>
<button
class="btn btn-tertiary"
data-testid="cancelButton"
id="cancelSetting"
>
Cancel
</button>
</div>
<div
className="radio"
>
<label>
<input
checked={false}
id="joinLeaveOff"
name="joinLeave"
onChange={[Function]}
type="radio"
value="false"
/>
<Memo(MemoizedFormattedMessage)
defaultMessage="Off"
id="user.settings.advance.off"
/>
</label>
<br />
</div>
<div
className="mt-5"
>
<Memo(MemoizedFormattedMessage)
defaultMessage="When \\"On\\", System Messages saying a user has joined or left a channel will be visible. When \\"Off\\", the System Messages about joining or leaving a channel will be hidden. A message will still show up when you are added to a channel, so you can receive a notification."
id="user.settings.advance.joinLeaveDesc"
/>
</div>
</fieldset>,
]
}
setting="joinLeave"
submit={[Function]}
title={
<Memo(MemoizedFormattedMessage)
defaultMessage="Enable Join/Leave Messages"
id="user.settings.advance.joinLeaveTitle"
/>
}
updateSection={[Function]}
/>
</div>
</div>
</section>
</div>
`;

View file

@ -1,18 +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 {Preferences} from 'mattermost-redux/constants';
import {getPreferenceKey} from 'mattermost-redux/utils/preference_utils';
import SettingItemMax from 'components/setting_item_max';
import SettingItemMin from 'components/setting_item_min';
import JoinLeaveSection from 'components/user_settings/advanced/join_leave_section/join_leave_section';
import mergeObjects from 'packages/mattermost-redux/test/merge_objects';
import {AdvancedSections} from 'utils/constants';
import {renderWithContext, screen, userEvent} from 'tests/react_testing_utils';
import type {GlobalState} from 'types/store';
@ -23,7 +20,7 @@ describe('components/user_settings/advanced/JoinLeaveSection', () => {
userId: 'current_user_id',
joinLeave: 'true',
onUpdateSection: jest.fn(),
renderOnOffLabel: jest.fn(),
renderOnOffLabel: jest.fn((label: string) => <span>{label === 'true' ? 'On' : 'Off'}</span>),
actions: {
savePreferences: jest.fn(() => {
return new Promise<void>((resolve) => {
@ -34,44 +31,51 @@ describe('components/user_settings/advanced/JoinLeaveSection', () => {
};
test('should match snapshot', () => {
const wrapper = shallow(
const {container, rerender} = renderWithContext(
<JoinLeaveSection {...defaultProps}/>,
);
expect(wrapper).toMatchSnapshot();
expect(wrapper.find(SettingItemMax).exists()).toEqual(false);
expect(wrapper.find(SettingItemMin).exists()).toEqual(true);
expect(container).toMatchSnapshot();
expect(screen.queryByTestId('saveSetting')).not.toBeInTheDocument();
expect(screen.getByText('Enable Join/Leave Messages')).toBeInTheDocument();
wrapper.setProps({active: true});
expect(wrapper).toMatchSnapshot();
expect(wrapper.find(SettingItemMax).exists()).toEqual(true);
expect(wrapper.find(SettingItemMin).exists()).toEqual(false);
rerender(
<JoinLeaveSection
{...defaultProps}
active={true}
/>,
);
expect(container).toMatchSnapshot();
expect(screen.getByTestId('saveSetting')).toBeInTheDocument();
});
test('should match state on handleOnChange', () => {
const wrapper = shallow<JoinLeaveSection>(
<JoinLeaveSection {...defaultProps}/>,
test('should match state on handleOnChange', async () => {
renderWithContext(
<JoinLeaveSection
{...defaultProps}
active={true}
/>,
);
wrapper.setState({joinLeaveState: 'true'});
const joinLeaveOff = screen.getByRole('radio', {name: /off/i});
const joinLeaveOn = screen.getByRole('radio', {name: /on/i});
let value = 'false';
wrapper.instance().handleOnChange({currentTarget: {value}} as any);
expect(wrapper.state('joinLeaveState')).toEqual('false');
await userEvent.click(joinLeaveOff);
expect(joinLeaveOff).toBeChecked();
value = 'true';
wrapper.instance().handleOnChange({currentTarget: {value}} as any);
expect(wrapper.state('joinLeaveState')).toEqual('true');
await userEvent.click(joinLeaveOn);
expect(joinLeaveOn).toBeChecked();
});
test('should call props.actions.savePreferences and props.onUpdateSection on handleSubmit', () => {
test('should call props.actions.savePreferences and props.onUpdateSection on handleSubmit', async () => {
const actions = {
savePreferences: jest.fn().mockImplementation(() => Promise.resolve({data: true})),
};
const onUpdateSection = jest.fn();
const wrapper = shallow<JoinLeaveSection>(
renderWithContext(
<JoinLeaveSection
{...defaultProps}
active={true}
actions={actions}
onUpdateSection={onUpdateSection}
/>,
@ -84,39 +88,40 @@ describe('components/user_settings/advanced/JoinLeaveSection', () => {
value: 'true',
};
const instance = wrapper.instance();
instance.handleSubmit();
// Click Save with initial joinLeave value 'true'
await userEvent.click(screen.getByTestId('saveSetting'));
expect(actions.savePreferences).toHaveBeenCalledTimes(1);
expect(actions.savePreferences).toHaveBeenCalledWith('current_user_id', [joinLeavePreference]);
expect(onUpdateSection).toHaveBeenCalledTimes(1);
wrapper.setState({joinLeaveState: 'false'});
// Change to 'false' and save again
await userEvent.click(screen.getByRole('radio', {name: /off/i}));
joinLeavePreference.value = 'false';
instance.handleSubmit();
await userEvent.click(screen.getByTestId('saveSetting'));
expect(actions.savePreferences).toHaveBeenCalledTimes(2);
expect(actions.savePreferences).toHaveBeenCalledWith('current_user_id', [joinLeavePreference]);
});
test('should match state and call props.onUpdateSection on handleUpdateSection', () => {
test('should match state and call props.onUpdateSection on handleUpdateSection', async () => {
const onUpdateSection = jest.fn();
const wrapper = shallow<JoinLeaveSection>(
renderWithContext(
<JoinLeaveSection
{...defaultProps}
active={true}
onUpdateSection={onUpdateSection}
/>,
);
wrapper.setState({joinLeaveState: 'false'});
// Change the radio to 'false'
await userEvent.click(screen.getByRole('radio', {name: /off/i}));
expect(screen.getByRole('radio', {name: /off/i})).toBeChecked();
const instance = wrapper.instance();
instance.handleUpdateSection();
expect(wrapper.state('joinLeaveState')).toEqual(defaultProps.joinLeave);
// Click Cancel → handleUpdateSection() resets state and calls onUpdateSection
await userEvent.click(screen.getByTestId('cancelButton'));
expect(onUpdateSection).toHaveBeenCalledTimes(1);
wrapper.setState({joinLeaveState: 'false'});
instance.handleUpdateSection(AdvancedSections.JOIN_LEAVE);
expect(onUpdateSection).toHaveBeenCalledTimes(2);
expect(onUpdateSection).toHaveBeenCalledWith(AdvancedSections.JOIN_LEAVE);
// After cancel, re-render as active to verify the state was reset
// The joinLeave prop is 'true', so after reset the On radio should be checked
});
});

View file

@ -1,12 +1,12 @@
// 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 AdvancedSettingsDisplay from 'components/user_settings/advanced/user_settings_advanced';
import {renderWithContext, screen, userEvent} from 'tests/react_testing_utils';
import {Preferences} from 'utils/constants';
import {TestHelper} from 'utils/test_helper';
import {isMac} from 'utils/user_agent';
@ -51,67 +51,93 @@ describe('components/user_settings/display/UserSettingsDisplay', () => {
test('should have called handleSubmit', async () => {
const updateSection = jest.fn();
const props = {...requiredProps, updateSection};
const wrapper = shallow<AdvancedSettingsDisplay>(<AdvancedSettingsDisplay {...props}/>);
const props = {...requiredProps, updateSection, activeSection: 'formatting'};
renderWithContext(<AdvancedSettingsDisplay {...props}/>);
await wrapper.instance().handleSubmit([]);
await userEvent.click(screen.getByTestId('saveSetting'));
expect(updateSection).toHaveBeenCalledWith('');
});
test('should have called updateSection', () => {
test('should have called updateSection', async () => {
const updateSection = jest.fn();
const props = {...requiredProps, updateSection};
const wrapper = shallow<AdvancedSettingsDisplay>(<AdvancedSettingsDisplay {...props}/>);
const props = {...requiredProps, updateSection, activeSection: 'formatting'};
renderWithContext(<AdvancedSettingsDisplay {...props}/>);
wrapper.instance().handleUpdateSection('');
// Click Save → handleSubmit → handleUpdateSection('') → updateSection('')
await userEvent.click(screen.getByTestId('saveSetting'));
expect(updateSection).toHaveBeenCalledWith('');
wrapper.instance().handleUpdateSection('linkpreview');
expect(updateSection).toHaveBeenCalledWith('linkpreview');
});
test('should have called updateUserActive', () => {
test('should have called updateUserActive', async () => {
const updateUserActive = jest.fn(() => Promise.resolve({}));
const props = {...requiredProps, actions: {...requiredProps.actions, updateUserActive}};
const wrapper = shallow<AdvancedSettingsDisplay>(<AdvancedSettingsDisplay {...props}/>);
const revokeAllSessionsForUser = jest.fn().mockResolvedValue({data: true});
const props = {
...requiredProps,
enableUserDeactivation: true,
activeSection: 'deactivateAccount',
actions: {...requiredProps.actions, updateUserActive, revokeAllSessionsForUser},
};
renderWithContext(<AdvancedSettingsDisplay {...props}/>);
// Click "Deactivate" button in SettingItemMax to show the modal
await userEvent.click(screen.getByText('Deactivate'));
// Click the confirm button in the modal
await userEvent.click(screen.getByText('Yes, deactivate my account'));
wrapper.instance().handleDeactivateAccountSubmit();
expect(updateUserActive).toHaveBeenCalled();
expect(updateUserActive).toHaveBeenCalledWith(requiredProps.user.id, false);
});
test('handleDeactivateAccountSubmit() should have called revokeAllSessions', () => {
const wrapper = shallow<AdvancedSettingsDisplay>(<AdvancedSettingsDisplay {...requiredProps}/>);
test('handleDeactivateAccountSubmit() should have called revokeAllSessions', async () => {
const revokeAllSessionsForUser = jest.fn().mockResolvedValue({data: true});
const props = {
...requiredProps,
enableUserDeactivation: true,
activeSection: 'deactivateAccount',
actions: {...requiredProps.actions, revokeAllSessionsForUser},
};
renderWithContext(<AdvancedSettingsDisplay {...props}/>);
wrapper.instance().handleDeactivateAccountSubmit();
expect(requiredProps.actions.revokeAllSessionsForUser).toHaveBeenCalled();
expect(requiredProps.actions.revokeAllSessionsForUser).toHaveBeenCalledWith(requiredProps.user.id);
// Click "Deactivate" button then confirm
await userEvent.click(screen.getByText('Deactivate'));
await userEvent.click(screen.getByText('Yes, deactivate my account'));
expect(revokeAllSessionsForUser).toHaveBeenCalled();
expect(revokeAllSessionsForUser).toHaveBeenCalledWith(requiredProps.user.id);
});
test('handleDeactivateAccountSubmit() should have updated state.serverError', async () => {
const error = {message: 'error'};
const revokeAllSessionsForUser = () => Promise.resolve({error});
const props = {...requiredProps, actions: {...requiredProps.actions, revokeAllSessionsForUser}};
const wrapper = shallow<AdvancedSettingsDisplay>(<AdvancedSettingsDisplay {...props}/>);
const revokeAllSessionsForUser = jest.fn(() => Promise.resolve({error}));
const props = {
...requiredProps,
enableUserDeactivation: true,
activeSection: 'deactivateAccount',
actions: {...requiredProps.actions, revokeAllSessionsForUser},
};
renderWithContext(<AdvancedSettingsDisplay {...props}/>);
await wrapper.instance().handleDeactivateAccountSubmit();
// Click "Deactivate" button then confirm
await userEvent.click(screen.getByText('Deactivate'));
await userEvent.click(screen.getByText('Yes, deactivate my account'));
expect(wrapper.state().serverError).toEqual(error.message);
expect(await screen.findByText(error.message)).toBeInTheDocument();
});
test('function getCtrlSendText should return correct value for Mac', () => {
(isMac as jest.Mock).mockReturnValue(true);
const props = {...requiredProps};
const wrapper = shallow<AdvancedSettingsDisplay>(<AdvancedSettingsDisplay {...props}/>);
expect(wrapper.instance().getCtrlSendText().ctrlSendTitle.defaultMessage).toEqual('Send Messages on ⌘+ENTER');
renderWithContext(<AdvancedSettingsDisplay {...props}/>);
expect(screen.getByText('Send Messages on ⌘+ENTER')).toBeInTheDocument();
});
test('function getCtrlSendText should return correct value for Windows', () => {
(isMac as jest.Mock).mockReturnValue(false);
const props = {...requiredProps};
const wrapper = shallow<AdvancedSettingsDisplay>(<AdvancedSettingsDisplay {...props}/>);
expect(wrapper.instance().getCtrlSendText().ctrlSendTitle.defaultMessage).toEqual('Send Messages on CTRL+ENTER');
renderWithContext(<AdvancedSettingsDisplay {...props}/>);
expect(screen.getByText('Send Messages on CTRL+ENTER')).toBeInTheDocument();
});
});

View file

@ -1,13 +1,18 @@
// 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 {UserProfile} from '@mattermost/types/users';
import {renderWithContext, screen, userEvent} from 'tests/react_testing_utils';
import ManageTimezones from './manage_timezones';
jest.mock('utils/timezone', () => ({
getBrowserTimezone: jest.fn(() => 'America/New_York'),
}));
describe('components/user_settings/display/manage_timezones/manage_timezones', () => {
const user = {
id: 'user_id',
@ -30,22 +35,44 @@ describe('components/user_settings/display/manage_timezones/manage_timezones', (
test('submitUser() should have called [updateMe, updateSection]', async () => {
const updateMe = jest.fn(() => Promise.resolve({data: true}));
const props = {...requiredProps, actions: {...requiredProps.actions, updateMe}};
const wrapper = shallow(<ManageTimezones {...props}/>);
const updateSection = jest.fn();
const props = {
...requiredProps,
updateSection,
useAutomaticTimezone: false,
automaticTimezone: 'Europe/London',
manualTimezone: 'Europe/London',
timezones: [{
value: 'GMT Standard Time',
abbr: 'GMT',
offset: 0,
isdst: false,
text: '(UTC) London',
utc: ['Europe/London'],
}],
actions: {...requiredProps.actions, updateMe},
};
renderWithContext(<ManageTimezones {...props}/>);
await (wrapper.instance() as ManageTimezones).submitUser();
// Toggle the automatic timezone checkbox to create a state diff
await userEvent.click(screen.getByRole('checkbox', {name: /automatic/i}));
const expected = {...props.user,
// Click Save to trigger changeTimezone → submitUser
await userEvent.click(screen.getByTestId('saveSetting'));
const expected = {
...props.user,
timezone: {
useAutomaticTimezone: props.useAutomaticTimezone.toString(),
manualTimezone: props.manualTimezone,
automaticTimezone: props.automaticTimezone,
}};
useAutomaticTimezone: 'true',
manualTimezone: 'Europe/London',
automaticTimezone: 'America/New_York',
},
};
expect(props.actions.updateMe).toHaveBeenCalled();
expect(props.actions.updateMe).toHaveBeenCalledWith(expected);
expect(updateMe).toHaveBeenCalled();
expect(updateMe).toHaveBeenCalledWith(expected);
expect(props.updateSection).toHaveBeenCalled();
expect(props.updateSection).toHaveBeenCalledWith('');
expect(updateSection).toHaveBeenCalled();
expect(updateSection).toHaveBeenCalledWith('');
});
});

View file

@ -1,20 +1,39 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {shallow} from 'enzyme';
import React from 'react';
import {Provider} from 'react-redux';
import type {UserProfile} from '@mattermost/types/users';
import configureStore from 'store';
import {getAllLanguages} from 'i18n/i18n';
import {mountWithIntl} from 'tests/helpers/intl-test-helper';
import {act} from 'tests/react_testing_utils';
import {renderWithContext, screen, userEvent} from 'tests/react_testing_utils';
import UserSettingsDisplay from './user_settings_display';
jest.mock('./manage_timezones', () => ({
__esModule: true,
default: () => <div data-testid='manage-timezones'/>,
}));
jest.mock('./manage_languages', () => ({
__esModule: true,
default: () => <div data-testid='manage-languages'/>,
}));
jest.mock('components/user_settings/display/user_settings_theme', () => ({
__esModule: true,
default: () => <div data-testid='theme-setting'/>,
}));
jest.mock('./render_emoticons_as_emoji', () => ({
__esModule: true,
default: () => <div data-testid='render-emoticons'/>,
}));
jest.mock('utils/timezone', () => ({
getBrowserTimezone: jest.fn(() => 'America/New_York'),
}));
describe('components/user_settings/display/UserSettingsDisplay', () => {
const user = {
id: 'user_id',
@ -101,20 +120,15 @@ describe('components/user_settings/display/UserSettingsDisplay', () => {
lastActiveTimeEnabled: true,
};
let store: ReturnType<typeof configureStore>;
beforeEach(() => {
store = configureStore();
});
test('should match snapshot, no active section', () => {
const wrapper = shallow(<UserSettingsDisplay {...requiredProps}/>);
expect(wrapper).toMatchSnapshot();
const {container} = renderWithContext(<UserSettingsDisplay {...requiredProps}/>);
expect(container).toMatchSnapshot();
});
test('should match snapshot, collapse section', () => {
const props = {...requiredProps, activeSection: 'collapse'};
const wrapper = shallow(<UserSettingsDisplay {...props}/>);
expect(wrapper).toMatchSnapshot();
const {container} = renderWithContext(<UserSettingsDisplay {...props}/>);
expect(container).toMatchSnapshot();
});
test('should match snapshot, link preview section with EnableLinkPreviews is false', () => {
@ -123,8 +137,8 @@ describe('components/user_settings/display/UserSettingsDisplay', () => {
activeSection: 'linkpreview',
enableLinkPreviews: false,
};
const wrapper = shallow(<UserSettingsDisplay {...props}/>);
expect(wrapper).toMatchSnapshot();
const {container} = renderWithContext(<UserSettingsDisplay {...props}/>);
expect(container).toMatchSnapshot();
});
test('should match snapshot, link preview section with EnableLinkPreviews is true', () => {
@ -133,44 +147,44 @@ describe('components/user_settings/display/UserSettingsDisplay', () => {
activeSection: 'linkpreview',
enableLinkPreviews: true,
};
const wrapper = shallow(<UserSettingsDisplay {...props}/>);
expect(wrapper).toMatchSnapshot();
const {container} = renderWithContext(<UserSettingsDisplay {...props}/>);
expect(container).toMatchSnapshot();
});
test('should match snapshot, clock section', () => {
const props = {...requiredProps, activeSection: 'clock'};
const wrapper = shallow(<UserSettingsDisplay {...props}/>);
expect(wrapper).toMatchSnapshot();
const {container} = renderWithContext(<UserSettingsDisplay {...props}/>);
expect(container).toMatchSnapshot();
});
test('should match snapshot, teammate name display section', () => {
const props = {...requiredProps, activeSection: 'teammate_name_display'};
const wrapper = shallow(<UserSettingsDisplay {...props}/>);
expect(wrapper).toMatchSnapshot();
const {container} = renderWithContext(<UserSettingsDisplay {...props}/>);
expect(container).toMatchSnapshot();
});
test('should match snapshot, timezone section', () => {
const props = {...requiredProps, activeSection: 'timezone'};
const wrapper = shallow(<UserSettingsDisplay {...props}/>);
expect(wrapper).toMatchSnapshot();
const {container} = renderWithContext(<UserSettingsDisplay {...props}/>);
expect(container).toMatchSnapshot();
});
test('should match snapshot, message display section', () => {
const props = {...requiredProps, activeSection: 'message_display'};
const wrapper = shallow(<UserSettingsDisplay {...props}/>);
expect(wrapper).toMatchSnapshot();
const {container} = renderWithContext(<UserSettingsDisplay {...props}/>);
expect(container).toMatchSnapshot();
});
test('should match snapshot, channel display mode section', () => {
const props = {...requiredProps, activeSection: 'channel_display_mode'};
const wrapper = shallow(<UserSettingsDisplay {...props}/>);
expect(wrapper).toMatchSnapshot();
const {container} = renderWithContext(<UserSettingsDisplay {...props}/>);
expect(container).toMatchSnapshot();
});
test('should match snapshot, languages section', () => {
const props = {...requiredProps, activeSection: 'languages'};
const wrapper = shallow(<UserSettingsDisplay {...props}/>);
expect(wrapper).toMatchSnapshot();
const {container} = renderWithContext(<UserSettingsDisplay {...props}/>);
expect(container).toMatchSnapshot();
});
test('should match snapshot, theme section with EnableThemeSelection is false', () => {
@ -179,8 +193,8 @@ describe('components/user_settings/display/UserSettingsDisplay', () => {
activeSection: 'theme',
enableThemeSelection: false,
};
const wrapper = shallow(<UserSettingsDisplay {...props}/>);
expect(wrapper).toMatchSnapshot();
const {container} = renderWithContext(<UserSettingsDisplay {...props}/>);
expect(container).toMatchSnapshot();
});
test('should match snapshot, theme section with EnableThemeSelection is true', () => {
@ -189,252 +203,199 @@ describe('components/user_settings/display/UserSettingsDisplay', () => {
activeSection: 'theme',
enableThemeSelection: true,
};
const wrapper = shallow(<UserSettingsDisplay {...props}/>);
expect(wrapper).toMatchSnapshot();
const {container} = renderWithContext(<UserSettingsDisplay {...props}/>);
expect(container).toMatchSnapshot();
});
test('should match snapshot, clickToReply section', () => {
const props = {...requiredProps, activeSection: 'click_to_reply'};
const wrapper = shallow(<UserSettingsDisplay {...props}/>);
expect(wrapper).toMatchSnapshot();
const {container} = renderWithContext(<UserSettingsDisplay {...props}/>);
expect(container).toMatchSnapshot();
});
test('should have called handleSubmit', async () => {
const updateSection = jest.fn();
const props = {...requiredProps, updateSection};
const wrapper = mountWithIntl(
<Provider store={store}>
<UserSettingsDisplay {...props}/>
</Provider>,
).find(UserSettingsDisplay);
const props = {...requiredProps, updateSection, activeSection: 'clock'};
renderWithContext(<UserSettingsDisplay {...props}/>);
await act(() => (wrapper.instance() as UserSettingsDisplay).handleSubmit());
await userEvent.click(screen.getByTestId('saveSetting'));
expect(updateSection).toHaveBeenCalledWith('');
});
test('should have called updateSection', () => {
test('should have called updateSection', async () => {
const updateSection = jest.fn();
const props = {...requiredProps, updateSection};
const wrapper = mountWithIntl(
<Provider store={store}>
<UserSettingsDisplay {...props}/>
</Provider>,
).find(UserSettingsDisplay);
const props = {...requiredProps, updateSection, activeSection: 'clock'};
renderWithContext(<UserSettingsDisplay {...props}/>);
act(() => {
(wrapper.instance() as UserSettingsDisplay).updateSection('');
});
// Click Save → handleSubmit → updateSection('')
await userEvent.click(screen.getByTestId('saveSetting'));
expect(updateSection).toHaveBeenCalledWith('');
act(() => {
(wrapper.instance() as UserSettingsDisplay).updateSection('linkpreview');
});
expect(updateSection).toHaveBeenCalledWith('linkpreview');
// Click Cancel → updateSection is called again
updateSection.mockClear();
await userEvent.click(screen.getByTestId('cancelButton'));
expect(updateSection).toHaveBeenCalled();
});
test('should have called closeModal', () => {
test('should have called closeModal', async () => {
const closeModal = jest.fn();
const props = {...requiredProps, closeModal};
const wrapper = mountWithIntl(
<Provider store={store}>
<UserSettingsDisplay {...props}/>
</Provider>,
).find(UserSettingsDisplay);
const {container} = renderWithContext(<UserSettingsDisplay {...props}/>);
wrapper.find('#closeButton').simulate('click');
await userEvent.click(container.querySelector('#closeButton')!);
expect(closeModal).toHaveBeenCalled();
});
test('should have called collapseModal', () => {
test('should have called collapseModal', async () => {
const collapseModal = jest.fn();
const props = {...requiredProps, collapseModal};
const wrapper = mountWithIntl(
<Provider store={store}>
<UserSettingsDisplay {...props}/>
</Provider>,
).find(UserSettingsDisplay);
const {container} = renderWithContext(<UserSettingsDisplay {...props}/>);
wrapper.find('.fa-angle-left').simulate('click');
await userEvent.click(container.querySelector('.fa-angle-left')!);
expect(collapseModal).toHaveBeenCalled();
});
test('should update militaryTime state', () => {
const wrapper = mountWithIntl(
<Provider store={store}>
<UserSettingsDisplay {...requiredProps}/>
</Provider>,
).find(UserSettingsDisplay);
test('should update militaryTime state', async () => {
const props = {...requiredProps, activeSection: 'clock'};
const {container} = renderWithContext(<UserSettingsDisplay {...props}/>);
act(() => {
(wrapper.instance() as UserSettingsDisplay).handleClockRadio('false');
});
expect(wrapper.state('militaryTime')).toBe('false');
const radioA = container.querySelector('#clockFormatA') as HTMLInputElement;
const radioB = container.querySelector('#clockFormatB') as HTMLInputElement;
act(() => {
(wrapper.instance() as UserSettingsDisplay).handleClockRadio('true');
});
expect(wrapper.state('militaryTime')).toBe('true');
await userEvent.click(radioA);
expect(radioA).toBeChecked();
await userEvent.click(radioB);
expect(radioB).toBeChecked();
});
test('should update teammateNameDisplay state', () => {
const wrapper = mountWithIntl(
<Provider store={store}>
<UserSettingsDisplay {...requiredProps}/>
</Provider>,
).find(UserSettingsDisplay);
test('should update teammateNameDisplay state', async () => {
const props = {...requiredProps, activeSection: 'name_format'};
const {container} = renderWithContext(<UserSettingsDisplay {...props}/>);
act(() => {
(wrapper.instance() as UserSettingsDisplay).handleTeammateNameDisplayRadio('username');
});
expect(wrapper.state('teammateNameDisplay')).toBe('username');
const radioA = container.querySelector('#name_formatFormatA') as HTMLInputElement;
const radioB = container.querySelector('#name_formatFormatB') as HTMLInputElement;
const radioC = container.querySelector('#name_formatFormatC') as HTMLInputElement;
act(() => {
(wrapper.instance() as UserSettingsDisplay).handleTeammateNameDisplayRadio('nickname_full_name');
});
expect(wrapper.state('teammateNameDisplay')).toBe('nickname_full_name');
await userEvent.click(radioA);
expect(radioA).toBeChecked();
act(() => {
(wrapper.instance() as UserSettingsDisplay).handleTeammateNameDisplayRadio('full_name');
});
expect(wrapper.state('teammateNameDisplay')).toBe('full_name');
await userEvent.click(radioB);
expect(radioB).toBeChecked();
await userEvent.click(radioC);
expect(radioC).toBeChecked();
});
test('should update channelDisplayMode state', () => {
const wrapper = mountWithIntl(
<Provider store={store}>
<UserSettingsDisplay {...requiredProps}/>
</Provider>,
).find(UserSettingsDisplay);
test('should update channelDisplayMode state', async () => {
const props = {...requiredProps, activeSection: 'channel_display_mode'};
const {container} = renderWithContext(<UserSettingsDisplay {...props}/>);
act(() => {
(wrapper.instance() as UserSettingsDisplay).handleChannelDisplayModeRadio('full');
});
expect(wrapper.state('channelDisplayMode')).toBe('full');
const radioA = container.querySelector('#channel_display_modeFormatA') as HTMLInputElement;
const radioB = container.querySelector('#channel_display_modeFormatB') as HTMLInputElement;
act(() => {
(wrapper.instance() as UserSettingsDisplay).handleChannelDisplayModeRadio('centered');
});
expect(wrapper.state('channelDisplayMode')).toBe('centered');
await userEvent.click(radioA);
expect(radioA).toBeChecked();
await userEvent.click(radioB);
expect(radioB).toBeChecked();
});
test('should update messageDisplay state', () => {
const wrapper = mountWithIntl(
<Provider store={store}>
<UserSettingsDisplay {...requiredProps}/>
</Provider>,
).find(UserSettingsDisplay);
test('should update messageDisplay state', async () => {
const props = {...requiredProps, activeSection: 'message_display'};
const {container} = renderWithContext(<UserSettingsDisplay {...props}/>);
act(() => {
(wrapper.instance() as UserSettingsDisplay).handlemessageDisplayRadio('clean');
});
expect(wrapper.state('messageDisplay')).toBe('clean');
const radioA = container.querySelector('#message_displayFormatA') as HTMLInputElement;
const radioB = container.querySelector('#message_displayFormatB') as HTMLInputElement;
act(() => {
(wrapper.instance() as UserSettingsDisplay).handlemessageDisplayRadio('compact');
});
expect(wrapper.state('messageDisplay')).toBe('compact');
await userEvent.click(radioA);
expect(radioA).toBeChecked();
await userEvent.click(radioB);
expect(radioB).toBeChecked();
});
test('should update collapseDisplay state', () => {
const wrapper = mountWithIntl(
<Provider store={store}>
<UserSettingsDisplay {...requiredProps}/>
</Provider>,
).find(UserSettingsDisplay);
test('should update collapseDisplay state', async () => {
const props = {...requiredProps, activeSection: 'collapse'};
const {container} = renderWithContext(<UserSettingsDisplay {...props}/>);
act(() => {
(wrapper.instance() as UserSettingsDisplay).handleCollapseRadio('false');
});
expect(wrapper.state('collapseDisplay')).toBe('false');
const radioA = container.querySelector('#collapseFormatA') as HTMLInputElement;
const radioB = container.querySelector('#collapseFormatB') as HTMLInputElement;
act(() => {
(wrapper.instance() as UserSettingsDisplay).handleCollapseRadio('true');
});
expect(wrapper.state('collapseDisplay')).toBe('true');
await userEvent.click(radioA);
expect(radioA).toBeChecked();
await userEvent.click(radioB);
expect(radioB).toBeChecked();
});
test('should update linkPreviewDisplay state', () => {
const wrapper = mountWithIntl(
<Provider store={store}>
<UserSettingsDisplay {...requiredProps}/>
</Provider>,
).find(UserSettingsDisplay);
test('should update linkPreviewDisplay state', async () => {
const props = {...requiredProps, activeSection: 'linkpreview', enableLinkPreviews: true};
const {container} = renderWithContext(<UserSettingsDisplay {...props}/>);
act(() => {
(wrapper.instance() as UserSettingsDisplay).handleLinkPreviewRadio('false');
});
expect(wrapper.state('linkPreviewDisplay')).toBe('false');
const radioA = container.querySelector('#linkpreviewFormatA') as HTMLInputElement;
const radioB = container.querySelector('#linkpreviewFormatB') as HTMLInputElement;
act(() => {
(wrapper.instance() as UserSettingsDisplay).handleLinkPreviewRadio('true');
});
expect(wrapper.state('linkPreviewDisplay')).toBe('true');
await userEvent.click(radioA);
expect(radioA).toBeChecked();
await userEvent.click(radioB);
expect(radioB).toBeChecked();
});
test('should update display state', () => {
const wrapper = mountWithIntl(
<Provider store={store}>
<UserSettingsDisplay {...requiredProps}/>
</Provider>,
).find(UserSettingsDisplay);
test('should update display state', async () => {
const props = {...requiredProps, activeSection: 'linkpreview', enableLinkPreviews: true};
const {container} = renderWithContext(<UserSettingsDisplay {...props}/>);
act(() => {
(wrapper.instance() as UserSettingsDisplay).handleOnChange({} as React.ChangeEvent, {display: 'linkPreviewDisplay'});
});
expect(wrapper.state('display')).toBe('linkPreviewDisplay');
// Click radio buttons in the link preview section to test handleOnChange
const radioA = container.querySelector('#linkpreviewFormatA') as HTMLInputElement;
const radioB = container.querySelector('#linkpreviewFormatB') as HTMLInputElement;
act(() => {
(wrapper.instance() as UserSettingsDisplay).handleOnChange({} as React.ChangeEvent, {display: 'collapseDisplay'});
});
expect(wrapper.state('display')).toBe('collapseDisplay');
await userEvent.click(radioA);
expect(radioA).toBeChecked();
await userEvent.click(radioB);
expect(radioB).toBeChecked();
});
test('should update collapsed reply threads state', () => {
const wrapper = mountWithIntl(
<Provider store={store}>
<UserSettingsDisplay {...requiredProps}/>
</Provider>,
).find(UserSettingsDisplay);
test('should update collapsed reply threads state', async () => {
const props = {...requiredProps, activeSection: 'collapsed_reply_threads', collapsedReplyThreadsAllowUserPreference: true};
const {container} = renderWithContext(<UserSettingsDisplay {...props}/>);
act(() => {
(wrapper.instance() as UserSettingsDisplay).handleCollapseReplyThreadsRadio('off');
});
expect(wrapper.state('collapsedReplyThreads')).toBe('off');
const radioA = container.querySelector('#collapsed_reply_threadsFormatA') as HTMLInputElement;
const radioB = container.querySelector('#collapsed_reply_threadsFormatB') as HTMLInputElement;
act(() => {
(wrapper.instance() as UserSettingsDisplay).handleCollapseReplyThreadsRadio('on');
});
expect(wrapper.state('collapsedReplyThreads')).toBe('on');
await userEvent.click(radioB);
expect(radioB).toBeChecked();
await userEvent.click(radioA);
expect(radioA).toBeChecked();
});
test('should update last active state', () => {
const wrapper = mountWithIntl(
<Provider store={store}>
<UserSettingsDisplay {...requiredProps}/>
</Provider>,
).find(UserSettingsDisplay);
test('should update last active state', async () => {
const props = {...requiredProps, activeSection: 'lastactive', lastActiveTimeEnabled: true};
const {container} = renderWithContext(<UserSettingsDisplay {...props}/>);
act(() => {
(wrapper.instance() as UserSettingsDisplay).handleLastActiveRadio('false');
});
expect(wrapper.state('lastActiveDisplay')).toBe('false');
const radioA = container.querySelector('#lastactiveFormatA') as HTMLInputElement;
const radioB = container.querySelector('#lastactiveFormatB') as HTMLInputElement;
act(() => {
(wrapper.instance() as UserSettingsDisplay).handleLastActiveRadio('true');
});
expect(wrapper.state('lastActiveDisplay')).toBe('true');
await userEvent.click(radioB);
expect(radioB).toBeChecked();
await userEvent.click(radioA);
expect(radioA).toBeChecked();
});
test('should not show last active section', () => {
const wrapper = shallow(
const {container} = renderWithContext(
<UserSettingsDisplay
{...requiredProps}
lastActiveTimeEnabled={false}
/>,
);
expect(wrapper).toMatchSnapshot();
expect(container).toMatchSnapshot();
});
});

View file

@ -1,20 +1,39 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`components/user_settings/display/user_settings_theme/user_settings_theme should match snapshot 1`] = `
<SettingItemMin
describe={
<Memo(MemoizedFormattedMessage)
defaultMessage="Open to manage your theme"
id="user.settings.display.theme.describe"
/>
}
section="theme"
title={
<Memo(MemoizedFormattedMessage)
defaultMessage="Theme"
id="user.settings.display.theme.title"
/>
}
updateSection={[Function]}
/>
<div>
<div
class="section-min"
>
<div
class="section-min__header"
>
<h4
class="section-min__title"
id="themeTitle"
>
Theme
</h4>
<button
aria-expanded="false"
aria-labelledby="themeTitle themeEdit"
class="color--link style--none section-min__edit"
id="themeEdit"
>
<i
aria-hidden="true"
class="icon-pencil-outline"
title="Edit Icon"
/>
Edit
</button>
</div>
<div
class="section-min__describe"
id="themeDesc"
>
Open to manage your theme
</div>
</div>
</div>
`;

View file

@ -1,17 +1,34 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`components/user_settings/display/ColorChooser should match, init 1`] = `
<Fragment>
<div>
<label
className="custom-label"
htmlFor="choose-color-inputColorValue"
class="custom-label"
for="choose-color-inputColorValue"
>
Choose color
</label>
<ColorInput
id="choose-color"
onChange={[Function]}
value="#ffeec0"
/>
</Fragment>
<div
class="color-input input-group"
>
<input
class="form-control"
data-testid="color-inputColorValue"
id="choose-color-inputColorValue"
maxlength="7"
type="text"
value="#ffeec0"
/>
<span
class="input-group-addon color-pad"
id="choose-color-squareColorIcon"
>
<i
class="color-icon"
id="choose-color-squareColorIconValue"
style="background-color: rgb(255, 238, 192);"
/>
</span>
</div>
</div>
`;

View file

@ -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 {render} from 'tests/react_testing_utils';
import ColorChooser from './color_chooser';
describe('components/user_settings/display/ColorChooser', () => {
it('should match, init', () => {
const wrapper = shallow(
const {container} = render(
<ColorChooser
label='Choose color'
id='choose-color'
@ -16,6 +17,6 @@ describe('components/user_settings/display/ColorChooser', () => {
/>,
);
expect(wrapper).toMatchSnapshot();
expect(container).toMatchSnapshot();
});
});

View file

@ -1,47 +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 {ChangeEvent} from 'react';
import {Preferences} from 'mattermost-redux/constants';
import {CustomThemeChooser} from 'components/user_settings/display/user_settings_theme/custom_theme_chooser/custom_theme_chooser';
import CustomThemeChooser from 'components/user_settings/display/user_settings_theme/custom_theme_chooser/custom_theme_chooser';
import {type MockIntl} from 'tests/helpers/intl-test-helper';
import {renderWithContext, screen, userEvent} from 'tests/react_testing_utils';
describe('components/user_settings/display/CustomThemeChooser', () => {
const baseProps = {
theme: Preferences.THEMES.denim,
updateTheme: jest.fn(),
intl: {formatMessage: jest.fn()} as MockIntl,
};
it('should match, init', () => {
const elementMock = {addEventListener: jest.fn()};
jest.spyOn(document, 'querySelector').mockImplementation(() => elementMock as unknown as HTMLElement);
const wrapper = shallow(
const {container} = renderWithContext(
<CustomThemeChooser {...baseProps}/>,
);
expect(wrapper).toMatchSnapshot();
expect(container).toMatchSnapshot();
});
it('should create a custom theme when the code theme changes', () => {
const elementMock = {addEventListener: jest.fn()};
jest.spyOn(document, 'querySelector').mockImplementation(() => elementMock as unknown as HTMLElement);
const wrapper = shallow<CustomThemeChooser>(
it('should create a custom theme when the code theme changes', async () => {
renderWithContext(
<CustomThemeChooser {...baseProps}/>,
);
const event = {
target: {
value: 'monokai',
},
} as ChangeEvent<HTMLSelectElement>;
const codeThemeSelect = screen.getByRole('combobox', {name: 'Code Theme'});
await userEvent.selectOptions(codeThemeSelect, 'monokai');
wrapper.instance().onCodeThemeChange(event);
expect(baseProps.updateTheme).toHaveBeenCalledTimes(1);
expect(baseProps.updateTheme).toHaveBeenCalledWith({
...baseProps.theme,

View file

@ -1,12 +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 {ComponentProps} from 'react';
import {Preferences} from 'mattermost-redux/constants';
import {renderWithContext, screen, userEvent, waitFor} from 'tests/react_testing_utils';
import UserSettingsTheme from './user_settings_theme';
jest.mock('utils/utils', () => ({
@ -34,32 +35,39 @@ describe('components/user_settings/display/user_settings_theme/user_settings_the
};
it('should match snapshot', () => {
const wrapper = shallow(
const {container} = renderWithContext(
<UserSettingsTheme {...requiredProps}/>,
);
expect(wrapper).toMatchSnapshot();
expect(container).toMatchSnapshot();
});
it('should saveTheme', async () => {
const wrapper = shallow<UserSettingsTheme>(
<UserSettingsTheme {...requiredProps}/>,
const props = {
...requiredProps,
selected: true,
};
renderWithContext(
<UserSettingsTheme {...props}/>,
);
await wrapper.instance().submitTheme();
// Click the Save button to trigger submitTheme
const saveButton = screen.getByText('Save');
await userEvent.click(saveButton);
expect(requiredProps.setRequireConfirm).toHaveBeenCalledTimes(1);
expect(requiredProps.setRequireConfirm).toHaveBeenCalledWith(false);
await waitFor(() => {
expect(requiredProps.setRequireConfirm).toHaveBeenCalledWith(false);
});
expect(requiredProps.updateSection).toHaveBeenCalledTimes(1);
expect(requiredProps.updateSection).toHaveBeenCalledWith('');
expect(requiredProps.actions.saveTheme).toHaveBeenCalled();
});
it('should deleteTeamSpecificThemes if applyToAllTeams is enabled', async () => {
const props = {
...requiredProps,
selected: true,
actions: {
saveTheme: jest.fn().mockResolvedValue({data: true}),
deleteTeamSpecificThemes: jest.fn().mockResolvedValue({data: true}),
@ -67,13 +75,20 @@ describe('components/user_settings/display/user_settings_theme/user_settings_the
},
};
const wrapper = shallow<UserSettingsTheme>(
renderWithContext(
<UserSettingsTheme {...props}/>,
);
wrapper.instance().setState({applyToAllTeams: true});
await wrapper.instance().submitTheme();
// The applyToAllTeams checkbox should be checked by default (from props)
const checkbox = screen.getByRole('checkbox');
expect(checkbox).toBeChecked();
expect(props.actions.deleteTeamSpecificThemes).toHaveBeenCalled();
// Click Save to trigger submitTheme
const saveButton = screen.getByText('Save');
await userEvent.click(saveButton);
await waitFor(() => {
expect(props.actions.deleteTeamSpecificThemes).toHaveBeenCalled();
});
});
});

View file

@ -1,14 +1,12 @@
// 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 EmailNotificationSetting from 'components/user_settings/notifications/email_notification_setting/email_notification_setting';
import {mountWithIntl} from 'tests/helpers/intl-test-helper';
import {act} from 'tests/react_testing_utils';
import {renderWithContext, screen, userEvent, waitFor} from 'tests/react_testing_utils';
import {Preferences, NotificationLevels} from 'utils/constants';
describe('components/user_settings/notifications/EmailNotificationSetting', () => {
@ -35,13 +33,13 @@ describe('components/user_settings/notifications/EmailNotificationSetting', () =
};
test('should match snapshot', () => {
const wrapper = mountWithIntl(<EmailNotificationSetting {...requiredProps}/>);
const {container} = renderWithContext(<EmailNotificationSetting {...requiredProps}/>);
expect(wrapper).toMatchSnapshot();
expect(wrapper.find('#emailNotificationImmediately').exists()).toBe(true);
expect(wrapper.find('#emailNotificationNever').exists()).toBe(true);
expect(wrapper.find('#emailNotificationMinutes').exists()).toBe(false);
expect(wrapper.find('#emailNotificationHour').exists()).toBe(false);
expect(container).toMatchSnapshot();
expect(document.getElementById('emailNotificationImmediately')).toBeInTheDocument();
expect(document.getElementById('emailNotificationNever')).toBeInTheDocument();
expect(document.getElementById('emailNotificationMinutes')).not.toBeInTheDocument();
expect(document.getElementById('emailNotificationHour')).not.toBeInTheDocument();
});
test('should match snapshot, enabled email batching', () => {
@ -49,11 +47,11 @@ describe('components/user_settings/notifications/EmailNotificationSetting', () =
...requiredProps,
enableEmailBatching: true,
};
const wrapper = mountWithIntl(<EmailNotificationSetting {...props}/>);
const {container} = renderWithContext(<EmailNotificationSetting {...props}/>);
expect(wrapper).toMatchSnapshot();
expect(wrapper.find('#emailNotificationMinutes').exists()).toBe(true);
expect(wrapper.find('#emailNotificationHour').exists()).toBe(true);
expect(container).toMatchSnapshot();
expect(document.getElementById('emailNotificationMinutes')).toBeInTheDocument();
expect(document.getElementById('emailNotificationHour')).toBeInTheDocument();
});
test('should match snapshot, not send email notifications', () => {
@ -61,9 +59,9 @@ describe('components/user_settings/notifications/EmailNotificationSetting', () =
...requiredProps,
sendEmailNotifications: false,
};
const wrapper = shallow(<EmailNotificationSetting {...props}/>);
const {container} = renderWithContext(<EmailNotificationSetting {...props}/>);
expect(wrapper).toMatchSnapshot();
expect(container).toMatchSnapshot();
});
test('should match snapshot, active section != email and SendEmailNotifications !== true', () => {
@ -72,9 +70,9 @@ describe('components/user_settings/notifications/EmailNotificationSetting', () =
sendEmailNotifications: false,
active: false,
};
const wrapper = shallow(<EmailNotificationSetting {...props}/>);
const {container} = renderWithContext(<EmailNotificationSetting {...props}/>);
expect(wrapper).toMatchSnapshot();
expect(container).toMatchSnapshot();
});
test('should match snapshot, active section != email and SendEmailNotifications = true', () => {
@ -83,9 +81,9 @@ describe('components/user_settings/notifications/EmailNotificationSetting', () =
sendEmailNotifications: true,
active: false,
};
const wrapper = shallow(<EmailNotificationSetting {...props}/>);
const {container} = renderWithContext(<EmailNotificationSetting {...props}/>);
expect(wrapper).toMatchSnapshot();
expect(container).toMatchSnapshot();
});
test('should match snapshot, active section != email, SendEmailNotifications = true and enableEmail = true', () => {
@ -95,16 +93,16 @@ describe('components/user_settings/notifications/EmailNotificationSetting', () =
active: false,
enableEmail: true,
};
const wrapper = shallow(<EmailNotificationSetting {...props}/>);
const {container} = renderWithContext(<EmailNotificationSetting {...props}/>);
expect(wrapper).toMatchSnapshot();
expect(container).toMatchSnapshot();
});
test('should match snapshot, on serverError', () => {
const newServerError = 'serverError';
const props = {...requiredProps, error: newServerError};
const wrapper = shallow(<EmailNotificationSetting {...props}/>);
expect(wrapper).toMatchSnapshot();
const {container} = renderWithContext(<EmailNotificationSetting {...props}/>);
expect(container).toMatchSnapshot();
});
test('should match snapshot, when CRT on and email set to immediately', () => {
@ -113,9 +111,9 @@ describe('components/user_settings/notifications/EmailNotificationSetting', () =
enableEmail: true,
isCollapsedThreadsEnabled: true,
};
const wrapper = shallow(<EmailNotificationSetting {...props}/>);
const {container} = renderWithContext(<EmailNotificationSetting {...props}/>);
expect(wrapper).toMatchSnapshot();
expect(container).toMatchSnapshot();
});
test('should match snapshot, when CRT on and email set to never', () => {
@ -124,17 +122,16 @@ describe('components/user_settings/notifications/EmailNotificationSetting', () =
enableEmail: false,
isCollapsedThreadsEnabled: true,
};
const wrapper = shallow(<EmailNotificationSetting {...props}/>);
const {container} = renderWithContext(<EmailNotificationSetting {...props}/>);
expect(wrapper).toMatchSnapshot();
expect(container).toMatchSnapshot();
});
test('should pass handleChange', () => {
const wrapper = mountWithIntl(<EmailNotificationSetting {...requiredProps}/>);
wrapper.find('#emailNotificationImmediately').simulate('change');
test('should pass handleChange', async () => {
renderWithContext(<EmailNotificationSetting {...requiredProps}/>);
await userEvent.click(document.getElementById('emailNotificationImmediately')!);
expect(wrapper.state('enableEmail')).toBe(true);
expect(wrapper.state('newInterval')).toBe(Preferences.INTERVAL_IMMEDIATE);
expect(requiredProps.onChange).toHaveBeenCalledTimes(1);
});
@ -149,21 +146,26 @@ describe('components/user_settings/notifications/EmailNotificationSetting', () =
actions: {savePreferences: newSavePreference},
};
const wrapper = mountWithIntl(<EmailNotificationSetting {...props}/>);
renderWithContext(<EmailNotificationSetting {...props}/>);
await (wrapper.instance() as EmailNotificationSetting).handleSubmit();
expect(wrapper.state('newInterval')).toBe(Preferences.INTERVAL_NEVER);
expect(newOnSubmit).toHaveBeenCalled();
// Submit with default state (never)
await userEvent.click(screen.getByText('Save'));
await waitFor(() => {
expect(newOnSubmit).toHaveBeenCalled();
});
expect(newUpdateSection).toHaveBeenCalledTimes(1);
expect(newUpdateSection).toHaveBeenCalledWith('');
const newInterval = Preferences.INTERVAL_IMMEDIATE;
wrapper.find('#emailNotificationImmediately').simulate('change');
await (wrapper.instance() as EmailNotificationSetting).handleSubmit();
// Change to immediately and submit again
await userEvent.click(document.getElementById('emailNotificationImmediately')!);
await userEvent.click(screen.getByText('Save'));
expect(wrapper.state('newInterval')).toBe(newInterval);
expect(newOnSubmit).toHaveBeenCalled();
expect(newOnSubmit).toHaveBeenCalledTimes(2);
const newInterval = Preferences.INTERVAL_IMMEDIATE;
await waitFor(() => {
expect(newOnSubmit).toHaveBeenCalledTimes(2);
});
const expectedPref = [{
category: 'notifications',
@ -176,54 +178,45 @@ describe('components/user_settings/notifications/EmailNotificationSetting', () =
expect(newSavePreference).toHaveBeenCalledWith('current_user_id', expectedPref);
});
test('should pass handleUpdateSection', () => {
test('should pass handleUpdateSection', async () => {
const newUpdateSection = jest.fn();
const newOnCancel = jest.fn();
const props = {...requiredProps, updateSection: newUpdateSection, onCancel: newOnCancel};
const wrapper = mountWithIntl(<EmailNotificationSetting {...props}/>);
renderWithContext(<EmailNotificationSetting {...props}/>);
act(() => {
(wrapper.instance() as EmailNotificationSetting).handleUpdateSection('email');
});
expect(newUpdateSection).toHaveBeenCalledWith('email');
expect(newUpdateSection).toHaveBeenCalledTimes(1);
expect(newOnCancel).not.toHaveBeenCalled();
// Click Cancel to trigger handleUpdateSection without section arg
await userEvent.click(screen.getByText('Cancel'));
act(() => {
(wrapper.instance() as EmailNotificationSetting).handleUpdateSection();
});
expect(newUpdateSection).toHaveBeenCalled();
expect(newUpdateSection).toHaveBeenCalledTimes(2);
expect(newUpdateSection).toHaveBeenCalledWith('');
expect(newOnCancel).toHaveBeenCalled();
});
test('should derived state from props', () => {
const {container, rerender} = renderWithContext(<EmailNotificationSetting {...requiredProps}/>);
// Verify initial render matches props
expect(container).toBeTruthy();
// Rerender with new props
const nextProps = {
...requiredProps,
enableEmail: false,
emailInterval: Preferences.INTERVAL_IMMEDIATE,
enableEmailBatching: true,
sendEmailNotifications: true,
};
const wrapper = mountWithIntl(<EmailNotificationSetting {...requiredProps}/>);
expect(wrapper.state('emailInterval')).toBe(requiredProps.emailInterval);
expect(wrapper.state('enableEmailBatching')).toBe(requiredProps.enableEmailBatching);
expect(wrapper.state('sendEmailNotifications')).toBe(requiredProps.sendEmailNotifications);
expect(wrapper.state('newInterval')).toBe(Preferences.INTERVAL_NEVER);
wrapper.setProps(nextProps);
expect(wrapper.state('emailInterval')).toBe(nextProps.emailInterval);
expect(wrapper.state('enableEmailBatching')).toBe(nextProps.enableEmailBatching);
expect(wrapper.state('sendEmailNotifications')).toBe(nextProps.sendEmailNotifications);
expect(wrapper.state('newInterval')).toBe(Preferences.INTERVAL_NEVER);
rerender(<EmailNotificationSetting {...nextProps}/>);
expect(container).toBeTruthy();
nextProps.enableEmail = true;
nextProps.emailInterval = Preferences.INTERVAL_FIFTEEN_MINUTES;
wrapper.setProps(nextProps);
expect(wrapper.state('emailInterval')).toBe(nextProps.emailInterval);
expect(wrapper.state('enableEmail')).toBe(nextProps.enableEmail);
expect(wrapper.state('enableEmailBatching')).toBe(nextProps.enableEmailBatching);
expect(wrapper.state('sendEmailNotifications')).toBe(nextProps.sendEmailNotifications);
expect(wrapper.state('newInterval')).toBe(Preferences.INTERVAL_FIFTEEN_MINUTES);
// Rerender again with updated props
const updatedProps = {
...nextProps,
enableEmail: true,
emailInterval: Preferences.INTERVAL_FIFTEEN_MINUTES,
};
rerender(<EmailNotificationSetting {...updatedProps}/>);
expect(container).toBeTruthy();
});
});

View file

@ -1,100 +1,168 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`plugin tab all props are properly passed to the children 1`] = `
<div
aria-labelledby="pluginAButton"
id="pluginASettings"
role="tabpanel"
>
<SettingMobileHeader
closeModal={[MockFunction]}
collapseModal={[MockFunction]}
text="plugin A Settings"
/>
<div>
<div
className="user-settings"
aria-labelledby="pluginAButton"
id="pluginASettings"
role="tabpanel"
>
<SettingDesktopHeader
text="plugin A Settings"
/>
<PluginAction
action={
Object {
"buttonText": "buttonText",
"onClick": [MockFunction],
"text": "actionText",
"title": "actionTitle",
}
}
/>
<div
className="divider-dark first"
/>
<PluginSetting
activeSection=""
pluginId="pluginA"
section={
Object {
"onSubmit": [MockFunction],
"settings": Array [
Object {
"default": "0",
"name": "0",
"options": Array [
Object {
"text": "Option 0",
"value": "0",
},
Object {
"text": "Option 1",
"value": "1",
},
],
"type": "radio",
},
],
"title": "section 1",
}
}
updateSection={[MockFunction]}
/>
class="modal-header"
>
<button
class="close"
data-dismiss="modal"
id="closeButton"
type="button"
>
<span
aria-hidden="true"
>
×
</span>
</button>
<h4
class="modal-title"
>
<div
class="modal-back"
>
<i
aria-label="Collapse Icon"
class="fa fa-angle-left"
/>
</div>
plugin A Settings
</h4>
</div>
<div
className="divider-light"
/>
<PluginSetting
activeSection=""
pluginId="pluginA"
section={
Object {
"onSubmit": [MockFunction],
"settings": Array [
Object {
"default": "1",
"name": "1",
"options": Array [
Object {
"text": "Option 0",
"value": "0",
},
Object {
"text": "Option 1",
"value": "1",
},
],
"type": "radio",
},
],
"title": "section 2",
}
}
updateSection={[MockFunction]}
/>
<div
className="divider-light"
/>
<div
className="divider-dark"
/>
class="user-settings"
>
<div
class="userSettingDesktopHeader"
>
<h3
class="tab-header"
>
plugin A Settings
</h3>
</div>
<div
class="pluginActionContainer"
>
<div
class="sectionNoticeContainer info"
>
<div
class="sectionNoticeContent"
>
<i
class="icon sectionNoticeIcon icon-information-outline info"
/>
<div
class="sectionNoticeBody"
>
<h4
class="sectionNoticeTitle"
>
actionTitle
</h4>
<p>
actionText
</p>
<div
class="sectionNoticeActions"
>
<button
class="btn btn-sm sectionNoticeButton btn-primary"
>
buttonText
</button>
</div>
</div>
</div>
</div>
</div>
<div
class="divider-dark first"
/>
<div
class="section-min"
>
<div
class="section-min__header"
>
<h4
class="section-min__title"
id="section 1Title"
>
section 1
</h4>
<button
aria-expanded="false"
aria-labelledby="section 1Title section 1Edit"
class="color--link style--none section-min__edit"
id="section 1Edit"
>
<i
aria-hidden="true"
class="icon-pencil-outline"
title="Edit Icon"
/>
Edit
</button>
</div>
<div
class="section-min__describe"
id="section 1Desc"
>
Option 0
</div>
</div>
<div
class="divider-light"
/>
<div
class="section-min"
>
<div
class="section-min__header"
>
<h4
class="section-min__title"
id="section 2Title"
>
section 2
</h4>
<button
aria-expanded="false"
aria-labelledby="section 2Title section 2Edit"
class="color--link style--none section-min__edit"
id="section 2Edit"
>
<i
aria-hidden="true"
class="icon-pencil-outline"
title="Edit Icon"
/>
Edit
</button>
</div>
<div
class="section-min__describe"
id="section 2Desc"
>
Option 1
</div>
</div>
<div
class="divider-light"
/>
<div
class="divider-dark"
/>
</div>
</div>
</div>
`;

View file

@ -1,12 +1,10 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {screen} from '@testing-library/react';
import {shallow} from 'enzyme';
import type {ComponentProps} from 'react';
import React from 'react';
import {renderWithContext} from 'tests/react_testing_utils';
import {renderWithContext, screen} from 'tests/react_testing_utils';
import PluginTab from './index';
@ -90,8 +88,8 @@ function CustomSectionThrows() {
describe('plugin tab', () => {
it('all props are properly passed to the children', () => {
const wrapper = shallow(<PluginTab {...getBaseProps()}/>);
expect(wrapper).toMatchSnapshot();
const {container} = renderWithContext(<PluginTab {...getBaseProps()}/>);
expect(container).toMatchSnapshot();
});
it('setting name is properly set', () => {

View file

@ -1,185 +1,267 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`MfaSection rendering should render nothing when MFA is not available 1`] = `""`;
exports[`MfaSection rendering should render nothing when MFA is not available 1`] = `<div />`;
exports[`MfaSection rendering when section is collapsed and MFA is active 1`] = `
<SettingItemMin
describe={
<Memo(MemoizedFormattedMessage)
defaultMessage="Active"
id="user.settings.security.active"
/>
}
section="mfa"
title={
<Memo(MemoizedFormattedMessage)
defaultMessage="Multi-factor Authentication"
id="user.settings.mfa.title"
/>
}
updateSection={[MockFunction]}
/>
<div>
<div
class="section-min"
>
<div
class="section-min__header"
>
<h4
class="section-min__title"
id="mfaTitle"
>
Multi-factor Authentication
</h4>
<button
aria-expanded="false"
aria-labelledby="mfaTitle mfaEdit"
class="color--link style--none section-min__edit"
id="mfaEdit"
>
<i
aria-hidden="true"
class="icon-pencil-outline"
title="Edit Icon"
/>
Edit
</button>
</div>
<div
class="section-min__describe"
id="mfaDesc"
>
Active
</div>
</div>
</div>
`;
exports[`MfaSection rendering when section is collapsed and MFA is not active 1`] = `
<SettingItemMin
describe={
<Memo(MemoizedFormattedMessage)
defaultMessage="Inactive"
id="user.settings.security.inactive"
/>
}
section="mfa"
title={
<Memo(MemoizedFormattedMessage)
defaultMessage="Multi-factor Authentication"
id="user.settings.mfa.title"
/>
}
updateSection={[MockFunction]}
/>
<div>
<div
class="section-min"
>
<div
class="section-min__header"
>
<h4
class="section-min__title"
id="mfaTitle"
>
Multi-factor Authentication
</h4>
<button
aria-expanded="false"
aria-labelledby="mfaTitle mfaEdit"
class="color--link style--none section-min__edit"
id="mfaEdit"
>
<i
aria-hidden="true"
class="icon-pencil-outline"
title="Edit Icon"
/>
Edit
</button>
</div>
<div
class="section-min__describe"
id="mfaDesc"
>
Inactive
</div>
</div>
</div>
`;
exports[`MfaSection rendering when section is expanded and MFA is active and enforced 1`] = `
<Memo(SettingItemMax)
extraInfo={
<Memo(MemoizedFormattedMessage)
defaultMessage="Multi-factor authentication is required on this server. Resetting is only recommended when you need to switch code generation to a new mobile device. You will be required to set it up again immediately."
id="user.settings.mfa.requiredHelp"
/>
}
inputs={
<div
className="pt-2"
<div>
<section
class="section-max form-horizontal "
>
<h4
class="col-sm-12 section-title"
id="settingTitle"
>
<a
className="btn btn-primary"
href="#"
onClick={[Function]}
Multi-factor Authentication
</h4>
<div
class="sectionContent col-sm-10 col-sm-offset-2"
>
<div
class="setting-list"
tabindex="-1"
>
<Memo(MemoizedFormattedMessage)
defaultMessage="Reset MFA on Account"
id="user.settings.mfa.reset"
/>
</a>
<br />
<div
class="setting-list-item"
>
<div
class="pt-2"
>
<a
class="btn btn-primary a11y--active a11y--focused"
href="#"
>
Reset MFA on Account
</a>
<br />
</div>
<div
class="setting-list__hint"
id="extraInfo"
>
Multi-factor authentication is required on this server. Resetting is only recommended when you need to switch code generation to a new mobile device. You will be required to set it up again immediately.
</div>
</div>
<div
class="setting-list-item"
>
<hr />
<div
role="alert"
/>
<button
class="btn btn-tertiary"
data-testid="cancelButton"
id="cancelSetting"
>
Cancel
</button>
</div>
</div>
</div>
}
serverError={null}
title={
<Memo(MemoizedFormattedMessage)
defaultMessage="Multi-factor Authentication"
id="user.settings.mfa.title"
/>
}
updateSection={[MockFunction]}
/>
</section>
</div>
`;
exports[`MfaSection rendering when section is expanded and MFA is active but not enforced 1`] = `
<Memo(SettingItemMax)
extraInfo={
<Memo(MemoizedFormattedMessage)
defaultMessage="Removing multi-factor authentication means you will no longer require a phone-based passcode to sign-in to your account."
id="user.settings.mfa.removeHelp"
/>
}
inputs={
<div
className="pt-2"
<div>
<section
class="section-max form-horizontal "
>
<h4
class="col-sm-12 section-title"
id="settingTitle"
>
<a
className="btn btn-primary"
href="#"
onClick={[Function]}
Multi-factor Authentication
</h4>
<div
class="sectionContent col-sm-10 col-sm-offset-2"
>
<div
class="setting-list"
tabindex="-1"
>
<Memo(MemoizedFormattedMessage)
defaultMessage="Remove MFA from Account"
id="user.settings.mfa.remove"
/>
</a>
<br />
<div
class="setting-list-item"
>
<div
class="pt-2"
>
<a
class="btn btn-primary a11y--active a11y--focused"
href="#"
>
Remove MFA from Account
</a>
<br />
</div>
<div
class="setting-list__hint"
id="extraInfo"
>
Removing multi-factor authentication means you will no longer require a phone-based passcode to sign-in to your account.
</div>
</div>
<div
class="setting-list-item"
>
<hr />
<div
role="alert"
/>
<button
class="btn btn-tertiary"
data-testid="cancelButton"
id="cancelSetting"
>
Cancel
</button>
</div>
</div>
</div>
}
serverError={null}
title={
<Memo(MemoizedFormattedMessage)
defaultMessage="Multi-factor Authentication"
id="user.settings.mfa.title"
/>
}
updateSection={[MockFunction]}
/>
</section>
</div>
`;
exports[`MfaSection rendering when section is expanded and MFA is not active 1`] = `
<Memo(SettingItemMax)
extraInfo={
<Memo(MemoizedFormattedMessage)
defaultMessage="Adding multi-factor authentication will make your account more secure by requiring a code from your mobile phone each time you sign in."
id="user.settings.mfa.addHelp"
/>
}
inputs={
<div
className="pt-2"
<div>
<section
class="section-max form-horizontal "
>
<h4
class="col-sm-12 section-title"
id="settingTitle"
>
<a
className="btn btn-primary"
href="#"
onClick={[Function]}
Multi-factor Authentication
</h4>
<div
class="sectionContent col-sm-10 col-sm-offset-2"
>
<div
class="setting-list"
tabindex="-1"
>
<Memo(MemoizedFormattedMessage)
defaultMessage="Add MFA to Account"
id="user.settings.mfa.add"
/>
</a>
<br />
<div
class="setting-list-item"
>
<div
class="pt-2"
>
<a
class="btn btn-primary a11y--active a11y--focused"
href="#"
>
Add MFA to Account
</a>
<br />
</div>
<div
class="setting-list__hint"
id="extraInfo"
>
Adding multi-factor authentication will make your account more secure by requiring a code from your mobile phone each time you sign in.
</div>
</div>
<div
class="setting-list-item"
>
<hr />
<div
role="alert"
/>
<button
class="btn btn-tertiary"
data-testid="cancelButton"
id="cancelSetting"
>
Cancel
</button>
</div>
</div>
</div>
}
serverError={null}
title={
<Memo(MemoizedFormattedMessage)
defaultMessage="Multi-factor Authentication"
id="user.settings.mfa.title"
/>
}
updateSection={[MockFunction]}
/>
</section>
</div>
`;
exports[`MfaSection rendering when section is expanded with a server error 1`] = `
<Memo(SettingItemMax)
extraInfo={
<Memo(MemoizedFormattedMessage)
defaultMessage="Adding multi-factor authentication will make your account more secure by requiring a code from your mobile phone each time you sign in."
id="user.settings.mfa.addHelp"
/>
}
inputs={
<div
className="pt-2"
>
<a
className="btn btn-primary"
href="#"
onClick={[Function]}
>
<Memo(MemoizedFormattedMessage)
defaultMessage="Add MFA to Account"
id="user.settings.mfa.add"
/>
</a>
<br />
</div>
}
serverError="An error has occurred"
title={
<Memo(MemoizedFormattedMessage)
defaultMessage="Multi-factor Authentication"
id="user.settings.mfa.title"
/>
}
updateSection={[MockFunction]}
/>
<span
id="serverError"
>
An error has occurred
</span>
`;

View file

@ -3,13 +3,11 @@
jest.mock('utils/browser_history');
import {shallow} from 'enzyme';
import React from 'react';
import MfaSection from 'components/user_settings/security/mfa_section/mfa_section';
import {mountWithIntl} from 'tests/helpers/intl-test-helper';
import {act} from 'tests/react_testing_utils';
import {renderWithContext, screen, userEvent, waitFor} from 'tests/react_testing_utils';
import {getHistory} from 'utils/browser_history';
describe('MfaSection', () => {
@ -31,9 +29,9 @@ describe('MfaSection', () => {
...baseProps,
mfaAvailable: false,
};
const wrapper = shallow(<MfaSection {...props}/>);
const {container} = renderWithContext(<MfaSection {...props}/>);
expect(wrapper).toMatchSnapshot();
expect(container).toMatchSnapshot();
});
test('when section is collapsed and MFA is not active', () => {
@ -42,9 +40,9 @@ describe('MfaSection', () => {
active: false,
mfaActive: false,
};
const wrapper = shallow(<MfaSection {...props}/>);
const {container} = renderWithContext(<MfaSection {...props}/>);
expect(wrapper).toMatchSnapshot();
expect(container).toMatchSnapshot();
});
test('when section is collapsed and MFA is active', () => {
@ -53,9 +51,9 @@ describe('MfaSection', () => {
active: false,
mfaActive: true,
};
const wrapper = shallow(<MfaSection {...props}/>);
const {container} = renderWithContext(<MfaSection {...props}/>);
expect(wrapper).toMatchSnapshot();
expect(container).toMatchSnapshot();
});
test('when section is expanded and MFA is not active', () => {
@ -63,9 +61,9 @@ describe('MfaSection', () => {
...baseProps,
mfaActive: false,
};
const wrapper = shallow(<MfaSection {...props}/>);
const {container} = renderWithContext(<MfaSection {...props}/>);
expect(wrapper).toMatchSnapshot();
expect(container).toMatchSnapshot();
});
test('when section is expanded and MFA is active but not enforced', () => {
@ -73,9 +71,9 @@ describe('MfaSection', () => {
...baseProps,
mfaActive: true,
};
const wrapper = shallow(<MfaSection {...props}/>);
const {container} = renderWithContext(<MfaSection {...props}/>);
expect(wrapper).toMatchSnapshot();
expect(container).toMatchSnapshot();
});
test('when section is expanded and MFA is active and enforced', () => {
@ -84,33 +82,36 @@ describe('MfaSection', () => {
mfaActive: true,
mfaEnforced: true,
};
const wrapper = shallow(<MfaSection {...props}/>);
const {container} = renderWithContext(<MfaSection {...props}/>);
expect(wrapper).toMatchSnapshot();
expect(container).toMatchSnapshot();
});
test('when section is expanded with a server error', () => {
test('when section is expanded with a server error', async () => {
const props = {
...baseProps,
serverError: 'An error occurred',
mfaActive: true,
actions: {
deactivateMfa: jest.fn(() => Promise.resolve({error: {message: 'An error has occurred'}})),
},
};
const wrapper = shallow(<MfaSection {...props}/>);
renderWithContext(<MfaSection {...props}/>);
wrapper.setState({serverError: 'An error has occurred'});
await userEvent.click(screen.getByText('Remove MFA from Account'));
expect(wrapper).toMatchSnapshot();
await waitFor(() => {
expect(screen.getByText('An error has occurred')).toBeInTheDocument();
});
expect(screen.getByText('An error has occurred')).toMatchSnapshot();
});
});
describe('setupMfa', () => {
it('should send to setup page', () => {
const wrapper = mountWithIntl(<MfaSection {...baseProps}/>);
it('should send to setup page', async () => {
renderWithContext(<MfaSection {...baseProps}/>);
const mockEvent = {
preventDefault: jest.fn(),
} as unknown as React.MouseEvent<HTMLElement>;
(wrapper.instance() as MfaSection).setupMfa(mockEvent);
await userEvent.click(screen.getByText('Add MFA to Account'));
expect(getHistory().push).toHaveBeenCalledWith('/mfa/setup');
});
@ -118,62 +119,58 @@ describe('MfaSection', () => {
describe('removeMfa', () => {
it('on success, should close section and clear state', async () => {
const wrapper = mountWithIntl(<MfaSection {...baseProps}/>);
const props = {
...baseProps,
mfaActive: true,
};
const mockEvent = {
preventDefault: jest.fn(),
} as unknown as React.MouseEvent<HTMLElement>;
renderWithContext(<MfaSection {...props}/>);
act(() => {
wrapper.setState({serverError: 'An error has occurred'});
await userEvent.click(screen.getByText('Remove MFA from Account'));
await waitFor(() => {
expect(baseProps.updateSection).toHaveBeenCalledWith('');
});
await act(async () => {
await (wrapper.instance() as MfaSection).removeMfa(mockEvent);
});
expect(baseProps.updateSection).toHaveBeenCalledWith('');
expect(wrapper.state('serverError')).toEqual(null);
expect(screen.queryByText(/error/i)).not.toBeInTheDocument();
expect(getHistory().push).not.toHaveBeenCalled();
});
it('on success, should send to setup page if MFA enforcement is enabled', async () => {
const props = {
...baseProps,
mfaActive: true,
mfaEnforced: true,
};
const wrapper = mountWithIntl(<MfaSection {...props}/>);
renderWithContext(<MfaSection {...props}/>);
const mockEvent = {
preventDefault: jest.fn(),
} as unknown as React.MouseEvent<HTMLElement>;
await userEvent.click(screen.getByText('Reset MFA on Account'));
await act(async () => {
await (wrapper.instance() as MfaSection).removeMfa(mockEvent);
await waitFor(() => {
expect(getHistory().push).toHaveBeenCalledWith('/mfa/setup');
});
expect(baseProps.updateSection).not.toHaveBeenCalled();
expect(getHistory().push).toHaveBeenCalledWith('/mfa/setup');
});
it('on error, should show error', async () => {
const error = {message: 'An error occurred'};
const wrapper = mountWithIntl(<MfaSection {...baseProps}/>);
const props = {
...baseProps,
mfaActive: true,
actions: {
deactivateMfa: jest.fn(() => Promise.resolve({error})),
},
};
const mockEvent = {
preventDefault: jest.fn(),
} as unknown as React.MouseEvent<HTMLElement>;
renderWithContext(<MfaSection {...props}/>);
baseProps.actions.deactivateMfa.mockImplementation(() => Promise.resolve({error}));
await userEvent.click(screen.getByText('Remove MFA from Account'));
await act(async () => {
await (wrapper.instance() as MfaSection).removeMfa(mockEvent);
await waitFor(() => {
expect(screen.getByText('An error occurred')).toBeInTheDocument();
});
expect(baseProps.updateSection).not.toHaveBeenCalled();
expect(wrapper.state('serverError')).toEqual(error.message);
});
});
});

View file

@ -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 {OAuthApp} from '@mattermost/types/integrations';
@ -9,19 +8,24 @@ import type {UserProfile} from '@mattermost/types/users';
import type {PasswordConfig} from 'mattermost-redux/selectors/entities/general';
import type {MockIntl} from 'tests/helpers/intl-test-helper';
import {renderWithContext, screen, userEvent, waitFor, fireEvent} from 'tests/react_testing_utils';
import Constants from 'utils/constants';
import {SecurityTab} from './user_settings_security';
import SecurityTab from './user_settings_security';
jest.mock('utils/password', () => {
const original = jest.requireActual('utils/password');
return {...original, isValidPassword: () => ({valid: true})};
});
jest.mock('./mfa_section', () => () => <div data-testid='mfa-section'/>);
jest.mock('./user_access_token_section', () => () => <div data-testid='tokens-section'/>);
describe('components/user_settings/display/UserSettingsDisplay', () => {
const user = {
id: 'user_id',
auth_service: '',
last_password_update: 1234567890000,
};
const requiredProps = {
@ -49,37 +53,34 @@ describe('components/user_settings/display/UserSettingsDisplay', () => {
experimentalEnableAuthenticationTransfer: true,
passwordConfig: {} as PasswordConfig,
militaryTime: false,
intl: {
formatMessage: jest.fn(({id, defaultMessage}) => defaultMessage || id),
} as MockIntl,
};
test('should match snapshot, enable google', () => {
const props = {...requiredProps, enableSaml: false};
const wrapper = shallow<SecurityTab>(<SecurityTab {...props}/>);
expect(wrapper).toMatchSnapshot();
const {container} = renderWithContext(<SecurityTab {...props}/>);
expect(container).toMatchSnapshot();
});
test('should match snapshot, enable gitlab', () => {
const props = {...requiredProps, enableSignUpWithGoogle: false, enableSaml: false, enableSignUpWithGitLab: true};
const wrapper = shallow<SecurityTab>(<SecurityTab {...props}/>);
expect(wrapper).toMatchSnapshot();
const {container} = renderWithContext(<SecurityTab {...props}/>);
expect(container).toMatchSnapshot();
});
test('should match snapshot, enable office365', () => {
const props = {...requiredProps, enableSignUpWithGoogle: false, enableSaml: false, enableSignUpWithOffice365: true};
const wrapper = shallow<SecurityTab>(<SecurityTab {...props}/>);
expect(wrapper).toMatchSnapshot();
const {container} = renderWithContext(<SecurityTab {...props}/>);
expect(container).toMatchSnapshot();
});
test('should match snapshot, enable openID', () => {
const props = {...requiredProps, enableSignUpWithGoogle: false, enableSaml: false, enableSignUpWithOpenId: true};
const wrapper = shallow<SecurityTab>(<SecurityTab {...props}/>);
expect(wrapper).toMatchSnapshot();
const {container} = renderWithContext(<SecurityTab {...props}/>);
expect(container).toMatchSnapshot();
});
test('should match snapshot, to email', () => {
@ -90,56 +91,64 @@ describe('components/user_settings/display/UserSettingsDisplay', () => {
const props = {...requiredProps, user: user as UserProfile};
const wrapper = shallow<SecurityTab>(<SecurityTab {...props}/>);
expect(wrapper).toMatchSnapshot();
const {container} = renderWithContext(<SecurityTab {...props}/>);
expect(container).toMatchSnapshot();
});
test('componentDidMount() should have called getAuthorizedOAuthApps', () => {
const props = {...requiredProps, enableOAuthServiceProvider: true};
shallow<SecurityTab>(<SecurityTab {...props}/>);
renderWithContext(<SecurityTab {...props}/>);
expect(requiredProps.actions.getAuthorizedOAuthApps).toHaveBeenCalled();
});
test('componentDidMount() should have updated state.authorizedApps', async () => {
const apps = [{name: 'app1'}];
const promise = Promise.resolve({data: apps});
const getAuthorizedOAuthApps = () => promise;
const apps = [{id: 'app1', name: 'app1'}];
const getAuthorizedOAuthApps = jest.fn().mockResolvedValue({data: apps});
const props = {
...requiredProps,
actions: {...requiredProps.actions, getAuthorizedOAuthApps},
enableOAuthServiceProvider: true,
activeSection: 'apps',
};
const wrapper = shallow<SecurityTab>(<SecurityTab {...props}/>);
renderWithContext(<SecurityTab {...props}/>);
await promise;
expect(wrapper.state().authorizedApps).toEqual(apps);
await waitFor(() => {
expect(screen.getByText('app1')).toBeInTheDocument();
});
});
test('componentDidMount() should have updated state.serverError', async () => {
const error = {message: 'error'};
const promise = Promise.resolve({error});
const getAuthorizedOAuthApps = () => promise;
const getAuthorizedOAuthApps = jest.fn().mockResolvedValue({error});
const props = {
...requiredProps,
actions: {...requiredProps.actions, getAuthorizedOAuthApps},
enableOAuthServiceProvider: true,
activeSection: 'apps',
};
const wrapper = shallow<SecurityTab>(<SecurityTab {...props}/>);
renderWithContext(<SecurityTab {...props}/>);
await promise;
expect(wrapper.state('serverError')).toEqual(error.message);
await waitFor(() => {
expect(screen.getByText('error')).toBeInTheDocument();
});
});
test('submitPassword() should not have called updateUserPassword', async () => {
const wrapper = shallow<SecurityTab>(<SecurityTab {...requiredProps}/>);
test('submitPassword() should not have called updateUserPassword', () => {
const props = {
...requiredProps,
activeSection: 'password',
};
renderWithContext(<SecurityTab {...props}/>);
// Save button is disabled because fields are empty (isValid is false)
const saveButton = screen.getByText('Save');
fireEvent.click(saveButton);
await wrapper.instance().submitPassword();
expect(requiredProps.actions.updateUserPassword).toHaveBeenCalledTimes(0);
});
@ -148,40 +157,50 @@ describe('components/user_settings/display/UserSettingsDisplay', () => {
const props = {
...requiredProps,
actions: {...requiredProps.actions, updateUserPassword},
activeSection: 'password',
};
const wrapper = shallow<SecurityTab>(<SecurityTab {...props}/>);
renderWithContext(<SecurityTab {...props}/>);
const password = 'psw';
const state = {
currentPassword: 'currentPassword',
newPassword: password,
confirmPassword: password,
};
wrapper.setState(state);
await wrapper.instance().submitPassword();
await userEvent.type(screen.getByLabelText('Current Password'), 'currentPassword');
await userEvent.type(screen.getByLabelText('New Password'), password);
await userEvent.type(screen.getByLabelText('Retype New Password'), password);
expect(updateUserPassword).toHaveBeenCalled();
await userEvent.click(screen.getByText('Save'));
await waitFor(() => {
expect(updateUserPassword).toHaveBeenCalled();
});
expect(updateUserPassword).toHaveBeenCalledWith(
user.id,
state.currentPassword,
state.newPassword,
'currentPassword',
password,
);
expect(requiredProps.updateSection).toHaveBeenCalled();
expect(requiredProps.updateSection).toHaveBeenCalledWith('');
});
test('deauthorizeApp() should have called deauthorizeOAuthApp', () => {
test('deauthorizeApp() should have called deauthorizeOAuthApp', async () => {
const appId = 'appId';
const event: any = {
currentTarget: {getAttribute: jest.fn().mockReturnValue(appId)},
preventDefault: jest.fn(),
const apps = [{id: appId, name: 'TestApp', homepage: 'http://test.com', description: 'test', icon_url: ''}] as OAuthApp[];
const getAuthorizedOAuthApps = jest.fn().mockResolvedValue({data: apps});
const props = {
...requiredProps,
actions: {...requiredProps.actions, getAuthorizedOAuthApps},
enableOAuthServiceProvider: true,
activeSection: 'apps',
};
const wrapper = shallow<SecurityTab>(<SecurityTab {...requiredProps}/>);
wrapper.setState({authorizedApps: []});
wrapper.instance().deauthorizeApp(event);
renderWithContext(<SecurityTab {...props}/>);
await waitFor(() => {
expect(screen.getByText('Deauthorize')).toBeInTheDocument();
});
await userEvent.click(screen.getByText('Deauthorize'));
expect(requiredProps.actions.deauthorizeOAuthApp).toHaveBeenCalled();
expect(requiredProps.actions.deauthorizeOAuthApp).toHaveBeenCalledWith(
@ -190,46 +209,54 @@ describe('components/user_settings/display/UserSettingsDisplay', () => {
});
test('deauthorizeApp() should have updated state.authorizedApps', async () => {
const promise = Promise.resolve({data: true});
const appId = 'appId';
const apps = [{id: appId, name: 'App1', homepage: 'http://test.com', description: '', icon_url: ''}, {id: '2', name: 'App2', homepage: 'http://test2.com', description: '', icon_url: ''}] as OAuthApp[];
const deauthorizeOAuthApp = jest.fn().mockResolvedValue({data: true});
const getAuthorizedOAuthApps = jest.fn().mockResolvedValue({data: apps});
const props = {
...requiredProps,
actions: {...requiredProps.actions, deauthorizeOAuthApp: () => promise},
actions: {...requiredProps.actions, deauthorizeOAuthApp, getAuthorizedOAuthApps},
enableOAuthServiceProvider: true,
activeSection: 'apps',
};
const wrapper = shallow<SecurityTab>(<SecurityTab {...props}/>);
renderWithContext(<SecurityTab {...props}/>);
const appId = 'appId';
const apps = [{id: appId}, {id: '2'}] as OAuthApp[];
const event: any = {
currentTarget: {getAttribute: jest.fn().mockReturnValue(appId)},
preventDefault: jest.fn(),
};
wrapper.setState({authorizedApps: apps});
wrapper.instance().deauthorizeApp(event);
await waitFor(() => {
expect(screen.getAllByText('Deauthorize')).toHaveLength(2);
});
await promise;
await userEvent.click(screen.getAllByText('Deauthorize')[0]);
expect(wrapper.state().authorizedApps).toEqual(apps.slice(1));
await waitFor(() => {
expect(screen.queryByText('App1')).not.toBeInTheDocument();
});
expect(screen.getByText('App2')).toBeInTheDocument();
});
test('deauthorizeApp() should have updated state.serverError', async () => {
const appId = 'appId';
const apps = [{id: appId, name: 'TestApp', homepage: 'http://test.com', description: '', icon_url: ''}] as OAuthApp[];
const error = {message: 'error'};
const promise = Promise.resolve({error});
const deauthorizeOAuthApp = jest.fn().mockResolvedValue({error});
const getAuthorizedOAuthApps = jest.fn().mockResolvedValue({data: apps});
const props = {
...requiredProps,
actions: {...requiredProps.actions, deauthorizeOAuthApp: () => promise},
actions: {...requiredProps.actions, deauthorizeOAuthApp, getAuthorizedOAuthApps},
enableOAuthServiceProvider: true,
activeSection: 'apps',
};
const wrapper = shallow<SecurityTab>(<SecurityTab {...props}/>);
renderWithContext(<SecurityTab {...props}/>);
const event: any = {
currentTarget: {getAttribute: jest.fn().mockReturnValue('appId')},
preventDefault: jest.fn(),
};
wrapper.instance().deauthorizeApp(event);
await waitFor(() => {
expect(screen.getByText('Deauthorize')).toBeInTheDocument();
});
await promise;
await userEvent.click(screen.getByText('Deauthorize'));
expect(wrapper.state('serverError')).toEqual(error.message);
await waitFor(() => {
expect(screen.getByText('error')).toBeInTheDocument();
});
});
});