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 d2b376b01c8e..d2b7fb7f3bd6 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 @@ -4,8 +4,7 @@ import { import dateLocalization from '@js/common/core/localization/date'; import { CustomStore } from '@js/common/data/custom_store'; import $ from '@js/core/renderer'; -import { loadMessages, locale } from '@js/localization'; -import type { GroupItem, Item as FormItem, SimpleItem } from '@js/ui/form'; +import type { GroupItem } from '@js/ui/form'; import type { ToolbarItem } from '@js/ui/popup'; import { toMilliseconds } from '@ts/utils/toMilliseconds'; @@ -344,36 +343,7 @@ describe('Appointment Form', () => { }); describe('Validation', () => { - it.each([ - 'startDateEditor', 'startTimeEditor', 'endDateEditor', 'endTimeEditor', - ])('should not close popup on save button click when %s is empty', async (editorName) => { - const { scheduler, POM } = await createScheduler(getDefaultConfig()); - - scheduler.showAppointmentPopup({ ...commonAppointment }); - - POM.popup.setInputValue(editorName, null); - POM.popup.saveButton.click(); - await Promise.resolve(); - - expect(POM.isPopupVisible()).toBe(true); - }); - - it.each([ - 'startTimeEditor', 'endDateEditor', 'endTimeEditor', - ])('should not close popup on save button click in recurrence form when %s editor is empty', async (editorName) => { - const { scheduler, POM } = await createScheduler(getDefaultConfig()); - - scheduler.showAppointmentPopup({ ...commonAppointment }); - - POM.popup.setInputValue(editorName, null); - POM.popup.selectRepeatValue('daily'); - POM.popup.saveButton.click(); - await Promise.resolve(); - - expect(POM.isPopupVisible()).toBe(true); - }); - - it('should close popup on save button click in recurrence form when startEditor editor is empty', async () => { + it('should close popup on save when startDateEditor is empty in recurrence form', async () => { const { scheduler, POM } = await createScheduler(getDefaultConfig()); scheduler.showAppointmentPopup({ ...commonAppointment }); @@ -1034,1757 +1004,814 @@ describe('Appointment Form', () => { .toBe((scheduler as any).resourceManager); }); }); - - describe('Recurrence Form', () => { - it('should allow opening recurrence settings when allowUpdating is false', async () => { - const appointment = { - text: 'Recurrent Appointment', - startDate: new Date(2017, 4, 1, 9, 30), - endDate: new Date(2017, 4, 1, 11), - recurrenceRule: 'FREQ=WEEKLY;BYDAY=MO,WE,FR;COUNT=10', - }; - - const { POM, scheduler } = await createScheduler({ - ...getDefaultConfig(), - editing: { allowUpdating: false }, - }); - - scheduler.showAppointmentPopup(appointment); - - expect(POM.popup.isRecurrenceGroupVisible()).toBe(false); - - POM.popup.recurrenceSettingsButton.click(); - - expect(POM.popup.isRecurrenceGroupVisible()).toBe(true); + describe('firstDayOfWeek', () => { + beforeEach(() => { + jest.spyOn(dateLocalization, 'firstDayOfWeekIndex').mockReturnValue(3); }); - it('should close repeat selectbox popup when navigating to recurrence group via settings button', async () => { - const { POM, scheduler } = await createScheduler({ - ...getDefaultConfig(), - dataSource: [{ ...recurringAppointment }], - }); - - const dataSource = (scheduler as any).getDataSource(); - const appointment = dataSource.items()[0]; - - scheduler.showAppointmentPopup(appointment); - POM.popup.editSeriesButton.click(); - - const repeatEditor = POM.popup.dxForm.getEditor('repeatEditor'); - POM.popup.getInput('repeatEditor').click(); - - expect(repeatEditor?.option('opened')).toBe(true); - - POM.popup.recurrenceSettingsButton.click(); - - expect(repeatEditor?.option('opened')).toBe(false); + afterEach(() => { + jest.restoreAllMocks(); }); - it('should have disabled week day buttons when allowUpdating is false', async () => { + it('should pass value from localization firstDayOfWeek to calendars when option is not set', async () => { const { POM, scheduler } = await createScheduler({ ...getDefaultConfig(), - dataSource: [{ ...recurringAppointment, recurrenceRule: 'FREQ=WEEKLY;BYDAY=WE,TU,TH,FR,SA' }], - editing: { allowUpdating: false }, + firstDayOfWeek: undefined, }); - const dataSource = (scheduler as any).getDataSource(); - const appointment = dataSource.items()[0]; - - scheduler.showAppointmentPopup(appointment); - POM.popup.recurrenceSettingsButton.click(); - - const weekDayButtons = $(POM.popup.recurrenceWeekDayButtons); - const disabledButtons = weekDayButtons?.find('.dx-button.dx-state-disabled'); - - expect(disabledButtons.length).toBe(7); - }); - - it('should be visible after changing repeat editor\'s value', async () => { - const { scheduler, POM } = await createScheduler(getDefaultConfig()); - - scheduler.showAppointmentPopup(); - - expect(POM.popup.isMainGroupVisible()).toBe(true); - expect(POM.popup.mainGroup.getAttribute('inert')).toBeNull(); - expect(POM.popup.isRecurrenceGroupVisible()).toBe(false); - expect(POM.popup.recurrenceGroup.getAttribute('inert')).toBe('true'); - - POM.popup.selectRepeatValue('weekly'); - - const popupHeight = POM.popup.component.option('height'); - expect(popupHeight).toBeDefined(); - expect(typeof popupHeight).toBe('number'); - - expect(POM.popup.isMainGroupVisible()).toBe(false); - expect(POM.popup.mainGroup.getAttribute('inert')).toBe('true'); - expect(POM.popup.isRecurrenceGroupVisible()).toBe(true); - expect(POM.popup.recurrenceGroup.getAttribute('inert')).toBeNull(); - - POM.popup.backButton.click(); - - expect(POM.popup.component.option('height')).toBe('auto'); - expect(POM.popup.isMainGroupVisible()).toBe(true); - expect(POM.popup.mainGroup.getAttribute('inert')).toBeNull(); - expect(POM.popup.isRecurrenceGroupVisible()).toBe(false); - expect(POM.popup.recurrenceGroup.getAttribute('inert')).toBe('true'); - }); - - it('should open main form when opening recurring appointment', async () => { - const appointment = { - text: 'Recurrent Appointment', - startDate: new Date(2017, 4, 1, 9, 30), - endDate: new Date(2017, 4, 1, 11), - recurrenceRule: 'FREQ=WEEKLY;BYDAY=MO,WE,FR;COUNT=10', - }; - - const { POM, scheduler } = await createScheduler(getDefaultConfig()); - - scheduler.showAppointmentPopup(appointment); - - POM.popup.editSeriesButton.click(); + scheduler.showAppointmentPopup(commonAppointment); - expect(POM.popup.isMainGroupVisible()).toBe(true); - expect(POM.popup.isRecurrenceGroupVisible()).toBe(false); + const startDateEditor = POM.popup.dxForm.getEditor('startDateEditor'); + expect(startDateEditor).toBeDefined(); + expect(startDateEditor?.option('calendarOptions.firstDayOfWeek')).toBe(3); }); + }); - describe('State', () => { - it('should have correct input values for appointment with hour frequency', async () => { + describe('Icons', () => { + describe('Subject icon', () => { + it('has default color when appointment has no resources', async () => { const { scheduler, POM } = await createScheduler(getDefaultConfig()); - scheduler.showAppointmentPopup({ - text: 'Meeting', - startDate: new Date(2017, 4, 1, 10, 30), - endDate: new Date(2017, 4, 1, 11), - recurrenceRule: 'FREQ=HOURLY;INTERVAL=2;COUNT=10', - repeatEnd: 'count', - }); - POM.popup.editSeriesButton.click(); - - expect(POM.popup.getInputValue('repeatEditor')).toBe('Hourly'); - - POM.popup.recurrenceSettingsButton.click(); + scheduler.showAppointmentPopup(commonAppointment); - expect(POM.popup.getInputValue('recurrenceStartDateEditor')).toBe('5/1/2017'); - expect(POM.popup.getInputValue('recurrenceCountEditor')).toBe('2'); - expect(POM.popup.getInputValue('recurrencePeriodEditor')).toBe('Hour(s)'); - expect(POM.popup.getInputValue('recurrenceEndCountEditor')).toBe('10 occurrence(s)'); + const $icon = $(POM.popup.subjectIcon); + expect($icon.css('color')).toBe(''); }); - it('should have correct input values for appointment with daily frequency', async () => { + it('has default color when showAppointmentPopup is called without data', async () => { const { scheduler, POM } = await createScheduler(getDefaultConfig()); - scheduler.showAppointmentPopup({ - text: 'Meeting', - startDate: new Date(2017, 4, 1, 10, 30), - endDate: new Date(2017, 4, 1, 11), - recurrenceRule: 'FREQ=DAILY;INTERVAL=2;COUNT=10', - repeatEnd: 'count', - }); - POM.popup.editSeriesButton.click(); - - expect(POM.popup.getInputValue('repeatEditor')).toBe('Daily'); - - POM.popup.recurrenceSettingsButton.click(); + scheduler.showAppointmentPopup(); - expect(POM.popup.getInputValue('recurrenceStartDateEditor')).toBe('5/1/2017'); - expect(POM.popup.getInputValue('recurrenceCountEditor')).toBe('2'); - expect(POM.popup.getInputValue('recurrencePeriodEditor')).toBe('Day(s)'); - expect(POM.popup.getInputValue('recurrenceEndCountEditor')).toBe('10 occurrence(s)'); + const $icon = $(POM.popup.subjectIcon); + expect($icon.css('color')).toBe(''); }); - it('should have correct input values for appointment with week frequency', async () => { - const { scheduler, POM } = await createScheduler(getDefaultConfig()); + it('has resource color when appointment has resource', async () => { + const resourceColor1 = 'rgb(255, 0, 0)'; + const resourceColor2 = 'rgb(0, 0, 255)'; + const { scheduler, POM } = await createScheduler({ + ...getDefaultConfig(), + resources: [{ + fieldExpr: 'roomId', + dataSource: [ + { id: 1, text: 'Room 1', color: resourceColor1 }, + { id: 2, text: 'Room 2', color: resourceColor2 }, + ], + }], + }); scheduler.showAppointmentPopup({ - text: 'Meeting', - startDate: new Date(2017, 4, 1, 10, 30), - endDate: new Date(2017, 4, 1, 11), - recurrenceRule: 'FREQ=WEEKLY;INTERVAL=2;BYDAY=MO,WE,FR;COUNT=10', - repeatEnd: 'count', + ...commonAppointment, + roomId: 1, }); - POM.popup.editSeriesButton.click(); - - expect(POM.popup.getInputValue('repeatEditor')).toBe('Weekly'); - - POM.popup.recurrenceSettingsButton.click(); + await new Promise(process.nextTick); - expect(POM.popup.getInputValue('recurrenceStartDateEditor')).toBe('5/1/2017'); - expect(POM.popup.getInputValue('recurrenceCountEditor')).toBe('2'); - expect(POM.popup.getInputValue('recurrencePeriodEditor')).toBe('Week(s)'); + const $icon = $(POM.popup.subjectIcon); + expect($icon.css('color')).toBe(resourceColor1); - const expectedWeekDaysSelection = [true, false, true, false, true, false, false]; - expect(POM.popup.getWeekDaysSelection()).toEqual(expectedWeekDaysSelection); + POM.popup.setInputValue('roomId', 2); + await new Promise(process.nextTick); - expect(POM.popup.getInputValue('recurrenceEndCountEditor')).toBe('10 occurrence(s)'); + expect($icon.css('color')).toBe(resourceColor2); }); + }); - it('should have correct input values for appointment with monthly frequency', async () => { - const { scheduler, POM } = await createScheduler(getDefaultConfig()); - - scheduler.showAppointmentPopup({ - text: 'Meeting', - startDate: new Date(2017, 4, 1, 10, 30), - endDate: new Date(2017, 4, 1, 11), - recurrenceRule: 'FREQ=MONTHLY;INTERVAL=2;BYMONTHDAY=1;COUNT=10', - repeatEnd: 'count', + describe('Resource icons', () => { + it.each<{ + iconsShowMode: 'both' | 'main' | 'none' | 'recurrence'; + visibleMain: boolean; + visibleRecurrence: boolean; + }>([ + { iconsShowMode: 'both', visibleMain: true, visibleRecurrence: true }, + { iconsShowMode: 'main', visibleMain: true, visibleRecurrence: false }, + { iconsShowMode: 'recurrence', visibleMain: false, visibleRecurrence: true }, + { iconsShowMode: 'none', visibleMain: false, visibleRecurrence: false }, + ])('should shown icons correctly when iconsShowMode is \'$iconsShowMode\'', async ({ iconsShowMode, visibleMain, visibleRecurrence }) => { + const { scheduler, POM } = await createScheduler({ + ...getDefaultConfig(), + editing: { form: { iconsShowMode } }, }); - POM.popup.editSeriesButton.click(); - expect(POM.popup.getInputValue('repeatEditor')).toBe('Monthly'); + scheduler.showAppointmentPopup(commonAppointment); - POM.popup.recurrenceSettingsButton.click(); + const mainFormIcons = POM.popup.mainGroup.querySelectorAll(`.${CLASSES.icon}`); + const recurrenceFormIcons = POM.popup.recurrenceGroup.querySelectorAll(`.${CLASSES.icon}`); - expect(POM.popup.getInputValue('recurrenceStartDateEditor')).toBe('5/1/2017'); - expect(POM.popup.getInputValue('recurrenceCountEditor')).toBe('2'); - expect(POM.popup.getInputValue('recurrencePeriodEditor')).toBe('Month(s)'); - expect(POM.popup.getInputValue('recurrenceDayOfMonthEditor')).toBe('1'); - expect(POM.popup.getInputValue('recurrenceEndCountEditor')).toBe('10 occurrence(s)'); + expect(mainFormIcons.length).toBe(visibleMain ? 4 : 0); + expect(recurrenceFormIcons.length).toBe(visibleRecurrence ? 3 : 0); }); + }); + }); - it('should have correct input values for appointment with yearly frequency', async () => { - const { scheduler, POM } = await createScheduler(getDefaultConfig()); - - scheduler.showAppointmentPopup({ - text: 'Meeting', - startDate: new Date(2017, 4, 1, 10, 30), - endDate: new Date(2017, 4, 1, 11), - recurrenceRule: 'FREQ=YEARLY;INTERVAL=2;BYMONTHDAY=1;BYMONTH=5;COUNT=10', - repeatEnd: 'count', + describe('Callbacks', () => { + describe('OnAppointmentFormOpening', () => { + it('should be called when showing appointment popup', async () => { + const onAppointmentFormOpening = jest.fn(); + const { scheduler } = await createScheduler({ + ...getDefaultConfig(), + onAppointmentFormOpening, }); - POM.popup.editSeriesButton.click(); - expect(POM.popup.getInputValue('repeatEditor')).toBe('Yearly'); + scheduler.showAppointmentPopup(commonAppointment); - POM.popup.recurrenceSettingsButton.click(); + const arg = onAppointmentFormOpening.mock.calls[0][0] as any; - expect(POM.popup.getInputValue('recurrenceStartDateEditor')).toBe('5/1/2017'); - expect(POM.popup.getInputValue('recurrenceCountEditor')).toBe('2'); - expect(POM.popup.getInputValue('recurrencePeriodEditor')).toBe('Year(s)'); - expect(POM.popup.getInputValue('recurrenceDayOfYearDayEditor')).toBe('1'); - expect(POM.popup.getInputValue('recurrenceDayOfYearMonthEditor')).toBe('May'); - expect(POM.popup.getInputValue('recurrenceEndCountEditor')).toBe('10 occurrence(s)'); + expect(onAppointmentFormOpening).toHaveBeenCalledTimes(1); + expect(arg).toHaveProperty('popup'); + expect(arg).toHaveProperty('form'); + expect(arg.appointmentData).toEqual( + expect.objectContaining({ ...commonAppointment }), + ); }); - it('T1325870: should use current locale for recurrence editors after locale change', async () => { - const currentLocale = locale(); - - loadMessages({ - de: { - 'dxScheduler-recurrenceYearly': 'custom yearly', - 'dxScheduler-recurrenceRepeatYearly': 'custom repeat yearly', - }, + it('should correctly handle e.cancel=true', async () => { + const { POM, scheduler } = await createScheduler({ + ...getDefaultConfig(), + dataSource: [{ ...commonAppointment }], + onAppointmentFormOpening: (e) => { e.cancel = true; }, }); - locale('de'); - try { - const { scheduler, POM } = await createScheduler(getDefaultConfig()); + scheduler.showAppointmentPopup(commonAppointment); - scheduler.showAppointmentPopup({ - text: 'Meeting', - startDate: new Date(2017, 4, 1, 10, 30), - endDate: new Date(2017, 4, 1, 11), - recurrenceRule: 'FREQ=YEARLY;INTERVAL=2;BYMONTHDAY=1;BYMONTH=5;COUNT=10', - repeatEnd: 'count', - }); - POM.popup.editSeriesButton.click(); + expect(POM.isPopupVisible()).toBe(false); + }); - expect(POM.popup.getInputValue('repeatEditor')).toBe('custom yearly'); + it('should handle e.cancel value: false', async () => { + const { POM, scheduler } = await createScheduler({ + ...getDefaultConfig(), + dataSource: [{ ...commonAppointment }], + onAppointmentFormOpening: (e) => { e.cancel = false; }, + }); - POM.popup.recurrenceSettingsButton.click(); + scheduler.showAppointmentPopup(commonAppointment); - expect(POM.popup.getInputValue('recurrencePeriodEditor')).toBe('Custom repeat yearly'); - expect(POM.popup.getInputValue('recurrenceDayOfYearMonthEditor')).toBe(dateLocalization.getMonthNames()[4]); - } finally { - locale(currentLocale); - } + expect(POM.isPopupVisible()).toBe(true); }); + }); - it('should have correct input values for appointment with no end', async () => { + describe('onAppointmentAdding', () => { + it('should be called when saving new appointment', async () => { const { scheduler, POM } = await createScheduler(getDefaultConfig()); - scheduler.showAppointmentPopup({ - text: 'Meeting', - startDate: new Date(2017, 4, 1, 10, 30), - endDate: new Date(2017, 4, 1, 11), - recurrenceRule: 'FREQ=DAILY;INTERVAL=2', - repeatEnd: 'never', - }); - POM.popup.editSeriesButton.click(); - POM.popup.recurrenceSettingsButton.click(); + const addAppointmentSpy = jest.spyOn(scheduler, 'addAppointment'); - expect(POM.popup.getInputValue('recurrenceRepeatEndEditor')).toBe('never'); - }); + scheduler.showAppointmentPopup({ ...commonAppointment }, true); + POM.popup.saveButton.click(); + await Promise.resolve(); - it('should have correct input values for appointment with end by date', async () => { - const { scheduler, POM } = await createScheduler(getDefaultConfig()); + expect(addAppointmentSpy).toHaveBeenCalledTimes(1); + expect(addAppointmentSpy).toHaveBeenCalledWith( + expect.objectContaining({ ...commonAppointment }), + ); + }); - scheduler.showAppointmentPopup({ - text: 'Meeting', - startDate: new Date(2017, 4, 1, 10, 30), - endDate: new Date(2017, 4, 1, 11), - recurrenceRule: 'FREQ=DAILY;INTERVAL=2;UNTIL=20170601T000000Z', - repeatEnd: 'until', + it('should correctly handle e.cancel=true', async () => { + const { scheduler, POM } = await createScheduler({ + ...getDefaultConfig(), + onAppointmentAdding: (e) => { e.cancel = true; }, }); - POM.popup.editSeriesButton.click(); - POM.popup.recurrenceSettingsButton.click(); - expect(POM.popup.getInputValue('recurrenceRepeatEndEditor')).toBe('until'); - expect(POM.popup.getInputValue('recurrenceEndUntilEditor')).toBe('6/1/2017'); - }); + scheduler.showAppointmentPopup({ ...commonAppointment }, true); + POM.popup.saveButton.click(); + await Promise.resolve(); - it('should have correct input values for appointment with end by count', async () => { - const { scheduler, POM } = await createScheduler(getDefaultConfig()); + const dataSource = (scheduler as any).getDataSource(); + expect(dataSource.items().length).toBe(0); + }); - scheduler.showAppointmentPopup({ - text: 'Meeting', - startDate: new Date(2017, 4, 1, 10, 30), - endDate: new Date(2017, 4, 1, 11), - recurrenceRule: 'FREQ=DAILY;INTERVAL=2;COUNT=10', - repeatEnd: 'count', + it('should correctly handle e.cancel=false', async () => { + const { scheduler, POM } = await createScheduler({ + ...getDefaultConfig(), + onAppointmentAdding: (e) => { e.cancel = false; }, }); - POM.popup.editSeriesButton.click(); - POM.popup.recurrenceSettingsButton.click(); - expect(POM.popup.getInputValue('recurrenceRepeatEndEditor')).toBe('count'); - expect(POM.popup.getInputValue('recurrenceEndCountEditor')).toBe('10 occurrence(s)'); + scheduler.showAppointmentPopup({ ...commonAppointment }, true); + POM.popup.saveButton.click(); + await Promise.resolve(); + + const dataSource = (scheduler as any).getDataSource(); + expect(dataSource.items().length).toBe(1); + expect(dataSource.items()[0]).toMatchObject(commonAppointment); }); }); - describe('Repeat End Values Preservation', () => { - it('should preserve count value when switching between recurrence types', async () => { - const { scheduler, POM } = await createScheduler(getDefaultConfig()); - const testCount = 15; - - scheduler.showAppointmentPopup({ - text: 'Meeting', - startDate: new Date(2017, 4, 1, 10, 30), - endDate: new Date(2017, 4, 1, 11), + describe('onAppointmentUpdating', () => { + it('should be called when saving appointment', async () => { + const { scheduler, POM } = await createScheduler({ + ...getDefaultConfig(), + dataSource: [{ ...commonAppointment }], }); + const updateAppointmentSpy = jest.spyOn(scheduler, 'updateAppointment'); + const dataSource = (scheduler as any).getDataSource(); + const updatedItem = dataSource.items()[0]; - POM.popup.selectRepeatValue('daily'); - - POM.popup.setInputValue('recurrenceRepeatEndEditor', 'count'); - POM.popup.setInputValue('recurrenceEndCountEditor', testCount); - - POM.popup.backButton.click(); - - POM.popup.selectRepeatValue('weekly'); - - POM.popup.recurrenceSettingsButton.click(); - - expect(POM.popup.getInputValue('recurrenceEndCountEditor')).toBe(`${testCount} occurrence(s)`); - - scheduler.hideAppointmentPopup(); - }); - - it('should preserve until value when switching between recurrence types', async () => { - const { scheduler, POM } = await createScheduler(getDefaultConfig()); - const testUntilDate = new Date(2017, 5, 16); - - scheduler.showAppointmentPopup({ - text: 'Meeting', - startDate: new Date(2017, 4, 1, 10, 30), - endDate: new Date(2017, 4, 1, 11), - }); - - POM.popup.selectRepeatValue('daily'); - - POM.popup.setInputValue('recurrenceRepeatEndEditor', 'until'); - POM.popup.setInputValue('recurrenceEndUntilEditor', testUntilDate); - - POM.popup.backButton.click(); - - POM.popup.selectRepeatValue('weekly'); - - POM.popup.recurrenceSettingsButton.click(); - - expect(POM.popup.getInputValue('recurrenceEndUntilEditor')).toBe('6/16/2017'); - - scheduler.hideAppointmentPopup(); - }); - }); - - describe('Repeat End Editors Disabled State', () => { - ['never', 'until', 'count'].forEach((repeatEndValue) => { - it(`should set correct disabled state when repeatEnd is ${repeatEndValue}`, async () => { - const { scheduler, POM } = await createScheduler(getDefaultConfig()); - let recurrenceRule = ''; - switch (repeatEndValue) { - case 'count': - recurrenceRule = 'FREQ=DAILY;COUNT=10'; - break; - case 'until': - recurrenceRule = 'FREQ=DAILY;UNTIL=20170615T000000Z'; - break; - default: - recurrenceRule = 'FREQ=DAILY'; - } - - scheduler.showAppointmentPopup({ - text: 'Meeting', - startDate: new Date(2017, 4, 1, 10, 30), - endDate: new Date(2017, 4, 1, 11), - recurrenceRule, - }); - - POM.popup.editSeriesButton.click(); - POM.popup.recurrenceSettingsButton.click(); - - const untilEditor = POM.popup.dxForm.getEditor('recurrenceEndUntilEditor'); - const countEditor = POM.popup.dxForm.getEditor('recurrenceEndCountEditor'); - - expect(untilEditor?.option('disabled')).toBe(repeatEndValue !== 'until'); - expect(countEditor?.option('disabled')).toBe(repeatEndValue !== 'count'); - }); - }); - }); - - describe('FrequencyEditor focus', () => { - it('should not be focused when value is changed via API', async () => { - const { POM, scheduler } = await createScheduler({ - ...getDefaultConfig(), - dataSource: [], - views: ['week'], - currentView: 'week', - currentDate: new Date(2021, 2, 25), - }); - - scheduler.showAppointmentPopup(recurringAppointment); - POM.popup.editSeriesButton.click(); - POM.popup.recurrenceSettingsButton.click(); - - const frequencyEditor = POM.popup.dxForm.getEditor('recurrencePeriodEditor'); - const frequencyEditorInputElement = POM.popup.getInput('recurrencePeriodEditor'); - - frequencyEditor?.option('value', 'yearly'); - - expect(document.activeElement).not.toBe(frequencyEditorInputElement); - }); - - it('should be focused when value is changed via keyboard', async () => { - const { POM, scheduler, keydown } = await createScheduler({ - ...getDefaultConfig(), - dataSource: [], - views: ['week'], - currentView: 'week', - currentDate: new Date(2021, 2, 25), - }); - - scheduler.showAppointmentPopup(recurringAppointment); - POM.popup.editSeriesButton.click(); - POM.popup.recurrenceSettingsButton.click(); - - const frequencyEditorInputElement = POM.popup.getInput('recurrencePeriodEditor'); - - frequencyEditorInputElement.click(); - jest.useFakeTimers(); - keydown(frequencyEditorInputElement, 'ArrowDown'); - jest.runAllTimers(); - - expect(document.activeElement).toBe(frequencyEditorInputElement); - }); - }); - - it('should set animation offset CSS variable when switching to recurrence form', async () => { - setupSchedulerTestEnvironment({ - height: 600, - classRects: { - 'dx-form': { top: 10 }, - 'dx-scheduler-form-main-group': { top: 60 }, - }, - }); - - const { scheduler, POM } = await createScheduler(getDefaultConfig()); - - scheduler.showAppointmentPopup(); - POM.popup.selectRepeatValue('weekly'); - - const animationTop = POM.popup.dxForm.$element()[0].style.getPropertyValue('--dx-scheduler-animation-top'); - expect(animationTop).toBe('50px'); - }); - }); - - it('recurrence editors with hidden outer label must have editorOptions.labelMode set to hidden (T1318550)', async () => { - const flattenBy = ( - items: T[], - getChildren: (item: T) => T[] | undefined, - ): T[] => items.flatMap((item) => { - const children = getChildren(item); - return children?.length ? flattenBy(children, getChildren) : [item]; - }); - - const { scheduler, POM } = await createScheduler({ - ...getDefaultConfig(), - dataSource: [{ ...recurringAppointment }], - }); - - scheduler.showAppointmentPopup(commonAppointment); - - const formItems = POM.popup.dxForm.option('items') ?? []; - const recurrenceGroup = formItems[1] as GroupItem; - const allItems = flattenBy( - recurrenceGroup.items as SimpleItem[], - (i) => (i as unknown as GroupItem).items as SimpleItem[] | undefined, - ); - - const missingLabelMode = allItems - .filter((i) => i.label?.visible === false && i.editorOptions) - .filter((i) => (i.editorOptions as Record).labelMode !== 'hidden'); - - expect(missingLabelMode.length).toEqual(0); - }); - - describe('firstDayOfWeek', () => { - beforeEach(() => { - jest.spyOn(dateLocalization, 'firstDayOfWeekIndex').mockReturnValue(3); - }); - - afterEach(() => { - jest.restoreAllMocks(); - }); - - it('should pass value from localization firstDayOfWeek to calendars when option is not set', async () => { - const { POM, scheduler } = await createScheduler({ - ...getDefaultConfig(), - firstDayOfWeek: undefined, - }); - - scheduler.showAppointmentPopup(commonAppointment); - - const startDateEditor = POM.popup.dxForm.getEditor('startDateEditor'); - expect(startDateEditor).toBeDefined(); - expect(startDateEditor?.option('calendarOptions.firstDayOfWeek')).toBe(3); - }); - }); - - describe('Icons', () => { - describe('Subject icon', () => { - it('has default color when appointment has no resources', async () => { - const { scheduler, POM } = await createScheduler(getDefaultConfig()); - - scheduler.showAppointmentPopup(commonAppointment); - - const $icon = $(POM.popup.subjectIcon); - expect($icon.css('color')).toBe(''); - }); - - it('has default color when showAppointmentPopup is called without data', async () => { - const { scheduler, POM } = await createScheduler(getDefaultConfig()); - - scheduler.showAppointmentPopup(); + scheduler.showAppointmentPopup(updatedItem); + POM.popup.setInputValue('subjectEditor', 'Updated Subject'); + POM.popup.saveButton.click(); + await Promise.resolve(); - const $icon = $(POM.popup.subjectIcon); - expect($icon.css('color')).toBe(''); + expect(updateAppointmentSpy).toHaveBeenCalledTimes(1); + expect(updateAppointmentSpy).toHaveBeenCalledWith(updatedItem, updatedItem); }); - it('has resource color when appointment has resource', async () => { - const resourceColor1 = 'rgb(255, 0, 0)'; - const resourceColor2 = 'rgb(0, 0, 255)'; + it('should correctly handle e.cancel=true (T907281)', async () => { const { scheduler, POM } = await createScheduler({ ...getDefaultConfig(), - resources: [{ - fieldExpr: 'roomId', - dataSource: [ - { id: 1, text: 'Room 1', color: resourceColor1 }, - { id: 2, text: 'Room 2', color: resourceColor2 }, - ], - }], - }); - - scheduler.showAppointmentPopup({ - ...commonAppointment, - roomId: 1, + dataSource: [{ ...commonAppointment }], + onAppointmentUpdating: (e) => { e.cancel = true; }, }); - await new Promise(process.nextTick); - - const $icon = $(POM.popup.subjectIcon); - expect($icon.css('color')).toBe(resourceColor1); + const dataSource = (scheduler as any).getDataSource(); + const updatedItem = dataSource.items()[0]; - POM.popup.setInputValue('roomId', 2); - await new Promise(process.nextTick); + scheduler.showAppointmentPopup(updatedItem); + POM.popup.setInputValue('subjectEditor', 'Updated Subject'); + POM.popup.saveButton.click(); + await Promise.resolve(); - expect($icon.css('color')).toBe(resourceColor2); + expect(dataSource.items()[0]).toEqual(commonAppointment); }); - }); - describe('Resource icons', () => { - it.each<{ - iconsShowMode: 'both' | 'main' | 'none' | 'recurrence'; - visibleMain: boolean; - visibleRecurrence: boolean; - }>([ - { iconsShowMode: 'both', visibleMain: true, visibleRecurrence: true }, - { iconsShowMode: 'main', visibleMain: true, visibleRecurrence: false }, - { iconsShowMode: 'recurrence', visibleMain: false, visibleRecurrence: true }, - { iconsShowMode: 'none', visibleMain: false, visibleRecurrence: false }, - ])('should shown icons correctly when iconsShowMode is \'$iconsShowMode\'', async ({ iconsShowMode, visibleMain, visibleRecurrence }) => { + it('should correctly handle e.cancel=false (T907281)', async () => { const { scheduler, POM } = await createScheduler({ ...getDefaultConfig(), - editing: { form: { iconsShowMode } }, + dataSource: [{ ...commonAppointment }], + onAppointmentUpdating: (e) => { e.cancel = false; }, }); + const dataSource = (scheduler as any).getDataSource(); + const updatedItem = dataSource.items()[0]; - scheduler.showAppointmentPopup(commonAppointment); - - const mainFormIcons = POM.popup.mainGroup.querySelectorAll(`.${CLASSES.icon}`); - const recurrenceFormIcons = POM.popup.recurrenceGroup.querySelectorAll(`.${CLASSES.icon}`); + scheduler.showAppointmentPopup(updatedItem); + POM.popup.setInputValue('subjectEditor', 'New Subject'); + POM.popup.saveButton.click(); + await Promise.resolve(); - expect(mainFormIcons.length).toBe(visibleMain ? 4 : 0); - expect(recurrenceFormIcons.length).toBe(visibleRecurrence ? 3 : 0); + expect(dataSource.items()[0]).toEqual({ + allDay: false, + recurrenceRule: '', + ...commonAppointment, + text: 'New Subject', + }); }); }); - }); - describe('Callbacks', () => { - describe('OnAppointmentFormOpening', () => { - it('should be called when showing appointment popup', async () => { - const onAppointmentFormOpening = jest.fn(); + describe('onAppointmentDeleting', () => { + it('should be called when deleting appointment', async () => { const { scheduler } = await createScheduler({ - ...getDefaultConfig(), - onAppointmentFormOpening, - }); - - scheduler.showAppointmentPopup(commonAppointment); - - const arg = onAppointmentFormOpening.mock.calls[0][0] as any; - - expect(onAppointmentFormOpening).toHaveBeenCalledTimes(1); - expect(arg).toHaveProperty('popup'); - expect(arg).toHaveProperty('form'); - expect(arg.appointmentData).toEqual( - expect.objectContaining({ ...commonAppointment }), - ); - }); - - it('should correctly handle e.cancel=true', async () => { - const { POM, scheduler } = await createScheduler({ ...getDefaultConfig(), dataSource: [{ ...commonAppointment }], - onAppointmentFormOpening: (e) => { e.cancel = true; }, }); + const deleteAppointmentSpy = jest.spyOn(scheduler, 'deleteAppointment'); + const dataSource = (scheduler as any).getDataSource(); + const dataItem = dataSource.items()[0]; - scheduler.showAppointmentPopup(commonAppointment); + scheduler.deleteAppointment(dataItem); - expect(POM.isPopupVisible()).toBe(false); + expect(deleteAppointmentSpy).toHaveBeenCalledTimes(1); + expect(deleteAppointmentSpy).toHaveBeenCalledWith(dataItem); }); - it('should handle e.cancel value: false', async () => { - const { POM, scheduler } = await createScheduler({ + it('should correctly handle e.cancel=true', async () => { + const { scheduler } = await createScheduler({ ...getDefaultConfig(), dataSource: [{ ...commonAppointment }], - onAppointmentFormOpening: (e) => { e.cancel = false; }, + onAppointmentDeleting: (e) => { e.cancel = true; }, }); + const dataSource = (scheduler as any).getDataSource(); + const dataItem = dataSource.items()[0]; - scheduler.showAppointmentPopup(commonAppointment); - - expect(POM.isPopupVisible()).toBe(true); - }); - }); - - describe('onAppointmentAdding', () => { - it('should be called when saving new appointment', async () => { - const { scheduler, POM } = await createScheduler(getDefaultConfig()); - - const addAppointmentSpy = jest.spyOn(scheduler, 'addAppointment'); - - scheduler.showAppointmentPopup({ ...commonAppointment }, true); - POM.popup.saveButton.click(); - await Promise.resolve(); + scheduler.deleteAppointment(dataItem); - expect(addAppointmentSpy).toHaveBeenCalledTimes(1); - expect(addAppointmentSpy).toHaveBeenCalledWith( - expect.objectContaining({ ...commonAppointment }), - ); + expect(dataSource.items().length).toBe(1); }); - it('should correctly handle e.cancel=true', async () => { - const { scheduler, POM } = await createScheduler({ - ...getDefaultConfig(), - onAppointmentAdding: (e) => { e.cancel = true; }, - }); - - scheduler.showAppointmentPopup({ ...commonAppointment }, true); - POM.popup.saveButton.click(); - await Promise.resolve(); - - const dataSource = (scheduler as any).getDataSource(); - expect(dataSource.items().length).toBe(0); - }); - - it('should correctly handle e.cancel=false', async () => { - const { scheduler, POM } = await createScheduler({ - ...getDefaultConfig(), - onAppointmentAdding: (e) => { e.cancel = false; }, - }); - - scheduler.showAppointmentPopup({ ...commonAppointment }, true); - POM.popup.saveButton.click(); - await Promise.resolve(); - - const dataSource = (scheduler as any).getDataSource(); - expect(dataSource.items().length).toBe(1); - expect(dataSource.items()[0]).toMatchObject(commonAppointment); - }); - }); - - describe('onAppointmentUpdating', () => { - it('should be called when saving appointment', async () => { - const { scheduler, POM } = await createScheduler({ - ...getDefaultConfig(), - dataSource: [{ ...commonAppointment }], - }); - const updateAppointmentSpy = jest.spyOn(scheduler, 'updateAppointment'); - const dataSource = (scheduler as any).getDataSource(); - const updatedItem = dataSource.items()[0]; - - 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); - }); - - it('should correctly handle e.cancel=true (T907281)', async () => { - const { scheduler, POM } = await createScheduler({ - ...getDefaultConfig(), - dataSource: [{ ...commonAppointment }], - onAppointmentUpdating: (e) => { e.cancel = true; }, - }); - const dataSource = (scheduler as any).getDataSource(); - const updatedItem = dataSource.items()[0]; - - scheduler.showAppointmentPopup(updatedItem); - POM.popup.setInputValue('subjectEditor', 'Updated Subject'); - POM.popup.saveButton.click(); - await Promise.resolve(); - - expect(dataSource.items()[0]).toEqual(commonAppointment); - }); - - it('should correctly handle e.cancel=false (T907281)', async () => { - const { scheduler, POM } = await createScheduler({ - ...getDefaultConfig(), - dataSource: [{ ...commonAppointment }], - onAppointmentUpdating: (e) => { e.cancel = false; }, - }); - const dataSource = (scheduler as any).getDataSource(); - const updatedItem = dataSource.items()[0]; - - scheduler.showAppointmentPopup(updatedItem); - POM.popup.setInputValue('subjectEditor', 'New Subject'); - POM.popup.saveButton.click(); - await Promise.resolve(); - - expect(dataSource.items()[0]).toEqual({ - allDay: false, - recurrenceRule: '', - ...commonAppointment, - text: 'New Subject', - }); - }); - }); - - describe('onAppointmentDeleting', () => { - it('should be called when deleting appointment', async () => { - const { scheduler } = await createScheduler({ - ...getDefaultConfig(), - dataSource: [{ ...commonAppointment }], - }); - const deleteAppointmentSpy = jest.spyOn(scheduler, 'deleteAppointment'); - const dataSource = (scheduler as any).getDataSource(); - const dataItem = dataSource.items()[0]; - - scheduler.deleteAppointment(dataItem); - - expect(deleteAppointmentSpy).toHaveBeenCalledTimes(1); - expect(deleteAppointmentSpy).toHaveBeenCalledWith(dataItem); - }); - - it('should correctly handle e.cancel=true', async () => { - const { scheduler } = await createScheduler({ - ...getDefaultConfig(), - dataSource: [{ ...commonAppointment }], - onAppointmentDeleting: (e) => { e.cancel = true; }, - }); - const dataSource = (scheduler as any).getDataSource(); - const dataItem = dataSource.items()[0]; - - scheduler.deleteAppointment(dataItem); - - expect(dataSource.items().length).toBe(1); - }); - - it('should correctly handle e.cancel=false', async () => { - const { scheduler } = await createScheduler({ - ...getDefaultConfig(), - dataSource: [{ ...commonAppointment }], - onAppointmentDeleting: (e) => { e.cancel = false; }, - }); - const dataSource = (scheduler as any).getDataSource(); - const dataItem = dataSource.items()[0]; - - scheduler.deleteAppointment(dataItem); - - expect(dataSource.items().length).toBe(0); - }); - }); - }); - - describe('showAppointmentPopup', () => { - it('should open appointment popup without data', async () => { - const { scheduler, POM } = await createScheduler(getDefaultConfig()); - - scheduler.showAppointmentPopup(); - - const expectedStartDate = new Date(scheduler.option('currentDate')); - const expectedEndDate = new Date(expectedStartDate.getTime() + scheduler.option('cellDuration') * toMilliseconds('minute')); - - expect(POM.popup.component.option('visible')).toBe(true); - expect(POM.popup.dxForm.option('formData')).toEqual({ - text: undefined, - allDay: false, - startDate: expectedStartDate, - endDate: expectedEndDate, - description: undefined, - recurrenceRule: '', - startDateTimeZone: undefined, - endDateTimeZone: undefined, - }); - }); - it('should open appointment popup with correct data', async () => { - const { scheduler, POM } = await createScheduler(getDefaultConfig()); - - scheduler.showAppointmentPopup(commonAppointment); - - expect(POM.popup.component.option('visible')).toBe(true); - expect(POM.popup.dxForm.option('formData')).toMatchObject({ - ...commonAppointment, - }); - }); - }); - - describe('hideAppointmentPopup', () => { - it('should hide appointment popup without saving changes', async () => { - const { scheduler, POM } = await createScheduler({ - ...getDefaultConfig(), - dataSource: [{ ...commonAppointment }], - }); - const dataSource = (scheduler as any).getDataSource(); - const item = dataSource.items()[0]; - - scheduler.showAppointmentPopup(item); - POM.popup.setInputValue('subjectEditor', 'New Subject'); - scheduler.hideAppointmentPopup(false); - - expect(dataSource.items()[0]).toMatchObject(commonAppointment); - }); - - it('should hide appointment popup with saving changes', async () => { - const { scheduler, POM } = await createScheduler({ - ...getDefaultConfig(), - dataSource: [{ ...commonAppointment }], - }); - const dataSource = (scheduler as any).getDataSource(); - const item = dataSource.items()[0]; - - scheduler.showAppointmentPopup(item); - POM.popup.setInputValue('subjectEditor', 'New Subject'); - scheduler.hideAppointmentPopup(true); - await Promise.resolve(); - - expect(dataSource.items()[0]).toMatchObject({ ...commonAppointment, text: 'New Subject' }); - }); - - it('should hide appointment popup with saving changes when recurrence form is opened', async () => { - const { scheduler, POM } = await createScheduler({ - ...getDefaultConfig(), - dataSource: [{ ...commonAppointment }], - }); - const dataSource = (scheduler as any).getDataSource(); - const item = dataSource.items()[0]; - - scheduler.showAppointmentPopup(item); - 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, - startDate: new Date(2024, 4, 25, 9, 30), - endDate: new Date(2024, 4, 25, 11), - recurrenceRule: 'FREQ=WEEKLY;BYDAY=TU', - }); - }); - }); - - describe('Customization', () => { - it('should propagate editing.form options to the form instance', async () => { - const { scheduler, POM } = await createScheduler({ - ...getDefaultConfig(), - editing: { - allowAdding: true, - allowUpdating: true, - form: { - height: 500, - }, - }, - }); - - scheduler.showAppointmentPopup(commonAppointment); - - const { dxForm: form } = POM.popup; - const formHeight = form.option('height') as number; - - expect(formHeight).toBe(500); - }); - - it('should merge editing.form options with default form configuration', async () => { - const { scheduler, POM } = await createScheduler({ - ...getDefaultConfig(), - editing: { - allowAdding: true, - allowUpdating: true, - form: { - height: 500, - elementAttr: { id: 'custom-form' }, - }, - }, - }); - - scheduler.showAppointmentPopup(commonAppointment); - - const { dxForm: form } = POM.popup; - const formHeight = form.option('height') as number; - const elementAttr = form.option('elementAttr') as { class?: string; id?: string }; - const { class: className, id } = elementAttr; - - expect(formHeight).toBe(500); - expect(className).toBe('dx-scheduler-form'); - expect(id).toBe('custom-form'); - }); - }); -}); - -describe('Appointment Popup', () => { - beforeEach(() => { - fx.off = true; - setupSchedulerTestEnvironment({ height: 600 }); - }); - - afterEach(() => { - const $scheduler = $(document.querySelector(`.${CLASSES.scheduler}`)); - // @ts-expect-error - $scheduler.dxScheduler('dispose'); - document.body.innerHTML = ''; - fx.off = false; - jest.useRealTimers(); - }); - - it('should open on double click on appointment', async () => { - const { POM } = await createScheduler({ - ...getDefaultConfig(), - dataSource: [{ ...commonAppointment }], - }); - - expect(POM.isPopupVisible()).toBe(false); - - POM.openPopupByDblClick('common-app'); - - expect(POM.isPopupVisible()).toBe(true); - expect(POM.popup.dxForm.option('formData')).toMatchObject({ ...commonAppointment }); - }); - - it('should open on tooltip click', async () => { - const { POM } = await createScheduler({ - ...getDefaultConfig(), - dataSource: [{ ...commonAppointment }], - }); - - expect(POM.isPopupVisible()).toBe(false); - - jest.useFakeTimers(); - POM.getAppointment('common-app').element?.click(); - jest.runAllTimers(); - - POM.tooltip.getAppointmentItem()?.click(); - - expect(POM.isPopupVisible()).toBe(true); - expect(POM.popup.dxForm.option('formData')).toMatchObject({ ...commonAppointment }); - }); - - it('should focus appointment after closing popup', async () => { - const { POM, keydown } = await createScheduler({ - ...getDefaultConfig(), - dataSource: [{ ...recurringAppointment }], - }); - - const appointmentElement = POM.getAppointment('recurring-app').element as HTMLElement; - appointmentElement.focus(); - - jest.useFakeTimers(); - keydown(appointmentElement, 'Enter'); - POM.popup.closeButton.click(); - jest.runAllTimers(); - - expect(appointmentElement?.classList.contains('dx-state-focused')).toBe(true); - }); - - describe('Toolbar', () => { - describe('Popup Title', () => { - it('should display "New Appointment" when creating new appointment', async () => { - const { scheduler, POM } = await createScheduler({ - ...getDefaultConfig(), - editing: { allowAdding: true }, - }); - - scheduler.showAppointmentPopup(); - - const toolbarItems = POM.popup.component.option('toolbarItems'); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const titleItem = toolbarItems?.find((item: any) => item.cssClass === 'dx-toolbar-label'); - - expect(titleItem?.text).toBe('New Appointment'); - }); - - it('should display "Edit Appointment" when editing existing appointment', async () => { - const { scheduler, POM } = await createScheduler({ - ...getDefaultConfig(), - editing: { allowUpdating: true }, - }); - - scheduler.showAppointmentPopup(commonAppointment); - - const toolbarItems = POM.popup.component.option('toolbarItems'); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const titleItem = toolbarItems?.find((item: any) => item.cssClass === 'dx-toolbar-label'); - - expect(titleItem?.text).toBe('Edit Appointment'); - }); - }); - - it.each([ - { allowUpdating: false, disabled: false }, - { allowUpdating: false, disabled: true }, - { allowUpdating: true, disabled: false }, - { allowUpdating: true, disabled: true }, - ])('Buttons visibility in main form when %p', async ({ allowUpdating, disabled }) => { - const shouldHaveSaveButton = allowUpdating && !disabled; - - const { scheduler, POM } = await createScheduler({ - ...getDefaultConfig(), - editing: { allowUpdating }, - }); - - scheduler.showAppointmentPopup(disabled ? disabledAppointment : commonAppointment); - - const toolbarItems = POM.popup.component.option('toolbarItems') ?? []; - - expect(toolbarItems.some((i) => (i as any).shortcut === 'cancel')).toBe(true); - expect(toolbarItems.some((i) => (i as any).shortcut === 'done')).toBe(shouldHaveSaveButton); - }); - - it.each([ - { allowUpdating: false, disabled: false }, - { allowUpdating: false, disabled: true }, - { allowUpdating: true, disabled: false }, - { allowUpdating: true, disabled: true }, - ])('Buttons visibility in recurrence form when %p', async ({ allowUpdating, disabled }) => { - const shouldHaveSaveButton = allowUpdating && !disabled; - - const { scheduler, POM } = await createScheduler({ - ...getDefaultConfig(), - editing: { allowUpdating }, - dataSource: [], - }); - - scheduler.showAppointmentPopup({ - ...recurringAppointment, - disabled, - }); - - if (allowUpdating) { - POM.popup.editSeriesButton.click(); - } - - POM.popup.recurrenceSettingsButton.click(); - - const toolbarItems = POM.popup.component.option('toolbarItems') ?? []; - - expect(toolbarItems.some((i) => (i as any).shortcut === 'cancel')).toBe(true); - expect(toolbarItems.some((i) => (i as any).shortcut === 'done')).toBe(shouldHaveSaveButton); - }); - - it('Buttons visibility after editing option changed', async () => { - const { scheduler, POM } = await createScheduler({ - ...getDefaultConfig(), - editing: { - allowUpdating: true, - allowAdding: true, - }, - }); - - const getToolbarItems = (): ToolbarItem [] => POM.popup.component.option('toolbarItems') ?? []; - - const doneButtonMatcher = expect.arrayContaining([ - expect.objectContaining({ - shortcut: 'done', - }), - ]); - const cancelButtonMatcher = expect.arrayContaining([ - expect.objectContaining({ - shortcut: 'cancel', - }), - ]); - - scheduler.showAppointmentPopup(); - - expect(getToolbarItems()).toEqual(doneButtonMatcher); - expect(getToolbarItems()).toEqual(cancelButtonMatcher); - - scheduler.option('editing', { allowUpdating: false, allowAdding: true }); - scheduler.showAppointmentPopup(commonAppointment); - - expect(getToolbarItems()).not.toEqual(doneButtonMatcher); - expect(getToolbarItems()).toEqual(cancelButtonMatcher); + it('should correctly handle e.cancel=false', async () => { + const { scheduler } = await createScheduler({ + ...getDefaultConfig(), + dataSource: [{ ...commonAppointment }], + onAppointmentDeleting: (e) => { e.cancel = false; }, + }); + const dataSource = (scheduler as any).getDataSource(); + const dataItem = dataSource.items()[0]; - await POM.popup.component.hide(); - scheduler.showAppointmentPopup(); + scheduler.deleteAppointment(dataItem); - expect(getToolbarItems()).toEqual(doneButtonMatcher); - expect(getToolbarItems()).toEqual(cancelButtonMatcher); + expect(dataSource.items().length).toBe(0); + }); }); }); - describe('Customization', () => { - it('should pass custom popup options from editing.popup to appointment popup', async () => { - const { scheduler, POM } = await createScheduler({ - ...getDefaultConfig(), - editing: { - allowAdding: true, - allowUpdating: true, - popup: { - showTitle: true, - title: 'Custom Appointment Form', - maxHeight: '80%', - dragEnabled: true, - }, - }, - }); + describe('showAppointmentPopup', () => { + it('should open appointment popup without data', async () => { + const { scheduler, POM } = await createScheduler(getDefaultConfig()); - scheduler.showAppointmentPopup(commonAppointment); + scheduler.showAppointmentPopup(); - expect(POM.popup.component.option('showTitle')).toBe(true); - expect(POM.popup.component.option('title')).toBe('Custom Appointment Form'); - expect(POM.popup.component.option('maxHeight')).toBe('80%'); - expect(POM.popup.component.option('dragEnabled')).toBe(true); - }); + const expectedStartDate = new Date(scheduler.option('currentDate')); + const expectedEndDate = new Date(expectedStartDate.getTime() + scheduler.option('cellDuration') * toMilliseconds('minute')); - it('should use default popup options when editing.popup is not specified', async () => { - const { scheduler, POM } = await createScheduler({ - ...getDefaultConfig(), - editing: { - allowAdding: true, - allowUpdating: true, - }, + expect(POM.popup.component.option('visible')).toBe(true); + expect(POM.popup.dxForm.option('formData')).toEqual({ + text: undefined, + allDay: false, + startDate: expectedStartDate, + endDate: expectedEndDate, + description: undefined, + recurrenceRule: '', + startDateTimeZone: undefined, + endDateTimeZone: undefined, }); + }); + it('should open appointment popup with correct data', async () => { + const { scheduler, POM } = await createScheduler(getDefaultConfig()); scheduler.showAppointmentPopup(commonAppointment); - expect(POM.popup.component.option('showTitle')).toBe(false); - expect(POM.popup.component.option('height')).toBe('auto'); - expect(POM.popup.component.option('maxHeight')).toBe('90%'); + expect(POM.popup.component.option('visible')).toBe(true); + expect(POM.popup.dxForm.option('formData')).toMatchObject({ + ...commonAppointment, + }); }); + }); - it('should merge custom popup options with default options', async () => { + describe('hideAppointmentPopup', () => { + it('should hide appointment popup without saving changes', async () => { const { scheduler, POM } = await createScheduler({ ...getDefaultConfig(), - editing: { - allowAdding: true, - allowUpdating: true, - popup: { - showTitle: true, - title: 'My Form', - }, - }, + dataSource: [{ ...commonAppointment }], }); + const dataSource = (scheduler as any).getDataSource(); + const item = dataSource.items()[0]; - scheduler.showAppointmentPopup(commonAppointment); - - expect(POM.popup.component.option('showTitle')).toBe(true); - expect(POM.popup.component.option('title')).toBe('My Form'); + scheduler.showAppointmentPopup(item); + POM.popup.setInputValue('subjectEditor', 'New Subject'); + scheduler.hideAppointmentPopup(false); - expect(POM.popup.component.option('showCloseButton')).toBe(false); - expect(POM.popup.component.option('enableBodyScroll')).toBe(false); - expect(POM.popup.component.option('preventScrollEvents')).toBe(false); + expect(dataSource.items()[0]).toMatchObject(commonAppointment); }); - it('should allow overriding default popup options', async () => { + it('should hide appointment popup with saving changes', async () => { const { scheduler, POM } = await createScheduler({ ...getDefaultConfig(), - editing: { - allowAdding: true, - allowUpdating: true, - popup: { - showCloseButton: true, - enableBodyScroll: true, - }, - }, + dataSource: [{ ...commonAppointment }], }); + const dataSource = (scheduler as any).getDataSource(); + const item = dataSource.items()[0]; - scheduler.showAppointmentPopup(commonAppointment); + scheduler.showAppointmentPopup(item); + POM.popup.setInputValue('subjectEditor', 'New Subject'); + scheduler.hideAppointmentPopup(true); + await Promise.resolve(); - expect(POM.popup.component.option('showCloseButton')).toBe(true); - expect(POM.popup.component.option('enableBodyScroll')).toBe(true); + expect(dataSource.items()[0]).toMatchObject({ ...commonAppointment, text: 'New Subject' }); }); - it('should apply wrapperAttr configuration to popup', async () => { + it('should hide appointment popup with saving changes when recurrence form is opened', async () => { const { scheduler, POM } = await createScheduler({ ...getDefaultConfig(), - editing: { - allowAdding: true, - allowUpdating: true, - popup: { - wrapperAttr: { - id: 'test', - }, - }, - }, + dataSource: [{ ...commonAppointment }], }); + const dataSource = (scheduler as any).getDataSource(); + const item = dataSource.items()[0]; - scheduler.showAppointmentPopup(commonAppointment); + scheduler.showAppointmentPopup(item); + POM.popup.selectRepeatValue('weekly'); + POM.popup.setInputValue('recurrenceStartDateEditor', new Date(2024, 4, 25)); + scheduler.hideAppointmentPopup(true); + await Promise.resolve(); - const wrapperAttr = POM.popup.component.option('wrapperAttr'); - expect(wrapperAttr.id).toBe('test'); - expect(wrapperAttr.class).toBeDefined(); + expect(dataSource.items()[0]).toMatchObject({ + ...commonAppointment, + startDate: new Date(2024, 4, 25, 9, 30), + endDate: new Date(2024, 4, 25, 11), + recurrenceRule: 'FREQ=WEEKLY;BYDAY=TU', + }); }); + }); - it('should call onInitialized callback when popup is initialized', async () => { - const onInitialized = jest.fn(); + describe('Customization', () => { + it('should propagate editing.form options to the form instance', async () => { const { scheduler, POM } = await createScheduler({ ...getDefaultConfig(), editing: { allowAdding: true, allowUpdating: true, - popup: { - onInitialized, + form: { + height: 500, }, }, }); scheduler.showAppointmentPopup(commonAppointment); - expect(POM.isPopupVisible()).toBe(true); - expect(onInitialized).toHaveBeenCalled(); - expect(onInitialized).toHaveBeenCalledTimes(1); - }); - - it('should call onShowing callback when popup is shown', async () => { - const onShowing = jest.fn(); - const onAppointmentFormOpening = jest.fn(); - const { scheduler } = await createScheduler({ - ...getDefaultConfig(), - editing: { - allowAdding: true, - allowUpdating: true, - popup: { - onShowing, - }, - }, - onAppointmentFormOpening, - }); - - scheduler.showAppointmentPopup(commonAppointment); + const { dxForm: form } = POM.popup; + const formHeight = form.option('height') as number; - expect(onShowing).toHaveBeenCalled(); - expect(onShowing).toHaveBeenCalledTimes(1); - expect(onAppointmentFormOpening).toHaveBeenCalled(); - expect(onAppointmentFormOpening).toHaveBeenCalledTimes(1); + expect(formHeight).toBe(500); }); - it('should call onHiding callback when popup is hidden', async () => { - const onHiding = jest.fn(); - const { scheduler } = await createScheduler({ + it('should merge editing.form options with default form configuration', async () => { + const { scheduler, POM } = await createScheduler({ ...getDefaultConfig(), editing: { allowAdding: true, allowUpdating: true, - popup: { - onHiding, + form: { + height: 500, + elementAttr: { id: 'custom-form' }, }, }, }); - const focusSpy = jest.spyOn(scheduler, 'focus'); - scheduler.showAppointmentPopup(commonAppointment); - expect(onHiding).not.toHaveBeenCalled(); - expect(focusSpy).not.toHaveBeenCalled(); - - scheduler.hideAppointmentPopup(); - - expect(onHiding).toHaveBeenCalled(); - expect(onHiding).toHaveBeenCalledTimes(1); - expect(focusSpy).toHaveBeenCalled(); - expect(focusSpy).toHaveBeenCalledTimes(1); + const { dxForm: form } = POM.popup; + const formHeight = form.option('height') as number; + const elementAttr = form.option('elementAttr') as { class?: string; id?: string }; + const { class: className, id } = elementAttr; - focusSpy.mockRestore(); + expect(formHeight).toBe(500); + expect(className).toBe('dx-scheduler-form'); + expect(id).toBe('custom-form'); }); + }); +}); - it('should preserve custom toolbarItems when popup opens', async () => { - const { scheduler, POM } = await createScheduler({ - ...getDefaultConfig(), - editing: { - popup: { - toolbarItems: [{ - toolbar: 'top', location: 'before', text: 'Custom Title', cssClass: 'custom-title', - }, { - toolbar: 'top', location: 'after', widget: 'dxButton', options: { text: 'Custom Save' }, - }], - }, - }, - }); - - scheduler.showAppointmentPopup(commonAppointment); +describe('Appointment Popup', () => { + beforeEach(() => { + fx.off = true; + setupSchedulerTestEnvironment({ height: 600 }); + }); - const toolbarItems = POM.popup.component.option('toolbarItems'); + afterEach(() => { + const $scheduler = $(document.querySelector(`.${CLASSES.scheduler}`)); + // @ts-expect-error + $scheduler.dxScheduler('dispose'); + document.body.innerHTML = ''; + fx.off = false; + jest.useRealTimers(); + }); - expect(toolbarItems).toBeDefined(); - expect(toolbarItems).toHaveLength(2); - expect(toolbarItems).toContainEqual(expect.objectContaining({ - cssClass: 'custom-title', location: 'before', text: 'Custom Title', toolbar: 'top', - })); - expect(toolbarItems).toContainEqual(expect.objectContaining( - { - toolbar: 'top', - location: 'after', - widget: 'dxButton', - options: expect.objectContaining({ text: 'Custom Save' }), - }, - )); + it('should open on double click on appointment', async () => { + const { POM } = await createScheduler({ + ...getDefaultConfig(), + dataSource: [{ ...commonAppointment }], }); - it('should preserve custom toolbarItems when popup is reopened', async () => { - const { scheduler, POM } = await createScheduler({ - ...getDefaultConfig(), - editing: { - allowAdding: true, - allowUpdating: true, - popup: { - toolbarItems: [{ toolbar: 'top', location: 'before', text: 'Custom Toolbar' }], - }, - }, - }); + expect(POM.isPopupVisible()).toBe(false); - scheduler.showAppointmentPopup(commonAppointment); - scheduler.hideAppointmentPopup(); - scheduler.showAppointmentPopup(allDayAppointment); + POM.openPopupByDblClick('common-app'); - const toolbarItems = POM.popup.component.option('toolbarItems'); - expect(toolbarItems).toBeDefined(); - expect(toolbarItems).toHaveLength(1); - expect(toolbarItems?.[0]?.text).toBe('Custom Toolbar'); + expect(POM.isPopupVisible()).toBe(true); + expect(POM.popup.dxForm.option('formData')).toMatchObject({ ...commonAppointment }); + }); + + it('should open on tooltip click', async () => { + const { POM } = await createScheduler({ + ...getDefaultConfig(), + dataSource: [{ ...commonAppointment }], }); - it('should open popup if popup.deferRendering is false', async () => { - const { scheduler, POM } = await createScheduler({ - ...getDefaultConfig(), - editing: { - allowAdding: true, - allowUpdating: true, - popup: { - deferRendering: false, - }, - }, - }); + expect(POM.isPopupVisible()).toBe(false); - scheduler.showAppointmentPopup(commonAppointment); + jest.useFakeTimers(); + POM.getAppointment('common-app').element?.click(); + jest.runAllTimers(); - expect(POM.isPopupVisible()).toBe(true); - }); + POM.tooltip.getAppointmentItem()?.click(); - describe('Popup width and maxWidth options', () => { - // Mock window width to avoid fullscreen mode - beforeEach(() => { - Object.defineProperty(document.documentElement, 'clientWidth', { - value: 1280, - }); - }); + expect(POM.isPopupVisible()).toBe(true); + expect(POM.popup.dxForm.option('formData')).toMatchObject({ ...commonAppointment }); + }); - it('should use custom maxWidth when specified', async () => { - const { scheduler, POM } = await createScheduler({ - ...getDefaultConfig(), - editing: { - allowAdding: true, - allowUpdating: true, - popup: { - maxWidth: 500, - }, - }, - }); + it('should focus appointment after closing popup', async () => { + const { POM, keydown } = await createScheduler({ + ...getDefaultConfig(), + dataSource: [{ ...recurringAppointment }], + }); - scheduler.showAppointmentPopup(commonAppointment); + const appointmentElement = POM.getAppointment('recurring-app').element as HTMLElement; + appointmentElement.focus(); - const maxWidth = POM.popup.component.option('maxWidth'); - expect(maxWidth).toBe(500); - }); + jest.useFakeTimers(); + keydown(appointmentElement, 'Enter'); + POM.popup.closeButton.click(); + jest.runAllTimers(); - it('should use custom width as maxWidth when maxWidth is not specified', async () => { + expect(appointmentElement?.classList.contains('dx-state-focused')).toBe(true); + }); + + describe('Toolbar', () => { + describe('Popup Title', () => { + it('should display "New Appointment" when creating new appointment', async () => { const { scheduler, POM } = await createScheduler({ ...getDefaultConfig(), - editing: { - allowAdding: true, - allowUpdating: true, - popup: { - width: 600, - }, - }, + editing: { allowAdding: true }, }); - scheduler.showAppointmentPopup(commonAppointment); + scheduler.showAppointmentPopup(); - const width = POM.popup.component.option('width'); - expect(width).toBe(600); + const toolbarItems = POM.popup.component.option('toolbarItems'); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const titleItem = toolbarItems?.find((item: any) => item.cssClass === 'dx-toolbar-label'); - const maxWidth = POM.popup.component.option('maxWidth'); - expect(maxWidth).toBe(600); + expect(titleItem?.text).toBe('New Appointment'); }); - it('should use maxWidth option value (not width) for maxWidth when both maxWidth and width are specified', async () => { + it('should display "Edit Appointment" when editing existing appointment', async () => { const { scheduler, POM } = await createScheduler({ ...getDefaultConfig(), - editing: { - allowAdding: true, - allowUpdating: true, - popup: { - width: 600, - maxWidth: 500, - }, - }, + editing: { allowUpdating: true }, }); scheduler.showAppointmentPopup(commonAppointment); - const width = POM.popup.component.option('width'); - expect(width).toBe(600); + const toolbarItems = POM.popup.component.option('toolbarItems'); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const titleItem = toolbarItems?.find((item: any) => item.cssClass === 'dx-toolbar-label'); - const maxWidth = POM.popup.component.option('maxWidth'); - expect(maxWidth).toBe(500); + expect(titleItem?.text).toBe('Edit Appointment'); }); }); - }); -}); - -describe('Appointment Popup Content', () => { - it.todo('appointmentPopup should not prevent mouse/touch events by default (T968188)'); - it.todo('showAppointmentPopup should render a popup only once'); - it.todo('showAppointmentPopup should work correctly after scheduler repainting'); - it.todo('changing editing should work correctly after showing popup'); - it.todo('showAppointmentPopup should render a popup form only once'); - it.todo('showAppointmentPopup should render a popup content only once'); - it.todo('Recurrence editor should has right startDate after form items change'); - it.todo('There are no exceptions when select date on the appointment popup,if dates are undefined'); - it.todo('Validate works always before done click'); - it.todo('Load panel should not be shown if validation is fail'); - it.todo('Load panel should be hidden if event validation fail'); - it.todo('Load panel should be hidden at the second appointment form opening'); -}); - -describe('Timezone Editors', () => { - it.todo('timeZone editors should have correct options'); - it.todo('timeZone editor should have correct display value for timezones with different offsets'); - it.todo('dataSource of timezoneEditor should be filtered'); -}); - -describe('Customize form items', () => { - beforeEach(() => { - fx.off = true; - setupSchedulerTestEnvironment({ height: 600 }); - }); - afterEach(() => { - const $scheduler = $(document.querySelector(`.${CLASSES.scheduler}`)); - // @ts-expect-error - $scheduler.dxScheduler('dispose'); - document.body.innerHTML = ''; - fx.off = false; - jest.useRealTimers(); - }); + it.each([ + { allowUpdating: false, disabled: false }, + { allowUpdating: false, disabled: true }, + { allowUpdating: true, disabled: false }, + { allowUpdating: true, disabled: true }, + ])('Buttons visibility in main form when %p', async ({ allowUpdating, disabled }) => { + const shouldHaveSaveButton = allowUpdating && !disabled; - describe('Basic form customization', () => { - it('should use default form when editing.items is not set', async () => { const { scheduler, POM } = await createScheduler({ ...getDefaultConfig(), - editing: { - allowAdding: true, - allowUpdating: true, - }, + editing: { allowUpdating }, }); - scheduler.showAppointmentPopup(commonAppointment); + scheduler.showAppointmentPopup(disabled ? disabledAppointment : commonAppointment); - const { dxForm: form } = POM.popup; - const formItems = form.option('items') as FormItem[]; + const toolbarItems = POM.popup.component.option('toolbarItems') ?? []; - expect(formItems).toBeDefined(); - expect(formItems?.length).toBeGreaterThan(0); + expect(toolbarItems.some((i) => (i as any).shortcut === 'cancel')).toBe(true); + expect(toolbarItems.some((i) => (i as any).shortcut === 'done')).toBe(shouldHaveSaveButton); }); - it('should show empty form when editing.items is empty array', async () => { + it.each([ + { allowUpdating: false, disabled: false }, + { allowUpdating: false, disabled: true }, + { allowUpdating: true, disabled: false }, + { allowUpdating: true, disabled: true }, + ])('Buttons visibility in recurrence form when %p', async ({ allowUpdating, disabled }) => { + const shouldHaveSaveButton = allowUpdating && !disabled; + const { scheduler, POM } = await createScheduler({ ...getDefaultConfig(), - editing: { - allowAdding: true, - allowUpdating: true, - form: { - items: [], - }, - }, + editing: { allowUpdating }, + dataSource: [], }); - scheduler.showAppointmentPopup(commonAppointment); + scheduler.showAppointmentPopup({ + ...recurringAppointment, + disabled, + }); - const { dxForm: form } = POM.popup; - const formItems = form.option('items') as FormItem[]; + if (allowUpdating) { + POM.popup.editSeriesButton.click(); + } + + POM.popup.recurrenceSettingsButton.click(); + + const toolbarItems = POM.popup.component.option('toolbarItems') ?? []; - expect(formItems?.length ?? 0).toBe(0); + expect(toolbarItems.some((i) => (i as any).shortcut === 'cancel')).toBe(true); + expect(toolbarItems.some((i) => (i as any).shortcut === 'done')).toBe(shouldHaveSaveButton); }); - it('should show mainGroup when specified in string array', async () => { + it('Buttons visibility after editing option changed', async () => { const { scheduler, POM } = await createScheduler({ ...getDefaultConfig(), editing: { - allowAdding: true, allowUpdating: true, - form: { - items: ['mainGroup'], - }, + allowAdding: true, }, }); + const getToolbarItems = (): ToolbarItem [] => POM.popup.component.option('toolbarItems') ?? []; + + const doneButtonMatcher = expect.arrayContaining([ + expect.objectContaining({ + shortcut: 'done', + }), + ]); + const cancelButtonMatcher = expect.arrayContaining([ + expect.objectContaining({ + shortcut: 'cancel', + }), + ]); + + scheduler.showAppointmentPopup(); + + expect(getToolbarItems()).toEqual(doneButtonMatcher); + expect(getToolbarItems()).toEqual(cancelButtonMatcher); + + scheduler.option('editing', { allowUpdating: false, allowAdding: true }); scheduler.showAppointmentPopup(commonAppointment); - const { dxForm: form } = POM.popup; - const formItems = form.option('items') as FormItem[]; + expect(getToolbarItems()).not.toEqual(doneButtonMatcher); + expect(getToolbarItems()).toEqual(cancelButtonMatcher); + + await POM.popup.component.hide(); + scheduler.showAppointmentPopup(); - expect(formItems?.length).toBe(1); - expect(formItems?.[0]?.name).toBe('mainGroup'); + expect(getToolbarItems()).toEqual(doneButtonMatcher); + expect(getToolbarItems()).toEqual(cancelButtonMatcher); }); + }); - it('should hide group when visible is false', async () => { + describe('Customization', () => { + it('should pass custom popup options from editing.popup to appointment popup', async () => { const { scheduler, POM } = await createScheduler({ ...getDefaultConfig(), editing: { allowAdding: true, allowUpdating: true, - form: { - items: [{ name: 'mainGroup', visible: false }], + popup: { + showTitle: true, + title: 'Custom Appointment Form', + maxHeight: '80%', + dragEnabled: true, }, }, }); scheduler.showAppointmentPopup(commonAppointment); - const { dxForm: form } = POM.popup; - const formItems = form.option('items') as FormItem[]; - - expect(formItems?.length).toBe(1); - expect(formItems?.[0]?.visible).toBe(false); + expect(POM.popup.component.option('showTitle')).toBe(true); + expect(POM.popup.component.option('title')).toBe('Custom Appointment Form'); + expect(POM.popup.component.option('maxHeight')).toBe('80%'); + expect(POM.popup.component.option('dragEnabled')).toBe(true); }); - it('should show group when visible is true', async () => { + it('should use default popup options when editing.popup is not specified', async () => { const { scheduler, POM } = await createScheduler({ ...getDefaultConfig(), editing: { allowAdding: true, allowUpdating: true, - form: { - items: [{ name: 'mainGroup', visible: true }], - }, }, }); scheduler.showAppointmentPopup(commonAppointment); - const { dxForm: form } = POM.popup; - const formItems = form.option('items') as FormItem[]; - - expect(formItems?.length).toBe(1); - expect(formItems?.[0]?.visible).toBe(true); + expect(POM.popup.component.option('showTitle')).toBe(false); + expect(POM.popup.component.option('height')).toBe('auto'); + expect(POM.popup.component.option('maxHeight')).toBe('90%'); }); - it('should filter children when items array is specified', async () => { + it('should merge custom popup options with default options', async () => { const { scheduler, POM } = await createScheduler({ ...getDefaultConfig(), editing: { allowAdding: true, allowUpdating: true, - form: { - items: [{ - name: 'mainGroup', - visible: true, - items: ['subjectGroup'], - }], + popup: { + showTitle: true, + title: 'My Form', }, }, }); scheduler.showAppointmentPopup(commonAppointment); - const { dxForm: form } = POM.popup; - const formItems = form.option('items') as FormItem[]; - const mainGroup = formItems?.[0] as GroupItem; + expect(POM.popup.component.option('showTitle')).toBe(true); + expect(POM.popup.component.option('title')).toBe('My Form'); - expect(formItems?.length).toBe(1); - expect(mainGroup?.items?.length).toBe(1); - expect(mainGroup?.items?.[0]?.name).toBe('subjectGroup'); + expect(POM.popup.component.option('showCloseButton')).toBe(false); + expect(POM.popup.component.option('enableBodyScroll')).toBe(false); + expect(POM.popup.component.option('preventScrollEvents')).toBe(false); }); - it('should handle non-existent groups gracefully', async () => { + it('should allow overriding default popup options', async () => { const { scheduler, POM } = await createScheduler({ ...getDefaultConfig(), editing: { allowAdding: true, allowUpdating: true, - form: { - items: ['nonExistentGroup'], + popup: { + showCloseButton: true, + enableBodyScroll: true, }, }, }); scheduler.showAppointmentPopup(commonAppointment); - const { dxForm: form } = POM.popup; - const formItems = form.option('items') as FormItem[]; - - expect(formItems?.length ?? 0).toBe(1); + expect(POM.popup.component.option('showCloseButton')).toBe(true); + expect(POM.popup.component.option('enableBodyScroll')).toBe(true); }); - it('should call custom onContentReady and onInitialized and preserving default', async () => { - const onContentReady = jest.fn(); - const onInitialized = jest.fn(); + it('should apply wrapperAttr configuration to popup', async () => { const { scheduler, POM } = await createScheduler({ ...getDefaultConfig(), - ...{ - editing: { - form: { - onContentReady, - onInitialized, + editing: { + allowAdding: true, + allowUpdating: true, + popup: { + wrapperAttr: { + id: 'test', }, }, }, }); - scheduler.showAppointmentPopup(); - - POM.popup.selectRepeatValue('weekly'); - - expect(POM.popup.isMainGroupVisible()).toBe(false); - expect(POM.popup.isRecurrenceGroupVisible()).toBe(true); - - expect(onContentReady).toHaveBeenCalled(); - expect(onInitialized).toHaveBeenCalled(); - }); - }); + scheduler.showAppointmentPopup(commonAppointment); - it('should call custom onContentReady and onInitialized and preserving default', async () => { - const onContentReady = jest.fn(); - const onInitialized = jest.fn(); - const { scheduler, POM } = await createScheduler({ - ...getDefaultConfig(), - ...{ - editing: { - form: { - onContentReady, - onInitialized, - }, - }, - }, + const wrapperAttr = POM.popup.component.option('wrapperAttr'); + expect(wrapperAttr.id).toBe('test'); + expect(wrapperAttr.class).toBeDefined(); }); - scheduler.showAppointmentPopup(); - - POM.popup.selectRepeatValue('weekly'); - - expect(POM.popup.isMainGroupVisible()).toBe(false); - expect(POM.popup.isRecurrenceGroupVisible()).toBe(true); - - expect(onContentReady).toHaveBeenCalled(); - expect(onInitialized).toHaveBeenCalled(); - }); - - describe('Form customization with editing.items', () => { - it('should handle empty items array', async () => { + it('should call onInitialized callback when popup is initialized', async () => { + const onInitialized = jest.fn(); const { scheduler, POM } = await createScheduler({ ...getDefaultConfig(), editing: { allowAdding: true, allowUpdating: true, - form: { - items: [], + popup: { + onInitialized, }, }, }); scheduler.showAppointmentPopup(commonAppointment); - const { dxForm: form } = POM.popup; - const formItems = form.option('items') as FormItem[]; - expect(formItems?.length).toBe(0); + expect(POM.isPopupVisible()).toBe(true); + expect(onInitialized).toHaveBeenCalled(); + expect(onInitialized).toHaveBeenCalledTimes(1); }); - it('should handle string array configuration', async () => { - const { scheduler, POM } = await createScheduler({ + it('should call onShowing callback when popup is shown', async () => { + const onShowing = jest.fn(); + const onAppointmentFormOpening = jest.fn(); + const { scheduler } = await createScheduler({ ...getDefaultConfig(), editing: { allowAdding: true, allowUpdating: true, - form: { - items: ['mainGroup'], + popup: { + onShowing, }, }, + onAppointmentFormOpening, }); scheduler.showAppointmentPopup(commonAppointment); - const { dxForm: form } = POM.popup; - const formItems = form.option('items') as FormItem[]; - expect(formItems?.length).toBe(1); - expect((formItems?.[0] as GroupItem)?.name).toBe('mainGroup'); + expect(onShowing).toHaveBeenCalled(); + expect(onShowing).toHaveBeenCalledTimes(1); + expect(onAppointmentFormOpening).toHaveBeenCalled(); + expect(onAppointmentFormOpening).toHaveBeenCalledTimes(1); }); - it('should handle object configuration with visible false', async () => { - const { scheduler, POM } = await createScheduler({ + it('should call onHiding callback when popup is hidden', async () => { + const onHiding = jest.fn(); + const { scheduler } = await createScheduler({ ...getDefaultConfig(), editing: { allowAdding: true, allowUpdating: true, - form: { - items: [{ name: 'mainGroup', visible: false }], + popup: { + onHiding, }, }, }); + const focusSpy = jest.spyOn(scheduler, 'focus'); + scheduler.showAppointmentPopup(commonAppointment); - const { dxForm: form } = POM.popup; - const formItems = form.option('items') as FormItem[]; - expect(formItems?.length).toBe(1); - expect(formItems?.[0]?.visible).toBe(false); + expect(onHiding).not.toHaveBeenCalled(); + expect(focusSpy).not.toHaveBeenCalled(); + + scheduler.hideAppointmentPopup(); + + expect(onHiding).toHaveBeenCalled(); + expect(onHiding).toHaveBeenCalledTimes(1); + expect(focusSpy).toHaveBeenCalled(); + expect(focusSpy).toHaveBeenCalledTimes(1); + + focusSpy.mockRestore(); }); - it('should handle object configuration with custom items', async () => { + it('should preserve custom toolbarItems when popup opens', async () => { const { scheduler, POM } = await createScheduler({ ...getDefaultConfig(), editing: { - allowAdding: true, - allowUpdating: true, - form: { - items: [{ - name: 'mainGroup', - items: ['subjectGroup', 'dateGroup'], + popup: { + toolbarItems: [{ + toolbar: 'top', location: 'before', text: 'Custom Title', cssClass: 'custom-title', + }, { + toolbar: 'top', location: 'after', widget: 'dxButton', options: { text: 'Custom Save' }, }], }, }, @@ -2792,98 +1819,151 @@ describe('Customize form items', () => { scheduler.showAppointmentPopup(commonAppointment); - const { dxForm: form } = POM.popup; - const formItems = form.option('items') as FormItem[]; - const mainGroup = formItems?.[0] as GroupItem; - expect(mainGroup?.items?.length).toBe(2); - expect((mainGroup?.items?.[0] as GroupItem)?.name).toBe('subjectGroup'); - expect((mainGroup?.items?.[1] as GroupItem)?.name).toBe('dateGroup'); + const toolbarItems = POM.popup.component.option('toolbarItems'); + + expect(toolbarItems).toBeDefined(); + expect(toolbarItems).toHaveLength(2); + expect(toolbarItems).toContainEqual(expect.objectContaining({ + cssClass: 'custom-title', location: 'before', text: 'Custom Title', toolbar: 'top', + })); + expect(toolbarItems).toContainEqual(expect.objectContaining( + { + toolbar: 'top', + location: 'after', + widget: 'dxButton', + options: expect.objectContaining({ text: 'Custom Save' }), + }, + )); }); - it('should handle non-existent group names', async () => { + it('should preserve custom toolbarItems when popup is reopened', async () => { const { scheduler, POM } = await createScheduler({ ...getDefaultConfig(), editing: { allowAdding: true, allowUpdating: true, - form: { - items: ['nonExistentGroup'], + popup: { + toolbarItems: [{ toolbar: 'top', location: 'before', text: 'Custom Toolbar' }], }, }, }); scheduler.showAppointmentPopup(commonAppointment); + scheduler.hideAppointmentPopup(); + scheduler.showAppointmentPopup(allDayAppointment); - const { dxForm: form } = POM.popup; - const formItems = form.option('items') as FormItem[]; - expect(formItems?.length).toBe(1); + const toolbarItems = POM.popup.component.option('toolbarItems'); + expect(toolbarItems).toBeDefined(); + expect(toolbarItems).toHaveLength(1); + expect(toolbarItems?.[0]?.text).toBe('Custom Toolbar'); }); - it('should handle undefined items', async () => { + it('should open popup if popup.deferRendering is false', async () => { const { scheduler, POM } = await createScheduler({ ...getDefaultConfig(), editing: { allowAdding: true, allowUpdating: true, - form: { - items: undefined, + popup: { + deferRendering: false, }, }, }); scheduler.showAppointmentPopup(commonAppointment); - const { dxForm: form } = POM.popup; - const formItems = form.option('items') as FormItem[]; - expect(formItems?.length).toBeGreaterThan(0); + expect(POM.isPopupVisible()).toBe(true); }); - it('should handle mixed configurations', async () => { - const { scheduler, POM } = await createScheduler({ - ...getDefaultConfig(), - editing: { - allowAdding: true, - allowUpdating: true, - form: { - items: [ - 'mainGroup', - { name: 'mainGroup', visible: false }, - ], - }, - }, + describe('Popup width and maxWidth options', () => { + // Mock window width to avoid fullscreen mode + beforeEach(() => { + Object.defineProperty(document.documentElement, 'clientWidth', { + value: 1280, + }); }); - scheduler.showAppointmentPopup(commonAppointment); + it('should use custom maxWidth when specified', async () => { + const { scheduler, POM } = await createScheduler({ + ...getDefaultConfig(), + editing: { + allowAdding: true, + allowUpdating: true, + popup: { + maxWidth: 500, + }, + }, + }); - const { dxForm: form } = POM.popup; - const formItems = form.option('items') as FormItem[]; - expect(formItems?.length).toBe(2); - expect((formItems?.[0] as any)?.name).toBe('mainGroup'); - expect((formItems?.[1] as any)?.name).toBe('mainGroup'); - expect(formItems?.[1]?.visible).toBe(false); - }); + scheduler.showAppointmentPopup(commonAppointment); - it('should handle empty items array in object config', async () => { - const { scheduler, POM } = await createScheduler({ - ...getDefaultConfig(), - editing: { - allowAdding: true, - allowUpdating: true, - form: { - items: [{ - name: 'mainGroup', - items: [], - }], + const maxWidth = POM.popup.component.option('maxWidth'); + expect(maxWidth).toBe(500); + }); + + it('should use custom width as maxWidth when maxWidth is not specified', async () => { + const { scheduler, POM } = await createScheduler({ + ...getDefaultConfig(), + editing: { + allowAdding: true, + allowUpdating: true, + popup: { + width: 600, + }, }, - }, + }); + + scheduler.showAppointmentPopup(commonAppointment); + + const width = POM.popup.component.option('width'); + expect(width).toBe(600); + + const maxWidth = POM.popup.component.option('maxWidth'); + expect(maxWidth).toBe(600); }); - scheduler.showAppointmentPopup(commonAppointment); + it('should use maxWidth option value (not width) for maxWidth when both maxWidth and width are specified', async () => { + const { scheduler, POM } = await createScheduler({ + ...getDefaultConfig(), + editing: { + allowAdding: true, + allowUpdating: true, + popup: { + width: 600, + maxWidth: 500, + }, + }, + }); - const { dxForm: form } = POM.popup; - const formItems = form.option('items') as FormItem[]; - const mainGroup = formItems?.[0] as any; - expect(mainGroup?.items?.length).toBe(0); + scheduler.showAppointmentPopup(commonAppointment); + + const width = POM.popup.component.option('width'); + expect(width).toBe(600); + + const maxWidth = POM.popup.component.option('maxWidth'); + expect(maxWidth).toBe(500); + }); }); }); }); + +describe('Appointment Popup Content', () => { + it.todo('appointmentPopup should not prevent mouse/touch events by default (T968188)'); + it.todo('showAppointmentPopup should render a popup only once'); + it.todo('showAppointmentPopup should work correctly after scheduler repainting'); + it.todo('changing editing should work correctly after showing popup'); + it.todo('showAppointmentPopup should render a popup form only once'); + it.todo('showAppointmentPopup should render a popup content only once'); + it.todo('Recurrence editor should has right startDate after form items change'); + it.todo('There are no exceptions when select date on the appointment popup,if dates are undefined'); + it.todo('Validate works always before done click'); + it.todo('Load panel should not be shown if validation is fail'); + it.todo('Load panel should be hidden if event validation fail'); + it.todo('Load panel should be hidden at the second appointment form opening'); +}); + +describe('Timezone Editors', () => { + it.todo('timeZone editors should have correct options'); + it.todo('timeZone editor should have correct display value for timezones with different offsets'); + it.todo('dataSource of timezoneEditor should be filtered'); +}); 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 b638a730c170..4d93cde96b41 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 @@ -1,12 +1,17 @@ import { afterEach, beforeEach, describe, expect, it, jest, } from '@jest/globals'; +import { loadMessages, locale } from '@js/localization'; +import type { GroupItem } from '@js/ui/form'; +import { fireEvent } from '@testing-library/dom'; +import DOMComponent from '@ts/core/widget/dom_component'; import fx from '../../../common/core/animation/fx'; import { createAppointmentPopup, disposeAppointmentPopups, } from '../__tests__/__mock__/create_appointment_popup'; +import { setupSchedulerTestEnvironment } from '../__tests__/__mock__/m_mock_scheduler'; describe('Isolated AppointmentPopup environment', () => { beforeEach(() => { @@ -350,4 +355,587 @@ describe('Isolated AppointmentPopup environment', () => { expect(recurrenceStartFDOW).toBe(1); expect(weekDayButtonsText).toBe('MTWTFSS'); }); + + describe('Validation', () => { + const commonAppointment = { + text: 'common-app', + startDate: new Date(2017, 4, 9, 9, 30), + endDate: new Date(2017, 4, 9, 11), + }; + + it.each([ + 'startDateEditor', 'startTimeEditor', 'endDateEditor', 'endTimeEditor', + ])('should block save when %s is empty', async (editorName) => { + const { POM, callbacks } = await createAppointmentPopup({ + appointmentData: { ...commonAppointment }, + }); + + POM.setInputValue(editorName, null); + POM.saveButton.click(); + await Promise.resolve(); + + expect(callbacks.onSave).not.toHaveBeenCalled(); + }); + + it.each([ + 'startTimeEditor', 'endDateEditor', 'endTimeEditor', + ])('should block save in recurrence form when %s is empty', async (editorName) => { + const { POM, callbacks } = await createAppointmentPopup({ + appointmentData: { ...commonAppointment }, + }); + + POM.setInputValue(editorName, null); + POM.selectRepeatValue('daily'); + POM.saveButton.click(); + await Promise.resolve(); + + expect(callbacks.onSave).not.toHaveBeenCalled(); + }); + + it('should not block save in recurrence form when startDateEditor is empty', async () => { + const { POM, callbacks } = await createAppointmentPopup({ + appointmentData: { ...commonAppointment }, + }); + + POM.setInputValue('startDateEditor', null); + POM.selectRepeatValue('daily'); + + const recurrenceStartDate = POM.getInputValue('recurrenceStartDateEditor'); + + expect(recurrenceStartDate).toBe('5/9/2017'); + + POM.saveButton.click(); + await Promise.resolve(); + + expect(callbacks.onSave).toHaveBeenCalledTimes(1); + }); + }); + + describe('Recurrence Form', () => { + const recurringAppointment = { + text: 'recurring-app', + startDate: new Date(2017, 4, 1, 9, 30), + endDate: new Date(2017, 4, 1, 11), + recurrenceRule: 'FREQ=DAILY;COUNT=5', + }; + + const baseAppointment = { + text: 'Meeting', + startDate: new Date(2017, 4, 1, 10, 30), + endDate: new Date(2017, 4, 1, 11), + }; + + it('should allow opening recurrence settings when allowUpdating is false', async () => { + const { POM } = await createAppointmentPopup({ + appointmentData: { ...recurringAppointment }, + editing: { allowUpdating: false }, + readOnly: true, + }); + + const visibleBefore = POM.isRecurrenceGroupVisible(); + + POM.recurrenceSettingsButton.click(); + + const visibleAfter = POM.isRecurrenceGroupVisible(); + + expect(visibleBefore).toBe(false); + expect(visibleAfter).toBe(true); + }); + + it('should close repeat selectbox popup when navigating to recurrence group via settings button', async () => { + const { POM } = await createAppointmentPopup({ + appointmentData: { ...recurringAppointment }, + }); + + const repeatEditor = POM.dxForm.getEditor('repeatEditor'); + POM.getInput('repeatEditor').click(); + + const openedBefore = repeatEditor?.option('opened'); + + POM.recurrenceSettingsButton.click(); + + const openedAfter = repeatEditor?.option('opened'); + + expect(openedBefore).toBe(true); + expect(openedAfter).toBe(false); + }); + + it('should have disabled week day buttons when allowUpdating is false', async () => { + const { POM } = await createAppointmentPopup({ + appointmentData: { ...recurringAppointment, recurrenceRule: 'FREQ=WEEKLY;BYDAY=WE,TU,TH,FR,SA' }, + editing: { allowUpdating: false }, + readOnly: true, + }); + + POM.recurrenceSettingsButton.click(); + + const disabledButtons = POM.recurrenceWeekDayButtons.querySelectorAll('.dx-button.dx-state-disabled'); + + expect(disabledButtons.length).toBe(7); + }); + + it('should show recurrence group when repeat value is selected', async () => { + const { POM } = await createAppointmentPopup(); + + POM.selectRepeatValue('weekly'); + + expect(POM.isMainGroupVisible()).toBe(false); + expect(POM.isRecurrenceGroupVisible()).toBe(true); + }); + + it('should restore main group when back button is clicked', async () => { + const { POM } = await createAppointmentPopup(); + POM.selectRepeatValue('weekly'); + + POM.backButton.click(); + + expect(POM.isMainGroupVisible()).toBe(true); + expect(POM.isRecurrenceGroupVisible()).toBe(false); + }); + + it('should set inert attribute on hidden group when switching forms', async () => { + const { POM } = await createAppointmentPopup(); + + POM.selectRepeatValue('weekly'); + + expect(POM.mainGroup.getAttribute('inert')).toBe('true'); + expect(POM.recurrenceGroup.getAttribute('inert')).toBeNull(); + + POM.backButton.click(); + + expect(POM.mainGroup.getAttribute('inert')).toBeNull(); + expect(POM.recurrenceGroup.getAttribute('inert')).toBe('true'); + }); + + it('should adjust popup height when switching to recurrence form', async () => { + const { POM } = await createAppointmentPopup(); + + POM.selectRepeatValue('weekly'); + + expect(typeof POM.component.option('height')).toBe('number'); + }); + + it('should reset popup height to auto when returning to main form', async () => { + const { POM } = await createAppointmentPopup(); + POM.selectRepeatValue('weekly'); + + POM.backButton.click(); + + expect(POM.component.option('height')).toBe('auto'); + }); + + it('should open main form when opening recurring appointment', async () => { + const { POM } = await createAppointmentPopup({ + appointmentData: { ...recurringAppointment }, + }); + + expect(POM.isMainGroupVisible()).toBe(true); + expect(POM.isRecurrenceGroupVisible()).toBe(false); + }); + + describe('State', () => { + it('should have correct input values for appointment with hour frequency', async () => { + const { POM } = await createAppointmentPopup({ + appointmentData: { ...baseAppointment, recurrenceRule: 'FREQ=HOURLY;INTERVAL=2;COUNT=10' }, + }); + + expect(POM.getInputValue('repeatEditor')).toBe('Hourly'); + + POM.recurrenceSettingsButton.click(); + + expect(POM.getInputValue('recurrenceStartDateEditor')).toBe('5/1/2017'); + expect(POM.getInputValue('recurrenceCountEditor')).toBe('2'); + expect(POM.getInputValue('recurrencePeriodEditor')).toBe('Hour(s)'); + expect(POM.getInputValue('recurrenceEndCountEditor')).toBe('10 occurrence(s)'); + }); + + it('should have correct input values for appointment with daily frequency', async () => { + const { POM } = await createAppointmentPopup({ + appointmentData: { ...baseAppointment, recurrenceRule: 'FREQ=DAILY;INTERVAL=2;COUNT=10' }, + }); + + expect(POM.getInputValue('repeatEditor')).toBe('Daily'); + + POM.recurrenceSettingsButton.click(); + + expect(POM.getInputValue('recurrenceStartDateEditor')).toBe('5/1/2017'); + expect(POM.getInputValue('recurrenceCountEditor')).toBe('2'); + expect(POM.getInputValue('recurrencePeriodEditor')).toBe('Day(s)'); + expect(POM.getInputValue('recurrenceEndCountEditor')).toBe('10 occurrence(s)'); + }); + + it('should have correct input values for appointment with week frequency', async () => { + const { POM } = await createAppointmentPopup({ + appointmentData: { ...baseAppointment, recurrenceRule: 'FREQ=WEEKLY;INTERVAL=2;BYDAY=MO,WE,FR;COUNT=10' }, + }); + + expect(POM.getInputValue('repeatEditor')).toBe('Weekly'); + + POM.recurrenceSettingsButton.click(); + + expect(POM.getInputValue('recurrenceStartDateEditor')).toBe('5/1/2017'); + expect(POM.getInputValue('recurrenceCountEditor')).toBe('2'); + expect(POM.getInputValue('recurrencePeriodEditor')).toBe('Week(s)'); + expect(POM.getWeekDaysSelection()).toEqual([false, true, false, true, false, true, false]); + expect(POM.getInputValue('recurrenceEndCountEditor')).toBe('10 occurrence(s)'); + }); + + it('should have correct input values for appointment with monthly frequency', async () => { + const { POM } = await createAppointmentPopup({ + appointmentData: { ...baseAppointment, recurrenceRule: 'FREQ=MONTHLY;INTERVAL=2;BYMONTHDAY=1;COUNT=10' }, + }); + + expect(POM.getInputValue('repeatEditor')).toBe('Monthly'); + + POM.recurrenceSettingsButton.click(); + + expect(POM.getInputValue('recurrenceStartDateEditor')).toBe('5/1/2017'); + expect(POM.getInputValue('recurrenceCountEditor')).toBe('2'); + expect(POM.getInputValue('recurrencePeriodEditor')).toBe('Month(s)'); + expect(POM.getInputValue('recurrenceDayOfMonthEditor')).toBe('1'); + expect(POM.getInputValue('recurrenceEndCountEditor')).toBe('10 occurrence(s)'); + }); + + it('should have correct input values for appointment with yearly frequency', async () => { + const { POM } = await createAppointmentPopup({ + appointmentData: { ...baseAppointment, recurrenceRule: 'FREQ=YEARLY;INTERVAL=2;BYMONTHDAY=1;BYMONTH=5;COUNT=10' }, + }); + + expect(POM.getInputValue('repeatEditor')).toBe('Yearly'); + + POM.recurrenceSettingsButton.click(); + + expect(POM.getInputValue('recurrenceStartDateEditor')).toBe('5/1/2017'); + expect(POM.getInputValue('recurrenceCountEditor')).toBe('2'); + expect(POM.getInputValue('recurrencePeriodEditor')).toBe('Year(s)'); + expect(POM.getInputValue('recurrenceDayOfYearDayEditor')).toBe('1'); + expect(POM.getInputValue('recurrenceDayOfYearMonthEditor')).toBe('May'); + expect(POM.getInputValue('recurrenceEndCountEditor')).toBe('10 occurrence(s)'); + }); + + it('T1325870: should use current locale for recurrence editors after locale change', async () => { + const currentLocale = locale(); + + loadMessages({ + de: { + 'dxScheduler-recurrenceYearly': 'custom yearly', + 'dxScheduler-recurrenceRepeatYearly': 'custom repeat yearly', + }, + }); + locale('de'); + + try { + const { POM } = await createAppointmentPopup({ + appointmentData: { ...baseAppointment, recurrenceRule: 'FREQ=YEARLY;INTERVAL=2;BYMONTHDAY=1;BYMONTH=5;COUNT=10' }, + }); + + expect(POM.getInputValue('repeatEditor')).toBe('custom yearly'); + + POM.recurrenceSettingsButton.click(); + + expect(POM.getInputValue('recurrencePeriodEditor')).toBe('Custom repeat yearly'); + expect(POM.getInputValue('recurrenceDayOfYearMonthEditor')).toBe('Mai'); + } finally { + locale(currentLocale); + } + }); + + it('should have correct input values for appointment with no end', async () => { + const { POM } = await createAppointmentPopup({ + appointmentData: { ...baseAppointment, recurrenceRule: 'FREQ=DAILY;INTERVAL=2' }, + }); + + POM.recurrenceSettingsButton.click(); + + expect(POM.getInputValue('recurrenceRepeatEndEditor')).toBe('never'); + }); + + it('should have correct input values for appointment with end by date', async () => { + const { POM } = await createAppointmentPopup({ + appointmentData: { ...baseAppointment, recurrenceRule: 'FREQ=DAILY;INTERVAL=2;UNTIL=20170601T000000Z' }, + }); + + POM.recurrenceSettingsButton.click(); + + expect(POM.getInputValue('recurrenceRepeatEndEditor')).toBe('until'); + expect(POM.getInputValue('recurrenceEndUntilEditor')).toBe('6/1/2017'); + }); + + it('should have correct input values for appointment with end by count', async () => { + const { POM } = await createAppointmentPopup({ + appointmentData: { ...baseAppointment, recurrenceRule: 'FREQ=DAILY;INTERVAL=2;COUNT=10' }, + }); + + POM.recurrenceSettingsButton.click(); + + expect(POM.getInputValue('recurrenceRepeatEndEditor')).toBe('count'); + expect(POM.getInputValue('recurrenceEndCountEditor')).toBe('10 occurrence(s)'); + }); + }); + + describe('Repeat End Values Preservation', () => { + it('should preserve count value when switching between recurrence types', async () => { + const testCount = 15; + const { POM } = await createAppointmentPopup({ + appointmentData: { ...baseAppointment }, + }); + + POM.selectRepeatValue('daily'); + + POM.setInputValue('recurrenceRepeatEndEditor', 'count'); + POM.setInputValue('recurrenceEndCountEditor', testCount); + + POM.backButton.click(); + + POM.selectRepeatValue('weekly'); + POM.recurrenceSettingsButton.click(); + + expect(POM.getInputValue('recurrenceEndCountEditor')).toBe(`${testCount} occurrence(s)`); + }); + + it('should preserve until value when switching between recurrence types', async () => { + const testUntilDate = new Date(2017, 5, 16); + const { POM } = await createAppointmentPopup({ + appointmentData: { ...baseAppointment }, + }); + + POM.selectRepeatValue('daily'); + + POM.setInputValue('recurrenceRepeatEndEditor', 'until'); + POM.setInputValue('recurrenceEndUntilEditor', testUntilDate); + + POM.backButton.click(); + + POM.selectRepeatValue('weekly'); + POM.recurrenceSettingsButton.click(); + + expect(POM.getInputValue('recurrenceEndUntilEditor')).toBe('6/16/2017'); + }); + }); + + describe('Repeat End Editors Disabled State', () => { + it.each([ + ['never', 'FREQ=DAILY'], + ['until', 'FREQ=DAILY;UNTIL=20170615T000000Z'], + ['count', 'FREQ=DAILY;COUNT=10'], + ] as ['never' | 'until' | 'count', string][])( + 'should set correct disabled state when repeatEnd is %s', + async (repeatEndValue, recurrenceRule) => { + const { POM } = await createAppointmentPopup({ + appointmentData: { ...baseAppointment, recurrenceRule }, + }); + + POM.recurrenceSettingsButton.click(); + + const untilEditor = POM.dxForm.getEditor('recurrenceEndUntilEditor'); + const countEditor = POM.dxForm.getEditor('recurrenceEndCountEditor'); + + expect(untilEditor?.option('disabled')).toBe(repeatEndValue !== 'until'); + expect(countEditor?.option('disabled')).toBe(repeatEndValue !== 'count'); + }, + ); + }); + + describe('FrequencyEditor focus', () => { + afterEach(() => { + jest.useRealTimers(); + }); + + it('should not be focused when value is changed via API', async () => { + const { POM } = await createAppointmentPopup({ + appointmentData: { ...recurringAppointment }, + }); + + POM.recurrenceSettingsButton.click(); + + const frequencyEditor = POM.dxForm.getEditor('recurrencePeriodEditor'); + const frequencyEditorInputElement = POM.getInput('recurrencePeriodEditor'); + + frequencyEditor?.option('value', 'yearly'); + + expect(document.activeElement).not.toBe(frequencyEditorInputElement); + }); + + it('should be focused when value is changed via keyboard', async () => { + const { POM } = await createAppointmentPopup({ + appointmentData: { ...recurringAppointment }, + }); + + POM.recurrenceSettingsButton.click(); + + const frequencyEditorInputElement = POM.getInput('recurrencePeriodEditor'); + + frequencyEditorInputElement.click(); + jest.useFakeTimers(); + fireEvent.keyDown(frequencyEditorInputElement, { key: 'ArrowDown' }); + jest.runAllTimers(); + + expect(document.activeElement).toBe(frequencyEditorInputElement); + }); + }); + + it('should set animation offset CSS variable when switching to recurrence form', async () => { + const originalGetComputedStyle = window.getComputedStyle; + const originalGetBoundingClientRect = Element.prototype.getBoundingClientRect; + const originalGetClientRects = Element.prototype.getClientRects; + const originalIsVisible = DOMComponent.prototype._isVisible; + + try { + setupSchedulerTestEnvironment({ + height: 600, + classRects: { + 'dx-form': { top: 10 }, + 'dx-scheduler-form-main-group': { top: 60 }, + }, + }); + + const { POM } = await createAppointmentPopup(); + POM.selectRepeatValue('weekly'); + + const animationTop = POM.dxForm.$element()[0].style.getPropertyValue('--dx-scheduler-animation-top'); + expect(animationTop).toBe('50px'); + } finally { + window.getComputedStyle = originalGetComputedStyle; + Element.prototype.getBoundingClientRect = originalGetBoundingClientRect; + Element.prototype.getClientRects = originalGetClientRects; + DOMComponent.prototype._isVisible = originalIsVisible; + } + }); + + it('T1318550: editors with hidden outer label must have labelMode: hidden', async () => { + const { POM } = await createAppointmentPopup({ + appointmentData: { ...baseAppointment, recurrenceRule: 'FREQ=YEARLY;BYMONTHDAY=1;BYMONTH=5' }, + }); + + POM.recurrenceSettingsButton.click(); + + const editorsWithHiddenLabel = [ + 'recurrencePeriodEditor', + 'recurrenceRepeatEndEditor', + 'recurrenceEndUntilEditor', + 'recurrenceEndCountEditor', + 'recurrenceDayOfYearDayEditor', + ]; + + editorsWithHiddenLabel.forEach((name) => { + expect(POM.dxForm.getEditor(name)?.option('labelMode')).toBe('hidden'); + }); + }); + }); + + describe('Customize form items', () => { + const appointment = { + text: 'Meeting', + startDate: new Date(2017, 4, 9, 9, 30), + endDate: new Date(2017, 4, 9, 11), + }; + + it('should use default form items when editing.form.items is not configured', async () => { + const { POM } = await createAppointmentPopup({ appointmentData: { ...appointment } }); + + const formItems = POM.dxForm.option('items') ?? []; + + expect(formItems.length).toBeGreaterThan(0); + }); + + it('should produce empty form when editing.form.items is empty array', async () => { + const { POM } = await createAppointmentPopup({ + appointmentData: { ...appointment }, + editing: { form: { items: [] } }, + }); + + const formItems = POM.dxForm.option('items') ?? []; + + expect(formItems.length).toBe(0); + }); + + it('should resolve named group when specified as string in items array', async () => { + const { POM } = await createAppointmentPopup({ + appointmentData: { ...appointment }, + editing: { form: { items: ['mainGroup'] } }, + }); + + const formItems = POM.dxForm.option('items') ?? []; + + expect(formItems.length).toBe(1); + expect(formItems[0]?.name).toBe('mainGroup'); + }); + + it.each([true, false])('should set group visibility to %s when specified in object config', async (visible) => { + const { POM } = await createAppointmentPopup({ + appointmentData: { ...appointment }, + editing: { form: { items: [{ name: 'mainGroup', visible }] } }, + }); + + const formItems = POM.dxForm.option('items') ?? []; + + expect(formItems[0]?.visible).toBe(visible); + }); + + it('should filter group children to specified named items', async () => { + const { POM } = await createAppointmentPopup({ + appointmentData: { ...appointment }, + editing: { form: { items: [{ name: 'mainGroup', items: ['subjectGroup', 'dateGroup'] }] } }, + }); + + const mainGroup = (POM.dxForm.option('items') ?? [])[0] as GroupItem; + + expect(mainGroup?.items?.length).toBe(2); + expect(mainGroup?.items?.[0]?.name).toBe('subjectGroup'); + expect(mainGroup?.items?.[1]?.name).toBe('dateGroup'); + }); + + it('should produce empty children when items array in group config is empty', async () => { + const { POM } = await createAppointmentPopup({ + appointmentData: { ...appointment }, + editing: { form: { items: [{ name: 'mainGroup', items: [] }] } }, + }); + + const mainGroup = (POM.dxForm.option('items') ?? [])[0] as GroupItem; + + expect(mainGroup?.items?.length).toBe(0); + }); + + it('should create placeholder item for non-existent group name', async () => { + const { POM } = await createAppointmentPopup({ + appointmentData: { ...appointment }, + editing: { form: { items: ['nonExistentGroup'] } }, + }); + + const formItems = POM.dxForm.option('items') ?? []; + + expect(formItems.length).toBe(1); + }); + + it('should handle mixed string and object items', async () => { + const { POM } = await createAppointmentPopup({ + appointmentData: { ...appointment }, + editing: { form: { items: ['mainGroup', { name: 'mainGroup', visible: false }] } }, + }); + + const formItems = POM.dxForm.option('items') ?? []; + + expect(formItems.length).toBe(2); + expect(formItems[0]?.name).toBe('mainGroup'); + expect(formItems[1]?.name).toBe('mainGroup'); + expect(formItems[1]?.visible).toBe(false); + }); + + it('should call custom onContentReady and onInitialized form callbacks', async () => { + const onContentReady = jest.fn(); + const onInitialized = jest.fn(); + + const { POM } = await createAppointmentPopup({ + editing: { form: { onContentReady, onInitialized } }, + }); + + expect(onContentReady).toHaveBeenCalled(); + expect(onInitialized).toHaveBeenCalled(); + + POM.selectRepeatValue('weekly'); + + expect(POM.isMainGroupVisible()).toBe(false); + expect(POM.isRecurrenceGroupVisible()).toBe(true); + }); + }); });