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
160 changes: 159 additions & 1 deletion country-explorer/src/components/App.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { getCountriesByRegion } from '../api/countries';
import { mockCountries } from '../test/fixtures';
import type { Country } from '../types/country';
import App from './App';

vi.mock('../api/countries', async (importOriginal) => {
Expand All @@ -21,6 +22,76 @@ function renderApp() {
);
}

// Spans multiple subregions with population figures that don't follow the
// alphabetical order, so region filtering and each sort option can be told
// apart from one another.
const filterSortCountries: Country[] = [
{
code: 'DEU',
name: 'Germany',
capital: 'Berlin',
region: 'Europe',
subregion: 'Western Europe',
population: 20000000,
area: 357114,
flagUrl: 'https://example.com/flags/de.svg',
currencies: ['Euro (EUR)'],
languages: ['German'],
},
{
code: 'ITA',
name: 'Italy',
capital: 'Rome',
region: 'Europe',
subregion: 'Southern Europe',
population: 60000000,
area: 301340,
flagUrl: 'https://example.com/flags/it.svg',
currencies: ['Euro (EUR)'],
languages: ['Italian'],
},
{
code: 'NOR',
name: 'Norway',
capital: 'Oslo',
region: 'Europe',
subregion: 'Northern Europe',
population: 5379475,
area: 323802,
flagUrl: 'https://example.com/flags/no.svg',
currencies: ['Norwegian krone (NOK)'],
languages: ['Norwegian'],
},
{
code: 'SWE',
name: 'Sweden',
capital: 'Stockholm',
region: 'Europe',
subregion: 'Northern Europe',
population: 10353442,
area: 450295,
flagUrl: 'https://example.com/flags/se.svg',
currencies: ['Swedish krona (SEK)'],
languages: ['Swedish'],
},
];

// Opens a Dropdown by its current accessible name (label + selected value).
function openDropdown(name: string) {
fireEvent.click(screen.getByRole('button', { name }));
}

// The selected country is kept across a filter/sort change when it's still
// in the list, so order-dependent assertions rewind to the first country
// first rather than assuming what's currently selected.
function rewindToFirstCountry() {
let previous = screen.getByRole('button', { name: 'Previous country' });
while (!previous.hasAttribute('disabled')) {
fireEvent.click(previous);
previous = screen.getByRole('button', { name: 'Previous country' });
}
}

beforeEach(() => {
window.sessionStorage.clear();
window.localStorage.clear();
Expand Down Expand Up @@ -63,7 +134,8 @@ describe('App navigation', () => {
renderApp();
await screen.findByRole('heading', { name: 'Norway' });

fireEvent.change(screen.getByLabelText('Country'), { target: { value: 'SWE' } });
openDropdown('Country Norway');
fireEvent.click(screen.getByRole('radio', { name: 'Sweden' }));

expect(screen.getByRole('heading', { name: 'Sweden' })).toBeInTheDocument();
expect(screen.getByText('Stockholm')).toBeInTheDocument();
Expand All @@ -82,3 +154,89 @@ describe('App navigation', () => {
expect(screen.getByRole('button', { name: 'Next country' })).toBeDisabled();
});
});

describe('App filtering and sorting', () => {
it('filters the countries down to the selected region', async () => {
mockedGetCountriesByRegion.mockResolvedValue(filterSortCountries);
renderApp();
await screen.findByRole('heading', { name: 'Germany' });

openDropdown('Region All regions');
fireEvent.click(screen.getByRole('radio', { name: 'Northern Europe' }));

expect(screen.getByRole('heading', { name: 'Norway' })).toBeInTheDocument();
expect(screen.getByText('Country 1 of 2')).toBeInTheDocument();

fireEvent.click(screen.getByRole('button', { name: 'Next country' }));

expect(screen.getByRole('heading', { name: 'Sweden' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Next country' })).toBeDisabled();
});

it('brings every country back into view once "All regions" is selected', async () => {
mockedGetCountriesByRegion.mockResolvedValue(filterSortCountries);
renderApp();
await screen.findByRole('heading', { name: 'Germany' });

openDropdown('Region All regions');
fireEvent.click(screen.getByRole('radio', { name: 'Northern Europe' }));
expect(screen.getByRole('heading', { name: 'Norway' })).toBeInTheDocument();
expect(screen.getByText('Country 1 of 2')).toBeInTheDocument();

openDropdown('Region Northern Europe');
fireEvent.click(screen.getByRole('radio', { name: 'All regions' }));

// the previously selected country stays selected since it's still
// available, but the full four-country list is back in view
expect(screen.getByRole('heading', { name: 'Norway' })).toBeInTheDocument();
expect(screen.getByText('Country 3 of 4')).toBeInTheDocument();

rewindToFirstCountry();
expect(screen.getByRole('heading', { name: 'Germany' })).toBeInTheDocument();
});

it('sorts the countries alphabetically by name', async () => {
mockedGetCountriesByRegion.mockResolvedValue(filterSortCountries);
renderApp();
await screen.findByRole('heading', { name: 'Germany' });
rewindToFirstCountry();

const expectedOrder = ['Germany', 'Italy', 'Norway', 'Sweden'];
expect(screen.getByRole('heading', { name: expectedOrder[0] })).toBeInTheDocument();

for (const name of expectedOrder.slice(1)) {
fireEvent.click(screen.getByRole('button', { name: 'Next country' }));
expect(screen.getByRole('heading', { name })).toBeInTheDocument();
}
expect(screen.getByRole('button', { name: 'Next country' })).toBeDisabled();
});

it('sorts the countries by population from high to low', async () => {
mockedGetCountriesByRegion.mockResolvedValue(filterSortCountries);
renderApp();
await screen.findByRole('heading', { name: 'Germany' });

openDropdown('Sort by Name (A–Z)');
fireEvent.click(screen.getByRole('radio', { name: 'Population (high to low)' }));
rewindToFirstCountry();

const expectedOrder = ['Italy', 'Germany', 'Sweden', 'Norway'];
expect(screen.getByRole('heading', { name: expectedOrder[0] })).toBeInTheDocument();

for (const name of expectedOrder.slice(1)) {
fireEvent.click(screen.getByRole('button', { name: 'Next country' }));
expect(screen.getByRole('heading', { name })).toBeInTheDocument();
}
expect(screen.getByRole('button', { name: 'Next country' })).toBeDisabled();
});

it('shows a message when the selected region has no matching countries', async () => {
window.sessionStorage.setItem('country-explorer:selectedRegion', 'Eastern Europe');
mockedGetCountriesByRegion.mockResolvedValue(filterSortCountries);
renderApp();

await screen.findByText('No countries match the selected region.');

expect(screen.queryByRole('heading', { level: 3 })).not.toBeInTheDocument();
});
});
66 changes: 66 additions & 0 deletions country-explorer/src/utils/sortCountries.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { describe, expect, it } from 'vitest';
import type { Country } from '../types/country';
import { isSortOption, sortCountries } from './sortCountries';

function makeCountry(overrides: Partial<Country>): Country {
return {
code: 'XXX',
name: 'Country',
capital: '',
region: '',
subregion: '',
population: null,
area: null,
flagUrl: '',
currencies: [],
languages: [],
...overrides,
};
}

const norway = makeCountry({ code: 'NOR', name: 'Norway', population: 5379475 });
const sweden = makeCountry({ code: 'SWE', name: 'Sweden', population: 10353442 });
const iceland = makeCountry({ code: 'ISL', name: 'Iceland', population: 372520 });
const unknownPopulation = makeCountry({ code: 'ATL', name: 'Atlantis', population: null });

describe('sortCountries', () => {
it('sorts alphabetically by name for name-asc', () => {
const result = sortCountries([sweden, norway, iceland], 'name-asc');

expect(result.map((country) => country.name)).toEqual(['Iceland', 'Norway', 'Sweden']);
});

it('sorts by population from high to low for population-desc', () => {
const result = sortCountries([iceland, sweden, norway], 'population-desc');

expect(result.map((country) => country.name)).toEqual(['Sweden', 'Norway', 'Iceland']);
});

it('sorts countries with unknown population last', () => {
const result = sortCountries([unknownPopulation, sweden, iceland], 'population-desc');

expect(result.map((country) => country.name)).toEqual(['Sweden', 'Iceland', 'Atlantis']);
});

it('returns an empty array for both sort options', () => {
expect(sortCountries([], 'name-asc')).toEqual([]);
expect(sortCountries([], 'population-desc')).toEqual([]);
});

it('does not modify the original array or the countries in it', () => {
const original = [sweden, norway, iceland];
const originalOrder = [...original];

const result = sortCountries(original, 'name-asc');

expect(original).toEqual(originalOrder);
expect(result).not.toBe(original);
});

it('recognizes valid and invalid sort option values', () => {
expect(isSortOption('name-asc')).toBe(true);
expect(isSortOption('population-desc')).toBe(true);
expect(isSortOption('area-desc')).toBe(false);
expect(isSortOption(null)).toBe(false);
});
});