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