Skip to content
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { render } from '@testing-library/react';
import { setupServer } from 'msw/node';
import { rest } from 'msw';
import type { SetupServerApi } from 'msw/node';
import { fetch } from 'whatwg-fetch';
import { useSandbox } from '@18f/identity-test-helpers';
import userEvent from '@testing-library/user-event';
import AddressSearch, { ADDRESS_SEARCH_URL } from './address-search';

const DEFAULT_RESPONSE = [
{
address: '100 Main St E, Bronwood, Georgia, 39826',
location: {
latitude: 31.831686000000005,
longitude: -84.363768,
},
street_address: '100 Main St E',
city: 'Bronwood',
state: 'GA',
zip_code: '39826',
},
];

describe('AddressSearch', () => {
const sandbox = useSandbox();

let server: SetupServerApi;
before(() => {
global.window.fetch = fetch;
Copy link
Copy Markdown
Contributor Author

@allthesignals allthesignals Dec 16, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Real fetch is needed for this, but changing that in the spec_helper breaks other tests

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The useDefineProperty helper may have helped for the approach we ended up taking, as an alternative to duplicating the restored original fetch. Or something like...

let originalFetch;

before(() => {
  originalFetch = window.fetch;
  window.fetch = fetch;
});

after(() => {
  window.fetch = originalFetch;
});

But hopefully we can get native fetch working so we won't need this anyways.

server = setupServer(
rest.post(ADDRESS_SEARCH_URL, (_req, res, ctx) => res(ctx.json(DEFAULT_RESPONSE))),
);
server.listen();
});

after(() => {
server.close();
global.window.fetch = () => Promise.reject(new Error('Fetch must be stubbed'));
});

it('fires the callback with correct input', async () => {
Copy link
Copy Markdown
Contributor Author

@allthesignals allthesignals Dec 16, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This makes me reconsider how opinionated this component should be about the return value... I don't think it's appropriate for it only return the best match, and maybe that should be dealt with on the consumer side. This component should return all results.

const handleAddressFound = sandbox.stub();
const { findByText, findByLabelText } = render(
<AddressSearch onAddressFound={handleAddressFound} />,
);

await userEvent.type(
await findByLabelText('in_person_proofing.body.location.po_search.address_search_label'),
'200 main',
);
await userEvent.click(
await findByText('in_person_proofing.body.location.po_search.search_button'),
);

await expect(handleAddressFound).to.eventually.be.calledWith(DEFAULT_RESPONSE[0]);
});

it('validates input and shows inline error', async () => {
const { findByText } = render(<AddressSearch />);

await userEvent.click(
await findByText('in_person_proofing.body.location.po_search.search_button'),
);

await findByText('in_person_proofing.body.location.inline_error');
});
});
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import { TextInput, Button } from '@18f/identity-components';
import { TextInput } from '@18f/identity-components';
import { request } from '@18f/identity-request';
import { useState, useCallback, ChangeEvent, useRef, Ref } from 'react';
import { useState, useCallback, ChangeEvent, useRef, useEffect } from 'react';
import { useI18n } from '@18f/identity-react-i18n';
import ValidatedField from '@18f/identity-validated-field/validated-field';
import SpinnerButton, { SpinnerButtonRefHandle } from '@18f/identity-spinner-button/spinner-button';
import type { RegisterFieldCallback } from '@18f/identity-form-steps';
import useSWR from 'swr';

interface Location {
street_address: string;
Expand All @@ -14,31 +17,49 @@ interface Location {

interface AddressSearchProps {
onAddressFound?: (location: Location) => void;
registerField: (field: string) => Ref<HTMLInputElement>;
registerField?: RegisterFieldCallback;
}

export const ADDRESS_SEARCH_URL = '/api/addresses';

function AddressSearch({ onAddressFound = () => {}, registerField }: AddressSearchProps) {
function requestAddressCandidates(unvalidatedAddressInput: string): Promise<Location[]> {
return request<Location[]>(ADDRESS_SEARCH_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
json: { address: unvalidatedAddressInput },
});
}

function AddressSearch({
onAddressFound = () => {},
registerField = () => undefined,
}: AddressSearchProps) {
const validatedFieldRef = useRef<HTMLFormElement | null>(null);
const [unvalidatedAddressInput, setUnvalidatedAddressInput] = useState('');
const [addressQuery, setAddressQuery] = useState('');
const { t } = useI18n();
const { data: addressCandidates } = useSWR([ADDRESS_SEARCH_URL, addressQuery], () =>
addressQuery ? requestAddressCandidates(unvalidatedAddressInput) : null,
);
const ref = useRef<SpinnerButtonRefHandle>(null);

useEffect(() => {
if (addressCandidates) {
const bestMatchedAddress = addressCandidates[0];
onAddressFound(bestMatchedAddress);
ref.current?.toggleSpinner(false);
}
}, [addressCandidates]);

const handleAddressSearch = useCallback(
async (event) => {
(event) => {
event.preventDefault();
validatedFieldRef.current?.reportValidity();
if (unvalidatedAddressInput === '') {
return;
}
const addressCandidates = await request<Location>(ADDRESS_SEARCH_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
json: { address: unvalidatedAddressInput },
});

const bestMatchedAddress = addressCandidates[0];
onAddressFound(bestMatchedAddress);
setAddressQuery(unvalidatedAddressInput);
},
[unvalidatedAddressInput],
);
Expand All @@ -63,9 +84,16 @@ function AddressSearch({ onAddressFound = () => {}, registerField }: AddressSear
hint={t('in_person_proofing.body.location.po_search.address_search_hint')}
/>
</ValidatedField>
<Button type="submit" className="margin-y-5" onClick={handleAddressSearch}>
<SpinnerButton
isWide
isBig
ref={ref}
type="submit"
className="margin-y-5"
onClick={handleAddressSearch}
>
{t('in_person_proofing.body.location.po_search.search_button')}
</Button>
</SpinnerButton>
</>
);
}
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@
"jsdom": "^20.0.0",
"mocha": "^10.0.0",
"mq-polyfill": "^1.1.8",
"msw": "^0.49.2",
"postcss": "^8.4.17",
"prettier": "^2.7.1",
"react-test-renderer": "^17.0.2",
Expand Down
Loading