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