1
0
mirror of https://github.com/LaCasemate/fab-manager.git synced 2024-11-28 09:24:24 +01:00

(test) tiptap & file uploads

This commit is contained in:
Sylvain 2022-11-29 18:07:05 +01:00
parent 7a3ab98b10
commit c1638ab54d
14 changed files with 132 additions and 46 deletions

View File

@ -70,6 +70,10 @@
"env": {
"jest/globals": true
},
"globals": {
"Range": true,
"Document": true
},
"parserOptions": {
"project": "./test/frontend/tsconfig.json"
}

View File

@ -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<FabTextEditorRef, FabTextEditorProps> = ({ heading, bulletList, blockquote, content, limit = 400, video, image, link, onChange, placeholder, error, disabled = false, editorId }, ref: RefObject<FabTextEditorRef>) => {
const FabTextEditor: React.ForwardRefRenderFunction<FabTextEditorRef, FabTextEditorProps> = ({ heading, bulletList, blockquote, content, limit = 400, video, image, link, onChange, placeholder, error, disabled = false, ariaLabel }, ref: RefObject<FabTextEditorRef>) => {
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<FabTextEditorRef, FabTextEdi
],
editorProps: {
attributes: {
id: editorId
'aria-label': ariaLabel,
role: 'textbox'
}
},
content,

View File

@ -7,8 +7,6 @@ import { get as _get } from 'lodash';
export interface AbstractFormItemProps<TFieldValues> extends PropsWithChildren<AbstractFormComponent<TFieldValues>> {
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<TFieldValues> extends PropsWithChildren<A
* This abstract component should not be used directly.
* Other forms components that are intended to be used with react-hook-form must extend this component.
*/
export const AbstractFormItem = <TFieldValues extends FieldValues>({ id, label, ariaLabel, ariaLabelledBy, tooltip, className, disabled, error, warning, rules, formState, onLabelClick, inLine, containerType, children }: AbstractFormItemProps<TFieldValues>) => {
export const AbstractFormItem = <TFieldValues extends FieldValues>({ id, label, tooltip, className, disabled, error, warning, rules, formState, onLabelClick, inLine, containerType, children }: AbstractFormItemProps<TFieldValues>) => {
const [isDirty, setIsDirty] = useState<boolean>(false);
const [fieldError, setFieldError] = useState<{ message: string }>(error);
const [isDisabled, setIsDisabled] = useState<boolean>(false);
@ -72,7 +70,7 @@ export const AbstractFormItem = <TFieldValues extends FieldValues>({ id, label,
</div>}
</div>}
<div className='form-item-field' aria-label={ariaLabel} aria-labelledby={ariaLabelledBy}>
<div className='form-item-field'>
{inLine && <div className='form-item-header'><p>{label}</p>
{tooltip && <div className="fab-tooltip">
<span className="trigger"><i className="fa fa-question-circle" /></span>

View File

@ -87,18 +87,19 @@ export const FormFileUpload = <TFieldValues extends FieldValues>({ id, label, re
</a>
)}
<FormInput type="file"
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()}/>
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() &&
<FabButton onClick={onRemoveFile} icon={<Trash size={20} weight="fill" />} className="is-main" />
}

View File

@ -19,13 +19,14 @@ interface FormInputProps<TFieldValues, TInputType> extends FormComponent<TFieldV
placeholder?: string,
step?: number | 'any',
onChange?: (event: React.ChangeEvent<HTMLInputElement>) => 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 = <TFieldValues extends FieldValues, TInputType>({ id, register, label, tooltip, defaultValue, icon, className, rules, disabled, type, addOn, addOnAction, addOnClassName, placeholder, error, warning, formState, step, onChange, debounce, accept, nullable = false }: FormInputProps<TFieldValues, TInputType>) => {
export const FormInput = <TFieldValues extends FieldValues, TInputType>({ 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<TFieldValues, TInputType>) => {
/**
* Debounced (ie. temporised) version of the 'on change' callback.
*/
@ -56,7 +57,7 @@ export const FormInput = <TFieldValues extends FieldValues, TInputType>({ id, re
disabled={disabled}
rules={rules} error={error} warning={warning}>
{icon && <span className="icon">{icon}</span>}
<input id={id}
<input id={id} aria-label={ariaLabel}
{...register(id as FieldPath<TFieldValues>, {
...rules,
valueAsDate: type === 'date',

View File

@ -46,8 +46,6 @@ export const FormRichText = <TFieldValues extends FieldValues, TContext extends
return (
<AbstractFormItem id={id} label={label} tooltip={tooltip}
ariaLabel={label as string}
ariaLabelledBy={id}
containerType={'div'}
className={`form-rich-text ${className || ''}`}
error={error} warning={warning} rules={rules}
@ -68,7 +66,7 @@ export const FormRichText = <TFieldValues extends FieldValues, TContext extends
link={link}
disabled={isDisabled}
ref={textEditorRef}
editorId={id} />
ariaLabel={label as string}/>
} />
</AbstractFormItem>
);

View File

@ -207,6 +207,7 @@ export const PlanForm: React.FC<PlanFormProps> = ({ action, plan, onError, onSuc
<FormSwitch control={control}
formState={formState}
id="disabled"
defaultValue={false}
label={t('app.admin.plan_form.disabled')}
tooltip={t('app.admin.plan_form.disabled_help')} />
<h4>{t('app.admin.plan_form.duration')}</h4>

View File

@ -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<Price>,
plan_file_attributes: FileType,
prices_attributes?: Array<Price>,
plan_file_attributes?: FileType,
plan_file_url?: string,
partner_id?: number,
partners?: Array<Partner>
partnership?: boolean,
partners?: Array<Partner>,
advanced_accounting_attributes?: AdvancedAccounting
}
export interface PlansDuration {

View File

@ -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: [
'<rootDir>/test/frontend/__setup__/mocks.js',
'<rootDir>/test/frontend/__setup__/server.js'
'<rootDir>/test/frontend/__setup__/server.js',
'<rootDir>/test/frontend/__setup__/rects.js'
],
// The number of seconds after which a test is considered as slow and reported as such in the results.

View File

@ -0,0 +1,16 @@
import { act, waitFor } from '@testing-library/react';
interface TipTapEvent {
type: (element: Element, content: string) => Promise<void>
}
export const tiptapEvent: TipTapEvent = {
type: async (element, content) => {
await act(async () => {
element.innerHTML = content;
await waitFor(() => {
expect(element.innerHTML).toBe(content);
});
});
}
};

View File

@ -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();

View File

@ -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(<PlanForm action="create" onError={onError} onSuccess={onSuccess} beforeSubmit={beforeSubmit} />);
render(<PlanForm action="create" onError={onError} onSuccess={onSuccess} />);
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(<PlanForm action="create" onError={onError} onSuccess={onSuccess} beforeSubmit={beforeSubmit} />);
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: '<p>Lorem ipsum dolor sit amet</p>',
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));
});
});

View File

@ -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"

View File

@ -7,7 +7,7 @@
"module": "ES2020",
"moduleResolution": "node",
"sourceMap": true,
"target": "es2015",
"target": "ES2015",
"jsx": "react-jsx",
"noEmit": true,
"allowSyntheticDefaultImports": true,