diff --git a/__tests__/pages/apply/address-history.spec.tsx b/__tests__/pages/apply/address-history.spec.tsx new file mode 100644 index 00000000..f6ef1c17 --- /dev/null +++ b/__tests__/pages/apply/address-history.spec.tsx @@ -0,0 +1,153 @@ +import { configureStore } from '@reduxjs/toolkit'; +import { fireEvent, render, screen, waitFor } from '@testing-library/react'; +import { type ReactNode } from 'react'; +import { Provider } from 'react-redux'; + +import { lookUpAddress } from '../../../lib/gateways/internal-api'; +import { ApiCallStatusCode } from '../../../lib/store/apiCallsStatus'; +import AddressHistoryPage from '../../../pages/apply/[resident]/address-history'; +import { generateApplication } from '../../../testUtils/applicationHelper'; + +const personId = 'person-1'; +const application = generateApplication('app-1', personId, true, false); + +const lookUpAddressMock = lookUpAddress as jest.MockedFunction< + typeof lookUpAddress +>; + +jest.mock('next/router', () => ({ + useRouter: () => ({ + push: jest.fn(), + query: { resident: 'person-1' }, + }), +})); + +jest.mock('../../../lib/gateways/internal-api', () => ({ + lookUpAddress: jest.fn(), +})); + +jest.mock('../../../components/application/ApplicantStep', () => ({ + __esModule: true, + default: ({ children }: { children: ReactNode }) => <>{children}, +})); + +const foundAddress = { + UPRN: 123, + line1: '1 Test Street', + line2: '', + line3: '', + line4: '', + postcode: 'E9 6PT', + town: 'London', +}; + +const renderPage = () => { + const store = configureStore({ + reducer: { + application: (state = application) => state, + hrApiCallsStatus: ( + state = { + updateApplication: { + callStatus: ApiCallStatusCode.IDLE, + error: null, + }, + }, + ) => state, + }, + }); + + return render( + + + , + ); +}; + +const submitPostcode = (postcode = 'E9 6PT') => { + fireEvent.change(screen.getByLabelText('Postcode'), { + target: { value: postcode }, + }); + fireEvent.click( + screen.getByTestId( + 'test-apply-resident-address-history-find-address-button', + ), + ); +}; + +describe('Apply resident address history page', () => { + afterEach(() => { + lookUpAddressMock.mockReset(); + }); + + it('falls back to manual entry when lookup throws', async () => { + const consoleError = jest.spyOn(console, 'error').mockImplementation(); + lookUpAddressMock.mockRejectedValue( + new Error('Unable to look up address (500)'), + ); + + renderPage(); + submitPostcode('not a UK postcode'); + + expect( + await screen.findByRole('heading', { name: 'What is your address?' }), + ).toBeInTheDocument(); + expect( + screen.queryByLabelText('Select an address'), + ).not.toBeInTheDocument(); + expect(consoleError).not.toHaveBeenCalled(); + consoleError.mockRestore(); + }); + + it('falls back to manual entry when lookup returns no address list', async () => { + lookUpAddressMock.mockResolvedValue({ + page_count: 0, + total_count: 0, + } as never); + + renderPage(); + submitPostcode(); + + expect( + await screen.findByRole('heading', { name: 'What is your address?' }), + ).toBeInTheDocument(); + expect( + screen.queryByLabelText('Select an address'), + ).not.toBeInTheDocument(); + }); + + it('falls back to manual entry when lookup returns an empty address list', async () => { + lookUpAddressMock.mockResolvedValue({ + address: [], + page_count: 0, + total_count: 0, + }); + + renderPage(); + submitPostcode(); + + expect( + await screen.findByRole('heading', { name: 'What is your address?' }), + ).toBeInTheDocument(); + }); + + it('shows the address list when lookup returns matches', async () => { + lookUpAddressMock.mockResolvedValue({ + address: [foundAddress], + page_count: 1, + total_count: 1, + }); + + renderPage(); + submitPostcode(); + + expect( + await screen.findByLabelText('Select an address'), + ).toBeInTheDocument(); + expect( + screen.queryByRole('heading', { name: 'What is your address?' }), + ).not.toBeInTheDocument(); + await waitFor(() => { + expect(lookUpAddressMock).toHaveBeenCalledWith('E9 6PT'); + }); + }); +}); diff --git a/cypress/e2e/pages/apply/[resident]/address-history.cy.ts b/cypress/e2e/pages/apply/[resident]/address-history.cy.ts index f7bd2421..e723424a 100644 --- a/cypress/e2e/pages/apply/[resident]/address-history.cy.ts +++ b/cypress/e2e/pages/apply/[resident]/address-history.cy.ts @@ -80,4 +80,27 @@ describe('Apply resident address history page', () => { ApplyResidentAddressHistoryPage.getGetSaveAndContinueButton().click(); cy.contains('Unable to update application (409)'); }); + + it('falls back to manual address entry when postcode lookup fails', () => { + cy.intercept('GET', '/api/address/*', { + statusCode: StatusCodes.INTERNAL_SERVER_ERROR, + body: { message: 'Unable to look up address' }, + }).as('failedAddressLookup'); + + ApplyHouseholdPage.visit(); + ApplyHouseholdPage.getContinueToNextStepLink().scrollIntoView().click(); + ApplyExpectPage.getContinueToNextStepButton().click(); + ApplyOverviewPage.getApplicantButton(personId).click(); + ApplyResidentIndexPage.getAddressHistorySectionLink().click(); + + ApplyResidentAddressHistoryPage.getPostcodeInputField().type( + 'not a UK postcode', + ); + ApplyResidentAddressHistoryPage.getFindAddressButton().click(); + cy.wait('@failedAddressLookup'); + ApplyResidentAddressHistoryPage.getManualAddressHeading().should( + 'be.visible', + ); + cy.contains('Select an address').should('not.exist'); + }); }); diff --git a/cypress/pages/apply/[resident]/address-history.ts b/cypress/pages/apply/[resident]/address-history.ts index 969d4764..2f18f01e 100644 --- a/cypress/pages/apply/[resident]/address-history.ts +++ b/cypress/pages/apply/[resident]/address-history.ts @@ -21,6 +21,10 @@ class ApplyResidentAddressHistoryPage { static getMovingDateYear() { return cy.get('#date-year'); } + + static getManualAddressHeading() { + return cy.contains('What is your address?'); + } } export default ApplyResidentAddressHistoryPage; diff --git a/lib/gateways/internal-api.spec.ts b/lib/gateways/internal-api.spec.ts index 8566e173..7dd15d0c 100644 --- a/lib/gateways/internal-api.spec.ts +++ b/lib/gateways/internal-api.spec.ts @@ -1,5 +1,9 @@ import { Application } from '../../domain/HousingApi'; -import { CreateApplicationError, createApplication } from './internal-api'; +import { + CreateApplicationError, + createApplication, + lookUpAddress, +} from './internal-api'; const application = { id: 'app-1' } as Application; @@ -88,3 +92,39 @@ describe('createApplication', () => { consoleError.mockRestore(); }); }); + +describe('lookUpAddress', () => { + afterEach(() => { + jest.restoreAllMocks(); + }); + + it('returns lookup results when the request succeeds', async () => { + const result = { + address: [{ UPRN: 1, line1: '1 Test Street' }], + page_count: 1, + total_count: 1, + }; + mockFetch({ + ok: true, + status: 200, + json: async () => result, + }); + + await expect(lookUpAddress('E9 6PT')).resolves.toEqual(result); + expect(global.fetch).toHaveBeenCalledWith('/api/address/E9%206PT', { + method: 'GET', + }); + }); + + it('throws when the lookup API returns an error body without addresses', async () => { + mockFetch({ + ok: false, + status: 500, + json: async () => ({ message: 'Unable to look up address' }), + }); + + await expect(lookUpAddress('not a UK postcode')).rejects.toThrow( + 'Unable to look up address (500)', + ); + }); +}); diff --git a/lib/gateways/internal-api.ts b/lib/gateways/internal-api.ts index 612acf98..a05a118a 100644 --- a/lib/gateways/internal-api.ts +++ b/lib/gateways/internal-api.ts @@ -41,10 +41,14 @@ const createApplicationErrorMessage = ( }; export const lookUpAddress = async (postCode: string) => { - const res = await fetch(`/api/address/${postCode}`, { + const res = await fetch(`/api/address/${encodeURIComponent(postCode)}`, { method: 'GET', }); + if (!res.ok) { + throw Error(`Unable to look up address (${res.status})`); + } + return (await res.json()) as AddressLookupResult; }; diff --git a/pages/apply/[resident]/address-history.tsx b/pages/apply/[resident]/address-history.tsx index 5ac77139..0825cccb 100644 --- a/pages/apply/[resident]/address-history.tsx +++ b/pages/apply/[resident]/address-history.tsx @@ -294,14 +294,18 @@ const ApplicationStep = (): JSX.Element => { case 'postcode-entry': try { const r = await lookUpAddress(values.postcode); - setPostcodeResults(r.address); + const addresses = r.address ?? []; + if (addresses.length === 0) { + setState('manual-entry'); + break; + } + setPostcodeResults(addresses); formikHelpers.setValues({ ...values, - uprn: r.address[0]?.UPRN.toString(), + uprn: addresses[0].UPRN.toString(), }); setState('choose-address'); - } catch (e) { - console.error(e); + } catch { setState('manual-entry'); }