fix(events): render a not-found state instead of hanging on a 404
EventDetailsPage gated on `if (eventLoading || !event)`. The backend returns a clean 404 for a nonexistent id, but once isLoading settled false `event` stayed undefined forever, so /admin/events/999999 sat on the loading spinner permanently with no error state. Destructure isError and split the gate: spinner while loading, then a not-found Card. Reuses the existing `events.notFound` key (already used by EventFeedbackPage for the same entity) and the Card padding="lg" not-found shape from contracts/ContractDetailPage. No new i18n keys. Refs testplan REPORT.md #5 (Part 7.02).
This commit is contained in:
@@ -5,7 +5,7 @@ import { useTranslation } from 'react-i18next';
|
||||
import { toast } from 'react-toastify';
|
||||
import { useLocalizedDate } from '../../hooks/useLocalizedDate';
|
||||
|
||||
import { Loading } from '../../components/common';
|
||||
import { Button, Card, Loading } from '../../components/common';
|
||||
import { PasswordResetModal, PublishGalleryDialog, SendGalleryEmailDialog, DuplicateEventDialog, EventRenameDialog, AdminGuestsList } from '../../components/admin';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { eventsService } from '../../services/events.service';
|
||||
@@ -101,7 +101,7 @@ export const EventDetailsPage: React.FC = () => {
|
||||
});
|
||||
|
||||
// Fetch event details
|
||||
const { data: event, isLoading: eventLoading, refetch: refetchEvent } = useQuery({
|
||||
const { data: event, isLoading: eventLoading, isError: eventError, refetch: refetchEvent } = useQuery({
|
||||
queryKey: ['admin-event', id],
|
||||
queryFn: () => eventsService.getEvent(parseInt(id!)),
|
||||
enabled: !!id,
|
||||
@@ -324,7 +324,7 @@ export const EventDetailsPage: React.FC = () => {
|
||||
},
|
||||
});
|
||||
|
||||
if (eventLoading || !event) {
|
||||
if (eventLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-[400px]">
|
||||
<Loading size="lg" text={t('events.loadingEventDetails')} />
|
||||
@@ -332,6 +332,19 @@ export const EventDetailsPage: React.FC = () => {
|
||||
);
|
||||
}
|
||||
|
||||
// A 404 (or any settled failure) leaves `event` undefined forever — without
|
||||
// this branch the spinner above never resolved (QA 7.02).
|
||||
if (eventError || !event) {
|
||||
return (
|
||||
<Card padding="lg">
|
||||
<p className="text-neutral-900 dark:text-neutral-100">{t('events.notFound', 'Event not found')}</p>
|
||||
<Button variant="outline" className="mt-4" onClick={() => navigate('/admin/events')}>
|
||||
{t('events.backToEvents')}
|
||||
</Button>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
const expiresAtDate = safeParseDate(event.expires_at);
|
||||
// Timestamp comparison, not truncated whole days (#909): the old
|
||||
// differenceInDays <= 0 marked events "expired" up to 24h early.
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
/**
|
||||
* /admin/events/:id hung on the spinner forever for a nonexistent id
|
||||
* (QA 7.02). The backend returns a clean 404, but the page gated on
|
||||
* `eventLoading || !event`, so once the query settled `event` stayed
|
||||
* undefined and the condition never went false.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import { MemoryRouter, Route, Routes } from 'react-router-dom';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
|
||||
vi.mock('react-i18next', async () => {
|
||||
const actual = await vi.importActual<typeof import('react-i18next')>('react-i18next');
|
||||
return {
|
||||
...actual,
|
||||
useTranslation: () => ({
|
||||
t: (k: string, fb?: unknown) => (typeof fb === 'string' ? fb : k),
|
||||
i18n: { language: 'en' },
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('react-toastify', () => ({ toast: { success: vi.fn(), error: vi.fn(), info: vi.fn() } }));
|
||||
|
||||
const getEvent = vi.fn();
|
||||
vi.mock('../../../services/events.service', () => ({
|
||||
eventsService: {
|
||||
getEvent: (...args: unknown[]) => getEvent(...args),
|
||||
updateEvent: vi.fn(),
|
||||
deleteEvent: vi.fn(),
|
||||
extendExpiration: vi.fn(),
|
||||
duplicateEvent: vi.fn(),
|
||||
resetPassword: vi.fn(),
|
||||
publishEvent: vi.fn(),
|
||||
renameEvent: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('../../../hooks/usePublicSettings', () => ({
|
||||
PUBLIC_SETTINGS_QUERY_KEY: ['public-settings'],
|
||||
usePublicSettings: () => ({ data: {} }),
|
||||
}));
|
||||
|
||||
vi.mock('../../../contexts/FeatureFlagsContext', () => ({
|
||||
useFeatureFlags: () => ({ flags: {}, isLoading: false }),
|
||||
useFeatureEnabled: () => false,
|
||||
}));
|
||||
|
||||
vi.mock('../../../contexts/PermissionsContext', () => ({
|
||||
usePermissions: () => ({ hasAnyPermission: () => true, hasPermission: () => true, isLoading: false }),
|
||||
}));
|
||||
|
||||
import { EventDetailsPage } from '../EventDetailsPage';
|
||||
|
||||
function renderPage() {
|
||||
const qc = new QueryClient({ defaultOptions: { queries: { retry: false } } });
|
||||
return render(
|
||||
<QueryClientProvider client={qc}>
|
||||
<MemoryRouter initialEntries={['/admin/events/999999']}>
|
||||
<Routes>
|
||||
<Route path="/admin/events/:id" element={<EventDetailsPage />} />
|
||||
<Route path="/admin/events" element={<div>events list</div>} />
|
||||
</Routes>
|
||||
</MemoryRouter>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
}
|
||||
|
||||
describe('EventDetailsPage 404 handling (QA 7.02)', () => {
|
||||
it('renders a not-found state instead of spinning forever when the event 404s', async () => {
|
||||
getEvent.mockRejectedValue({ response: { status: 404, data: { error: 'Event not found' } } });
|
||||
|
||||
renderPage();
|
||||
|
||||
expect(screen.getByText('events.loadingEventDetails')).toBeInTheDocument();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Event not found')).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.queryByText('events.loadingEventDetails')).not.toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: 'events.backToEvents' })).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user