diff --git a/src/components/MovieCard.css b/src/components/MovieCard.css index c84b12b..00298a2 100644 --- a/src/components/MovieCard.css +++ b/src/components/MovieCard.css @@ -53,6 +53,7 @@ text-overflow: ellipsis; display: -webkit-box; -webkit-line-clamp: 2; + line-clamp: 2; -webkit-box-orient: vertical; } diff --git a/src/components/MovieCard.test.tsx b/src/components/MovieCard.test.tsx new file mode 100644 index 0000000..fe580be --- /dev/null +++ b/src/components/MovieCard.test.tsx @@ -0,0 +1,63 @@ +import { describe, it, expect, vi } from 'vitest'; +import { screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import MovieCard from './MovieCard'; +import { lagFilm, renderMedQuery } from '../test/utils'; + +// Erstatter hele api-modulen med en falsk versjon. MovieCard henter +// skuespillere via useMovieCredits, og uten dette ville testen gjort et +// ekte kall til TMDB. +vi.mock('../api/tmdb', () => ({ + fetchMovieCredits: vi.fn().mockResolvedValue({ + id: 1, + cast: [ + { id: 10, name: 'Skuespiller En', character: 'A', order: 0 }, + { id: 11, name: 'Skuespiller To', character: 'B', order: 1 }, + { id: 12, name: 'Skuespiller Tre', character: 'C', order: 2 }, + ], + }), +})); + +function lagKort(movie = lagFilm(), onToggleFavorite = () => {}) { + return renderMedQuery( + {}} + />, + ); +} + +describe('MovieCard', () => { + it('viser tittelen og ratingen den får inn som props', () => { + lagKort(lagFilm({ title: 'Colony', vote_average: 8.14 })); + + expect(screen.getByRole('heading', { name: 'Colony' })).toBeInTheDocument(); + expect(screen.getByText('★ 8.1')).toBeInTheDocument(); + }); + + it('kaller onToggleFavorite med filmens id når hjertet klikkes', async () => { + const bruker = userEvent.setup(); + const onToggleFavorite = vi.fn(); + lagKort(lagFilm({ id: 42 }), onToggleFavorite); + + await bruker.click( + screen.getByRole('button', { name: 'Legg Testfilm til favoritter' }), + ); + + expect(onToggleFavorite).toHaveBeenCalledWith(42); + }); + + // Venter til skuespillerne er på plass før snapshotet tas, ellers ville + // det vekslet mellom lastetilstand og ferdig kort mellom kjøringer. + it('matcher snapshot', async () => { + const { container } = lagKort(); + + await screen.findByText('Skuespiller En, Skuespiller To, Skuespiller Tre'); + + expect(container).toMatchSnapshot(); + }); +}); + +//snapshot + props test diff --git a/src/components/MovieList.test.tsx b/src/components/MovieList.test.tsx new file mode 100644 index 0000000..471c376 --- /dev/null +++ b/src/components/MovieList.test.tsx @@ -0,0 +1,66 @@ +import { describe, it, expect, vi } from 'vitest'; +import { screen, waitFor } from '@testing-library/react'; +import { MovieList } from './MovieList'; +import { fetchMovieCredits } from '../api/tmdb'; +import { lagFilm, renderMedQuery } from '../test/utils'; + +vi.mock('../api/tmdb', () => ({ + fetchMovieCredits: vi.fn().mockResolvedValue({ id: 1, cast: [] }), +})); + +const filmer = [ + lagFilm({ id: 1, title: 'Colony' }), + lagFilm({ id: 2, title: 'Alfa' }), + lagFilm({ id: 3, title: 'Beta' }), +]; + +describe('MovieList', () => { + it('rendrer ett kort per film', () => { + renderMedQuery( + false} + onToggleFavorite={() => {}} + onSelectMovie={() => {}} + />, + ); + + expect(screen.getAllByRole('heading')).toHaveLength(3); + expect(screen.getByRole('heading', { name: 'Colony' })).toBeInTheDocument(); + }); + + // Selve poenget med mockingen: komponenten oppfører seg som om den hentet + // data, men ingenting går ut på nettet. + it('gjør ingen ekte nettverkskall', async () => { + const fetchSpion = vi.spyOn(globalThis, 'fetch'); + + renderMedQuery( + false} + onToggleFavorite={() => {}} + onSelectMovie={() => {}} + />, + ); + + await waitFor(() => + expect(vi.mocked(fetchMovieCredits)).toHaveBeenCalledTimes(3), + ); + expect(fetchSpion).not.toHaveBeenCalled(); + }); + + it('viser tom liste uten å krasje', () => { + renderMedQuery( + false} + onToggleFavorite={() => {}} + onSelectMovie={() => {}} + />, + ); + + expect(screen.queryAllByRole('heading')).toHaveLength(0); + }); +}); + +//mocking test diff --git a/src/components/SearchField.test.tsx b/src/components/SearchField.test.tsx new file mode 100644 index 0000000..3ca70f0 --- /dev/null +++ b/src/components/SearchField.test.tsx @@ -0,0 +1,35 @@ +import { describe, it, expect, vi } from 'vitest'; +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { SearchField } from './SearchField'; + +const LABEL = 'Søk etter filmer eller skuespillere'; + +describe('SearchField', () => { + // Finner vi feltet via labelen, er htmlFor/id-koblingen riktig satt opp. + // Det er samme kobling en skjermleser bruker. + it('finner feltet via labelen', () => { + render( {}} />); + expect(screen.getByLabelText(LABEL)).toBeInTheDocument(); + }); + + it('viser verdien den får inn som prop', () => { + render( {}} />); + expect(screen.getByLabelText(LABEL)).toHaveValue('indiana'); + }); + + // Feltet er kontrollert: det holder ingen egen state, men melder fra + // ved hvert tastetrykk. Derfor tre kall for tre tegn. + it('kaller onChange for hvert tegn brukeren skriver', async () => { + const bruker = userEvent.setup(); + const onChange = vi.fn(); + render(); + + await bruker.type(screen.getByLabelText(LABEL), 'hei'); + + expect(onChange).toHaveBeenCalledTimes(3); + expect(onChange).toHaveBeenNthCalledWith(1, 'h'); + }); +}); + +//dette er brukerinteraksjon diff --git a/src/components/Sidebar.test.tsx b/src/components/Sidebar.test.tsx new file mode 100644 index 0000000..bb6d96e --- /dev/null +++ b/src/components/Sidebar.test.tsx @@ -0,0 +1,58 @@ +import { describe, it, expect, vi } from 'vitest'; +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { Sidebar } from './Sidebar'; + +// matchMedia-stubben i src/test/setup.ts returnerer matches: false, +// så Sidebar rendrer desktop-varianten i disse testene. +describe('Sidebar', () => { + it('har undermenyen åpen ved første render', () => { + render( {}} />); + + expect(screen.getByLabelText('Skjul undermeny')).toHaveAttribute( + 'aria-expanded', + 'true', + ); + expect( + screen.getByRole('link', { name: 'Populære filmer' }), + ).toBeInTheDocument(); + }); + + // isOpen er komponentens egen state. Her testes at et klikk endrer den, + // og at både DOM-en og aria-expanded følger med. + it('lukker undermenyen når man klikker på pila', async () => { + const bruker = userEvent.setup(); + render( {}} />); + + await bruker.click(screen.getByLabelText('Skjul undermeny')); + + expect(screen.getByLabelText('Vis undermeny')).toHaveAttribute( + 'aria-expanded', + 'false', + ); + expect( + screen.queryByRole('link', { name: 'Populære filmer' }), + ).not.toBeInTheDocument(); + }); + + it('kaller onViewChange med favoritter når FAVORITTER klikkes', async () => { + const bruker = userEvent.setup(); + const onViewChange = vi.fn(); + render(); + + await bruker.click(screen.getByRole('button', { name: 'FAVORITTER' })); + + expect(onViewChange).toHaveBeenCalledWith('favoritter'); + }); + + it('markerer FAVORITTER som aktiv side når view er favoritter', () => { + render( {}} />); + + expect(screen.getByRole('button', { name: 'FAVORITTER' })).toHaveAttribute( + 'aria-current', + 'page', + ); + }); +}); + +// dette er en state test diff --git a/src/components/__snapshots__/MovieCard.test.tsx.snap b/src/components/__snapshots__/MovieCard.test.tsx.snap new file mode 100644 index 0000000..ff1bd00 --- /dev/null +++ b/src/components/__snapshots__/MovieCard.test.tsx.snap @@ -0,0 +1,42 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`MovieCard > matcher snapshot 1`] = ` +
+
+ + + ★ + 7.5 + + Testfilm +
+

+ Testfilm +

+

+ Skuespiller En, Skuespiller To, Skuespiller Tre +

+
+
+
+`; diff --git a/src/test/utils.tsx b/src/test/utils.tsx new file mode 100644 index 0000000..2df917d --- /dev/null +++ b/src/test/utils.tsx @@ -0,0 +1,35 @@ +import type { ReactElement, ReactNode } from 'react'; +import { render } from '@testing-library/react'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import type { Movie } from '../types/tmdb'; + +// Bygger en testfilm. Testen oppgir bare feltene den faktisk bryr seg om. +export function lagFilm(overrides: Partial = {}): Movie { + return { + id: 1, + title: 'Testfilm', + overview: '', + backdrop_path: null, + poster_path: null, + vote_average: 7.5, + release_date: '2024-01-01', + ...overrides, + }; +} + +// Komponenter som bruker TanStack Query må ha en QueryClientProvider rundt seg, +// ellers krasjer de. retry: false gjør at en feilende query ikke prøver på nytt +// og drar testen ut i tid. +export function renderMedQuery(ui: ReactElement) { + const queryClient = new QueryClient({ + defaultOptions: { queries: { retry: false } }, + }); + + function Wrapper({ children }: { children: ReactNode }) { + return ( + {children} + ); + } + + return render(ui, { wrapper: Wrapper }); +}