diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/deleteAppointments.ts b/e2e/testcafe-devextreme/tests/scheduler/common/deleteAppointments.ts deleted file mode 100644 index a68b8a792f7f..000000000000 --- a/e2e/testcafe-devextreme/tests/scheduler/common/deleteAppointments.ts +++ /dev/null @@ -1,102 +0,0 @@ -import Scheduler from 'devextreme-testcafe-models/scheduler'; -import { createWidget } from '../../../helpers/createWidget'; -import url from '../../../helpers/getPageUrl'; - -fixture.disablePageReloads`Delete appointments` - .page(url(__dirname, '../../container.html')); - -const createRecurrenceData = (): Record[] => [{ - Text: 'Text', - StartDate: new Date(2017, 4, 22, 1, 30, 0, 0), - EndDate: new Date(2017, 4, 22, 2, 30, 0, 0), - RecurrenceRule: 'FREQ=DAILY', -}]; - -const createScheduler = async (data): Promise => { - await createWidget('dxScheduler', { - dataSource: data, - views: ['week'], - currentView: 'week', - currentDate: new Date(2017, 4, 22), - textExpr: 'Text', - startDateExpr: 'StartDate', - endDateExpr: 'EndDate', - allDayExpr: 'AllDay', - recurrenceRuleExpr: 'RecurrenceRule', - recurrenceExceptionExpr: 'RecurrenceException', - }); -}; - -const createSimpleData = (): Record[] => [{ - Text: 'Text', - StartDate: new Date(2017, 4, 22, 1, 30, 0, 0), - EndDate: new Date(2017, 4, 22, 2, 30, 0, 0), -}, { - Text: 'Text2', - StartDate: new Date(2017, 4, 22, 12, 0, 0, 0), - EndDate: new Date(2017, 4, 22, 13, 0, 0, 0), -}]; - -test('Recurrence appointments should be deleted by click on \'delete\' button', async (t) => { - const scheduler = new Scheduler('#container'); - - await t - .expect(scheduler.getAppointmentCount()).eql(6) - .click(scheduler.getAppointment('Text', 3).element) - - .expect(scheduler.appointmentTooltip.element.exists) - .ok() - .click(scheduler.appointmentTooltip.deleteButton) - .click(Scheduler.getDeleteRecurrenceDialog().appointment) - .wait(100) - - .expect(scheduler.getAppointmentCount()) - .eql(5); - - await t - .click(scheduler.getAppointment('Text', 3).element) - - .click(scheduler.appointmentTooltip.deleteButton) - .click(Scheduler.getDeleteRecurrenceDialog().series) - - .expect(scheduler.getAppointmentCount()) - .eql(0); -}).before(async () => createScheduler(createRecurrenceData())); - -test('Recurrence appointments should be deleted by press \'delete\' key', async (t) => { - const scheduler = new Scheduler('#container'); - - await t - .expect(scheduler.getAppointmentCount()).eql(6) - .click(scheduler.getAppointment('Text', 3).element) - .pressKey('delete') - .click(Scheduler.getDeleteRecurrenceDialog().appointment) - .wait(100) - .expect(scheduler.getAppointmentCount()) - .eql(5); - - await t - .click(scheduler.getAppointment('Text', 3).element) - .pressKey('delete') - .click(Scheduler.getDeleteRecurrenceDialog().series) - .expect(scheduler.getAppointmentCount()) - .eql(0); -}).before(async () => createScheduler(createRecurrenceData())); - -test('Common appointments should be deleted by click on \'delete\' button and press \'delete\' key', async (t) => { - const scheduler = new Scheduler('#container'); - - await t - .expect(scheduler.getAppointmentCount()).eql(2) - .click(scheduler.getAppointment('Text').element) - .click(scheduler.appointmentTooltip.deleteButton) - .expect(scheduler.getAppointmentCount()) - .eql(1); - - await t - .expect(scheduler.getAppointmentCount()).eql(1) - .click(scheduler.getAppointment('Text2').element) - .pressKey('delete') - .expect(scheduler.getAppointmentCount()) - .eql(0); -}).before(async () => createScheduler(createSimpleData())); diff --git a/packages/devextreme/js/__internal/scheduler/__tests__/__mock__/model/popup.ts b/packages/devextreme/js/__internal/scheduler/__tests__/__mock__/model/popup.ts index d901240db91c..dd0757e23655 100644 --- a/packages/devextreme/js/__internal/scheduler/__tests__/__mock__/model/popup.ts +++ b/packages/devextreme/js/__internal/scheduler/__tests__/__mock__/model/popup.ts @@ -72,10 +72,18 @@ export class PopupModel { return this.queries.getByRole('button', { name: 'Edit series' }) as HTMLElement; } + get deleteSeriesButton(): HTMLElement { + return this.queries.getByRole('button', { name: 'Delete series' }) as HTMLElement; + } + get editAppointmentButton(): HTMLElement { return this.queries.getByRole('button', { name: 'Edit appointment' }) as HTMLElement; } + get deleteAppointmentButton(): HTMLElement { + return this.queries.getByRole('button', { name: 'Delete appointment' }) as HTMLElement; + } + get recurrenceSettingsButton(): HTMLElement { return queryRequiredElement(this.element, '.dx-scheduler-form-recurrence-settings-button'); } diff --git a/packages/devextreme/js/__internal/scheduler/__tests__/__mock__/model/scheduler.ts b/packages/devextreme/js/__internal/scheduler/__tests__/__mock__/model/scheduler.ts index 946fb15ad4ea..fecaf1f7b029 100644 --- a/packages/devextreme/js/__internal/scheduler/__tests__/__mock__/model/scheduler.ts +++ b/packages/devextreme/js/__internal/scheduler/__tests__/__mock__/model/scheduler.ts @@ -6,6 +6,7 @@ import { POPUP_DIALOG_CLASS } from '../../../m_scheduler'; import type { AppointmentModel } from './appointment'; import { createAppointmentModel } from './appointment'; import { PopupModel } from './popup'; +import { TooltipModel } from './tooltip'; const getTexts = ( cells: ArrayLike, @@ -25,6 +26,10 @@ export class SchedulerModel { return this.getPopup(); } + get tooltip(): TooltipModel { + return new TooltipModel(); + } + get toolbar(): ToolbarModel { return new ToolbarModel(this.queries.getByRole('toolbar')); } @@ -55,6 +60,17 @@ export class SchedulerModel { return getTexts(collectors); } + getCollectorButton(index = 0): HTMLElement { + const allButtons = this.queries.queryAllByRole('button') as HTMLElement[]; + const collectors = allButtons.filter((btn) => btn.classList.contains('dx-scheduler-appointment-collector')); + + if (collectors.length === 0) { + throw new Error('Collector button not found'); + } + + return collectors[index]; + } + getDateTableContent(): string[] { const cells = this.container.querySelectorAll('.dx-scheduler-date-table-cell'); return getTexts(cells); diff --git a/packages/devextreme/js/__internal/scheduler/__tests__/__mock__/model/tooltip.ts b/packages/devextreme/js/__internal/scheduler/__tests__/__mock__/model/tooltip.ts new file mode 100644 index 000000000000..6444af4f4e57 --- /dev/null +++ b/packages/devextreme/js/__internal/scheduler/__tests__/__mock__/model/tooltip.ts @@ -0,0 +1,38 @@ +import { within } from '@testing-library/dom'; + +const TOOLTIP_WRAPPER_SELECTOR = '.dx-overlay-wrapper.dx-scheduler-appointment-tooltip-wrapper'; + +export class TooltipModel { + private get element(): HTMLElement | null { + return document.querySelector(TOOLTIP_WRAPPER_SELECTOR); + } + + isVisible(): boolean { + return this.element !== null; + } + + getScrollableContent(): Element | null { + return this.element?.querySelector('.dx-scrollable .dx-scrollview-content') ?? null; + } + + getDeleteButton(index = 0): HTMLElement { + const tooltip = this.element; + const buttons = tooltip + ? within(tooltip).queryAllByRole('button').filter((btn) => btn.classList.contains('dx-tooltip-appointment-item-delete-button')) + : []; + + if (buttons.length === 0) { + throw new Error('Tooltip delete button not found'); + } + + return buttons[index]; + } + + getAppointmentItem(index = 0): HTMLElement | null { + const tooltip = this.element; + if (!tooltip) { + return null; + } + return within(tooltip).queryAllByRole('option')[index] ?? null; + } +} diff --git a/packages/devextreme/js/__internal/scheduler/__tests__/appointment_tooltip.test.ts b/packages/devextreme/js/__internal/scheduler/__tests__/appointment_tooltip.test.ts new file mode 100644 index 000000000000..39ede355bea2 --- /dev/null +++ b/packages/devextreme/js/__internal/scheduler/__tests__/appointment_tooltip.test.ts @@ -0,0 +1,218 @@ +import { + afterEach, beforeEach, describe, expect, it, jest, +} from '@jest/globals'; +import fx from '@js/common/core/animation/fx'; + +import { createScheduler } from './__mock__/create_scheduler'; +import { setupSchedulerTestEnvironment } from './__mock__/m_mock_scheduler'; + +describe('Appointment tooltip behavior', () => { + beforeEach(() => { + fx.off = true; + setupSchedulerTestEnvironment(); + }); + + afterEach(() => { + fx.off = false; + jest.useRealTimers(); + document.body.innerHTML = ''; + }); + + describe('Deleting appointments', () => { + it('should delete appointment by Delete key when focused in tooltip from collector', async () => { + const data = [ + { + text: 'Apt1', + startDate: new Date(2017, 4, 22, 9, 30), + endDate: new Date(2017, 4, 22, 10, 30), + }, + { + text: 'Apt2', + startDate: new Date(2017, 4, 22, 9, 30), + endDate: new Date(2017, 4, 22, 10, 30), + }, + ]; + + const { scheduler, POM } = await createScheduler({ + dataSource: [...data], + views: [{ type: 'month', maxAppointmentsPerCell: 1 }], + currentView: 'month', + currentDate: new Date(2017, 4, 22), + height: 600, + }); + + POM.getCollectorButton().click(); + + const tooltipScrollableContent = POM.tooltip.getScrollableContent(); + tooltipScrollableContent?.dispatchEvent(new FocusEvent('focusin', { bubbles: true })); + tooltipScrollableContent?.dispatchEvent(new KeyboardEvent('keydown', { key: 'Delete', bubbles: true })); + + expect(POM.tooltip.isVisible()).toBe(false); + expect((scheduler as any).getDataSource().items()).toEqual([data[0]]); + }); + + it('should delete appointment on delete button click in tooltip', async () => { + const data = [ + { + text: 'Apt1', + startDate: new Date(2017, 4, 22, 9, 30), + endDate: new Date(2017, 4, 22, 10, 30), + }, + { + text: 'Apt2', + startDate: new Date(2017, 4, 22, 9, 30), + endDate: new Date(2017, 4, 22, 10, 30), + }, + ]; + + const { POM, scheduler } = await createScheduler({ + dataSource: [...data], + views: [{ type: 'month', maxAppointmentsPerCell: 1 }], + currentView: 'month', + currentDate: new Date(2017, 4, 22), + height: 600, + }); + + POM.getCollectorButton().click(); + POM.tooltip.getDeleteButton().click(); + + expect(POM.tooltip.isVisible()).toBe(false); + expect((scheduler as any).getDataSource().items()).toEqual([data[0]]); + }); + + it('should not delete appointment by Delete key when editing.allowDeleting=false', async () => { + const data = [ + { + text: 'Apt1', + startDate: new Date(2017, 4, 22, 9, 30), + endDate: new Date(2017, 4, 22, 10, 30), + }, + { + text: 'Apt2', + startDate: new Date(2017, 4, 22, 9, 30), + endDate: new Date(2017, 4, 22, 10, 30), + }, + ]; + + const { POM, scheduler } = await createScheduler({ + dataSource: [...data], + views: [{ type: 'month', maxAppointmentsPerCell: 1 }], + currentView: 'month', + currentDate: new Date(2017, 4, 22), + height: 600, + editing: { + allowDeleting: false, + }, + }); + + POM.getCollectorButton().click(); + + const tooltipScrollableContent = POM.tooltip.getScrollableContent(); + tooltipScrollableContent?.dispatchEvent(new FocusEvent('focusin', { bubbles: true })); + tooltipScrollableContent?.dispatchEvent(new KeyboardEvent('keydown', { key: 'Delete', bubbles: true })); + + expect((scheduler as any).getDataSource().items()).toEqual([...data]); + }); + + it('should not delete disabled appointment by Delete key when focused in tooltip from collector', async () => { + const data = [ + { + text: 'Apt1', + startDate: new Date(2017, 4, 22, 9, 30), + endDate: new Date(2017, 4, 22, 10, 30), + }, + { + text: 'Apt2', + startDate: new Date(2017, 4, 22, 9, 30), + endDate: new Date(2017, 4, 22, 10, 30), + disabled: true, + }, + ]; + + const { POM, scheduler } = await createScheduler({ + dataSource: [...data], + views: [{ type: 'month', maxAppointmentsPerCell: 1 }], + currentView: 'month', + currentDate: new Date(2017, 4, 22), + height: 600, + }); + + POM.getCollectorButton().click(); + + const tooltipScrollableContent = POM.tooltip.getScrollableContent(); + tooltipScrollableContent?.dispatchEvent(new FocusEvent('focusin', { bubbles: true })); + tooltipScrollableContent?.dispatchEvent(new KeyboardEvent('keydown', { key: 'Delete', bubbles: true })); + + expect((scheduler as any).getDataSource().items()).toEqual([...data]); + }); + + it('should delete single occurrence on delete button click and clicking \'Delete appointment\'', async () => { + const data = [ + { + text: 'Apt1', + startDate: new Date(2017, 4, 22, 9, 30), + endDate: new Date(2017, 4, 22, 10, 30), + }, + { + text: 'Apt2', + startDate: new Date(2017, 4, 22, 9, 30), + endDate: new Date(2017, 4, 22, 10, 30), + recurrenceRule: 'FREQ=DAILY', + }, + ]; + + const { POM, scheduler } = await createScheduler({ + dataSource: [{ ...data[0] }, { ...data[1] }], + views: [{ type: 'month', maxAppointmentsPerCell: 1 }], + currentView: 'month', + currentDate: new Date(2017, 4, 22), + editing: true, + height: 600, + }); + + POM.getCollectorButton().click(); + POM.tooltip.getDeleteButton(0).click(); + POM.popup.deleteAppointmentButton.click(); + + const items = (scheduler as any).getDataSource().items(); + + expect(items).toEqual([ + data[0], + expect.objectContaining(data[1]), + ]); + + expect(items[1].recurrenceException).toContain('20170522'); + }); + + it('should delete all occurrences on delete and clicking \'Delete series\'', async () => { + const data = [ + { + text: 'Apt1', + startDate: new Date(2017, 4, 22, 9, 30), + endDate: new Date(2017, 4, 22, 10, 30), + }, + { + text: 'Apt2', + startDate: new Date(2017, 4, 22, 9, 30), + endDate: new Date(2017, 4, 22, 10, 30), + recurrenceRule: 'FREQ=DAILY', + }, + ]; + + const { POM, scheduler } = await createScheduler({ + dataSource: [{ ...data[0] }, { ...data[1] }], + views: [{ type: 'month', maxAppointmentsPerCell: 1 }], + currentView: 'month', + currentDate: new Date(2017, 4, 22), + editing: true, + height: 600, + }); + + POM.getCollectorButton().click(); + POM.tooltip.getDeleteButton(0).click(); + POM.popup.deleteSeriesButton.click(); + + expect((scheduler as any).getDataSource().items()).toEqual([data[0]]); + }); + }); +}); diff --git a/packages/devextreme/js/__internal/scheduler/__tests__/appointments.test.ts b/packages/devextreme/js/__internal/scheduler/__tests__/appointments.test.ts index e50e51e78830..86bc90b389ad 100644 --- a/packages/devextreme/js/__internal/scheduler/__tests__/appointments.test.ts +++ b/packages/devextreme/js/__internal/scheduler/__tests__/appointments.test.ts @@ -1,16 +1,24 @@ import { afterEach, describe, expect, it, jest, } from '@jest/globals'; +import $ from '@js/core/renderer'; import { createScheduler } from './__mock__/create_scheduler'; import { setupSchedulerTestEnvironment } from './__mock__/m_mock_scheduler'; describe('Appointments', () => { + beforeEach(() => { + setupSchedulerTestEnvironment(); + }); afterEach(() => { + const $scheduler = $('.dx-scheduler'); + // @ts-expect-error + $scheduler.dxScheduler('dispose'); + document.body.innerHTML = ''; jest.useRealTimers(); }); + it('All-day appointment should not be resizable if current view is "day"', async () => { - setupSchedulerTestEnvironment(); const { POM } = await createScheduler({ dataSource: [{ text: 'Appointment 1', @@ -27,7 +35,6 @@ describe('Appointments', () => { }); it('should display "(No subject)" for appointments without title', async () => { - setupSchedulerTestEnvironment({ height: 200 }); const appointmentWithoutTitle = { startDate: new Date(2017, 4, 9, 9, 30), endDate: new Date(2017, 4, 9, 11), @@ -50,7 +57,6 @@ describe('Appointments', () => { }); it('should display "(No subject)" in tooltip for appointments without title', async () => { - setupSchedulerTestEnvironment({ height: 200 }); const appointmentWithoutTitle = { startDate: new Date(2017, 4, 9, 9, 30), endDate: new Date(2017, 4, 9, 11), @@ -81,4 +87,74 @@ describe('Appointments', () => { expect(tooltipTitleElement?.textContent?.trim()).toBe('(No subject)'); } }); + + describe('Keyboard Navigation', () => { + describe('Delete hotkey', () => { + it('should delete single occurrence on Delete and clicking \'Delete appointment\'', async () => { + const { POM, keydown } = await createScheduler({ + dataSource: [{ + text: 'Recurring Appointment', + startDate: new Date(2015, 1, 9, 8), + endDate: new Date(2015, 1, 9, 9), + recurrenceRule: 'FREQ=DAILY', + }], + currentView: 'week', + currentDate: new Date(2015, 1, 9), + }); + + const initialCount = POM.getAppointments().length; + const appointment = POM.getAppointments()[2]; + + appointment.element.focus(); + keydown(appointment.element, 'Delete'); + + POM.popup.deleteAppointmentButton.click(); + + expect(POM.getAppointments().length).toBe(initialCount - 1); + }); + + it('should delete all occurrences on delete and clicking \'Delete series\'', async () => { + const { POM, keydown } = await createScheduler({ + dataSource: [{ + text: 'Recurring Appointment', + startDate: new Date(2015, 1, 9, 8), + endDate: new Date(2015, 1, 9, 9), + recurrenceRule: 'FREQ=DAILY', + }], + editing: true, + currentView: 'week', + currentDate: new Date(2015, 1, 9), + }); + + const appointment = POM.getAppointments()[0]; + + appointment.element.focus(); + keydown(appointment.element, 'Delete'); + + POM.popup.deleteSeriesButton.click(); + + expect(POM.getAppointments().length).toBe(0); + }); + + it('should delete appointment on Delete', async () => { + const { POM, keydown } = await createScheduler({ + dataSource: [{ + text: 'Appointment', + startDate: new Date(2015, 1, 9, 8), + endDate: new Date(2015, 1, 9, 9), + }], + currentView: 'day', + currentDate: new Date(2015, 1, 9), + }); + + const initialCount = POM.getAppointments().length; + const appointment = POM.getAppointments()[0]; + + appointment.element.focus(); + keydown(appointment.element, 'Delete'); + + expect(POM.getAppointments().length).toBe(initialCount - 1); + }); + }); + }); });