Skip to content
131 changes: 88 additions & 43 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -440,10 +440,8 @@ Implementert:
- Filter som viser bare favorittland, lagret i sessionStorage.
- Responsiv layout for desktop, nettbrett og mobil i stående og liggende
format.

Gjenstår i senere issues:

- Automatiske tester, se [Testing](#testing).
- Automatiske komponent-, hook- og integrasjonstester med Vitest, se
[Testing](#testing).

## Arbeidsflyt

Expand All @@ -453,7 +451,8 @@ pull requests med kodegjennomgang fra et annet gruppemedlem.

## Testing

Automatiske komponent- og hook-tester kjøres med Vitest.
Automatiske komponent-, hook- og integrasjonstester kjøres med Vitest, se
[Automatisk testdekning](#automatisk-testdekning) for hva som er dekket.
Kommandoen `npm run lint` er statisk kodekontroll, ikke en funksjonstest.

### Testoppsett
Expand Down Expand Up @@ -672,44 +671,90 @@ Testet i nettleserens enhetsemulering:
- Lange landnavn, mange språk, store tall og manglende flagg ble testet med
midlertidige testdata og brøt ikke layouten.
- Lastetilstand, feilmelding og tomt favorittutvalg passer inn i layouten.
- Ikke testet på ekte mobil eller i Safari ennå.

Automatiske tester som gjenstår, planlagt i en egen issue:

- `fetchCountries`: vellykket henting, HTTP-feil, nettverksfeil, ugyldig
JSON, uventet format, tom respons, manglende felter og avbrutt
forespørsel, med mocket `fetch`.
- `App`: lastetilstand, feilmelding med riktig tekst, «Prøv igjen», tomt
resultat, og at eksisterende data vises når en ny henting feiler.
- `CountryCard`: fullstendige data, manglende felter og flagg som ikke
kan lastes.
- `CountryNavigation`: forrige/neste gir riktig land, knappene er
deaktivert ved første og siste land, posisjonen «Land X av Y» og liste
med ett land.
- `CountryMenu`: klikk på et land velger det, valgt land har
`aria-current`, favoritter er markert, og tomt utvalg gir melding.
«Landliste»-knappen bytter `aria-expanded`, og valg av land lukker
listen og flytter fokus til knappen.
- `App`: kort og navigasjon viser samme land, og ugyldig eller manglende
valgt kode gir første land.
- `sortCountries`: navn med norske bokstaver, synkende befolkning,
manglende befolkning sist, likt antall etter navn og uendret original.
- `loadSortOrder`/`saveSortOrder`: gyldig og ugyldig lagret verdi, og
lagring som kaster feil.
- `App` med sortering: valgt land beholdes ved bytte av sortering,
navigasjonen følger sortert liste, og valget gjenopprettes ved ny
montering.
- `loadFavorites`/`saveFavorites`: gyldig liste, ugyldig JSON, feil
format, duplikater og lagring som kaster feil.
- `useFavorites`/`FavoriteButton`: legge til og fjerne favoritt,
`aria-pressed`, riktig status ved blaing og gjenoppretting ved ny
montering.
- `loadShowOnlyFavorites`/`saveShowOnlyFavorites`: `"true"`, manglende
eller ugyldig verdi, og lagring som kaster feil.
- `App` med favorittfilter: utvalget inneholder bare favoritter i valgt
sortering, valgt land beholdes eller byttes til første land, tomt
utvalg viser melding og «Vis alle land», og fjerning av siste favoritt
krasjer ikke.

Testet på ekte enheter i Safari, med utviklingsserveren eksponert på
nettverket (`npm run dev -- --host`):

- **iPhone 13 Pro** (mobil, stående og liggende)
- **iPad 11** (nettbrett, stående og liggende)

Ingen horisontal scrolling eller layoutfeil på noen av enhetene.

### Automatisk testdekning

Se [Testoppsett](#testoppsett) for hvordan testene kjøres. Testene ligger
ved siden av filen de tester (for eksempel `CountryCard.test.tsx` ved
siden av `CountryCard.tsx`), og bruker mockede API-svar og en isolert
`QueryClient` der det er aktuelt, se [Testoppsett](#testoppsett).

- **`src/api/countries.test.ts`:** Et rått svar med fem land gir fire
gyldige `Country`-objekter. Et land uten `alpha3Code` hoppes over, et
land med bare `png`-flagg får `flagUrl` satt til `png`-URL-en, og
tekstverdier for `population`/`area` og en `languages`-verdi som ikke
er en array, blir til `undefined`/tom liste. Tomt svar (`[]`) gir tom
liste. HTTP 500, et objekt i stedet for en array, og en `fetch` som
kaster ved `json()`, gir hver sin feilmelding fra
[Feilhåndtering](#feilhåndtering). En avbrutt forespørsel (signal
allerede `aborted`) sender den opprinnelige feilen videre uendret.
- **`src/App.test.tsx`:** En `fetch` som aldri løser seg, viser
«Laster land …». To mislykkede kall (første forsøk og det automatiske
nye forsøket, se `retry: 1` i [Hentestrategien](#hentestrategi-med-tanstack-query))
gir feilmeldingen med «HTTP 500», og et tredje kall via «Prøv igjen»
lykkes og viser «Land 1 av 2». Et tomt svar gir «Fant ingen land.».
Med Norway og Sweden i utvalget: å markere Norway som favoritt og slå
på favorittfilteret gir «Land 1 av 1» og bare Norway i visningen. Uten
favoritter gir filteret meldingen «Du har ingen favorittland å vise.»,
og «Vis alle land» gjenoppretter begge landene. Fjernes Norway som
favoritt mens filteret er på, vises samme melding.
- **`src/components/CountryCard.test.tsx`:** Et fullstendig land viser
«5 400 000», «385 207 km²» og «Norwegian og Sami». Et land uten
hovedstad, region, befolkning, areal, språk eller flagg viser «Ikke
oppgitt» for hvert felt og «Flagg ikke tilgjengelig» i stedet for
bilde. Et flagg som feiler ved lasting (`onError`), bytter til samme
reservetekst.
- **`src/components/CountryMenu.test.tsx`:** Klikk på «Sweden» kaller
`onSelect('SWE')`, og valgt land får `aria-current="true"`. Er Sweden
favoritt, viser knappen stjerne og skjult tekst «(favoritt)». Tomt
utvalg viser «Ingen land i utvalget.» uten landliste. Åpnes
«Landliste»-knappen og et land velges, settes `aria-expanded` til
`false`, og fokus flyttes til knappen.
- **`src/components/CountryNavigation.test.tsx`:** Med Norway, Sweden og
Denmark, og Sweden valgt, viser komponenten «Land 2 av 3». «Neste»
kaller `onSelect('DNK')`, og «Forrige» kaller `onSelect('NOR')`.
«Forrige» er deaktivert ved Norway, «Neste» ved Denmark. En valgt kode
som ikke finnes i utvalget, gir ingen rendret komponent.
- **`src/components/FavoriteButton.test.tsx`:** Klikk kaller `onToggle`
én gang, og `aria-pressed` er `"true"`/`"false"` i tråd med props. En
snapshot-test fanger markeringen (☆/★) i begge tilstander.
- **`src/hooks/useFavorites.test.ts`:** `toggleFavorite('NOR')` gjør
`isFavorite('NOR')` sann og lagrer `["NOR"]` i localStorage. Er
`["BRA","BGD"]` lagret fra før, gjenoppretter en ny montering av hooken
favorittstatusen for begge.
- **`src/utils/favoritesStorage.test.ts`:** `["NOR","SWE","NOR"]` gir
`["NOR","SWE"]` uten duplikat, og
`["NOR", 7, null, "", {"a":1}, "SWE"]` gir samme resultat, siden
verdier som ikke er tekst eller er tomme, hoppes over. Ugyldig JSON og
en lagret verdi som ikke er en array, gir tom liste. Kaster
`localStorage` ved lesing eller skriving, brukes tom liste uten at
appen krasjer.
- **`src/utils/sortCountries.test.ts`:** Navn gir «Albania, Brasil,
Ukjentland, Østland, Ålesundia» (norsk sortering: U før Æ, Ø og Å).
Befolkning gir «Brasil, Ålesundia, Østland, Albania, Ukjentland», med
manglende befolkning sist. Ved lik befolkning brukes navn som
tiebreak, og en frosset originalliste endres ikke.
- **`src/utils/sortOrderStorage.test.ts`:** Lagret `"population"` leses
tilbake uendret, mens den ugyldige verdien `"hacket"` og en `getItem`
som kaster feil, begge gir standardvalget `"name"`.

Gjenstår, ikke kritisk for innleveringen:

- `useShowOnlyFavorites`/`favoritesFilterStorage`: egne tester av
lagringsfunksjonene, i tillegg til dekningen gjennom `App.test.tsx`.
- `App` med sortering: at valgt land beholdes ved bytte av sortering, og
at navigasjonen følger den sorterte listen.
- `App`: at eksisterende data vises videre når en ny henting feiler i
bakgrunnen (beskrevet i [Feilhåndtering](#feilhåndtering)).
- `CountryNavigation` med bare ett land i utvalget.

## KI-bruk

Expand Down
134 changes: 134 additions & 0 deletions src/App.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import { QueryClientProvider } from '@tanstack/react-query';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { describe, expect, it, vi } from 'vitest';
import { createTestQueryClient } from './test/queryClient';
import App from './App';

const RAW_COUNTRIES = [
{ alpha3Code: 'NOR', name: 'Norway', population: 5_400_000 },
{ alpha3Code: 'SWE', name: 'Sweden', population: 10_000_000 },
];

function stubFetchResolved(json: unknown, ok = true, status = 200) {
vi.stubGlobal(
'fetch',
vi.fn().mockResolvedValue({ ok, status, json: async () => json }),
);
}

function renderApp() {
const queryClient = createTestQueryClient();
return render(
<QueryClientProvider client={queryClient}>
<App />
</QueryClientProvider>,
);
}

describe('App', () => {
it('viser lastetilstand mens data hentes', () => {
// Løser seg aldri i denne testen, så komponenten blir stående i pending.
vi.stubGlobal('fetch', vi.fn().mockReturnValue(new Promise(() => {})));

renderApp();

expect(screen.getByRole('status')).toHaveTextContent('Laster land');
});

it('viser feilmelding ved HTTP-feil og henter på nytt ved «Prøv igjen»', async () => {
// useCountries har retry: 1, så de to første kallene (første forsøk og
// det automatiske nye forsøket) må feile før feilsiden vises.
let callCount = 0;
vi.stubGlobal(
'fetch',
vi.fn().mockImplementation(async () => {
callCount += 1;
if (callCount <= 2) {
return { ok: false, status: 500, json: async () => null };
}
return { ok: true, status: 200, json: async () => RAW_COUNTRIES };
}),
);

renderApp();

const alert = await screen.findByRole('alert');
expect(alert).toHaveTextContent('HTTP 500');

await userEvent.click(screen.getByRole('button', { name: 'Prøv igjen' }));

expect(await screen.findByText('Land 1 av 2')).toBeInTheDocument();
});

it('viser melding når API-et ikke returnerer land', async () => {
stubFetchResolved([]);

renderApp();

expect(await screen.findByText('Fant ingen land.')).toBeInTheDocument();
});

it('viser melding om tomt favorittutvalg og lar brukeren vise alle land igjen', async () => {
stubFetchResolved(RAW_COUNTRIES);

renderApp();
await screen.findByText('Land 1 av 2');

await userEvent.click(
screen.getByRole('checkbox', { name: 'Vis bare favoritter' }),
);

expect(
screen.getByText('Du har ingen favorittland å vise.'),
).toBeInTheDocument();

await userEvent.click(
screen.getByRole('button', { name: 'Vis alle land' }),
);

expect(await screen.findByText('Land 1 av 2')).toBeInTheDocument();
});

it('viser bare favoritter når filteret er på', async () => {
stubFetchResolved(RAW_COUNTRIES);

renderApp();
// Norway vises først, siden standardsorteringen er alfabetisk.
await screen.findByText('Land 1 av 2');

await userEvent.click(
screen.getByRole('button', { name: 'Favoritt: Norway' }),
);
await userEvent.click(
screen.getByRole('checkbox', { name: 'Vis bare favoritter' }),
);

expect(screen.getByText('Land 1 av 1')).toBeInTheDocument();
expect(screen.getByRole('heading', { name: 'Norway' })).toBeInTheDocument();
});

it('viser tomt favorittutvalg når siste favoritt fjernes mens filteret er på', async () => {
stubFetchResolved(RAW_COUNTRIES);

renderApp();
await screen.findByText('Land 1 av 2');

await userEvent.click(
screen.getByRole('button', { name: 'Favoritt: Norway' }),
);
await userEvent.click(
screen.getByRole('checkbox', { name: 'Vis bare favoritter' }),
);
expect(screen.getByText('Land 1 av 1')).toBeInTheDocument();

// Fjerner den eneste favoritten mens filteret fortsatt er på.
await userEvent.click(
screen.getByRole('button', { name: 'Favoritt: Norway' }),
);

expect(
screen.getByText('Du har ingen favorittland å vise.'),
).toBeInTheDocument();
});
});
Loading