diff --git a/.gitignore b/.gitignore index a7f24c2..3db747d 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ dist .env *.db .turbo +coverage diff --git a/packages/web/src/pages/Home.test.tsx b/packages/web/src/pages/Home.test.tsx index 6b0764b..d324630 100644 --- a/packages/web/src/pages/Home.test.tsx +++ b/packages/web/src/pages/Home.test.tsx @@ -27,9 +27,9 @@ beforeEach(() => { consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}) global.fetch = vi.fn().mockImplementation((url: string) => { if (url.includes('/meta/genres')) { - return Promise.resolve({ json: () => Promise.resolve(mockGenres) }) + return Promise.resolve({ ok: true, json: () => Promise.resolve(mockGenres) }) } - return Promise.resolve({ json: () => Promise.resolve(mockMovies) }) + return Promise.resolve({ ok: true, json: () => Promise.resolve(mockMovies) }) }) }) @@ -92,7 +92,7 @@ describe('Home — search and genre filter', () => { const fetchError = new Error('Network error') global.fetch = vi.fn().mockImplementation((url: string) => { if (url.includes('/meta/genres')) { - return Promise.resolve({ json: () => Promise.resolve(mockGenres) }) + return Promise.resolve({ ok: true, json: () => Promise.resolve(mockGenres) }) } return Promise.reject(fetchError) }) @@ -107,10 +107,55 @@ describe('Home — search and genre filter', () => { if (url.includes('/meta/genres')) { return Promise.reject(fetchError) } - return Promise.resolve({ json: () => Promise.resolve(mockMovies) }) + return Promise.resolve({ ok: true, json: () => Promise.resolve(mockMovies) }) }) renderHome() await waitFor(() => expect(consoleErrorSpy).toHaveBeenCalledWith('Failed to fetch genres:', fetchError)) }) + + it('does not log console.error when fetch is aborted (component unmount)', async () => { + const abortError = Object.assign(new Error('Aborted'), { name: 'AbortError' }) + global.fetch = vi.fn().mockRejectedValue(abortError) + + renderHome() + // Wait for promises to settle + await new Promise(r => setTimeout(r, 0)) + + expect(consoleErrorSpy).not.toHaveBeenCalled() + }) + + it('logs console.error when movies fetch returns a non-OK HTTP status', async () => { + global.fetch = vi.fn().mockImplementation((url: string) => { + if (url.includes('/meta/genres')) { + return Promise.resolve({ ok: true, json: () => Promise.resolve(mockGenres) }) + } + return Promise.resolve({ ok: false, status: 500 }) + }) + + renderHome() + await waitFor(() => + expect(consoleErrorSpy).toHaveBeenCalledWith( + 'Failed to fetch movies:', + expect.objectContaining({ message: 'HTTP error! status: 500' }) + ) + ) + }) + + it('logs console.error when genres fetch returns a non-OK HTTP status', async () => { + global.fetch = vi.fn().mockImplementation((url: string) => { + if (url.includes('/meta/genres')) { + return Promise.resolve({ ok: false, status: 404 }) + } + return Promise.resolve({ ok: true, json: () => Promise.resolve(mockMovies) }) + }) + + renderHome() + await waitFor(() => + expect(consoleErrorSpy).toHaveBeenCalledWith( + 'Failed to fetch genres:', + expect.objectContaining({ message: 'HTTP error! status: 404' }) + ) + ) + }) }) diff --git a/packages/web/src/pages/Home.tsx b/packages/web/src/pages/Home.tsx index d5bd713..1c51572 100644 --- a/packages/web/src/pages/Home.tsx +++ b/packages/web/src/pages/Home.tsx @@ -20,23 +20,38 @@ function Home() { const [genres, setGenres] = useState([]); useEffect(() => { - fetch('/api/movies') - .then(res => res.json()) + const controller = new AbortController(); + const { signal } = controller; + + fetch('/api/movies', { signal }) + .then(res => { + if (!res.ok) throw new Error(`HTTP error! status: ${res.status}`); + return res.json(); + }) .then((data: Movie[]) => { setMovies(data); setLoading(false); }) .catch((err: unknown) => { + if (err instanceof Error && err.name === 'AbortError') return; console.error('Failed to fetch movies:', err); setLoading(false); }); - fetch('/api/movies/meta/genres') - .then(res => res.json()) + fetch('/api/movies/meta/genres', { signal }) + .then(res => { + if (!res.ok) throw new Error(`HTTP error! status: ${res.status}`); + return res.json(); + }) .then((data: string[]) => setGenres(data)) .catch((err: unknown) => { + if (err instanceof Error && err.name === 'AbortError') return; console.error('Failed to fetch genres:', err); }); + + return () => { + controller.abort(); + }; }, []); const filteredMovies = useMemo(() => {