import { DefaultTheme, ThemeProvider } from '@react-navigation/native' import { QueryClientProvider } from '@tanstack/react-query' import { fireEvent, render } from '@testing-library/react-native' import React from 'react' import { Alert } from 'react-native' import { SafeAreaProvider } from 'react-native-safe-area-context' import { buildTestQueryClient } from '../../__utils__/query_client' import type { ConversationResource } from '../../types' import { ConversationDetailsScreen } from '../conversation_details_screen' const baseConversation = { type: 'Conversation', id: 7, title: 'Youth Group', muted: false, repliesDisabled: false, unreadCount: 0, unreadReaction: false, conversationPreview: null, createdAt: '2026-01-01T00:00:00Z', updatedAt: '2026-01-01T00:00:00Z', badges: [], groups: [], disabled: false, disabledReason: null, disabledMessage: null, memberAbility: { type: 'MemberAbility', canUpdate: true, canDelete: true, canDeleteNonAuthoredMessages: true, leader: true, canReply: true, canMessageAvailableMembers: false, }, conversationMembership: { notificationLevelDescription: 'All messages' }, } as unknown as ConversationResource const archivedConversation = { ...baseConversation, disabled: true, disabledReason: 'archived', disabledMessage: 'This conversation is archived. Only you and other leaders can unarchive.', memberAbility: { ...baseConversation.memberAbility, canUpdate: false }, } as ConversationResource const mockNavigation = { navigate: jest.fn(), dispatch: jest.fn(), goBack: jest.fn(), setOptions: jest.fn(), } jest.mock('@react-navigation/native', () => ({ ...jest.requireActual('@react-navigation/native'), useNavigation: () => mockNavigation, })) jest.mock('../../hooks/use_api_client', () => ({ useApiClient: () => ({}) })) let mockConversation: ConversationResource const mockArchive = jest.fn() const mockSaveTitle = jest.fn() jest.mock('../../hooks/use_conversation', () => ({ useConversation: () => ({ data: mockConversation }), useConversationArchive: () => ({ setArchived: mockArchive, isPending: false }), useConversationDelete: () => ({ mutate: jest.fn() }), useConversationDisableReplies: () => ({ repliesDisabled: false, setRepliesDisabled: jest.fn() }), useConversationMute: () => ({ muted: false, setMuted: jest.fn() }), useConversationUpdate: () => ({ mutate: mockSaveTitle }), })) let mockEnabledFeatures: string[] = [] jest.mock('../../hooks/use_features', () => ({ ...jest.requireActual('../../hooks/use_features'), useFeatures: () => ({ features: [], featureEnabled: (name: string) => mockEnabledFeatures.includes(name), }), })) jest.mock('../../hooks/use_suspense_api', () => ({ ...jest.requireActual('../../hooks/use_suspense_api'), useSuspensePaginator: () => ({ data: [], fetchNextPage: jest.fn(), isFetchingNextPage: false }), })) let mockPressDone = () => {} jest.mock('../../components/display/platform_modal_header_buttons', () => ({ ...jest.requireActual('../../components/display/platform_modal_header_buttons'), useHeaderRight: ({ onPress }: { onPress: () => void }) => { mockPressDone = onPress }, })) jest.mock('../../components/display/conversation_avatar', () => ({ ConversationAvatar: () => null, })) const buildConversation = (canArchive: boolean) => ({ type: 'Conversation', id: 7, title: 'Youth Group', memberAbility: { type: 'MemberAbility', canArchive, canDelete: false }, groups: [], badges: [], }) as unknown as ConversationResource const safeAreaMetrics = { frame: { x: 0, y: 0, width: 390, height: 844 }, insets: { top: 0, right: 0, bottom: 0, left: 0 }, } const renderScreen = () => render( ) const alertButton = (text: string) => { const buttons = jest.mocked(Alert.alert).mock.calls[0][2] ?? [] return buttons.find(button => button.text === text) } describe('ConversationDetailsScreen', () => { beforeEach(() => { mockConversation = baseConversation mockEnabledFeatures = [] jest.clearAllMocks() }) describe('for an active conversation', () => { it('offers mute and the leader settings', () => { mockEnabledFeatures = ['ROLLOUT_custom_conversation_avatars'] const { getByText } = renderScreen() expect(getByText('Mute')).toBeTruthy() expect(getByText('Only leaders can message')).toBeTruthy() expect(getByText('Update avatar')).toBeTruthy() }) it('offers notification levels when granular notifications are on', () => { mockEnabledFeatures = ['ROLLOUT_granular_notification_preferences_ui'] const { getByText } = renderScreen() expect(getByText('Notifications')).toBeTruthy() }) it('saves the title on done', () => { renderScreen() mockPressDone() expect(mockSaveTitle).toHaveBeenCalledWith({ title: 'Youth Group' }) }) it('renders a conversation with no title and asks for one', () => { mockConversation = { ...baseConversation, title: null } const { getByText, getByDisplayValue } = renderScreen() expect(getByDisplayValue('')).toBeTruthy() expect(getByText('A title is required for your conversation.')).toBeTruthy() }) it('does not save a cleared title on done', () => { const { getByDisplayValue } = renderScreen() fireEvent.changeText(getByDisplayValue('Youth Group'), ' ') mockPressDone() expect(mockSaveTitle).not.toHaveBeenCalled() }) }) describe('for an archived conversation', () => { beforeEach(() => { mockConversation = archivedConversation }) it('replaces mute and the leader settings with the archived notice', () => { mockEnabledFeatures = ['ROLLOUT_custom_conversation_avatars'] const { getByText, queryByText } = renderScreen() expect( getByText('This conversation is archived. Only you and other leaders can unarchive.') ).toBeTruthy() expect(queryByText('Mute')).toBeNull() expect(queryByText('Only leaders can message')).toBeNull() expect(queryByText('Update avatar')).toBeNull() expect(getByText('Delete conversation…')).toBeTruthy() }) it('falls back to a default notice when the server sends no message', () => { mockConversation = { ...archivedConversation, disabledMessage: null } const { getByText } = renderScreen() expect(getByText('This conversation is archived.')).toBeTruthy() }) it('hides notification levels even when granular notifications are on', () => { mockEnabledFeatures = ['ROLLOUT_granular_notification_preferences_ui'] const { queryByText } = renderScreen() expect(queryByText('Notifications')).toBeNull() }) it('locks the title', () => { const { getByDisplayValue } = renderScreen() expect(getByDisplayValue('Youth Group').props.editable).toBe(false) }) it('does not save the title on done', () => { renderScreen() mockPressDone() expect(mockSaveTitle).not.toHaveBeenCalled() }) it('unarchives from the notice for someone who can unarchive', () => { mockEnabledFeatures = ['ROLLOUT_MOBILE_manual_archiving'] mockConversation = { ...archivedConversation, memberAbility: { ...archivedConversation.memberAbility!, canUnarchive: true }, } const { getByText } = renderScreen() fireEvent.press(getByText('Unarchive')) expect(mockArchive).toHaveBeenCalledWith(false) }) }) }) describe('ConversationDetailsScreen archive', () => { beforeEach(() => { jest.clearAllMocks() jest.spyOn(Alert, 'alert').mockImplementation(() => {}) mockConversation = buildConversation(true) mockEnabledFeatures = ['ROLLOUT_MOBILE_manual_archiving'] }) afterEach(() => { jest.restoreAllMocks() }) it('hides archiving when the member cannot archive', () => { mockConversation = buildConversation(false) const { queryByText } = renderScreen() expect(queryByText('Archive conversation…')).toBeNull() }) it.each(['QA_MOBILE_manual_archiving', 'ROLLOUT_MOBILE_manual_archiving'])( 'offers archiving with only %s on', flag => { mockEnabledFeatures = [flag] const { getByText } = renderScreen() expect(getByText('Archive conversation…')).toBeTruthy() } ) it('hides archiving when neither mobile flag is on', () => { mockEnabledFeatures = ['ROLLOUT_WEB_manual_archiving'] const { queryByText } = renderScreen() expect(queryByText('Archive conversation…')).toBeNull() }) it('asks for confirmation before archiving', () => { const { getByText } = renderScreen() fireEvent.press(getByText('Archive conversation…')) expect(Alert.alert).toHaveBeenCalledWith( 'Archive conversation', 'This conversation will be archived for everyone. Only its leaders and creator can open and unarchive it.', expect.any(Array) ) expect(mockArchive).not.toHaveBeenCalled() }) it('archives once confirmed and stays on the details', () => { const { getByText } = renderScreen() fireEvent.press(getByText('Archive conversation…')) alertButton('Archive')?.onPress?.() expect(mockArchive).toHaveBeenCalledTimes(1) expect(mockArchive).toHaveBeenCalledWith(true) expect(mockNavigation.dispatch).not.toHaveBeenCalled() expect(mockNavigation.goBack).not.toHaveBeenCalled() expect(mockNavigation.navigate).not.toHaveBeenCalled() }) it('does nothing when the confirmation is cancelled', () => { const { getByText } = renderScreen() fireEvent.press(getByText('Archive conversation…')) alertButton('Cancel')?.onPress?.() expect(mockArchive).not.toHaveBeenCalled() }) })