Skip to content

Commit

Permalink
Feature: Adds onAddressSelected to address lookup functionality (#2406)
Browse files Browse the repository at this point in the history
* feat: adds onAddresSelected

* fix: change memo to callback

* fix: makes some fnc to useCallback

* feat: typings and blur on close

* add: changeset

* fix: typo in changset

* fix: exported debounce

* add: fix typings

* add: unit test to address search

* fix: assertion count

* fix: sonar cloud code smells
  • Loading branch information
m1aw authored Dec 4, 2023
1 parent f0aefbd commit 9d04b6f
Show file tree
Hide file tree
Showing 10 changed files with 315 additions and 61 deletions.
5 changes: 5 additions & 0 deletions .changeset/fifty-parrots-collect.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@adyen/adyen-web': minor
---

feature: adds new onAddressSelected to fill data when an item is selected in AddressSearch
Original file line number Diff line number Diff line change
Expand Up @@ -493,6 +493,8 @@ const CardInput: FunctionalComponent<CardInputProps> = props => {
partialAddressSchema={partialAddressSchema}
handleAddress={handleAddress}
onAddressLookup={props.onAddressLookup}
onAddressSelected={props.onAddressSelected}
addressSearchDebounceMs={props.addressSearchDebounceMs}
//
iOSFocusedField={iOSFocusedField}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ export const CardFieldsWrapper = ({
setAddressRef,
partialAddressSchema,
onAddressLookup,
onAddressSelected,
addressSearchDebounceMs,
// For this comp (props passed through from CardInput)
amount,
billingAddressRequired,
Expand Down Expand Up @@ -159,6 +161,8 @@ export const CardFieldsWrapper = ({
specifications={partialAddressSchema}
iOSFocusedField={iOSFocusedField}
onAddressLookup={onAddressLookup}
onAddressSelected={onAddressSelected}
addressSearchDebounceMs={addressSearchDebounceMs}
/>
)}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import Analytics from '../../../../core/Analytics';
import RiskElement from '../../../../core/RiskModule';
import { ComponentMethodsRef } from '../../../types';
import { DisclaimerMsgObject } from '../../../internal/DisclaimerMessage/DisclaimerMessage';
import { OnAddressLookupType } from '../../../internal/Address/components/AddressSearch';
import { OnAddressLookupType, OnAddressSelectedType } from '../../../internal/Address/components/AddressSearch';

export interface CardInputValidState {
holderName?: boolean;
Expand Down Expand Up @@ -108,6 +108,8 @@ export interface CardInputProps {
onFocus?: (e) => {};
onLoad?: () => {};
onAddressLookup?: OnAddressLookupType;
onAddressSelected?: OnAddressSelectedType;
addressSearchDebounceMs?: number;
payButton?: (obj) => {};
placeholders?: Placeholders;
positionHolderNameOnTop?: boolean;
Expand Down
33 changes: 19 additions & 14 deletions packages/lib/src/components/internal/Address/Address.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Fragment, h } from 'preact';
import { useEffect, useMemo, useRef, useState } from 'preact/hooks';
import { useCallback, useEffect, useMemo, useRef, useState } from 'preact/hooks';
import Fieldset from '../FormFields/Fieldset';
import ReadOnlyAddress from './components/ReadOnlyAddress';
import { getAddressValidationRules } from './validate';
Expand Down Expand Up @@ -49,22 +49,25 @@ export default function Address(props: AddressProps) {
formatters: addressFormatters
});

const setSearchData = selectedAddress => {
const propsKeysToProcess = ADDRESS_SCHEMA;
propsKeysToProcess.forEach(propKey => {
// Make sure the data provided by the merchant is always strings
const providedValue = selectedAddress[propKey];
if (providedValue === null || providedValue === undefined) return;
// Cast everything to string
setData(propKey, String(providedValue));
const setSearchData = useCallback(
(selectedAddress: AddressData) => {
const propsKeysToProcess = ADDRESS_SCHEMA;
propsKeysToProcess.forEach(propKey => {
// Make sure the data provided by the merchant is always strings
const providedValue = selectedAddress[propKey];
if (providedValue === null || providedValue === undefined) return;
// Cast everything to string
setData(propKey, String(providedValue));
});
triggerValidation();
});
setHasSelectedAddress(true);
};
setHasSelectedAddress(true);
},
[setHasSelectedAddress, triggerValidation, setData]
);

const onManualAddress = () => {
const onManualAddress = useCallback(() => {
setUseManualAddress(true);
};
}, []);

// Expose method expected by (parent) Address.tsx
addressRef.current.showValidation = () => {
Expand Down Expand Up @@ -172,10 +175,12 @@ export default function Address(props: AddressProps) {
{showAddressSearch && (
<AddressSearch
onAddressLookup={props.onAddressLookup}
onAddressSelected={props.onAddressSelected}
onSelect={setSearchData}
onManualAddress={onManualAddress}
externalErrorMessage={searchErrorMessage}
hideManualButton={showAddressFields}
addressSearchDebounceMs={props.addressSearchDebounceMs}
/>
)}
{showAddressFields && (
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
import { h } from 'preact';
import userEvent from '@testing-library/user-event';
import { render, screen, waitFor } from '@testing-library/preact';
import AddressSearch from './AddressSearch';

const ADDRESS_LOOKUP_RESULT = [
{
id: 1,
name: 'Road 1, 2000, UK',
street: 'Road 1'
},
{
id: 2,
name: 'Road 2, 2500, UK',
street: 'Road 2'
},
{
id: 3,
name: 'Road 3, 3000, UK',
street: 'Road 3'
}
];

const ADDRESS_SELECT_RESULT = {
id: 1,
name: 'Road 1, 2000, UK',
street: 'Road 1',
city: 'London',
houseNumberOrName: '1',
postalCode: '2000',
country: 'GB',
raw: {
id: 1,
raw: 'RAW_DATA_MOCK'
}
};
const onAddressLookupMockFn = async (value, { resolve }) => {
resolve(ADDRESS_LOOKUP_RESULT);
};
const onAddressSelectMockFn = async (value, { resolve }) => {
resolve(ADDRESS_SELECT_RESULT);
};

const onAddressSelectMockFnReject =
rejectReason =>
async (value, { reject }) => {
reject(rejectReason);
};

test('onAddressLookupMock should be triggered when typing', async () => {
const user = userEvent.setup({ delay: 100 });

const onAddressLookupMock = jest.fn(onAddressLookupMockFn);

render(
<AddressSearch
onSelect={() => {}}
onManualAddress={() => {}}
externalErrorMessage={'failed'}
onAddressLookup={onAddressLookupMock}
hideManualButton={true}
addressSearchDebounceMs={0}
/>
);

// Helps to make sure all tests ran
expect.assertions(5);

// Get the input
const searchBar = screen.getByRole('combobox');
await user.click(searchBar);
expect(searchBar).toHaveFocus();

await user.keyboard('Test');
expect(searchBar).toHaveValue('Test');

// Test if onAddressLookup is called with the correct values
await waitFor(() => expect(onAddressLookupMock).toHaveBeenCalledTimes(4));
await waitFor(() => expect(onAddressLookupMock.mock.lastCall[0]).toBe('Test'));
// Test if the return of the function is displayed
const resultList = screen.getByRole('listbox');
expect(resultList).toHaveTextContent('Road 1, 2000, UK');
});

test('onSelect is triggered with correct data', async () => {
const user = userEvent.setup({ delay: 100 });

const onAddressLookupMock = jest.fn(onAddressLookupMockFn);
const onAddressSelectMock = jest.fn(onAddressSelectMockFn);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const internalSetDataMock = jest.fn(data => {});

render(
<AddressSearch
onSelect={internalSetDataMock}
onManualAddress={() => {}}
externalErrorMessage={'failed'}
onAddressLookup={onAddressLookupMock}
onAddressSelected={onAddressSelectMock}
hideManualButton={true}
addressSearchDebounceMs={0}
/>
);
const searchBar = screen.getByRole('combobox');

await user.click(searchBar);
await user.keyboard('Test');

// Move down with the keyboard and select the first option
await user.keyboard('[ArrowDown][Enter]');
await waitFor(() => expect(onAddressSelectMock).toHaveBeenCalledTimes(1));
await waitFor(() =>
expect(onAddressSelectMock.mock.lastCall[0]).toStrictEqual({
id: 1,
name: 'Road 1, 2000, UK',
street: 'Road 1'
})
);

// CHeck if the parents select function is called with full data
await waitFor(() => expect(internalSetDataMock.mock.lastCall[0]).toStrictEqual(ADDRESS_SELECT_RESULT));
});

test('rejecting onAddressLookupMock should not trigger error', async () => {
const user = userEvent.setup({ delay: 100 });

const onAddressLookupMock = jest.fn(onAddressSelectMockFnReject({}));

render(
<AddressSearch
onSelect={() => {}}
onManualAddress={() => {}}
externalErrorMessage={'failed'}
onAddressLookup={onAddressLookupMock}
hideManualButton={true}
addressSearchDebounceMs={0}
/>
);

// Helps to make sure all tests ran
expect.assertions(3);

// Get the input
const searchBar = screen.getByRole('combobox');
await user.click(searchBar);
await user.keyboard('Test');

// Still test if correct values are being called
await waitFor(() => expect(onAddressLookupMock).toHaveBeenCalledTimes(4));
await waitFor(() => expect(onAddressLookupMock.mock.lastCall[0]).toBe('Test'));

// Test if no options are displayed
const resultList = screen.getByRole('listbox');
expect(resultList).toHaveTextContent('No options found');

// TODO fix this
// const resultError = screen.getByText('failed');
// expect(resultError).not.toBeVisible();
});

test('rejecting onAddressLookupMock with errorMessage displays error and message', async () => {
const user = userEvent.setup({ delay: 100 });

const onAddressLookupMock = jest.fn(onAddressSelectMockFnReject({ errorMessage: 'Refused Mock' }));

render(
<AddressSearch
onSelect={() => {}}
onManualAddress={() => {}}
externalErrorMessage={'failed'}
onAddressLookup={onAddressLookupMock}
hideManualButton={true}
addressSearchDebounceMs={0}
/>
);

// Helps to make sure all tests ran
expect.assertions(3);

// Get the input
const searchBar = screen.getByRole('combobox');
await user.click(searchBar);
await user.keyboard('Test');

await waitFor(() => expect(onAddressLookupMock).toHaveBeenCalledTimes(4));
await waitFor(() => expect(onAddressLookupMock.mock.lastCall[0]).toBe('Test'));

// Test if no options are displayed
const resultError = screen.getByText('Refused Mock');
expect(resultError).toBeVisible();
});
Loading

0 comments on commit 9d04b6f

Please sign in to comment.