diff --git a/.eslintignore b/.eslintignore
index 81e04e4d2..99285fadd 100644
--- a/.eslintignore
+++ b/.eslintignore
@@ -1,3 +1,4 @@
docs/
lib/
-node_modules/
\ No newline at end of file
+node_modules/
+coverage/
diff --git a/src/core/classes/Iterable.ts b/src/core/classes/Iterable.ts
index 1d070856b..2f9aff913 100644
--- a/src/core/classes/Iterable.ts
+++ b/src/core/classes/Iterable.ts
@@ -1,4 +1,4 @@
-/* eslint-disable eslint-comments/no-unlimited-disable */
+
import {
Linking,
NativeEventEmitter,
@@ -81,11 +81,11 @@ export class Iterable {
// Lazy initialization to avoid circular dependency
if (!this._inAppManager) {
// Import here to avoid circular dependency at module level
-
+ /* eslint-disable @typescript-eslint/no-var-requires, @typescript-eslint/no-require-imports */
const {
IterableInAppManager,
- // eslint-disable-next-line
} = require('../../inApp/classes/IterableInAppManager');
+ /* eslint-enable @typescript-eslint/no-var-requires, @typescript-eslint/no-require-imports */
this._inAppManager = new IterableInAppManager();
}
return this._inAppManager;
diff --git a/src/inbox/components/IterableInboxMessageList.test.tsx b/src/inbox/components/IterableInboxMessageList.test.tsx
new file mode 100644
index 000000000..766fc90b0
--- /dev/null
+++ b/src/inbox/components/IterableInboxMessageList.test.tsx
@@ -0,0 +1,935 @@
+import { render } from '@testing-library/react-native';
+import {
+ IterableInAppMessage,
+ IterableInAppTrigger,
+ IterableInboxMetadata,
+} from '../../inApp/classes';
+import { IterableInAppTriggerType } from '../../inApp/enums';
+import { IterableInboxDataModel } from '../classes';
+import type {
+ IterableInboxCustomizations,
+ IterableInboxImpressionRowInfo,
+ IterableInboxRowViewModel,
+} from '../types';
+import { IterableInboxMessageList } from './IterableInboxMessageList';
+
+// Mock the IterableInboxMessageCell component
+jest.mock('./IterableInboxMessageCell', () => ({
+ IterableInboxMessageCell: ({
+ rowViewModel,
+ index,
+ last,
+ }: {
+ rowViewModel: IterableInboxRowViewModel;
+ index: number;
+ last: boolean;
+ }) => {
+ const { View, Text } = require('react-native');
+ return (
+
+ {rowViewModel.title}
+
+ {last ? 'last' : 'not-last'}
+
+
+ );
+ },
+}));
+
+describe('IterableInboxMessageList', () => {
+ const mockMessage1 = new IterableInAppMessage(
+ 'messageId1',
+ 1,
+ new IterableInAppTrigger(IterableInAppTriggerType.immediate),
+ new Date('2023-01-01T00:00:00Z'),
+ undefined,
+ true,
+ new IterableInboxMetadata('Title 1', 'Subtitle 1', 'imageUrl1.png'),
+ undefined,
+ false,
+ 0
+ );
+
+ const mockMessage2 = new IterableInAppMessage(
+ 'messageId2',
+ 2,
+ new IterableInAppTrigger(IterableInAppTriggerType.immediate),
+ new Date('2023-01-02T00:00:00Z'),
+ undefined,
+ true,
+ new IterableInboxMetadata('Title 2', 'Subtitle 2', 'imageUrl2.png'),
+ undefined,
+ true,
+ 1
+ );
+
+ const mockRowViewModel1: IterableInboxRowViewModel = {
+ inAppMessage: mockMessage1,
+ title: 'Title 1',
+ subtitle: 'Subtitle 1',
+ imageUrl: 'imageUrl1.png',
+ read: false,
+ createdAt: new Date('2023-01-01T00:00:00Z'),
+ };
+
+ const mockRowViewModel2: IterableInboxRowViewModel = {
+ inAppMessage: mockMessage2,
+ title: 'Title 2',
+ subtitle: 'Subtitle 2',
+ imageUrl: 'imageUrl2.png',
+ read: true,
+ createdAt: new Date('2023-01-02T00:00:00Z'),
+ };
+
+ const mockDataModel = new IterableInboxDataModel();
+ const mockCustomizations: IterableInboxCustomizations = {
+ messageRow: {
+ height: 150,
+ backgroundColor: 'white',
+ flexDirection: 'row',
+ paddingTop: 10,
+ paddingBottom: 10,
+ },
+ title: {
+ fontSize: 16,
+ paddingBottom: 5,
+ },
+ body: {
+ fontSize: 14,
+ color: 'gray',
+ paddingBottom: 5,
+ },
+ createdAt: {
+ fontSize: 12,
+ color: 'lightgray',
+ },
+ unreadIndicator: {
+ width: 8,
+ height: 8,
+ backgroundColor: 'blue',
+ borderRadius: 4,
+ },
+ };
+
+ const defaultProps = {
+ dataModel: mockDataModel,
+ rowViewModels: [mockRowViewModel1, mockRowViewModel2],
+ customizations: mockCustomizations,
+ messageListItemLayout: jest.fn().mockReturnValue([null, 150]),
+ deleteRow: jest.fn(),
+ handleMessageSelect: jest.fn(),
+ updateVisibleMessageImpressions: jest.fn(),
+ contentWidth: 300,
+ isPortrait: true,
+ };
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ describe('Basic Rendering', () => {
+ it('should render without crashing with minimal valid props', () => {
+ expect(() =>
+ render()
+ ).not.toThrow();
+ });
+
+ it('should render FlatList component', () => {
+ const { getByTestId } = render(
+
+ );
+ // FlatList doesn't have a testID by default, but we can check if it renders
+ expect(() => getByTestId('message-cell-messageId1')).not.toThrow();
+ });
+
+ it('should render message cells for each row view model', () => {
+ const { getByTestId } = render(
+
+ );
+
+ expect(getByTestId('message-cell-messageId1')).toBeTruthy();
+ expect(getByTestId('message-cell-messageId2')).toBeTruthy();
+ });
+
+ it('should render with empty row view models array', () => {
+ const propsWithEmptyData = { ...defaultProps, rowViewModels: [] };
+ expect(() =>
+ render()
+ ).not.toThrow();
+ });
+
+ it('should render with single row view model', () => {
+ const propsWithSingleItem = {
+ ...defaultProps,
+ rowViewModels: [mockRowViewModel1],
+ };
+ const { getByTestId } = render(
+
+ );
+
+ expect(getByTestId('message-cell-messageId1')).toBeTruthy();
+ });
+ });
+
+ 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('FlatList Functionality', () => {
+ it('should pass correct data to FlatList', () => {
+ const { getByTestId } = render(
+
+ );
+
+ // Verify that both message cells are rendered
+ expect(getByTestId('message-cell-messageId1')).toBeTruthy();
+ expect(getByTestId('message-cell-messageId2')).toBeTruthy();
+ });
+
+ it('should use correct keyExtractor', () => {
+ const { getByTestId } = render(
+
+ );
+
+ // The keyExtractor should use messageId, which is used in the mock component
+ expect(getByTestId('message-cell-messageId1')).toBeTruthy();
+ expect(getByTestId('message-cell-messageId2')).toBeTruthy();
+ });
+
+ it('should pass correct props to message cells', () => {
+ const { getByTestId } = render(
+
+ );
+
+ // Check that the first message is not marked as last
+ expect(getByTestId('message-title-0')).toBeTruthy();
+ expect(getByTestId('message-last-0')).toHaveTextContent('not-last');
+
+ // Check that the second message is marked as last
+ expect(getByTestId('message-title-1')).toBeTruthy();
+ expect(getByTestId('message-last-1')).toHaveTextContent('last');
+ });
+
+ it('should handle single item as last', () => {
+ const singleItemProps = {
+ ...defaultProps,
+ rowViewModels: [mockRowViewModel1],
+ };
+ const { getByTestId } = render(
+
+ );
+
+ expect(getByTestId('message-last-0')).toHaveTextContent('last');
+ });
+ });
+
+ describe('Viewability Configuration', () => {
+ it('should have correct viewability configuration', () => {
+ const { getByTestId } = render(
+
+ );
+
+ // The component should render without errors, indicating viewability config is valid
+ expect(getByTestId('message-cell-messageId1')).toBeTruthy();
+ });
+
+ it('should handle viewability config with minimum view time', () => {
+ // Test that the component renders with the configured minimumViewTime of 500ms
+ expect(() =>
+ render()
+ ).not.toThrow();
+ });
+
+ it('should handle viewability config with item visible percent threshold', () => {
+ // Test that the component renders with the configured itemVisiblePercentThreshold of 100
+ expect(() =>
+ render()
+ ).not.toThrow();
+ });
+
+ it('should handle viewability config with waitForInteraction false', () => {
+ // Test that the component renders with waitForInteraction set to false
+ expect(() =>
+ render()
+ ).not.toThrow();
+ });
+ });
+
+ describe('Impression Tracking', () => {
+ it('should call updateVisibleMessageImpressions when viewable items change', () => {
+ const mockUpdateVisibleMessageImpressions = jest.fn();
+ const propsWithMockCallback = {
+ ...defaultProps,
+ updateVisibleMessageImpressions: mockUpdateVisibleMessageImpressions,
+ };
+
+ render();
+
+ // The callback should be defined and ready to be called
+ expect(mockUpdateVisibleMessageImpressions).toBeDefined();
+ });
+
+ it('should process view tokens correctly in getRowInfosFromViewTokens', () => {
+ const mockUpdateVisibleMessageImpressions = jest.fn();
+ const propsWithMockCallback = {
+ ...defaultProps,
+ updateVisibleMessageImpressions: mockUpdateVisibleMessageImpressions,
+ };
+
+ const { getByTestId } = render(
+
+ );
+
+ // Verify the component renders and can process view tokens
+ expect(getByTestId('message-cell-messageId1')).toBeTruthy();
+ });
+
+ it('should handle inboxSessionItemsChanged callback', () => {
+ const mockUpdateVisibleMessageImpressions = jest.fn();
+ const propsWithMockCallback = {
+ ...defaultProps,
+ updateVisibleMessageImpressions: mockUpdateVisibleMessageImpressions,
+ };
+
+ const { getByTestId } = render(
+
+ );
+
+ // The component should render and have the callback ready
+ expect(getByTestId('message-cell-messageId1')).toBeTruthy();
+ expect(mockUpdateVisibleMessageImpressions).toBeDefined();
+ });
+
+ it('should handle updateVisibleMessageImpressions with different implementations', () => {
+ const customUpdateCallback = jest.fn(
+ (rowInfos: IterableInboxImpressionRowInfo[]) => {
+ // Custom implementation
+ rowInfos.forEach((rowInfo) => {
+ console.log(`Message ${rowInfo.messageId} is visible`);
+ });
+ }
+ );
+
+ const propsWithCustomCallback = {
+ ...defaultProps,
+ updateVisibleMessageImpressions: customUpdateCallback,
+ };
+
+ expect(() =>
+ render()
+ ).not.toThrow();
+ });
+
+ it('should handle updateVisibleMessageImpressions with undefined callback', () => {
+ const propsWithUndefinedCallback = {
+ ...defaultProps,
+ updateVisibleMessageImpressions: undefined as unknown as (
+ rowInfos: IterableInboxImpressionRowInfo[]
+ ) => void,
+ };
+
+ expect(() =>
+ render()
+ ).not.toThrow();
+ });
+ });
+
+ describe('Scroll Behavior', () => {
+ it('should have scrollEnabled true by default (when not swiping)', () => {
+ const { getByTestId } = render(
+
+ );
+
+ // Component should render without errors, indicating scroll is enabled by default
+ expect(getByTestId('message-cell-messageId1')).toBeTruthy();
+ });
+
+ it('should handle swiping state changes', () => {
+ const { getByTestId } = render(
+
+ );
+
+ // The component should render and handle swiping state internally
+ expect(getByTestId('message-cell-messageId1')).toBeTruthy();
+ });
+ });
+
+ describe('Layout Callback', () => {
+ it('should handle onLayout callback', () => {
+ const { getByTestId } = render(
+
+ );
+
+ // Component should render without errors, indicating onLayout is handled
+ expect(getByTestId('message-cell-messageId1')).toBeTruthy();
+ });
+
+ it('should call recordInteraction on layout', () => {
+ // We can't directly test the ref behavior, but we can ensure the component renders
+ expect(() =>
+ render()
+ ).not.toThrow();
+ });
+
+ it('should handle onLayout callback with FlatList ref', () => {
+ const { getByTestId } = render(
+
+ );
+
+ // The component should render and handle the onLayout callback
+ // The onLayout callback calls flatListRef.current?.recordInteraction()
+ expect(getByTestId('message-cell-messageId1')).toBeTruthy();
+ });
+ });
+
+ describe('Function Props', () => {
+ it('should handle deleteRow function', () => {
+ const mockDeleteRow = jest.fn();
+ const propsWithDeleteRow = { ...defaultProps, deleteRow: mockDeleteRow };
+
+ expect(() =>
+ render()
+ ).not.toThrow();
+ });
+
+ it('should handle handleMessageSelect function', () => {
+ const mockHandleMessageSelect = jest.fn();
+ const propsWithHandleMessageSelect = {
+ ...defaultProps,
+ handleMessageSelect: mockHandleMessageSelect,
+ };
+
+ expect(() =>
+ render()
+ ).not.toThrow();
+ });
+
+ it('should handle messageListItemLayout function', () => {
+ const mockMessageListItemLayout = jest.fn().mockReturnValue([null, 200]);
+ const propsWithLayout = {
+ ...defaultProps,
+ messageListItemLayout: mockMessageListItemLayout,
+ };
+
+ expect(() =>
+ render()
+ ).not.toThrow();
+ });
+
+ it('should handle undefined function props gracefully', () => {
+ const propsWithUndefinedFunctions = {
+ ...defaultProps,
+ deleteRow: undefined as unknown as (messageId: string) => void,
+ handleMessageSelect: undefined as unknown as (
+ messageId: string,
+ index: number
+ ) => void,
+ messageListItemLayout: undefined as unknown as (
+ last: boolean,
+ rowViewModel: IterableInboxRowViewModel
+ ) => [React.ReactElement | null, number],
+ };
+
+ expect(() =>
+ render()
+ ).not.toThrow();
+ });
+ });
+
+ describe('Data Model Integration', () => {
+ it('should work with different data model instances', () => {
+ const differentDataModel = new IterableInboxDataModel();
+ const propsWithDifferentDataModel = {
+ ...defaultProps,
+ dataModel: differentDataModel,
+ };
+
+ expect(() =>
+ render()
+ ).not.toThrow();
+ });
+
+ it('should handle data model with custom functions', () => {
+ const customDataModel = new IterableInboxDataModel();
+ customDataModel.set(
+ (message) => message.campaignId > 1,
+ (msg1, msg2) => msg1.priorityLevel - msg2.priorityLevel,
+ (message) => message.createdAt?.toISOString() ?? 'No date'
+ );
+
+ const propsWithCustomDataModel = {
+ ...defaultProps,
+ dataModel: customDataModel,
+ };
+ expect(() =>
+ render()
+ ).not.toThrow();
+ });
+ });
+
+ describe('Customizations', () => {
+ it('should handle different customizations', () => {
+ const differentCustomizations: IterableInboxCustomizations = {
+ messageRow: {
+ height: 200,
+ backgroundColor: 'red',
+ },
+ title: {
+ fontSize: 20,
+ paddingBottom: 10,
+ },
+ };
+
+ const propsWithDifferentCustomizations = {
+ ...defaultProps,
+ customizations: differentCustomizations,
+ };
+ expect(() =>
+ render(
+
+ )
+ ).not.toThrow();
+ });
+
+ it('should handle minimal customizations', () => {
+ const minimalCustomizations: IterableInboxCustomizations = {
+ messageRow: {
+ height: 100,
+ },
+ };
+
+ const propsWithMinimalCustomizations = {
+ ...defaultProps,
+ customizations: minimalCustomizations,
+ };
+ expect(() =>
+ render()
+ ).not.toThrow();
+ });
+ });
+
+ describe('Edge Cases', () => {
+ it('should handle null row view models', () => {
+ const propsWithNullData = {
+ ...defaultProps,
+ rowViewModels: null as unknown as IterableInboxRowViewModel[],
+ };
+ expect(() =>
+ render()
+ ).not.toThrow();
+ });
+
+ it('should handle undefined row view models', () => {
+ const propsWithUndefinedData = {
+ ...defaultProps,
+ rowViewModels: undefined as unknown as IterableInboxRowViewModel[],
+ };
+ expect(() =>
+ render()
+ ).not.toThrow();
+ });
+
+ it('should handle row view models with missing properties', () => {
+ const incompleteRowViewModel = {
+ inAppMessage: mockMessage1,
+ title: 'Incomplete Title',
+ // Missing other properties
+ } as IterableInboxRowViewModel;
+
+ const propsWithIncompleteData = {
+ ...defaultProps,
+ rowViewModels: [incompleteRowViewModel],
+ };
+ expect(() =>
+ render()
+ ).not.toThrow();
+ });
+
+ it('should handle very large number of row view models', () => {
+ const largeRowViewModels = Array.from({ length: 1000 }, (_, i) => ({
+ inAppMessage: new IterableInAppMessage(
+ `messageId${i}`,
+ i,
+ new IterableInAppTrigger(IterableInAppTriggerType.immediate),
+ new Date(),
+ undefined,
+ true,
+ new IterableInboxMetadata(
+ `Title ${i}`,
+ `Subtitle ${i}`,
+ `imageUrl${i}.png`
+ ),
+ undefined,
+ false,
+ i
+ ),
+ title: `Title ${i}`,
+ subtitle: `Subtitle ${i}`,
+ imageUrl: `imageUrl${i}.png`,
+ read: false,
+ createdAt: new Date(),
+ }));
+
+ const propsWithLargeData = {
+ ...defaultProps,
+ rowViewModels: largeRowViewModels,
+ };
+ expect(() =>
+ render()
+ ).not.toThrow();
+ });
+
+ it('should handle row view models with special characters in message IDs', () => {
+ const specialMessage = new IterableInAppMessage(
+ 'message-id-with-special-chars_123@test.com#special',
+ 1,
+ new IterableInAppTrigger(IterableInAppTriggerType.immediate),
+ new Date(),
+ undefined,
+ true,
+ new IterableInboxMetadata(
+ 'Special Title',
+ 'Special Subtitle',
+ 'special.png'
+ ),
+ undefined,
+ false,
+ 0
+ );
+
+ const specialRowViewModel: IterableInboxRowViewModel = {
+ inAppMessage: specialMessage,
+ title: 'Special Title',
+ subtitle: 'Special Subtitle',
+ imageUrl: 'special.png',
+ read: false,
+ createdAt: new Date(),
+ };
+
+ const propsWithSpecialData = {
+ ...defaultProps,
+ rowViewModels: [specialRowViewModel],
+ };
+ expect(() =>
+ render()
+ ).not.toThrow();
+ });
+
+ it('should handle row view models with unicode characters', () => {
+ const unicodeMessage = new IterableInAppMessage(
+ '测试消息ID_🚀_ñáéíóú',
+ 1,
+ new IterableInAppTrigger(IterableInAppTriggerType.immediate),
+ new Date(),
+ undefined,
+ true,
+ new IterableInboxMetadata('测试标题', '测试副标题', '测试图片.png'),
+ undefined,
+ false,
+ 0
+ );
+
+ const unicodeRowViewModel: IterableInboxRowViewModel = {
+ inAppMessage: unicodeMessage,
+ title: '测试标题',
+ subtitle: '测试副标题',
+ imageUrl: '测试图片.png',
+ read: false,
+ createdAt: new Date(),
+ };
+
+ const propsWithUnicodeData = {
+ ...defaultProps,
+ rowViewModels: [unicodeRowViewModel],
+ };
+ expect(() =>
+ render()
+ ).not.toThrow();
+ });
+ });
+
+ describe('Component State Management', () => {
+ it('should manage swiping state internally', () => {
+ const { getByTestId } = render(
+
+ );
+
+ // Component should render and manage swiping state
+ expect(getByTestId('message-cell-messageId1')).toBeTruthy();
+ });
+
+ it('should maintain FlatList ref', () => {
+ const { getByTestId } = render(
+
+ );
+
+ // Component should render and maintain ref
+ expect(getByTestId('message-cell-messageId1')).toBeTruthy();
+ });
+ });
+
+ describe('Performance Considerations', () => {
+ it('should handle rapid prop changes', () => {
+ const { rerender, getByTestId } = render(
+
+ );
+
+ // 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();
+
+ expect(getByTestId('message-cell-messageId1')).toBeTruthy();
+ });
+
+ it('should handle memory efficiently with large datasets', () => {
+ const largeDataset = Array.from({ length: 100 }, (_, i) => ({
+ inAppMessage: new IterableInAppMessage(
+ `largeMessageId${i}`,
+ i,
+ new IterableInAppTrigger(IterableInAppTriggerType.immediate),
+ new Date(),
+ undefined,
+ true,
+ new IterableInboxMetadata(
+ `Large Title ${i}`,
+ `Large Subtitle ${i}`,
+ `largeImage${i}.png`
+ ),
+ undefined,
+ false,
+ i
+ ),
+ title: `Large Title ${i}`,
+ subtitle: `Large Subtitle ${i}`,
+ imageUrl: `largeImage${i}.png`,
+ read: false,
+ createdAt: new Date(),
+ }));
+
+ const propsWithLargeDataset = {
+ ...defaultProps,
+ rowViewModels: largeDataset,
+ };
+ expect(() =>
+ render()
+ ).not.toThrow();
+ });
+ });
+
+ describe('Integration with IterableInboxMessageCell', () => {
+ it('should pass all required props to message cells', () => {
+ const { getByTestId } = render(
+
+ );
+
+ // Verify that message cells receive the correct props by checking their rendering
+ expect(getByTestId('message-cell-messageId1')).toBeTruthy();
+ expect(getByTestId('message-cell-messageId2')).toBeTruthy();
+ });
+
+ it('should handle message cell prop changes', () => {
+ const { rerender, getByTestId } = render(
+
+ );
+
+ // Change props that affect message cells
+ const newProps = {
+ ...defaultProps,
+ contentWidth: 600,
+ isPortrait: false,
+ };
+ rerender();
+
+ expect(getByTestId('message-cell-messageId1')).toBeTruthy();
+ });
+ });
+
+ describe('Viewable Items Change Handling', () => {
+ it('should call updateVisibleMessageImpressions when onViewableItemsChanged is triggered', () => {
+ const mockUpdateVisibleMessageImpressions = jest.fn();
+ const propsWithMockCallback = {
+ ...defaultProps,
+ updateVisibleMessageImpressions: mockUpdateVisibleMessageImpressions,
+ };
+
+ // Render the component
+ const { UNSAFE_root } = render(
+
+ );
+
+ // Find the FlatList component
+ const flatListInstance = UNSAFE_root.findByType(
+ require('react-native').FlatList
+ );
+
+ // Create mock ViewTokens that simulate what FlatList provides
+ const mockViewToken1 = {
+ item: mockRowViewModel1,
+ index: 0,
+ isViewable: true,
+ key: 'messageId1',
+ };
+
+ const mockViewToken2 = {
+ item: mockRowViewModel2,
+ index: 1,
+ isViewable: true,
+ key: 'messageId2',
+ };
+
+ // Simulate the FlatList calling onViewableItemsChanged
+ const mockInfo = {
+ viewableItems: [mockViewToken1, mockViewToken2],
+ changed: [mockViewToken1, mockViewToken2],
+ };
+
+ // Call the onViewableItemsChanged prop directly
+ flatListInstance.props.onViewableItemsChanged(mockInfo);
+
+ // Verify updateVisibleMessageImpressions was called
+ expect(mockUpdateVisibleMessageImpressions).toHaveBeenCalledTimes(1);
+
+ // Verify it was called with the correct structure
+ expect(mockUpdateVisibleMessageImpressions).toHaveBeenCalledWith([
+ {
+ messageId: 'messageId1',
+ silentInbox: expect.any(Boolean),
+ },
+ {
+ messageId: 'messageId2',
+ silentInbox: expect.any(Boolean),
+ },
+ ]);
+ });
+
+ it('should process view tokens and extract messageId and silentInbox', () => {
+ const mockUpdateVisibleMessageImpressions = jest.fn();
+ const propsWithMockCallback = {
+ ...defaultProps,
+ updateVisibleMessageImpressions: mockUpdateVisibleMessageImpressions,
+ };
+
+ const { UNSAFE_root } = render(
+
+ );
+ const flatListInstance = UNSAFE_root.findByType(
+ require('react-native').FlatList
+ );
+
+ const mockViewToken = {
+ item: mockRowViewModel1,
+ index: 0,
+ isViewable: true,
+ key: 'messageId1',
+ };
+
+ const mockInfo = {
+ viewableItems: [mockViewToken],
+ changed: [mockViewToken],
+ };
+
+ flatListInstance.props.onViewableItemsChanged(mockInfo);
+
+ expect(mockUpdateVisibleMessageImpressions).toHaveBeenCalledWith(
+ expect.arrayContaining([
+ expect.objectContaining({
+ messageId: 'messageId1',
+ silentInbox: expect.any(Boolean),
+ }),
+ ])
+ );
+ });
+
+ it('should handle empty viewableItems array', () => {
+ const mockUpdateVisibleMessageImpressions = jest.fn();
+ const propsWithMockCallback = {
+ ...defaultProps,
+ updateVisibleMessageImpressions: mockUpdateVisibleMessageImpressions,
+ };
+
+ const { UNSAFE_root } = render(
+
+ );
+ const flatListInstance = UNSAFE_root.findByType(
+ require('react-native').FlatList
+ );
+
+ const mockInfo = {
+ viewableItems: [],
+ changed: [],
+ };
+
+ flatListInstance.props.onViewableItemsChanged(mockInfo);
+
+ expect(mockUpdateVisibleMessageImpressions).toHaveBeenCalledWith([]);
+ });
+
+ it('should have correct viewability configuration', () => {
+ const { UNSAFE_root } = render(
+
+ );
+ const flatListInstance = UNSAFE_root.findByType(
+ require('react-native').FlatList
+ );
+
+ const viewabilityConfig = flatListInstance.props.viewabilityConfig;
+
+ expect(viewabilityConfig).toEqual({
+ minimumViewTime: 500,
+ itemVisiblePercentThreshold: 100,
+ waitForInteraction: false,
+ });
+ });
+ });
+});