mattermost-mobile-upstream/app/utils/channel/index.test.ts
Elias Nahum b6fc85385e
Migrate from RNN to Expo router (#9402)
* Expo-Router partial migration

* Full migration to ExpoRouter

* i18n

* add tests

* Merge branch 'main' into expo-router

* align package-lock.json

* i18n

* feedback review

* feedback review

* android 16kb patch

* Refactored emoji picker keyboard using a state machine

* set android launch_screen in drawables instead of layout

* no conditional hooks

* remove duplicate call in test

* address feedback

* fix tests and address more feedback

* small corrections

* small corrections

* small corrections

* fix draft test

* fix draft test 2

* remove navigateToScreen in draff test

* fix nit in draft test

* remove doc file

* Feedback review

* fix snapshots

* improve test coverage

* fix i18n

* Upgrade t o RN 0.83, other dependencies and new architecture (#9737)

* library hardware-keyboard android new arch support fix

* library rnshare android new arch support fix

* library rnutils android new arch support fix

* library secure-pdf-viewer add new arch support

* Enable new arch on iOS and Android

* update emm and network client with new-arch

* update intune to use new-arch

* performance improvements

* fix tests

* fixes post input on new-arch

* fix android emoji picker

* remove doc

* run tests in parallel

* upgrade to React Native 0.83.9, Expo 55, React 19, and New Architecture

Bumps core dependencies to their latest versions:
- React Native 0.77.3 → 0.83.9 (New Architecture enabled)
- React 18.3.1 → 19.2.5
- Expo SDK 52 → 55
- TypeScript 5.8.3 → 6.0.3
- Node.js 22.16.0 → 24.15.0
- react-native-reanimated 3.17.3 → 4.3.0
- Various other dependency upgrades

Migrates iOS AppDelegate from Objective-C to Swift, updates Android
build tooling and Gradle wrapper, refreshes all patches and font
assets, and adapts app code, tests, and CI workflows throughout for
API changes introduced by these upgrades.

* remove unecessary dismissKeyboard in notifications

* feedback review

* update intune to use new-arch

* update @mattermost deps

* missing i18n string

* additional fixes

* update fastlane
2026-05-13 17:44:55 +08:00

275 lines
10 KiB
TypeScript

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {createIntl} from 'react-intl';
import {Channel, General, Permissions} from '@constants';
import {hasPermission} from '@utils/role';
import {
compareNotifyProps,
filterChannelsMatchingTerm,
generateChannelNameFromDisplayName,
getDirectChannelName,
isArchived,
isDefaultChannel,
isDMorGM,
isTypeDMorGM,
selectDefaultChannelForTeam,
sortChannelsByDisplayName,
sortChannelsModelByDisplayName,
validateDisplayName,
} from './index';
import type ChannelModel from '@typings/database/models/servers/channel';
jest.mock('@utils/role', () => ({
hasPermission: jest.fn(() => true),
}));
jest.mock('@utils/general', () => ({
generateId: jest.fn(() => 'generated-id'),
}));
describe('getDirectChannelName', () => {
it('should return the correct direct channel name', () => {
expect(getDirectChannelName('id1', 'id2')).toBe('id1__id2');
expect(getDirectChannelName('id2', 'id1')).toBe('id1__id2');
});
});
describe('isDMorGM', () => {
it('should return true for DM or GM channels', () => {
expect(isDMorGM({type: General.DM_CHANNEL} as Channel)).toBe(true);
expect(isDMorGM({type: General.GM_CHANNEL} as Channel)).toBe(true);
});
it('should return false for other channel types', () => {
expect(isDMorGM({type: General.OPEN_CHANNEL} as Channel)).toBe(false);
});
});
describe('isTypeDMorGM', () => {
it('should return true for DM or GM channel types', () => {
expect(isTypeDMorGM(General.DM_CHANNEL)).toBe(true);
expect(isTypeDMorGM(General.GM_CHANNEL)).toBe(true);
});
it('should return false for other channel types', () => {
expect(isTypeDMorGM(General.OPEN_CHANNEL)).toBe(false);
expect(isTypeDMorGM(undefined)).toBe(false);
});
});
describe('isArchived', () => {
it('should return true if channel is archived', () => {
expect(isArchived({delete_at: 12345} as Channel)).toBe(true);
expect(isArchived({deleteAt: 12345} as ChannelModel)).toBe(true);
});
it('should return false if channel is not archived', () => {
expect(isArchived({delete_at: 0} as Channel)).toBe(false);
expect(isArchived({deleteAt: 0} as ChannelModel)).toBe(false);
});
});
describe('selectDefaultChannelForTeam', () => {
const channels = [
{id: '1', name: General.DEFAULT_CHANNEL, team_id: 'team1', type: General.OPEN_CHANNEL},
{id: '2', name: 'random', team_id: 'team1', type: General.OPEN_CHANNEL},
] as Channel[];
const memberships = [
{channel_id: '2'},
] as ChannelMembership[];
const roles = [
{permissions: [Permissions.JOIN_PUBLIC_CHANNELS]},
] as Role[];
it('should select the default channel if user is a member', () => {
const result = selectDefaultChannelForTeam(channels, memberships, 'team1', roles);
expect(result).toEqual(channels[0]);
const channelModels = [
{id: '1', name: General.DEFAULT_CHANNEL, teamId: 'team1', type: General.OPEN_CHANNEL},
{id: '2', name: 'random', teamId: 'team1', type: General.OPEN_CHANNEL},
] as ChannelModel[];
const result2 = selectDefaultChannelForTeam(channelModels, memberships, 'team1', roles);
expect(result2).toEqual(channelModels[0]);
});
it('should select the first channel in the team if user can join public channels', () => {
const result = selectDefaultChannelForTeam(channels, [], 'team1', roles);
expect(result).toEqual(channels[0]);
});
it('should select the first channel in the team if user cannot join public channels', () => {
(hasPermission as jest.Mock).mockReturnValueOnce(false);
const result = selectDefaultChannelForTeam(channels, memberships, 'team1', roles);
expect(result).toEqual(channels[1]);
});
it('should select the default channel in the team if user cannot join public channels', () => {
(hasPermission as jest.Mock).mockReturnValueOnce(false);
const memberOf = [
{channel_id: '1'},
] as ChannelMembership[];
const result = selectDefaultChannelForTeam(channels, memberOf, 'team1', roles);
expect(result).toEqual(channels[0]);
});
it('should return undefined if no channels are available', () => {
const result = selectDefaultChannelForTeam([], [], 'team1', roles);
expect(result).toBeUndefined();
});
});
describe('sortChannelsByDisplayName', () => {
it('should sort channels by display name', () => {
const channels = [
{name: 'name1', display_name: 'Zeta'},
{name: 'name2', display_name: 'Alpha'},
] as Channel[];
const channelsModels = [
{name: 'name1', displayName: 'Zeta'},
{name: 'name2', displayName: 'Alpha'},
] as ChannelModel[];
const result = channels.sort((a, b) => sortChannelsByDisplayName('en', a, b));
expect(result[0].name).toBe('name2');
const result2 = channelsModels.sort((a, b) => sortChannelsByDisplayName('en', a, b));
expect(result2[0].name).toBe('name2');
});
it('should sort channels by name if display name is not defined', () => {
const channels = [
{name: 'Zeta'},
{name: 'Alpha'},
] as Channel[];
const result = channels.sort((a, b) => sortChannelsByDisplayName('en', a, b));
expect(result[0].name).toBe('Alpha');
});
});
describe('sortChannelsModelByDisplayName', () => {
it('should sort channel models by display name', () => {
const channels = [
{name: 'name1', displayName: 'Zeta'},
{name: 'name2', displayName: 'Alpha'},
] as ChannelModel[];
const result = channels.sort((a, b) => sortChannelsModelByDisplayName('en', a, b));
expect(result[0].name).toBe('name2');
});
it('should sort channel models by name if display name is not defined', () => {
const channels = [
{name: 'Zeta'},
{name: 'Alpha'},
] as ChannelModel[];
const result = channels.sort((a, b) => sortChannelsModelByDisplayName('en', a, b));
expect(result[0].name).toBe('Alpha');
});
});
describe('validateDisplayName', () => {
const intl = createIntl({locale: 'en', messages: {}});
it('should return an error if display name is empty', () => {
const result = validateDisplayName(intl, '');
expect(result.error).toBe('Channel name is required');
});
it('should return an error if display name is too long', () => {
const result = validateDisplayName(intl, 'a'.repeat(100));
expect(result.error).toBe('Channel name must be less than 64 characters');
});
it('should return an error if display name is too short', () => {
const minLength = Channel.MIN_CHANNEL_NAME_LENGTH;
Channel.MIN_CHANNEL_NAME_LENGTH = 2;
const result = validateDisplayName(intl, 'a');
expect(result.error).toBe('Channel name must be 2 or more characters');
Channel.MIN_CHANNEL_NAME_LENGTH = minLength;
});
it('should return no error if display name is valid', () => {
const result = validateDisplayName(intl, 'valid name');
expect(result.error).toBe('');
});
});
describe('generateChannelNameFromDisplayName', () => {
it('should generate a channel name from display name', () => {
const result = generateChannelNameFromDisplayName('Valid Name');
expect(result).toBe('valid-name');
});
it('should generate an ID if display name is empty after cleanup', () => {
const result = generateChannelNameFromDisplayName('');
expect(result).toBe('generated-id');
});
});
describe('compareNotifyProps', () => {
it('should return true if notify props are equal', () => {
const propsA: Partial<ChannelNotifyProps> = {desktop: 'default', email: 'default', mark_unread: 'all', push: 'default', ignore_channel_mentions: 'default'};
const propsB = {...propsA};
expect(compareNotifyProps(propsA, propsB)).toBe(true);
});
it('should return false if notify props are different', () => {
const propsA: Partial<ChannelNotifyProps> = {desktop: 'default', email: 'default', mark_unread: 'all', push: 'default', ignore_channel_mentions: 'default'};
const propsB: Partial<ChannelNotifyProps> = {...propsA, desktop: 'none'};
expect(compareNotifyProps(propsA, propsB)).toBe(false);
});
});
describe('filterChannelsMatchingTerm', () => {
const channels = [
{name: 'channel1', display_name: 'Channel One'},
{name: 'channel2', display_name: 'Channel Two'},
{display_name: 'Undefined name'},
{},
] as Channel[];
it('should filter channels by name or display name', () => {
let result = filterChannelsMatchingTerm(channels, 'channel1');
expect(result.length).toBe(1);
expect(result[0].name).toBe('channel1');
result = filterChannelsMatchingTerm(channels, 'Channel One');
expect(result.length).toBe(1);
expect(result[0].display_name).toBe('Channel One');
result = filterChannelsMatchingTerm([...channels, undefined] as Channel[], 'Channel One');
expect(result.length).toBe(1);
expect(result[0].display_name).toBe('Channel One');
});
it('should return an empty array if no channels match the term', () => {
const result = filterChannelsMatchingTerm(channels, 'nonexistent');
expect(result.length).toBe(0);
});
});
describe('isDefaultChannel', () => {
it('should return true if channel is the default channel', () => {
const channel = {name: General.DEFAULT_CHANNEL} as Channel; // using casting instead of TestHelper to avoid circular dependency
expect(isDefaultChannel(channel)).toBe(true);
});
it('should return false if channel is not the default channel', () => {
const channel = {name: 'random'} as Channel; // using casting instead of TestHelper to avoid circular dependency
expect(isDefaultChannel(channel)).toBe(false);
});
it('should return false if channel is undefined', () => {
expect(isDefaultChannel(undefined)).toBe(false);
});
});