Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/components/MovieCard.css
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
line-clamp: 2;
-webkit-box-orient: vertical;
}

Expand Down
63 changes: 63 additions & 0 deletions src/components/MovieCard.test.tsx
Original file line number Diff line number Diff line change
@@ -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(
<MovieCard
movie={movie}
isFavorite={false}
onToggleFavorite={onToggleFavorite}
onSelectMovie={() => {}}
/>,
);
}

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
66 changes: 66 additions & 0 deletions src/components/MovieList.test.tsx
Original file line number Diff line number Diff line change
@@ -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(
<MovieList
movies={filmer}
isFavorite={() => 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(
<MovieList
movies={filmer}
isFavorite={() => false}
onToggleFavorite={() => {}}
onSelectMovie={() => {}}
/>,
);

await waitFor(() =>
expect(vi.mocked(fetchMovieCredits)).toHaveBeenCalledTimes(3),
);
expect(fetchSpion).not.toHaveBeenCalled();
});

it('viser tom liste uten å krasje', () => {
renderMedQuery(
<MovieList
movies={[]}
isFavorite={() => false}
onToggleFavorite={() => {}}
onSelectMovie={() => {}}
/>,
);

expect(screen.queryAllByRole('heading')).toHaveLength(0);
});
});

//mocking test
35 changes: 35 additions & 0 deletions src/components/SearchField.test.tsx
Original file line number Diff line number Diff line change
@@ -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(<SearchField value="" onChange={() => {}} />);
expect(screen.getByLabelText(LABEL)).toBeInTheDocument();
});

it('viser verdien den får inn som prop', () => {
render(<SearchField value="indiana" onChange={() => {}} />);
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(<SearchField value="" onChange={onChange} />);

await bruker.type(screen.getByLabelText(LABEL), 'hei');

expect(onChange).toHaveBeenCalledTimes(3);
expect(onChange).toHaveBeenNthCalledWith(1, 'h');
});
});

//dette er brukerinteraksjon
58 changes: 58 additions & 0 deletions src/components/Sidebar.test.tsx
Original file line number Diff line number Diff line change
@@ -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(<Sidebar view="hjem" onViewChange={() => {}} />);

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(<Sidebar view="hjem" onViewChange={() => {}} />);

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(<Sidebar view="hjem" onViewChange={onViewChange} />);

await bruker.click(screen.getByRole('button', { name: 'FAVORITTER' }));

expect(onViewChange).toHaveBeenCalledWith('favoritter');
});

it('markerer FAVORITTER som aktiv side når view er favoritter', () => {
render(<Sidebar view="favoritter" onViewChange={() => {}} />);

expect(screen.getByRole('button', { name: 'FAVORITTER' })).toHaveAttribute(
'aria-current',
'page',
);
});
});

// dette er en state test
42 changes: 42 additions & 0 deletions src/components/__snapshots__/MovieCard.test.tsx.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html

exports[`MovieCard > matcher snapshot 1`] = `
<div>
<div
class="movie-card"
>
<button
aria-label="Legg Testfilm til favoritter"
aria-pressed="false"
class="favorite-button movie-card__favorite"
type="button"
>
</button>
<span
class="movie-card__rating"
>
7.5
</span>
<img
alt="Testfilm"
class="movie-card__poster"
/>
<div
class="movie-card__info"
>
<h3
class="movie-card__title"
>
Testfilm
</h3>
<p
class="movie-card__actors"
>
Skuespiller En, Skuespiller To, Skuespiller Tre
</p>
</div>
</div>
</div>
`;
35 changes: 35 additions & 0 deletions src/test/utils.tsx
Original file line number Diff line number Diff line change
@@ -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> = {}): 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 (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
);
}

return render(ui, { wrapper: Wrapper });
}