richie-education 2.15.1 → 2.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.storybook/main.js +17 -0
- package/.storybook/preview-body.html +1 -0
- package/.storybook/preview-head.html +1 -0
- package/.storybook/preview.tsx +38 -0
- package/jest/setup.ts +17 -0
- package/js/components/AddressesManagement/AddressForm.spec.tsx +191 -0
- package/js/components/AddressesManagement/AddressForm.tsx +29 -1
- package/js/components/AddressesManagement/index.spec.tsx +42 -182
- package/js/components/AddressesManagement/validationSchema.spec.ts +147 -0
- package/js/components/AddressesManagement/validationSchema.ts +51 -0
- package/js/components/Banner/index.stories.tsx +32 -0
- package/js/components/Form/CheckboxField.stories.tsx +13 -0
- package/js/components/Form/Field.stories.tsx +16 -0
- package/js/components/Form/Inputs.tsx +3 -3
- package/js/components/Form/RadioField.stories.tsx +18 -0
- package/js/components/Form/SelectField.stories.tsx +26 -0
- package/js/components/Form/TextAreaField.stories.tsx +13 -0
- package/js/components/Form/TextField.stories.tsx +13 -0
- package/js/components/Icon/index.stories.tsx +11 -0
- package/js/components/Modal/index.stories.tsx +42 -0
- package/js/components/SaleTunnelStepPayment/index.spec.tsx +1 -1
- package/js/components/SaleTunnelStepValidation/index.spec.tsx +1 -1
- package/js/components/Search/FiltersPaneCloseButton.tsx +49 -0
- package/js/components/Search/index.spec.tsx +57 -25
- package/js/components/Search/index.tsx +60 -49
- package/js/components/SearchFiltersPane/index.tsx +4 -1
- package/js/components/Spinner/index.stories.tsx +26 -0
- package/package.json +12 -2
- package/scss/components/templates/search/_search.scss +17 -0
- package/scss/objects/_form.scss +26 -21
- package/stories/Introduction.stories.mdx +13 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
module.exports = {
|
|
2
|
+
"stories": [
|
|
3
|
+
"../stories/**/*.stories.mdx",
|
|
4
|
+
"../stories/**/*.stories.@(js|jsx|ts|tsx)",
|
|
5
|
+
"../js/**/*.stories.@(js|jsx|ts|tsx)",
|
|
6
|
+
],
|
|
7
|
+
"addons": [
|
|
8
|
+
"@storybook/addon-links",
|
|
9
|
+
"@storybook/addon-essentials",
|
|
10
|
+
"@storybook/addon-interactions"
|
|
11
|
+
],
|
|
12
|
+
"framework": "@storybook/react",
|
|
13
|
+
"core": {
|
|
14
|
+
"builder": "@storybook/builder-webpack5"
|
|
15
|
+
},
|
|
16
|
+
staticDirs: ['../../richie/static', '../../richie/apps/core/templates/richie'],
|
|
17
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<div id="modal-exclude" style="min-height: 0;"></div>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<link rel="stylesheet" type="text/css" href="/richie/css/main.css">
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import { useAsyncEffect } from "../js/utils/useAsyncEffect";
|
|
3
|
+
|
|
4
|
+
export const parameters = {
|
|
5
|
+
actions: {argTypesRegex: "^on[A-Z].*"},
|
|
6
|
+
controls: {
|
|
7
|
+
matchers: {
|
|
8
|
+
color: /(background|color)$/i,
|
|
9
|
+
date: /Date$/,
|
|
10
|
+
},
|
|
11
|
+
},
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
const IconsWrapper = props => {
|
|
16
|
+
const [symbols, setSymbols] = useState('');
|
|
17
|
+
|
|
18
|
+
useAsyncEffect(async () => {
|
|
19
|
+
const response = await fetch('/icons.html');
|
|
20
|
+
const body = await response.text();
|
|
21
|
+
setSymbols(body);
|
|
22
|
+
}, []);
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<div>
|
|
26
|
+
<div dangerouslySetInnerHTML={{__html: symbols}}/>
|
|
27
|
+
{props.children}
|
|
28
|
+
</div>
|
|
29
|
+
);
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export const decorators = [
|
|
33
|
+
(Story) => (
|
|
34
|
+
<IconsWrapper>
|
|
35
|
+
<Story/>
|
|
36
|
+
</IconsWrapper>
|
|
37
|
+
),
|
|
38
|
+
];
|
package/jest/setup.ts
CHANGED
|
@@ -4,6 +4,23 @@ import '@testing-library/jest-dom/extend-expect';
|
|
|
4
4
|
import { setLogger } from 'react-query';
|
|
5
5
|
import { noop } from 'utils';
|
|
6
6
|
|
|
7
|
+
/*
|
|
8
|
+
* A little trick to prevent so package to be reset when using `jest.resetModules()`.
|
|
9
|
+
* https://github.com/facebook/jest/issues/8987#issuecomment-584898030
|
|
10
|
+
*/
|
|
11
|
+
const RESET_MODULE_EXCEPTIONS = ['react', 'react-intl'];
|
|
12
|
+
|
|
13
|
+
const mockActualRegistry: Record<PropertyKey, any> = {};
|
|
14
|
+
|
|
15
|
+
RESET_MODULE_EXCEPTIONS.forEach((moduleName) => {
|
|
16
|
+
jest.doMock(moduleName, () => {
|
|
17
|
+
if (!mockActualRegistry[moduleName]) {
|
|
18
|
+
mockActualRegistry[moduleName] = jest.requireActual(moduleName);
|
|
19
|
+
}
|
|
20
|
+
return mockActualRegistry[moduleName];
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
|
|
7
24
|
/* Prevent log error during tests */
|
|
8
25
|
setLogger({
|
|
9
26
|
// eslint-disable-next-line no-console
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { act } from '@testing-library/react-hooks';
|
|
2
|
+
import { fireEvent, getByText, render, screen } from '@testing-library/react';
|
|
3
|
+
import * as mockFactories from 'utils/test/factories';
|
|
4
|
+
import { AddressFactory } from 'utils/test/factories';
|
|
5
|
+
import countries from 'i18n-iso-countries';
|
|
6
|
+
import { IntlProvider } from 'react-intl';
|
|
7
|
+
import { Address } from 'types/Joanie';
|
|
8
|
+
import { ErrorKeys } from './validationSchema';
|
|
9
|
+
import AddressForm from './AddressForm';
|
|
10
|
+
|
|
11
|
+
jest.mock('hooks/useAddresses', () => ({
|
|
12
|
+
useAddresses: () => ({
|
|
13
|
+
states: {
|
|
14
|
+
creating: false,
|
|
15
|
+
updating: false,
|
|
16
|
+
},
|
|
17
|
+
}),
|
|
18
|
+
}));
|
|
19
|
+
|
|
20
|
+
describe('AddressForm', () => {
|
|
21
|
+
const handleReset = jest.fn();
|
|
22
|
+
const onSubmit = jest.fn();
|
|
23
|
+
|
|
24
|
+
beforeEach(() => {
|
|
25
|
+
jest.resetAllMocks();
|
|
26
|
+
jest.resetModules();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('renders a button with label "Use this address" when no address is provided', () => {
|
|
30
|
+
render(
|
|
31
|
+
<IntlProvider locale="en">
|
|
32
|
+
<AddressForm handleReset={handleReset} onSubmit={onSubmit} />
|
|
33
|
+
</IntlProvider>,
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
screen.getByRole('button', { name: 'Use this address' });
|
|
37
|
+
expect(screen.queryByRole('button', { name: 'Cancel' })).toBeNull();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('renders a button with label "Use this address" and a cancel button when no address is provided', async () => {
|
|
41
|
+
const address: Address = AddressFactory.generate();
|
|
42
|
+
render(
|
|
43
|
+
<IntlProvider locale="en">
|
|
44
|
+
<AddressForm handleReset={handleReset} onSubmit={onSubmit} address={address} />
|
|
45
|
+
</IntlProvider>,
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
screen.getByRole('button', { name: 'Update this address' });
|
|
49
|
+
|
|
50
|
+
const $button = screen.getByRole('button', { name: 'Cancel' });
|
|
51
|
+
await act(async () => {
|
|
52
|
+
fireEvent.click($button);
|
|
53
|
+
});
|
|
54
|
+
expect(handleReset).toHaveBeenCalledTimes(1);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('renders an error message when a value in the form is invalid', async () => {
|
|
58
|
+
render(
|
|
59
|
+
<IntlProvider locale="en">
|
|
60
|
+
<AddressForm handleReset={handleReset} onSubmit={onSubmit} />
|
|
61
|
+
</IntlProvider>,
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
screen.getByRole('form');
|
|
65
|
+
const $titleInput = screen.getByRole('textbox', { name: 'Address title' });
|
|
66
|
+
const $firstnameInput = screen.getByRole('textbox', { name: "Recipient's first name" });
|
|
67
|
+
const $lastnameInput = screen.getByRole('textbox', { name: "Recipient's last name" });
|
|
68
|
+
const $addressInput = screen.getByRole('textbox', { name: 'Address' });
|
|
69
|
+
const $cityInput = screen.getByRole('textbox', { name: 'City' });
|
|
70
|
+
const $postcodeInput = screen.getByRole('textbox', { name: 'Postcode' });
|
|
71
|
+
const $countryInput = screen.getByRole('combobox', { name: 'Country' });
|
|
72
|
+
const $submitButton = screen.getByRole('button', {
|
|
73
|
+
name: 'Use this address',
|
|
74
|
+
}) as HTMLButtonElement;
|
|
75
|
+
|
|
76
|
+
// - Until form is not fulfill, submit button should be disabled
|
|
77
|
+
expect($submitButton.disabled).toBe(true);
|
|
78
|
+
|
|
79
|
+
// - User fulfills address fields
|
|
80
|
+
const address = mockFactories.AddressFactory.generate();
|
|
81
|
+
|
|
82
|
+
await act(async () => {
|
|
83
|
+
fireEvent.input($titleInput, { target: { value: address.title } });
|
|
84
|
+
fireEvent.change($firstnameInput, { target: { value: address.first_name } });
|
|
85
|
+
fireEvent.change($lastnameInput, { target: { value: address.last_name } });
|
|
86
|
+
fireEvent.change($addressInput, { target: { value: address.address } });
|
|
87
|
+
fireEvent.change($cityInput, { target: { value: address.city } });
|
|
88
|
+
fireEvent.change($postcodeInput, { target: { value: address.postcode } });
|
|
89
|
+
fireEvent.change($countryInput, { target: { value: address.country } });
|
|
90
|
+
// - As form validation is triggered on blur, we need to trigger this event in
|
|
91
|
+
// order to update form state.
|
|
92
|
+
fireEvent.blur($countryInput);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// Once the form has been fulfilled properly, submit button should be enabled.
|
|
96
|
+
expect($submitButton.disabled).toBe(false);
|
|
97
|
+
|
|
98
|
+
// Before submitting, we change field values to corrupt the form data
|
|
99
|
+
await act(async () => {
|
|
100
|
+
fireEvent.input($titleInput, { target: { value: 'a' } });
|
|
101
|
+
fireEvent.input($firstnameInput, { target: { value: '' } });
|
|
102
|
+
fireEvent.change($countryInput, { target: { value: '-' } });
|
|
103
|
+
fireEvent.click($submitButton);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
expect(onSubmit).not.toHaveBeenCalled();
|
|
107
|
+
|
|
108
|
+
// Error messages should have been displayed.
|
|
109
|
+
// Title field should have a message saying that the value is too short.
|
|
110
|
+
getByText($titleInput.closest('.form-field')!, 'The minimum length is 2 chars.');
|
|
111
|
+
// Firstname field should have a message saying that the value is required.
|
|
112
|
+
getByText($firstnameInput.closest('.form-field')!, 'This field is required.');
|
|
113
|
+
// Country field should have a message saying that the value is not valid.
|
|
114
|
+
getByText(
|
|
115
|
+
$countryInput.closest('.form-field')!,
|
|
116
|
+
`You must select a value within: ${Object.keys(countries.getAlpha2Codes()).join(', ')}.`,
|
|
117
|
+
);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('renders default error message when error message does not exist', async () => {
|
|
121
|
+
jest.doMock('./validationSchema', () => ({
|
|
122
|
+
__esModule: true,
|
|
123
|
+
...jest.requireActual('./validationSchema'),
|
|
124
|
+
errorMessages: {
|
|
125
|
+
[ErrorKeys.MIXED_INVALID]: {
|
|
126
|
+
id: 'components.AddressesManagement.validationSchema.mixedInvalid',
|
|
127
|
+
defaultMessage: 'This field is invalid.',
|
|
128
|
+
description: 'Error message displayed when a field value is invalid.',
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
}));
|
|
132
|
+
|
|
133
|
+
// Import locally to get module with mocked error messages.
|
|
134
|
+
const Form = jest.requireActual('./AddressForm').default;
|
|
135
|
+
|
|
136
|
+
render(
|
|
137
|
+
<IntlProvider locale="en">
|
|
138
|
+
<Form handleReset={handleReset} onSubmit={onSubmit} />
|
|
139
|
+
</IntlProvider>,
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
screen.getByRole('form');
|
|
143
|
+
const $titleInput = screen.getByRole('textbox', { name: 'Address title' });
|
|
144
|
+
const $firstnameInput = screen.getByRole('textbox', { name: "Recipient's first name" });
|
|
145
|
+
const $lastnameInput = screen.getByRole('textbox', { name: "Recipient's last name" });
|
|
146
|
+
const $addressInput = screen.getByRole('textbox', { name: 'Address' });
|
|
147
|
+
const $cityInput = screen.getByRole('textbox', { name: 'City' });
|
|
148
|
+
const $postcodeInput = screen.getByRole('textbox', { name: 'Postcode' });
|
|
149
|
+
const $countryInput = screen.getByRole('combobox', { name: 'Country' });
|
|
150
|
+
const $submitButton = screen.getByRole('button', {
|
|
151
|
+
name: 'Use this address',
|
|
152
|
+
}) as HTMLButtonElement;
|
|
153
|
+
|
|
154
|
+
// - Until form is not fulfill, submit button should be disabled
|
|
155
|
+
expect($submitButton.disabled).toBe(true);
|
|
156
|
+
|
|
157
|
+
// - User fulfills address fields
|
|
158
|
+
const address = mockFactories.AddressFactory.generate();
|
|
159
|
+
|
|
160
|
+
await act(async () => {
|
|
161
|
+
fireEvent.input($titleInput, { target: { value: address.title } });
|
|
162
|
+
fireEvent.change($firstnameInput, { target: { value: address.first_name } });
|
|
163
|
+
fireEvent.change($lastnameInput, { target: { value: address.last_name } });
|
|
164
|
+
fireEvent.change($addressInput, { target: { value: address.address } });
|
|
165
|
+
fireEvent.change($cityInput, { target: { value: address.city } });
|
|
166
|
+
fireEvent.change($postcodeInput, { target: { value: address.postcode } });
|
|
167
|
+
fireEvent.change($countryInput, { target: { value: address.country } });
|
|
168
|
+
// - As form validation is triggered on blur, we need to trigger this event in
|
|
169
|
+
// order to update form state.
|
|
170
|
+
fireEvent.blur($countryInput);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
// Once the form has been fulfilled properly, submit button should be enabled.
|
|
174
|
+
expect($submitButton.disabled).toBe(false);
|
|
175
|
+
|
|
176
|
+
// Before submitting, we change field values to corrupt the form data
|
|
177
|
+
await act(async () => {
|
|
178
|
+
fireEvent.input($titleInput, { target: { value: 'a' } });
|
|
179
|
+
fireEvent.input($firstnameInput, { target: { value: '' } });
|
|
180
|
+
fireEvent.change($countryInput, { target: { value: '-' } });
|
|
181
|
+
fireEvent.click($submitButton);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
expect(onSubmit).not.toHaveBeenCalled();
|
|
185
|
+
|
|
186
|
+
// Default error messages should have been displayed.
|
|
187
|
+
getByText($titleInput.closest('.form-field')!, 'This field is invalid.');
|
|
188
|
+
getByText($firstnameInput.closest('.form-field')!, 'This field is invalid.');
|
|
189
|
+
getByText($countryInput.closest('.form-field')!, `This field is invalid.`);
|
|
190
|
+
});
|
|
191
|
+
});
|
|
@@ -7,7 +7,8 @@ import { messages } from 'components/AddressesManagement/index';
|
|
|
7
7
|
import { CheckboxField, SelectField, TextField } from 'components/Form';
|
|
8
8
|
import { useAddresses } from 'hooks/useAddresses';
|
|
9
9
|
import type { Address } from 'types/Joanie';
|
|
10
|
-
import
|
|
10
|
+
import { Maybe } from 'types/utils';
|
|
11
|
+
import validationSchema, { ErrorKeys, errorMessages } from './validationSchema';
|
|
11
12
|
|
|
12
13
|
export type AddressFormValues = Omit<Address, 'id' | 'is_main'> & { save: boolean };
|
|
13
14
|
|
|
@@ -41,6 +42,25 @@ const AddressForm = ({ handleReset, onSubmit, address }: Props) => {
|
|
|
41
42
|
const [languageCode] = intl.locale.split('-');
|
|
42
43
|
const countryList = countries.getNames(languageCode);
|
|
43
44
|
|
|
45
|
+
const getLocalizedErrorMessage = (
|
|
46
|
+
error: Maybe<
|
|
47
|
+
| string
|
|
48
|
+
| {
|
|
49
|
+
key: ErrorKeys;
|
|
50
|
+
values: Record<PropertyKey, string | number | Array<string | number>>;
|
|
51
|
+
}
|
|
52
|
+
>,
|
|
53
|
+
) => {
|
|
54
|
+
if (!error) return undefined;
|
|
55
|
+
|
|
56
|
+
if (typeof error === 'string' || errorMessages[error.key] === undefined) {
|
|
57
|
+
// If the error has not been translated we return a default error message.
|
|
58
|
+
return intl.formatMessage(errorMessages[ErrorKeys.MIXED_INVALID]);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return intl.formatMessage(errorMessages[error.key], error.values);
|
|
62
|
+
};
|
|
63
|
+
|
|
44
64
|
/**
|
|
45
65
|
* Prevent form to be submitted and clear `editedAddress` state.
|
|
46
66
|
*/
|
|
@@ -62,6 +82,7 @@ const AddressForm = ({ handleReset, onSubmit, address }: Props) => {
|
|
|
62
82
|
id="title"
|
|
63
83
|
label={intl.formatMessage(messages.titleInputLabel)}
|
|
64
84
|
error={!!formState.errors.title}
|
|
85
|
+
message={getLocalizedErrorMessage(formState.errors.title?.message)}
|
|
65
86
|
{...register('title')}
|
|
66
87
|
/>
|
|
67
88
|
<div className="form-group">
|
|
@@ -71,6 +92,7 @@ const AddressForm = ({ handleReset, onSubmit, address }: Props) => {
|
|
|
71
92
|
id="first_name"
|
|
72
93
|
label={intl.formatMessage(messages.first_nameInputLabel)}
|
|
73
94
|
error={!!formState.errors.first_name}
|
|
95
|
+
message={getLocalizedErrorMessage(formState.errors.first_name?.message)}
|
|
74
96
|
{...register('first_name')}
|
|
75
97
|
/>
|
|
76
98
|
<TextField
|
|
@@ -79,6 +101,7 @@ const AddressForm = ({ handleReset, onSubmit, address }: Props) => {
|
|
|
79
101
|
id="last_name"
|
|
80
102
|
label={intl.formatMessage(messages.last_nameInputLabel)}
|
|
81
103
|
error={!!formState.errors.last_name}
|
|
104
|
+
message={getLocalizedErrorMessage(formState.errors.last_name?.message)}
|
|
82
105
|
{...register('last_name')}
|
|
83
106
|
/>
|
|
84
107
|
</div>
|
|
@@ -88,6 +111,7 @@ const AddressForm = ({ handleReset, onSubmit, address }: Props) => {
|
|
|
88
111
|
id="address"
|
|
89
112
|
label={intl.formatMessage(messages.addressInputLabel)}
|
|
90
113
|
error={!!formState.errors.address}
|
|
114
|
+
message={getLocalizedErrorMessage(formState.errors.address?.message)}
|
|
91
115
|
{...register('address')}
|
|
92
116
|
/>
|
|
93
117
|
<div className="form-group">
|
|
@@ -97,6 +121,7 @@ const AddressForm = ({ handleReset, onSubmit, address }: Props) => {
|
|
|
97
121
|
id="postcode"
|
|
98
122
|
label={intl.formatMessage(messages.postcodeInputLabel)}
|
|
99
123
|
error={!!formState.errors.postcode}
|
|
124
|
+
message={getLocalizedErrorMessage(formState.errors.postcode?.message)}
|
|
100
125
|
{...register('postcode')}
|
|
101
126
|
/>
|
|
102
127
|
<TextField
|
|
@@ -105,6 +130,7 @@ const AddressForm = ({ handleReset, onSubmit, address }: Props) => {
|
|
|
105
130
|
id="city"
|
|
106
131
|
label={intl.formatMessage(messages.cityInputLabel)}
|
|
107
132
|
error={!!formState.errors.city}
|
|
133
|
+
message={getLocalizedErrorMessage(formState.errors.city?.message)}
|
|
108
134
|
{...register('city')}
|
|
109
135
|
/>
|
|
110
136
|
</div>
|
|
@@ -114,6 +140,7 @@ const AddressForm = ({ handleReset, onSubmit, address }: Props) => {
|
|
|
114
140
|
id="country"
|
|
115
141
|
label={intl.formatMessage(messages.countryInputLabel)}
|
|
116
142
|
error={!!formState.errors.country}
|
|
143
|
+
message={getLocalizedErrorMessage(formState.errors.country?.message)}
|
|
117
144
|
{...register('country', { value: address?.country, required: true })}
|
|
118
145
|
>
|
|
119
146
|
<option disabled value="-">
|
|
@@ -132,6 +159,7 @@ const AddressForm = ({ handleReset, onSubmit, address }: Props) => {
|
|
|
132
159
|
id="save"
|
|
133
160
|
label={intl.formatMessage(messages.saveInputLabel)}
|
|
134
161
|
error={!!formState.errors?.save}
|
|
162
|
+
message={getLocalizedErrorMessage(formState.errors.save?.message)}
|
|
135
163
|
{...register('save')}
|
|
136
164
|
/>
|
|
137
165
|
) : null}
|
|
@@ -1,20 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Test suite for AddressesManagement component
|
|
3
3
|
*/
|
|
4
|
-
import { yupResolver } from '@hookform/resolvers/yup';
|
|
5
4
|
import { fireEvent, render, screen } from '@testing-library/react';
|
|
6
|
-
import { act
|
|
7
|
-
import faker from 'faker';
|
|
5
|
+
import { act } from '@testing-library/react-hooks';
|
|
8
6
|
import fetchMock from 'fetch-mock';
|
|
9
7
|
import * as mockFactories from 'utils/test/factories';
|
|
10
8
|
import { IntlProvider } from 'react-intl';
|
|
11
9
|
import { QueryClientProvider } from 'react-query';
|
|
12
|
-
import { useForm } from 'react-hook-form';
|
|
13
10
|
import { SessionProvider } from 'data/SessionProvider';
|
|
14
11
|
import { REACT_QUERY_SETTINGS, RICHIE_USER_TOKEN } from 'settings';
|
|
15
12
|
import type * as Joanie from 'types/Joanie';
|
|
16
13
|
import createQueryClient from 'utils/react-query/createQueryClient';
|
|
17
|
-
import validationSchema from './validationSchema';
|
|
18
14
|
import AddressesManagement from '.';
|
|
19
15
|
|
|
20
16
|
jest.mock('utils/context', () => ({
|
|
@@ -31,142 +27,6 @@ jest.mock('utils/indirection/window', () => ({
|
|
|
31
27
|
confirm: jest.fn(() => true),
|
|
32
28
|
}));
|
|
33
29
|
|
|
34
|
-
describe('validationSchema', () => {
|
|
35
|
-
// Creation and Update form validation relies on a schema resolves by Yup.
|
|
36
|
-
it('should not have error if values are valid', async () => {
|
|
37
|
-
const defaultValues = {
|
|
38
|
-
address: faker.address.streetAddress(),
|
|
39
|
-
city: faker.address.city(),
|
|
40
|
-
country: faker.address.countryCode(),
|
|
41
|
-
first_name: faker.name.firstName(),
|
|
42
|
-
last_name: faker.name.lastName(),
|
|
43
|
-
postcode: faker.address.zipCode(),
|
|
44
|
-
title: faker.random.word(),
|
|
45
|
-
save: false,
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
const { result } = renderHook(() =>
|
|
49
|
-
useForm({
|
|
50
|
-
defaultValues,
|
|
51
|
-
resolver: yupResolver(validationSchema),
|
|
52
|
-
}),
|
|
53
|
-
);
|
|
54
|
-
|
|
55
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
|
56
|
-
result.current.formState.errors;
|
|
57
|
-
result.current.register('address');
|
|
58
|
-
result.current.register('city');
|
|
59
|
-
result.current.register('country');
|
|
60
|
-
result.current.register('first_name');
|
|
61
|
-
result.current.register('last_name');
|
|
62
|
-
result.current.register('postcode');
|
|
63
|
-
result.current.register('title');
|
|
64
|
-
result.current.register('save');
|
|
65
|
-
|
|
66
|
-
await act(async () => {
|
|
67
|
-
result.current.trigger();
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
const { formState } = result.current;
|
|
71
|
-
|
|
72
|
-
expect(formState.errors.address).not.toBeDefined();
|
|
73
|
-
expect(formState.errors.city).not.toBeDefined();
|
|
74
|
-
expect(formState.errors.country).not.toBeDefined();
|
|
75
|
-
expect(formState.errors.first_name).not.toBeDefined();
|
|
76
|
-
expect(formState.errors.last_name).not.toBeDefined();
|
|
77
|
-
expect(formState.errors.postcode).not.toBeDefined();
|
|
78
|
-
expect(formState.errors.title).not.toBeDefined();
|
|
79
|
-
expect(formState.errors.save).not.toBeDefined();
|
|
80
|
-
expect(formState.isValid).toBe(true);
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
it('should have an error if values are invalid', async () => {
|
|
84
|
-
const { result } = renderHook(() =>
|
|
85
|
-
useForm({
|
|
86
|
-
resolver: yupResolver(validationSchema),
|
|
87
|
-
}),
|
|
88
|
-
);
|
|
89
|
-
|
|
90
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
|
91
|
-
result.current.formState.errors;
|
|
92
|
-
result.current.register('address');
|
|
93
|
-
result.current.register('city');
|
|
94
|
-
result.current.register('country');
|
|
95
|
-
result.current.register('first_name');
|
|
96
|
-
result.current.register('last_name');
|
|
97
|
-
result.current.register('postcode');
|
|
98
|
-
result.current.register('title');
|
|
99
|
-
result.current.register('save');
|
|
100
|
-
|
|
101
|
-
// - Trigger form validation with empty values
|
|
102
|
-
await act(async () => {
|
|
103
|
-
result.current.trigger();
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
let { formState } = result.current;
|
|
107
|
-
expect(formState.errors.address?.type).toEqual('required');
|
|
108
|
-
expect(formState.errors.address?.message).toEqual('address is a required field');
|
|
109
|
-
expect(formState.errors.city?.type).toEqual('required');
|
|
110
|
-
expect(formState.errors.city?.message).toEqual('city is a required field');
|
|
111
|
-
expect(formState.errors.country?.type).toEqual('required');
|
|
112
|
-
expect(formState.errors.country?.message).toEqual('country is a required field');
|
|
113
|
-
expect(formState.errors.first_name?.type).toEqual('required');
|
|
114
|
-
expect(formState.errors.first_name?.message).toEqual('first_name is a required field');
|
|
115
|
-
expect(formState.errors.last_name?.type).toEqual('required');
|
|
116
|
-
expect(formState.errors.last_name?.message).toEqual('last_name is a required field');
|
|
117
|
-
expect(formState.errors.postcode?.type).toEqual('required');
|
|
118
|
-
expect(formState.errors.postcode?.message).toEqual('postcode is a required field');
|
|
119
|
-
expect(formState.errors.title?.type).toEqual('required');
|
|
120
|
-
expect(formState.errors.title?.message).toEqual('title is a required field');
|
|
121
|
-
expect(formState.errors.save).not.toBeDefined();
|
|
122
|
-
expect(formState.isValid).toBe(false);
|
|
123
|
-
|
|
124
|
-
// - Set values for all field but with a wrong one for country field
|
|
125
|
-
await act(async () => {
|
|
126
|
-
result.current.setValue('address', faker.address.streetAddress());
|
|
127
|
-
result.current.setValue('city', faker.address.city());
|
|
128
|
-
// set country value with an invalid country code
|
|
129
|
-
result.current.setValue('country', 'AA');
|
|
130
|
-
result.current.setValue('first_name', faker.name.firstName());
|
|
131
|
-
result.current.setValue('last_name', faker.name.lastName());
|
|
132
|
-
result.current.setValue('postcode', faker.address.zipCode());
|
|
133
|
-
result.current.setValue('title', faker.random.word());
|
|
134
|
-
result.current.trigger();
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
formState = result.current.formState;
|
|
138
|
-
expect(formState.errors.address).not.toBeDefined();
|
|
139
|
-
expect(formState.errors.city).not.toBeDefined();
|
|
140
|
-
expect(formState.errors.country?.type).toEqual('oneOf');
|
|
141
|
-
expect(formState.errors.country?.message).toContain(
|
|
142
|
-
'country must be one of the following values:',
|
|
143
|
-
);
|
|
144
|
-
expect(formState.errors.first_name).not.toBeDefined();
|
|
145
|
-
expect(formState.errors.last_name).not.toBeDefined();
|
|
146
|
-
expect(formState.errors.postcode).not.toBeDefined();
|
|
147
|
-
expect(formState.errors.title).not.toBeDefined();
|
|
148
|
-
expect(formState.errors.save).not.toBeDefined();
|
|
149
|
-
expect(formState.isValid).toBe(false);
|
|
150
|
-
|
|
151
|
-
// - Set country value with a valid country code
|
|
152
|
-
await act(async () => {
|
|
153
|
-
result.current.setValue('country', 'FR');
|
|
154
|
-
result.current.trigger();
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
formState = result.current.formState;
|
|
158
|
-
expect(formState.errors.address).not.toBeDefined();
|
|
159
|
-
expect(formState.errors.city).not.toBeDefined();
|
|
160
|
-
expect(formState.errors.country).not.toBeDefined();
|
|
161
|
-
expect(formState.errors.first_name).not.toBeDefined();
|
|
162
|
-
expect(formState.errors.last_name).not.toBeDefined();
|
|
163
|
-
expect(formState.errors.postcode).not.toBeDefined();
|
|
164
|
-
expect(formState.errors.title).not.toBeDefined();
|
|
165
|
-
expect(formState.errors.save).not.toBeDefined();
|
|
166
|
-
expect(formState.isValid).toBe(true);
|
|
167
|
-
});
|
|
168
|
-
});
|
|
169
|
-
|
|
170
30
|
describe('AddressesManagement', () => {
|
|
171
31
|
const initializeUser = () => {
|
|
172
32
|
const user = mockFactories.FonzieUserFactory.generate();
|
|
@@ -221,6 +81,46 @@ describe('AddressesManagement', () => {
|
|
|
221
81
|
expect(handleClose).toHaveBeenCalledTimes(1);
|
|
222
82
|
});
|
|
223
83
|
|
|
84
|
+
it("renders the user's addresses", async () => {
|
|
85
|
+
initializeUser();
|
|
86
|
+
const addresses = mockFactories.AddressFactory.generate(Math.ceil(Math.random() * 5));
|
|
87
|
+
fetchMock.get('https://joanie.endpoint/api/addresses/', addresses);
|
|
88
|
+
|
|
89
|
+
let container: HTMLElement;
|
|
90
|
+
|
|
91
|
+
await act(async () => {
|
|
92
|
+
({ container } = render(
|
|
93
|
+
<QueryClientProvider client={createQueryClient({ persistor: true })}>
|
|
94
|
+
<IntlProvider locale="en">
|
|
95
|
+
<SessionProvider>
|
|
96
|
+
<AddressesManagement handleClose={handleClose} selectAddress={selectAddress} />
|
|
97
|
+
</SessionProvider>
|
|
98
|
+
</IntlProvider>
|
|
99
|
+
</QueryClientProvider>,
|
|
100
|
+
));
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
// All user's addresses should be displayed
|
|
104
|
+
const $addresses = container!.querySelectorAll('.registered-addresses-item');
|
|
105
|
+
expect($addresses).toHaveLength(addresses.length);
|
|
106
|
+
|
|
107
|
+
addresses.forEach((address: Joanie.Address) => {
|
|
108
|
+
const $address = screen.getByTestId(`address-${address.id}-title`);
|
|
109
|
+
expect($address.textContent).toEqual(address.title);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// - User selects one of its existing address
|
|
113
|
+
const address = addresses[0];
|
|
114
|
+
const $selectButton = screen.getByRole('button', {
|
|
115
|
+
name: `Select "${address.title}" address`,
|
|
116
|
+
exact: true,
|
|
117
|
+
});
|
|
118
|
+
await act(async () => {
|
|
119
|
+
fireEvent.click($selectButton);
|
|
120
|
+
});
|
|
121
|
+
expect(selectAddress).toHaveBeenNthCalledWith(1, address);
|
|
122
|
+
});
|
|
123
|
+
|
|
224
124
|
it('renders a form to create an address', async () => {
|
|
225
125
|
initializeUser();
|
|
226
126
|
fetchMock.get('https://joanie.endpoint/api/addresses/', []);
|
|
@@ -307,47 +207,7 @@ describe('AddressesManagement', () => {
|
|
|
307
207
|
});
|
|
308
208
|
});
|
|
309
209
|
|
|
310
|
-
it(
|
|
311
|
-
initializeUser();
|
|
312
|
-
const addresses = mockFactories.AddressFactory.generate(Math.ceil(Math.random() * 5));
|
|
313
|
-
fetchMock.get('https://joanie.endpoint/api/addresses/', addresses);
|
|
314
|
-
|
|
315
|
-
let container: HTMLElement;
|
|
316
|
-
|
|
317
|
-
await act(async () => {
|
|
318
|
-
({ container } = render(
|
|
319
|
-
<QueryClientProvider client={createQueryClient({ persistor: true })}>
|
|
320
|
-
<IntlProvider locale="en">
|
|
321
|
-
<SessionProvider>
|
|
322
|
-
<AddressesManagement handleClose={handleClose} selectAddress={selectAddress} />
|
|
323
|
-
</SessionProvider>
|
|
324
|
-
</IntlProvider>
|
|
325
|
-
</QueryClientProvider>,
|
|
326
|
-
));
|
|
327
|
-
});
|
|
328
|
-
|
|
329
|
-
// All user's addresses should be displayed
|
|
330
|
-
const $addresses = container!.querySelectorAll('.registered-addresses-item');
|
|
331
|
-
expect($addresses).toHaveLength(addresses.length);
|
|
332
|
-
|
|
333
|
-
addresses.forEach((address: Joanie.Address) => {
|
|
334
|
-
const $address = screen.getByTestId(`address-${address.id}-title`);
|
|
335
|
-
expect($address.textContent).toEqual(address.title);
|
|
336
|
-
});
|
|
337
|
-
|
|
338
|
-
// - User selects one of its existing address
|
|
339
|
-
const address = addresses[0];
|
|
340
|
-
const $selectButton = screen.getByRole('button', {
|
|
341
|
-
name: `Select "${address.title}" address`,
|
|
342
|
-
exact: true,
|
|
343
|
-
});
|
|
344
|
-
await act(async () => {
|
|
345
|
-
fireEvent.click($selectButton);
|
|
346
|
-
});
|
|
347
|
-
expect(selectAddress).toHaveBeenNthCalledWith(1, address);
|
|
348
|
-
});
|
|
349
|
-
|
|
350
|
-
it('renders an updated form when user selects an address to edit', async () => {
|
|
210
|
+
it('renders a form to edit an address when user selects an address to edit', async () => {
|
|
351
211
|
initializeUser();
|
|
352
212
|
const address = mockFactories.AddressFactory.generate();
|
|
353
213
|
fetchMock.get('https://joanie.endpoint/api/addresses/', [address]);
|