mattermost-mobile-upstream/app/components/navigation_header/header.test.tsx
Pablo Vélez 4d842026f6
Some checks failed
ci / lint-typecheck (push) Has been cancelled
ci / detox-lint-typecheck (push) Has been cancelled
ci / test (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
Mm 70564 channel labels use in channel creation (#10146)
* 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>
2026-10-30 15:03:55 +01:00

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);
});
});