From 021cf05e81f90ba3ef7dbd8aba2ce43c8f7f7d01 Mon Sep 17 00:00:00 2001 From: Maksim Zakharov Date: Wed, 13 May 2026 19:38:17 +0400 Subject: [PATCH 1/4] refactor(scheduler): appointment popup and recurrence form, drop m_ prefix --- .../__mock__/create_appointment_popup.ts | 6 +- .../__tests__/__mock__/model/scheduler.ts | 2 +- .../customize_form_items.test.ts | 2 +- ..._form_items.ts => customize_form_items.ts} | 0 .../appointment_popup/{m_form.ts => form.ts} | 67 +++-- .../{m_popup.ts => popup.ts} | 260 +++++++++++------- ..._recurrence_form.ts => recurrence_form.ts} | 59 ++-- .../scheduler/appointment_popup/utils.ts | 3 +- .../js/__internal/scheduler/m_scheduler.ts | 26 +- 9 files changed, 265 insertions(+), 160 deletions(-) rename packages/devextreme/js/__internal/scheduler/appointment_popup/{m_customize_form_items.ts => customize_form_items.ts} (100%) rename packages/devextreme/js/__internal/scheduler/appointment_popup/{m_form.ts => form.ts} (93%) rename packages/devextreme/js/__internal/scheduler/appointment_popup/{m_popup.ts => popup.ts} (62%) rename packages/devextreme/js/__internal/scheduler/appointment_popup/{m_recurrence_form.ts => recurrence_form.ts} (92%) 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 index 6e5fa1f384fb..89b16d6b4d52 100644 --- a/packages/devextreme/js/__internal/scheduler/__tests__/__mock__/create_appointment_popup.ts +++ b/packages/devextreme/js/__internal/scheduler/__tests__/__mock__/create_appointment_popup.ts @@ -4,11 +4,11 @@ import $ from '@js/core/renderer'; import { Deferred } from '@js/core/utils/deferred'; import { mockTimeZoneCalculator } from '../../__mock__/timezone_calculator.mock'; -import { AppointmentForm } from '../../appointment_popup/m_form'; +import { AppointmentForm } from '../../appointment_popup/form'; import { APPOINTMENT_POPUP_CLASS, AppointmentPopup, -} from '../../appointment_popup/m_popup'; +} from '../../appointment_popup/popup'; import { AppointmentDataAccessor, } from '../../utils/data_accessor/appointment_data_accessor'; @@ -132,7 +132,7 @@ export const createAppointmentPopup = async ( const form = new AppointmentForm(formSchedulerProxy); - const noop = (): void => {}; + const noop = (): void => { }; const popupSchedulerProxy = { getElement: (): ReturnType => $(container), diff --git a/packages/devextreme/js/__internal/scheduler/__tests__/__mock__/model/scheduler.ts b/packages/devextreme/js/__internal/scheduler/__tests__/__mock__/model/scheduler.ts index 4d59d0186a62..8a59c6cd9f84 100644 --- a/packages/devextreme/js/__internal/scheduler/__tests__/__mock__/model/scheduler.ts +++ b/packages/devextreme/js/__internal/scheduler/__tests__/__mock__/model/scheduler.ts @@ -1,7 +1,7 @@ import { within } from '@testing-library/dom'; import { ToolbarModel } from '@ts/scheduler/__tests__/__mock__/model/toolbar'; -import { APPOINTMENT_POPUP_CLASS } from '../../../appointment_popup/m_popup'; +import { APPOINTMENT_POPUP_CLASS } from '../../../appointment_popup/popup'; import { POPUP_DIALOG_CLASS } from '../../../m_scheduler'; import type { AppointmentModel } from './appointment'; import { createAppointmentModel } from './appointment'; diff --git a/packages/devextreme/js/__internal/scheduler/appointment_popup/customize_form_items.test.ts b/packages/devextreme/js/__internal/scheduler/appointment_popup/customize_form_items.test.ts index f5f0e065f352..37bf97a0d2ce 100644 --- a/packages/devextreme/js/__internal/scheduler/appointment_popup/customize_form_items.test.ts +++ b/packages/devextreme/js/__internal/scheduler/appointment_popup/customize_form_items.test.ts @@ -2,7 +2,7 @@ import { describe, expect, it, } from '@jest/globals'; -import { type ConfigItem, customizeFormItems, type FormItem } from './m_customize_form_items'; +import { type ConfigItem, customizeFormItems, type FormItem } from './customize_form_items'; const subjectGroup: FormItem = { name: 'subjectGroup', diff --git a/packages/devextreme/js/__internal/scheduler/appointment_popup/m_customize_form_items.ts b/packages/devextreme/js/__internal/scheduler/appointment_popup/customize_form_items.ts similarity index 100% rename from packages/devextreme/js/__internal/scheduler/appointment_popup/m_customize_form_items.ts rename to packages/devextreme/js/__internal/scheduler/appointment_popup/customize_form_items.ts diff --git a/packages/devextreme/js/__internal/scheduler/appointment_popup/m_form.ts b/packages/devextreme/js/__internal/scheduler/appointment_popup/form.ts similarity index 93% rename from packages/devextreme/js/__internal/scheduler/appointment_popup/m_form.ts rename to packages/devextreme/js/__internal/scheduler/appointment_popup/form.ts index f399691e525d..676c1d36a4b0 100644 --- a/packages/devextreme/js/__internal/scheduler/appointment_popup/m_form.ts +++ b/packages/devextreme/js/__internal/scheduler/appointment_popup/form.ts @@ -4,11 +4,12 @@ import '@js/ui/tag_box'; import '@js/ui/switch'; import '@js/ui/select_box'; -import type { TextEditorButton } from '@js/common'; +import type { DayOfWeek, TextEditorButton } from '@js/common'; import messageLocalization from '@js/common/core/localization/message'; import { DataSource } from '@js/common/data'; import type { dxElementWrapper } from '@js/core/renderer'; import $ from '@js/core/renderer'; +import { noop } from '@js/core/utils/common'; import dateUtils from '@js/core/utils/date'; import { extend } from '@js/core/utils/extend'; import { isBoolean } from '@js/core/utils/type'; @@ -27,14 +28,17 @@ import DropDownEditor from '@ts/ui/drop_down_editor/m_drop_down_editor'; import type Popup from '@ts/ui/popup/m_popup'; import timeZoneUtils from '../m_utils_time_zone'; +import type { TimeZoneCalculator } from '../r1/timezone_calculator/calculator'; import type { SafeAppointment } from '../types'; +import type { AppointmentDataAccessor } from '../utils/data_accessor/appointment_data_accessor'; import type { ResourceLoader } from '../utils/loader/resource_loader'; import { DEFAULT_ICONS_SHOW_MODE } from '../utils/options/constants'; import { getAppointmentGroupIndex, getRawAppointmentGroupValues, getSafeGroupValues } from '../utils/resource_manager/appointment_groups_utils'; import type { ResourceManager } from '../utils/resource_manager/resource_manager'; +import { customizeFormItems } from './customize_form_items'; import { getRepeatSelectItems, REPEAT_NEVER_VALUE } from './localized_items'; -import { customizeFormItems } from './m_customize_form_items'; -import { RecurrenceForm } from './m_recurrence_form'; +import type { AppointmentPopupContext } from './popup'; +import { RecurrenceForm } from './recurrence_form'; import { createFormIconTemplate, getStartDateCommonConfig, RecurrenceRule } from './utils'; const CLASSES = { @@ -117,8 +121,20 @@ const REPEAT_ICON_NAME = 'repeatIcon'; const RESOURCES_GROUP_ICON_NAME = 'resourcesGroupIcon'; const DESCRIPTION_ICON_NAME = 'descriptionIcon'; +export interface AppointmentFormScheduler { + getResourceById: () => ResourceManager['resourceById']; + getDataAccessors: () => AppointmentDataAccessor; + createComponent: (element: dxElementWrapper, component: unknown, options: unknown) => T; + getEditingConfig: () => SchedulerProperties['editing']; + getResourceManager: () => ResourceManager; + getFirstDayOfWeek: () => DayOfWeek; + getStartDayHour: () => number; + getCalculatedEndDate: (startDate: Date) => Date; + getTimeZoneCalculator: () => TimeZoneCalculator; +} + export class AppointmentForm { - private readonly scheduler: any; + private readonly scheduler: AppointmentFormScheduler; private readonly resourceManager!: ResourceManager; @@ -126,7 +142,7 @@ export class AppointmentForm { private recurrenceForm!: RecurrenceForm; - private _popup!: any; + private _popup!: AppointmentPopupContext; private $mainGroup?: dxElementWrapper; @@ -137,7 +153,7 @@ export class AppointmentForm { } private get dxPopup(): Popup { - return this._popup.dxPopup as Popup; + return this._popup.dxPopup; } get readOnly(): boolean { @@ -149,11 +165,11 @@ export class AppointmentForm { this.recurrenceForm.setReadOnly(value); } - get formData(): Record { - return this.dxForm.option('formData') as Record; + get formData(): Record { + return this.dxForm.option('formData') as Record; } - set formData(formData: Record) { + set formData(formData: Record) { this.dxForm.option('formData', formData); } @@ -178,12 +194,12 @@ export class AppointmentForm { return value ?? null; } - constructor(scheduler: any) { + constructor(scheduler: AppointmentFormScheduler) { this.scheduler = scheduler; this.resourceManager = scheduler.getResourceManager(); } - private getFormDataField(field: string): any { + private getFormDataField(field: string): unknown { return this.dxForm.option(`formData.${field}`); } @@ -195,7 +211,7 @@ export class AppointmentForm { } } - create(popup: any): void { + create(popup: AppointmentPopupContext): void { this._popup = popup; const mainGroup = this.createMainFormGroup(); @@ -213,13 +229,16 @@ export class AppointmentForm { this.applyFormItemDefaults(recurrenceGroup, showRecurrenceGroupIcons); const editingConfig = this.scheduler.getEditingConfig(); - const customizedItems = customizeFormItems(items, editingConfig?.form?.items); + const customizedItems = customizeFormItems( + items, + !isBoolean(editingConfig) ? editingConfig?.form?.items : undefined, + ); this.createForm(customizedItems); } private getIconsShowMode(): AppointmentFormIconsShowMode { - const editingConfig = this.scheduler.getEditingConfig() as SchedulerProperties['editing']; + const editingConfig = this.scheduler.getEditingConfig(); if (isBoolean(editingConfig)) { return DEFAULT_ICONS_SHOW_MODE; @@ -231,9 +250,10 @@ export class AppointmentForm { private createForm(items: FormProperties['items']): dxForm { const element = $('
'); const editingConfig = this.scheduler.getEditingConfig(); + const formConfig = !isBoolean(editingConfig) ? editingConfig?.form : undefined; const { items: formItems, onContentReady, onInitialized, ...customFormOptions - } = editingConfig?.form ?? {}; + } = formConfig ?? {}; const defaultOptions: FormProperties = { items, @@ -279,7 +299,7 @@ export class AppointmentForm { } if (isResourceChanged) { - this.updateSubjectIconColor(); + this.updateSubjectIconColor().catch(noop); } }, onInitialized: (e): void => { @@ -300,7 +320,7 @@ export class AppointmentForm { } as FormProperties; const formOptions = extend(true, defaultOptions, customFormOptions); - return this.scheduler.createComponent(element, dxForm, formOptions) as dxForm; + return this.scheduler.createComponent(element, dxForm, formOptions); } private createMainFormGroup(): GroupItem { @@ -498,7 +518,9 @@ export class AppointmentForm { timeItemOptions?: SimpleItem, timezoneItemOptions?: SimpleItem, ): GroupItem { - const { allowTimeZoneEditing } = this.scheduler.getEditingConfig(); + const editingConfig = this.scheduler.getEditingConfig(); + const allowTimeZoneEditing = !isBoolean(editingConfig) + ? editingConfig?.allowTimeZoneEditing : undefined; const { startDateExpr, endDateExpr } = this.scheduler.getDataAccessors().expr; const isStartDateEditor = dateExpr === startDateExpr; @@ -572,7 +594,11 @@ export class AppointmentForm { editorOptions: { onValueChanged: (e) => { dateValueChanged(e, (date: Date): void => { - date.setFullYear(e.value.getFullYear(), e.value.getMonth(), e.value.getDate()); + date.setFullYear( + e.value.getFullYear(), + e.value.getMonth(), + e.value.getDate(), + ); }); }, onContentReady: (e): void => { @@ -855,7 +881,8 @@ export class AppointmentForm { showMainGroup(): void { const currentHeight = this.dxPopup.option('height') as string | number | undefined; const editingConfig = this.scheduler.getEditingConfig(); - const configuredHeight = editingConfig?.popup?.height ?? 'auto'; + const popupConfig = !isBoolean(editingConfig) ? editingConfig?.popup : undefined; + const configuredHeight = popupConfig?.height ?? 'auto'; if (typeof currentHeight === 'number') { this.dxPopup.option('height', configuredHeight); diff --git a/packages/devextreme/js/__internal/scheduler/appointment_popup/m_popup.ts b/packages/devextreme/js/__internal/scheduler/appointment_popup/popup.ts similarity index 62% rename from packages/devextreme/js/__internal/scheduler/appointment_popup/m_popup.ts rename to packages/devextreme/js/__internal/scheduler/appointment_popup/popup.ts index 2f58384931b9..a2b627ae3c90 100644 --- a/packages/devextreme/js/__internal/scheduler/appointment_popup/m_popup.ts +++ b/packages/devextreme/js/__internal/scheduler/appointment_popup/popup.ts @@ -2,21 +2,31 @@ import { triggerResizeEvent } from '@js/common/core/events/visibility_change'; import messageLocalization from '@js/common/core/localization/message'; import type { dxElementWrapper } from '@js/core/renderer'; import $ from '@js/core/renderer'; +import { noop } from '@js/core/utils/common'; import dateUtils from '@js/core/utils/date'; -import { Deferred, when } from '@js/core/utils/deferred'; import { extend } from '@js/core/utils/extend'; import { getWidth } from '@js/core/utils/size'; +import { isBoolean } from '@js/core/utils/type'; import { getWindow } from '@js/core/utils/window'; -import type { Properties as PopupProperties, ToolbarItem } from '@js/ui/popup'; +import type { + Properties as PopupProperties, + ShowingEvent, + ToolbarItem, +} from '@js/ui/popup'; import type dxPopup from '@js/ui/popup'; import Popup from '@js/ui/popup/ui.popup'; +import type { Properties as SchedulerProperties } from '@js/ui/scheduler'; import { current, isFluent } from '@js/ui/themes'; +import errors from '@js/ui/widget/ui.errors'; import { hide as hideLoading, show as showLoading } from '../m_loading'; +import type { TimeZoneCalculator } from '../r1/timezone_calculator/calculator'; import type { SafeAppointment } from '../types'; import { AppointmentAdapter } from '../utils/appointment_adapter/appointment_adapter'; +import type { AppointmentDataAccessor } from '../utils/data_accessor/appointment_data_accessor'; import { getRawAppointmentGroupValues } from '../utils/resource_manager/appointment_groups_utils'; -import type { AppointmentForm } from './m_form'; +import type { ResourceManager } from '../utils/resource_manager/resource_manager'; +import type { AppointmentForm } from './form'; export const APPOINTMENT_POPUP_CLASS = 'dx-scheduler-appointment-popup'; @@ -28,17 +38,56 @@ export interface AppointmentPopupConfig { readOnly: boolean; } +export interface AppointmentPopupState { + saveChangesLocker: boolean; + appointment: { + data: SafeAppointment | null; + }; +} + +interface AppointmentFormOpeningArgs { + form: AppointmentForm['dxForm']; + popup: dxPopup; + appointmentData: SafeAppointment | null; + cancel: boolean; +} + +export interface AppointmentPopupContext { + dxPopup: Popup; + updateToolbarForMainGroup: () => void; + updateToolbarForRecurrenceGroup: () => void; +} + +interface AppointmentPopupScheduler { + getElement: () => dxElementWrapper; + createComponent: ( + element: dxElementWrapper, + component: unknown, + options: unknown, + ) => unknown; + focus: () => void; + getResourceManager: () => ResourceManager; + getEditingConfig: () => SchedulerProperties['editing']; + getTimeZoneCalculator: () => TimeZoneCalculator; + getDataAccessors: () => AppointmentDataAccessor; + getAppointmentFormOpening: () => (args: AppointmentFormOpeningArgs) => void; + processActionResult: ( + args: AppointmentFormOpeningArgs, + callback: (canceled: boolean) => void, + ) => void; +} + export class AppointmentPopup { - scheduler: any; + scheduler: AppointmentPopupScheduler; form: AppointmentForm; // TODO: backing field for popup getter, cannot rename due to name conflict - private _popup?: dxPopup; + private popupInstance?: Popup; private customPopupOptions?: PopupProperties; - state: any; + state: AppointmentPopupState; private config: AppointmentPopupConfig = { onSave: () => Promise.resolve(), @@ -46,15 +95,15 @@ export class AppointmentPopup { readOnly: false, }; - get popup(): dxPopup { - return this._popup as dxPopup; + get popup(): Popup { + return this.popupInstance as Popup; } get visible(): boolean { - return Boolean(this._popup?.option('visible')); + return Boolean(this.popupInstance?.option('visible')); } - constructor(scheduler: any, form: AppointmentForm) { + constructor(scheduler: AppointmentPopupScheduler, form: AppointmentForm) { this.scheduler = scheduler; this.form = form; @@ -66,7 +115,7 @@ export class AppointmentPopup { }; } - show(appointment, config: AppointmentPopupConfig) { + show(appointment: SafeAppointment, config: AppointmentPopupConfig): void { this.state.appointment.data = appointment; this.config = config; @@ -75,28 +124,36 @@ export class AppointmentPopup { const popupConfig = this.createPopupConfig(); this.createPopup(popupConfig); - this._popup!.show(); + if (this.popupInstance) { + this.popupInstance.show().catch(noop); + } else { + throw errors.Error('E1033'); + } } - hide() { - this._popup?.hide(); + hide(): void { + if (this.popupInstance) { + this.popupInstance.hide().catch(noop); + } else { + throw errors.Error('E1033'); + } } - dispose() { + dispose(): void { this.disposePopup(); } private disposePopup(): void { - if (this._popup) { - const $element = this._popup.$element(); + if (this.popupInstance) { + const $element = this.popupInstance.$element(); this.form.dispose(); - this._popup.dispose(); + this.popupInstance.dispose(); $element.remove(); - this._popup = undefined; + this.popupInstance = undefined; } } - private createPopup(options): void { + private createPopup(options: PopupProperties): void { const popupElement = $('
') .addClass(APPOINTMENT_POPUP_CLASS) .appendTo(this.scheduler.getElement()); @@ -106,7 +163,8 @@ export class AppointmentPopup { private createPopupConfig(): PopupProperties { const editingConfig = this.scheduler.getEditingConfig(); - const customPopupOptions = editingConfig?.popup ?? {}; + const popupConfig = !isBoolean(editingConfig) ? editingConfig?.popup : undefined; + const customPopupOptions = popupConfig ?? {}; this.customPopupOptions = customPopupOptions; @@ -119,7 +177,7 @@ export class AppointmentPopup { enableBodyScroll: false, _ignorePreventScrollEventsDeprecation: true, onInitialized: (e): void => { - this._popup = e.component; + this.popupInstance = e.component; customPopupOptions?.onInitialized?.(e); }, onHiding: (e): void => { @@ -135,7 +193,7 @@ export class AppointmentPopup { return this.form.dxForm.$element(); }, - onShowing: (e): void => { + onShowing: (e: ShowingEvent): void => { this.onShowing(e); customPopupOptions?.onShowing?.(e); }, @@ -149,13 +207,15 @@ export class AppointmentPopup { }) as PopupProperties; } - private onShowing(e) { + private onShowing(e): void { this.updateForm(); - e.component.$overlayContent().attr( - 'aria-label', - messageLocalization.format('dxScheduler-ariaEditForm'), - ); + e.component + .$overlayContent() + .attr( + 'aria-label', + messageLocalization.format('dxScheduler-ariaEditForm'), + ); const arg = { form: this.form.dxForm, @@ -195,7 +255,9 @@ export class AppointmentPopup { this.form.showMainGroup(); } - private createFormData(appointmentAdapter: AppointmentAdapter): Record { + private createFormData( + appointmentAdapter: AppointmentAdapter, + ): Record { const { resources } = this.scheduler.getResourceManager(); const groupValues = getRawAppointmentGroupValues( appointmentAdapter.source as SafeAppointment, @@ -230,7 +292,7 @@ export class AppointmentPopup { updatePopupFullScreenMode(): void { if (this.visible) { - const isPopupFullScreenNeeded = () => { + const isPopupFullScreenNeeded = (): boolean => { const window = getWindow(); const width = window && getWidth(window); @@ -250,62 +312,52 @@ export class AppointmentPopup { } } - saveChangesAsync(isShowLoadPanel) { + saveChangesAsync(isShowLoadPanel = false): Promise { this.form.saveRecurrenceValue(); - // @ts-expect-error - const deferred = new Deferred(); const validation = this.form.dxForm.validate(); - isShowLoadPanel && this.showLoadPanel(); - - when(validation?.complete ?? validation).done((validation) => { - if (validation && !(validation as any).isValid) { - hideLoading(); - deferred.resolve(false); - return; - } - - const adapter = this.createAppointmentAdapter(this.form.formData); - const clonedAdapter = adapter - .clone() - .calculateDates(this.scheduler.getTimeZoneCalculator(), 'fromAppointment'); + if (isShowLoadPanel) { + this.showLoadPanel(); + } - this.addMissingDSTTime(adapter, clonedAdapter); + return Promise.resolve(validation?.complete ?? validation) + .then((validationResult) => { + if (!validationResult?.isValid) { + return undefined; + } - const appointment = clonedAdapter.source; + const adapter = this.createAppointmentAdapter(this.form.formData); + const clonedAdapter = adapter + .clone() + .calculateDates(this.scheduler.getTimeZoneCalculator(), 'fromAppointment'); - when(this.config.onSave(appointment)).done(deferred.resolve); + this.addMissingDSTTime(adapter, clonedAdapter); - deferred.done(() => { - hideLoading(); - }); - }); + const appointment = clonedAdapter.source; - return deferred.promise(); + return this.config.onSave(appointment); + }) + .then(() => { hideLoading(); }) + .catch(noop); } - private saveButtonClickHandler(e) { + private saveButtonClickHandler(e): void { e.cancel = true; - this.saveEditDataAsync(); + this.saveEditDataAsync().catch(noop); } - saveEditDataAsync() { - // @ts-expect-error - const deferred = new Deferred(); - + private saveEditDataAsync(): Promise { if (this.tryLockSaveChanges()) { - when(this.saveChangesAsync(true)).done(() => { + return this.saveChangesAsync(true).then(() => { this.unlockSaveChanges(); - deferred.resolve(); }); } - - return deferred.promise(); + return Promise.resolve(); } - private showLoadPanel() { - const container = (this.popup as any).$overlayContent(); + private showLoadPanel(): void { + const container = this.popupInstance?.$overlayContent(); showLoading({ container, @@ -315,19 +367,22 @@ export class AppointmentPopup { }); } - private tryLockSaveChanges() { - if (this.state.saveChangesLocker === false) { + private tryLockSaveChanges(): boolean { + if (!this.state.saveChangesLocker) { this.state.saveChangesLocker = true; return true; } return false; } - private unlockSaveChanges() { + private unlockSaveChanges(): void { this.state.saveChangesLocker = false; } - private addMissingDSTTime(formAppointmentAdapter, clonedAppointmentAdapter) { + private addMissingDSTTime( + formAppointmentAdapter: AppointmentAdapter, + clonedAppointmentAdapter: AppointmentAdapter, + ): void { const timeZoneCalculator = this.scheduler.getTimeZoneCalculator(); clonedAppointmentAdapter.startDate = this.addMissingDSTShiftToDate( @@ -345,13 +400,26 @@ export class AppointmentPopup { } } - private addMissingDSTShiftToDate(timeZoneCalculator, originFormDate, clonedDate) { - const originTimezoneShift = timeZoneCalculator.getOffsets(originFormDate)?.common; - const clonedTimezoneShift = timeZoneCalculator.getOffsets(clonedDate)?.common; + private addMissingDSTShiftToDate( + timeZoneCalculator: TimeZoneCalculator, + originFormDate: Date, + clonedDate: Date, + ): Date { + const originTimezoneShift = timeZoneCalculator.getOffsets( + originFormDate, + undefined, + ).common; + const clonedTimezoneShift = timeZoneCalculator.getOffsets( + clonedDate, + undefined, + ).common; const shiftDifference = originTimezoneShift - clonedTimezoneShift; return shiftDifference - ? new Date(clonedDate.getTime() + shiftDifference * dateUtils.dateToMilliseconds('hour')) + ? new Date( + clonedDate.getTime() + + shiftDifference * dateUtils.dateToMilliseconds('hour'), + ) : clonedDate; } @@ -368,28 +436,30 @@ export class AppointmentPopup { return; } - const toolbarItems: ToolbarItem[] = [{ - toolbar: 'top', - location: 'before', - text: this.config.title, - cssClass: 'dx-toolbar-label', - }]; + const toolbarItems: ToolbarItem[] = [ + { + toolbar: 'top', + location: 'before', + text: this.config.title, + cssClass: 'dx-toolbar-label', + }, + ]; const canSave = !this.form.readOnly; if (canSave) { - toolbarItems.push( - { - toolbar: 'top', - location: 'after', - options: { - onClick: (e) => this.saveButtonClickHandler(e), - stylingMode: 'contained', - type: 'default', - text: messageLocalization.format('dxScheduler-editPopupSaveButtonText'), - }, - shortcut: 'done', - } as ToolbarItem, - ); + toolbarItems.push({ + toolbar: 'top', + location: 'after', + options: { + onClick: (e) => this.saveButtonClickHandler(e), + stylingMode: 'contained', + type: 'default', + text: messageLocalization.format( + 'dxScheduler-editPopupSaveButtonText', + ), + }, + shortcut: 'done', + } as ToolbarItem); } toolbarItems.push({ @@ -443,7 +513,9 @@ export class AppointmentPopup { onClick: (e) => this.saveButtonClickHandler(e), stylingMode: 'contained', type: 'default', - text: messageLocalization.format('dxScheduler-editPopupSaveButtonText'), + text: messageLocalization.format( + 'dxScheduler-editPopupSaveButtonText', + ), }, shortcut: 'done', } as ToolbarItem); diff --git a/packages/devextreme/js/__internal/scheduler/appointment_popup/m_recurrence_form.ts b/packages/devextreme/js/__internal/scheduler/appointment_popup/recurrence_form.ts similarity index 92% rename from packages/devextreme/js/__internal/scheduler/appointment_popup/m_recurrence_form.ts rename to packages/devextreme/js/__internal/scheduler/appointment_popup/recurrence_form.ts index 0247a77a473d..b5383a1ebd71 100644 --- a/packages/devextreme/js/__internal/scheduler/appointment_popup/m_recurrence_form.ts +++ b/packages/devextreme/js/__internal/scheduler/appointment_popup/recurrence_form.ts @@ -10,7 +10,7 @@ import type { Properties as NumberBoxProperties } from '@js/ui/number_box'; import type { Properties as RadioGroupProperties } from '@js/ui/radio_group'; import type { Properties as SelectBoxProperties } from '@js/ui/select_box'; -import type Scheduler from '../m_scheduler'; +import type { AppointmentFormScheduler } from './form'; import { getRecurrenceFrequencyItems, getRecurrenceMonthItems, @@ -84,7 +84,7 @@ const RECURRENCE_GROUP_NAME = 'recurrenceGroup'; export class RecurrenceForm { recurrenceRule: RecurrenceRule = new RecurrenceRule('', new Date()); - private readonly scheduler: any; + private readonly scheduler: AppointmentFormScheduler; private dxFormInstance?: dxForm; @@ -92,7 +92,7 @@ export class RecurrenceForm { private readOnly = false; - constructor(scheduler: Scheduler) { + constructor(scheduler: AppointmentFormScheduler) { this.scheduler = scheduler; } @@ -271,6 +271,7 @@ export class RecurrenceForm { e.component.option('value', this.recurrenceRule.frequency); if (needRestoreFrequencyEditorFocus) { + // eslint-disable-next-line no-restricted-globals setTimeout(() => { e.component.focus(); needRestoreFrequencyEditorFocus = false; @@ -313,33 +314,37 @@ export class RecurrenceForm { const buttonContainer = $('
').appendTo($container); this.weekDayButtons[item.key]?.dispose(); - this.weekDayButtons[item.key] = this.scheduler.createComponent(buttonContainer, Button, { - text: item.text, - disabled: this.readOnly, - onContentReady: (e): void => { - $(e.element).removeClass('dx-button-has-text'); + this.weekDayButtons[item.key] = this.scheduler.createComponent( + buttonContainer, + Button, + { + text: item.text, + disabled: this.readOnly, + onContentReady: (e): void => { + $(e.element).removeClass('dx-button-has-text'); - const isSelected = this.recurrenceRule.byDay.includes(item.key); + const isSelected = this.recurrenceRule.byDay.includes(item.key); - e.component.option('stylingMode', isSelected ? 'contained' : 'outlined'); - e.component.option('type', isSelected ? 'default' : 'normal'); - }, - onClick: (e): void => { - const isSelected = this.recurrenceRule.byDay.includes(item.key); - - if (isSelected) { - const index = this.recurrenceRule.byDay.indexOf(item.key); - - this.recurrenceRule.byDay.splice(index, 1); - e.component.option('stylingMode', 'outlined'); - e.component.option('type', 'normal'); - } else { - this.recurrenceRule.byDay.push(item.key); - e.component.option('stylingMode', 'contained'); - e.component.option('type', 'default'); - } + e.component.option('stylingMode', isSelected ? 'contained' : 'outlined'); + e.component.option('type', isSelected ? 'default' : 'normal'); + }, + onClick: (e): void => { + const isSelected = this.recurrenceRule.byDay.includes(item.key); + + if (isSelected) { + const index = this.recurrenceRule.byDay.indexOf(item.key); + + this.recurrenceRule.byDay.splice(index, 1); + e.component.option('stylingMode', 'outlined'); + e.component.option('type', 'normal'); + } else { + this.recurrenceRule.byDay.push(item.key); + e.component.option('stylingMode', 'contained'); + e.component.option('type', 'default'); + } + }, }, - }); + ); }); return $container; diff --git a/packages/devextreme/js/__internal/scheduler/appointment_popup/utils.ts b/packages/devextreme/js/__internal/scheduler/appointment_popup/utils.ts index 7acc0615dd6a..ca86ae5b6914 100644 --- a/packages/devextreme/js/__internal/scheduler/appointment_popup/utils.ts +++ b/packages/devextreme/js/__internal/scheduler/appointment_popup/utils.ts @@ -1,4 +1,5 @@ /* eslint-disable spellcheck/spell-checker */ +import type { DayOfWeek } from '@js/common'; import type { dxElementWrapper } from '@js/core/renderer'; import $ from '@js/core/renderer'; import dateUtils from '@js/core/utils/date'; @@ -13,7 +14,7 @@ import type { Rule } from '../recurrence/types'; export const createFormIconTemplate = (iconName: string): () => dxElementWrapper => (): dxElementWrapper => getImageContainer(iconName) ?? $('
').addClass('dx-scheduler-form-icon-sized-gap'); -export const getStartDateCommonConfig = (firstDayOfWeek: string): SimpleItem => ({ +export const getStartDateCommonConfig = (firstDayOfWeek: DayOfWeek): SimpleItem => ({ colSpan: 1, itemType: 'simple', editorType: 'dxDateBox', diff --git a/packages/devextreme/js/__internal/scheduler/m_scheduler.ts b/packages/devextreme/js/__internal/scheduler/m_scheduler.ts index 9ccfdc1142b8..4893a126b582 100644 --- a/packages/devextreme/js/__internal/scheduler/m_scheduler.ts +++ b/packages/devextreme/js/__internal/scheduler/m_scheduler.ts @@ -37,8 +37,8 @@ import { dateUtilsTs } from '@ts/core/utils/date'; import { createA11yStatusContainer } from './a11y_status/a11y_status_render'; import { getA11yStatusText } from './a11y_status/a11y_status_text'; -import { AppointmentForm } from './appointment_popup/m_form'; -import { AppointmentPopup } from './appointment_popup/m_popup'; +import { AppointmentForm } from './appointment_popup/form'; +import { AppointmentPopup } from './appointment_popup/popup'; import AppointmentCollection from './appointments/m_appointment_collection'; import type { AppointmentsProperties } from './appointments_new/appointments'; import { Appointments } from './appointments_new/appointments'; @@ -396,7 +396,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { this.postponeDataSourceLoading(); break; - // TODO Vinogradov refactoring: merge it with startDayHour / endDayHour + // TODO Vinogradov refactoring: merge it with startDayHour / endDayHour case 'offset': this.updateAppointmentDataSource(); @@ -1432,13 +1432,13 @@ class Scheduler extends SchedulerOptionsBaseWidget { const scrolling = this.getViewOption('scrolling'); const isVirtualScrolling = scrolling.mode === 'virtual'; const horizontalVirtualScrollingAllowed = isVirtualScrolling - && ( - !isDefined(scrolling.orientation) - || ['horizontal', 'both'].includes(scrolling.orientation) - ); + && ( + !isDefined(scrolling.orientation) + || ['horizontal', 'both'].includes(scrolling.orientation) + ); const crossScrollingEnabled = this.option('crossScrollingEnabled') - || horizontalVirtualScrollingAllowed - || isTimelineView(currentViewOptions.type); + || horizontalVirtualScrollingAllowed + || isTimelineView(currentViewOptions.type); const result = extend({ resources: this.option('resources'), @@ -1764,8 +1764,8 @@ class Scheduler extends SchedulerOptionsBaseWidget { const duration = appointmentEndDate.getTime() - appointmentStartDate.getTime(); const isKeepAppointmentHours = this._workSpace.keepOriginalHours() - && dateUtilsTs.isValidDate(appointment.startDate) - && dateUtilsTs.isValidDate(cellStartDate); + && dateUtilsTs.isValidDate(appointment.startDate) + && dateUtilsTs.isValidDate(cellStartDate); if (isKeepAppointmentHours) { const startDate = this.timeZoneCalculator.createDate(appointmentStartDate, 'toGrid'); @@ -2173,7 +2173,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { const isVirtualScrolling = mode === 'virtual'; return isVirtualScrolling - && (orientation === 'horizontal' || orientation === 'both'); + && (orientation === 'horizontal' || orientation === 'both'); } addAppointment(rawAppointment) { @@ -2252,7 +2252,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { } this.checkRecurringAppointment( appointment, - { }, + {}, date, () => { this.processDeleteAppointment( From 17bdcd814dfadae058c466daf3b8ab53f6e98abe Mon Sep 17 00:00:00 2001 From: Maksim Zakharov Date: Wed, 13 May 2026 20:05:16 +0400 Subject: [PATCH 2/4] fix(scheduler): set getFirstDayOfWeek instead of option('firstDayOfWeek') --- packages/devextreme/js/__internal/scheduler/m_scheduler.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/devextreme/js/__internal/scheduler/m_scheduler.ts b/packages/devextreme/js/__internal/scheduler/m_scheduler.ts index 4893a126b582..6d42ff34ba8b 100644 --- a/packages/devextreme/js/__internal/scheduler/m_scheduler.ts +++ b/packages/devextreme/js/__internal/scheduler/m_scheduler.ts @@ -37,6 +37,7 @@ import { dateUtilsTs } from '@ts/core/utils/date'; import { createA11yStatusContainer } from './a11y_status/a11y_status_render'; import { getA11yStatusText } from './a11y_status/a11y_status_text'; +import type { AppointmentFormScheduler } from './appointment_popup/form'; import { AppointmentForm } from './appointment_popup/form'; import { AppointmentPopup } from './appointment_popup/popup'; import AppointmentCollection from './appointments/m_appointment_collection'; @@ -1132,7 +1133,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { } createAppointmentForm() { - const scheduler = { + const scheduler: AppointmentFormScheduler = { getResourceById: () => this.resourceManager.resourceById, getDataAccessors: () => this._dataAccessors, // @ts-expect-error @@ -1141,7 +1142,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { getEditingConfig: () => this.editing, getResourceManager: () => this.resourceManager, - getFirstDayOfWeek: () => this.option('firstDayOfWeek'), + getFirstDayOfWeek: () => this.getFirstDayOfWeek(), getStartDayHour: () => this.option('startDayHour'), getCalculatedEndDate: (startDateWithStartHour) => this._workSpace.calculateEndDate(startDateWithStartHour), getTimeZoneCalculator: () => this.timeZoneCalculator, From c92bbab2cd81b1e1ceed76b6f08fcfb108407f62 Mon Sep 17 00:00:00 2001 From: Maksim Zakharov Date: Thu, 14 May 2026 17:34:45 +0400 Subject: [PATCH 3/4] test(scheduler): await save before asserting in popup tests --- .../__mock__/create_appointment_popup.ts | 1 + .../scheduler/__tests__/scheduler.test.ts | 1 + .../appointment_popup.integration.test.ts | 15 +++++++++++++++ .../appointment_popup/appointment_popup.test.ts | 3 +++ .../appointment.scroll.tests.js | 7 +++++++ .../timezones.tests.js | 6 ++++++ 6 files changed, 33 insertions(+) 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 index 89b16d6b4d52..f2f057b71518 100644 --- a/packages/devextreme/js/__internal/scheduler/__tests__/__mock__/create_appointment_popup.ts +++ b/packages/devextreme/js/__internal/scheduler/__tests__/__mock__/create_appointment_popup.ts @@ -130,6 +130,7 @@ export const createAppointmentPopup = async ( getTimeZoneCalculator: (): typeof timeZoneCalculator => timeZoneCalculator, }; + // @ts-expect-error // TODO: redo changed Scheduler popup api const form = new AppointmentForm(formSchedulerProxy); const noop = (): void => { }; diff --git a/packages/devextreme/js/__internal/scheduler/__tests__/scheduler.test.ts b/packages/devextreme/js/__internal/scheduler/__tests__/scheduler.test.ts index cd8174bb94de..ab3c5586279c 100644 --- a/packages/devextreme/js/__internal/scheduler/__tests__/scheduler.test.ts +++ b/packages/devextreme/js/__internal/scheduler/__tests__/scheduler.test.ts @@ -109,6 +109,7 @@ describe('Scheduler scrollTo', () => { const item = (scheduler as any).getDataSource().items()[0]; scheduler.showAppointmentPopup(item); POM.popup.saveButton.click(); + await Promise.resolve(); expect(scrollLeftBeforeSave).toBe(0); expect(scrollableContainer.scrollLeft).toBeGreaterThan(0); diff --git a/packages/devextreme/js/__internal/scheduler/appointment_popup/appointment_popup.integration.test.ts b/packages/devextreme/js/__internal/scheduler/appointment_popup/appointment_popup.integration.test.ts index 7253ba603631..a3843ab104e2 100644 --- a/packages/devextreme/js/__internal/scheduler/appointment_popup/appointment_popup.integration.test.ts +++ b/packages/devextreme/js/__internal/scheduler/appointment_popup/appointment_popup.integration.test.ts @@ -85,6 +85,7 @@ describe('Appointment Form', () => { scheduler.showAppointmentPopup(item); POM.popup.setInputValue('subjectEditor', 'New Subject'); POM.popup.saveButton.click(); + await Promise.resolve(); expect(dataSource.items()[0]).toMatchObject({ ...commonAppointment, @@ -121,6 +122,7 @@ describe('Appointment Form', () => { POM.popup.setInputValue('subjectEditor', 'New Subject'); POM.popup.recurrenceSettingsButton.click(); POM.popup.saveButton.click(); + await Promise.resolve(); expect(dataSource.items()[0]).toMatchObject({ ...recurringAppointment, @@ -156,6 +158,7 @@ describe('Appointment Form', () => { scheduler.showAppointmentPopup(item); POM.popup.selectRepeatValue('daily'); POM.popup.saveButton.click(); + await Promise.resolve(); expect(dataSource.items()[0]).toMatchObject({ ...commonAppointment, @@ -190,6 +193,7 @@ describe('Appointment Form', () => { scheduler.showAppointmentPopup(item); POM.popup.editSeriesButton.click(); POM.popup.saveButton.click(); + await Promise.resolve(); expect(dataSource.items()[0]).toMatchObject(recurringAppointment); }); @@ -207,6 +211,7 @@ describe('Appointment Form', () => { POM.popup.editSeriesButton.click(); POM.popup.selectRepeatValue('never'); POM.popup.saveButton.click(); + await Promise.resolve(); expect(dataSource.items()[0]).toMatchObject({ ...recurringAppointment, @@ -297,6 +302,7 @@ describe('Appointment Form', () => { scheduler.showAppointmentPopup(item); POM.popup.setInputValue('roomId', 2); POM.popup.saveButton.click(); + await Promise.resolve(); expect(dataSource.items()[0].roomId).toBe(2); }); @@ -344,6 +350,7 @@ describe('Appointment Form', () => { POM.popup.setInputValue(editorName, null); POM.popup.saveButton.click(); + await Promise.resolve(); expect(POM.isPopupVisible()).toBe(true); }); @@ -358,6 +365,7 @@ describe('Appointment Form', () => { POM.popup.setInputValue(editorName, null); POM.popup.selectRepeatValue('daily'); POM.popup.saveButton.click(); + await Promise.resolve(); expect(POM.isPopupVisible()).toBe(true); }); @@ -373,6 +381,7 @@ describe('Appointment Form', () => { expect(POM.popup.getInputValue('recurrenceStartDateEditor')).toBe('5/9/2017'); POM.popup.saveButton.click(); + await Promise.resolve(); expect(POM.isPopupVisible()).toBe(false); }); @@ -1888,6 +1897,7 @@ describe('Appointment Form', () => { scheduler.showAppointmentPopup({ ...commonAppointment }, true); POM.popup.saveButton.click(); + await Promise.resolve(); expect(addAppointmentSpy).toHaveBeenCalledTimes(1); expect(addAppointmentSpy).toHaveBeenCalledWith( @@ -1903,6 +1913,7 @@ describe('Appointment Form', () => { scheduler.showAppointmentPopup({ ...commonAppointment }, true); POM.popup.saveButton.click(); + await Promise.resolve(); const dataSource = (scheduler as any).getDataSource(); expect(dataSource.items().length).toBe(0); @@ -1916,6 +1927,7 @@ describe('Appointment Form', () => { scheduler.showAppointmentPopup({ ...commonAppointment }, true); POM.popup.saveButton.click(); + await Promise.resolve(); const dataSource = (scheduler as any).getDataSource(); expect(dataSource.items().length).toBe(1); @@ -1936,6 +1948,7 @@ describe('Appointment Form', () => { scheduler.showAppointmentPopup(updatedItem); POM.popup.setInputValue('subjectEditor', 'Updated Subject'); POM.popup.saveButton.click(); + await Promise.resolve(); expect(updateAppointmentSpy).toHaveBeenCalledTimes(1); expect(updateAppointmentSpy).toHaveBeenCalledWith(updatedItem, updatedItem); @@ -1953,6 +1966,7 @@ describe('Appointment Form', () => { scheduler.showAppointmentPopup(updatedItem); POM.popup.setInputValue('subjectEditor', 'Updated Subject'); POM.popup.saveButton.click(); + await Promise.resolve(); expect(dataSource.items()[0]).toEqual(commonAppointment); }); @@ -1969,6 +1983,7 @@ describe('Appointment Form', () => { scheduler.showAppointmentPopup(updatedItem); POM.popup.setInputValue('subjectEditor', 'New Subject'); POM.popup.saveButton.click(); + await Promise.resolve(); expect(dataSource.items()[0]).toEqual({ allDay: false, diff --git a/packages/devextreme/js/__internal/scheduler/appointment_popup/appointment_popup.test.ts b/packages/devextreme/js/__internal/scheduler/appointment_popup/appointment_popup.test.ts index b945eab02b5a..8b9f4bfd9798 100644 --- a/packages/devextreme/js/__internal/scheduler/appointment_popup/appointment_popup.test.ts +++ b/packages/devextreme/js/__internal/scheduler/appointment_popup/appointment_popup.test.ts @@ -54,6 +54,7 @@ describe('Isolated AppointmentPopup environment', () => { }); POM.saveButton.click(); + await new Promise(process.nextTick); expect(callbacks.onSave).toHaveBeenCalledTimes(1); expect(callbacks.onSave).toHaveBeenCalledWith( @@ -75,6 +76,7 @@ describe('Isolated AppointmentPopup environment', () => { }); POM.saveButton.click(); + await new Promise(process.nextTick); expect(callbacks.addAppointment).not.toHaveBeenCalled(); expect(callbacks.updateAppointment).not.toHaveBeenCalled(); @@ -139,6 +141,7 @@ describe('Isolated AppointmentPopup environment', () => { }); POM.saveButton.click(); + await new Promise(process.nextTick); expect(onSave).toHaveBeenCalledTimes(1); expect(updateAppointment).toHaveBeenCalledWith(sourceAppointment, updatedAppointment); diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets.scheduler/appointment.scroll.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets.scheduler/appointment.scroll.tests.js index ce15cfcd6b57..335ffb11016f 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets.scheduler/appointment.scroll.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets.scheduler/appointment.scroll.tests.js @@ -80,6 +80,7 @@ module('Integration: Appointment scroll', { try { scheduler.instance.showAppointmentPopup(appointment); $('.dx-scheduler-appointment-popup .dx-popup-done').trigger('dxclick'); + await Promise.resolve(); assert.ok(scrollToSpy.calledOnce, 'scrollTo was called'); } finally { @@ -105,6 +106,7 @@ module('Integration: Appointment scroll', { try { scheduler.instance.showAppointmentPopup(appointment); $('.dx-scheduler-appointment-popup .dx-popup-done').trigger('dxclick'); + await Promise.resolve(); assert.ok(scrollToSpy.calledOnce, 'scrollTo was called'); } finally { @@ -131,6 +133,7 @@ module('Integration: Appointment scroll', { try { scheduler.instance.showAppointmentPopup(appointment); $('.dx-scheduler-appointment-popup .dx-popup-done').trigger('dxclick'); + await Promise.resolve(); assert.notOk(scrollTo.calledOnce, 'scrollTo was not called'); } finally { @@ -156,6 +159,7 @@ module('Integration: Appointment scroll', { try { scheduler.instance.showAppointmentPopup(appointment); $('.dx-scheduler-appointment-popup .dx-popup-done').trigger('dxclick'); + await Promise.resolve(); assert.ok(scrollToSpy.calledOnce, 'scrollTo was called'); } finally { @@ -182,6 +186,7 @@ module('Integration: Appointment scroll', { try { scheduler.instance.showAppointmentPopup(appointment); $('.dx-scheduler-appointment-popup .dx-popup-done').trigger('dxclick'); + await Promise.resolve(); assert.ok(scrollToSpy.calledOnce, 'scrollTo was called'); } finally { @@ -211,6 +216,7 @@ module('Integration: Appointment scroll', { try { scheduler.instance.showAppointmentPopup(appointment); $('.dx-scheduler-appointment-popup .dx-popup-done').trigger('dxclick'); + await Promise.resolve(); assert.ok(scrollToSpy.calledOnce, 'scrollTo was called'); } finally { @@ -239,6 +245,7 @@ module('Integration: Appointment scroll', { try { scheduler.instance.showAppointmentPopup(appointment); $('.dx-scheduler-appointment-popup .dx-popup-done').trigger('dxclick'); + await Promise.resolve(); assert.ok(scrollToSpy.calledOnce, 'scrollTo was called'); } finally { diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets.scheduler/timezones.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets.scheduler/timezones.tests.js index d5d612a56b18..cb0b322f14f5 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets.scheduler/timezones.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets.scheduler/timezones.tests.js @@ -265,6 +265,7 @@ module('API', moduleConfig, () => { scheduler.instance.showAppointmentPopup(appointment, true); $('.dx-scheduler-appointment-popup .dx-popup-done').trigger('dxclick'); + await Promise.resolve(); const $appointment = scheduler.instance.$element().find(CLASSES.appointment); const startDate = $appointment.dxSchedulerAppointment('instance').option('startDate'); @@ -294,6 +295,7 @@ module('API', moduleConfig, () => { scheduler.instance.showAppointmentPopup(updatedItem); $('.dx-scheduler-appointment-popup .dx-popup-done').trigger('dxclick'); + await Promise.resolve(); hide(); @@ -1431,6 +1433,7 @@ module('Scheduler grid', moduleConfigWithClock, () => { scheduler.instance.showAppointmentPopup(appointment); $('.dx-scheduler-appointment-popup .dx-popup-done').trigger('dxclick'); + await Promise.resolve(); assert.notOk(scrollToSpy.calledOnce, 'scrollTo was not called'); } finally { @@ -1460,6 +1463,7 @@ module('Scheduler grid', moduleConfigWithClock, () => { try { scheduler.instance.showAppointmentPopup(appointment); $('.dx-scheduler-appointment-popup .dx-popup-done').trigger('dxclick'); + await Promise.resolve(); assert.ok(scrollToSpy.calledOnce, 'scrollTo was called'); } finally { @@ -1723,6 +1727,7 @@ module('Appointment popup', moduleConfig, () => { scheduler.instance.showAppointmentPopup(updatedItem); $('.dx-scheduler-appointment-popup .dx-popup-done').trigger('dxclick'); + await Promise.resolve(); assert.ok(updateAppointment.calledOnce, 'Update method is called'); assert.deepEqual(updateAppointment.getCall(0).args[0], updatedItem, 'Target item is correct'); @@ -1984,6 +1989,7 @@ module('Fixed client time zone offset', () => { const initialPosition = $appointment.position(); $('.dx-scheduler-appointment-popup .dx-popup-done').trigger('dxclick'); + await Promise.resolve(); const updatedPosition = $(scheduler.getElement()).find(CLASSES.appointment).not('.dx-scheduler-appointment-recurrence').position(); assert.equal(updatedPosition.top, initialPosition.top, 'Top is updated correctly'); From 531f0e197b7157c77584b9bef585b21065d12bc8 Mon Sep 17 00:00:00 2001 From: Maksim Zakharov Date: Thu, 14 May 2026 19:08:25 +0400 Subject: [PATCH 4/4] test(scheduler): yet another await Promise.resolve() after popup save --- .../appointment_popup.integration.test.ts | 10 ++++++++++ .../allDayAppointments.common-0.tests.js | 3 +++ .../integration.timeline.tests.js | 5 +++++ .../perfomance.tests.js | 1 + 4 files changed, 19 insertions(+) diff --git a/packages/devextreme/js/__internal/scheduler/appointment_popup/appointment_popup.integration.test.ts b/packages/devextreme/js/__internal/scheduler/appointment_popup/appointment_popup.integration.test.ts index a3843ab104e2..45765cc758c7 100644 --- a/packages/devextreme/js/__internal/scheduler/appointment_popup/appointment_popup.integration.test.ts +++ b/packages/devextreme/js/__internal/scheduler/appointment_popup/appointment_popup.integration.test.ts @@ -236,6 +236,7 @@ describe('Appointment Form', () => { scheduler.showAppointmentPopup(appointment); POM.popup.setInputValue('subjectEditor', 'Updated subject'); scheduler.hideAppointmentPopup(true); + await Promise.resolve(); const items = (scheduler as any).getDataSource().items(); @@ -266,6 +267,7 @@ describe('Appointment Form', () => { POM.popup.setInputValue('endTimeEditor', new Date(2017, 4, 25, 10, 0)); POM.popup.setInputValue('descriptionEditor', 'New appointment description'); scheduler.hideAppointmentPopup(true); + await Promise.resolve(); const items = (scheduler as any).getDataSource().items(); @@ -325,6 +327,7 @@ describe('Appointment Form', () => { POM.popup.editAppointmentButton.click(); POM.popup.setInputValue('subjectEditor', 'single appointment'); scheduler.hideAppointmentPopup(true); + await Promise.resolve(); expect(dataSource.items()).toHaveLength(2); expect(dataSource.items()[0]).toEqual({ @@ -486,6 +489,7 @@ describe('Appointment Form', () => { scheduler.showAppointmentPopup({ ...commonAppointment }, true); scheduler.hideAppointmentPopup(true); + await Promise.resolve(); const item = dataSource.items()[0]; @@ -674,6 +678,7 @@ describe('Appointment Form', () => { POM.popup.setInputValue(editorName, value); scheduler.hideAppointmentPopup(true); + await Promise.resolve(); const customFieldValue = scheduler.option(`dataSource[0].${exprValue}`); const defaultFieldValue = scheduler.option(`dataSource[0].${defaultField}`); @@ -704,6 +709,7 @@ describe('Appointment Form', () => { POM.popup.setInputValue(dateEditorName, value); POM.popup.setInputValue(timeEditorName, value); scheduler.hideAppointmentPopup(true); + await Promise.resolve(); const customFieldValue = scheduler.option(`dataSource[0].${exprValue}`); const defaultFieldValue = scheduler.option(`dataSource[0].${defaultField}`); @@ -728,6 +734,7 @@ describe('Appointment Form', () => { scheduler.showAppointmentPopup(); POM.popup.selectRepeatValue('daily'); scheduler.hideAppointmentPopup(true); + await Promise.resolve(); const customFieldValue = scheduler.option(`dataSource[0].${exprValue}`); const defaultFieldValue = scheduler.option(`dataSource[0].${defaultField}`); @@ -760,6 +767,7 @@ describe('Appointment Form', () => { POM.popup.setInputValue(exprValue, 2); scheduler.hideAppointmentPopup(true); + await Promise.resolve(); const customFieldValue = scheduler.option(`dataSource[0].${exprValue}`); const defaultFieldValue = scheduler.option(`dataSource[0].${defaultField}`); @@ -2100,6 +2108,7 @@ describe('Appointment Form', () => { scheduler.showAppointmentPopup(item); POM.popup.setInputValue('subjectEditor', 'New Subject'); scheduler.hideAppointmentPopup(true); + await Promise.resolve(); expect(dataSource.items()[0]).toMatchObject({ ...commonAppointment, text: 'New Subject' }); }); @@ -2116,6 +2125,7 @@ describe('Appointment Form', () => { POM.popup.selectRepeatValue('weekly'); POM.popup.setInputValue('recurrenceStartDateEditor', new Date(2024, 4, 25)); scheduler.hideAppointmentPopup(true); + await Promise.resolve(); expect(dataSource.items()[0]).toMatchObject({ ...commonAppointment, diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets.scheduler/allDayAppointments.common-0.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets.scheduler/allDayAppointments.common-0.tests.js index bf8a2bcb8ba8..dfc9752bef90 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets.scheduler/allDayAppointments.common-0.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets.scheduler/allDayAppointments.common-0.tests.js @@ -568,6 +568,7 @@ module('All day appointments common', config, () => { scheduler.instance.showAppointmentPopup(newItem, true); $('.dx-scheduler-appointment-popup .dx-popup-done').trigger('dxclick'); + await Promise.resolve(); const $addedAppointment = $(scheduler.instance.$element()).find('.dx-scheduler-all-day-appointment').eq(0); const $allDayCell = $(scheduler.instance.$element()).find('.dx-scheduler-all-day-table-cell').eq(0); @@ -585,6 +586,7 @@ module('All day appointments common', config, () => { scheduler.instance.showAppointmentPopup(newItem); $('.dx-scheduler-appointment-popup .dx-popup-done').trigger('dxclick'); + await Promise.resolve(); const workspace = $(scheduler.instance.$element()).find('.dx-scheduler-work-space').dxSchedulerWorkSpaceDay('instance'); @@ -613,6 +615,7 @@ module('All day appointments common', config, () => { scheduler.instance.showAppointmentPopup(newItem, true); $('.dx-scheduler-appointment-popup .dx-popup-done').trigger('dxclick'); + await Promise.resolve(); assert.notOk($workspace.hasClass('dx-scheduler-work-space-all-day-collapsed'), 'Work space has not specific class'); }); diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets.scheduler/integration.timeline.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets.scheduler/integration.timeline.tests.js index af442ecb8401..197ffae1696d 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets.scheduler/integration.timeline.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets.scheduler/integration.timeline.tests.js @@ -101,6 +101,7 @@ QUnit.test('Scheduler should not update scroll position if appointment is visibl try { scheduler.instance.showAppointmentPopup(appointment); scheduler.appointmentPopup.clickDoneButton(); + await Promise.resolve(); assert.notOk(scrollToSpy.calledOnce, 'scrollTo was not called'); } finally { @@ -132,6 +133,7 @@ QUnit.test('Scheduler should not update scroll position if appointment is visibl try { scheduler.instance.showAppointmentPopup(appointment); scheduler.appointmentPopup.clickDoneButton(); + await Promise.resolve(); assert.notOk(scrollToSpy.calledOnce, 'scrollTo was not called'); } finally { @@ -162,6 +164,7 @@ QUnit.test('Scheduler should update scroll position if appointment is not visibl try { scheduler.instance.showAppointmentPopup(appointment); scheduler.appointmentPopup.clickDoneButton(); + await Promise.resolve(); assert.ok(scrollToSpy.calledOnce, 'scrollTo was called'); } finally { @@ -206,6 +209,7 @@ QUnit.test('Scheduler should not update scroll position if appointment is visibl try { scheduler.instance.showAppointmentPopup(appointment); scheduler.appointmentPopup.clickDoneButton(); + await Promise.resolve(); assert.notOk(scrollToSpy.calledOnce, 'scrollTo was not called'); } finally { @@ -236,6 +240,7 @@ QUnit.test('Scheduler should update scroll position if appointment is not visibl try { scheduler.instance.showAppointmentPopup(appointment); scheduler.appointmentPopup.clickDoneButton(); + await Promise.resolve(); assert.ok(scrollToSpy.calledOnce, 'scrollTo was called'); } finally { diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets.scheduler/perfomance.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets.scheduler/perfomance.tests.js index ae7d1d9b3c23..70750249c40c 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets.scheduler/perfomance.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets.scheduler/perfomance.tests.js @@ -336,6 +336,7 @@ QUnit.module('Render layout', renderLayoutModuleOptions, function() { scheduler.tooltip.clickOnItem(); scheduler.appointmentForm.setSubject('new text'); scheduler.appointmentPopup.clickDoneButton(); + await Promise.resolve(); assert.equal(scheduler.appointments.getAppointmentCount(), 2, 'Should render 2 appointments'); assert.equal(scheduler.appointments.getAppointmentCount(), getUnmarkedAppointments(scheduler).length, 'Should re-rendered all appointments');