diff --git a/country-explorer/src/components/App.test.tsx b/country-explorer/src/components/App.test.tsx index 9f40a98..184c94c 100644 --- a/country-explorer/src/components/App.test.tsx +++ b/country-explorer/src/components/App.test.tsx @@ -1,4 +1,4 @@ -import { fireEvent, render, screen } from '@testing-library/react'; +import { fireEvent, render, screen, within } from '@testing-library/react'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { getCountriesByRegion } from '../api/countries'; @@ -93,8 +93,6 @@ function rewindToFirstCountry() { } beforeEach(() => { - window.sessionStorage.clear(); - window.localStorage.clear(); mockedGetCountriesByRegion.mockResolvedValue(mockCountries); }); @@ -240,3 +238,130 @@ describe('App filtering and sorting', () => { expect(screen.queryByRole('heading', { level: 3 })).not.toBeInTheDocument(); }); }); + +describe('App storage and restoration', () => { + const regionKey = 'country-explorer:selectedRegion'; + const sortKey = 'country-explorer:sortOption'; + const favoritesKey = 'country-explorer:favoriteCountryCodes'; + + it('stores filter and sorting choices and restores them after a fresh mount', async () => { + mockedGetCountriesByRegion.mockResolvedValue(filterSortCountries); + const app = renderApp(); + await screen.findByRole('heading', { name: 'Germany' }); + openDropdown('Region All regions'); + fireEvent.click(screen.getByRole('radio', { name: 'Northern Europe' })); + openDropdown('Sort by Name (A–Z)'); + fireEvent.click(screen.getByRole('radio', { name: 'Population (high to low)' })); + expect(sessionStorage.getItem(regionKey)).toBe('Northern Europe'); + expect(sessionStorage.getItem(sortKey)).toBe('population-desc'); + expect(localStorage.getItem(regionKey)).toBeNull(); + expect(localStorage.getItem(sortKey)).toBeNull(); + + app.unmount(); + renderApp(); + await screen.findByRole('heading', { name: 'Sweden' }); + expect(screen.getByRole('button', { name: 'Region Northern Europe' })).toBeInTheDocument(); + expect( + screen.getByRole('button', { name: 'Sort by Population (high to low)' }), + ).toBeInTheDocument(); + expect(screen.getByText('Country 1 of 2')).toBeInTheDocument(); + fireEvent.click(screen.getByRole('button', { name: 'Next country' })); + expect(screen.getByRole('heading', { name: 'Norway' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'Next country' })).toBeDisabled(); + }); + + it('persists added and removed favorites across fresh mounts and sessions', async () => { + const app = renderApp(); + await screen.findByRole('heading', { name: 'Norway' }); + fireEvent.click(screen.getByRole('button', { name: 'Add Norway to favorites' })); + expect(JSON.parse(localStorage.getItem(favoritesKey)!)).toEqual(['NOR']); + expect(sessionStorage.getItem(favoritesKey)).toBeNull(); + app.unmount(); + sessionStorage.clear(); + + const restarted = renderApp(); + await screen.findByRole('heading', { name: 'Norway' }); + expect(screen.getByRole('button', { name: 'Remove Norway from favorites' })).toHaveAttribute( + 'aria-pressed', + 'true', + ); + const favorites = screen.getByRole('region', { name: 'Favorites' }); + expect(within(favorites).getByRole('button', { name: 'Norway' })).toBeInTheDocument(); + fireEvent.click(screen.getByRole('button', { name: 'Remove Norway from favorites' })); + expect(JSON.parse(localStorage.getItem(favoritesKey)!)).toEqual([]); + restarted.unmount(); + renderApp(); + await screen.findByRole('heading', { name: 'Norway' }); + expect(screen.getByRole('button', { name: 'Add Norway to favorites' })).toHaveAttribute( + 'aria-pressed', + 'false', + ); + expect(screen.getByText("You haven't added any favorites yet.")).toBeInTheDocument(); + }); + + it.each(['invalid-sort', '', '{"sort":true}'])( + 'falls back to alphabetical sorting for %j', + async (stored) => { + sessionStorage.setItem(sortKey, stored); + renderApp(); + await screen.findByRole('heading', { name: 'Norway' }); + expect(screen.getByRole('button', { name: 'Sort by Name (A–Z)' })).toBeInTheDocument(); + expect(sessionStorage.getItem(sortKey)).toBe('name-asc'); + }, + ); + + it('handles an unknown stored filter and lets the user recover', async () => { + sessionStorage.setItem(regionKey, '{invalid region}'); + renderApp(); + await screen.findByText('No countries match the selected region.'); + openDropdown('Region {invalid region}'); + fireEvent.click(screen.getByRole('radio', { name: 'All regions' })); + expect(screen.getByRole('heading', { name: 'Norway' })).toBeInTheDocument(); + expect(sessionStorage.getItem(regionKey)).toBe(''); + }); + + it.each(['{broken', 'null', '{}', '42', '"NOR"', '[null, 7, false, ""]'])( + 'ignores invalid favorite data %j without crashing', + async (stored) => { + localStorage.setItem(favoritesKey, stored); + renderApp(); + await screen.findByRole('heading', { name: 'Norway' }); + expect(screen.getByText("You haven't added any favorites yet.")).toBeInTheDocument(); + expect(JSON.parse(localStorage.getItem(favoritesKey)!)).toEqual([]); + }, + ); + + it('restores valid favorites from a mixed array and ignores unavailable countries', async () => { + localStorage.setItem(favoritesKey, JSON.stringify(['NOR', null, 7, '', 'UNKNOWN'])); + renderApp(); + await screen.findByRole('heading', { name: 'Norway' }); + const favorites = screen.getByRole('region', { name: 'Favorites' }); + expect(within(favorites).getAllByRole('button')).toHaveLength(1); + expect(within(favorites).getByRole('button', { name: 'Norway' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'Remove Norway from favorites' })).toHaveAttribute( + 'aria-pressed', + 'true', + ); + }); + + it('remains usable when storage access throws', async () => { + vi.spyOn(Storage.prototype, 'getItem').mockImplementation(() => { + throw new Error('Storage blocked'); + }); + vi.spyOn(Storage.prototype, 'setItem').mockImplementation(() => { + throw new Error('Storage full'); + }); + renderApp(); + await screen.findByRole('heading', { name: 'Norway' }); + fireEvent.click(screen.getByRole('button', { name: 'Add Norway to favorites' })); + expect(screen.getByRole('button', { name: 'Remove Norway from favorites' })).toHaveAttribute( + 'aria-pressed', + 'true', + ); + openDropdown('Sort by Name (A–Z)'); + fireEvent.click(screen.getByRole('radio', { name: 'Population (high to low)' })); + expect( + screen.getByRole('button', { name: 'Sort by Population (high to low)' }), + ).toBeInTheDocument(); + }); +}); diff --git a/country-explorer/src/test/setup.ts b/country-explorer/src/test/setup.ts index 5fcf45e..8ed4148 100644 --- a/country-explorer/src/test/setup.ts +++ b/country-explorer/src/test/setup.ts @@ -1,10 +1,18 @@ import '@testing-library/jest-dom/vitest'; -import { afterEach, vi } from 'vitest'; +import { afterEach, beforeEach, vi } from 'vitest'; import { cleanup } from '@testing-library/react'; +beforeEach(() => { + window.sessionStorage.clear(); + window.localStorage.clear(); +}); + afterEach(() => { // no globals: true, so RTL won't auto-cleanup between tests cleanup(); + vi.restoreAllMocks(); vi.resetAllMocks(); + window.sessionStorage.clear(); + window.localStorage.clear(); });