Update ChannelHeaderPlug tests to not use snapshots and ensure its more accessible (#26784)
* Update ChannelHeaderPlug tests to not use snapshots * Change HeaderIconWrapper to always set aria-label based off tooltip * Re-add shortcut to Recent Mentions button * Remove unneeded div
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
d40cc68b1c
Коммит
527fe4a654
@@ -1,175 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`plugins/ChannelHeaderPlug should match snapshot with one extended component 1`] = `
|
||||
<DocumentFragment>
|
||||
<div>
|
||||
<button
|
||||
class="channel-header__icon"
|
||||
id="someid"
|
||||
>
|
||||
<i
|
||||
class="fa fa-anchor"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`plugins/ChannelHeaderPlug should match snapshot with six extended components 1`] = `
|
||||
<DocumentFragment>
|
||||
<div>
|
||||
<button
|
||||
class="channel-header__icon"
|
||||
id="someid"
|
||||
>
|
||||
<i
|
||||
class="fa fa-anchor"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
<button
|
||||
class="channel-header__icon"
|
||||
id="someid2"
|
||||
>
|
||||
<i
|
||||
class="fa fa-anchor"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
<button
|
||||
class="channel-header__icon"
|
||||
id="someid3"
|
||||
>
|
||||
<i
|
||||
class="fa fa-anchor"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
<button
|
||||
class="channel-header__icon"
|
||||
id="someid4"
|
||||
>
|
||||
<i
|
||||
class="fa fa-anchor"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
<button
|
||||
class="channel-header__icon"
|
||||
id="someid5"
|
||||
>
|
||||
<i
|
||||
class="fa fa-anchor"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
<button
|
||||
class="channel-header__icon"
|
||||
id="someid6"
|
||||
>
|
||||
<i
|
||||
class="fa fa-anchor"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
<button
|
||||
class="channel-header__icon"
|
||||
id="someid7"
|
||||
>
|
||||
<i
|
||||
class="fa fa-anchor"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
<button
|
||||
class="channel-header__icon"
|
||||
id="someid8"
|
||||
>
|
||||
<i
|
||||
class="fa fa-anchor"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
<button
|
||||
class="channel-header__icon"
|
||||
id="someid9"
|
||||
>
|
||||
<i
|
||||
class="fa fa-anchor"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
<button
|
||||
class="channel-header__icon"
|
||||
id="someid10"
|
||||
>
|
||||
<i
|
||||
class="fa fa-anchor"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
<button
|
||||
class="channel-header__icon"
|
||||
id="someid11"
|
||||
>
|
||||
<i
|
||||
class="fa fa-anchor"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
<button
|
||||
class="channel-header__icon"
|
||||
id="someid12"
|
||||
>
|
||||
<i
|
||||
class="fa fa-anchor"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
<button
|
||||
class="channel-header__icon"
|
||||
id="someid13"
|
||||
>
|
||||
<i
|
||||
class="fa fa-anchor"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
<button
|
||||
class="channel-header__icon"
|
||||
id="someid14"
|
||||
>
|
||||
<i
|
||||
class="fa fa-anchor"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
<button
|
||||
class="channel-header__icon"
|
||||
id="someid15"
|
||||
>
|
||||
<i
|
||||
class="fa fa-anchor"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`plugins/ChannelHeaderPlug should not render anything when the App Bar is visible 1`] = `<DocumentFragment />`;
|
||||
|
||||
exports[`plugins/ChannelHeaderPlug should not render anything with no extended component 1`] = `<DocumentFragment />`;
|
||||
@@ -3,127 +3,115 @@
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import type {Channel, ChannelMembership} from '@mattermost/types/channels';
|
||||
|
||||
import type {Theme} from 'mattermost-redux/selectors/entities/preferences';
|
||||
|
||||
import ChannelHeaderPlug from 'plugins/channel_header_plug/channel_header_plug';
|
||||
import {renderWithContext} from 'tests/react_testing_utils';
|
||||
import {renderWithContext, screen} from 'tests/react_testing_utils';
|
||||
import {TestHelper} from 'utils/test_helper';
|
||||
|
||||
import type {PluginComponent} from 'types/store/plugins';
|
||||
|
||||
import ChannelHeaderPlug, {maxComponentsBeforeDropdown} from './channel_header_plug';
|
||||
|
||||
describe('plugins/ChannelHeaderPlug', () => {
|
||||
const testPlug: PluginComponent = {
|
||||
id: 'someid',
|
||||
pluginId: 'pluginid',
|
||||
icon: <i className='fa fa-anchor'/>,
|
||||
action: jest.fn,
|
||||
dropdownText: 'some dropdown text',
|
||||
tooltipText: 'some tooltip text',
|
||||
} as PluginComponent;
|
||||
const baseProps = {
|
||||
components: [],
|
||||
channel: TestHelper.getChannelMock({id: 'channel1'}),
|
||||
channelMember: TestHelper.getChannelMembershipMock({channel_id: 'channel1', user_id: 'user1'}),
|
||||
sidebarOpen: false,
|
||||
actions: {
|
||||
handleBindingClick: jest.fn(),
|
||||
postEphemeralCallResponseForChannel: jest.fn(),
|
||||
openAppsModal: jest.fn(),
|
||||
},
|
||||
appBindings: [],
|
||||
appsEnabled: false,
|
||||
shouldShowAppBar: false,
|
||||
};
|
||||
|
||||
function makeTestPlug(n = 1): PluginComponent {
|
||||
return {
|
||||
id: 'someid' + n,
|
||||
pluginId: 'pluginid' + n,
|
||||
icon: <i className='fa fa-anchor'/>,
|
||||
action: jest.fn,
|
||||
dropdownText: 'some dropdown text ' + n,
|
||||
tooltipText: 'some tooltip text ' + n,
|
||||
};
|
||||
}
|
||||
|
||||
test('should not render anything with no extended component', () => {
|
||||
const {asFragment} = renderWithContext(
|
||||
const {container} = renderWithContext(
|
||||
<ChannelHeaderPlug
|
||||
components={[]}
|
||||
channel={{} as Channel}
|
||||
channelMember={{} as ChannelMembership}
|
||||
theme={{} as Theme}
|
||||
sidebarOpen={false}
|
||||
actions={{
|
||||
handleBindingClick: jest.fn(),
|
||||
postEphemeralCallResponseForChannel: jest.fn(),
|
||||
openAppsModal: jest.fn(),
|
||||
}}
|
||||
appBindings={[]}
|
||||
appsEnabled={false}
|
||||
shouldShowAppBar={false}
|
||||
{...baseProps}
|
||||
/>,
|
||||
);
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
|
||||
expect(container).toBeEmptyDOMElement();
|
||||
});
|
||||
|
||||
test('should match snapshot with one extended component', () => {
|
||||
const {asFragment} = renderWithContext(
|
||||
test('should render a single plug', () => {
|
||||
renderWithContext(
|
||||
<ChannelHeaderPlug
|
||||
components={[testPlug]}
|
||||
channel={{} as Channel}
|
||||
channelMember={{} as ChannelMembership}
|
||||
theme={{} as Theme}
|
||||
sidebarOpen={false}
|
||||
actions={{
|
||||
handleBindingClick: jest.fn(),
|
||||
postEphemeralCallResponseForChannel: jest.fn(),
|
||||
openAppsModal: jest.fn(),
|
||||
}}
|
||||
appBindings={[]}
|
||||
appsEnabled={false}
|
||||
shouldShowAppBar={false}
|
||||
{...baseProps}
|
||||
components={[makeTestPlug()]}
|
||||
/>,
|
||||
);
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
|
||||
expect(screen.getByLabelText('some tooltip text 1')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should match snapshot with six extended components', () => {
|
||||
const {asFragment} = renderWithContext(
|
||||
test(`should render ${maxComponentsBeforeDropdown} plugs in the header`, () => {
|
||||
const components = [];
|
||||
for (let i = 0; i < maxComponentsBeforeDropdown; i++) {
|
||||
components.push(makeTestPlug(i));
|
||||
}
|
||||
|
||||
renderWithContext(
|
||||
<ChannelHeaderPlug
|
||||
components={[
|
||||
testPlug,
|
||||
{...testPlug, id: 'someid2'},
|
||||
{...testPlug, id: 'someid3'},
|
||||
{...testPlug, id: 'someid4'},
|
||||
{...testPlug, id: 'someid5'},
|
||||
{...testPlug, id: 'someid6'},
|
||||
{...testPlug, id: 'someid7'},
|
||||
{...testPlug, id: 'someid8'},
|
||||
{...testPlug, id: 'someid9'},
|
||||
{...testPlug, id: 'someid10'},
|
||||
{...testPlug, id: 'someid11'},
|
||||
{...testPlug, id: 'someid12'},
|
||||
{...testPlug, id: 'someid13'},
|
||||
{...testPlug, id: 'someid14'},
|
||||
{...testPlug, id: 'someid15'},
|
||||
]}
|
||||
channel={{} as Channel}
|
||||
channelMember={{} as ChannelMembership}
|
||||
theme={{} as Theme}
|
||||
sidebarOpen={false}
|
||||
actions={{
|
||||
handleBindingClick: jest.fn(),
|
||||
postEphemeralCallResponseForChannel: jest.fn(),
|
||||
openAppsModal: jest.fn(),
|
||||
}}
|
||||
appBindings={[]}
|
||||
appsEnabled={false}
|
||||
shouldShowAppBar={false}
|
||||
{...baseProps}
|
||||
components={components}
|
||||
/>,
|
||||
);
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
|
||||
for (let i = 0; i < components.length; i++) {
|
||||
expect(screen.getByLabelText('some tooltip text ' + i)).toBeInTheDocument();
|
||||
}
|
||||
});
|
||||
|
||||
test(`should render more than ${maxComponentsBeforeDropdown} plugs in a dropdown`, () => {
|
||||
const components = [];
|
||||
for (let i = 0; i < maxComponentsBeforeDropdown + 1; i++) {
|
||||
components.push(makeTestPlug(i));
|
||||
}
|
||||
|
||||
renderWithContext(
|
||||
<ChannelHeaderPlug
|
||||
{...baseProps}
|
||||
components={components}
|
||||
/>,
|
||||
);
|
||||
|
||||
for (let i = 0; i < components.length; i++) {
|
||||
expect(screen.queryByLabelText('some tooltip text ' + i)).not.toBeInTheDocument();
|
||||
}
|
||||
|
||||
// Ideally, this would identify the dropdown button better, but this uses a custom dropdown which is
|
||||
// not at all accessible
|
||||
expect(screen.getByRole('button', {name: components.length.toString()})).toBeVisible();
|
||||
});
|
||||
|
||||
test('should not render anything when the App Bar is visible', () => {
|
||||
const {asFragment} = renderWithContext(
|
||||
const {container} = renderWithContext(
|
||||
<ChannelHeaderPlug
|
||||
{...baseProps}
|
||||
components={[
|
||||
testPlug,
|
||||
{...testPlug, id: 'someid2'},
|
||||
{...testPlug, id: 'someid3'},
|
||||
{...testPlug, id: 'someid4'},
|
||||
makeTestPlug(1),
|
||||
makeTestPlug(2),
|
||||
makeTestPlug(3),
|
||||
makeTestPlug(4),
|
||||
]}
|
||||
channel={{} as Channel}
|
||||
channelMember={{} as ChannelMembership}
|
||||
theme={{} as Theme}
|
||||
sidebarOpen={false}
|
||||
actions={{
|
||||
handleBindingClick: jest.fn(),
|
||||
postEphemeralCallResponseForChannel: jest.fn(),
|
||||
openAppsModal: jest.fn(),
|
||||
}}
|
||||
appBindings={[]}
|
||||
appsEnabled={false}
|
||||
shouldShowAppBar={true}
|
||||
/>,
|
||||
);
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
|
||||
expect(container).toBeEmptyDOMElement();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -13,7 +13,6 @@ import type {AppBinding} from '@mattermost/types/apps';
|
||||
import type {Channel, ChannelMembership} from '@mattermost/types/channels';
|
||||
|
||||
import {AppCallResponseTypes} from 'mattermost-redux/constants/apps';
|
||||
import type {Theme} from 'mattermost-redux/selectors/entities/preferences';
|
||||
|
||||
import HeaderIconWrapper from 'components/channel_header/components/header_icon_wrapper';
|
||||
import OverlayTrigger from 'components/overlay_trigger';
|
||||
@@ -33,6 +32,8 @@ type CustomMenuProps = {
|
||||
bsRole: string;
|
||||
}
|
||||
|
||||
export const maxComponentsBeforeDropdown = 15;
|
||||
|
||||
class CustomMenu extends React.PureComponent<CustomMenuProps> {
|
||||
handleRootClose = () => {
|
||||
this.props.onClose();
|
||||
@@ -104,7 +105,6 @@ type ChannelHeaderPlugProps = {
|
||||
appsEnabled: boolean;
|
||||
channel: Channel;
|
||||
channelMember?: ChannelMembership;
|
||||
theme: Theme;
|
||||
sidebarOpen: boolean;
|
||||
shouldShowAppBar: boolean;
|
||||
actions: {
|
||||
@@ -165,15 +165,44 @@ class ChannelHeaderPlug extends React.PureComponent<ChannelHeaderPlugProps, Chan
|
||||
};
|
||||
|
||||
createComponentButton = (plug: PluginComponent) => {
|
||||
// These values are supposed to be strings based on PluginComponent, but some plugins pass non-strings,
|
||||
// so do some hacky stuff to try to convert it back to a string. DO NOT USE THIS ELSEWHERE!
|
||||
function tooltipToAriaLabelHack(intl: IntlShape, stringOrElement: string | React.ReactElement) {
|
||||
if (typeof stringOrElement === 'string') {
|
||||
// This is the case that we hope for
|
||||
return stringOrElement;
|
||||
}
|
||||
|
||||
if (stringOrElement.type === FormattedMessage) {
|
||||
// This is a FormattedMessage, so extract the props to translate the text manually
|
||||
return intl.formatMessage(
|
||||
{
|
||||
id: stringOrElement.props.id,
|
||||
defaultMessage: stringOrElement.props.defaultMessage,
|
||||
},
|
||||
stringOrElement.props.value,
|
||||
);
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
let ariaLabel;
|
||||
if (plug.tooltipText) {
|
||||
ariaLabel = tooltipToAriaLabelHack(this.props.intl, plug.tooltipText);
|
||||
} else if (plug.dropdownText) {
|
||||
ariaLabel = tooltipToAriaLabelHack(this.props.intl, plug.dropdownText);
|
||||
}
|
||||
|
||||
return (
|
||||
<HeaderIconWrapper
|
||||
key={'channelHeaderButton' + plug.id}
|
||||
buttonClass='channel-header__icon'
|
||||
iconComponent={plug.icon!}
|
||||
onClick={() => this.fireAction(plug.action!)}
|
||||
buttonId={plug.id}
|
||||
tooltipKey={'plugin'}
|
||||
tooltipText={plug.tooltipText ? plug.tooltipText : plug.dropdownText}
|
||||
buttonId={plug.id + 'ChannelHeaderButton'}
|
||||
tooltip={plug.tooltipText ?? plug.dropdownText ?? ''}
|
||||
ariaLabelOverride={ariaLabel}
|
||||
pluginId={plug.pluginId}
|
||||
/>
|
||||
);
|
||||
@@ -245,8 +274,7 @@ class ChannelHeaderPlug extends React.PureComponent<ChannelHeaderPlugProps, Chan
|
||||
)}
|
||||
onClick={() => this.onBindingClick(binding)}
|
||||
buttonId={`${binding.app_id}_${binding.location}`}
|
||||
tooltipKey={'plugin'}
|
||||
tooltipText={binding.label}
|
||||
tooltip={binding.label}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -346,7 +374,7 @@ class ChannelHeaderPlug extends React.PureComponent<ChannelHeaderPlugProps, Chan
|
||||
const appBindings = this.props.appsEnabled ? this.props.appBindings || [] : [];
|
||||
if (this.props.shouldShowAppBar || (components.length === 0 && appBindings.length === 0)) {
|
||||
return null;
|
||||
} else if ((components.length + appBindings.length) <= 15) {
|
||||
} else if ((components.length + appBindings.length) <= maxComponentsBeforeDropdown) {
|
||||
let componentButtons = components.filter((plug) => plug.icon && plug.action).map(this.createComponentButton);
|
||||
if (this.props.appsEnabled) {
|
||||
componentButtons = componentButtons.concat(appBindings.map(this.createAppBindingButton));
|
||||
|
||||
Ссылка в новой задаче
Block a user