-
Notifications
You must be signed in to change notification settings - Fork 667
Scheduler - Create isolated AppointmentPopup test environment #33002
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
base: 26_1
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,207 @@ | ||
| import { jest } from '@jest/globals'; | ||
| import $ from '@js/core/renderer'; | ||
| // eslint-disable-next-line devextreme-custom/no-deferred | ||
| import { Deferred } from '@js/core/utils/deferred'; | ||
|
|
||
| import { mockTimeZoneCalculator } from '../../__mock__/timezone_calculator.mock'; | ||
| import { AppointmentForm } from '../../appointment_popup/m_form'; | ||
| import { | ||
| ACTION_TO_APPOINTMENT, | ||
| APPOINTMENT_POPUP_CLASS, | ||
| AppointmentPopup, | ||
| } from '../../appointment_popup/m_popup'; | ||
| import { | ||
| AppointmentDataAccessor, | ||
| } from '../../utils/data_accessor/appointment_data_accessor'; | ||
| import type { IFieldExpr } from '../../utils/data_accessor/types'; | ||
| import { | ||
| ResourceManager, | ||
| } from '../../utils/resource_manager/resource_manager'; | ||
| import { PopupModel } from './model/popup'; | ||
|
|
||
| const DEFAULT_FIELDS: IFieldExpr = { | ||
| startDateExpr: 'startDate', | ||
| endDateExpr: 'endDate', | ||
| startDateTimeZoneExpr: 'startDateTimeZone', | ||
| endDateTimeZoneExpr: 'endDateTimeZone', | ||
| allDayExpr: 'allDay', | ||
| textExpr: 'text', | ||
| descriptionExpr: 'description', | ||
| recurrenceRuleExpr: 'recurrenceRule', | ||
| recurrenceExceptionExpr: 'recurrenceException', | ||
| disabledExpr: 'disabled', | ||
| visibleExpr: 'visible', | ||
| }; | ||
|
|
||
| const DEFAULT_EDITING = { | ||
| allowAdding: true, | ||
| allowUpdating: true, | ||
| allowDeleting: true, | ||
| allowResizing: true, | ||
| allowDragging: true, | ||
| legacyForm: false, | ||
| }; | ||
|
|
||
| const DEFAULT_APPOINTMENT = { | ||
| text: 'Test Appointment', | ||
| startDate: new Date(2021, 3, 26, 9, 30), | ||
| endDate: new Date(2021, 3, 26, 11, 0), | ||
| }; | ||
|
|
||
| // eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
| const resolvedDeferred = (): any => { | ||
| // @ts-expect-error | ||
| // eslint-disable-next-line devextreme-custom/no-deferred | ||
| const d = new Deferred(); | ||
| d.resolve(); | ||
| return d.promise(); | ||
| }; | ||
|
|
||
| interface CreateAppointmentPopupOptions { | ||
| appointmentData?: Record<string, unknown>; | ||
| action?: number; | ||
| editing?: Record<string, unknown>; | ||
| firstDayOfWeek?: number; | ||
| startDayHour?: number; | ||
| onAppointmentFormOpening?: (...args: unknown[]) => void; | ||
| addAppointment?: jest.Mock; | ||
| updateAppointment?: jest.Mock; | ||
| } | ||
|
|
||
| interface CreateAppointmentPopupResult { | ||
| container: HTMLDivElement; | ||
| popup: AppointmentPopup; | ||
| form: AppointmentForm; | ||
| POM: PopupModel; | ||
| callbacks: { | ||
| addAppointment: jest.Mock; | ||
| updateAppointment: jest.Mock; | ||
| focus: jest.Mock; | ||
| updateScrollPosition: jest.Mock; | ||
| }; | ||
| dispose: () => void; | ||
| } | ||
|
|
||
| // eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
| const createComponent = (element: any, Component: any, opts: any): any => ( | ||
| new Component($(element), opts) | ||
| ); | ||
|
|
||
| const disposables: (() => void)[] = []; | ||
|
|
||
| export const disposeAppointmentPopups = (): void => { | ||
| disposables.forEach((fn) => fn()); | ||
| disposables.length = 0; | ||
| document.body.innerHTML = ''; | ||
| }; | ||
|
|
||
| export const createAppointmentPopup = async ( | ||
| options: CreateAppointmentPopupOptions = {}, | ||
| ): Promise<CreateAppointmentPopupResult> => { | ||
| const container = document.createElement('div'); | ||
| document.body.appendChild(container); | ||
|
|
||
| const dataAccessors = new AppointmentDataAccessor(DEFAULT_FIELDS, false); | ||
| const resourceManager = new ResourceManager([]); | ||
| const timeZoneCalculator = mockTimeZoneCalculator; | ||
| const editing = { ...DEFAULT_EDITING, ...options.editing }; | ||
|
|
||
| const addAppointment = options.addAppointment | ||
| ?? jest.fn(resolvedDeferred); | ||
| const updateAppointment = options.updateAppointment | ||
| ?? jest.fn(resolvedDeferred); | ||
| const focus = jest.fn(); | ||
| const updateScrollPosition = jest.fn(); | ||
|
|
||
| const formSchedulerProxy = { | ||
| getResourceById: (): Record<string, unknown> => ( | ||
| resourceManager.resourceById | ||
| ), | ||
| getDataAccessors: (): AppointmentDataAccessor => dataAccessors, | ||
| createComponent, | ||
| getEditingConfig: (): typeof editing => editing, | ||
| getResourceManager: (): ResourceManager => resourceManager, | ||
| getFirstDayOfWeek: (): number => options.firstDayOfWeek ?? 0, | ||
| getStartDayHour: (): number => options.startDayHour ?? 0, | ||
| getCalculatedEndDate: (startDate: Date): Date => { | ||
| const endDate = new Date(startDate); | ||
| endDate.setHours(endDate.getHours() + 1); | ||
| return endDate; | ||
| }, | ||
| getTimeZoneCalculator: (): typeof timeZoneCalculator => ( | ||
| timeZoneCalculator | ||
| ), | ||
| }; | ||
|
|
||
| const form = new AppointmentForm(formSchedulerProxy); | ||
|
|
||
| const noop = (): void => {}; | ||
|
|
||
| const popupSchedulerProxy = { | ||
| getElement: (): ReturnType<typeof $> => $(container), | ||
| createComponent, | ||
| focus, | ||
| getResourceManager: (): ResourceManager => resourceManager, | ||
| getEditingConfig: (): typeof editing => editing, | ||
| getTimeZoneCalculator: (): typeof timeZoneCalculator => ( | ||
| timeZoneCalculator | ||
| ), | ||
| getDataAccessors: (): AppointmentDataAccessor => dataAccessors, | ||
| getAppointmentFormOpening: (): ( | ||
| (...args: unknown[]) => void | ||
| ) => options.onAppointmentFormOpening ?? noop, | ||
| processActionResult: ( | ||
| // eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
| arg: any, | ||
| callback: (canceled: boolean) => void, | ||
| ): void => { | ||
| callback(arg.cancel); | ||
| }, | ||
| addAppointment, | ||
| updateAppointment, | ||
| updateScrollPosition, | ||
| }; | ||
|
|
||
| const popup = new AppointmentPopup(popupSchedulerProxy, form); | ||
|
|
||
| const appointmentData = options.appointmentData | ||
| ?? { ...DEFAULT_APPOINTMENT }; | ||
| const action = options.action ?? ACTION_TO_APPOINTMENT.CREATE; | ||
|
|
||
| popup.show(appointmentData, { action, allowSaving: true }); | ||
| await new Promise(process.nextTick); | ||
|
|
||
| const selector = `.dx-overlay-wrapper.${APPOINTMENT_POPUP_CLASS}`; | ||
| const overlayWrapper = document.querySelector( | ||
| selector, | ||
| ) as HTMLDivElement; | ||
|
|
||
| if (!overlayWrapper) { | ||
| throw new Error( | ||
| 'AppointmentPopup overlay wrapper not found in DOM', | ||
| ); | ||
| } | ||
|
|
||
| const POM = new PopupModel(overlayWrapper); | ||
|
|
||
| const dispose = (): void => { | ||
| popup.dispose(); | ||
| container.remove(); | ||
| }; | ||
|
|
||
| disposables.push(dispose); | ||
|
|
||
| return { | ||
| container, | ||
| popup, | ||
| form, | ||
| POM, | ||
| callbacks: { | ||
| addAppointment, | ||
| updateAppointment, | ||
| focus, | ||
| updateScrollPosition, | ||
| }, | ||
| dispose, | ||
| }; | ||
| }; | ||
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,92 @@ | ||||||||
| import { | ||||||||
| afterEach, beforeEach, describe, expect, it, | ||||||||
| } from '@jest/globals'; | ||||||||
|
|
||||||||
| import fx from '../../../common/core/animation/fx'; | ||||||||
| import { ACTION_TO_APPOINTMENT } from '../appointment_popup/m_popup'; | ||||||||
| import { | ||||||||
| createAppointmentPopup, | ||||||||
| disposeAppointmentPopups, | ||||||||
| } from './__mock__/create_appointment_popup'; | ||||||||
|
|
||||||||
| describe('Isolated AppointmentPopup environment', () => { | ||||||||
| beforeEach(() => { | ||||||||
| fx.off = true; | ||||||||
|
||||||||
| fx.off = true; | |
| fx.off = true; | |
| setupSchedulerTestEnvironment(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The option types here are very generic (
Record<string, unknown>,number), which reduces the value of these helpers and can hide breaking changes in the popup contract. Consider typingappointmentDataasPartial<SafeAppointment>(or similar) andactionas a union ofACTION_TO_APPOINTMENTvalues, and tighteningeditingto the scheduler editing config shape used byAppointmentPopup/AppointmentForm.