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:
parent
7a3ab98b10
commit
c1638ab54d
@ -70,6 +70,10 @@
|
||||
"env": {
|
||||
"jest/globals": true
|
||||
},
|
||||
"globals": {
|
||||
"Range": true,
|
||||
"Document": true
|
||||
},
|
||||
"parserOptions": {
|
||||
"project": "./test/frontend/tsconfig.json"
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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>
|
||||
|
@ -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" />
|
||||
}
|
||||
|
@ -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',
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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>
|
||||
|
@ -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 {
|
||||
|
@ -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.
|
||||
|
16
test/frontend/__lib__/tiptap.ts
Normal file
16
test/frontend/__lib__/tiptap.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
16
test/frontend/__setup__/rects.js
Normal file
16
test/frontend/__setup__/rects.js
Normal 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();
|
@ -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));
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -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"
|
||||
|
@ -7,7 +7,7 @@
|
||||
"module": "ES2020",
|
||||
"moduleResolution": "node",
|
||||
"sourceMap": true,
|
||||
"target": "es2015",
|
||||
"target": "ES2015",
|
||||
"jsx": "react-jsx",
|
||||
"noEmit": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
|
Loading…
Reference in New Issue
Block a user