Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add tests to address lookup / address component #2932

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
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
5 changes: 5 additions & 0 deletions .changeset/cold-jeans-yawn.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@adyen/adyen-web": patch
---

Fix address lookup reseting state field after country change
221 changes: 219 additions & 2 deletions packages/lib/src/components/internal/Address/Address.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,31 @@ import { AddressData } from '../../../types';
import { FALLBACK_VALUE } from './constants';
import { render, screen } from '@testing-library/preact';
import { CoreProvider } from '../../../core/Context/CoreProvider';
import userEvent from '@testing-library/user-event';

jest.mock('../../../core/Services/get-dataset');
(getDataset as jest.Mock).mockImplementation(jest.fn(() => Promise.resolve([{ id: 'NL', name: 'Netherlands' }])));
(getDataset as jest.Mock).mockImplementation(
jest.fn(dataset => {
switch (dataset) {
case 'countries':
return Promise.resolve([
{ id: 'US', name: 'United States' },
{ id: 'CA', name: 'Canada' },
{ id: 'NL', name: 'Netherlands' }
]);
case 'states/US':
return Promise.resolve([
{ id: 'AR', name: 'Arkansas' },
{ id: 'CA', name: 'California' }
]);
case 'states/CA':
return Promise.resolve([
{ id: 'AB', name: 'Alberta' },
{ id: 'BC', name: 'British Columbia' }
]);
}
})
);

describe('Address', () => {
const addressSpecificationsMock: AddressSpecifications = {
Expand Down Expand Up @@ -165,7 +187,7 @@ describe('Address', () => {
expect(receivedData.country).toBe(data.country);
});

test('should not include fields without a value in the data object', () => {
test('should not include fields without a value in the data object', () => {
const data: AddressData = { country: 'NL' };
const onChangeMock = jest.fn();

Expand Down Expand Up @@ -203,4 +225,199 @@ describe('Address', () => {
const receivedData = lastOnChangeCall[0].data;
expect(receivedData.stateOrProvince).toBe(undefined);
});

describe('Country and State or Province', () => {
const user = userEvent.setup();
const onChangeMock = jest.fn();
test('should set the stateOrProvince value to empty and show options when country changes', async () => {
const data: AddressData = { country: 'US' };

customRender(<Address countryCode={'US'} data={data} onChange={onChangeMock} />);

const countrySearch = await screen.findByLabelText('Country/Region');
await user.click(countrySearch);
// write in the searchbar
await user.keyboard('Canada');
// select one option
await user.keyboard('[ArrowDown][Enter]');
const stateSearch = await screen.findByLabelText('Province or Territory');
expect(stateSearch).toBeInTheDocument();
// open the state selector
await user.click(stateSearch);
// check if options are avaliable
expect(screen.getByText('Alberta')).toBeVisible();
});

test('should reset the stateOrProvince value when country changes', async () => {
const data: AddressData = { country: 'US', stateOrProvince: 'CA' };

customRender(<Address countryCode={'US'} data={data} onChange={onChangeMock} />);

// check if US values for state or province are set
expect(await screen.findByDisplayValue('United States')).toBeVisible();
expect(await screen.findByDisplayValue('California')).toBeVisible();

// search for CountryLabel and region, choose Canada
const countrySearch = await screen.findByLabelText('Country/Region');
await user.click(countrySearch);
// write in the searchbar
await user.keyboard('Canada');
// select one option
await user.keyboard('[ArrowDown][Enter]');

// Check if the state has reset to empty value
const stateSearch = await screen.findByLabelText('Province or Territory');
expect(stateSearch).toBeInTheDocument();
expect(stateSearch).toHaveValue('');
});

test('should trigger postal code validation on country change and show error message', async () => {
const data: AddressData = { country: 'US', stateOrProvince: 'CA', postalCode: '90000' };

customRender(<Address countryCode={'US'} data={data} onChange={onChangeMock} />);

// search for CountryLabel and region, choose Canada
const countrySearch = await screen.findByLabelText('Country/Region');
await user.click(countrySearch);
// write in the searchbar
await user.keyboard('Canada');
// select one option
await user.keyboard('[ArrowDown][Enter]');

// Check if the state has reset to empty value
const postalCodeField = await screen.findByRole('textbox', { name: 'Postal code' });
expect(postalCodeField).toBeInTheDocument();
expect(postalCodeField).toHaveValue('90000');
expect(screen.getByText('Invalid format. Expected format: A9A 9A9 or A9A9A9')).toBeVisible();
});
});

describe('AddressSearch in Address', () => {
// 0. delay the test since it rellies on user input
// there's probably a performance optimisation here, but delay was the simples and most reliable way to fix it
const user = userEvent.setup({ delay: 100 });
const onChangeMock = jest.fn();

test('should fill the stateOrProvince field for countries who support state', async () => {
const data: AddressData = {};

// 1. setup the test
// 1a. create mock for this tests
const addressMock = {
id: 1,
name: '1000 Test Road, California',
street: '1000 Test Road',
city: 'Los Santos',
//houseNumberOrName: '',
postalCode: '90000',
country: 'US',
stateOrProvince: 'CA'
};

// 1b. pass the mock to the the mock functions so we get it as the search result
const onAdressSearchMock = jest.fn(async (value, { resolve }) => {
await resolve([addressMock]);
});
const onAddressSelectedMock = jest.fn(async (value, { resolve }) => {
await resolve(addressMock);
});

// 2. render and intereact
customRender(
<Address data={data} onChange={onChangeMock} onAddressLookup={onAdressSearchMock} onAddressSelected={onAddressSelectedMock} />
);
const searchBar = screen.getByRole('combobox');
await user.click(searchBar);
// write in the searchbar
await user.keyboard('mock');
// select one option
await user.keyboard('[ArrowDown][Enter]');

// 3. check filled values are correct
expect(screen.getByDisplayValue('1000 Test Road')).toBeInTheDocument();
expect(screen.getByDisplayValue('90000')).toBeInTheDocument();
expect(screen.getByDisplayValue('California')).toBeInTheDocument();
});

test('should fill the stateOrProvince field for countries who support state', async () => {
const data: AddressData = { country: 'CA' };

// 1. setup the test
// 1a. create mock for this tests
const addressMock = {
id: 1,
name: '1000 Test Road, California',
street: '1000 Test Road',
city: 'Los Santos',
//houseNumberOrName: '',
postalCode: '90000',
country: 'US',
stateOrProvince: 'CA'
};

// 1b. pass the mock to the the mock functions so we get it as the search result
const onAdressSearchMock = jest.fn(async (value, { resolve }) => {
await resolve([addressMock]);
});
const onAddressSelectedMock = jest.fn(async (value, { resolve }) => {
await resolve(addressMock);
});

// 2. render and intereact
customRender(
<Address data={data} onChange={onChangeMock} onAddressLookup={onAdressSearchMock} onAddressSelected={onAddressSelectedMock} />
);
const searchBar = screen.getByRole('combobox');
await user.click(searchBar);
// write in the searchbar
await user.keyboard('mock');
// select one option
await user.keyboard('[ArrowDown][Enter]');

// 3. check filled values are correct
expect(screen.getByDisplayValue('1000 Test Road')).toBeInTheDocument();
expect(screen.getByDisplayValue('90000')).toBeInTheDocument();
expect(screen.getByDisplayValue('California')).toBeInTheDocument();
});

test('should trigger field validation after address is selected', async () => {
const data: AddressData = {};

// 1. setup the test
// 1a. create mock for this tests
const incorrectPostalCodeAddressMock = {
id: 1,
name: '1000 Test Road, California',
street: '1000 Test Road',
city: 'Los Santos',
//houseNumberOrName: '',
postalCode: '9000 AA',
country: 'US',
stateOrProvince: 'CA'
};

// 1b. pass the mock to the the mock functions so we get it as the search result
const onAdressSearchMock = jest.fn(async (value, { resolve }) => {
await resolve([incorrectPostalCodeAddressMock]);
});
const onAddressSelectedMock = jest.fn(async (value, { resolve }) => {
await resolve(incorrectPostalCodeAddressMock);
});

// 2. render and intereact
customRender(
<Address data={data} onChange={onChangeMock} onAddressLookup={onAdressSearchMock} onAddressSelected={onAddressSelectedMock} />
);
const searchBar = screen.getByRole('combobox');
await user.click(searchBar);
// write in the searchbar
await user.keyboard('mock');
// select one option
await user.keyboard('[ArrowDown][Enter]');

// 3. check filled values are correct and error state is triggered
expect(screen.getByRole('textbox', { name: 'Zip code' })).toHaveValue('9000 AA');
expect(screen.getByText('Invalid format. Expected format: 99999 or 99999-9999')).toBeVisible();
});
});
});
24 changes: 18 additions & 6 deletions packages/lib/src/components/internal/Address/Address.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,11 @@ export default function Address(props: AddressProps) {

const showAddressSearch = !!props.onAddressLookup;

const [ignoreCountryChange, setIgnoreCountryChange] = useState(false);

const showAddressFields = props.onAddressLookup ? hasSelectedAddress || useManualAddress : true;

const { data, errors, valid, isValid, handleChangeFor, triggerValidation, setData } = useForm<AddressData>({
const { data, errors, valid, isValid, handleChangeFor, triggerValidation, setData, mergeData } = useForm<AddressData>({
schema: requiredFieldsSchema,
defaultData: props.data,
// Ensure any passed validation rules are merged with the default ones
Expand All @@ -52,13 +54,17 @@ export default function Address(props: AddressProps) {
const setSearchData = useCallback(
(selectedAddress: AddressData) => {
const propsKeysToProcess = ADDRESS_SCHEMA;
propsKeysToProcess.forEach(propKey => {
const newStateData = propsKeysToProcess.reduce((acc: AddressData, 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));
});
if (providedValue !== null && providedValue !== undefined) {
// Cast everything to string
acc[propKey] = String(providedValue);
}
return acc;
}, {});
mergeData(newStateData);
setIgnoreCountryChange(true);
triggerValidation();
setHasSelectedAddress(true);
},
Expand Down Expand Up @@ -93,6 +99,12 @@ export default function Address(props: AddressProps) {
* - Applies validation on postalCode field in case it has any value
*/
useEffect((): void => {
// if the country was set via setSearchData we don't want to trigger this
if (ignoreCountryChange) {
setIgnoreCountryChange(false);
return;
}

const stateOrProvince = specifications.countryHasDataset(data.country) ? '' : FALLBACK_VALUE;
const newData = { ...data, stateOrProvince };

Expand Down
5 changes: 4 additions & 1 deletion packages/lib/src/utils/useForm/reducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,13 +54,16 @@ export function init({ schema, defaultData, processField, fieldProblems }) {
}

export function getReducer(processField) {
return function reducer(state, { type, key, value, mode, schema, defaultData, formValue, selectedSchema, fieldProblems }: any) {
return function reducer(state, { type, key, value, mode, schema, defaultData, formValue, selectedSchema, fieldProblems, data }) {
const validationSchema: string[] = selectedSchema || state.schema;

switch (type) {
case 'setData': {
return { ...state, data: { ...state['data'], [key]: value } };
}
case 'mergeData': {
return { ...state, data: { ...state['data'], ...data } };
}
case 'setValid': {
return { ...state, valid: { ...state['valid'], [key]: value } };
}
Expand Down
1 change: 1 addition & 0 deletions packages/lib/src/utils/useForm/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export interface Form<FormSchema> extends FormState<FormSchema> {
triggerValidation: (schema?: any) => void;
setSchema: (schema: any) => void;
setData: (key: string, value: any) => void;
mergeData: (data: FormSchema) => void;
setValid: (key: string, value: any) => void;
setErrors: (key: string, value: any) => void;
mergeForm: (formValue: any) => void;
Expand Down
2 changes: 2 additions & 0 deletions packages/lib/src/utils/useForm/useForm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ function useForm<FormSchema>(props: FormProps): Form<FormSchema> {
const setErrors = useCallback((key, value) => dispatch({ type: 'setErrors', key, value }), []);
const setValid = useCallback((key, value) => dispatch({ type: 'setValid', key, value }), []);
const setData = useCallback((key, value) => dispatch({ type: 'setData', key, value }), []);
const mergeData = useCallback(data => dispatch({ type: 'mergeData', data }), []);
const setSchema = useCallback(schema => dispatch({ type: 'setSchema', schema, defaultData }), [state.schema]);
const mergeForm = useCallback(formValue => dispatch({ type: 'mergeForm', formValue }), []);
const setFieldProblems = useCallback(fieldProblems => dispatch({ type: 'setFieldProblems', fieldProblems }), [state.schema]);
Expand All @@ -69,6 +70,7 @@ function useForm<FormSchema>(props: FormProps): Form<FormSchema> {
triggerValidation,
setSchema,
setData,
mergeData,
setValid,
setErrors,
isValid,
Expand Down
Loading