import { HeaderHeightContext } from '@react-navigation/elements' import { DefaultTheme, ThemeProvider } from '@react-navigation/native' import { QueryClientProvider } from '@tanstack/react-query' import { fireEvent, render, waitFor } from '@testing-library/react-native' import React from 'react' import { SafeAreaProvider } from 'react-native-safe-area-context' import { buildTestQueryClient } from '../../__utils__/query_client' import { MessageActionsScreen, MessageActionsScreenProps, } from '../../screens/message_actions_screen' import { ConversationResource, MessageResource } from '../../types' import { AudioAdapter, ChatAdapters, ClipboardAdapter, HapticAdapter, ImagePickerAdapter, VideoAdapter, } from '../../utils/native_adapters' const mockMessage: MessageResource = { type: 'Message', id: '1', text: 'Copy me', html: '

Copy me

', createdAt: '2026-01-01T00:00:00Z', deletedAt: null, textEditedAt: null, mine: false, attachments: [], author: { id: 1, type: 'Person', name: 'Author', avatar: '' }, reactionCounts: [], replyCount: 0, replyRootId: null, messageType: 'user', } const myMessageWithLinkPreview = { ...mockMessage, mine: true, attachments: [{ type: 'ExpandedLink', id: '9' }], } as MessageResource let mockCurrentMessage = mockMessage let mockConversation = { id: 1, disabled: false } as ConversationResource let mockEnabledFeatures: string[] = [] const mockGoBack = jest.fn() jest.mock('@react-navigation/native', () => ({ ...jest.requireActual('@react-navigation/native'), useNavigation: () => ({ navigate: jest.fn(), dispatch: jest.fn(), goBack: mockGoBack }), })) jest.mock('../../hooks/use_api_client', () => ({ useApiClient: () => ({}) })) jest.mock('../../hooks/use_conversation', () => ({ useConversation: () => ({ data: mockConversation }), })) jest.mock('../../hooks/use_features', () => ({ ...jest.requireActual('../../hooks/use_features'), useFeatures: () => ({ features: [], featureEnabled: (name: string) => mockEnabledFeatures.includes(name), }), })) jest.mock('../../hooks/use_message_reaction_toggle', () => ({ useMessageReactionToggle: () => ({ handleReactionToggle: jest.fn(), isPending: false }), })) jest.mock('../../hooks/use_conversation_messages', () => ({ useConversationMessages: () => ({ messages: [mockCurrentMessage], refetch: jest.fn() }), })) const setStringAsync = jest.fn(async (_: string) => {}) const notificationSuccess = jest.fn() const safeAreaMetrics = { frame: { x: 0, y: 0, width: 390, height: 844 }, insets: { top: 0, right: 0, bottom: 0, left: 0 }, } const route = { key: 'MessageActions', name: 'MessageActions', params: { conversation_id: 1, message_id: mockMessage.id }, } as MessageActionsScreenProps['route'] const renderScreen = () => render( ) describe('MessageActionsScreen', () => { beforeAll(() => { ChatAdapters.configure({ clipboard: new ClipboardAdapter({ getStringAsync: async () => '', setStringAsync }), audio: new AudioAdapter({ useAudio: () => ({}) as any }), video: new VideoAdapter({ Player: React.forwardRef(() => null) }), imagePicker: new ImagePickerAdapter({ openCameraAsync: async () => ({ canceled: true, assets: null }), openImageLibraryAsync: async () => ({ canceled: true, assets: null }), }), haptic: new HapticAdapter({ impactLight: jest.fn(), impactMedium: jest.fn(), impactHeavy: jest.fn(), rigid: jest.fn(), soft: jest.fn(), notificationSuccess, notificationWarning: jest.fn(), notificationError: jest.fn(), }), }) }) beforeEach(() => { jest.clearAllMocks() mockCurrentMessage = mockMessage mockConversation = { id: 1, disabled: false } as ConversationResource mockEnabledFeatures = [] }) it('offers Delete on my own message', () => { mockCurrentMessage = { ...mockMessage, mine: true } const { getByText } = renderScreen() expect(getByText('Delete message')).toBeTruthy() }) it('hides Delete when the conversation is disabled', () => { mockCurrentMessage = { ...mockMessage, mine: true } mockConversation = { id: 1, disabled: true, disabledReason: 'locked' } as ConversationResource const { queryByText, getByText } = renderScreen() expect(queryByText('Delete message')).toBeNull() expect(getByText('Copy text')).toBeTruthy() }) it('offers reactions, reply, edit, and link preview removal on my message', () => { mockCurrentMessage = myMessageWithLinkPreview const { getByText } = renderScreen() expect(getByText('👍')).toBeTruthy() expect(getByText('Reply to message')).toBeTruthy() expect(getByText('Edit message')).toBeTruthy() expect(getByText('Remove link preview')).toBeTruthy() }) it('hides reactions, reply, edit, and link preview removal when the conversation is disabled', () => { mockCurrentMessage = myMessageWithLinkPreview mockConversation = { id: 1, disabled: true, disabledReason: 'archived' } as ConversationResource const { getByText, queryByText } = renderScreen() expect(queryByText('👍')).toBeNull() expect(queryByText('Reply to message')).toBeNull() expect(queryByText('Edit message')).toBeNull() expect(queryByText('Remove link preview')).toBeNull() expect(getByText('Copy text')).toBeTruthy() expect(getByText('View read receipts')).toBeTruthy() }) it('keeps Report when the conversation is disabled', () => { mockEnabledFeatures = ['ROLLOUT_MOBILE_message_reporting'] mockConversation = { id: 1, disabled: true, disabledReason: 'archived' } as ConversationResource const { getByText } = renderScreen() expect(getByText('Report message')).toBeTruthy() }) it('copies the message text and gives success haptic feedback before closing', async () => { const { getByText } = renderScreen() fireEvent.press(getByText('Copy text')) expect(setStringAsync).toHaveBeenCalledWith('Copy me') await waitFor(() => expect(mockGoBack).toHaveBeenCalledTimes(1)) expect(notificationSuccess).toHaveBeenCalledTimes(1) expect(notificationSuccess.mock.invocationCallOrder[0]).toBeLessThan( mockGoBack.mock.invocationCallOrder[0] ) }) it('leaves the clipboard alone and gives no haptic when there is nothing to copy', async () => { mockCurrentMessage = { ...mockMessage, text: null } const { getByText } = renderScreen() fireEvent.press(getByText('Copy text')) await waitFor(() => expect(mockGoBack).toHaveBeenCalledTimes(1)) expect(setStringAsync).not.toHaveBeenCalled() expect(notificationSuccess).not.toHaveBeenCalled() }) })