From 655672ce1f9548c53cfba23b2a8c6a5f543aac67 Mon Sep 17 00:00:00 2001 From: Ilya Vinogradov <48182348+williamvinogradov@users.noreply.github.com> Date: Mon, 25 Dec 2023 22:20:00 +0500 Subject: [PATCH] Scheduler(T1201196): Fix nested expressions issue. (#26320) (#26350) --- .../scheduler/appointment_popup/m_form.ts | 171 ++++-- .../scheduler/appointment_popup/m_popup.ts | 10 +- .../model/scheduler/appointment/popup.ts | 78 ++- ...rence-editor-first-opening_nested-expr.png | Bin 0 -> 33671 bytes .../scheduler/appointmentForm/expressions.ts | 532 ++++++++++++++++++ 5 files changed, 702 insertions(+), 89 deletions(-) create mode 100644 packages/devextreme/testing/testcafe/tests/scheduler/appointmentForm/etalons/form_recurrence-editor-first-opening_nested-expr.png create mode 100644 packages/devextreme/testing/testcafe/tests/scheduler/appointmentForm/expressions.ts diff --git a/packages/devextreme/js/__internal/scheduler/appointment_popup/m_form.ts b/packages/devextreme/js/__internal/scheduler/appointment_popup/m_form.ts index 22f88ec1f774..12a1c04dfb59 100644 --- a/packages/devextreme/js/__internal/scheduler/appointment_popup/m_form.ts +++ b/packages/devextreme/js/__internal/scheduler/appointment_popup/m_form.ts @@ -14,6 +14,7 @@ import messageLocalization from '@js/localization/message'; import { Semaphore } from '@js/renovation/ui/scheduler/utils/semaphore/semaphore'; import Form from '@js/ui/form'; import { current, isFluent } from '@js/ui/themes'; +import { ExpressionUtils } from '@ts/scheduler/m_expression_utils'; import { createAppointmentAdapter } from '../m_appointment_adapter'; import timeZoneDataUtils from '../timezones/m_utils_timezones_data'; @@ -25,6 +26,21 @@ export const APPOINTMENT_FORM_GROUP_NAMES = { Recurrence: 'recurrenceGroup', }; +// TODO: Remove duplication in the scheduler's popup testing model. +// NOTE: These CSS classes allow access the editors +// from e2e testcafe tests. +const E2E_TEST_CLASSES = { + form: 'e2e-dx-scheduler-form', + textEditor: 'e2e-dx-scheduler-form-text', + descriptionEditor: 'e2e-dx-scheduler-form-description', + startDateEditor: 'e2e-dx-scheduler-form-start-date', + endDateEditor: 'e2e-dx-scheduler-form-end-date', + startDateTimeZoneEditor: 'e2e-dx-scheduler-form-start-date-timezone', + endDateTimeZoneEditor: 'e2e-dx-scheduler-form-end-date-timezone', + allDaySwitch: 'e2e-dx-scheduler-form-all-day-switch', + recurrenceSwitch: 'e2e-dx-scheduler-form-recurrence-switch', +}; + const getStylingModeFunc = (): string | undefined => (isFluent(current()) ? 'filled' : undefined); const getStartDateWithStartHour = (startDate, startDayHour) => new Date(new Date(startDate).setHours(startDayHour)); @@ -43,27 +59,6 @@ const updateRecurrenceItemVisibility = (recurrenceRuleExpr, value, form) => { form.getEditor(recurrenceRuleExpr)?.changeValueByVisibility(value); }; -const createDateBoxEditor = (dataField, colSpan, firstDayOfWeek, label, onValueChanged) => ({ - editorType: 'dxDateBox', - dataField, - colSpan, - label: { - text: messageLocalization.format(label), - }, - validationRules: [{ - type: 'required', - }], - editorOptions: { - stylingMode: getStylingModeFunc(), - width: '100%', - calendarOptions: { - firstDayOfWeek, - }, - onValueChanged, - useMaskBehavior: true, - }, -}); - export class AppointmentForm { scheduler: any; @@ -100,17 +95,19 @@ export class AppointmentForm { create(triggerResize, changeSize, formData) { const { allowTimeZoneEditing } = this.scheduler.getEditingConfig(); - const { expr } = this.scheduler.getDataAccessors(); + const dataAccessors = this.scheduler.getDataAccessors(); + const { expr } = dataAccessors; - const recurrenceEditorVisibility = !!formData[expr.recurrenceRuleExpr]; // TODO - const colSpan = recurrenceEditorVisibility ? 1 : 2; + const isRecurrence = !!ExpressionUtils.getField(dataAccessors, 'recurrenceRule', formData); + const colSpan = isRecurrence ? 1 : 2; const mainItems = [ ...this._createMainItems(expr, triggerResize, changeSize, allowTimeZoneEditing), ...this.scheduler.createResourceEditorModel(), ]; - changeSize(recurrenceEditorVisibility); + changeSize(isRecurrence); + const items = [ { itemType: 'group', @@ -124,7 +121,7 @@ export class AppointmentForm { }, { itemType: 'group', name: APPOINTMENT_FORM_GROUP_NAMES.Recurrence, - visible: recurrenceEditorVisibility, + visible: isRecurrence, colSpan, items: this._createRecurrenceEditor(expr), }, @@ -167,6 +164,9 @@ export class AppointmentForm { } }, screenByWidth: (width) => (width < SCREEN_SIZE_OF_SINGLE_COLUMN || devices.current().deviceType !== 'desktop' ? 'xs' : 'lg'), + elementAttr: { + class: E2E_TEST_CLASSES.form, + }, }); } @@ -192,20 +192,23 @@ export class AppointmentForm { const previousValue = dateSerialization.deserializeDate(args.previousValue); const dateEditor = this.form.getEditor(dateExpr); const dateValue = dateSerialization.deserializeDate(dateEditor.option('value')); + if (this.semaphore.isFree() && dateValue && value && isNeedCorrect(dateValue, value)) { const duration = previousValue ? dateValue.getTime() - previousValue.getTime() : 0; dateEditor.option('value', new Date(value.getTime() + duration)); } } - _createTimezoneEditor(timeZoneExpr, secondTimeZoneExpr, visibleIndex, colSpan, isMainTimeZone, visible = false) { + _createTimezoneEditor(timeZoneExpr, secondTimeZoneExpr, visibleIndex, colSpan, isMainTimeZone, cssClass, visible = false) { const noTzTitle = messageLocalization.format('dxScheduler-noTimezoneTitle'); return { + name: this.normalizeEditorName(timeZoneExpr), dataField: timeZoneExpr, editorType: 'dxSelectBox', visibleIndex, colSpan, + cssClass, label: { text: ' ', }, @@ -231,46 +234,67 @@ export class AppointmentForm { const firstDayOfWeek = this.scheduler.getFirstDayOfWeek(); return [ - createDateBoxEditor( + this.createDateBoxEditor( dataExprs.startDateExpr, colSpan, firstDayOfWeek, 'dxScheduler-editorLabelStartDate', + E2E_TEST_CLASSES.startDateEditor, (args) => { this._dateBoxValueChanged(args, dataExprs.endDateExpr, (endValue, startValue) => endValue < startValue); }, ), - this._createTimezoneEditor(dataExprs.startDateTimeZoneExpr, dataExprs.endDateTimeZoneExpr, 1, colSpan, true, allowTimeZoneEditing), + this._createTimezoneEditor( + dataExprs.startDateTimeZoneExpr, + dataExprs.endDateTimeZoneExpr, + 1, + colSpan, + true, + E2E_TEST_CLASSES.startDateTimeZoneEditor, + allowTimeZoneEditing, + ), - createDateBoxEditor( + this.createDateBoxEditor( dataExprs.endDateExpr, colSpan, firstDayOfWeek, 'dxScheduler-editorLabelEndDate', + E2E_TEST_CLASSES.endDateEditor, (args) => { this._dateBoxValueChanged(args, dataExprs.startDateExpr, (startValue, endValue) => endValue < startValue); }, ), - this._createTimezoneEditor(dataExprs.endDateTimeZoneExpr, dataExprs.startDateTimeZoneExpr, 3, colSpan, false, allowTimeZoneEditing), + this._createTimezoneEditor( + dataExprs.endDateTimeZoneExpr, + dataExprs.startDateTimeZoneExpr, + 3, + colSpan, + false, + E2E_TEST_CLASSES.endDateTimeZoneEditor, + allowTimeZoneEditing, + ), ]; } - _changeFormItemDateType(itemPath, isAllDay) { - const itemEditorOptions = this.form.itemOption(itemPath).editorOptions; + _changeFormItemDateType(name: string, groupName: string, isAllDay: boolean): void { + const editorPath = this.getEditorPath(name, groupName); + const itemEditorOptions = this.form.itemOption(editorPath).editorOptions; const type = isAllDay ? 'date' : 'datetime'; const newEditorOption = { ...itemEditorOptions, type }; - this.form.itemOption(itemPath, 'editorOptions', newEditorOption); + this.form.itemOption(editorPath, 'editorOptions', newEditorOption); } _createMainItems(dataExprs, triggerResize, changeSize, allowTimeZoneEditing) { return [ { + name: this.normalizeEditorName(dataExprs.textExpr), dataField: dataExprs.textExpr, + cssClass: E2E_TEST_CLASSES.textEditor, editorType: 'dxTextBox', colSpan: 2, label: { @@ -297,8 +321,9 @@ export class AppointmentForm { xs: 2, }, items: [{ + name: this.normalizeEditorName(dataExprs.allDayExpr), dataField: dataExprs.allDayExpr, - cssClass: 'dx-appointment-form-switch', + cssClass: `dx-appointment-form-switch ${E2E_TEST_CLASSES.allDaySwitch}`, editorType: 'dxSwitch', label: { text: messageLocalization.format('dxScheduler-allDay'), @@ -324,17 +349,14 @@ export class AppointmentForm { } } - const startDateItemPath = `${APPOINTMENT_FORM_GROUP_NAMES.Main}.${dataExprs.startDateExpr}`; - const endDateItemPath = `${APPOINTMENT_FORM_GROUP_NAMES.Main}.${dataExprs.endDateExpr}`; - - this._changeFormItemDateType(startDateItemPath, value); - this._changeFormItemDateType(endDateItemPath, value); + this._changeFormItemDateType(dataExprs.startDateExpr, 'Main', value); + this._changeFormItemDateType(dataExprs.endDateExpr, 'Main', value); }, }, }, { editorType: 'dxSwitch', dataField: 'repeat', - cssClass: 'dx-appointment-form-switch', + cssClass: `dx-appointment-form-switch ${E2E_TEST_CLASSES.recurrenceSwitch}`, name: 'visibilityChanged', label: { text: messageLocalization.format('dxScheduler-editorLabelRecurrence'), @@ -361,7 +383,9 @@ export class AppointmentForm { colSpan: 2, }, { + name: this.normalizeEditorName(dataExprs.descriptionExpr), dataField: dataExprs.descriptionExpr, + cssClass: E2E_TEST_CLASSES.descriptionEditor, editorType: 'dxTextArea', colSpan: 2, label: { @@ -380,6 +404,7 @@ export class AppointmentForm { _createRecurrenceEditor(dataExprs) { return [{ + name: this.normalizeEditorName(dataExprs.recurrenceRuleExpr), dataField: dataExprs.recurrenceRuleExpr, editorType: 'dxRecurrenceEditor', editorOptions: { @@ -397,8 +422,8 @@ export class AppointmentForm { setEditorsType(allDay) { const { startDateExpr, endDateExpr } = this.scheduler.getDataAccessors().expr; - const startDateItemPath = `${APPOINTMENT_FORM_GROUP_NAMES.Main}.${startDateExpr}`; - const endDateItemPath = `${APPOINTMENT_FORM_GROUP_NAMES.Main}.${endDateExpr}`; + const startDateItemPath = this.getEditorPath(startDateExpr, 'Main'); + const endDateItemPath = this.getEditorPath(endDateExpr, 'Main'); const startDateFormItem = this.form.itemOption(startDateItemPath); const endDateFormItem = this.form.itemOption(endDateItemPath); @@ -421,15 +446,15 @@ export class AppointmentForm { } setEditorOptions(name, groupName: 'Main' | 'Recurrence', options) { - const editorPath = `${APPOINTMENT_FORM_GROUP_NAMES[groupName]}.${name}`; + const editorPath = this.getEditorPath(name, groupName); const editor = this.form.itemOption(editorPath); editor && this.form.itemOption(editorPath, 'editorOptions', extend({}, editor.editorOptions, options)); } - setTimeZoneEditorDataSource(date, path) { + setTimeZoneEditorDataSource(date, name) { const dataSource = this.createTimeZoneDataSource(date); - this.setEditorOptions(path, 'Main', { dataSource }); + this.setEditorOptions(name, 'Main', { dataSource }); } updateFormData(formData) { @@ -437,20 +462,60 @@ export class AppointmentForm { this.form.option('formData', formData); - const dataExprs = this.scheduler.getDataAccessors().expr; + const dataAccessors = this.scheduler.getDataAccessors(); + const { expr } = dataAccessors; - const allDay = formData[dataExprs.allDayExpr]; + const rawStartDate = ExpressionUtils.getField(dataAccessors, 'startDate', formData); + const rawEndDate = ExpressionUtils.getField(dataAccessors, 'endDate', formData); - const startDate = new Date(formData[dataExprs.startDateExpr]); - const endDate = new Date(formData[dataExprs.endDateExpr]); + const allDay = ExpressionUtils.getField(dataAccessors, 'allDay', formData); + const startDate = new Date(rawStartDate); + const endDate = new Date(rawEndDate); - this.setTimeZoneEditorDataSource(startDate, dataExprs.startDateTimeZoneExpr); - this.setTimeZoneEditorDataSource(endDate, dataExprs.endDateTimeZoneExpr); + this.setTimeZoneEditorDataSource(startDate, expr.startDateTimeZoneExpr); + this.setTimeZoneEditorDataSource(endDate, expr.endDateTimeZoneExpr); - this.updateRecurrenceEditorStartDate(startDate, dataExprs.recurrenceRuleExpr); + this.updateRecurrenceEditorStartDate(startDate, expr.recurrenceRuleExpr); this.setEditorsType(allDay); this.semaphore.release(); } + + private createDateBoxEditor(dataField, colSpan, firstDayOfWeek, label, cssClass, onValueChanged) { + return { + editorType: 'dxDateBox', + name: this.normalizeEditorName(dataField), + dataField, + colSpan, + cssClass, + label: { + text: messageLocalization.format(label), + }, + validationRules: [{ + type: 'required', + }], + editorOptions: { + stylingMode: getStylingModeFunc(), + width: '100%', + calendarOptions: { + firstDayOfWeek, + }, + onValueChanged, + useMaskBehavior: true, + }, + }; + } + + private getEditorPath(name: string, groupName: string): string { + const normalizedName = this.normalizeEditorName(name); + return `${APPOINTMENT_FORM_GROUP_NAMES[groupName]}.${normalizedName}`; + } + + private normalizeEditorName(name: string): string { + // NOTE: This ternary operator covers the "recurrenceRuleExpr: null/''" scenarios. + return name + ? name.replace(/\./g, '_') + : name; + } } diff --git a/packages/devextreme/js/__internal/scheduler/appointment_popup/m_popup.ts b/packages/devextreme/js/__internal/scheduler/appointment_popup/m_popup.ts index 3016a5a6feb1..ddc77cefa52f 100644 --- a/packages/devextreme/js/__internal/scheduler/appointment_popup/m_popup.ts +++ b/packages/devextreme/js/__internal/scheduler/appointment_popup/m_popup.ts @@ -9,6 +9,7 @@ import { isPopupFullScreenNeeded, } from '@js/renovation/ui/scheduler/appointment_edit_form/popup_config'; import Popup from '@js/ui/popup/ui.popup'; +import { ExpressionUtils } from '@ts/scheduler/m_expression_utils'; import { createAppointmentAdapter } from '../m_appointment_adapter'; import { hide as hideLoading, show as showLoading } from '../m_loading'; @@ -222,13 +223,12 @@ export class AppointmentPopup { } updatePopupFullScreenMode() { - if (this.form.dxForm) { // TODO + if (this.form.dxForm && this.visible) { // TODO const { formData } = this.form; - const isRecurrence = formData[this.scheduler.getDataAccessors().expr.recurrenceRuleExpr]; + const dataAccessors = this.scheduler.getDataAccessors(); + const isRecurrence = ExpressionUtils.getField(dataAccessors, 'recurrenceRule', formData); - if (this.visible) { - this.changeSize(isRecurrence); - } + this.changeSize(isRecurrence); } } diff --git a/packages/devextreme/testing/testcafe/model/scheduler/appointment/popup.ts b/packages/devextreme/testing/testcafe/model/scheduler/appointment/popup.ts index 617193868caf..49fec72b2943 100644 --- a/packages/devextreme/testing/testcafe/model/scheduler/appointment/popup.ts +++ b/packages/devextreme/testing/testcafe/model/scheduler/appointment/popup.ts @@ -10,57 +10,65 @@ export const CLASS = { textEditorInput: 'dx-texteditor-input', overlayWrapper: 'dx-overlay-wrapper', fullScreen: 'dx-popup-fullscreen', + switch: 'dx-switch', + // e2e + form: 'e2e-dx-scheduler-form', + textEditor: 'e2e-dx-scheduler-form-text', + descriptionEditor: 'e2e-dx-scheduler-form-description', + startDateEditor: 'e2e-dx-scheduler-form-start-date', + endDateEditor: 'e2e-dx-scheduler-form-end-date', + startDateTimeZoneEditor: 'e2e-dx-scheduler-form-start-date-timezone', + endDateTimeZoneEditor: 'e2e-dx-scheduler-form-end-date-timezone', + allDaySwitch: 'e2e-dx-scheduler-form-all-day-switch', + recurrenceSwitch: 'e2e-dx-scheduler-form-recurrence-switch', +}; +export const SELECTORS = { + textInput: `.${CLASS.textEditor} .${CLASS.textEditorInput}`, + descriptionTextArea: `.${CLASS.descriptionEditor} .${CLASS.textEditorInput}`, + startDateInput: `.${CLASS.startDateEditor} .${CLASS.textEditorInput}`, + endDateInput: `.${CLASS.endDateEditor} .${CLASS.textEditorInput}`, + startDateTimeZoneInput: `.${CLASS.startDateTimeZoneEditor} .${CLASS.textEditorInput}`, + endDateTimeZoneInput: `.${CLASS.endDateTimeZoneEditor} .${CLASS.textEditorInput}`, + allDaySwitch: `.${CLASS.allDaySwitch} .${CLASS.switch}`, + recurrenceSwitch: `.${CLASS.recurrenceSwitch} .${CLASS.switch}`, }; export default class AppointmentPopup { - element: Selector; - - wrapper: Selector; + element = this.scheduler.find(`.${CLASS.popup}.${CLASS.appointmentPopup}`); - subjectElement: Selector; + form = Selector(`.${CLASS.form}`); - descriptionElement: Selector; + wrapper = Selector(`.${CLASS.popupWrapper}.${CLASS.appointmentPopup}`); - startDateElement: Selector; + subjectElement = this.wrapper.find(SELECTORS.textInput); - endDateElement: Selector; + descriptionElement = this.wrapper.find(SELECTORS.descriptionTextArea); - doneButton: Selector; + startDateElement = this.wrapper.find(SELECTORS.startDateInput); - cancelButton: Selector; + endDateElement = this.wrapper.find(SELECTORS.endDateInput); - allDayElement: Selector; + startDateTimeZoneElement = this.wrapper.find(SELECTORS.startDateTimeZoneInput); - recurrenceElement: Selector; + endDateTimeZoneElement = this.wrapper.find(SELECTORS.endDateTimeZoneInput); - freqElement: Selector; + doneButton = this.wrapper.find('.dx-popup-done.dx-button'); - endRepeatDateElement: Selector; + cancelButton = this.wrapper.find(`.${CLASS.cancelButton}`); - repeatEveryElement: Selector; + allDayElement = this.wrapper.find(SELECTORS.allDaySwitch); - fullScreen: Promise; + recurrenceElement = this.wrapper.find(SELECTORS.recurrenceSwitch); - constructor(scheduler: Selector) { - this.element = scheduler.find(`.${CLASS.popup}.${CLASS.appointmentPopup}`); - this.wrapper = Selector(`.${CLASS.popupWrapper}.${CLASS.appointmentPopup}`); + freqElement = this.wrapper.find('.dx-recurrence-selectbox-freq .dx-selectbox'); - this.subjectElement = this.wrapper.find('.dx-texteditor-input').nth(0); - this.startDateElement = this.wrapper.find('.dx-texteditor-input').nth(1); - this.endDateElement = this.wrapper.find('.dx-texteditor-input').nth(2); - this.descriptionElement = this.wrapper.find('.dx-texteditor-input').nth(3); - this.allDayElement = this.wrapper.find('.dx-switch').nth(0); - this.recurrenceElement = this.wrapper.find('.dx-switch').nth(1); + endRepeatDateElement = this.wrapper.find(`.${CLASS.recurrenceEditor} .${CLASS.textEditorInput}`).nth(2); - this.freqElement = this.wrapper.find('.dx-recurrence-selectbox-freq .dx-selectbox'); + repeatEveryElement = this.wrapper.find(`.${CLASS.recurrenceEditor} .${CLASS.textEditorInput}`).nth(1); - this.doneButton = this.wrapper.find('.dx-popup-done.dx-button'); - this.cancelButton = this.wrapper.find(`.${CLASS.cancelButton}`); + fullScreen = this.wrapper.find(`.${CLASS.overlayWrapper} .${CLASS.fullScreen}`).exists; - this.endRepeatDateElement = this.wrapper.find(`.${CLASS.recurrenceEditor} .${CLASS.textEditorInput}`).nth(2); - this.repeatEveryElement = this.wrapper.find(`.${CLASS.recurrenceEditor} .${CLASS.textEditorInput}`).nth(1); - - this.fullScreen = this.wrapper.find(`.${CLASS.overlayWrapper} .${CLASS.fullScreen}`).exists; + constructor(private readonly scheduler: Selector) { } isVisible(): Promise { @@ -71,4 +79,12 @@ export default class AppointmentPopup { dependencies: { element, invisibleStateClass }, })(); } + + getAllDaySwitchValue(): Promise { + return this.allDayElement.find('input[type="hidden"]').value; + } + + getRecurrenceRuleSwitchValue(): Promise { + return this.recurrenceElement.find('input[type="hidden"]').value; + } } diff --git a/packages/devextreme/testing/testcafe/tests/scheduler/appointmentForm/etalons/form_recurrence-editor-first-opening_nested-expr.png b/packages/devextreme/testing/testcafe/tests/scheduler/appointmentForm/etalons/form_recurrence-editor-first-opening_nested-expr.png new file mode 100644 index 0000000000000000000000000000000000000000..219f01a73abf781c32dddbc9743d886fb4232fda GIT binary patch literal 33671 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

!MhqfOsMO<4w%Mt@4x{QZ3wHy6`R zb&%TY;r*j)lTV@XC@ISe4#)9c@ioNIvdV6XB#vKGnxSQ^gt){g?&n9%UH7de^mlpl z&Cz$ZCqzeYV4p+5N;~YZOz^alhRM8&@-lz{e9irvKi-VPXXYtB0modEL90I}2UnIa zJh<-rOv;Bp>5EK4uiGD1Rw@>H_b9A;X#yQ_0BD*99ijsf7d?O(D|;%T2xi_yoZx$cgUu-OC-)sZ_<;H%`v6;0at{9P72iM3 z#CAO9GPxFwUeD>hbR$rTve^LX5JglOHY;Ds{w}k4xD?N=Z-7i!q z&NmOsFK_QmaU66&xD$*^QlZbsAk~(2W7^{cN%;u?y9axG(_t=Nt`<3$kp>AUWR+v) z;6w<2l#P0rWbEyAsyv0}mu*J?CLrB#$vd3lg%43;i9-R@g;A9Tkdwzw5J!&z8DuHp zGj_6NzzEpeHE`%f^pA~+`7)WdK(vG6k`n4$qqN7Vsj~2%Z{N-%Lq`OG_$8r$c~7-s z?11G*$mL@^*dVfX;Z)JLdd|4`r@Td#3Re2XQm$@1b=nHrN|~KyJRY9|L<1VCjxdk5 z>|-#S47q%Gb|FQZVnhF7D|!r%drCXfxb%&ll@*1Rxy#*uwd*IH1Ei;~pI z7s==K8#T&dDaot$$LXL~h`oN+RV~5mv;l}pRc>&Dbpia5p!Y2>!=C4`tQ{Z(pJJwi zTqmq-zabEPPWmXKcv#}IjU0K#p6UVh>x*LS zK-5XX8Y3-88j%&*x?{(Qaei8$x#ACJE?A*^oa2qy9jCfM!pnQEx^aLU35fVFAGUSt z)-RZ!PSAEqZ(;F(-XDOyPDh+L?Xj*44S${S1sYJ%UTvk-1eQ>lIu4MIx&kAnuC(WH z$&j=5(gcs=U3J)v6DAXsysI+dvSMIGleP9WJFK+4xVChFZe_AbpBEYpYt^?LV^pKo zo-STPYb==9xnI`_XJR*b#HDw7Jg2q&;-anU-d(-AcE9E}DW;40zIt6-`1Kg;-MiVT z{auIc8&mUzO_+&>|4Y4$0eQLEM?7k5FEvP+up&?y))D4zmUZ#HtnMR{AT`X*70o-O9QEuJqJ^ief`mm^ytrCcg1YUic&if5)-zVnBI0`Vujyt;fLtuweV{R0Vc z`s|f0!L1#e*84JEW(z3`)ee;oo9=YG0zWPjU_#514Ak%oEx>3JR4*iT%mmm+3iL*C z+3;6n4iXtn_#<;98#ZRntzVz#W2N3mawFw0J{K0&oY#59XIZj~$sD1$9x$&gJAUlg z&Rx6iGJvOyZBklKYT=wWt{ng_nqj2dH9+umUUn^ZTbHoXT6hoO+U#U4cZde&EnP0Ek=n&s}}9 z)ob*q`ME93@r$EZNztWoF#q+}e$o`_2UJ3Rt=COkI|S+$nNiG6=sZ4Ywq3FzT{1y> z*3b!xss=$%0pX+QiO=Qa=I+|JZ{*I5!iCHT;zANxg(s`VE|+F|!i1q-FP5SLvV9j_ zppyH7FqXhvOSDYmR!I1Oi)NGN%#ogzrziqEY&TZb`Vi$^X#9j4GherE2$OyDQ9Z=O zO&tnHu|Os=b9B@NUK}~c?i9Bik6OIk#i>^@b#gy5s@M3^chG%rr}q;~jsMk$x`NKh zlBz(71dQj=8=4p-(Bii-a2W_l2TTwV5&m4D$W@UpB_}Uji2t-I zYt;=gcpX1}oN<($JfYFEXQS%2LvNixsL@{r1hn-4MaFTPjCp0z{EFcW{Icfj`#X|) z$+K#6F8GPX!LU5D31yk#La+n`i@CYE@GX}wUp{Moc`L3x@su$_BJ_|EoVPm6YVyG$ zf!rIs7av9q;Fj4-`d@C>a%HHZMa!0GO!l6E!^jK9Y8b;&eKX8Tmg{X=_%Nh~s zkhqMWBx2#%x#PnBpO+p^ZXuM6jA{P-xu*KDG0f4t7^kx^03P@*?okZLInw?KHTvO$ zt?+#^Dl`MbK8ZyTlo(9#^I)7C*iw=t)d91IHO*{-*(^9*qVU4Aag!%2Rfi+{m&oqm ze`kzDkBZOcoy5J;CHk>dADBeX@X_t=&f4^QBf{p2KHe3VVsSPh8h+S84DJv=Zd^Bjw^JOw}&^Yh>KL5 z)YvO^oxjZUkXVM48Ua{3Hy(~a%oL30h7qoqItCH@!fnVrM^RCc0^QnLw#F`BU!z$- z!t&3nqdH|%yQ}ayDry%TAQpQ=X(AmMI-#JHq7Vu?cd+we!piA&0(du(|)z;D0W12*-5{+PkXLu z0?B^N1l|)!JV1<+Sw%VwF$#vDLlD5Q2*Ov&Q5ZfIF_pokEndPKi&c8`6o-LW<*pZ;KYIN5ra3p)6j2nFuqHf&K#-{-`tD1Y zN$PFewTs9NShA=Wa)aNTHCqm$-|yP9$C%5I?n1nB2|D*{|b3HzW>|cXXDK_5bC6?ze|(?3)Fokt#!+)d*MunAib)yGF&&DJ=;^F zK0N>j#DG`!wb-jhRZ{G&5zuL{gB_%8kncvN?Ht2z#@U0h^flXsL65QbC3U25Y0{lE zWb7lWw>0DYdffQ$@tE{G?U}bG9jH54xf-a7V|@l&{7aduSHhN}GFJu!i{{to_zB8g z*>ZzmA3RokMzYn8ZQF>c}AN<#rm?~0t8`YL68TSMzyK7%dAlTC0hS#y)F zvHF|n9M7MZu_!)E-NBXjaQRZsEA5Wb*Bi1q=Ubr$EKQhVq@r|f>Cr)TE=hwPbE6?c6+o<5mbqrfY z>mW%ydGh4#svC=KZHF0GxO2B$8Opw=sV^y5qJp6q z0|ElxCvZs&TSw~%HRP)sh$#S0peBga$r>oZt6+kgmg)6iY{K=AqSoM8*(I!1!{Cah5iEB{T z)!ThOuCwDCzLxIT(Ejunha-)rI=i^Wi{NjZofm7;<8xb8&7|2`Ss@>{_BcO}DxmNd;YFQt+(X6R{&2oi&x2TXYsTnf)FJ^`z%FHfC5Z3T`K);99%<8i<|VK#9XyoZ7{ zg743yUq5>2&>yg*2pb|N8Qs5sUkuvtEE6Chu&bAr1Xr4%J&OJ$Rw6Vmnx>13Q>)@a zLD6&r1mBlL&EN^MW{qP!1#o!;Mx*^e`#+J3Qo)F+&k;LTu3WkC>&F+nsipSah2}ws zeuSAroU(FSN!3b;4M{w@G>B|VscEOroT={u;eG1-`CJA<;&c?ng)Yty8p-4f?!T8D zbjfXF7g;nCTx+yE>UV!mUGNIB6{m$*_3)({j~eCpXhiE3JpKt-j1iT`f-^)sD*PeF zyE$t=^<$5`PvGhBsN`XbI*dfn?fP6USjJV_cG+7BLSlk~LFiz3pBN zG~G*_9S~IKB)9(db`5b@m_CKLjzGndDQ+@EIS$2`ypm=PP#G7|_+YNLFi>{4p|*8p z#czf2IBFzJK^z`1Ji}mlrHreGOj^0JpZm|$;P(tdplWm%zFw-e41^kyR%yC%pr)9` zinz&%7g0acF6bz3c=gIc`UJ6Uy|&Y2pqE0&Hdcz zJtsZbfMZ#AXjA|G%i0Q`WdurDie`q3o=H6E1W%k01+^+6lq#Bray3=<4paw^2L`{E z3O_+Mz}2FbU={jsU!wJ0CMvtHV>HFJD7Li)tE9lGBsxsOKx{-X^fIsw6@_4JDu=10 z=65~@B_;X`DtqdyS0*H6HTX)A1IbW2fLJ-9g&Wt0J79(AL%)6dR^{dJGy=9r$Ai{x z!weL3$@!>?H5^cp3@iTLOQyZ(sD@AI*i<6YCRzD&WQnP)xCbphgwrLs5jh>jh(2Ir zXAO%RG)(*vAF;iT{hiyym`>do`_RuP>=iU^UmTYI+B}?R^HufDbKnb!Ccblkr!no;U12<%wL2EA&{X_IT02j3alnmvdJT2TcwP+S?Se;UOtOeECKI zflJf>gX0LIa0r!k{^oacG!SvE=uN~M#n5I`na6!r(1oG6ZFJt}uf1!4SYd=?<@oL60dxYtaYvf z)^Ns_;NPKoEY^OyOa}Ep36XT-yL@48ivp7V{J3qIe|Ji}Ol;C6zxF@ZznlcEJJ-zHiI$xbuDCR_#-NJQ(0 z{vpd)BMDnosp-hs_@y)JYcMQNv`6Nr+Oh9Mi=#$YJwR7Uoet5I)%U30V0_NZoCgHB2(C{7A^ z?_uN?oQex&{>=VPZD?oFi~nZwP@OqGO4=!M^4pR!XO4qN#2LmT68?(kAqPGHvnWIG zlA$n*DrosK8o*RR1mmQP<_WjSM5KyKb~Dj{R)P%z<0Jg-5G9NVj^!3zIW{JztwK zLZP}LyVvB@l`H!YdV-^Ve_H652x!!$w6KT{&$C( z-r(r0lHw%o-6))Ux=j{P&UfurzVvSEm3QZhR`juWG%fq(YtpEsQm^tSPGGCOzN$O?r=3uA$lQ9x;RxTvAJx5QDL$(2s|k83y68$x+>flL%j95+`M z+NRP7@>IPzwJ9wnaIvXx@D8jjGQYDU4|hcF=`-iwnLK^?@HS-SwU1#*Ow#G@=5zzE zpjASk0M{IQF zZ>{TIa*_)dUUU5)azqSrx+YVDUNBL+IFf*E24|Y}bk=goaKe@ybo;qjR%p-xN%M@N zUllV4xZgiF*9HqsEa77YyD5#Cx7>31d&Ow!YGn2vF+dLVz!4dPYikiPb>*d97RaZc zboh0fsAQ8i31thG$`UM1 zG6*T>x)F&K5EKDkCj%q$*r-r4!h@;lA@eL^Ad*AL@`4;21BP;SiCW)OyJ~Ug@jig) zF^nx6@ltw#ahS>Mq@huVE_pP@QyHWw)9|!>>kv^-3}>Rp(VARp)w=bl(JZZ+r)5+A zh;trV9_S$c##zX67>Y!jmowYM&>VPw%Jk`3C1pf@a9xfNYo_}W$y42J<{DsW>LGQIs5!;|{iheYfXhGmc zfUz!eqLAfCp2(m}R0g^xAxW}vBm{|@)&Vlv96vH~ziB67t@N1v$S58u8V+6>H;iU# zFB+U{Zluaj!1p_U-ibU4P%E6Q2MD-`okiW(j^nX*VBnwnznw zFd3}Q_t2xw$J8ljPYOwa-rgHWgYYU=4a)1Xu*Ko4z~wS1q!m@}AX{tSzu=05*((me zO10z_x3vF{QceFC-%R$8ss+6Y6kti&qNPivgRCCcVH3gs5#VLrgpTr6odUe3jY41* zu&`yz9jnl$PpZ2ooC;zj0>;0#V+QG1r>c#qYQkm$h{>4_S&(xSb(Bqp#yMQaN?Fb> zB#Syt5h63Yt|-JzFA)YxY1Aq{qHB*HSa6OVtvaqaWH#@`z!0ys50lTQwMD>{H!!Yx zgG;9`TnK?M!I^yP0)9p4G`fA#+Oea(Q)~Q9JQWI{lB6x)*n-^+uHLcm$ND}t`acck z7rwspeC=wTtVcgweq`jdXKs@txRmNC3~WPIkyBq0(H^HW(x!*f&YCRN-cTuaSn)2= ziIP6zNmAE7oIW!Uil^-GY`3x;I8ak#;6935W>}I|r}yv=8ft1aSkWD)nJ%fjb;EtngYNT>?(jhXd87%P$DN>%8^5-Y`z!@XRyF; zwr5IS7PO04%`*d*oELY$3|6R4qS$J7|Bq6Y|qF^*f3ZJ#Mp-TzRyi{%>H?z7M-`H!ez9k%k3dtY{~Ys1c9pa* zWChv|!bLF!z7C+id-r#8FqqgSa3lzbldezM)99fbI>kVf9PTCr1Be*O*yC2k&n3Aj z!6{H?&K!mzXOw{T?;y_bDICRhSGBTq6aflRA)*s>!9}DKyIW;8)Om&=L>(0eJJ|(M zOu7dXa+lB~;t3k?#<>%*4*YF)379K$o^mkPMNZ!k7jMDY@Om5yQU~Tt(uPoh@_&&h z&^#nVY^wGi$pK2M3YV0%nEte6S@)X;S``bLpYy+O1FcQXcxQYQ?oT=j3Ip!igf4y- z9*NdR;4`>@4S3j6A3weeaFH1WnR9}%6Jc1O0F8GmpZI7{xd?@7;@{XcPU|rFS)^ui zxJXZ;X!Bnw&?Oe^&iPa}w72jSGG2oFoBAhsAv*c$HO@s4bFv77R3e~=99cv6h_-lw z-ny=e^Ph*Xtw|>!6rY@G2u?_u?=wUOhKCR*G^spH{*}S)W zc%vPZCh^GhU@uW|9aF~-;EW%pNV{MfJ(6jcn``U$`AH?Q30ydiB{xB%stYlG9iwSGZ%|2sFCA)rQO0&FM2IF3wo57 zl24_}gdV;)qo*soNYrB)79eN1G0Gv2jFVsL|4kCcXZ`pUL2{~xJ~a;-P7Y3jwYzm+ z&VhwRjpDGq`fc$gRDPRV84Fb*mSrPY1~nWQhwuP^MC6InB$Gaaw5CNTudc69Gia=^ zk3iOUceef;00KCH7YlJJG3&7rpKfo=-dOeo(P6g}&rct= zv%tUlAJQ-P$cV|b=alzxqmfbYbh-M$-#jp^bTu!4HN|YaQFfp(Nq;eHAs%VA$0H)z zaDv|w@?mJ6_fglt@ax8@)94sDV6Jcp9K7|yg(Y(69wP|D04$Ux8NyrM$F1C1$R>D1 zi9Eqdunuy-6>F(?5T9xFlg{l^+KNO<8vxX5!d?9Z23>c9I~a}ZDSa&)N$gE>HZN5G z8vE9EOJ!H3s0_0uy?XTmYTnWgP8sDTBVoqbPK?(g_DKAG`Y`u+Sk+aZaZB5Mg~)_+ zyA&^HZsB;Hh0kf67)!ypRAD(t(gP+34n>HNzL=||KZjrymwRgq`x-_7aH`%UbMvvl z)%sFoDGed1TL^%%WLIyn52ta-xz+#i!qL?dphDSiHxTpt7vY*>L6AgA$(NM^#7;tw zfJjl36T)b!g1j&vGC)RZgxbmi~_4e)CFRi3V<(huj5WDn9 zFYgyI!P=jrFV2lgoko?#>-b%&g$#v=b@AB0qm`N>G;;Rf4B8{<3}Gz~kpUmWpUO$D zIDDuzv?sdm6|Ssr-ks!12q-icIz%DpP`-007j>&9rW7~)yKnU%Y53F0$W8FnWCBPI zeZcL{#+8K;7lec*jj*_fsPLv6(`*+&8}{wn*PclbIR=$HTtDo^Qu$751e)r(@xWN5 zc19L`G72@xC&KU|sZmGV>Y?*tEZMOz?xaejQ%P5?@n(m|Egd*JOZ{5kA)EZ{RATpb zT5GT0uU|hX!t{b4QI$hjwWl)72M-SUi!nhu3LHR-mw|cY+GHH}U%oGB{Mt4^SSfDl^v(B2-? zs}2ExQzmhhMXOfaW*Q7wntg*?)4{x=b>m|nf=U_aAwgwCrpb=TJ2<~MJ5gZAQ>|-n zYf7OPd7nxkxjP43ymvJe+age&8_TmjP69b}Ve_{>1EHb2KxsL`_y;t3ZfAh)mwGaQT`dz0o z^T)4K+*(%A!zWK*(h|&O!&zZEAv8M~PSq_IO#)akYWe{eD492i~q$@Ax4 zmdXSXz6N+W6)F-l!_Y@yclCu@RTtD@XG&hxPbx87#}d&o2SrrfqSH{;SUA+|kQ5X$ zF6^&=;GO^3woujA{m`O)y zL00DO0)=X$eed`bjit(I+ zo5~M2F_u+ISUh#^T%VM@PKcL$QC2Ub#+uyvA;0c{5z2 zImVE!^2gCTtFB=hO(Z*0F5)7#&55xD*|kGeFH_ccRl6ky6z7Z29Q{ z1T8~2V`c#=Bsm?z%!*co^nhBfi{Qd}M`Pq1NI4>dM&D_W_;NXWMC~Lb@sPFOzaYlH z`ym7N^Ej-Re4-?xBqP~Fc`QVn%+#T+6TnUNW|FRU=X_h!tbTQ0&XT~O5~GW>qji89 zq?8qX&;uiwc*U|H6NEYPlVxOP5+zNzRSIs&JZYFM9@AEuC> z;3?Ib)+3X}&=TP{o`>sI#`ADI$@m+gxu`0orL!erLkCW+1}M~3Q{#?JrDv251@=o( zBwWmb!o14R06psh21wXA0ODxe9%UCQe`;5kjq<|E-%(|;+HOXp_96R_<*QEbPJ8xj zH!Lh~VgxtGthy>zWGsG0at0CT_z;sBOkR>w75LE{+b~^Eh6$t|gC6DhZN}=+>rG~X zd4arGjO3P~y68SV>ToJ9zTRqbN7Qwm%Xm;({kAw+WCW>O*RCD0ZvU%~@U~PIwTuRv zvo1lxB4?QylqvXrzeWwFU3B%69oyUrcE{LM=2lpw5DJYXN~NhB^n^y22x06^8+)17 z@BH---3?0L)}lQkO@2jd4#l?&kje)y`3(84+~GCj4!m)DGf<~kFJlUSq&|N>8p+v+ z_8Nbf90lwlM}{&dRv+#Zb(YwzCnFnh70V~lUonwkgjrrD3z~wdk=MC)1x}LL&8a<0 z{vsp)x3?f`%;T_p61oQ!sFY4bPErL|^p$k%kWRlyatztZ4{FFOS#d;s77r!Dc1Q>w zp-ZNa4-*JO`8A>+$bmO`XVR>m!tIOTZ)<&0E5HngsUWj!j;sV5FtNlGIdhuec+Yh$^Cft}6GZiUarg z^Mq3nkc^xJon$Ds=~phmUZ1qUItVvU5bJJF$(v74g_B@6L@iL@7@Jk5Fz6waMO5W9 z0r^2%K_~IrfaoYCYS5XXn474vi3tg4;LR~oiC}ygA0p2{O%x4c?)-|;*8(#y)>y}oPKtnrGqYVlvb z&o}+$OX2MFTuXJLMS9FGm?r?IQ{0th`YBmw-npn;S>~wjRuY&YBpN+coH*#{lb$nu zAhx-iH*dTOOM$eHt%GQWqFByfR(qvxOs`}mtiycR%bl+5Pz4VruL2&%a*%r@^Pw4tEEo}r- z8`1yr-4;P1$X=oRy}hm_G@-wVCmr(376|y;o%K!m&Z_r5U$84FN8tL(*|ZZW&W#p6!L;|Vzq&IjW+)>j(X zj0n!+I1yc$LBn1%_1%NvXLd+LeM>-jxhPfUz9UI_^An~@9}BV=iWz4&Mz#K z8H4tVIn2O1d?wgldEb4T7aTM>Le3@_bsakx;r*8W>&}mLo^lY`5$R(4_7yU!%@vvc z|Mg4GP8$^fZ%r2~thP9PZ~?LmR8NaF`NyAM8Q(u}0fXc%)-edxJ|8>CiNlB2i+fU> zXhJbf=qiV8qXITopL-ciRo$WIdN|+z)!nrRMOp4)Q^OHSUEB<82X#dRBZ8G|A*3TW z0bv8ul9MFH5;#h-v|1QwipH{Xaa|Ra+W-mKQ8QFD97#oFk>ny2)}|o4%%YZtD()ia z#N-&HX;U1vAz&d0G+G1jDaYNR3Nglu70K(dDn?GMz(08kYQ#BW@{QiyE{|y?wWCl|?waW7XqN1-$^_G=bMD)Vmtx66J>< z#zjv>@05Q2p7==Puy3*HMyf-xNwB>>S%h;mG;QDFngH3{21T%=EtaoOOI&R1sIEbm zD%T16YV?C804##*D75B%8b6ybc65MPfCt8%RC*Ib#7kpKVRgQCods$UAQxI&WnlRP z7(4P?Odd94>`&Y9Yl3DrRsEViUEnn^RGXKi*Il(F782_ps>9v#;}R29X})=?!VUL^ zeEj|WjVWHPO@0^M_&AzKXEbbP1^ntWsdJr+&>9Fry}QjCy_0QLteur){XTwob`%Jx zKw=R0VG(bzFO5)PQzBMghUYGg;BiLwUO}GIuo-YK7qRIA9c{_@4ay}`@ybolM!9X* zAU`%Ct!vJ{u${8Xk(NlYB<0&!_h8ql2DXbL62$t&b+UE%ieN`jsp>q#@w?o(_fh_; z{e;Lt1L7^=%d;$eX#7!^&5XJ63LbA63|tUuu{#*asL{WVC)l~|`I`(3 z#pPkXC3Ea-#80N!ST&l=zrTx!wdhL3nPjXZc9F z5M(A75XS=>`;KGrLDt8Bz&l+3Y|*uB2zf#~**?x*c+KvL%4) zFtfF_zQ&FnmQSU<>0RVhB%39ce9x7xQ=y`9elQ(D@53tC0&Fx4a?p9iE>uy1#L3{z zzP>+&PMMeqEyhw%LJ>(I=t9cEUy`NSX=g~N<-u#4Hodl}9mXDh*`iJdmLzYdY{Yl% znmctnNwcJ@=J$i~0s6QN#cX_6g8^~s$l|x(ejafnTNu}L)fT<6(6=N3poS0N8(6HF zDXwq~)S_b_6CN#>{*wLBD1Amc3ny#hEMTWV8+ssFl!+yN5G$oR38T3pX!YtBo{Lcm zfGAhbDo%no1(je&~f)V4;Orl9hMFu1KY}F60aC~qHK7-_cqSywVWE<= zWJqayvfcBNb$u;rmcz*rz^vk-w0Wk~uzDl8+$FRLVoMDKgM z^UF%G!VovBkw7+p!ng{&9V!6`tv!$cs%vU$P67O3cNWha?Ca}e0*@^bi9#vGX}rKg zasCmCDiD!-7Y7MYBN!ys46q)j?eavA@42bCg#tmuXX^E|$PQciLU}@!9LFEDEk;W&3$b!x24Le4;eIR@!oshC=$eu@zFz83+ zXU-H=7H?4dm7XP300sB+0658MPF%az1i^54$7Frd$$sD-$)LG^;Dg=${r7J|d9WVC zPzO+LIzo$^w{GE_@{;CO|49lg5sO?c#U997&TH@p*OH1y*r*KsL_koB zQ5qsVi#p~Ijx4-9SUb3Yw3#c{uQ!8p0oTe$?rq8^S)NG*W*j+kIf9`1TlY@wbWKOW(Uo$s*ek;111Fld0$Y$;T!`qQU3RXzM-*`958 zhYLT4s!+-g@%fQT&}1pja}@etwJO4kxw#BvKQ%~sOdT2!j)FCYJy-`5bQTCS8#V6@ z=z%P<;}Bfbde3mTnm#cH9271;xBT+MhjWM~qQVe`2E_$~Kh*&%LU4He#0kZINH$65u8$&5{U1v?(23(E!5_Wr3GRy-|benBtK zAgFS+%^F*0HEtY;ROLW)6Fo0MP$3a<>ec9kqQLnD2g=LG2)w)*UD&r1`NB3mW!$;g z87%Xam?9W9V#>Ld$Bt>2yEP!l9XeE8>;Nn7tZp#q`r5wxt{(eMr;)w(Sjn?$5m?(z zn_!-^)Fmm=_Ls*(y0A3iL!ewv*Bqj)T|#J0S6 z@~18F(W321dH~gC7_MJavC(y&;qv?D^b#Zoh%CB~fr=Q2sPm;JBv|tOPK#1Rndr4l z{%+}83Qyz^Eufk>3$4=}EJ34*Yz{L;@=*EHYV^rQeA!K^KLv=y|+ zrv*bjj4b6Bu2$#V9o|~EvevrTgz#ZSM{Db33Ij6H0M`88GPisK3M#!wCk~+e5*OlV zYlDOkaR3!Y00m5juS0c9uEhLMICa*ElP4)Sg(<@lLD$e;noCYKa^$aj)Gz$&yKDs5 z6OZ7ijx#D-;^UA25Jt2LO(*~o!TV{z_P4jQo3Sq}3WpW8@t6$oC=i^SJGYR<@@-ri`M0ls?=lG1J0ugl<*QKMHXl@U0$z>yu*Sh=KQ7Jd$3v4JY|)SJfoLfmnA z>02@N{I{Pam3ht54Ahos@)!Yu6BO=S0$0>b0S^iUVDZ8~qNA5ZMj?X6?qT1eq9S3h zIh9Nd3P@U_Ehfe&aMZ!8`8x~6n&26KU-C!_TJ`$HmgqG~z1%I+luFXcOJ5*vz}dpF z(NjxKK%SEA6*Z?%4;|up!|Xf4d+ZtuciT`tG@k!T`E|<^VY!M2*W{~ zL!)+o_=Ak;k3Jz+7uP3)uUk8Ccx^wO22jK(B|HY8((^BOF3*UMWb}8Yt=Rf2I$(QvP7Q{fgkw~H2<36R_m3U@6vG4tgv3CBg^|hwerzcb zG(b7vwyTo#P}A0e4*tdxR8_^AZYXunaL7Yg|23lnkZ#F2fm1_!i~=gj9zVl-8c%mU zcB@W;9eGarXe4;UxZfWbAR_33=mPNPMTO{Mja(7}Ed1V+k*xUn=8gA9jT$qbB_quh zG8zH(nyHO$Kn}sl zji{DLi+MaKNGx!$c`8wXWtW^nKyAstma)lE!$7j~1**D5}!tNlCob|t_-d+PRb!tS9fj5%( zA|XJQDyPAov)drWHphPyn>^gzKe;|ie+3|;k){9u literal 0 HcmV?d00001 diff --git a/packages/devextreme/testing/testcafe/tests/scheduler/appointmentForm/expressions.ts b/packages/devextreme/testing/testcafe/tests/scheduler/appointmentForm/expressions.ts new file mode 100644 index 000000000000..cfe6f14bd242 --- /dev/null +++ b/packages/devextreme/testing/testcafe/tests/scheduler/appointmentForm/expressions.ts @@ -0,0 +1,532 @@ +import { ClientFunction } from 'testcafe'; +import { createScreenshotsComparer } from 'devextreme-screenshot-comparer'; +import createWidget from '../../../helpers/createWidget'; +import url from '../../../helpers/getPageUrl'; +import Scheduler from '../../../model/scheduler'; + +fixture.disablePageReloads`Appointment form: expressions` + .page(url(__dirname, '../../container.html')); + +const SCHEDULER_SELECTOR = '#container'; +const TEST_TITLE = 'Test'; +const TEST_DESCRIPTION = 'Test description...'; + +// TODO Vinogradov: Create a separate "disposeWidget" helper function. +const disposeScheduler = async () => ClientFunction(() => { + ($(SCHEDULER_SELECTOR) as any).dxScheduler('dispose'); +}, { dependencies: { SCHEDULER_SELECTOR } })(); + +// General test cases +[ + // Text + { + editor: 'text', + errorMessage: 'appointment\'s text incorrect', + getValue: async (scheduler: Scheduler) => scheduler.appointmentPopup.subjectElement().value, + expectedValue: TEST_TITLE, + cases: [ + { + name: 'expression should work', + options: { + dataSource: [{ + textCustom: TEST_TITLE, + startDate: '2023-12-10T10:00:00', + endDate: '2023-12-10T14:00:00', + }], + textExpr: 'textCustom', + }, + }, + { + name: 'nested expression should work', + options: { + dataSource: [{ + nested: { + textCustom: TEST_TITLE, + }, + startDate: '2023-12-10T10:00:00', + endDate: '2023-12-10T14:00:00', + }], + textExpr: 'nested.textCustom', + }, + }, + { + name: 'deep nested expression should work', + options: { + dataSource: [{ + nestedA: { + nestedB: { + nestedC: { + textCustom: TEST_TITLE, + }, + }, + }, + startDate: '2023-12-10T10:00:00', + endDate: '2023-12-10T14:00:00', + }], + textExpr: 'nestedA.nestedB.nestedC.textCustom', + }, + }, + ], + }, + // Description + { + editor: 'description', + errorMessage: 'appointment\'s description incorrect', + getValue: async (scheduler: Scheduler) => scheduler.appointmentPopup.descriptionElement().value, + expectedValue: TEST_DESCRIPTION, + cases: [ + { + name: 'expression should work', + options: { + dataSource: [{ + text: TEST_TITLE, + startDate: '2023-12-10T10:00:00', + endDate: '2023-12-10T14:00:00', + descriptionCustom: TEST_DESCRIPTION, + }], + descriptionExpr: 'descriptionCustom', + }, + }, + { + name: 'nested expression should work', + options: { + dataSource: [{ + text: TEST_TITLE, + startDate: '2023-12-10T10:00:00', + endDate: '2023-12-10T14:00:00', + nested: { + descriptionCustom: TEST_DESCRIPTION, + }, + }], + descriptionExpr: 'nested.descriptionCustom', + }, + }, + { + name: 'deep nested expression should work', + options: { + dataSource: [{ + text: TEST_TITLE, + startDate: '2023-12-10T10:00:00', + endDate: '2023-12-10T14:00:00', + nestedA: { + nestedB: { + nestedC: { + descriptionCustom: TEST_DESCRIPTION, + }, + }, + }, + }], + descriptionExpr: 'nestedA.nestedB.nestedC.descriptionCustom', + }, + }, + ], + }, + // startDate + { + editor: 'startDate', + errorMessage: 'appointment\'s startDate incorrect', + getValue: async (scheduler: Scheduler) => scheduler.appointmentPopup.startDateElement().value, + expectedValue: '12/10/2023, 10:00 AM', + cases: [ + { + name: 'expression should work', + options: { + dataSource: [{ + text: TEST_TITLE, + startDateCustom: '2023-12-10T10:00:00', + endDate: '2023-12-10T14:00:00', + }], + startDateExpr: 'startDateCustom', + }, + }, + { + name: 'nested expression should work', + options: { + dataSource: [{ + text: TEST_TITLE, + endDate: '2023-12-10T14:00:00', + nested: { + startDateCustom: '2023-12-10T10:00:00', + }, + }], + startDateExpr: 'nested.startDateCustom', + }, + }, + { + name: 'deep nested expression should work', + options: { + dataSource: [{ + text: TEST_TITLE, + endDate: '2023-12-10T14:00:00', + nestedA: { + nestedB: { + nestedC: { + startDateCustom: '2023-12-10T10:00:00', + }, + }, + }, + }], + startDateExpr: 'nestedA.nestedB.nestedC.startDateCustom', + }, + }, + ], + }, + // endDate + { + editor: 'endDate', + errorMessage: 'appointment\'s endDate incorrect', + getValue: async (scheduler: Scheduler) => scheduler.appointmentPopup.endDateElement().value, + expectedValue: '12/10/2023, 2:00 PM', + cases: [ + { + name: 'expression should work', + options: { + dataSource: [{ + text: TEST_TITLE, + startDate: '2023-12-10T10:00:00', + endDateCustom: '2023-12-10T14:00:00', + }], + endDateExpr: 'endDateCustom', + }, + }, + { + name: 'nested expression should work', + options: { + dataSource: [{ + text: TEST_TITLE, + startDate: '2023-12-10T10:00:00', + nested: { + endDateCustom: '2023-12-10T14:00:00', + }, + }], + endDateExpr: 'nested.endDateCustom', + }, + }, + { + name: 'deep nested expression should work', + options: { + dataSource: [{ + text: TEST_TITLE, + startDate: '2023-12-10T10:00:00', + nestedA: { + nestedB: { + nestedC: { + endDateCustom: '2023-12-10T14:00:00', + }, + }, + }, + }], + endDateExpr: 'nestedA.nestedB.nestedC.endDateCustom', + }, + }, + ], + }, + // startDateTimeZone + { + editor: 'startDateTimeZone', + errorMessage: 'appointment\'s startDateTimeZone incorrect', + getValue: async (scheduler: Scheduler) => scheduler + .appointmentPopup.startDateTimeZoneElement().value, + expectedValue: '(GMT -01:00) Etc - GMT+1', + cases: [ + { + name: 'expression should work', + options: { + dataSource: [{ + text: TEST_TITLE, + startDate: '2023-12-10T10:00:00', + endDate: '2023-12-10T14:00:00', + startDateTimeZoneCustom: 'Etc/GMT+1', + }], + editing: { + allowTimeZoneEditing: true, + }, + startDateTimeZoneExpr: 'startDateTimeZoneCustom', + }, + }, + { + name: 'nested expression should work', + options: { + dataSource: [{ + text: TEST_TITLE, + startDate: '2023-12-10T10:00:00', + endDate: '2023-12-10T14:00:00', + nested: { + startDateTimeZoneCustom: 'Etc/GMT+1', + }, + }], + editing: { + allowTimeZoneEditing: true, + }, + startDateTimeZoneExpr: 'nested.startDateTimeZoneCustom', + }, + }, + { + name: 'deep nested expression should work', + options: { + dataSource: [{ + text: TEST_TITLE, + startDate: '2023-12-10T10:00:00', + endDate: '2023-12-10T14:00:00', + nestedA: { + nestedB: { + nestedC: { + startDateTimeZoneCustom: 'Etc/GMT+1', + }, + }, + }, + }], + editing: { + allowTimeZoneEditing: true, + }, + startDateTimeZoneExpr: 'nestedA.nestedB.nestedC.startDateTimeZoneCustom', + }, + }, + ], + }, + // endDateTimeZone + { + editor: 'endDateTimeZone', + errorMessage: 'appointment\'s endDateTimeZone incorrect', + getValue: async (scheduler: Scheduler) => scheduler + .appointmentPopup.endDateTimeZoneElement().value, + expectedValue: '(GMT -02:00) Etc - GMT+2', + cases: [ + { + name: 'expression should work', + options: { + dataSource: [{ + text: TEST_TITLE, + startDate: '2023-12-10T10:00:00', + endDate: '2023-12-10T14:00:00', + endDateTimeZoneCustom: 'Etc/GMT+2', + }], + editing: { + allowTimeZoneEditing: true, + }, + endDateTimeZoneExpr: 'endDateTimeZoneCustom', + }, + }, + { + name: 'nested expression should work', + options: { + dataSource: [{ + text: TEST_TITLE, + startDate: '2023-12-10T10:00:00', + endDate: '2023-12-10T14:00:00', + nested: { + endDateTimeZoneCustom: 'Etc/GMT+2', + }, + }], + editing: { + allowTimeZoneEditing: true, + }, + endDateTimeZoneExpr: 'nested.endDateTimeZoneCustom', + }, + }, + { + name: 'deep nested expression should work', + options: { + dataSource: [{ + text: TEST_TITLE, + startDate: '2023-12-10T10:00:00', + endDate: '2023-12-10T14:00:00', + nestedA: { + nestedB: { + nestedC: { + endDateTimeZoneCustom: 'Etc/GMT+2', + }, + }, + }, + }], + editing: { + allowTimeZoneEditing: true, + }, + endDateTimeZoneExpr: 'nestedA.nestedB.nestedC.endDateTimeZoneCustom', + }, + }, + ], + }, + // allDay + { + editor: 'allDay', + errorMessage: 'appointment\'s allDay incorrect', + getValue: async (scheduler: Scheduler) => scheduler + .appointmentPopup.getAllDaySwitchValue(), + expectedValue: 'true', + cases: [ + { + name: 'expression should work', + options: { + dataSource: [{ + text: TEST_TITLE, + startDate: '2023-12-10T10:00:00', + endDate: '2023-12-10T14:00:00', + allDayCustom: true, + }], + allDayExpr: 'allDayCustom', + }, + }, + { + name: 'nested expression should work', + options: { + dataSource: [{ + text: TEST_TITLE, + startDate: '2023-12-10T10:00:00', + endDate: '2023-12-10T14:00:00', + nested: { + allDayCustom: true, + }, + }], + allDayExpr: 'nested.allDayCustom', + }, + }, + { + name: 'deep nested expression should work', + options: { + dataSource: [{ + text: TEST_TITLE, + startDate: '2023-12-10T10:00:00', + endDate: '2023-12-10T14:00:00', + nestedA: { + nestedB: { + nestedC: { + allDayCustom: true, + }, + }, + }, + }], + allDayExpr: 'nestedA.nestedB.nestedC.allDayCustom', + }, + }, + ], + }, + // recurrenceRule + { + editor: 'recurrenceRule', + errorMessage: 'appointment\'s recurrenceRule incorrect', + getValue: async (scheduler: Scheduler) => scheduler + .appointmentPopup.getRecurrenceRuleSwitchValue(), + expectedValue: 'true', + cases: [ + { + name: 'expression should work', + options: { + dataSource: [{ + text: TEST_TITLE, + startDate: '2023-12-10T10:00:00', + endDate: '2023-12-10T14:00:00', + recurrenceRuleCustom: 'FREQ=DAILY', + }], + recurrenceEditMode: 'series', + recurrenceRuleExpr: 'recurrenceRuleCustom', + }, + }, + { + name: 'nested expression should work', + options: { + dataSource: [{ + text: TEST_TITLE, + startDate: '2023-12-10T10:00:00', + endDate: '2023-12-10T14:00:00', + nested: { + recurrenceRuleCustom: 'FREQ=DAILY', + }, + }], + recurrenceEditMode: 'series', + recurrenceRuleExpr: 'nested.recurrenceRuleCustom', + }, + }, + { + name: 'deep nested expression should work', + options: { + dataSource: [{ + text: TEST_TITLE, + startDate: '2023-12-10T10:00:00', + endDate: '2023-12-10T14:00:00', + nestedA: { + nestedB: { + nestedC: { + recurrenceRuleCustom: 'FREQ=DAILY', + }, + }, + }, + }], + recurrenceEditMode: 'series', + recurrenceRuleExpr: 'nestedA.nestedB.nestedC.recurrenceRuleCustom', + }, + }, + ], + }, +].forEach(({ + editor, + errorMessage, + getValue, + expectedValue, + cases, +}) => { + cases.forEach(({ + name, + options, + }) => { + test(`${editor}: ${name}`, async (t) => { + const scheduler = new Scheduler(SCHEDULER_SELECTOR); + const appointment = scheduler.getAppointment(TEST_TITLE); + + await t.expect(appointment).ok(`appointment with title: ${TEST_TITLE} not found.`); + + await t.doubleClick(appointment.element); + + const value = await getValue(scheduler); + + await t.expect(value).eql(expectedValue, errorMessage); + }).before(async () => { + await createWidget('dxScheduler', { + currentDate: '2023-12-10', + cellDuration: 240, + ...options, + }); + }).after(async () => disposeScheduler()); + }); +}); + +test( + 'Appointment popup should has correct width when the nested "recurrenceRuleExpr" option is set', + async (t) => { + const { takeScreenshot, compareResults } = createScreenshotsComparer(t); + const scheduler = new Scheduler(SCHEDULER_SELECTOR); + const appointment = scheduler.getAppointment(TEST_TITLE); + + await t.doubleClick(appointment.element); + await t.expect(scheduler.appointmentPopup.form.exists).ok(); + + await takeScreenshot( + 'form_recurrence-editor-first-opening_nested-expr.png', + scheduler.appointmentPopup.form, + ); + + await t.expect(compareResults.isValid()) + .ok(compareResults.errorMessages()); + }, +).before(async () => { + await createWidget('dxScheduler', { + dataSource: [ + { + startDate: '2023-12-10T10:00:00', + endDate: '2023-12-10T14:00:00', + text: TEST_TITLE, + nestedA: { + nestedB: { + nestedC: { + recurrenceRuleCustom: 'FREQ=DAILY', + }, + }, + }, + }, + ], + currentDate: '2023-12-10', + cellDuration: 240, + recurrenceEditMode: 'series', + recurrenceRuleExpr: 'nestedA.nestedB.nestedC.recurrenceRuleCustom', + }); +}).after(async () => disposeScheduler());