Skip to content

Commit f0c663d

Browse files
Scheduler - Create isolated AppointmentPopup test environment (#33003)
1 parent fd9cafd commit f0c663d

3 files changed

Lines changed: 3272 additions & 2978 deletions

File tree

Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
import { jest } from '@jest/globals';
2+
import $ from '@js/core/renderer';
3+
// eslint-disable-next-line devextreme-custom/no-deferred
4+
import { Deferred } from '@js/core/utils/deferred';
5+
6+
import { mockTimeZoneCalculator } from '../../__mock__/timezone_calculator.mock';
7+
import { AppointmentForm } from '../../appointment_popup/m_form';
8+
import {
9+
ACTION_TO_APPOINTMENT,
10+
APPOINTMENT_POPUP_CLASS,
11+
AppointmentPopup,
12+
} from '../../appointment_popup/m_popup';
13+
import {
14+
AppointmentDataAccessor,
15+
} from '../../utils/data_accessor/appointment_data_accessor';
16+
import type { IFieldExpr } from '../../utils/data_accessor/types';
17+
import {
18+
ResourceManager,
19+
} from '../../utils/resource_manager/resource_manager';
20+
import { PopupModel } from './model/popup';
21+
22+
const DEFAULT_FIELDS: IFieldExpr = {
23+
startDateExpr: 'startDate',
24+
endDateExpr: 'endDate',
25+
startDateTimeZoneExpr: 'startDateTimeZone',
26+
endDateTimeZoneExpr: 'endDateTimeZone',
27+
allDayExpr: 'allDay',
28+
textExpr: 'text',
29+
descriptionExpr: 'description',
30+
recurrenceRuleExpr: 'recurrenceRule',
31+
recurrenceExceptionExpr: 'recurrenceException',
32+
disabledExpr: 'disabled',
33+
visibleExpr: 'visible',
34+
};
35+
36+
const DEFAULT_EDITING = {
37+
allowAdding: true,
38+
allowUpdating: true,
39+
allowDeleting: true,
40+
allowResizing: true,
41+
allowDragging: true,
42+
legacyForm: false,
43+
};
44+
45+
const DEFAULT_APPOINTMENT = {
46+
text: 'Test Appointment',
47+
startDate: new Date(2021, 3, 26, 9, 30),
48+
endDate: new Date(2021, 3, 26, 11, 0),
49+
};
50+
51+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
52+
const resolvedDeferred = (): any => {
53+
// @ts-expect-error
54+
// eslint-disable-next-line devextreme-custom/no-deferred
55+
const d = new Deferred();
56+
d.resolve();
57+
return d.promise();
58+
};
59+
60+
interface CreateAppointmentPopupOptions {
61+
appointmentData?: Record<string, unknown>;
62+
action?: number;
63+
editing?: Record<string, unknown>;
64+
firstDayOfWeek?: number;
65+
startDayHour?: number;
66+
onAppointmentFormOpening?: (...args: unknown[]) => void;
67+
addAppointment?: jest.Mock;
68+
updateAppointment?: jest.Mock;
69+
}
70+
71+
interface CreateAppointmentPopupResult {
72+
container: HTMLDivElement;
73+
popup: AppointmentPopup;
74+
form: AppointmentForm;
75+
POM: PopupModel;
76+
callbacks: {
77+
addAppointment: jest.Mock;
78+
updateAppointment: jest.Mock;
79+
focus: jest.Mock;
80+
updateScrollPosition: jest.Mock;
81+
};
82+
dispose: () => void;
83+
}
84+
85+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
86+
function createComponent(element: any, Component: any, opts: any): any {
87+
return new Component($(element), opts);
88+
}
89+
90+
const disposables: (() => void)[] = [];
91+
92+
export const disposeAppointmentPopups = (): void => {
93+
disposables.forEach((fn) => fn());
94+
disposables.length = 0;
95+
document.body.innerHTML = '';
96+
};
97+
98+
export const createAppointmentPopup = async (
99+
options: CreateAppointmentPopupOptions = {},
100+
): Promise<CreateAppointmentPopupResult> => {
101+
const container = document.createElement('div');
102+
document.body.appendChild(container);
103+
104+
const dataAccessors = new AppointmentDataAccessor(DEFAULT_FIELDS, false);
105+
const resourceManager = new ResourceManager([]);
106+
const timeZoneCalculator = mockTimeZoneCalculator;
107+
const editing = { ...DEFAULT_EDITING, ...options.editing };
108+
109+
const addAppointment = options.addAppointment
110+
?? jest.fn(resolvedDeferred);
111+
const updateAppointment = options.updateAppointment
112+
?? jest.fn(resolvedDeferred);
113+
const focus = jest.fn();
114+
const updateScrollPosition = jest.fn();
115+
116+
const formSchedulerProxy = {
117+
getResourceById: (): Record<string, unknown> => resourceManager.resourceById,
118+
getDataAccessors: (): AppointmentDataAccessor => dataAccessors,
119+
createComponent,
120+
getEditingConfig: (): typeof editing => editing,
121+
getResourceManager: (): ResourceManager => resourceManager,
122+
getFirstDayOfWeek: (): number => options.firstDayOfWeek ?? 0,
123+
getStartDayHour: (): number => options.startDayHour ?? 0,
124+
getCalculatedEndDate: (startDate: Date): Date => {
125+
const endDate = new Date(startDate);
126+
endDate.setHours(endDate.getHours() + 1);
127+
return endDate;
128+
},
129+
getTimeZoneCalculator: (): typeof timeZoneCalculator => timeZoneCalculator,
130+
};
131+
132+
const form = new AppointmentForm(formSchedulerProxy);
133+
134+
const noop = (): void => {};
135+
136+
const popupSchedulerProxy = {
137+
getElement: (): ReturnType<typeof $> => $(container),
138+
createComponent,
139+
focus,
140+
getResourceManager: (): ResourceManager => resourceManager,
141+
getEditingConfig: (): typeof editing => editing,
142+
getTimeZoneCalculator: (): typeof timeZoneCalculator => timeZoneCalculator,
143+
getDataAccessors: (): AppointmentDataAccessor => dataAccessors,
144+
getAppointmentFormOpening():
145+
(...args: unknown[]) => void {
146+
return options.onAppointmentFormOpening ?? noop;
147+
},
148+
processActionResult: (
149+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
150+
arg: any,
151+
callback: (canceled: boolean) => void,
152+
): void => {
153+
callback(arg.cancel);
154+
},
155+
addAppointment,
156+
updateAppointment,
157+
updateScrollPosition,
158+
};
159+
160+
const popup = new AppointmentPopup(popupSchedulerProxy, form);
161+
162+
const appointmentData = options.appointmentData
163+
?? { ...DEFAULT_APPOINTMENT };
164+
const action = options.action ?? ACTION_TO_APPOINTMENT.CREATE;
165+
166+
popup.show(appointmentData, { action, allowSaving: true });
167+
await new Promise(process.nextTick);
168+
169+
const selector = `.dx-overlay-wrapper.${APPOINTMENT_POPUP_CLASS}`;
170+
const overlayWrapper = document.querySelector(
171+
selector,
172+
) as HTMLDivElement;
173+
174+
if (!overlayWrapper) {
175+
throw new Error(
176+
'AppointmentPopup overlay wrapper not found in DOM',
177+
);
178+
}
179+
180+
const POM = new PopupModel(overlayWrapper);
181+
182+
const dispose = (): void => {
183+
popup.dispose();
184+
container.remove();
185+
};
186+
187+
disposables.push(dispose);
188+
189+
return {
190+
container,
191+
popup,
192+
form,
193+
POM,
194+
callbacks: {
195+
addAppointment,
196+
updateAppointment,
197+
focus,
198+
updateScrollPosition,
199+
},
200+
dispose,
201+
};
202+
};

0 commit comments

Comments
 (0)