From cd76d0717a2fb443671732f566a40e253a0b0a51 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Tue, 14 Oct 2025 19:43:29 -0700 Subject: [PATCH 1/4] test: add comprehensive unit tests for IterableInboxMessageDisplay component --- .../IterableInboxMessageDisplay.test.tsx | 748 ++++++++++++++++++ 1 file changed, 748 insertions(+) create mode 100644 src/inbox/components/IterableInboxMessageDisplay.test.tsx diff --git a/src/inbox/components/IterableInboxMessageDisplay.test.tsx b/src/inbox/components/IterableInboxMessageDisplay.test.tsx new file mode 100644 index 000000000..93d0eb501 --- /dev/null +++ b/src/inbox/components/IterableInboxMessageDisplay.test.tsx @@ -0,0 +1,748 @@ +import { fireEvent, render, waitFor } from '@testing-library/react-native'; + +import { IterableEdgeInsets } from '../../core'; +import { IterableInAppMessage, IterableInAppTrigger, IterableInboxMetadata } from '../../inApp/classes'; +import { IterableHtmlInAppContent } from '../../inApp/classes/IterableHtmlInAppContent'; +import { IterableInAppTriggerType } from '../../inApp/enums'; +import type { IterableInboxRowViewModel } from '../types'; +import { IterableInboxMessageDisplay } from './IterableInboxMessageDisplay'; + +// Suppress act() warnings for this test suite since they're expected from the component's useEffect +const originalError = console.error; +beforeAll(() => { + console.error = jest.fn(); +}); + +afterAll(() => { + console.error = originalError; +}); + +// Mock the Iterable class +jest.mock('../../core/classes/Iterable', () => ({ + Iterable: { + trackInAppClick: jest.fn(), + trackInAppClose: jest.fn(), + savedConfig: { + customActionHandler: jest.fn(), + urlHandler: jest.fn(), + }, + }, +})); + +// Mock react-native-vector-icons +jest.mock('react-native-vector-icons/Ionicons', () => { + const { View } = require('react-native'); + const MockIcon = ({ testID, ...props }: { testID?: string; [key: string]: unknown }) => ( + + ); + MockIcon.displayName = 'MockIcon'; + return MockIcon; +}); + +// Mock Linking +jest.mock('react-native', () => ({ + ...jest.requireActual('react-native'), + Linking: { + openURL: jest.fn(), + }, +})); + +// Mock WebView +jest.mock('react-native-webview', () => { + const { View, Text } = require('react-native'); + + const MockWebView = ({ onMessage, injectedJavaScript, source, ...props }: { + onMessage?: (event: { nativeEvent: { data: string } }) => void; + injectedJavaScript?: string; + source?: { html: string }; + [key: string]: unknown; + }) => ( + + {source?.html} + {injectedJavaScript} + { + if (onMessage) { + onMessage({ + nativeEvent: { + data: 'https://example.com', + }, + }); + } + }} + > + Trigger Message + + { + if (onMessage) { + onMessage({ + nativeEvent: { + data: 'iterable://delete', + }, + }); + } + }} + > + Trigger Delete + + { + if (onMessage) { + onMessage({ + nativeEvent: { + data: 'iterable://dismiss', + }, + }); + } + }} + > + Trigger Dismiss + + { + if (onMessage) { + onMessage({ + nativeEvent: { + data: 'action://customAction', + }, + }); + } + }} + > + Trigger Custom Action + + { + if (onMessage) { + onMessage({ + nativeEvent: { + data: 'myapp://deep-link', + }, + }); + } + }} + > + Trigger Deep Link + + + ); + + MockWebView.displayName = 'MockWebView'; + + return { + WebView: MockWebView, + }; +}); + +describe('IterableInboxMessageDisplay', () => { + const mockMessage = new IterableInAppMessage( + 'test-message-id', + 123, + new IterableInAppTrigger(IterableInAppTriggerType.immediate), + new Date('2023-01-01T00:00:00Z'), + undefined, + true, + new IterableInboxMetadata('Test Message Title', 'Test Subtitle', 'test-image.png'), + undefined, + false, + 0 + ); + + const mockRowViewModel: IterableInboxRowViewModel = { + inAppMessage: mockMessage, + title: 'Test Message Title', + subtitle: 'Test Subtitle', + imageUrl: 'test-image.png', + read: false, + createdAt: new Date('2023-01-01T00:00:00Z'), + }; + + const mockHtmlContent = new IterableHtmlInAppContent( + new IterableEdgeInsets(10, 10, 10, 10), + '

Test HTML Content

Test Link' + ); + + const defaultProps = { + rowViewModel: mockRowViewModel, + inAppContentPromise: Promise.resolve(mockHtmlContent), + returnToInbox: jest.fn(), + deleteRow: jest.fn(), + contentWidth: 300, + isPortrait: true, + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('Basic Rendering', () => { + it('should render without crashing with valid props', () => { + expect(() => render()).not.toThrow(); + }); + + it('should render the message title', () => { + const { getByText } = render(); + expect(getByText('Test Message Title')).toBeTruthy(); + }); + + it('should render the return button with "Inbox" text', () => { + const { getByText } = render(); + expect(getByText('Inbox')).toBeTruthy(); + }); + + it('should render the return button icon', () => { + const { getByTestId } = render(); + expect(getByTestId('Icon')).toBeTruthy(); + }); + + it('should handle missing message title gracefully', () => { + const messageWithoutTitle = new IterableInAppMessage( + 'test-message-id', + 123, + new IterableInAppTrigger(IterableInAppTriggerType.immediate), + new Date('2023-01-01T00:00:00Z'), + undefined, + true, + undefined, // No inbox metadata + undefined, + false, + 0 + ); + + const rowViewModelWithoutTitle: IterableInboxRowViewModel = { + ...mockRowViewModel, + inAppMessage: messageWithoutTitle, + }; + + const propsWithoutTitle = { + ...defaultProps, + rowViewModel: rowViewModelWithoutTitle, + }; + + expect(() => render()).not.toThrow(); + }); + }); + + describe('Async Content Loading', () => { + it('should show content after inAppContentPromise resolves', async () => { + const { getByTestId } = render(); + + await waitFor(() => { + expect(getByTestId('webview')).toBeTruthy(); + }); + + expect(getByTestId('webview-source')).toHaveTextContent('

Test HTML Content

Test Link'); + }); + + it('should handle rejected inAppContentPromise', async () => { + // Since the component doesn't handle promise rejections, we'll test that it doesn't crash + // when the promise never resolves (which simulates a network failure) + const neverResolvingPromise = new Promise(() => { + // Never resolve or reject - simulates a hanging network request + }); + + const propsWithNeverResolvingPromise = { + ...defaultProps, + inAppContentPromise: neverResolvingPromise, + }; + + const { queryByTestId } = render(); + + // Component should render without crashing + // The component always renders the header, so we can check that the WebView is not shown + // since the promise never resolves and inAppContent remains undefined + expect(queryByTestId('webview')).toBeFalsy(); + }); + + it('should handle component unmounting before promise resolves', async () => { + const slowPromise = new Promise((resolve) => { + setTimeout(() => resolve(mockHtmlContent), 1000); + }); + + const propsWithSlowPromise = { + ...defaultProps, + inAppContentPromise: slowPromise, + }; + + const { unmount } = render(); + + // Unmount before promise resolves + unmount(); + + // Should not crash + await waitFor(() => { + expect(true).toBe(true); // Just ensure we don't crash + }); + }); + }); + + describe('Return Button Interaction', () => { + it('should call returnToInbox when return button is pressed', () => { + const mockReturnToInbox = jest.fn(); + const propsWithMockReturn = { + ...defaultProps, + returnToInbox: mockReturnToInbox, + }; + + const { getByText } = render(); + const returnButton = getByText('Inbox'); + + fireEvent.press(returnButton); + + expect(mockReturnToInbox).toHaveBeenCalledTimes(1); + expect(mockReturnToInbox).toHaveBeenCalledWith(); + }); + + it('should track in-app close with back source when return button is pressed', () => { + const { Iterable } = require('../../core/classes/Iterable'); + const { getByText } = render(); + const returnButton = getByText('Inbox'); + + fireEvent.press(returnButton); + + expect(Iterable.trackInAppClose).toHaveBeenCalledWith( + mockRowViewModel.inAppMessage, + 1, // IterableInAppLocation.inbox + 0 // IterableInAppCloseSource.back + ); + }); + }); + + describe('WebView Message Handling', () => { + it('should handle external HTTP links', async () => { + const mockReturnToInbox = jest.fn(); + const propsWithMockReturn = { + ...defaultProps, + returnToInbox: mockReturnToInbox, + }; + + const { getByTestId } = render(); + + await waitFor(() => { + expect(getByTestId('webview')).toBeTruthy(); + }); + + const messageTrigger = getByTestId('webview-message-trigger'); + fireEvent.press(messageTrigger); + + expect(mockReturnToInbox).toHaveBeenCalledWith(expect.any(Function)); + }); + + it('should handle delete action', async () => { + const mockReturnToInbox = jest.fn(); + const mockDeleteRow = jest.fn(); + const propsWithMocks = { + ...defaultProps, + returnToInbox: mockReturnToInbox, + deleteRow: mockDeleteRow, + }; + + const { getByTestId } = render(); + + await waitFor(() => { + expect(getByTestId('webview')).toBeTruthy(); + }); + + const deleteTrigger = getByTestId('webview-delete-trigger'); + fireEvent.press(deleteTrigger); + + expect(mockReturnToInbox).toHaveBeenCalledWith(expect.any(Function)); + }); + + it('should handle dismiss action', async () => { + const mockReturnToInbox = jest.fn(); + const propsWithMockReturn = { + ...defaultProps, + returnToInbox: mockReturnToInbox, + }; + + const { getByTestId } = render(); + + await waitFor(() => { + expect(getByTestId('webview')).toBeTruthy(); + }); + + const dismissTrigger = getByTestId('webview-dismiss-trigger'); + fireEvent.press(dismissTrigger); + + expect(mockReturnToInbox).toHaveBeenCalledWith(); + }); + + it('should handle custom action', async () => { + const mockReturnToInbox = jest.fn(); + const { Iterable } = require('../../core/classes/Iterable'); + const mockCustomActionHandler = jest.fn(); + Iterable.savedConfig.customActionHandler = mockCustomActionHandler; + + const propsWithMockReturn = { + ...defaultProps, + returnToInbox: mockReturnToInbox, + }; + + const { getByTestId } = render(); + + await waitFor(() => { + expect(getByTestId('webview')).toBeTruthy(); + }); + + const customActionTrigger = getByTestId('webview-custom-action-trigger'); + fireEvent.press(customActionTrigger); + + expect(mockReturnToInbox).toHaveBeenCalledWith(expect.any(Function)); + }); + + it('should handle deep link', async () => { + const mockReturnToInbox = jest.fn(); + const { Iterable } = require('../../core/classes/Iterable'); + const mockUrlHandler = jest.fn(); + Iterable.savedConfig.urlHandler = mockUrlHandler; + + const propsWithMockReturn = { + ...defaultProps, + returnToInbox: mockReturnToInbox, + }; + + const { getByTestId } = render(); + + await waitFor(() => { + expect(getByTestId('webview')).toBeTruthy(); + }); + + const deepLinkTrigger = getByTestId('webview-deep-link-trigger'); + fireEvent.press(deepLinkTrigger); + + expect(mockReturnToInbox).toHaveBeenCalledWith(expect.any(Function)); + }); + }); + + describe('Tracking and Analytics', () => { + it('should track in-app click when link is clicked', async () => { + const { Iterable } = require('../../core/classes/Iterable'); + const { getByTestId } = render(); + + await waitFor(() => { + expect(getByTestId('webview')).toBeTruthy(); + }); + + const messageTrigger = getByTestId('webview-message-trigger'); + fireEvent.press(messageTrigger); + + expect(Iterable.trackInAppClick).toHaveBeenCalledWith( + mockRowViewModel.inAppMessage, + 1, // IterableInAppLocation.inbox + 'https://example.com' + ); + }); + + it('should track in-app close with link source when link is clicked', async () => { + const { Iterable } = require('../../core/classes/Iterable'); + const { getByTestId } = render(); + + await waitFor(() => { + expect(getByTestId('webview')).toBeTruthy(); + }); + + const messageTrigger = getByTestId('webview-message-trigger'); + fireEvent.press(messageTrigger); + + expect(Iterable.trackInAppClose).toHaveBeenCalledWith( + mockRowViewModel.inAppMessage, + 1, // IterableInAppLocation.inbox + 1, // IterableInAppCloseSource.link + 'https://example.com' + ); + }); + }); + + describe('Props Variations', () => { + it('should handle different content widths', () => { + const propsWithDifferentWidth = { ...defaultProps, contentWidth: 600 }; + expect(() => render()).not.toThrow(); + }); + + it('should handle portrait mode', () => { + const portraitProps = { ...defaultProps, isPortrait: true }; + expect(() => render()).not.toThrow(); + }); + + it('should handle landscape mode', () => { + const landscapeProps = { ...defaultProps, isPortrait: false }; + expect(() => render()).not.toThrow(); + }); + + it('should handle zero content width', () => { + const zeroWidthProps = { ...defaultProps, contentWidth: 0 }; + expect(() => render()).not.toThrow(); + }); + + it('should handle negative content width', () => { + const negativeWidthProps = { ...defaultProps, contentWidth: -100 }; + expect(() => render()).not.toThrow(); + }); + + it('should handle very large content width', () => { + const largeWidthProps = { ...defaultProps, contentWidth: 2000 }; + expect(() => render()).not.toThrow(); + }); + }); + + describe('Function Props', () => { + it('should handle returnToInbox function', () => { + const mockReturnToInbox = jest.fn(); + const propsWithReturnToInbox = { ...defaultProps, returnToInbox: mockReturnToInbox }; + + expect(() => render()).not.toThrow(); + }); + + it('should handle deleteRow function', () => { + const mockDeleteRow = jest.fn(); + const propsWithDeleteRow = { ...defaultProps, deleteRow: mockDeleteRow }; + + expect(() => render()).not.toThrow(); + }); + + it('should handle undefined function props gracefully', () => { + const propsWithUndefinedFunctions = { + ...defaultProps, + returnToInbox: undefined as unknown as (callback?: () => void) => void, + deleteRow: undefined as unknown as (id: string) => void, + }; + + expect(() => render()).not.toThrow(); + }); + }); + + describe('WebView Configuration', () => { + it('should configure WebView with correct props', async () => { + const { getByTestId } = render(); + + await waitFor(() => { + expect(getByTestId('webview')).toBeTruthy(); + }); + + // Check that injected JavaScript is present + const jsContent = getByTestId('webview-js').props.children; + expect(jsContent).toContain('const links = document.querySelectorAll(\'a\')'); + expect(jsContent).toContain('links.forEach(link => {'); + expect(jsContent).toContain('window.ReactNativeWebView.postMessage'); + }); + + it('should set correct originWhiteList', async () => { + const { getByTestId } = render(); + + await waitFor(() => { + expect(getByTestId('webview')).toBeTruthy(); + }); + + // The WebView should be rendered with the correct configuration + expect(getByTestId('webview')).toBeTruthy(); + }); + }); + + describe('Edge Cases', () => { + it('should handle empty HTML content', async () => { + const emptyHtmlContent = new IterableHtmlInAppContent( + new IterableEdgeInsets(0, 0, 0, 0), + '' + ); + + const propsWithEmptyContent = { + ...defaultProps, + inAppContentPromise: Promise.resolve(emptyHtmlContent), + }; + + const { getByTestId } = render(); + + await waitFor(() => { + expect(getByTestId('webview')).toBeTruthy(); + }); + + expect(getByTestId('webview-source')).toHaveTextContent(''); + }); + + it('should handle HTML content with special characters', async () => { + const specialHtmlContent = new IterableHtmlInAppContent( + new IterableEdgeInsets(10, 10, 10, 10), + '

测试标题 🚀

测试链接' + ); + + const propsWithSpecialContent = { + ...defaultProps, + inAppContentPromise: Promise.resolve(specialHtmlContent), + }; + + const { getByTestId } = render(); + + await waitFor(() => { + expect(getByTestId('webview')).toBeTruthy(); + }); + + expect(getByTestId('webview-source')).toHaveTextContent('

测试标题 🚀

测试链接'); + }); + + it('should handle very long message titles', () => { + const longTitle = 'A'.repeat(1000); + const messageWithLongTitle = new IterableInAppMessage( + 'test-message-id', + 123, + new IterableInAppTrigger(IterableInAppTriggerType.immediate), + new Date('2023-01-01T00:00:00Z'), + undefined, + true, + new IterableInboxMetadata(longTitle, 'Test Subtitle', 'test-image.png'), + undefined, + false, + 0 + ); + + const rowViewModelWithLongTitle: IterableInboxRowViewModel = { + ...mockRowViewModel, + inAppMessage: messageWithLongTitle, + title: longTitle, + }; + + const propsWithLongTitle = { + ...defaultProps, + rowViewModel: rowViewModelWithLongTitle, + }; + + expect(() => render()).not.toThrow(); + }); + + it('should handle message with no inbox metadata', () => { + const messageWithoutMetadata = new IterableInAppMessage( + 'test-message-id', + 123, + new IterableInAppTrigger(IterableInAppTriggerType.immediate), + new Date('2023-01-01T00:00:00Z'), + undefined, + true, + undefined, // No inbox metadata + undefined, + false, + 0 + ); + + const rowViewModelWithoutMetadata: IterableInboxRowViewModel = { + ...mockRowViewModel, + inAppMessage: messageWithoutMetadata, + }; + + const propsWithoutMetadata = { + ...defaultProps, + rowViewModel: rowViewModelWithoutMetadata, + }; + + expect(() => render()).not.toThrow(); + }); + }); + + describe('Performance Considerations', () => { + it('should handle rapid prop changes', async () => { + const { rerender, getByTestId } = render(); + + await waitFor(() => { + expect(getByTestId('webview')).toBeTruthy(); + }); + + // Change props rapidly + const newProps1 = { ...defaultProps, contentWidth: 400 }; + const newProps2 = { ...defaultProps, isPortrait: false }; + const newProps3 = { ...defaultProps, contentWidth: 500, isPortrait: true }; + + expect(() => { + rerender(); + rerender(); + rerender(); + }).not.toThrow(); + }); + + it('should handle multiple message displays efficiently', () => { + const messages = Array.from({ length: 10 }, (_, i) => { + const message = new IterableInAppMessage( + `message-${i}`, + i, + new IterableInAppTrigger(IterableInAppTriggerType.immediate), + new Date(), + undefined, + true, + new IterableInboxMetadata(`Title ${i}`, `Subtitle ${i}`, `image${i}.png`), + undefined, + false, + i + ); + + return { + inAppMessage: message, + title: `Title ${i}`, + subtitle: `Subtitle ${i}`, + imageUrl: `image${i}.png`, + read: false, + createdAt: new Date(), + } as IterableInboxRowViewModel; + }); + + messages.forEach((rowViewModel) => { + const props = { + ...defaultProps, + rowViewModel, + }; + + expect(() => render()).not.toThrow(); + }); + }); + }); + + describe('Integration with Iterable SDK', () => { + it('should work with Iterable.savedConfig.customActionHandler', async () => { + const { Iterable } = require('../../core/classes/Iterable'); + const mockCustomActionHandler = jest.fn(); + Iterable.savedConfig.customActionHandler = mockCustomActionHandler; + + const { getByTestId } = render(); + + await waitFor(() => { + expect(getByTestId('webview')).toBeTruthy(); + }); + + // The component should render without errors when customActionHandler is set + expect(getByTestId('webview')).toBeTruthy(); + }); + + it('should work with Iterable.savedConfig.urlHandler', async () => { + const { Iterable } = require('../../core/classes/Iterable'); + const mockUrlHandler = jest.fn(); + Iterable.savedConfig.urlHandler = mockUrlHandler; + + const { getByTestId } = render(); + + await waitFor(() => { + expect(getByTestId('webview')).toBeTruthy(); + }); + + // The component should render without errors when urlHandler is set + expect(getByTestId('webview')).toBeTruthy(); + }); + + it('should handle missing Iterable.savedConfig handlers', async () => { + const { Iterable } = require('../../core/classes/Iterable'); + Iterable.savedConfig.customActionHandler = undefined; + Iterable.savedConfig.urlHandler = undefined; + + const { getByTestId } = render(); + + await waitFor(() => { + expect(getByTestId('webview')).toBeTruthy(); + }); + + // The component should render without errors when handlers are undefined + expect(getByTestId('webview')).toBeTruthy(); + }); + }); +}); From 5e12bb5c03e66b501c6b7c2cc9efd4adf42331fb Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Tue, 14 Oct 2025 19:46:33 -0700 Subject: [PATCH 2/4] test: enhance unit tests for IterableInboxMessageDisplay with additional scenarios --- .../IterableInboxMessageDisplay.test.tsx | 208 ++++++++++++++++++ 1 file changed, 208 insertions(+) diff --git a/src/inbox/components/IterableInboxMessageDisplay.test.tsx b/src/inbox/components/IterableInboxMessageDisplay.test.tsx index 93d0eb501..c24035a13 100644 --- a/src/inbox/components/IterableInboxMessageDisplay.test.tsx +++ b/src/inbox/components/IterableInboxMessageDisplay.test.tsx @@ -419,6 +419,214 @@ describe('IterableInboxMessageDisplay', () => { expect(mockReturnToInbox).toHaveBeenCalledWith(expect.any(Function)); }); + + // Additional comprehensive tests for the specific lines highlighted + it('should execute deleteRow callback when delete action is triggered', async () => { + const mockReturnToInbox = jest.fn(); + const mockDeleteRow = jest.fn(); + const propsWithMocks = { + ...defaultProps, + returnToInbox: mockReturnToInbox, + deleteRow: mockDeleteRow, + }; + + const { getByTestId } = render(); + + await waitFor(() => { + expect(getByTestId('webview')).toBeTruthy(); + }); + + const deleteTrigger = getByTestId('webview-delete-trigger'); + fireEvent.press(deleteTrigger); + + // Verify returnToInbox is called with a callback + expect(mockReturnToInbox).toHaveBeenCalledWith(expect.any(Function)); + + // Execute the callback to verify deleteRow is called with correct messageId + const callback = mockReturnToInbox.mock.calls[0][0]; + callback(); + expect(mockDeleteRow).toHaveBeenCalledWith('test-message-id'); + }); + + it('should call returnToInbox without callback for dismiss action', async () => { + const mockReturnToInbox = jest.fn(); + const propsWithMockReturn = { + ...defaultProps, + returnToInbox: mockReturnToInbox, + }; + + const { getByTestId } = render(); + + await waitFor(() => { + expect(getByTestId('webview')).toBeTruthy(); + }); + + const dismissTrigger = getByTestId('webview-dismiss-trigger'); + fireEvent.press(dismissTrigger); + + // Verify returnToInbox is called without any arguments (no callback) + expect(mockReturnToInbox).toHaveBeenCalledWith(); + }); + + it('should call Linking.openURL for HTTP URLs', async () => { + const mockReturnToInbox = jest.fn(); + const { Linking } = require('react-native'); + const propsWithMockReturn = { + ...defaultProps, + returnToInbox: mockReturnToInbox, + }; + + const { getByTestId } = render(); + + await waitFor(() => { + expect(getByTestId('webview')).toBeTruthy(); + }); + + const messageTrigger = getByTestId('webview-message-trigger'); + fireEvent.press(messageTrigger); + + // Verify returnToInbox is called with a callback + expect(mockReturnToInbox).toHaveBeenCalledWith(expect.any(Function)); + + // Execute the callback to verify Linking.openURL is called + const callback = mockReturnToInbox.mock.calls[0][0]; + callback(); + expect(Linking.openURL).toHaveBeenCalledWith('https://example.com'); + }); + + it('should call customActionHandler with correct action and context', async () => { + const mockReturnToInbox = jest.fn(); + const { Iterable } = require('../../core/classes/Iterable'); + const mockCustomActionHandler = jest.fn(); + Iterable.savedConfig.customActionHandler = mockCustomActionHandler; + + const propsWithMockReturn = { + ...defaultProps, + returnToInbox: mockReturnToInbox, + }; + + const { getByTestId } = render(); + + await waitFor(() => { + expect(getByTestId('webview')).toBeTruthy(); + }); + + const customActionTrigger = getByTestId('webview-custom-action-trigger'); + fireEvent.press(customActionTrigger); + + // Verify returnToInbox is called with a callback + expect(mockReturnToInbox).toHaveBeenCalledWith(expect.any(Function)); + + // Execute the callback to verify customActionHandler is called + const callback = mockReturnToInbox.mock.calls[0][0]; + callback(); + expect(mockCustomActionHandler).toHaveBeenCalledWith( + expect.objectContaining({ + type: 'customAction', + data: 'action://customAction', + userInput: '', + }), + expect.objectContaining({ + action: expect.objectContaining({ + type: 'customAction', + data: 'action://customAction', + userInput: '', + }), + source: 2, // IterableActionSource.inApp + }) + ); + }); + + it('should call urlHandler with correct URL and context for deep links', async () => { + const mockReturnToInbox = jest.fn(); + const { Iterable } = require('../../core/classes/Iterable'); + const mockUrlHandler = jest.fn(); + Iterable.savedConfig.urlHandler = mockUrlHandler; + + const propsWithMockReturn = { + ...defaultProps, + returnToInbox: mockReturnToInbox, + }; + + const { getByTestId } = render(); + + await waitFor(() => { + expect(getByTestId('webview')).toBeTruthy(); + }); + + const deepLinkTrigger = getByTestId('webview-deep-link-trigger'); + fireEvent.press(deepLinkTrigger); + + // Verify returnToInbox is called with a callback + expect(mockReturnToInbox).toHaveBeenCalledWith(expect.any(Function)); + + // Execute the callback to verify urlHandler is called + const callback = mockReturnToInbox.mock.calls[0][0]; + callback(); + expect(mockUrlHandler).toHaveBeenCalledWith( + 'myapp://deep-link', + expect.objectContaining({ + action: expect.objectContaining({ + type: 'openUrl', + data: 'myapp://deep-link', + userInput: '', + }), + source: 2, // IterableActionSource.inApp + }) + ); + }); + + it('should handle missing customActionHandler gracefully', async () => { + const mockReturnToInbox = jest.fn(); + const { Iterable } = require('../../core/classes/Iterable'); + // Set customActionHandler to undefined + Iterable.savedConfig.customActionHandler = undefined; + + const propsWithMockReturn = { + ...defaultProps, + returnToInbox: mockReturnToInbox, + }; + + const { getByTestId } = render(); + + await waitFor(() => { + expect(getByTestId('webview')).toBeTruthy(); + }); + + const customActionTrigger = getByTestId('webview-custom-action-trigger'); + + // Should not throw an error even when customActionHandler is undefined + expect(() => fireEvent.press(customActionTrigger)).not.toThrow(); + + // Verify returnToInbox is still called + expect(mockReturnToInbox).toHaveBeenCalledWith(expect.any(Function)); + }); + + it('should handle missing urlHandler gracefully', async () => { + const mockReturnToInbox = jest.fn(); + const { Iterable } = require('../../core/classes/Iterable'); + // Set urlHandler to undefined + Iterable.savedConfig.urlHandler = undefined; + + const propsWithMockReturn = { + ...defaultProps, + returnToInbox: mockReturnToInbox, + }; + + const { getByTestId } = render(); + + await waitFor(() => { + expect(getByTestId('webview')).toBeTruthy(); + }); + + const deepLinkTrigger = getByTestId('webview-deep-link-trigger'); + + // Should not throw an error even when urlHandler is undefined + expect(() => fireEvent.press(deepLinkTrigger)).not.toThrow(); + + // Verify returnToInbox is still called + expect(mockReturnToInbox).toHaveBeenCalledWith(expect.any(Function)); + }); }); describe('Tracking and Analytics', () => { From 581c7b71b724a30c86ba6fc925ef9e3c4b516457 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Fri, 17 Oct 2025 12:40:43 -0700 Subject: [PATCH 3/4] refactor: remove unused mock for react-native-vector-icons --- .../IterableInboxMessageDisplay.test.tsx | 14 ++------------ .../components/IterableInboxMessageDisplay.tsx | 5 +++++ 2 files changed, 7 insertions(+), 12 deletions(-) diff --git a/src/inbox/components/IterableInboxMessageDisplay.test.tsx b/src/inbox/components/IterableInboxMessageDisplay.test.tsx index c24035a13..95055afbd 100644 --- a/src/inbox/components/IterableInboxMessageDisplay.test.tsx +++ b/src/inbox/components/IterableInboxMessageDisplay.test.tsx @@ -5,7 +5,7 @@ import { IterableInAppMessage, IterableInAppTrigger, IterableInboxMetadata } fro import { IterableHtmlInAppContent } from '../../inApp/classes/IterableHtmlInAppContent'; import { IterableInAppTriggerType } from '../../inApp/enums'; import type { IterableInboxRowViewModel } from '../types'; -import { IterableInboxMessageDisplay } from './IterableInboxMessageDisplay'; +import { displayTestIds, IterableInboxMessageDisplay } from './IterableInboxMessageDisplay'; // Suppress act() warnings for this test suite since they're expected from the component's useEffect const originalError = console.error; @@ -29,16 +29,6 @@ jest.mock('../../core/classes/Iterable', () => ({ }, })); -// Mock react-native-vector-icons -jest.mock('react-native-vector-icons/Ionicons', () => { - const { View } = require('react-native'); - const MockIcon = ({ testID, ...props }: { testID?: string; [key: string]: unknown }) => ( - - ); - MockIcon.displayName = 'MockIcon'; - return MockIcon; -}); - // Mock Linking jest.mock('react-native', () => ({ ...jest.requireActual('react-native'), @@ -198,7 +188,7 @@ describe('IterableInboxMessageDisplay', () => { it('should render the return button icon', () => { const { getByTestId } = render(); - expect(getByTestId('Icon')).toBeTruthy(); + expect(getByTestId(displayTestIds.icon)).toBeTruthy(); }); it('should handle missing message title gracefully', () => { diff --git a/src/inbox/components/IterableInboxMessageDisplay.tsx b/src/inbox/components/IterableInboxMessageDisplay.tsx index d42306a04..1d833227f 100644 --- a/src/inbox/components/IterableInboxMessageDisplay.tsx +++ b/src/inbox/components/IterableInboxMessageDisplay.tsx @@ -26,6 +26,10 @@ import { ITERABLE_INBOX_COLORS } from '../constants'; import { type IterableInboxRowViewModel } from '../types'; import { HeaderBackButton } from './HeaderBackButton'; +export const displayTestIds = { + icon: 'message-display-icon', +}; + /** * Props for the IterableInboxMessageDisplay component. */ @@ -207,6 +211,7 @@ export const IterableInboxMessageDisplay = ({ { returnToInbox(); From 178f4a017488f8c5b626bec4f496d22ccda73cb8 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Fri, 17 Oct 2025 13:09:57 -0700 Subject: [PATCH 4/4] test: update test IDs for IterableInboxMessageDisplay component and improve test readability --- .../IterableInboxMessageDisplay.test.tsx | 321 ++++++++++++------ .../IterableInboxMessageDisplay.tsx | 18 +- 2 files changed, 236 insertions(+), 103 deletions(-) diff --git a/src/inbox/components/IterableInboxMessageDisplay.test.tsx b/src/inbox/components/IterableInboxMessageDisplay.test.tsx index 95055afbd..73c7a054b 100644 --- a/src/inbox/components/IterableInboxMessageDisplay.test.tsx +++ b/src/inbox/components/IterableInboxMessageDisplay.test.tsx @@ -1,11 +1,18 @@ import { fireEvent, render, waitFor } from '@testing-library/react-native'; import { IterableEdgeInsets } from '../../core'; -import { IterableInAppMessage, IterableInAppTrigger, IterableInboxMetadata } from '../../inApp/classes'; +import { + IterableInAppMessage, + IterableInAppTrigger, + IterableInboxMetadata, +} from '../../inApp/classes'; import { IterableHtmlInAppContent } from '../../inApp/classes/IterableHtmlInAppContent'; import { IterableInAppTriggerType } from '../../inApp/enums'; import type { IterableInboxRowViewModel } from '../types'; -import { displayTestIds, IterableInboxMessageDisplay } from './IterableInboxMessageDisplay'; +import { + IterableInboxMessageDisplay, + iterableMessageDisplayTestIds, +} from './IterableInboxMessageDisplay'; // Suppress act() warnings for this test suite since they're expected from the component's useEffect const originalError = console.error; @@ -41,7 +48,12 @@ jest.mock('react-native', () => ({ jest.mock('react-native-webview', () => { const { View, Text } = require('react-native'); - const MockWebView = ({ onMessage, injectedJavaScript, source, ...props }: { + const MockWebView = ({ + onMessage, + injectedJavaScript, + source, + ...props + }: { onMessage?: (event: { nativeEvent: { data: string } }) => void; injectedJavaScript?: string; source?: { html: string }; @@ -138,7 +150,11 @@ describe('IterableInboxMessageDisplay', () => { new Date('2023-01-01T00:00:00Z'), undefined, true, - new IterableInboxMetadata('Test Message Title', 'Test Subtitle', 'test-image.png'), + new IterableInboxMetadata( + 'Test Message Title', + 'Test Subtitle', + 'test-image.png' + ), undefined, false, 0 @@ -173,22 +189,32 @@ describe('IterableInboxMessageDisplay', () => { describe('Basic Rendering', () => { it('should render without crashing with valid props', () => { - expect(() => render()).not.toThrow(); + expect(() => + render() + ).not.toThrow(); }); it('should render the message title', () => { - const { getByText } = render(); + const { getByText } = render( + + ); expect(getByText('Test Message Title')).toBeTruthy(); }); it('should render the return button with "Inbox" text', () => { - const { getByText } = render(); + const { getByText } = render( + + ); expect(getByText('Inbox')).toBeTruthy(); }); - it('should render the return button icon', () => { - const { getByTestId } = render(); - expect(getByTestId(displayTestIds.icon)).toBeTruthy(); + it('should render the return button', () => { + const { getByTestId } = render( + + ); + expect( + getByTestId(iterableMessageDisplayTestIds.returnButton) + ).toBeTruthy(); }); it('should handle missing message title gracefully', () => { @@ -215,40 +241,50 @@ describe('IterableInboxMessageDisplay', () => { rowViewModel: rowViewModelWithoutTitle, }; - expect(() => render()).not.toThrow(); + expect(() => + render() + ).not.toThrow(); }); }); describe('Async Content Loading', () => { it('should show content after inAppContentPromise resolves', async () => { - const { getByTestId } = render(); + const { getByTestId } = render( + + ); await waitFor(() => { - expect(getByTestId('webview')).toBeTruthy(); + expect(getByTestId(iterableMessageDisplayTestIds.webview)).toBeTruthy(); }); - expect(getByTestId('webview-source')).toHaveTextContent('

Test HTML Content

Test Link'); + expect(getByTestId('webview-source')).toHaveTextContent( + '

Test HTML Content

Test Link' + ); }); - it('should handle rejected inAppContentPromise', async () => { - // Since the component doesn't handle promise rejections, we'll test that it doesn't crash - // when the promise never resolves (which simulates a network failure) - const neverResolvingPromise = new Promise(() => { - // Never resolve or reject - simulates a hanging network request - }); + it('should handle rejected inAppContentPromise', async () => { + // Since the component doesn't handle promise rejections, we'll test that it doesn't crash + // when the promise never resolves (which simulates a network failure) + const neverResolvingPromise = new Promise( + () => { + // Never resolve or reject - simulates a hanging network request + } + ); - const propsWithNeverResolvingPromise = { - ...defaultProps, - inAppContentPromise: neverResolvingPromise, - }; + const propsWithNeverResolvingPromise = { + ...defaultProps, + inAppContentPromise: neverResolvingPromise, + }; - const { queryByTestId } = render(); + const { queryByTestId } = render( + + ); - // Component should render without crashing - // The component always renders the header, so we can check that the WebView is not shown - // since the promise never resolves and inAppContent remains undefined - expect(queryByTestId('webview')).toBeFalsy(); - }); + // Component should render without crashing + // The component always renders the header, so we can check that the WebView is not shown + // since the promise never resolves and inAppContent remains undefined + expect(queryByTestId(iterableMessageDisplayTestIds.webview)).toBeFalsy(); + }); it('should handle component unmounting before promise resolves', async () => { const slowPromise = new Promise((resolve) => { @@ -260,7 +296,9 @@ describe('IterableInboxMessageDisplay', () => { inAppContentPromise: slowPromise, }; - const { unmount } = render(); + const { unmount } = render( + + ); // Unmount before promise resolves unmount(); @@ -280,7 +318,9 @@ describe('IterableInboxMessageDisplay', () => { returnToInbox: mockReturnToInbox, }; - const { getByText } = render(); + const { getByText } = render( + + ); const returnButton = getByText('Inbox'); fireEvent.press(returnButton); @@ -291,7 +331,9 @@ describe('IterableInboxMessageDisplay', () => { it('should track in-app close with back source when return button is pressed', () => { const { Iterable } = require('../../core/classes/Iterable'); - const { getByText } = render(); + const { getByText } = render( + + ); const returnButton = getByText('Inbox'); fireEvent.press(returnButton); @@ -312,10 +354,12 @@ describe('IterableInboxMessageDisplay', () => { returnToInbox: mockReturnToInbox, }; - const { getByTestId } = render(); + const { getByTestId } = render( + + ); await waitFor(() => { - expect(getByTestId('webview')).toBeTruthy(); + expect(getByTestId(iterableMessageDisplayTestIds.webview)).toBeTruthy(); }); const messageTrigger = getByTestId('webview-message-trigger'); @@ -333,10 +377,12 @@ describe('IterableInboxMessageDisplay', () => { deleteRow: mockDeleteRow, }; - const { getByTestId } = render(); + const { getByTestId } = render( + + ); await waitFor(() => { - expect(getByTestId('webview')).toBeTruthy(); + expect(getByTestId(iterableMessageDisplayTestIds.webview)).toBeTruthy(); }); const deleteTrigger = getByTestId('webview-delete-trigger'); @@ -352,10 +398,12 @@ describe('IterableInboxMessageDisplay', () => { returnToInbox: mockReturnToInbox, }; - const { getByTestId } = render(); + const { getByTestId } = render( + + ); await waitFor(() => { - expect(getByTestId('webview')).toBeTruthy(); + expect(getByTestId(iterableMessageDisplayTestIds.webview)).toBeTruthy(); }); const dismissTrigger = getByTestId('webview-dismiss-trigger'); @@ -375,10 +423,12 @@ describe('IterableInboxMessageDisplay', () => { returnToInbox: mockReturnToInbox, }; - const { getByTestId } = render(); + const { getByTestId } = render( + + ); await waitFor(() => { - expect(getByTestId('webview')).toBeTruthy(); + expect(getByTestId(iterableMessageDisplayTestIds.webview)).toBeTruthy(); }); const customActionTrigger = getByTestId('webview-custom-action-trigger'); @@ -398,10 +448,12 @@ describe('IterableInboxMessageDisplay', () => { returnToInbox: mockReturnToInbox, }; - const { getByTestId } = render(); + const { getByTestId } = render( + + ); await waitFor(() => { - expect(getByTestId('webview')).toBeTruthy(); + expect(getByTestId(iterableMessageDisplayTestIds.webview)).toBeTruthy(); }); const deepLinkTrigger = getByTestId('webview-deep-link-trigger'); @@ -420,10 +472,12 @@ describe('IterableInboxMessageDisplay', () => { deleteRow: mockDeleteRow, }; - const { getByTestId } = render(); + const { getByTestId } = render( + + ); await waitFor(() => { - expect(getByTestId('webview')).toBeTruthy(); + expect(getByTestId(iterableMessageDisplayTestIds.webview)).toBeTruthy(); }); const deleteTrigger = getByTestId('webview-delete-trigger'); @@ -445,10 +499,12 @@ describe('IterableInboxMessageDisplay', () => { returnToInbox: mockReturnToInbox, }; - const { getByTestId } = render(); + const { getByTestId } = render( + + ); await waitFor(() => { - expect(getByTestId('webview')).toBeTruthy(); + expect(getByTestId(iterableMessageDisplayTestIds.webview)).toBeTruthy(); }); const dismissTrigger = getByTestId('webview-dismiss-trigger'); @@ -466,10 +522,12 @@ describe('IterableInboxMessageDisplay', () => { returnToInbox: mockReturnToInbox, }; - const { getByTestId } = render(); + const { getByTestId } = render( + + ); await waitFor(() => { - expect(getByTestId('webview')).toBeTruthy(); + expect(getByTestId(iterableMessageDisplayTestIds.webview)).toBeTruthy(); }); const messageTrigger = getByTestId('webview-message-trigger'); @@ -495,10 +553,12 @@ describe('IterableInboxMessageDisplay', () => { returnToInbox: mockReturnToInbox, }; - const { getByTestId } = render(); + const { getByTestId } = render( + + ); await waitFor(() => { - expect(getByTestId('webview')).toBeTruthy(); + expect(getByTestId(iterableMessageDisplayTestIds.webview)).toBeTruthy(); }); const customActionTrigger = getByTestId('webview-custom-action-trigger'); @@ -538,10 +598,12 @@ describe('IterableInboxMessageDisplay', () => { returnToInbox: mockReturnToInbox, }; - const { getByTestId } = render(); + const { getByTestId } = render( + + ); await waitFor(() => { - expect(getByTestId('webview')).toBeTruthy(); + expect(getByTestId(iterableMessageDisplayTestIds.webview)).toBeTruthy(); }); const deepLinkTrigger = getByTestId('webview-deep-link-trigger'); @@ -577,10 +639,12 @@ describe('IterableInboxMessageDisplay', () => { returnToInbox: mockReturnToInbox, }; - const { getByTestId } = render(); + const { getByTestId } = render( + + ); await waitFor(() => { - expect(getByTestId('webview')).toBeTruthy(); + expect(getByTestId(iterableMessageDisplayTestIds.webview)).toBeTruthy(); }); const customActionTrigger = getByTestId('webview-custom-action-trigger'); @@ -603,10 +667,12 @@ describe('IterableInboxMessageDisplay', () => { returnToInbox: mockReturnToInbox, }; - const { getByTestId } = render(); + const { getByTestId } = render( + + ); await waitFor(() => { - expect(getByTestId('webview')).toBeTruthy(); + expect(getByTestId(iterableMessageDisplayTestIds.webview)).toBeTruthy(); }); const deepLinkTrigger = getByTestId('webview-deep-link-trigger'); @@ -622,10 +688,12 @@ describe('IterableInboxMessageDisplay', () => { describe('Tracking and Analytics', () => { it('should track in-app click when link is clicked', async () => { const { Iterable } = require('../../core/classes/Iterable'); - const { getByTestId } = render(); + const { getByTestId } = render( + + ); await waitFor(() => { - expect(getByTestId('webview')).toBeTruthy(); + expect(getByTestId(iterableMessageDisplayTestIds.webview)).toBeTruthy(); }); const messageTrigger = getByTestId('webview-message-trigger'); @@ -640,10 +708,12 @@ describe('IterableInboxMessageDisplay', () => { it('should track in-app close with link source when link is clicked', async () => { const { Iterable } = require('../../core/classes/Iterable'); - const { getByTestId } = render(); + const { getByTestId } = render( + + ); await waitFor(() => { - expect(getByTestId('webview')).toBeTruthy(); + expect(getByTestId(iterableMessageDisplayTestIds.webview)).toBeTruthy(); }); const messageTrigger = getByTestId('webview-message-trigger'); @@ -661,48 +731,67 @@ describe('IterableInboxMessageDisplay', () => { describe('Props Variations', () => { it('should handle different content widths', () => { const propsWithDifferentWidth = { ...defaultProps, contentWidth: 600 }; - expect(() => render()).not.toThrow(); + expect(() => + render() + ).not.toThrow(); }); it('should handle portrait mode', () => { const portraitProps = { ...defaultProps, isPortrait: true }; - expect(() => render()).not.toThrow(); + expect(() => + render() + ).not.toThrow(); }); it('should handle landscape mode', () => { const landscapeProps = { ...defaultProps, isPortrait: false }; - expect(() => render()).not.toThrow(); + expect(() => + render() + ).not.toThrow(); }); it('should handle zero content width', () => { const zeroWidthProps = { ...defaultProps, contentWidth: 0 }; - expect(() => render()).not.toThrow(); + expect(() => + render() + ).not.toThrow(); }); it('should handle negative content width', () => { const negativeWidthProps = { ...defaultProps, contentWidth: -100 }; - expect(() => render()).not.toThrow(); + expect(() => + render() + ).not.toThrow(); }); it('should handle very large content width', () => { const largeWidthProps = { ...defaultProps, contentWidth: 2000 }; - expect(() => render()).not.toThrow(); + expect(() => + render() + ).not.toThrow(); }); }); describe('Function Props', () => { it('should handle returnToInbox function', () => { const mockReturnToInbox = jest.fn(); - const propsWithReturnToInbox = { ...defaultProps, returnToInbox: mockReturnToInbox }; + const propsWithReturnToInbox = { + ...defaultProps, + returnToInbox: mockReturnToInbox, + }; - expect(() => render()).not.toThrow(); + expect(() => + render() + ).not.toThrow(); }); it('should handle deleteRow function', () => { const mockDeleteRow = jest.fn(); const propsWithDeleteRow = { ...defaultProps, deleteRow: mockDeleteRow }; - expect(() => render()).not.toThrow(); + expect(() => + render() + ).not.toThrow(); }); it('should handle undefined function props gracefully', () => { @@ -712,34 +801,42 @@ describe('IterableInboxMessageDisplay', () => { deleteRow: undefined as unknown as (id: string) => void, }; - expect(() => render()).not.toThrow(); + expect(() => + render() + ).not.toThrow(); }); }); describe('WebView Configuration', () => { it('should configure WebView with correct props', async () => { - const { getByTestId } = render(); + const { getByTestId } = render( + + ); await waitFor(() => { - expect(getByTestId('webview')).toBeTruthy(); + expect(getByTestId(iterableMessageDisplayTestIds.webview)).toBeTruthy(); }); // Check that injected JavaScript is present const jsContent = getByTestId('webview-js').props.children; - expect(jsContent).toContain('const links = document.querySelectorAll(\'a\')'); + expect(jsContent).toContain( + "const links = document.querySelectorAll('a')" + ); expect(jsContent).toContain('links.forEach(link => {'); expect(jsContent).toContain('window.ReactNativeWebView.postMessage'); }); it('should set correct originWhiteList', async () => { - const { getByTestId } = render(); + const { getByTestId } = render( + + ); await waitFor(() => { - expect(getByTestId('webview')).toBeTruthy(); + expect(getByTestId(iterableMessageDisplayTestIds.webview)).toBeTruthy(); }); // The WebView should be rendered with the correct configuration - expect(getByTestId('webview')).toBeTruthy(); + expect(getByTestId(iterableMessageDisplayTestIds.webview)).toBeTruthy(); }); }); @@ -755,10 +852,12 @@ describe('IterableInboxMessageDisplay', () => { inAppContentPromise: Promise.resolve(emptyHtmlContent), }; - const { getByTestId } = render(); + const { getByTestId } = render( + + ); await waitFor(() => { - expect(getByTestId('webview')).toBeTruthy(); + expect(getByTestId(iterableMessageDisplayTestIds.webview)).toBeTruthy(); }); expect(getByTestId('webview-source')).toHaveTextContent(''); @@ -775,13 +874,17 @@ describe('IterableInboxMessageDisplay', () => { inAppContentPromise: Promise.resolve(specialHtmlContent), }; - const { getByTestId } = render(); + const { getByTestId } = render( + + ); await waitFor(() => { - expect(getByTestId('webview')).toBeTruthy(); + expect(getByTestId(iterableMessageDisplayTestIds.webview)).toBeTruthy(); }); - expect(getByTestId('webview-source')).toHaveTextContent('

测试标题 🚀

测试链接'); + expect(getByTestId('webview-source')).toHaveTextContent( + '

测试标题 🚀

测试链接' + ); }); it('should handle very long message titles', () => { @@ -810,7 +913,9 @@ describe('IterableInboxMessageDisplay', () => { rowViewModel: rowViewModelWithLongTitle, }; - expect(() => render()).not.toThrow(); + expect(() => + render() + ).not.toThrow(); }); it('should handle message with no inbox metadata', () => { @@ -837,22 +942,30 @@ describe('IterableInboxMessageDisplay', () => { rowViewModel: rowViewModelWithoutMetadata, }; - expect(() => render()).not.toThrow(); + expect(() => + render() + ).not.toThrow(); }); }); describe('Performance Considerations', () => { it('should handle rapid prop changes', async () => { - const { rerender, getByTestId } = render(); + const { rerender, getByTestId } = render( + + ); await waitFor(() => { - expect(getByTestId('webview')).toBeTruthy(); + expect(getByTestId(iterableMessageDisplayTestIds.webview)).toBeTruthy(); }); // Change props rapidly const newProps1 = { ...defaultProps, contentWidth: 400 }; const newProps2 = { ...defaultProps, isPortrait: false }; - const newProps3 = { ...defaultProps, contentWidth: 500, isPortrait: true }; + const newProps3 = { + ...defaultProps, + contentWidth: 500, + isPortrait: true, + }; expect(() => { rerender(); @@ -870,7 +983,11 @@ describe('IterableInboxMessageDisplay', () => { new Date(), undefined, true, - new IterableInboxMetadata(`Title ${i}`, `Subtitle ${i}`, `image${i}.png`), + new IterableInboxMetadata( + `Title ${i}`, + `Subtitle ${i}`, + `image${i}.png` + ), undefined, false, i @@ -892,7 +1009,9 @@ describe('IterableInboxMessageDisplay', () => { rowViewModel, }; - expect(() => render()).not.toThrow(); + expect(() => + render() + ).not.toThrow(); }); }); }); @@ -903,14 +1022,16 @@ describe('IterableInboxMessageDisplay', () => { const mockCustomActionHandler = jest.fn(); Iterable.savedConfig.customActionHandler = mockCustomActionHandler; - const { getByTestId } = render(); + const { getByTestId } = render( + + ); await waitFor(() => { - expect(getByTestId('webview')).toBeTruthy(); + expect(getByTestId(iterableMessageDisplayTestIds.webview)).toBeTruthy(); }); // The component should render without errors when customActionHandler is set - expect(getByTestId('webview')).toBeTruthy(); + expect(getByTestId(iterableMessageDisplayTestIds.webview)).toBeTruthy(); }); it('should work with Iterable.savedConfig.urlHandler', async () => { @@ -918,14 +1039,16 @@ describe('IterableInboxMessageDisplay', () => { const mockUrlHandler = jest.fn(); Iterable.savedConfig.urlHandler = mockUrlHandler; - const { getByTestId } = render(); + const { getByTestId } = render( + + ); await waitFor(() => { - expect(getByTestId('webview')).toBeTruthy(); + expect(getByTestId(iterableMessageDisplayTestIds.webview)).toBeTruthy(); }); // The component should render without errors when urlHandler is set - expect(getByTestId('webview')).toBeTruthy(); + expect(getByTestId(iterableMessageDisplayTestIds.webview)).toBeTruthy(); }); it('should handle missing Iterable.savedConfig handlers', async () => { @@ -933,14 +1056,16 @@ describe('IterableInboxMessageDisplay', () => { Iterable.savedConfig.customActionHandler = undefined; Iterable.savedConfig.urlHandler = undefined; - const { getByTestId } = render(); + const { getByTestId } = render( + + ); await waitFor(() => { - expect(getByTestId('webview')).toBeTruthy(); + expect(getByTestId(iterableMessageDisplayTestIds.webview)).toBeTruthy(); }); // The component should render without errors when handlers are undefined - expect(getByTestId('webview')).toBeTruthy(); + expect(getByTestId(iterableMessageDisplayTestIds.webview)).toBeTruthy(); }); }); }); diff --git a/src/inbox/components/IterableInboxMessageDisplay.tsx b/src/inbox/components/IterableInboxMessageDisplay.tsx index 1d833227f..b52eb47c3 100644 --- a/src/inbox/components/IterableInboxMessageDisplay.tsx +++ b/src/inbox/components/IterableInboxMessageDisplay.tsx @@ -26,8 +26,11 @@ import { ITERABLE_INBOX_COLORS } from '../constants'; import { type IterableInboxRowViewModel } from '../types'; import { HeaderBackButton } from './HeaderBackButton'; -export const displayTestIds = { - icon: 'message-display-icon', +export const iterableMessageDisplayTestIds = { + container: 'iterable-message-display-container', + returnButton: 'iterable-message-display-return-button', + messageTitle: 'iterable-message-display-message-title', + webview: 'iterable-message-display-webview', }; /** @@ -89,7 +92,7 @@ export const IterableInboxMessageDisplay = ({ header: { flexDirection: 'row', - height: Platform.OS === 'ios' ? 44 : 56, + height: Platform.OS === 'ios' ? 44 : 56, justifyContent: 'center', width: '100%', }, @@ -207,11 +210,14 @@ export const IterableInboxMessageDisplay = ({ } return ( - + { returnToInbox(); @@ -229,6 +235,7 @@ export const IterableInboxMessageDisplay = ({ numberOfLines={1} ellipsizeMode="tail" style={styles.messageTitleText} + testID={iterableMessageDisplayTestIds.messageTitle} > {messageTitle} @@ -238,6 +245,7 @@ export const IterableInboxMessageDisplay = ({ {inAppContent && (