import { render, screen, fireEvent, waitFor, cleanup } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest'; import SliderPage from './slider-page'; const mockMutateAsync = vi.fn(); const mockGetMarriageProfile = vi.fn(); const mockSetQueryData = vi.fn(); const mockReplace = vi.fn(); const mockRemoveItem = vi.spyOn(Storage.prototype, 'removeItem'); import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; vi.mock('@/hooks/marriage/use-profile-basic', () => ({ useUpdateMarriageProfileBasicMutation: () => ({ mutateAsync: mockMutateAsync, isPending: false, }), })); vi.mock('@/hooks/marriage/use-profile-main', () => ({ getMarriageProfile: (...args: any[]) => mockGetMarriageProfile(...args), useMarriageProfileQuery: () => ({ data: undefined, refetch: vi.fn() }), })); vi.mock('@/hooks/marriage/query-keys', () => ({ marriageQueryKeys: { profile: () => ['marriage', 'profile'], config: () => ['marriage', 'config'], }, })); vi.mock('next/navigation', () => ({ useRouter: () => ({ replace: mockReplace, prefetch: vi.fn(), back: vi.fn(), }), })); vi.mock('@/translations/provider', () => ({ useI18n: () => ({ locale: 'en', dictionary: { "Something went wrong. Please check your internet connection and try again.": "Something went wrong. Please check your internet connection and try again.", "Accept & Continue": "Accept & Continue", }, }), })); vi.mock('@/translations/config', () => ({ localizePath: (path: string, locale: string) => path, })); describe('SliderPage', () => { let queryClient: QueryClient; beforeEach(() => { vi.resetAllMocks(); queryClient = new QueryClient(); vi.spyOn(queryClient, 'setQueryData'); }); afterEach(() => { vi.useRealTimers(); cleanup(); }); const navigateToFinalSlide = async () => { render( ); const acceptBtn = screen.getByRole('button', { name: /accept/i }); if (!acceptBtn.hasAttribute('disabled')) { fireEvent.click(acceptBtn); } const dots = screen.getAllByRole('button', { name: /go to slide/i }); fireEvent.click(dots[4]); // Go directly to slide 5 return await screen.findByText('Finish'); }; it('a. Success: PATCH ok, GET ok -> replace', async () => { const finishBtn = await navigateToFinalSlide(); const initialRemoveCount = mockRemoveItem.mock.calls.filter(call => call[0] === 'marriage-slider-answers').length; let resolveGet: any; mockMutateAsync.mockResolvedValueOnce({ status: 'pending_info', gender: 'female', is_registering_for_self: true }); mockGetMarriageProfile.mockReturnValueOnce(new Promise(resolve => { resolveGet = resolve; })); fireEvent.click(finishBtn); // Wait for PATCH to resolve and GET to be called await waitFor(() => { expect(mockMutateAsync).toHaveBeenCalled(); expect(mockGetMarriageProfile).toHaveBeenCalled(); }); // Assert navigation and localStorage clear did not happen before GET resolves expect(mockReplace).not.toHaveBeenCalled(); const currentRemoveCount = mockRemoveItem.mock.calls.filter(call => call[0] === 'marriage-slider-answers').length; expect(currentRemoveCount).toBe(initialRemoveCount); // Resolve GET resolveGet({ status: 'pending_info', gender: 'female', is_registering_for_self: true }); await waitFor(() => { expect(queryClient.setQueryData).toHaveBeenCalled(); const finalRemoveCount = mockRemoveItem.mock.calls.filter(call => call[0] === 'marriage-slider-answers').length; expect(finalRemoveCount).toBe(initialRemoveCount + 1); expect(mockReplace).toHaveBeenCalledWith('/questions-list'); }); }); it('b. PATCH failure: no nav, Error UI, Retry works', async () => { const finishBtn = await navigateToFinalSlide(); const initialRemoveCount = mockRemoveItem.mock.calls.filter(call => call[0] === 'marriage-slider-answers').length; mockMutateAsync.mockRejectedValueOnce(new Error('Network Error')); fireEvent.click(finishBtn); await waitFor(() => { expect(screen.getByRole('alert')).toHaveTextContent('Something went wrong. Please check your internet connection and try again.'); }); expect(mockReplace).not.toHaveBeenCalled(); const currentRemoveCount = mockRemoveItem.mock.calls.filter(call => call[0] === 'marriage-slider-answers').length; expect(currentRemoveCount).toBe(initialRemoveCount); // Retry succeeds mockMutateAsync.mockResolvedValueOnce({ status: 'pending_info', gender: 'female', is_registering_for_self: true }); mockGetMarriageProfile.mockResolvedValueOnce({ status: 'pending_info', gender: 'female', is_registering_for_self: true }); const retryBtn = screen.getByText('Finish'); fireEvent.click(retryBtn); await waitFor(() => { expect(mockReplace).toHaveBeenCalledWith('/questions-list'); }); const finalRemoveCount = mockRemoveItem.mock.calls.filter(call => call[0] === 'marriage-slider-answers').length; expect(finalRemoveCount).toBe(initialRemoveCount + 1); }); it('c. Slow request: Promise pending -> no redirect', async () => { const finishBtn = await navigateToFinalSlide(); vi.useFakeTimers({ toFake: ['setTimeout', 'clearTimeout', 'setInterval', 'clearInterval', 'Date'] }); let resolvePatch: any; mockMutateAsync.mockReturnValueOnce(new Promise(resolve => { resolvePatch = resolve; })); mockGetMarriageProfile.mockResolvedValueOnce({ status: 'pending_info', gender: 'female', is_registering_for_self: true }); fireEvent.click(finishBtn); await vi.advanceTimersByTimeAsync(6000); expect(mockReplace).not.toHaveBeenCalled(); resolvePatch({ status: 'pending_info', gender: 'female', is_registering_for_self: true }); vi.useRealTimers(); await waitFor(() => { expect(mockReplace).toHaveBeenCalledWith('/questions-list'); }); }); it('d. GET with completed profile basics routes even if status still says onboarding', async () => { const finishBtn = await navigateToFinalSlide(); mockMutateAsync.mockResolvedValueOnce({ status: 'pending_info', gender: 'female', is_registering_for_self: true }); mockGetMarriageProfile.mockResolvedValueOnce({ status: 'pending_onboarding', gender: 'female', is_registering_for_self: true }); fireEvent.click(finishBtn); await waitFor(() => { expect(mockReplace).toHaveBeenCalledWith('/questions-list'); }); }); it('e. Double click: only 1 request', async () => { const finishBtn = await navigateToFinalSlide(); let resolvePatch: any; mockMutateAsync.mockReturnValueOnce(new Promise(resolve => { resolvePatch = resolve; })); mockGetMarriageProfile.mockResolvedValueOnce({ status: 'pending_info', gender: 'female', is_registering_for_self: true }); fireEvent.click(finishBtn); fireEvent.click(finishBtn); fireEvent.click(finishBtn); expect(mockMutateAsync).toHaveBeenCalledTimes(1); resolvePatch({ status: 'pending_info', gender: 'female', is_registering_for_self: true }); await waitFor(() => { expect(mockReplace).toHaveBeenCalled(); }); }); it('f. GET with an advanced status routes to the matching page', async () => { const finishBtn = await navigateToFinalSlide(); mockMutateAsync.mockResolvedValueOnce({ status: 'pending_info', gender: 'female', is_registering_for_self: true }); mockGetMarriageProfile.mockResolvedValueOnce({ status: 'waiting', gender: 'female', is_registering_for_self: true }); fireEvent.click(finishBtn); await waitFor(() => { expect(mockReplace).toHaveBeenCalledWith('/finding-match'); }); }); it('g. Slide 0 navigation guard: cannot swipe or click dots when rules not read', async () => { // Render slider page with rules container that has not scrolled to bottom // We mock element scroll properties before render const origScrollHeight = Object.getOwnPropertyDescriptor(HTMLElement.prototype, 'scrollHeight'); const origClientHeight = Object.getOwnPropertyDescriptor(HTMLElement.prototype, 'clientHeight'); const origScrollTop = Object.getOwnPropertyDescriptor(HTMLElement.prototype, 'scrollTop'); Object.defineProperty(HTMLElement.prototype, 'scrollHeight', { configurable: true, value: 2000 }); Object.defineProperty(HTMLElement.prototype, 'clientHeight', { configurable: true, value: 500 }); Object.defineProperty(HTMLElement.prototype, 'scrollTop', { configurable: true, value: 0 }); try { render( ); const acceptBtn = screen.getByRole('button', { name: /accept/i }); expect(acceptBtn).toBeDisabled(); // Attempting to click dot 2 (index 1) should be disabled and not navigate const dots = screen.getAllByRole('button', { name: /go to slide/i }); expect(dots[1]).toBeDisabled(); fireEvent.click(dots[1]); // We should still be on Slide 1 (terms) expect(screen.getByText('Terms & Conditions')).toBeInTheDocument(); // Swipe touch gesture forward (touchStart at 200, touchEnd at 50 -> deltaX = 150 > 40) const main = screen.getByRole('main'); fireEvent.touchStart(main, { touches: [{ clientX: 200 }] }); fireEvent.touchEnd(main, { changedTouches: [{ clientX: 50 }] }); // Still on Slide 1 (terms) expect(screen.getByText('Terms & Conditions')).toBeInTheDocument(); } finally { if (origScrollHeight) Object.defineProperty(HTMLElement.prototype, 'scrollHeight', origScrollHeight); if (origClientHeight) Object.defineProperty(HTMLElement.prototype, 'clientHeight', origClientHeight); if (origScrollTop) Object.defineProperty(HTMLElement.prototype, 'scrollTop', origScrollTop); } }); });