diff --git a/apps/fastvideo_studio/src/app/datasets/page.test.tsx b/apps/fastvideo_studio/src/app/datasets/page.test.tsx new file mode 100644 index 000000000..3245eba09 --- /dev/null +++ b/apps/fastvideo_studio/src/app/datasets/page.test.tsx @@ -0,0 +1,64 @@ +import { act, fireEvent, render, screen } from '@testing-library/react'; +import { describe, expect, it, vi } from 'vitest'; + +import { HeaderActionsProvider } from '@/components/shell/HeaderActionsContext'; +import { getDatasets, type Dataset } from '@/lib/api'; + +import DatasetsPage from './page'; + +vi.mock('@/lib/api', () => ({ + getDatasets: vi.fn(), +})); + +vi.mock('@/components/datasets/AddDatasetButton', () => ({ + default: () => null, +})); + +vi.mock('@/components/datasets/CreateDatasetModal', () => ({ + default: () => null, +})); + +vi.mock('@/components/datasets/DatasetCard', () => ({ + default: ({ dataset }: { dataset: Dataset }) =>
{dataset.name}
, +})); + +function renderPage() { + return render( + + + , + ); +} + +describe('DatasetsPage', () => { + it('shows loading content before the initial request settles', async () => { + let resolveDatasets: (datasets: Dataset[]) => void = () => {}; + vi.mocked(getDatasets).mockReturnValue( + new Promise((resolve) => { + resolveDatasets = resolve; + }), + ); + + renderPage(); + expect(screen.getByLabelText('Loading datasets')).toBeInTheDocument(); + expect(screen.queryByText('No datasets yet.')).not.toBeInTheDocument(); + + act(() => resolveDatasets([])); + expect(await screen.findByText('No datasets yet.')).toBeInTheDocument(); + }); + + it('shows API failures separately from an empty list and retries', async () => { + vi.spyOn(console, 'error').mockImplementation(() => {}); + vi.mocked(getDatasets).mockRejectedValueOnce(new Error('network down')); + + renderPage(); + expect( + await screen.findByText(/Could not load datasets from the Studio API/), + ).toBeInTheDocument(); + expect(screen.queryByText('No datasets yet.')).not.toBeInTheDocument(); + + vi.mocked(getDatasets).mockResolvedValueOnce([]); + fireEvent.click(screen.getByRole('button', { name: 'Try Again' })); + expect(await screen.findByText('No datasets yet.')).toBeInTheDocument(); + }); +}); diff --git a/apps/fastvideo_studio/src/app/datasets/page.tsx b/apps/fastvideo_studio/src/app/datasets/page.tsx index 2db891113..70458edbf 100644 --- a/apps/fastvideo_studio/src/app/datasets/page.tsx +++ b/apps/fastvideo_studio/src/app/datasets/page.tsx @@ -1,12 +1,14 @@ 'use client'; import * as React from 'react'; +import { AlertTriangle } from 'lucide-react'; import AddDatasetButton from '@/components/datasets/AddDatasetButton'; import CreateDatasetModal from '@/components/datasets/CreateDatasetModal'; import DatasetCard from '@/components/datasets/DatasetCard'; import { HeaderActions } from '@/components/shell/HeaderActionsContext'; import { Card } from '@/components/ui/card'; +import { Button } from '@/components/ui/button'; import { useStore } from '@/hooks/useStore'; import { getDatasets } from '@/lib/api'; import type { Dataset } from '@/lib/api'; @@ -21,18 +23,28 @@ import { export default function DatasetsPage() { const [datasets, setDatasets] = React.useState([]); + const [isInitialLoading, setIsInitialLoading] = React.useState(true); const [error, setError] = React.useState(null); const { open } = useStore(createDatasetModalStore); + const fetchSequence = React.useRef(0); const fetchDatasets = React.useCallback(async () => { + const sequence = ++fetchSequence.current; try { - setDatasets(await getDatasets()); - setError(null); + const next = await getDatasets(); + if (sequence === fetchSequence.current) { + setDatasets(next); + setError(null); + } } catch (err) { console.error('Failed to fetch datasets:', err); - // Distinguish an API outage from a genuinely empty list, so the user - // isn't told they have no datasets when the server is unreachable. - setError(err instanceof Error ? err.message : 'Failed to load datasets'); + if (sequence === fetchSequence.current) { + setError( + 'Could not load datasets from the Studio API. Check the server and try again.', + ); + } + } finally { + if (sequence === fetchSequence.current) setIsInitialLoading(false); } }, []); @@ -52,22 +64,61 @@ export default function DatasetsPage() {
-
- {error ? ( -

{error}

- ) : datasets.length === 0 ? ( -

- No datasets yet. -

- ) : ( - datasets.map((ds) => ( - handleSelectDataset(ds)} +
+ {isInitialLoading ? ( +
+ {[0, 1, 2].map((item) => ( +
+ ))} +
+ ) : error && datasets.length === 0 ? ( +
+ - )) +

+ {error} +

+ +
+ ) : ( + <> + {error && ( +

+ Dataset updates are temporarily unavailable. Showing the + most recent results. +

+ )} + {datasets.length === 0 ? ( +

+ No datasets yet. +

+ ) : ( + datasets.map((ds) => ( + handleSelectDataset(ds)} + /> + )) + )} + )}