/* Copyright 2026 Adobe. All rights reserved. This file is licensed to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ import { fireEvent, render, screen } from '@testing-library/react-native'; import React from 'react'; import { Animated } from 'react-native'; import { Pagination } from './Pagination'; // Use fake timers for animation timing jest.useFakeTimers(); // Stub Animated APIs to avoid act warnings and native errors beforeAll(() => { jest.spyOn(Animated, 'timing').mockReturnValue({ start: (cb?: any) => cb && cb() } as any); jest.spyOn(Animated, 'spring').mockReturnValue({ start: (cb?: any) => cb && cb() } as any); jest.spyOn(Animated, 'parallel').mockReturnValue({ start: (cb?: any) => cb && cb() } as any); jest.spyOn(require('react-native'), 'useColorScheme').mockReturnValue('light'); }); afterAll(() => { (Animated.timing as unknown as jest.Mock)?.mockRestore?.(); (Animated.spring as unknown as jest.Mock)?.mockRestore?.(); (Animated.parallel as unknown as jest.Mock)?.mockRestore?.(); }); describe('Pagination - rendering', () => { it('renders nothing when totalPages <= 1', () => { const { toJSON } = render( {}} /> ); expect(toJSON()).toBeNull(); }); }); describe('Pagination - interactions', () => { it('renders dots and calls onPageChange when a dot is pressed', () => { const onPageChange = jest.fn(); render(); const dots = screen.getAllByRole('button'); expect(dots.length).toBe(3); // press the last dot (page index 2) fireEvent.press(dots[2]); expect(onPageChange).toHaveBeenCalledWith(2); }); }); describe('Pagination - theming and sizing', () => { it('uses theme default colors when activeColor/inactiveColor not provided (light)', () => { jest.spyOn(require('react-native'), 'useColorScheme').mockReturnValue('light'); render( {}} />); const views = screen.UNSAFE_getAllByType(Animated.View); // Filter to dot views by borderRadius style (unique to dot style) const dotViews = views.filter((v) => { const styles = (Array.isArray(v.props.style) ? v.props.style : [v.props.style]).flat(Infinity).filter(Boolean); return styles.some((s: any) => s && s.borderRadius === 50); }); const dot0Styles = (Array.isArray(dotViews[0].props.style) ? dotViews[0].props.style : [dotViews[0].props.style]).flat(Infinity); const dot1Styles = (Array.isArray(dotViews[1].props.style) ? dotViews[1].props.style : [dotViews[1].props.style]).flat(Infinity); expect(dot0Styles.some((s: any) => s && s.backgroundColor === '#0a7ea4')).toBe(true); // active expect(dot1Styles.some((s: any) => s && s.backgroundColor === '#687076')).toBe(true); // inactive }); it('uses theme default colors when activeColor/inactiveColor not provided (dark)', () => { jest.spyOn(require('react-native'), 'useColorScheme').mockReturnValue('dark'); render( {}} />); const views = screen.UNSAFE_getAllByType(Animated.View); const dotViews = views.filter((v) => { const styles = (Array.isArray(v.props.style) ? v.props.style : [v.props.style]).flat(Infinity).filter(Boolean); return styles.some((s: any) => s && s.borderRadius === 50); }); const dot0Styles = (Array.isArray(dotViews[0].props.style) ? dotViews[0].props.style : [dotViews[0].props.style]).flat(Infinity); const dot1Styles = (Array.isArray(dotViews[1].props.style) ? dotViews[1].props.style : [dotViews[1].props.style]).flat(Infinity); expect(dot0Styles.some((s: any) => s && s.backgroundColor === '#fff')).toBe(true); // active expect(dot1Styles.some((s: any) => s && s.backgroundColor === '#9BA1A6')).toBe(true); // inactive }); it('respects custom active/inactive colors and dot size/spacing', () => { render( {}} activeColor="red" inactiveColor="gray" dotSize={12} spacing={10} /> ); const views = screen.UNSAFE_getAllByType(Animated.View); const dotViews = views.filter((v) => { const styles = (Array.isArray(v.props.style) ? v.props.style : [v.props.style]).flat(Infinity).filter(Boolean); return styles.some((s: any) => s && s.borderRadius === 50); }); const dot0Styles = (Array.isArray(dotViews[0].props.style) ? dotViews[0].props.style : [dotViews[0].props.style]).flat(Infinity); expect(dot0Styles.some((s: any) => s && s.backgroundColor === 'red')).toBe(true); expect(dot0Styles.some((s: any) => s && s.width === 12 && s.height === 12)).toBe(true); }); }); describe('Pagination - sliding and windowing', () => { it('slides when the visible window changes (Animated.timing called)', () => { const timingSpy = jest.spyOn(Animated, 'timing'); const { rerender } = render( {}} /> ); // Move within the same window (no slide expected) rerender( {}} />); const callsBefore = (timingSpy as unknown as jest.Mock).mock.calls.length; // Move to next window (should slide) rerender( {}} />); const callsAfter = (timingSpy as unknown as jest.Mock).mock.calls.length; expect(callsAfter).toBeGreaterThan(callsBefore); }); it('adjusts start page near the end (endPage === totalPages - 1 branch)', () => { const onPageChange = jest.fn(); render( ); // Expect visible pages to be [5,6,7,8,9]; pressing first dot should send 5 const dots = screen.getAllByRole('button'); expect(dots.length).toBe(5); fireEvent.press(dots[0]); expect(onPageChange).toHaveBeenCalledWith(5); fireEvent.press(dots[4]); expect(onPageChange).toHaveBeenCalledWith(9); }); it('slides backward when the visible window shifts left (direction -1)', () => { const timingSpy = jest.spyOn(Animated, 'timing'); const setValueSpy = jest.spyOn((Animated.Value as any).prototype, 'setValue'); const { rerender } = render( {}} /> ); rerender( {}} />); expect((timingSpy as unknown as jest.Mock).mock.calls.length).toBeGreaterThan(0); const calls = (setValueSpy as unknown as jest.Mock).mock.calls as any[]; const lastNumericArg = [...calls].reverse().map(c => c[0]).find(a => typeof a === 'number'); expect(lastNumericArg).toBeLessThan(0); // negative offset indicates sliding left (setValueSpy as unknown as jest.Mock).mockRestore?.(); }); });