You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
312 lines
10 KiB
312 lines
10 KiB
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(
|
|
<QueryClientProvider client={queryClient}>
|
|
<SliderPage />
|
|
</QueryClientProvider>
|
|
);
|
|
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(
|
|
<QueryClientProvider client={queryClient}>
|
|
<SliderPage />
|
|
</QueryClientProvider>
|
|
);
|
|
|
|
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);
|
|
}
|
|
});
|
|
});
|