/**
* Component: BookDate Page Tests
* Documentation: documentation/features/bookdate.md
*/
// @vitest-environment jsdom
import React from 'react';
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { resetMockRouter, routerMock } from '../helpers/mock-next-navigation';
vi.mock('@/components/layout/Header', () => ({
Header: () =>
,
}));
vi.mock('@/components/bookdate/LoadingScreen', () => ({
LoadingScreen: () => ,
}));
vi.mock('@/components/bookdate/SettingsWidget', () => ({
SettingsWidget: ({
isOpen,
isOnboarding,
onOnboardingComplete,
}: {
isOpen: boolean;
isOnboarding: boolean;
onOnboardingComplete: () => void;
}) => (
),
}));
vi.mock('@/components/bookdate/CardStack', () => ({
CardStack: ({
recommendations,
onSwipe,
onSwipeComplete,
}: {
recommendations: any[];
onSwipe: (action: 'left' | 'right' | 'up', markedAsKnown?: boolean) => void;
onSwipeComplete: () => void;
}) => (
{recommendations.length}
),
}));
const makeJsonResponse = (body: any, ok: boolean = true) => ({
ok,
status: ok ? 200 : 500,
json: async () => body,
});
describe('BookDatePage', () => {
beforeEach(() => {
resetMockRouter();
localStorage.clear();
vi.resetModules();
});
afterEach(() => {
vi.unstubAllGlobals();
});
it('redirects to login when no access token is available', async () => {
const fetchMock = vi.fn();
vi.stubGlobal('fetch', fetchMock);
const { default: BookDatePage } = await import('@/app/bookdate/page');
render();
await waitFor(() => {
expect(routerMock.push).toHaveBeenCalledWith('/login');
});
});
it('shows onboarding settings when onboarding is incomplete', async () => {
localStorage.setItem('accessToken', 'token');
const fetchMock = vi.fn(async (input: RequestInfo) => {
const url = typeof input === 'string' ? input : input.url;
if (url === '/api/bookdate/preferences') {
return makeJsonResponse({ onboardingComplete: false });
}
throw new Error(`Unexpected fetch: ${url}`);
});
vi.stubGlobal('fetch', fetchMock);
const { default: BookDatePage } = await import('@/app/bookdate/page');
render();
expect(await screen.findByText('Welcome to BookDate!')).toBeInTheDocument();
expect(screen.getByTestId('settings-widget')).toHaveAttribute('data-open', 'true');
});
it('loads recommendations after completing onboarding', async () => {
localStorage.setItem('accessToken', 'token');
const fetchMock = vi.fn(async (input: RequestInfo) => {
const url = typeof input === 'string' ? input : input.url;
if (url === '/api/bookdate/preferences') {
return makeJsonResponse({ onboardingComplete: false });
}
if (url === '/api/bookdate/recommendations') {
return makeJsonResponse({ recommendations: [{ id: 'rec-1' }] });
}
throw new Error(`Unexpected fetch: ${url}`);
});
vi.stubGlobal('fetch', fetchMock);
const { default: BookDatePage } = await import('@/app/bookdate/page');
render();
await screen.findByText('Welcome to BookDate!');
fireEvent.click(screen.getByRole('button', { name: 'Finish Onboarding' }));
expect(await screen.findByTestId('card-count')).toHaveTextContent('1');
});
it('loads recommendations when onboarding status check fails', async () => {
localStorage.setItem('accessToken', 'token');
const fetchMock = vi.fn(async (input: RequestInfo) => {
const url = typeof input === 'string' ? input : input.url;
if (url === '/api/bookdate/preferences') {
return makeJsonResponse({ error: 'fail' }, false);
}
if (url === '/api/bookdate/recommendations') {
return makeJsonResponse({ recommendations: [{ id: 'rec-1' }] });
}
throw new Error(`Unexpected fetch: ${url}`);
});
vi.stubGlobal('fetch', fetchMock);
const { default: BookDatePage } = await import('@/app/bookdate/page');
render();
expect(await screen.findByTestId('card-count')).toHaveTextContent('1');
});
it('renders an error state when recommendations fetch fails', async () => {
localStorage.setItem('accessToken', 'token');
const fetchMock = vi.fn(async (input: RequestInfo) => {
const url = typeof input === 'string' ? input : input.url;
if (url === '/api/bookdate/preferences') {
return makeJsonResponse({ onboardingComplete: true });
}
if (url === '/api/bookdate/recommendations') {
return makeJsonResponse({ error: 'bad' }, false);
}
throw new Error(`Unexpected fetch: ${url}`);
});
vi.stubGlobal('fetch', fetchMock);
const { default: BookDatePage } = await import('@/app/bookdate/page');
render();
expect(await screen.findByText(/Could not load recommendations/)).toBeInTheDocument();
fireEvent.click(screen.getByRole('button', { name: 'Try Again' }));
await waitFor(() => {
const recCalls = fetchMock.mock.calls.filter(([input]) => String(input).includes('/api/bookdate/recommendations'));
expect(recCalls.length).toBeGreaterThan(1);
});
});
it('navigates to settings from the error state', async () => {
localStorage.setItem('accessToken', 'token');
const fetchMock = vi.fn(async (input: RequestInfo) => {
const url = typeof input === 'string' ? input : input.url;
if (url === '/api/bookdate/preferences') {
return makeJsonResponse({ onboardingComplete: true });
}
if (url === '/api/bookdate/recommendations') {
return makeJsonResponse({ error: 'bad' }, false);
}
throw new Error(`Unexpected fetch: ${url}`);
});
vi.stubGlobal('fetch', fetchMock);
const { default: BookDatePage } = await import('@/app/bookdate/page');
render();
await screen.findByText(/Could not load recommendations/);
fireEvent.click(screen.getByRole('button', { name: 'Go to Settings' }));
expect(routerMock.push).toHaveBeenCalledWith('/settings');
});
it('shows empty state and triggers recommendation generation', async () => {
localStorage.setItem('accessToken', 'token');
const fetchMock = vi.fn(async (input: RequestInfo) => {
const url = typeof input === 'string' ? input : input.url;
if (url === '/api/bookdate/preferences') {
return makeJsonResponse({ onboardingComplete: true });
}
if (url === '/api/bookdate/recommendations') {
return makeJsonResponse({ recommendations: [] });
}
if (url === '/api/bookdate/generate') {
return makeJsonResponse({ recommendations: [{ id: 'rec-1' }] });
}
throw new Error(`Unexpected fetch: ${url}`);
});
vi.stubGlobal('fetch', fetchMock);
const { default: BookDatePage } = await import('@/app/bookdate/page');
render();
const generateButton = await screen.findByRole('button', { name: 'Get More Recommendations' });
fireEvent.click(generateButton);
await waitFor(() => {
expect(fetchMock).toHaveBeenCalledWith(
'/api/bookdate/generate',
expect.objectContaining({ method: 'POST' })
);
});
});
it('posts swipes and shows undo option', async () => {
localStorage.setItem('accessToken', 'token');
const fetchMock = vi.fn(async (input: RequestInfo) => {
const url = typeof input === 'string' ? input : input.url;
if (url === '/api/bookdate/preferences') {
return makeJsonResponse({ onboardingComplete: true });
}
if (url === '/api/bookdate/recommendations') {
return makeJsonResponse({ recommendations: [{ id: 'rec-1' }, { id: 'rec-2' }] });
}
if (url === '/api/bookdate/swipe') {
return makeJsonResponse({ success: true });
}
throw new Error(`Unexpected fetch: ${url}`);
});
vi.stubGlobal('fetch', fetchMock);
const { default: BookDatePage } = await import('@/app/bookdate/page');
render();
fireEvent.click(await screen.findByRole('button', { name: 'Swipe Left' }));
await waitFor(() => {
expect(fetchMock).toHaveBeenCalledWith(
'/api/bookdate/swipe',
expect.objectContaining({ method: 'POST' })
);
});
expect(await screen.findByRole('button', { name: /Undo/i })).toBeInTheDocument();
});
it('opens settings when the settings button is clicked', async () => {
localStorage.setItem('accessToken', 'token');
const fetchMock = vi.fn(async (input: RequestInfo) => {
const url = typeof input === 'string' ? input : input.url;
if (url === '/api/bookdate/preferences') {
return makeJsonResponse({ onboardingComplete: true });
}
if (url === '/api/bookdate/recommendations') {
return makeJsonResponse({ recommendations: [{ id: 'rec-1' }] });
}
throw new Error(`Unexpected fetch: ${url}`);
});
vi.stubGlobal('fetch', fetchMock);
const { default: BookDatePage } = await import('@/app/bookdate/page');
render();
expect(await screen.findByTestId('card-count')).toHaveTextContent('1');
expect(screen.getByTestId('settings-widget')).toHaveAttribute('data-open', 'false');
fireEvent.click(screen.getByRole('button', { name: 'Open settings' }));
expect(screen.getByTestId('settings-widget')).toHaveAttribute('data-open', 'true');
});
it('undoes a swipe and reloads recommendations', async () => {
localStorage.setItem('accessToken', 'token');
let recommendationsCall = 0;
const fetchMock = vi.fn(async (input: RequestInfo) => {
const url = typeof input === 'string' ? input : input.url;
if (url === '/api/bookdate/preferences') {
return makeJsonResponse({ onboardingComplete: true });
}
if (url === '/api/bookdate/recommendations') {
recommendationsCall += 1;
if (recommendationsCall === 1) {
return makeJsonResponse({ recommendations: [{ id: 'rec-1' }, { id: 'rec-2' }] });
}
return makeJsonResponse({ recommendations: [{ id: 'rec-restored' }] });
}
if (url === '/api/bookdate/swipe') {
return makeJsonResponse({ success: true });
}
if (url === '/api/bookdate/undo') {
return makeJsonResponse({ success: true });
}
throw new Error(`Unexpected fetch: ${url}`);
});
vi.stubGlobal('fetch', fetchMock);
const { default: BookDatePage } = await import('@/app/bookdate/page');
render();
fireEvent.click(await screen.findByRole('button', { name: 'Swipe Left' }));
const undoButton = await screen.findByRole('button', { name: /Undo/i });
fireEvent.click(undoButton);
await waitFor(() => {
expect(fetchMock).toHaveBeenCalledWith(
'/api/bookdate/undo',
expect.objectContaining({ method: 'POST' })
);
});
await waitFor(() => {
expect(screen.getByTestId('card-count')).toHaveTextContent('1');
});
await waitFor(() => {
expect(screen.queryByRole('button', { name: /Undo/i })).toBeNull();
});
});
});