From 1385ce53b2fccf9fc662ed36e08cd1317328c95f Mon Sep 17 00:00:00 2001 From: Aleksey Semikozov Date: Fri, 20 Mar 2026 15:05:07 -0300 Subject: [PATCH] Scheduler - Create isolated AppointmentPopup test environment --- .../__mock__/create_appointment_popup.ts | 207 ++++++++++++++++++ .../__tests__/appointment_popup.test.ts | 92 ++++++++ 2 files changed, 299 insertions(+) create mode 100644 packages/devextreme/js/__internal/scheduler/__tests__/__mock__/create_appointment_popup.ts create mode 100644 packages/devextreme/js/__internal/scheduler/__tests__/appointment_popup.test.ts diff --git a/packages/devextreme/js/__internal/scheduler/__tests__/__mock__/create_appointment_popup.ts b/packages/devextreme/js/__internal/scheduler/__tests__/__mock__/create_appointment_popup.ts new file mode 100644 index 000000000000..3af84b25abaf --- /dev/null +++ b/packages/devextreme/js/__internal/scheduler/__tests__/__mock__/create_appointment_popup.ts @@ -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; + action?: number; + editing?: Record; + 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 => { + 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 => ( + 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 => $(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, + }; +}; diff --git a/packages/devextreme/js/__internal/scheduler/__tests__/appointment_popup.test.ts b/packages/devextreme/js/__internal/scheduler/__tests__/appointment_popup.test.ts new file mode 100644 index 000000000000..49b90c39fff0 --- /dev/null +++ b/packages/devextreme/js/__internal/scheduler/__tests__/appointment_popup.test.ts @@ -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; + }); + + afterEach(() => { + disposeAppointmentPopups(); + fx.off = false; + }); + + it('should render popup with form fields', async () => { + const { POM } = await createAppointmentPopup(); + + expect(POM.element).toBeTruthy(); + expect(POM.dxForm).toBeTruthy(); + }); + + it('should display appointment data in form', async () => { + const { POM } = await createAppointmentPopup({ + appointmentData: { + text: 'My Meeting', + startDate: new Date(2021, 3, 26, 9, 30), + endDate: new Date(2021, 3, 26, 11, 0), + }, + }); + + expect(POM.getInputValue('subjectEditor')).toBe('My Meeting'); + }); + + it('should have Save and Cancel buttons', async () => { + const { POM } = await createAppointmentPopup(); + + expect(POM.saveButton).toBeTruthy(); + expect(POM.cancelButton).toBeTruthy(); + }); + + it('should call addAppointment on Save click for CREATE action', async () => { + const { POM, callbacks } = await createAppointmentPopup({ + appointmentData: { + text: 'New Appointment', + startDate: new Date(2021, 3, 26, 9, 30), + endDate: new Date(2021, 3, 26, 11, 0), + }, + action: ACTION_TO_APPOINTMENT.CREATE, + }); + + POM.saveButton.click(); + + expect(callbacks.addAppointment).toHaveBeenCalledTimes(1); + expect(callbacks.addAppointment).toHaveBeenCalledWith( + expect.objectContaining({ + text: 'New Appointment', + startDate: new Date(2021, 3, 26, 9, 30), + endDate: new Date(2021, 3, 26, 11, 0), + }), + ); + }); + + it('should call updateAppointment on Save click for UPDATE action', async () => { + const { POM, callbacks } = await createAppointmentPopup({ + appointmentData: { + text: 'Existing Appointment', + startDate: new Date(2021, 3, 26, 9, 30), + endDate: new Date(2021, 3, 26, 11, 0), + }, + action: ACTION_TO_APPOINTMENT.UPDATE, + }); + + POM.saveButton.click(); + + expect(callbacks.updateAppointment).toHaveBeenCalledTimes(1); + }); + + it('should hide popup on Cancel click', async () => { + const { popup, POM } = await createAppointmentPopup(); + + expect(popup.visible).toBe(true); + POM.cancelButton.click(); + expect(popup.visible).toBe(false); + }); +});