diff --git a/.eslintrc b/.eslintrc index dc01e1822..1d534f341 100644 --- a/.eslintrc +++ b/.eslintrc @@ -70,6 +70,10 @@ "env": { "jest/globals": true }, + "globals": { + "Range": true, + "Document": true + }, "parserOptions": { "project": "./test/frontend/tsconfig.json" } diff --git a/app/frontend/src/javascript/components/base/text-editor/fab-text-editor.tsx b/app/frontend/src/javascript/components/base/text-editor/fab-text-editor.tsx index 04d790f31..86bc81395 100644 --- a/app/frontend/src/javascript/components/base/text-editor/fab-text-editor.tsx +++ b/app/frontend/src/javascript/components/base/text-editor/fab-text-editor.tsx @@ -25,7 +25,7 @@ interface FabTextEditorProps { placeholder?: string, error?: string, disabled?: boolean - editorId?: string, + ariaLabel?: string, } export interface FabTextEditorRef { @@ -35,7 +35,7 @@ export interface FabTextEditorRef { /** * This component is a WYSIWYG text editor */ -const FabTextEditor: React.ForwardRefRenderFunction = ({ heading, bulletList, blockquote, content, limit = 400, video, image, link, onChange, placeholder, error, disabled = false, editorId }, ref: RefObject) => { +const FabTextEditor: React.ForwardRefRenderFunction = ({ heading, bulletList, blockquote, content, limit = 400, video, image, link, onChange, placeholder, error, disabled = false, ariaLabel }, ref: RefObject) => { const { t } = useTranslation('shared'); const placeholderText = placeholder || t('app.shared.text_editor.fab_text_editor.text_placeholder'); // TODO: Add ctrl+click on link to visit @@ -77,7 +77,8 @@ const FabTextEditor: React.ForwardRefRenderFunction extends PropsWithChildren> { id: string, label?: string|ReactNode, - ariaLabel?: string, - ariaLabelledBy?: string, tooltip?: ReactNode, className?: string, disabled?: boolean|((id: string) => boolean), @@ -21,7 +19,7 @@ export interface AbstractFormItemProps extends PropsWithChildren({ id, label, ariaLabel, ariaLabelledBy, tooltip, className, disabled, error, warning, rules, formState, onLabelClick, inLine, containerType, children }: AbstractFormItemProps) => { +export const AbstractFormItem = ({ id, label, tooltip, className, disabled, error, warning, rules, formState, onLabelClick, inLine, containerType, children }: AbstractFormItemProps) => { const [isDirty, setIsDirty] = useState(false); const [fieldError, setFieldError] = useState<{ message: string }>(error); const [isDisabled, setIsDisabled] = useState(false); @@ -72,7 +70,7 @@ export const AbstractFormItem = ({ id, label, } } -
+
{inLine &&

{label}

{tooltip &&
diff --git a/app/frontend/src/javascript/components/form/form-file-upload.tsx b/app/frontend/src/javascript/components/form/form-file-upload.tsx index 118a028c7..369f2cf52 100644 --- a/app/frontend/src/javascript/components/form/form-file-upload.tsx +++ b/app/frontend/src/javascript/components/form/form-file-upload.tsx @@ -87,18 +87,19 @@ export const FormFileUpload = ({ id, label, re )} + ariaLabel={label as string} + className="image-file-input" + accept={accept} + register={register} + label={label} + formState={formState} + rules={rules} + disabled={disabled} + error={error} + warning={warning} + id={`${id}[attachment_files]`} + onChange={onFileSelected} + placeholder={placeholder()}/> {hasFile() && } className="is-main" /> } diff --git a/app/frontend/src/javascript/components/form/form-input.tsx b/app/frontend/src/javascript/components/form/form-input.tsx index ed2b14c07..6d9ce4f25 100644 --- a/app/frontend/src/javascript/components/form/form-input.tsx +++ b/app/frontend/src/javascript/components/form/form-input.tsx @@ -19,13 +19,14 @@ interface FormInputProps extends FormComponent) => void, - nullable?: boolean + nullable?: boolean, + ariaLabel?: string, } /** * This component is a template for an input component to use within React Hook Form */ -export const FormInput = ({ id, register, label, tooltip, defaultValue, icon, className, rules, disabled, type, addOn, addOnAction, addOnClassName, placeholder, error, warning, formState, step, onChange, debounce, accept, nullable = false }: FormInputProps) => { +export const FormInput = ({ id, register, label, tooltip, defaultValue, icon, className, rules, disabled, type, addOn, addOnAction, addOnClassName, placeholder, error, warning, formState, step, onChange, debounce, accept, nullable = false, ariaLabel }: FormInputProps) => { /** * Debounced (ie. temporised) version of the 'on change' callback. */ @@ -56,7 +57,7 @@ export const FormInput = ({ id, re disabled={disabled} rules={rules} error={error} warning={warning}> {icon && {icon}} - , { ...rules, valueAsDate: type === 'date', diff --git a/app/frontend/src/javascript/components/form/form-rich-text.tsx b/app/frontend/src/javascript/components/form/form-rich-text.tsx index 2154bfaaa..5156c30de 100644 --- a/app/frontend/src/javascript/components/form/form-rich-text.tsx +++ b/app/frontend/src/javascript/components/form/form-rich-text.tsx @@ -46,8 +46,6 @@ export const FormRichText = + ariaLabel={label as string}/> } /> ); diff --git a/app/frontend/src/javascript/components/plans/plan-form.tsx b/app/frontend/src/javascript/components/plans/plan-form.tsx index f38603f08..f71c8f9a5 100644 --- a/app/frontend/src/javascript/components/plans/plan-form.tsx +++ b/app/frontend/src/javascript/components/plans/plan-form.tsx @@ -207,6 +207,7 @@ export const PlanForm: React.FC = ({ action, plan, onError, onSuc

{t('app.admin.plan_form.duration')}

diff --git a/app/frontend/src/javascript/models/plan.ts b/app/frontend/src/javascript/models/plan.ts index 34a91482b..6b7656521 100644 --- a/app/frontend/src/javascript/models/plan.ts +++ b/app/frontend/src/javascript/models/plan.ts @@ -1,5 +1,6 @@ import { Price } from './price'; import { FileType } from './file'; +import { AdvancedAccounting } from './advanced-accounting'; export type Interval = 'year' | 'month' | 'week'; @@ -12,26 +13,29 @@ export interface Partner { } export interface Plan { - id: number, + id?: number, base_name: string, - name: string, + name?: string, interval: Interval, interval_count: number, + all_groups?: boolean, group_id: number|'all', - plan_category_id: number, - training_credit_nb: number, + plan_category_id?: number, + training_credit_nb?: number, is_rolling: boolean, - description: string, + description?: string, type: PlanType, ui_weight: number, - disabled: boolean, + disabled?: boolean, monthly_payment: boolean amount: number - prices_attributes: Array, - plan_file_attributes: FileType, + prices_attributes?: Array, + plan_file_attributes?: FileType, plan_file_url?: string, partner_id?: number, - partners?: Array + partnership?: boolean, + partners?: Array, + advanced_accounting_attributes?: AdvancedAccounting } export interface PlansDuration { diff --git a/jest.config.js b/jest.config.js index 0c1484391..7810bc069 100644 --- a/jest.config.js +++ b/jest.config.js @@ -140,7 +140,8 @@ module.exports = { // A list of paths to modules that run some code to configure or set up the testing framework before each test setupFilesAfterEnv: [ '/test/frontend/__setup__/mocks.js', - '/test/frontend/__setup__/server.js' + '/test/frontend/__setup__/server.js', + '/test/frontend/__setup__/rects.js' ], // The number of seconds after which a test is considered as slow and reported as such in the results. diff --git a/test/frontend/__lib__/tiptap.ts b/test/frontend/__lib__/tiptap.ts new file mode 100644 index 000000000..304596180 --- /dev/null +++ b/test/frontend/__lib__/tiptap.ts @@ -0,0 +1,16 @@ +import { act, waitFor } from '@testing-library/react'; + +interface TipTapEvent { + type: (element: Element, content: string) => Promise +} + +export const tiptapEvent: TipTapEvent = { + type: async (element, content) => { + await act(async () => { + element.innerHTML = content; + await waitFor(() => { + expect(element.innerHTML).toBe(content); + }); + }); + } +}; diff --git a/test/frontend/__setup__/rects.js b/test/frontend/__setup__/rects.js new file mode 100644 index 000000000..79e372648 --- /dev/null +++ b/test/frontend/__setup__/rects.js @@ -0,0 +1,16 @@ +// the following workarounds are necessary to test with tiptap editor + +Range.prototype.getBoundingClientRect = () => ({ + bottom: 0, + height: 0, + left: 0, + right: 0, + top: 0, + width: 0 +}); +Range.prototype.getClientRects = () => ({ + item: () => null, + length: 0, + [Symbol.iterator]: jest.fn() +}); +Document.prototype.elementFromPoint = jest.fn(); diff --git a/test/frontend/components/plans/plan-form.test.tsx b/test/frontend/components/plans/plan-form.test.tsx index 35435a118..08dec7745 100644 --- a/test/frontend/components/plans/plan-form.test.tsx +++ b/test/frontend/components/plans/plan-form.test.tsx @@ -1,9 +1,11 @@ import { render, fireEvent, waitFor, screen } from '@testing-library/react'; import '@testing-library/jest-dom'; import { PlanForm } from 'components/plans/plan-form'; +import { Plan } from 'models/plan'; import selectEvent from 'react-select-event'; import userEvent from '@testing-library/user-event'; import plans from '../../__fixtures__/plans'; +import { tiptapEvent } from '../../__lib__/tiptap'; describe('PlanForm', () => { const onError = jest.fn(); @@ -11,9 +13,8 @@ describe('PlanForm', () => { const beforeSubmit = jest.fn(); test('render create PlanForm', async () => { - render(); + render(); await waitFor(() => screen.getByRole('combobox', { name: /app.admin.plan_form.group/ })); - // check inputs expect(screen.getByLabelText(/app.admin.plan_form.name/)).toBeInTheDocument(); expect(screen.getByLabelText(/app.admin.plan_form.transversal/)).toBeInTheDocument(); expect(screen.getByLabelText(/app.admin.plan_form.group/)).toBeInTheDocument(); @@ -30,23 +31,67 @@ describe('PlanForm', () => { expect(screen.getByLabelText(/app.admin.plan_form.partner_plan/)).toBeInTheDocument(); expect(screen.queryByTestId('plan-pricing-form')).toBeNull(); expect(screen.getByRole('button', { name: /app.admin.plan_form.ACTION_plan/ })).toBeInTheDocument(); - // input values + }); + + test('create new plan', async () => { + render(); + await waitFor(() => screen.getByRole('combobox', { name: /app.admin.plan_form.group/ })); const user = userEvent.setup(); + // base_name fireEvent.change(screen.getByLabelText(/app.admin.plan_form.name/), { target: { value: 'Test Plan' } }); + // group_id = 1 await selectEvent.select(screen.getByLabelText(/app.admin.plan_form.group/), 'Standard'); + // plan_category_id = 1 await selectEvent.select(screen.getByLabelText(/app.admin.plan_form.category/), 'beginners'); + // amount fireEvent.change(screen.getByLabelText(/app.admin.plan_form.subscription_price/), { target: { value: 25.21 } }); + // ui_weight fireEvent.change(screen.getByLabelText(/app.admin.plan_form.visual_prominence/), { target: { value: 10 } }); - fireEvent.change(screen.getByLabelText(/app.admin.plan_form.rolling_subscription/), { target: { value: true } }); - fireEvent.change(screen.getByLabelText(/app.admin.plan_form.monthly_payment/), { target: { value: true } }); - await user.click(screen.getByLabelText(/app.admin.plan_form.description/)); - await user.keyboard('Lorem ipsum dolor sit amet'); + // is_rolling + await user.click(screen.getByLabelText(/app.admin.plan_form.rolling_subscription/)); + // monthly_payment + await user.click(screen.getByLabelText(/app.admin.plan_form.monthly_payment/)); + // description + await tiptapEvent.type(screen.getByLabelText(/app.admin.plan_form.description/), 'Lorem ipsum dolor sit amet'); + // plan_file_attributes.attachment_files + const file = new File(['(⌐□_□)'], 'document.pdf', { type: 'application/pdf' }); + await user.upload(screen.getByLabelText(/app.admin.plan_form.information_sheet/), file); + // interval_count fireEvent.change(screen.getByLabelText(/app.admin.plan_form.number_of_periods/), { target: { value: 6 } }); + // interval await selectEvent.select(screen.getByLabelText(/app.admin.plan_form.period/), 'app.admin.plan_form.month'); + // advanced_accounting_attributes.code + fireEvent.change(screen.getByLabelText(/app.admin.advanced_accounting_form.code/), { target: { value: '705200' } }); + // advanced_accounting_attributes.analytical_section + fireEvent.change(screen.getByLabelText(/app.admin.advanced_accounting_form.analytical_section/), { target: { value: '9B20A' } }); // send the form fireEvent.click(screen.getByRole('button', { name: /app.admin.plan_form.ACTION_plan/ })); await waitFor(() => { - expect(beforeSubmit).toHaveBeenCalled(); + const expected: Plan = { + base_name: 'Test Plan', + type: 'Plan', + group_id: 1, + plan_category_id: 1, + amount: 25.21, + ui_weight: 10, + is_rolling: true, + monthly_payment: true, + description: '

Lorem ipsum dolor sit amet

', + interval: 'month', + interval_count: 6, + all_groups: false, + partnership: false, + disabled: false, + advanced_accounting_attributes: { + analytical_section: '9B20A', + code: '705200' + }, + plan_file_attributes: { + _destroy: false, + attachment_files: expect.any(FileList) + } + }; + expect(beforeSubmit).toHaveBeenCalledWith(expect.objectContaining(expected)); }); }); diff --git a/test/frontend/tsconfig.json b/test/frontend/tsconfig.json index 0ca845ecf..e672f21cb 100644 --- a/test/frontend/tsconfig.json +++ b/test/frontend/tsconfig.json @@ -2,10 +2,10 @@ "references": [ { "path": "../../" } ], - "include": ["components/**/*", "__fixtures__/**/*"], + "include": ["components/**/*", "__fixtures__/**/*", "__lib__/**/*"], "compilerOptions": { "jsx": "react-jsx", - "target": "es2015", + "target": "ES2015", "module": "ES2020", "moduleResolution": "node", "baseUrl": "../../app/frontend/src/javascript" diff --git a/tsconfig.json b/tsconfig.json index fe2643938..c66660802 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -7,7 +7,7 @@ "module": "ES2020", "moduleResolution": "node", "sourceMap": true, - "target": "es2015", + "target": "ES2015", "jsx": "react-jsx", "noEmit": true, "allowSyntheticDefaultImports": true,