mirror of
https://github.com/mattermost/mattermost-mobile.git
synced 2026-11-05 18:38:54 +00:00
* MM-70516 - Add ability to edit channel attribute values from Channel Info * Add API methods for patching property fields and setting channel attribute requirements * Update channel attribute clearing behavior for required and non-required fields * MM-70564 - use channel attributes on channel creation * Fix undefined propertyValues and ternary formatting in channel attributes * Fix spelling of "colour" to "color" in attribute chip tests and related comments * Add visibility checks for channel attribute labels in tests * Fix data-integrity, permission, and accessibility issues in channel attributes: scoped pruning, atomic writes, race serialization, required clear/stale-option guards * Fix handling of null values in apiGetChannelAttributeValue and update test assertions for channel attributes * Improve channel attribute editor sizing for long labels, keep Save visible, and prevent keyboard overlap on Android * Address CodeRabbit feedback for nullable values, bottom-sheet state, async validation, and reliable E2E cleanup * lock-safe field fetch, create-time gate recheck, scoped value pruning, and tappable header chips * Address CodeRabbit feedback: drop queued attribute saves on channel switch and use page object selectors in E2E * Respect the ChannelAttributesRequired flag on channel creation and enable it for channel attribute E2E * Skip required channel attribute E2E tests and stop enabling ChannelAttributesRequired on the shared server until they move to SITE_3 * Fix clearing channel attributes on iOS by sending an empty value instead of null, and stop E2E suites from turning off the ChannelAttributes flag mid-run * Refactor getEmptyPropertyValue to accept string type for fieldType parameter * E2E: give channel attribute test fields a suite-unique suffix Pool shards may only create optional property fields named <name>_<6 hex>; fixed names were blocked by the pool guard. * E2E: restore channel attribute cleanup checks; name network error codes Brings back the PR's afterEach/afterAll error handling that the main merge replaced, and makes a no-response error show its codes (an AggregateError has an empty message). * E2E: run channel attribute creation and editing specs on server B Their 11 required-attribute tests could not run on the shared servers. Both specs now run on the mutation server with ChannelAttributesRequired on for their run, and the tests are re-enabled. setChannelAttributesRequired moves to the shared helper. * Check header chips instead of their wrapper and use toExist for the create screen in channel attribute E2E --------- Co-authored-by: yasserfaraazkhan <attitude3cena.yf@gmail.com>
195 lines
6.9 KiB
TypeScript
195 lines
6.9 KiB
TypeScript
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
// See LICENSE.txt for license information.
|
|
|
|
import {fireEvent, render, within} from '@testing-library/react-native';
|
|
import React, {type ComponentProps} from 'react';
|
|
import {Pressable, Text} from 'react-native';
|
|
|
|
import {Preferences} from '@constants';
|
|
|
|
import Header from './header';
|
|
|
|
import type {NavigationButtonProps} from '@components/navigation_button';
|
|
|
|
describe('Header', () => {
|
|
const getBaseProps = (): ComponentProps<typeof Header> => ({
|
|
defaultHeight: 0,
|
|
hasSearch: false,
|
|
isLargeTitle: false,
|
|
heightOffset: 0,
|
|
theme: Preferences.THEMES.denim,
|
|
});
|
|
|
|
it('should render subtitleComponent when provided', () => {
|
|
const props = getBaseProps();
|
|
const subtitleText = 'Custom Subtitle';
|
|
props.subtitleComponent = <Text testID='custom-subtitle'>{subtitleText}</Text>;
|
|
const {getByTestId} = render(<Header {...props}/>);
|
|
expect(getByTestId('custom-subtitle')).toBeOnTheScreen();
|
|
});
|
|
|
|
it('should keep an interactive subtitle independent from the title action', () => {
|
|
const props = getBaseProps();
|
|
const onTitlePress = jest.fn();
|
|
const onSubtitlePress = jest.fn();
|
|
const subtitleText = 'Attributes';
|
|
props.onTitlePress = onTitlePress;
|
|
props.title = 'Town Square';
|
|
props.subtitleComponent = (
|
|
<Pressable
|
|
onPress={onSubtitlePress}
|
|
style={({pressed}) => ({opacity: pressed ? 0.72 : 1})}
|
|
testID='custom-subtitle'
|
|
>
|
|
<Text>{subtitleText}</Text>
|
|
</Pressable>
|
|
);
|
|
const {getByTestId} = render(<Header {...props}/>);
|
|
|
|
fireEvent.press(getByTestId('custom-subtitle'));
|
|
|
|
expect(onSubtitlePress).toHaveBeenCalledTimes(1);
|
|
expect(onTitlePress).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should call the title action when the non-interactive part of the subtitle is pressed', () => {
|
|
const props = getBaseProps();
|
|
const onTitlePress = jest.fn();
|
|
props.onTitlePress = onTitlePress;
|
|
props.title = 'Town Square';
|
|
props.subtitleComponent = <Text testID='custom-subtitle'>{'Attributes'}</Text>;
|
|
const {getByTestId} = render(<Header {...props}/>);
|
|
|
|
fireEvent.press(getByTestId('custom-subtitle'));
|
|
|
|
expect(onTitlePress).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('should fall back to subtitle text when subtitleComponent is absent', () => {
|
|
const props = getBaseProps();
|
|
props.subtitle = 'Legacy subtitle';
|
|
const {getByTestId, queryByTestId} = render(<Header {...props}/>);
|
|
expect(getByTestId('navigation.header.subtitle')).toBeOnTheScreen();
|
|
expect(queryByTestId('custom-subtitle')).toBeNull();
|
|
});
|
|
|
|
it('should not render the subtitle area when neither subtitleComponent nor subtitle is provided', () => {
|
|
const props = getBaseProps();
|
|
const {queryByTestId} = render(<Header {...props}/>);
|
|
expect(queryByTestId('navigation.header.subtitle')).toBeNull();
|
|
});
|
|
|
|
it('should render right buttons with count', () => {
|
|
const props = getBaseProps();
|
|
props.rightButtons = [
|
|
{
|
|
id: 'playbooks',
|
|
iconName: 'playlist-check',
|
|
count: 123,
|
|
onPress: jest.fn(),
|
|
testID: 'test-button',
|
|
},
|
|
];
|
|
const {getByTestId, rerender} = render(<Header {...props}/>);
|
|
|
|
let button = getByTestId('test-button');
|
|
expect(within(button).getByText('123')).toBeTruthy();
|
|
|
|
props.rightButtons = [
|
|
{
|
|
id: 'playbooks',
|
|
iconName: 'playlist-check',
|
|
count: undefined,
|
|
onPress: jest.fn(),
|
|
testID: 'test-button',
|
|
},
|
|
];
|
|
rerender(<Header {...props}/>);
|
|
button = getByTestId('test-button');
|
|
expect(button).toBeOnTheScreen();
|
|
expect(button).not.toHaveTextContent('123');
|
|
expect(button).not.toHaveTextContent('0');
|
|
expect(button).not.toHaveTextContent('undefined');
|
|
});
|
|
|
|
it('should show a spinner and ignore presses on right buttons while loading', () => {
|
|
const props = getBaseProps();
|
|
const onPress = jest.fn();
|
|
const callButton: NavigationButtonProps = {
|
|
id: 'calls',
|
|
accessibilityLabel: 'Start call',
|
|
iconName: 'phone',
|
|
isLoading: true,
|
|
onPress,
|
|
testID: 'test-button',
|
|
};
|
|
props.rightButtons = [callButton];
|
|
const {getByTestId, queryByTestId, rerender} = render(<Header {...props}/>);
|
|
|
|
const button = getByTestId('test-button');
|
|
expect(button.props.accessibilityLabel).toBe('Start call');
|
|
expect(getByTestId('test-button.loading')).toBeOnTheScreen();
|
|
|
|
fireEvent.press(button);
|
|
expect(onPress).not.toHaveBeenCalled();
|
|
|
|
props.rightButtons = [{...callButton, isLoading: false}];
|
|
rerender(<Header {...props}/>);
|
|
|
|
expect(queryByTestId('test-button.loading')).toBeNull();
|
|
fireEvent.press(getByTestId('test-button'));
|
|
expect(onPress).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('should not fire onPress on right buttons when disabled', () => {
|
|
const props = getBaseProps();
|
|
const onPress = jest.fn();
|
|
props.rightButtons = [
|
|
{
|
|
id: 'calls',
|
|
disabled: true,
|
|
iconName: 'phone',
|
|
onPress,
|
|
testID: 'test-button',
|
|
},
|
|
];
|
|
const {getByTestId} = render(<Header {...props}/>);
|
|
|
|
fireEvent.press(getByTestId('test-button'));
|
|
expect(onPress).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should keep the right button pressable when its icon changes', () => {
|
|
const props = getBaseProps();
|
|
const onPress = jest.fn();
|
|
const callButton: NavigationButtonProps = {
|
|
id: 'calls',
|
|
iconName: 'phone',
|
|
onPress,
|
|
testID: 'call-button',
|
|
};
|
|
props.rightButtons = [
|
|
callButton,
|
|
{
|
|
id: 'channel-quick-actions',
|
|
iconName: 'dots-horizontal',
|
|
onPress: jest.fn(),
|
|
testID: 'quick-actions-button',
|
|
},
|
|
];
|
|
const {getByTestId, rerender} = render(<Header {...props}/>);
|
|
|
|
expect(getByTestId('call-button')).toBeOnTheScreen();
|
|
expect(getByTestId('quick-actions-button')).toBeOnTheScreen();
|
|
|
|
// A call starting swaps the icon; the button must stay usable so an in-flight press is not lost.
|
|
props.rightButtons = [{...callButton, iconName: 'phone-in-talk'}, props.rightButtons[1]];
|
|
rerender(<Header {...props}/>);
|
|
|
|
expect(getByTestId('quick-actions-button')).toBeOnTheScreen();
|
|
fireEvent.press(getByTestId('call-button'));
|
|
expect(onPress).toHaveBeenCalledTimes(1);
|
|
});
|
|
});
|
|
|