From 974931a17181319d94e07864c342aaf5628782ce Mon Sep 17 00:00:00 2001 From: Aleksey Semikozov Date: Sat, 21 Mar 2026 23:12:11 -0300 Subject: [PATCH 01/13] Playwright POC - migrate TestCafe E2E tests to Playwright - Add Playwright config with CI-matching Chrome flags - Add Playwright test helpers (createWidget, testScreenshot, themeUtils) - Convert 560 TestCafe test files to Playwright format - Add CI workflow for parallel Playwright execution - Scheduler/month tests verified working (15/15 pass) --- .github/workflows/playwright_tests.yml | 170 ++ e2e/testcafe-devextreme/package.json | 25 +- .../playwright-helpers/createWidget.ts | 15 + .../playwright-helpers/domUtils.ts | 74 + .../generateOptionMatrix.ts | 28 + .../playwright-helpers/index.ts | 26 + .../playwright-helpers/testPageUtils.ts | 49 + .../playwright-helpers/themeUtils.ts | 74 + .../accessibility/accordion.spec.ts | 20 + .../accessibility/actionSheet.spec.ts | 20 + .../accessibility/autocomplete.spec.ts | 20 + .../accessibility/button.spec.ts | 20 + .../accessibility/buttonGroup.spec.ts | 20 + .../accessibility/calendar.spec.ts | 20 + .../cardView/columnChooser.spec.ts | 28 + .../cardView/columnSortable.spec.ts | 28 + .../accessibility/cardView/cover.spec.ts | 20 + .../accessibility/cardView/editing.spec.ts | 28 + .../cardView/filterPanel.spec.ts | 20 + .../cardView/headerFilter.spec.ts | 28 + .../cardView/headerPanel.spec.ts | 36 + .../accessibility/cardView/noData.spec.ts | 20 + .../accessibility/cardView/pager.spec.ts | 20 + .../accessibility/cardView/search.spec.ts | 20 + .../accessibility/cardView/selection.spec.ts | 36 + .../accessibility/cardView/sortable.spec.ts | 28 + .../accessibility/cardView/sorting.spec.ts | 44 + .../accessibility/chat.spec.ts | 20 + .../accessibility/checkBox.spec.ts | 20 + .../accessibility/colorBox.spec.ts | 20 + .../accessibility/contextMenu.spec.ts | 20 + .../accessibility/dataGrid/common.spec.ts | 20 + .../accessibility/dataGrid/editing.spec.ts | 20 + .../dataGrid/fixedColumns.spec.ts | 20 + .../accessibility/dataGrid/scrolling.spec.ts | 20 + .../accessibility/dataGrid/status.spec.ts | 20 + .../accessibility/dataGrid/templates.spec.ts | 20 + .../accessibility/dateBox.spec.ts | 20 + .../accessibility/dateRangeBox.spec.ts | 20 + .../accessibility/drawer.spec.ts | 20 + .../accessibility/dropDownBox.spec.ts | 20 + .../accessibility/dropDownButton.spec.ts | 20 + .../accessibility/fileUploader.spec.ts | 20 + .../accessibility/filterBuilder.spec.ts | 20 + .../floatingActionButton.spec.ts | 20 + .../accessibility/form.spec.ts | 20 + .../accessibility/gallery.spec.ts | 20 + .../accessibility/htmlEditor.spec.ts | 20 + .../accessibility/list.spec.ts | 20 + .../accessibility/loadIndicator.spec.ts | 20 + .../accessibility/loadPanel.spec.ts | 20 + .../accessibility/lookup.spec.ts | 20 + .../accessibility/menu.spec.ts | 20 + .../accessibility/multiView.spec.ts | 20 + .../accessibility/numberBox.spec.ts | 20 + .../accessibility/pagination.spec.ts | 20 + .../accessibility/popover.spec.ts | 20 + .../accessibility/popup.spec.ts | 20 + .../accessibility/progressBar.spec.ts | 20 + .../accessibility/radioGroup.spec.ts | 20 + .../accessibility/rangeSlider.spec.ts | 20 + .../scheduler/appointment.spec.ts | 20 + .../scheduler/appointmentForm.spec.ts | 20 + .../scheduler/legacyPopup.spec.ts | 20 + .../accessibility/scheduler/scheduler.spec.ts | 20 + .../accessibility/scheduler/status.spec.ts | 20 + .../accessibility/selectBox.spec.ts | 20 + .../accessibility/slider.spec.ts | 20 + .../accessibility/speechToText.spec.ts | 20 + .../accessibility/splitter.spec.ts | 20 + .../accessibility/stepper.spec.ts | 20 + .../accessibility/switch.spec.ts | 20 + .../accessibility/tabPanel.spec.ts | 20 + .../accessibility/tabs.spec.ts | 20 + .../accessibility/tagBox.spec.ts | 20 + .../accessibility/textArea.spec.ts | 20 + .../accessibility/textBox.spec.ts | 20 + .../accessibility/tileView.spec.ts | 20 + .../accessibility/toast.spec.ts | 20 + .../accessibility/toolbar.spec.ts | 20 + .../accessibility/tooltip.spec.ts | 20 + .../accessibility/treeList/aria.spec.ts | 20 + .../accessibility/treeList/common.spec.ts | 20 + .../accessibility/treeList/status.spec.ts | 20 + .../accessibility/treeView.spec.ts | 20 + .../accessibility/validationSummary.spec.ts | 20 + .../columnChooser/a11y.functional.spec.ts | 33 + .../columnChooser/api.functional.spec.ts | 51 + .../cardView/columnChooser/functional.spec.ts | 102 ++ .../cardView/columnChooser/visual.spec.ts | 85 + .../columnSortable/functional.spec.ts | 44 + .../cardView/columnSortable/visual.spec.ts | 35 + .../common/behavior.functional.spec.ts | 27 + .../cardView/contentView.events.spec.ts | 63 + .../contextMenu/behavior.visual.spec.ts | 23 + .../cardView/cover.visual.spec.ts | 31 + .../editing/editing.functional.spec.ts | 42 + .../cardView/editing/editing.visual.spec.ts | 60 + .../api.filterBuilder.functional.spec.ts | 70 + .../api.filterBuilderPopup.functional.spec.ts | 50 + .../filterPanel/api.functional.spec.ts | 59 + .../filterPanel/behavior.functional.spec.ts | 69 + .../filterPanel/behavior.themes.spec.ts | 40 + .../headerFilter/a11y.functional.spec.ts | 54 + .../headerFilter/api.functional.spec.ts | 34 + .../headerFilter/common.functional.spec.ts | 33 + .../headerFilter/local.functional.spec.ts | 35 + .../headerFilter/remote.functional.spec.ts | 20 + .../cardView/headerFilter/visual.spec.ts | 54 + .../headerPanel/sortable.visual.spec.ts | 20 + .../cardView/headerPanel/visual.spec.ts | 65 + .../cardView/items.functional.spec.ts | 27 + ...pi.onFocusedCardChanged.functional.spec.ts | 61 + .../api.onKeyDown.functional.spec.ts | 54 + .../contentView.functional.spec.ts | 74 + .../header.functional.spec.ts | 46 + .../search.functional.spec.ts | 33 + .../selection.functional.spec.ts | 66 + .../cardView/loadPanel.visual.spec.ts | 42 + .../cardView/noData.visual.spec.ts | 23 + .../playwright-tests/cardView/pager.spec.ts | 77 + .../cardView/search/a11y.functional.spec.ts | 27 + .../cardView/search/api.functional.spec.ts | 53 + .../search/behavior.functional.spec.ts | 39 + .../cardView/search/visual.spec.ts | 32 + .../cardView/security.functional.spec.ts | 25 + .../cardView/selection/functional.spec.ts | 96 ++ .../cardView/selection/visual.spec.ts | 70 + .../cardView/sorting/api.themes.spec.ts | 57 + .../sorting/behavior.functional.spec.ts | 79 + .../cardView/sorting/behavior.themes.spec.ts | 57 + .../playwright-tests/common/draggable.spec.ts | 94 ++ .../common/eventsEngine.spec.ts | 87 + .../filterBuilder/filterBuilderEditor.spec.ts | 54 + .../filterBuilder/filterBuilderNaming.spec.ts | 64 + .../filterBuilderScrolling.spec.ts | 37 + .../common/filterBuilder/index.spec.ts | 96 ++ .../common/gantt/common.spec.ts | 158 ++ .../playwright-tests/common/icons.spec.ts | 428 +++++ .../common/pagination/accessibility.spec.ts | 50 + .../common/pagination/baseProperties.spec.ts | 156 ++ .../common/pagination/index.spec.ts | 78 + .../common/pivotGrid/contextMenu.spec.ts | 105 ++ .../export/onExportingOption.spec.ts | 43 + .../T1138119_dragAndDropAreaItems.spec.ts | 135 ++ .../fieldChooser/fieldChooser.spec.ts | 600 +++++++ .../T1283238_OLAP_drag_and_drop_field.spec.ts | 63 + .../T1287521_fields_aria_label.spec.ts | 64 + .../fieldPanel/dragAndDropFieldItems.spec.ts | 126 ++ .../common/pivotGrid/headerFilter.spec.ts | 100 ++ .../runningTotal/runningTotal.spec.ts | 153 ++ .../common/pivotGrid/scrolling.spec.ts | 227 +++ .../pivotGrid/sort/localSort_T1150523.spec.ts | 193 +++ ...ortWithSummaryDisplayMode_T1173442.spec.ts | 119 ++ .../virtualScrolling_T1210807.spec.ts | 88 + .../playwright-tests/common/shadowDOM.spec.ts | 95 ++ .../common/treeList/API.spec.ts | 83 + .../common/treeList/adaptiveRow.spec.ts | 71 + .../treeList/aiColumn/functional.spec.ts | 515 ++++++ .../common/treeList/aiColumn/visual.spec.ts | 103 ++ .../common/treeList/columns.spec.ts | 93 ++ .../common/treeList/editing/editing.spec.ts | 72 + .../common/treeList/focus.spec.ts | 79 + .../common/treeList/focusedRow.spec.ts | 119 ++ .../customButtons.functional.spec.ts | 146 ++ .../keyboardNavigation.functional.spec.ts | 152 ++ .../markup.screenshots.spec.ts | 125 ++ .../onClick.functional.spec.ts | 65 + .../skipDragCell.functional.spec.ts | 150 ++ .../common/treeList/markup.spec.ts | 272 +++ .../common/treeList/rowDragging.spec.ts | 102 ++ .../common/treeList/scrolling.spec.ts | 154 ++ .../common/treeList/searchPanel.spec.ts | 83 + .../common/treeList/selection.spec.ts | 134 ++ .../stickyColumns/stickyColumns.spec.ts | 111 ++ .../stickyColumns/withDragAndDrop.spec.ts | 52 + .../common/treeList/toast.spec.ts | 30 + .../common/accessibility/bugs.spec.ts | 31 + .../common/accessibility/common.spec.ts | 41 + .../common/accessibility/contrast.spec.ts | 74 + .../dataGrid/common/adaptiveRow.spec.ts | 66 + .../aiColumn/adaptivity.functional.spec.ts | 68 + .../aiColumn/columnChooser.functional.spec.ts | 70 + .../aiColumn/columnFixing.functional.spec.ts | 50 + .../aiColumn/columnFixing.visual.spec.ts | 52 + .../columnReordering.functional.spec.ts | 52 + .../aiColumn/columnReordering.visual.spec.ts | 57 + .../columnResizing.functional.spec.ts | 58 + .../aiColumn/columnResizing.visual.spec.ts | 51 + .../common/aiColumn/functional.spec.ts | 47 + .../keyboardNavigation.visual.spec.ts | 66 + .../virtualScrolling.functional.spec.ts | 144 ++ .../dataGrid/common/aiColumn/visual.spec.ts | 43 + .../common/bandColumns/runtimeChange.spec.ts | 115 ++ .../dataGrid/common/builder.spec.ts | 47 + .../dataGrid/common/columnChooser.spec.ts | 52 + .../columnReordering/functional.spec.ts | 80 + .../common/columnReordering/visual.spec.ts | 57 + .../common/columnResizing/functional.spec.ts | 65 + .../common/columnResizing/visual.spec.ts | 75 + .../editing/T1154721_editingCellFocus.spec.ts | 88 + .../T1323684_readonlyEditorNewRow.spec.ts | 64 + .../editing/editing.functional_matrix.spec.ts | 615 +++++++ .../common/editing/editingEvents.spec.ts | 118 ++ .../editingNewRow.functional_matrix.spec.ts | 239 +++ .../common/editing/functional.spec.ts | 62 + .../common/editing/initNewRow.spec.ts | 49 + .../common/editing/undefinedValues.spec.ts | 54 + .../dataGrid/common/editing/visual.spec.ts | 47 + .../dataGrid/common/export/export.spec.ts | 31 + .../dataGrid/common/exportButton.spec.ts | 26 + .../common/filterPanel/functional.spec.ts | 157 ++ .../common/filterPanel/visual.spec.ts | 40 + ...100_changeFIlterIcon.visual_matrix.spec.ts | 68 + .../common/filterRow/functional.spec.ts | 58 + .../dataGrid/common/filterRow/visual.spec.ts | 32 + .../common/filtering/functional.spec.ts | 58 + .../dataGrid/common/filtering/visual.spec.ts | 53 + .../common/fixedColumns/functional.spec.ts | 138 ++ .../common/fixedColumns/visual.spec.ts | 89 + .../dataGrid/common/focus/focus.spec.ts | 44 + .../focusEvents/newRows_T1162227.spec.ts | 147 ++ .../focus/focusShowEditorAlwaysCell.spec.ts | 100 ++ .../focus/focusedRow/focusedRow.spec.ts | 46 + .../common/focus/focusedRow/markup.spec.ts | 65 + .../T1162057_oneGroupOnDifferentPages.spec.ts | 130 ++ .../calculateGroupValueRuntimeChanges.spec.ts | 224 +++ .../dataGrid/common/grouping/grouping.spec.ts | 40 + .../common/headerFilter/headerFilter.spec.ts | 57 + .../headerFilter/headerFilterList.spec.ts | 56 + .../dataGrid/common/headerPanel.spec.ts | 81 + .../columnReordering.visual.spec.ts | 44 + .../customButtons.functional.spec.ts | 75 + .../keyboardNavigation/editOnKeyPress.spec.ts | 64 + .../groupColumnReordering.functional.spec.ts | 53 + .../groupColumnReordering.visual.spec.ts | 58 + .../keyboardNavigation.functional.spec.ts | 127 ++ .../keyboardNavigation.visual.spec.ts | 42 + .../markup.screenshots.spec.ts | 58 + .../masterDetail/index.spec.ts | 37 + .../skipDragCell.functional.spec.ts | 75 + .../startEditing.functional.spec.ts | 44 + .../virtualColumns.functional.spec.ts | 72 + .../T1163515_alternateRowGroupBorders.spec.ts | 372 +++++ .../markup/T1240074_hoveringRows.spec.ts | 37 + .../markup/T1286265_deletedRowHeight.spec.ts | 69 + .../markup/T838734_alternateRowSizes.spec.ts | 57 + .../dataGrid/common/markup/iconSizes.spec.ts | 58 + .../dataGrid/common/markup/markup.spec.ts | 37 + .../dataGrid/common/markup/noDataText.spec.ts | 69 + .../dataGrid/common/masterDetail.spec.ts | 77 + .../dataGrid/common/pager.spec.ts | 70 + .../common/rowDragging/functional.spec.ts | 160 ++ .../common/rowDragging/visual.spec.ts | 62 + .../dataGrid/common/scrolling.spec.ts | 73 + .../dataGrid/common/searchPanel.spec.ts | 50 + .../dataGrid/common/security/xss.spec.ts | 39 + .../dataGrid/common/selection.spec.ts | 48 + .../dataGrid/common/sorting/sorting.spec.ts | 58 + .../common/stateStoring/stateStoring.spec.ts | 66 + .../dataGrid/common/summary.spec.ts | 47 + .../dataGrid/common/tagBox.spec.ts | 58 + .../dataGrid/common/toast.spec.ts | 25 + .../common/validation/cellEditing.spec.ts | 60 + .../common/validation/validationPopup.spec.ts | 55 + .../common/virtualColumns/functional.spec.ts | 55 + .../common/virtualColumns/visual.spec.ts | 73 + .../dataGrid/sticky/common/appearance.spec.ts | 71 + .../sticky/common/columnFixingIcons.spec.ts | 35 + .../sticky/common/focusOverlay.spec.ts | 116 ++ .../common/stickyColumnReordering.spec.ts | 42 + .../common/stickyColumnResizing.spec.ts | 57 + .../sticky/common/stickyColumns.spec.ts | 51 + .../sticky/common/withAdaptability.spec.ts | 47 + .../sticky/common/withBandColumns.spec.ts | 52 + .../sticky/common/withDragAndDrop.spec.ts | 48 + .../sticky/common/withEditing.spec.ts | 39 + .../sticky/common/withFilterRow.spec.ts | 38 + .../sticky/common/withGrouping.spec.ts | 81 + .../common/withKeyboardNavigation.spec.ts | 83 + .../sticky/common/withMasterDetail.spec.ts | 40 + .../sticky/common/withMultiRow.spec.ts | 45 + .../sticky/common/withRowSelection.spec.ts | 37 + .../sticky/common/withVirtualColumns.spec.ts | 45 + .../common/withVirtualScrolling.spec.ts | 57 + .../sticky/fixed/bandColumnFirstCases.spec.ts | 74 + .../fixed/bandColumnSecondCases.spec.ts | 65 + .../dataGrid/sticky/fixed/positions.spec.ts | 45 + .../editors/autocomplete/common.spec.ts | 34 + .../editors/calendar/common.spec.ts | 364 +++++ .../editors/calendar/keyboard.spec.ts | 147 ++ .../editors/chat/alertList.spec.ts | 79 + .../editors/chat/avatar.spec.ts | 68 + .../editors/chat/confirmationPopup.spec.ts | 51 + .../editors/chat/messageBox.spec.ts | 107 ++ .../editors/chat/messageBubble.spec.ts | 84 + .../editors/chat/messageGroup.spec.ts | 174 ++ .../editors/chat/messageList.spec.ts | 362 ++++ .../editors/chat/typingIndicator.spec.ts | 117 ++ .../editors/checkBox/common.spec.ts | 122 ++ .../checkBox/validationMessage.spec.ts | 82 + .../editors/colorbox/colorbox.spec.ts | 53 + .../editors/dateBox/common.spec.ts | 82 + .../editors/dateBox/dateBox.spec.ts | 94 ++ .../editors/dateBox/dateBoxGeometry.spec.ts | 54 + .../editors/dateBox/keyboard.spec.ts | 624 +++++++ .../editors/dateBox/label.spec.ts | 79 + .../editors/dateBox/validationMessage.spec.ts | 54 + .../editors/dateRangeBox/behavior.spec.ts | 1453 +++++++++++++++++ .../editors/dateRangeBox/calendar.spec.ts | 673 ++++++++ .../editors/dateRangeBox/common.spec.ts | 136 ++ .../editors/dateRangeBox/focus.spec.ts | 674 ++++++++ .../editors/dateRangeBox/keyboard.spec.ts | 1260 ++++++++++++++ .../dateRangeBox/validationMessage.spec.ts | 89 + .../T1245111_dropDownBox_height.spec.ts | 43 + .../editors/dropDownBox/popup.spec.ts | 32 + .../editors/dropDownButton/common.spec.ts | 125 ++ .../editors/dropDownButton/popup.spec.ts | 42 + .../editors/fileManager/common.spec.ts | 31 + .../editors/fileUploader/index.spec.ts | 35 + .../editors/htmlEditor/common.spec.ts | 78 + .../addImage/addImageFromDevice.spec.ts | 123 ++ .../dialogs/addImage/addImageUrl.spec.ts | 132 ++ .../dialogs/addImage/common.spec.ts | 152 ++ .../dialogs/aiDialog/common.spec.ts | 286 ++++ .../editors/htmlEditor/format.spec.ts | 48 + .../editors/htmlEditor/list.spec.ts | 79 + .../editors/loadIndIcator/common.spec.ts | 56 + .../editors/lookup/common.spec.ts | 169 ++ .../editors/numberBox/label.spec.ts | 85 + .../editors/overlays/dialog.spec.ts | 49 + .../editors/overlays/popup.drag.spec.ts | 162 ++ .../editors/overlays/popup.spec.ts | 167 ++ .../resizeObserverIntegration.spec.ts | 242 +++ .../editors/overlays/scrolling.spec.ts | 143 ++ .../editors/overlays/toast.spec.ts | 55 + .../overlays/toolbarIntegration.spec.ts | 242 +++ .../editors/radioGroup/common.spec.ts | 101 ++ .../radioGroup/validationMessage.spec.ts | 57 + .../editors/selectBox/actionButton.spec.ts | 189 +++ .../editors/selectBox/common.spec.ts | 123 ++ .../editors/selectBox/label.spec.ts | 57 + .../editors/selectBox/popup.spec.ts | 138 ++ .../selectBox/toolbarIntegration.spec.ts | 47 + .../editors/slider/slider.spec.ts | 29 + .../editors/tagBox/common.spec.ts | 159 ++ .../editors/tagBox/label.spec.ts | 95 ++ .../editors/textArea/index.spec.ts | 182 +++ .../editors/textArea/label.spec.ts | 67 + .../editors/textBox/label.spec.ts | 204 +++ .../editors/textBox/mask.spec.ts | 32 + .../editors/textBox/validationMessage.spec.ts | 49 + .../navigation/accordion/common.spec.ts | 52 + .../navigation/button/common.spec.ts | 134 ++ .../navigation/button/floatingAction.spec.ts | 126 ++ .../button/floatingActionInGrid.spec.ts | 92 ++ .../navigation/buttonGroup/common.spec.ts | 44 + .../navigation/buttonGroup/selection.spec.ts | 80 + .../navigation/contextMenu/common.spec.ts | 80 + .../contextMenu/contextMenu.spec.ts | 57 + .../navigation/contextMenu/scrolling.spec.ts | 42 + .../navigation/drawer/common.spec.ts | 118 ++ .../navigation/form/itemTypes.spec.ts | 52 + .../navigation/form/labels.spec.ts | 252 +++ .../navigation/form/layout.spec.ts | 275 ++++ .../navigation/gallery/common.spec.ts | 67 + .../navigation/list/common.spec.ts | 376 +++++ .../navigation/list/focus.spec.ts | 154 ++ .../navigation/list/grouping.spec.ts | 110 ++ .../navigation/list/paging.spec.ts | 222 +++ .../navigation/list/search.spec.ts | 37 + .../navigation/menu/common.spec.ts | 171 ++ .../navigation/menu/delimiter.spec.ts | 150 ++ .../navigation/menu/keyboard.spec.ts | 137 ++ .../navigation/scrollView/scrollView.spec.ts | 54 + .../navigation/scrollable/integration.spec.ts | 53 + .../navigation/scrollable/scrollable.spec.ts | 478 ++++++ .../navigation/scrollable/visibility.spec.ts | 67 + .../navigation/splitter/common.spec.ts | 120 ++ .../navigation/splitter/events.spec.ts | 44 + .../navigation/splitter/integration.spec.ts | 69 + .../navigation/splitter/keyboard.spec.ts | 101 ++ .../navigation/splitter/resize.spec.ts | 43 + .../navigation/stepper/common.spec.ts | 192 +++ .../navigation/tabPanel/common.spec.ts | 365 +++++ .../navigation/tabPanel/focus.spec.ts | 266 +++ .../navigation/tabs/common.spec.ts | 201 +++ .../navigation/toolbar/common.spec.ts | 111 ++ .../navigation/toolbar/overflowMenu.spec.ts | 350 ++++ .../toolbar/overflowMenuPopup.spec.ts | 95 ++ .../navigation/treeView/common.spec.ts | 264 +++ .../scheduler/common/a11y/contrast.spec.ts | 37 + .../common/adaptive.weekView.spec.ts | 62 + .../scheduler/common/agenda/API.spec.ts | 48 + .../scheduler/common/agenda/adaptive.spec.ts | 47 + .../scheduler/common/agenda/editing.spec.ts | 34 + .../scheduler/common/agenda/keyField.spec.ts | 164 ++ .../scheduler/common/agenda/layout.spec.ts | 183 +++ .../common/agenda/switchingToAgenda.spec.ts | 42 + .../scheduler/common/agenda/tooltip.spec.ts | 51 + .../common/api/deleteRecurrence.spec.ts | 115 ++ .../common/api/resourceRequestCount.spec.ts | 30 + .../appointmentForm/form.functional.spec.ts | 163 ++ .../appointmentForm/form.visual.spec.ts | 319 ++++ .../recurrence-form.visual.spec.ts | 214 +++ .../appointmentOverlapping/basic.spec.ts | 120 ++ .../common/appointments/T1017889.spec.ts | 36 + .../common/appointments/adaptive.spec.ts | 138 ++ .../common/appointments/allDay/allDay.spec.ts | 98 ++ .../allDay/allDayEndsAtMidnight.spec.ts | 110 ++ .../appointment_collector.spec.ts | 85 + .../appointments/dependendOptions.spec.ts | 39 + .../appointments/displayArguments.spec.ts | 65 + .../common/appointments/legacyEditing.spec.ts | 119 ++ .../maxAppointmentsPerCell/allDay.spec.ts | 106 ++ .../maxAppointmentsPerCell/day.spec.ts | 104 ++ .../maxAppointmentsPerCell/month.spec.ts | 106 ++ .../maxAppointmentsPerCell/timeline.spec.ts | 103 ++ .../maxAppointmentsPerCell/week.spec.ts | 103 ++ .../common/appointments/multiday.spec.ts | 297 ++++ .../appointments/multiday_screenshot.spec.ts | 41 + .../onAppointmentDeleting.spec.ts | 91 ++ .../common/appointments/resources.spec.ts | 207 +++ .../common/appointments/timelineMonth.spec.ts | 75 + .../appointments/timelineWorkWeek.spec.ts | 102 ++ .../appointments/workWeek/interval.spec.ts | 59 + .../bothDirectionsVirtualScrolling.spec.ts | 94 ++ .../cellsSelection/cellsSelection.spec.ts | 30 + .../virtualScrollingCellSelection.spec.ts | 85 + .../scheduler/common/dataSource/load.spec.ts | 64 + .../common/deleteAppointments.spec.ts | 108 ++ .../common/dragAndDrop/DNDToFakeCell.spec.ts | 55 + .../common/dragAndDrop/T1017720.spec.ts | 72 + .../common/dragAndDrop/T1080232.spec.ts | 82 + .../common/dragAndDrop/T1118059.spec.ts | 143 ++ .../common/dragAndDrop/T1235433.spec.ts | 157 ++ .../common/dragAndDrop/T1263508.spec.ts | 81 + .../common/dragAndDrop/T697037.spec.ts | 41 + .../dragAndDrop/appointmentCollector.spec.ts | 148 ++ .../common/dragAndDrop/basic.spec.ts | 166 ++ .../dragAppointmentInEqualCellIndexes.spec.ts | 50 + .../dragAppointmentWithDataSource.spec.ts | 75 + .../removeDroppableCellClass.spec.ts | 56 + .../dragAndDrop/cancelAppointmentDrag.spec.ts | 49 + .../dragAppointmentAfterResize.spec.ts | 79 + .../common/dragAndDrop/dragEvents.spec.ts | 187 +++ .../dragAndDrop/externalDragging.spec.ts | 65 + .../removeDroppableCellClass.spec.ts | 46 + .../dragAndDrop/outlookDragging/base.spec.ts | 432 +++++ .../schedulerInContainer.spec.ts | 63 + .../schedulerInTransformContainer.spec.ts | 66 + .../outlookDragging/shiftedContainer.spec.ts | 59 + .../common/dragAndDrop/timeline.spec.ts | 50 + .../dragAndDrop/verticalGrouping.spec.ts | 67 + .../grouping/groupHeaderLongNamesCss.spec.ts | 146 ++ .../common/grouping/groupingByDate.spec.ts | 90 + .../monthViewVerticalGrouping.spec.ts | 59 + .../common/grouping/overflow.spec.ts | 82 + .../grouping/resourceCellTemplate.spec.ts | 47 + .../common/grouping/smoothCellLines.spec.ts | 37 + .../common/header/customization.spec.ts | 79 + .../common/header/dateNavigator.spec.ts | 147 ++ .../scheduler/common/header/header.spec.ts | 178 ++ .../common/header/header_material.spec.ts | 63 + .../common/header/multiline_header.spec.ts | 43 + .../scheduler/common/header/sizes.spec.ts | 48 + .../common/header/todayButton.spec.ts | 56 + .../header/toolbar_option_change.spec.ts | 103 ++ .../common/header/viewSwitcher.spec.ts | 114 ++ .../hotkeysBehaviour/hotkeysBehaviour.spec.ts | 127 ++ .../keyboardNavigation/appointments.spec.ts | 145 ++ .../keyboardNavigation/dateTable.spec.ts | 56 + .../documentScrollPrevented.spec.ts | 61 + .../common/layout/adaptive/adaptive.spec.ts | 152 ++ .../adaptive/resize/browserResize.spec.ts | 87 + .../allDayPanel/allDayPanelMode.spec.ts | 100 ++ .../appointments/allDay/allDayExpr.spec.ts | 57 + .../allDay/longAppointment.spec.ts | 56 + .../layout/appointments/collector.spec.ts | 63 + .../layout/appointments/dataSource.spec.ts | 38 + .../layout/appointments/disable.spec.ts | 53 + .../appointments/longAppointments.spec.ts | 55 + .../layout/appointments/noSubject.spec.ts | 38 + .../layout/appointments/recurrence.spec.ts | 28 + .../appointments/two-schedulers.spec.ts | 51 + .../layout/appointments/visible.spec.ts | 32 + .../layout/customization/cellSizes.spec.ts | 87 + .../layout/customization/cellSizesCss.spec.ts | 64 + .../layout/customization/groupPanel.spec.ts | 71 + .../layout/customization/headerPanel.spec.ts | 71 + .../layout/customization/timePanel.spec.ts | 81 + .../legacyAppointmentForm/allDay.spec.ts | 133 ++ .../legacyAppointmentForm/common.spec.ts | 48 + .../integerFormatNumberBox.spec.ts | 49 + .../layout/resources/base/resources.spec.ts | 106 ++ .../layout/resources/groups/groups.spec.ts | 94 ++ .../templates/appointmentTemplate.spec.ts | 78 + .../appointmentTemplateTargetedData.spec.ts | 280 ++++ .../layout/templates/cellTemplate.spec.ts | 118 ++ .../layout/templates/tooltipTemplate.spec.ts | 60 + .../currentTimeIndicator.spec.ts | 126 ++ .../layout/timeIndication/shader.spec.ts | 123 ++ .../shaderVirtualScrolling.spec.ts | 91 ++ .../layout/views/crossScrolling.spec.ts | 94 ++ .../common/layout/views/day/allDay.spec.ts | 70 + .../layout/views/firstDayOfWeek.spec.ts | 37 + .../intervalCount/viewsWithStartDate.spec.ts | 121 ++ .../views/material/withoutAllDay.spec.ts | 35 + .../views/timeline/crossScrolling.spec.ts | 81 + .../layout/views/timeline/grouping.spec.ts | 49 + .../layout/views/timeline/month.spec.ts | 37 + .../common/legacyAppointmentForm.spec.ts | 210 +++ .../appointmentPopupErrors.spec.ts | 48 + .../legacyAppointmentForm/dataEditors.spec.ts | 175 ++ .../legacyAppointmentForm/expressions.spec.ts | 622 +++++++ .../recurrenceEditor.spec.ts | 160 ++ .../showAppointmentPopup.spec.ts | 81 + .../timezoneEditors.spec.ts | 97 ++ .../scheduler/common/loadingPanel.spec.ts | 55 + .../month/diferrentInvtervalCounts.spec.ts | 48 + .../month/etalons/month-february-2021.png | Bin 0 -> 20120 bytes .../month/etalons/month-interval-count-12.png | Bin 0 -> 18397 bytes ...l-false-text-Appointment-spans-2-rows-.png | Bin 0 -> 26385 bytes ...l-false-text-Appointment-spans-3-rows-.png | Bin 0 -> 29454 bytes ...false-text-Appointment-spans-all-rows-.png | Bin 0 -> 37622 bytes ...tl-true-text-Appointment-spans-2-rows-.png | Bin 0 -> 26058 bytes ...tl-true-text-Appointment-spans-3-rows-.png | Bin 0 -> 29208 bytes ...-true-text-Appointment-spans-all-rows-.png | Bin 0 -> 38408 bytes ...ent-several-months-february-rtl-false-.png | Bin 0 -> 27026 bytes ...ment-several-months-february-rtl-true-.png | Bin 0 -> 26939 bytes ...ment-several-months-january-rtl-false-.png | Bin 0 -> 26004 bytes ...tment-several-months-january-rtl-true-.png | Bin 0 -> 25799 bytes ...ntment-several-months-march-rtl-false-.png | Bin 0 -> 22132 bytes ...intment-several-months-march-rtl-true-.png | Bin 0 -> 21986 bytes ...ce-appointment-several-months-february.png | Bin 0 -> 35347 bytes ...nce-appointment-several-months-january.png | Bin 0 -> 32742 bytes .../regression-month-february-2021.png | Bin 0 -> 20120 bytes .../regression-month-with-appointment.png | Bin 0 -> 22949 bytes .../common/month/longAppointments.spec.ts | 121 ++ .../common/month/outOfDayHours.spec.ts | 50 + .../common/month/regression-detection.spec.ts | 49 + .../scheduler/common/navigator.spec.ts | 92 ++ .../appointmentTooltip.timeZone.spec.ts | 63 + .../common/recurrences/basic.spec.ts | 129 ++ .../common/recurrences/dialog.spec.ts | 72 + .../scheduler/common/rerenderOnResize.spec.ts | 96 ++ .../resizeAppointments/T1255474.spec.ts | 48 + .../resizeAppointments/T1294528.spec.ts | 174 ++ .../common/resizeAppointments/allDay.spec.ts | 48 + .../common/resizeAppointments/basic.spec.ts | 109 ++ .../cancelAppointmentResize.spec.ts | 106 ++ .../resizeAppointments/timeline.spec.ts | 181 ++ .../verticalGrouping.spec.ts | 90 + .../common/resizeAppointments/zooming.spec.ts | 46 + .../scheduler/common/scrollTo.spec.ts | 337 ++++ ...urrenceAppointmentInDstTimeEditing.spec.ts | 219 +++ .../appointmentWithoutTimezone.spec.ts | 262 +++ .../monthlyRecurrentAppointment.spec.ts | 365 +++++ .../weeklyRecurrentAppointment.spec.ts | 668 ++++++++ .../yearlyRecurrentAppointment.spec.ts | 365 +++++ .../tooltipBehaviour/hideTooltip.spec.ts | 35 + .../tooltipBehaviour/tooltipBehavior.spec.ts | 139 ++ .../scheduler/common/twoSchedulers.spec.ts | 42 + .../common/virtualScrolling/T1091980.spec.ts | 47 + .../common/virtualScrolling/T1258030.spec.ts | 39 + .../common/virtualScrolling/T1287345.spec.ts | 43 + .../virtualScrolling/appointments.spec.ts | 92 ++ .../common/virtualScrolling/layout.spec.ts | 143 ++ .../virtualScrolling/many-cells.spec.ts | 79 + .../common/virtualScrolling/resources.spec.ts | 45 + .../common/virtualScrolling/zooming.spec.ts | 94 ++ .../scheduler/common/workSpace.spec.ts | 373 +++++ .../appointmentCollectorTimezone.spec.ts | 64 + .../scheduler/timezones/check.spec.ts | 36 + .../timezones/dragAndDropDst.spec.ts | 186 +++ .../excludeFromRecurrence_T1225416.spec.ts | 86 + .../timezones/renderCrossDst.spec.ts | 175 ++ .../scheduler/timezones/renderDst.spec.ts | 142 ++ .../viewOffset/common/agenda.spec.ts | 51 + .../viewOffset/common/apiCallbacks.spec.ts | 380 +++++ .../common/currentTimeIndicator.spec.ts | 87 + .../viewOffset/common/dragAndDrop.spec.ts | 79 + .../viewOffset/common/expressions.spec.ts | 90 + .../common/keyboardNavigation.spec.ts | 75 + .../common/multiCellSelection.spec.ts | 87 + .../viewOffset/common/resize.spec.ts | 170 ++ .../markup/allDayAppointments.spec.ts | 139 ++ .../markup/appointmentsOrdering.spec.ts | 151 ++ .../markup/recurrentAppointments.spec.ts | 161 ++ .../markup/usualAppointments.spec.ts | 151 ++ .../markup/virtualScrolling.spec.ts | 62 + e2e/testcafe-devextreme/playwright.config.ts | 56 + pnpm-lock.yaml | 620 +++---- 593 files changed, 55813 insertions(+), 299 deletions(-) create mode 100644 .github/workflows/playwright_tests.yml create mode 100644 e2e/testcafe-devextreme/playwright-helpers/createWidget.ts create mode 100644 e2e/testcafe-devextreme/playwright-helpers/domUtils.ts create mode 100644 e2e/testcafe-devextreme/playwright-helpers/generateOptionMatrix.ts create mode 100644 e2e/testcafe-devextreme/playwright-helpers/index.ts create mode 100644 e2e/testcafe-devextreme/playwright-helpers/testPageUtils.ts create mode 100644 e2e/testcafe-devextreme/playwright-helpers/themeUtils.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/accessibility/accordion.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/accessibility/actionSheet.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/accessibility/autocomplete.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/accessibility/button.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/accessibility/buttonGroup.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/accessibility/calendar.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/accessibility/cardView/columnChooser.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/accessibility/cardView/columnSortable.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/accessibility/cardView/cover.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/accessibility/cardView/editing.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/accessibility/cardView/filterPanel.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/accessibility/cardView/headerFilter.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/accessibility/cardView/headerPanel.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/accessibility/cardView/noData.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/accessibility/cardView/pager.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/accessibility/cardView/search.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/accessibility/cardView/selection.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/accessibility/cardView/sortable.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/accessibility/cardView/sorting.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/accessibility/chat.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/accessibility/checkBox.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/accessibility/colorBox.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/accessibility/contextMenu.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/accessibility/dataGrid/common.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/accessibility/dataGrid/editing.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/accessibility/dataGrid/fixedColumns.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/accessibility/dataGrid/scrolling.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/accessibility/dataGrid/status.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/accessibility/dataGrid/templates.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/accessibility/dateBox.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/accessibility/dateRangeBox.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/accessibility/drawer.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/accessibility/dropDownBox.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/accessibility/dropDownButton.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/accessibility/fileUploader.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/accessibility/filterBuilder.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/accessibility/floatingActionButton.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/accessibility/form.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/accessibility/gallery.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/accessibility/htmlEditor.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/accessibility/list.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/accessibility/loadIndicator.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/accessibility/loadPanel.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/accessibility/lookup.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/accessibility/menu.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/accessibility/multiView.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/accessibility/numberBox.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/accessibility/pagination.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/accessibility/popover.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/accessibility/popup.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/accessibility/progressBar.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/accessibility/radioGroup.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/accessibility/rangeSlider.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/accessibility/scheduler/appointment.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/accessibility/scheduler/appointmentForm.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/accessibility/scheduler/legacyPopup.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/accessibility/scheduler/scheduler.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/accessibility/scheduler/status.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/accessibility/selectBox.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/accessibility/slider.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/accessibility/speechToText.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/accessibility/splitter.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/accessibility/stepper.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/accessibility/switch.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/accessibility/tabPanel.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/accessibility/tabs.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/accessibility/tagBox.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/accessibility/textArea.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/accessibility/textBox.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/accessibility/tileView.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/accessibility/toast.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/accessibility/toolbar.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/accessibility/tooltip.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/accessibility/treeList/aria.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/accessibility/treeList/common.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/accessibility/treeList/status.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/accessibility/treeView.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/accessibility/validationSummary.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/cardView/columnChooser/a11y.functional.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/cardView/columnChooser/api.functional.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/cardView/columnChooser/functional.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/cardView/columnChooser/visual.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/cardView/columnSortable/functional.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/cardView/columnSortable/visual.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/cardView/common/behavior.functional.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/cardView/contentView.events.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/cardView/contextMenu/behavior.visual.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/cardView/cover.visual.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/cardView/editing/editing.functional.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/cardView/editing/editing.visual.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/cardView/filterPanel/api.filterBuilder.functional.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/cardView/filterPanel/api.filterBuilderPopup.functional.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/cardView/filterPanel/api.functional.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/cardView/filterPanel/behavior.functional.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/cardView/filterPanel/behavior.themes.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/cardView/headerFilter/a11y.functional.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/cardView/headerFilter/api.functional.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/cardView/headerFilter/common.functional.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/cardView/headerFilter/local.functional.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/cardView/headerFilter/remote.functional.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/cardView/headerFilter/visual.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/cardView/headerPanel/sortable.visual.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/cardView/headerPanel/visual.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/cardView/items.functional.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/cardView/keyboardNavigation/api.onFocusedCardChanged.functional.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/cardView/keyboardNavigation/api.onKeyDown.functional.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/cardView/keyboardNavigation/contentView.functional.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/cardView/keyboardNavigation/header.functional.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/cardView/keyboardNavigation/search.functional.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/cardView/keyboardNavigation/selection.functional.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/cardView/loadPanel.visual.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/cardView/noData.visual.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/cardView/pager.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/cardView/search/a11y.functional.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/cardView/search/api.functional.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/cardView/search/behavior.functional.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/cardView/search/visual.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/cardView/security.functional.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/cardView/selection/functional.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/cardView/selection/visual.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/cardView/sorting/api.themes.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/cardView/sorting/behavior.functional.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/cardView/sorting/behavior.themes.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/common/draggable.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/common/eventsEngine.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/common/filterBuilder/filterBuilderEditor.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/common/filterBuilder/filterBuilderNaming.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/common/filterBuilder/filterBuilderScrolling.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/common/filterBuilder/index.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/common/gantt/common.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/common/icons.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/common/pagination/accessibility.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/common/pagination/baseProperties.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/common/pagination/index.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/common/pivotGrid/contextMenu.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/common/pivotGrid/export/onExportingOption.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/common/pivotGrid/fieldChooser/T1138119_dragAndDropAreaItems.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/common/pivotGrid/fieldChooser/fieldChooser.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/common/pivotGrid/fieldPanel/T1283238_OLAP_drag_and_drop_field.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/common/pivotGrid/fieldPanel/T1287521_fields_aria_label.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/common/pivotGrid/fieldPanel/dragAndDropFieldItems.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/common/pivotGrid/headerFilter.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/common/pivotGrid/runningTotal/runningTotal.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/common/pivotGrid/scrolling.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/common/pivotGrid/sort/localSort_T1150523.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/common/pivotGrid/sort/sortWithSummaryDisplayMode_T1173442.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/common/pivotGrid/virtualScrolling_T1210807.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/common/shadowDOM.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/common/treeList/API.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/common/treeList/adaptiveRow.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/common/treeList/aiColumn/functional.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/common/treeList/aiColumn/visual.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/common/treeList/columns.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/common/treeList/editing/editing.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/common/treeList/focus.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/common/treeList/focusedRow.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/common/treeList/keyboardNavigation/customButtons.functional.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/common/treeList/keyboardNavigation/keyboardNavigation.functional.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/common/treeList/keyboardNavigation/markup.screenshots.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/common/treeList/keyboardNavigation/onClick.functional.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/common/treeList/keyboardNavigation/skipDragCell.functional.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/common/treeList/markup.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/common/treeList/rowDragging.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/common/treeList/scrolling.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/common/treeList/searchPanel.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/common/treeList/selection.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/common/treeList/stickyColumns/stickyColumns.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/common/treeList/stickyColumns/withDragAndDrop.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/common/treeList/toast.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/dataGrid/common/accessibility/bugs.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/dataGrid/common/accessibility/common.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/dataGrid/common/accessibility/contrast.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/dataGrid/common/adaptiveRow.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/dataGrid/common/aiColumn/adaptivity.functional.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/dataGrid/common/aiColumn/columnChooser.functional.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/dataGrid/common/aiColumn/columnFixing.functional.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/dataGrid/common/aiColumn/columnFixing.visual.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/dataGrid/common/aiColumn/columnReordering.functional.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/dataGrid/common/aiColumn/columnReordering.visual.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/dataGrid/common/aiColumn/columnResizing.functional.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/dataGrid/common/aiColumn/columnResizing.visual.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/dataGrid/common/aiColumn/functional.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/dataGrid/common/aiColumn/keyboardNavigation.visual.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/dataGrid/common/aiColumn/virtualScrolling.functional.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/dataGrid/common/aiColumn/visual.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/dataGrid/common/bandColumns/runtimeChange.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/dataGrid/common/builder.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/dataGrid/common/columnChooser.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/dataGrid/common/columnReordering/functional.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/dataGrid/common/columnReordering/visual.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/dataGrid/common/columnResizing/functional.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/dataGrid/common/columnResizing/visual.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/dataGrid/common/editing/T1154721_editingCellFocus.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/dataGrid/common/editing/T1323684_readonlyEditorNewRow.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/dataGrid/common/editing/editing.functional_matrix.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/dataGrid/common/editing/editingEvents.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/dataGrid/common/editing/editingNewRow.functional_matrix.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/dataGrid/common/editing/functional.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/dataGrid/common/editing/initNewRow.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/dataGrid/common/editing/undefinedValues.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/dataGrid/common/editing/visual.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/dataGrid/common/export/export.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/dataGrid/common/exportButton.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/dataGrid/common/filterPanel/functional.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/dataGrid/common/filterPanel/visual.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/dataGrid/common/filterRow/T1163100_changeFIlterIcon.visual_matrix.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/dataGrid/common/filterRow/functional.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/dataGrid/common/filterRow/visual.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/dataGrid/common/filtering/functional.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/dataGrid/common/filtering/visual.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/dataGrid/common/fixedColumns/functional.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/dataGrid/common/fixedColumns/visual.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/dataGrid/common/focus/focus.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/dataGrid/common/focus/focusEvents/newRows_T1162227.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/dataGrid/common/focus/focusShowEditorAlwaysCell.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/dataGrid/common/focus/focusedRow/focusedRow.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/dataGrid/common/focus/focusedRow/markup.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/dataGrid/common/grouping/T1162057_oneGroupOnDifferentPages.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/dataGrid/common/grouping/calculateGroupValueRuntimeChanges.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/dataGrid/common/grouping/grouping.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/dataGrid/common/headerFilter/headerFilter.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/dataGrid/common/headerFilter/headerFilterList.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/dataGrid/common/headerPanel.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/dataGrid/common/keyboardNavigation/columnReordering.visual.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/dataGrid/common/keyboardNavigation/customButtons.functional.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/dataGrid/common/keyboardNavigation/editOnKeyPress.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/dataGrid/common/keyboardNavigation/groupColumnReordering.functional.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/dataGrid/common/keyboardNavigation/groupColumnReordering.visual.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/dataGrid/common/keyboardNavigation/keyboardNavigation.functional.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/dataGrid/common/keyboardNavigation/keyboardNavigation.visual.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/dataGrid/common/keyboardNavigation/markup.screenshots.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/dataGrid/common/keyboardNavigation/masterDetail/index.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/dataGrid/common/keyboardNavigation/skipDragCell.functional.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/dataGrid/common/keyboardNavigation/startEditing.functional.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/dataGrid/common/keyboardNavigation/virtualColumns.functional.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/dataGrid/common/markup/T1163515_alternateRowGroupBorders.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/dataGrid/common/markup/T1240074_hoveringRows.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/dataGrid/common/markup/T1286265_deletedRowHeight.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/dataGrid/common/markup/T838734_alternateRowSizes.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/dataGrid/common/markup/iconSizes.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/dataGrid/common/markup/markup.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/dataGrid/common/markup/noDataText.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/dataGrid/common/masterDetail.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/dataGrid/common/pager.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/dataGrid/common/rowDragging/functional.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/dataGrid/common/rowDragging/visual.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/dataGrid/common/scrolling.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/dataGrid/common/searchPanel.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/dataGrid/common/security/xss.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/dataGrid/common/selection.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/dataGrid/common/sorting/sorting.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/dataGrid/common/stateStoring/stateStoring.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/dataGrid/common/summary.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/dataGrid/common/tagBox.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/dataGrid/common/toast.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/dataGrid/common/validation/cellEditing.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/dataGrid/common/validation/validationPopup.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/dataGrid/common/virtualColumns/functional.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/dataGrid/common/virtualColumns/visual.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/appearance.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/columnFixingIcons.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/focusOverlay.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/stickyColumnReordering.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/stickyColumnResizing.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/stickyColumns.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/withAdaptability.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/withBandColumns.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/withDragAndDrop.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/withEditing.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/withFilterRow.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/withGrouping.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/withKeyboardNavigation.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/withMasterDetail.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/withMultiRow.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/withRowSelection.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/withVirtualColumns.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/withVirtualScrolling.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/fixed/bandColumnFirstCases.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/fixed/bandColumnSecondCases.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/fixed/positions.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/editors/autocomplete/common.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/editors/calendar/common.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/editors/calendar/keyboard.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/editors/chat/alertList.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/editors/chat/avatar.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/editors/chat/confirmationPopup.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/editors/chat/messageBox.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/editors/chat/messageBubble.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/editors/chat/messageGroup.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/editors/chat/messageList.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/editors/chat/typingIndicator.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/editors/checkBox/common.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/editors/checkBox/validationMessage.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/editors/colorbox/colorbox.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/editors/dateBox/common.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/editors/dateBox/dateBox.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/editors/dateBox/dateBoxGeometry.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/editors/dateBox/keyboard.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/editors/dateBox/label.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/editors/dateBox/validationMessage.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/editors/dateRangeBox/behavior.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/editors/dateRangeBox/calendar.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/editors/dateRangeBox/common.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/editors/dateRangeBox/focus.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/editors/dateRangeBox/keyboard.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/editors/dateRangeBox/validationMessage.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/editors/dropDownBox/T1245111_dropDownBox_height.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/editors/dropDownBox/popup.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/editors/dropDownButton/common.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/editors/dropDownButton/popup.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/editors/fileManager/common.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/editors/fileUploader/index.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/editors/htmlEditor/common.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/editors/htmlEditor/dialogs/addImage/addImageFromDevice.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/editors/htmlEditor/dialogs/addImage/addImageUrl.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/editors/htmlEditor/dialogs/addImage/common.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/editors/htmlEditor/dialogs/aiDialog/common.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/editors/htmlEditor/format.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/editors/htmlEditor/list.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/editors/loadIndIcator/common.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/editors/lookup/common.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/editors/numberBox/label.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/editors/overlays/dialog.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/editors/overlays/popup.drag.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/editors/overlays/popup.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/editors/overlays/resizeObserverIntegration.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/editors/overlays/scrolling.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/editors/overlays/toast.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/editors/overlays/toolbarIntegration.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/editors/radioGroup/common.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/editors/radioGroup/validationMessage.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/editors/selectBox/actionButton.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/editors/selectBox/common.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/editors/selectBox/label.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/editors/selectBox/popup.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/editors/selectBox/toolbarIntegration.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/editors/slider/slider.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/editors/tagBox/common.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/editors/tagBox/label.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/editors/textArea/index.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/editors/textArea/label.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/editors/textBox/label.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/editors/textBox/mask.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/editors/textBox/validationMessage.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/navigation/accordion/common.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/navigation/button/common.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/navigation/button/floatingAction.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/navigation/button/floatingActionInGrid.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/navigation/buttonGroup/common.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/navigation/buttonGroup/selection.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/navigation/contextMenu/common.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/navigation/contextMenu/contextMenu.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/navigation/contextMenu/scrolling.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/navigation/drawer/common.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/navigation/form/itemTypes.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/navigation/form/labels.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/navigation/form/layout.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/navigation/gallery/common.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/navigation/list/common.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/navigation/list/focus.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/navigation/list/grouping.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/navigation/list/paging.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/navigation/list/search.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/navigation/menu/common.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/navigation/menu/delimiter.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/navigation/menu/keyboard.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/navigation/scrollView/scrollView.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/navigation/scrollable/integration.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/navigation/scrollable/scrollable.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/navigation/scrollable/visibility.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/navigation/splitter/common.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/navigation/splitter/events.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/navigation/splitter/integration.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/navigation/splitter/keyboard.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/navigation/splitter/resize.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/navigation/stepper/common.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/navigation/tabPanel/common.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/navigation/tabPanel/focus.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/navigation/tabs/common.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/navigation/toolbar/common.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/navigation/toolbar/overflowMenu.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/navigation/toolbar/overflowMenuPopup.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/navigation/treeView/common.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/a11y/contrast.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/adaptive.weekView.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/agenda/API.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/agenda/adaptive.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/agenda/editing.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/agenda/keyField.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/agenda/layout.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/agenda/switchingToAgenda.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/agenda/tooltip.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/api/deleteRecurrence.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/api/resourceRequestCount.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/appointmentForm/form.functional.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/appointmentForm/form.visual.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/appointmentForm/recurrence-form.visual.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/appointmentOverlapping/basic.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/appointments/T1017889.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/appointments/adaptive.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/appointments/allDay/allDay.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/appointments/allDay/allDayEndsAtMidnight.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/appointments/appointment_collector.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/appointments/dependendOptions.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/appointments/displayArguments.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/appointments/legacyEditing.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/appointments/maxAppointmentsPerCell/allDay.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/appointments/maxAppointmentsPerCell/day.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/appointments/maxAppointmentsPerCell/month.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/appointments/maxAppointmentsPerCell/timeline.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/appointments/maxAppointmentsPerCell/week.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/appointments/multiday.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/appointments/multiday_screenshot.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/appointments/onAppointmentDeleting.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/appointments/resources.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/appointments/timelineMonth.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/appointments/timelineWorkWeek.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/appointments/workWeek/interval.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/cellsSelection/bothDirectionsVirtualScrolling.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/cellsSelection/cellsSelection.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/cellsSelection/virtualScrollingCellSelection.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/dataSource/load.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/deleteAppointments.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/DNDToFakeCell.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/T1017720.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/T1080232.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/T1118059.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/T1235433.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/T1263508.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/T697037.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/appointmentCollector.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/basic.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/betweenSchedulers/dragAppointmentInEqualCellIndexes.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/betweenSchedulers/dragAppointmentWithDataSource.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/betweenSchedulers/removeDroppableCellClass.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/cancelAppointmentDrag.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/dragAppointmentAfterResize.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/dragEvents.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/externalDragging.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/insideScheduler/removeDroppableCellClass.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/outlookDragging/base.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/outlookDragging/schedulerInContainer/schedulerInContainer.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/outlookDragging/schedulerInContainer/schedulerInTransformContainer.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/outlookDragging/shiftedContainer.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/timeline.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/verticalGrouping.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/grouping/groupHeaderLongNamesCss.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/grouping/groupingByDate.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/grouping/monthViewVerticalGrouping.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/grouping/overflow.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/grouping/resourceCellTemplate.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/grouping/smoothCellLines.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/header/customization.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/header/dateNavigator.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/header/header.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/header/header_material.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/header/multiline_header.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/header/sizes.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/header/todayButton.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/header/toolbar_option_change.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/header/viewSwitcher.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/hotkeysBehaviour/hotkeysBehaviour.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/keyboardNavigation/appointments.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/keyboardNavigation/dateTable.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/keyboardNavigation/documentScrollPrevented.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/adaptive/adaptive.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/adaptive/resize/browserResize.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/allDayPanel/allDayPanelMode.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/appointments/allDay/allDayExpr.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/appointments/allDay/longAppointment.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/appointments/collector.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/appointments/dataSource.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/appointments/disable.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/appointments/longAppointments.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/appointments/noSubject.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/appointments/recurrence.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/appointments/two-schedulers.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/appointments/visible.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/customization/cellSizes.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/customization/cellSizesCss.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/customization/groupPanel.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/customization/headerPanel.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/customization/timePanel.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/legacyAppointmentForm/allDay.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/legacyAppointmentForm/common.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/legacyAppointmentForm/integerFormatNumberBox.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/resources/base/resources.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/resources/groups/groups.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/templates/appointmentTemplate.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/templates/appointmentTemplateTargetedData.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/templates/cellTemplate.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/templates/tooltipTemplate.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/timeIndication/currentTimeIndicator.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/timeIndication/shader.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/timeIndication/shaderVirtualScrolling.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/views/crossScrolling.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/views/day/allDay.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/views/firstDayOfWeek.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/views/intervalCount/viewsWithStartDate.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/views/material/withoutAllDay.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/views/timeline/crossScrolling.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/views/timeline/grouping.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/views/timeline/month.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/legacyAppointmentForm.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/legacyAppointmentForm/appointmentPopupErrors.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/legacyAppointmentForm/dataEditors.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/legacyAppointmentForm/expressions.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/legacyAppointmentForm/recurrenceEditor.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/legacyAppointmentForm/showAppointmentPopup.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/legacyAppointmentForm/timezoneEditors.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/loadingPanel.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/month/diferrentInvtervalCounts.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/month/etalons/month-february-2021.png create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/month/etalons/month-interval-count-12.png create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/month/etalons/month-long-appointment-rtl-false-text-Appointment-spans-2-rows-.png create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/month/etalons/month-long-appointment-rtl-false-text-Appointment-spans-3-rows-.png create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/month/etalons/month-long-appointment-rtl-false-text-Appointment-spans-all-rows-.png create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/month/etalons/month-long-appointment-rtl-true-text-Appointment-spans-2-rows-.png create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/month/etalons/month-long-appointment-rtl-true-text-Appointment-spans-3-rows-.png create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/month/etalons/month-long-appointment-rtl-true-text-Appointment-spans-all-rows-.png create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/month/etalons/month-long-appointment-several-months-february-rtl-false-.png create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/month/etalons/month-long-appointment-several-months-february-rtl-true-.png create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/month/etalons/month-long-appointment-several-months-january-rtl-false-.png create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/month/etalons/month-long-appointment-several-months-january-rtl-true-.png create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/month/etalons/month-long-appointment-several-months-march-rtl-false-.png create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/month/etalons/month-long-appointment-several-months-march-rtl-true-.png create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/month/etalons/month-long-recurrence-appointment-several-months-february.png create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/month/etalons/month-long-recurrence-appointment-several-months-january.png create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/month/etalons/regression-month-february-2021.png create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/month/etalons/regression-month-with-appointment.png create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/month/longAppointments.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/month/outOfDayHours.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/month/regression-detection.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/navigator.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/recurrences/appointmentTooltip.timeZone.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/recurrences/basic.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/recurrences/dialog.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/rerenderOnResize.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/resizeAppointments/T1255474.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/resizeAppointments/T1294528.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/resizeAppointments/allDay.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/resizeAppointments/basic.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/resizeAppointments/cancelAppointmentResize.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/resizeAppointments/timeline.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/resizeAppointments/verticalGrouping.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/resizeAppointments/zooming.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/scrollTo.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/timezone/T1102713/recurrenceAppointmentInDstTimeEditing.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/timezone/recurrence/appointmentWithoutTimezone.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/timezone/recurrence/monthlyRecurrentAppointment.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/timezone/recurrence/weeklyRecurrentAppointment.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/timezone/recurrence/yearlyRecurrentAppointment.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/tooltipBehaviour/hideTooltip.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/tooltipBehaviour/tooltipBehavior.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/twoSchedulers.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/virtualScrolling/T1091980.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/virtualScrolling/T1258030.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/virtualScrolling/T1287345.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/virtualScrolling/appointments.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/virtualScrolling/layout.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/virtualScrolling/many-cells.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/virtualScrolling/resources.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/virtualScrolling/zooming.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/common/workSpace.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/timezones/appointmentCollectorTimezone.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/timezones/check.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/timezones/dragAndDropDst.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/timezones/recurrence/excludeFromRecurrence_T1225416.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/timezones/renderCrossDst.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/timezones/renderDst.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/viewOffset/common/agenda.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/viewOffset/common/apiCallbacks.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/viewOffset/common/currentTimeIndicator.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/viewOffset/common/dragAndDrop.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/viewOffset/common/expressions.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/viewOffset/common/keyboardNavigation.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/viewOffset/common/multiCellSelection.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/viewOffset/common/resize.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/viewOffset/markup/allDayAppointments.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/viewOffset/markup/appointmentsOrdering.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/viewOffset/markup/recurrentAppointments.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/viewOffset/markup/usualAppointments.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright-tests/scheduler/viewOffset/markup/virtualScrolling.spec.ts create mode 100644 e2e/testcafe-devextreme/playwright.config.ts diff --git a/.github/workflows/playwright_tests.yml b/.github/workflows/playwright_tests.yml new file mode 100644 index 000000000000..ba1d54b15c3a --- /dev/null +++ b/.github/workflows/playwright_tests.yml @@ -0,0 +1,170 @@ +name: Playwright tests (POC) + +concurrency: + group: wf-${{github.event.pull_request.number || github.sha}}-playwright + cancel-in-progress: true + +on: + pull_request: + workflow_dispatch: + inputs: + repeat_count: + description: 'Number of times to run tests (for stability check)' + required: false + default: '1' + type: string + +env: + NX_SKIP_NX_CACHE: ${{ contains(github.event.pull_request.labels.*.name, 'skip-cache') && 'true' || 'false' }} + +jobs: + build: + name: Build DevExtreme + runs-on: devextreme-shr2 + timeout-minutes: 15 + + steps: + - name: Get sources + uses: actions/checkout@v4 + + - name: Use Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - uses: pnpm/action-setup@v4 + with: + run_install: false + + - name: Get pnpm store directory + shell: bash + run: echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV + + - uses: actions/cache@v4 + name: Setup pnpm cache + with: + path: ${{ env.STORE_PATH }} + key: ${{ runner.os }}-pnpm-cache-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-pnpm-cache + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Build + shell: bash + env: + NODE_OPTIONS: --max-old-space-size=8192 + run: | + pnpx nx build devextreme-scss + pnpx nx build devextreme -c testing + + - name: Zip artifacts + working-directory: ./packages/devextreme + run: 7z a -tzip -mx3 -mmt2 artifacts.zip artifacts ../devextreme-scss/scss/bundles + + - name: Upload build artifacts + uses: actions/upload-artifact@v4 + with: + name: devextreme-artifacts + path: ./packages/devextreme/artifacts.zip + retention-days: 1 + + playwright: + name: ${{ matrix.ARGS.name }} + needs: build + strategy: + fail-fast: false + matrix: + ARGS: [ + { componentFolder: "scheduler/common", name: "scheduler / common (1/3)", shard: "1/3" }, + { componentFolder: "scheduler/common", name: "scheduler / common (2/3)", shard: "2/3" }, + { componentFolder: "scheduler/common", name: "scheduler / common (3/3)", shard: "3/3" }, + { componentFolder: "scheduler/timezones", name: "scheduler / timezones" }, + { componentFolder: "scheduler/viewOffset", name: "scheduler / viewOffset" }, + ] + runs-on: devextreme-shr2 + timeout-minutes: 30 + + steps: + - name: Get sources + uses: actions/checkout@v4 + + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + name: devextreme-artifacts + path: ./packages/devextreme + + - name: Unpack artifacts + working-directory: ./packages/devextreme + run: 7z x artifacts.zip -aoa + + - uses: pnpm/action-setup@v4 + with: + run_install: false + + - name: Get pnpm store directory + shell: bash + run: echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV + + - uses: actions/cache/restore@v4 + name: Restore pnpm cache + with: + path: ${{ env.STORE_PATH }} + key: ${{ runner.os }}-pnpm-cache-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-pnpm-cache + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Install Playwright browsers + working-directory: ./e2e/testcafe-devextreme + run: pnpx playwright install chromium + + - name: Run Playwright tests + working-directory: ./e2e/testcafe-devextreme + env: + NODE_OPTIONS: --max-old-space-size=8192 + THEME: fluent.blue.light + run: | + REPEAT_COUNT="${{ github.event.inputs.repeat_count || '1' }}" + SHARD_ARG="" + if [ "${{ matrix.ARGS.shard }}" != "" ]; then + SHARD_ARG="--shard=${{ matrix.ARGS.shard }}" + fi + + for i in $(seq 1 $REPEAT_COUNT); do + echo "=== Run $i / $REPEAT_COUNT ===" + pnpx playwright test \ + --config playwright.config.ts \ + playwright-tests/${{ matrix.ARGS.componentFolder }}/ \ + $SHARD_ARG \ + --reporter=list \ + 2>&1 | tee -a playwright-output-run-$i.log + echo "" + done + + - name: Upload test results + if: always() + uses: actions/upload-artifact@v4 + with: + name: playwright-results-${{ matrix.ARGS.name }} + path: | + e2e/testcafe-devextreme/playwright-results/ + e2e/testcafe-devextreme/playwright-output-*.log + e2e/testcafe-devextreme/test-results/ + + merge-results: + name: Merge Playwright results + if: always() + needs: playwright + runs-on: devextreme-shr2 + steps: + - name: Merge artifacts + uses: actions/upload-artifact/merge@v4 + with: + name: playwright-all-results + pattern: playwright-results-* + delete-merged: true diff --git a/e2e/testcafe-devextreme/package.json b/e2e/testcafe-devextreme/package.json index f4b479abb1b9..f2f2e4d358e1 100644 --- a/e2e/testcafe-devextreme/package.json +++ b/e2e/testcafe-devextreme/package.json @@ -9,28 +9,31 @@ "devDependencies": { "@babel/eslint-parser": "catalog:eslint8", "@babel/plugin-transform-runtime": "7.29.0", + "@eslint/eslintrc": "catalog:", + "@playwright/test": "^1.58.2", + "@stylistic/eslint-plugin": "catalog:", "@testcafe-community/axe": "3.5.0", "@types/jquery": "catalog:", + "@typescript-eslint/eslint-plugin": "catalog:", + "@typescript-eslint/parser": "catalog:", "axe-core": "catalog:", "devextreme": "workspace:*", "devextreme-screenshot-comparer": "2.0.17", "devextreme-testcafe-models": "workspace:*", + "eslint": "catalog:", + "eslint-config-devextreme": "catalog:", + "eslint-migration-utils": "workspace:*", + "eslint-plugin-i18n": "catalog:", + "eslint-plugin-import": "catalog:", + "eslint-plugin-no-only-tests": "catalog:", "glob": "11.1.0", "minimist": "1.2.8", "mockdate": "3.0.5", "nconf": "0.12.1", + "pixelmatch": "^7.1.0", + "pngjs": "^7.0.0", "testcafe": "3.7.4", "testcafe-reporter-spec-time": "4.0.0", - "ts-node": "10.9.2", - "eslint": "catalog:", - "@eslint/eslintrc": "catalog:", - "@stylistic/eslint-plugin": "catalog:", - "@typescript-eslint/eslint-plugin": "catalog:", - "@typescript-eslint/parser": "catalog:", - "eslint-config-devextreme": "catalog:", - "eslint-migration-utils": "workspace:*", - "eslint-plugin-i18n": "catalog:", - "eslint-plugin-import": "catalog:", - "eslint-plugin-no-only-tests": "catalog:" + "ts-node": "10.9.2" } } diff --git a/e2e/testcafe-devextreme/playwright-helpers/createWidget.ts b/e2e/testcafe-devextreme/playwright-helpers/createWidget.ts new file mode 100644 index 000000000000..e4079a662b09 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-helpers/createWidget.ts @@ -0,0 +1,15 @@ +import type { Page } from '@playwright/test'; + +export async function createWidget( + page: Page, + widgetName: string, + widgetOptions: Record | (() => Record), + selector = '#container', + disableFxAnimation = true, +): Promise { + await page.evaluate(({ name, opts, sel, disableFx }) => { + (window as any).DevExpress.fx.off = disableFx; + const options = typeof opts === 'function' ? opts() : opts; + ($(sel) as any)[name](options); + }, { name: widgetName, opts: widgetOptions, sel: selector, disableFx: disableFxAnimation }); +} diff --git a/e2e/testcafe-devextreme/playwright-helpers/domUtils.ts b/e2e/testcafe-devextreme/playwright-helpers/domUtils.ts new file mode 100644 index 000000000000..392ec294d9fa --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-helpers/domUtils.ts @@ -0,0 +1,74 @@ +import type { Page } from '@playwright/test'; + +export async function setAttribute( + page: Page, + selector: string, + attribute: string, + value: string, +): Promise { + await page.evaluate(({ sel, attr, val }) => { + document.querySelector(sel)?.setAttribute(attr, val); + }, { sel: selector, attr: attribute, val: value }); +} + +export async function getStyleAttribute(page: Page, selector: string): Promise { + return page.evaluate( + (sel) => document.querySelector(sel)?.getAttribute('style') ?? '', + selector, + ); +} + +export async function setStyleAttribute( + page: Page, + selector: string, + styleValue: string, +): Promise { + await page.evaluate(({ sel, style }) => { + const element = document.querySelector(sel); + const styles = element?.getAttribute('style') ?? ''; + element?.setAttribute('style', `${styles} ${style}`); + }, { sel: selector, style: styleValue }); +} + +export async function insertStylesheetRulesToPage( + page: Page, + rules: string, +): Promise { + await page.evaluate((css) => { + const styleTag = document.createElement('style'); + styleTag.setAttribute('data-playwright-style', 'true'); + styleTag.textContent = css; + document.head.appendChild(styleTag); + }, rules); +} + +export async function removeStylesheetRulesFromPage(page: Page): Promise { + await page.evaluate(() => { + document.querySelectorAll('style[data-playwright-style]').forEach((el) => el.remove()); + }); +} + +export async function appendElementTo( + page: Page, + parentSelector: string, + childSelector: string, + attrs?: Record, +): Promise { + await page.evaluate(({ parent, tag, attributes }) => { + const el = document.createElement(tag); + if (attributes) { + Object.entries(attributes).forEach(([key, val]) => el.setAttribute(key, val)); + } + document.querySelector(parent)?.appendChild(el); + }, { parent: parentSelector, tag: childSelector, attributes: attrs }); +} + +export async function setClassAttribute( + page: Page, + selector: string, + className: string, +): Promise { + await page.evaluate(({ sel, cls }) => { + document.querySelector(sel)?.setAttribute('class', cls); + }, { sel: selector, cls: className }); +} diff --git a/e2e/testcafe-devextreme/playwright-helpers/generateOptionMatrix.ts b/e2e/testcafe-devextreme/playwright-helpers/generateOptionMatrix.ts new file mode 100644 index 000000000000..65d9808c7886 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-helpers/generateOptionMatrix.ts @@ -0,0 +1,28 @@ +type OptionMatrix = { + [K in keyof T]: T[K][]; +}; + +export function generateOptionMatrix>( + matrix: OptionMatrix, +): T[] { + const keys = Object.keys(matrix) as (keyof T)[]; + const combinations: T[] = []; + + function generate(index: number, current: Partial): void { + if (index === keys.length) { + combinations.push({ ...current } as T); + return; + } + + const key = keys[index]; + const values = matrix[key]; + + for (const value of values) { + current[key] = value; + generate(index + 1, current); + } + } + + generate(0, {}); + return combinations; +} diff --git a/e2e/testcafe-devextreme/playwright-helpers/index.ts b/e2e/testcafe-devextreme/playwright-helpers/index.ts new file mode 100644 index 000000000000..00d96fdf73ae --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-helpers/index.ts @@ -0,0 +1,26 @@ +export { createWidget } from './createWidget'; +export { + changeTheme, + getCurrentTheme, + getFullThemeName, + getThemePostfix, + isFluent, + isMaterial, + isMaterialBased, + testScreenshot, +} from './themeUtils'; +export { + appendElementTo, + getStyleAttribute, + insertStylesheetRulesToPage, + removeStylesheetRulesFromPage, + setAttribute, + setClassAttribute, + setStyleAttribute, +} from './domUtils'; +export { + clearTestPage, + getContainerUrl, + setupTestPage, +} from './testPageUtils'; +export { generateOptionMatrix } from './generateOptionMatrix'; diff --git a/e2e/testcafe-devextreme/playwright-helpers/testPageUtils.ts b/e2e/testcafe-devextreme/playwright-helpers/testPageUtils.ts new file mode 100644 index 000000000000..245fa04e2611 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-helpers/testPageUtils.ts @@ -0,0 +1,49 @@ +import type { Page } from '@playwright/test'; +import path from 'path'; +import { removeStylesheetRulesFromPage } from './domUtils'; + +export function getContainerUrl(dirname: string, relativePath = '../../../tests/container.html'): string { + return `file://${path.resolve(dirname, relativePath)}`; +} + +export async function clearTestPage(page: Page): Promise { + await page.evaluate(() => { + const widgetSelector = '.dx-widget'; + const elements = document.querySelectorAll(widgetSelector); + elements.forEach((element) => { + if (element.closest(widgetSelector) === element) { + const $el = $(element) as any; + const widgetNames = $el.data()?.dxComponents; + widgetNames?.forEach((name: string) => { + if ($el.hasClass('dx-widget')) { + $el[name]?.('dispose'); + } + }); + $el.empty(); + } + }); + + const body = document.querySelector('body'); + if (body) { + body.innerHTML = ''; + body.className = 'dx-surface'; + + const parent = document.createElement('div'); + parent.id = 'parentContainer'; + parent.setAttribute('role', 'main'); + parent.innerHTML = '

Test header

'; + body.appendChild(parent); + } + }); + + await removeStylesheetRulesFromPage(page); +} + +export async function setupTestPage(page: Page, containerUrl: string, theme = 'fluent.blue.light'): Promise { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((themeName) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(themeName); + }), theme); +} diff --git a/e2e/testcafe-devextreme/playwright-helpers/themeUtils.ts b/e2e/testcafe-devextreme/playwright-helpers/themeUtils.ts new file mode 100644 index 000000000000..963ba0045cda --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-helpers/themeUtils.ts @@ -0,0 +1,74 @@ +import type { Page, Locator } from '@playwright/test'; +import { expect } from '@playwright/test'; + +const defaultThemeName = 'fluent.blue.light'; + +export const getThemePostfix = (theme?: string): string => { + const themeName = (theme ?? process.env.theme) ?? defaultThemeName; + return ` (${themeName})`; +}; + +export const getFullThemeName = (): string => process.env.theme ?? defaultThemeName; + +export const isMaterial = (): boolean => getFullThemeName().startsWith('material'); + +export const isFluent = (): boolean => getFullThemeName().startsWith('fluent'); + +export const isMaterialBased = (): boolean => isMaterial() || isFluent(); + +export async function changeTheme(page: Page, themeName: string): Promise { + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), themeName); +} + +export async function getCurrentTheme(page: Page): Promise { + return page.evaluate(() => (window as any).DevExpress?.ui.themes.current()); +} + +const getScreenshotName = (baseName: string, theme?: string): string => { + const themePostfix = getThemePostfix(theme); + return baseName.endsWith('.png') + ? baseName.replace('.png', `${themePostfix}.png`) + : `${baseName}${themePostfix}.png`; +}; + +export async function testScreenshot( + page: Page, + screenshotName: string, + options?: { + element?: Locator | string | null; + theme?: string; + shouldTestInCompact?: boolean; + }, +): Promise { + const { + element, + theme, + shouldTestInCompact = false, + } = options ?? {}; + + if (theme) { + await changeTheme(page, theme); + } + + const locator = typeof element === 'string' + ? page.locator(element) + : element ?? page.locator('#container'); + + await expect(locator).toHaveScreenshot(getScreenshotName(screenshotName, theme)); + + if (shouldTestInCompact) { + const themeName = (theme ?? process.env.theme) ?? defaultThemeName; + await changeTheme(page, `${themeName}.compact`); + + await expect(locator).toHaveScreenshot( + getScreenshotName(screenshotName, `${themeName}.compact`), + ); + } + + if (theme || shouldTestInCompact) { + await changeTheme(page, process.env.theme ?? defaultThemeName); + } +} diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/accordion.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/accordion.spec.ts new file mode 100644 index 000000000000..de6e8c2f2c04 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/accordion.spec.ts @@ -0,0 +1,20 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../tests/container.html')}`; + +test.describe('Accessibility - accordion', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test.skip('accessibility test', async ({ page }) => { + // TODO: Convert testAccessibility() to Playwright + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/actionSheet.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/actionSheet.spec.ts new file mode 100644 index 000000000000..02376adc6f7f --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/actionSheet.spec.ts @@ -0,0 +1,20 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../tests/container.html')}`; + +test.describe('Accessibility - actionSheet', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test.skip('accessibility test', async ({ page }) => { + // TODO: Convert testAccessibility() to Playwright + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/autocomplete.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/autocomplete.spec.ts new file mode 100644 index 000000000000..b4aa5f19c35c --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/autocomplete.spec.ts @@ -0,0 +1,20 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../tests/container.html')}`; + +test.describe('Accessibility - autocomplete', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test.skip('accessibility test', async ({ page }) => { + // TODO: Convert testAccessibility() to Playwright + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/button.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/button.spec.ts new file mode 100644 index 000000000000..1b0d1490dfe9 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/button.spec.ts @@ -0,0 +1,20 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../tests/container.html')}`; + +test.describe('Accessibility - button', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test.skip('accessibility test', async ({ page }) => { + // TODO: Convert testAccessibility() to Playwright + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/buttonGroup.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/buttonGroup.spec.ts new file mode 100644 index 000000000000..cf68c7d6422c --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/buttonGroup.spec.ts @@ -0,0 +1,20 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../tests/container.html')}`; + +test.describe('Accessibility - buttonGroup', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test.skip('accessibility test', async ({ page }) => { + // TODO: Convert testAccessibility() to Playwright + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/calendar.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/calendar.spec.ts new file mode 100644 index 000000000000..c567fec1c1bc --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/calendar.spec.ts @@ -0,0 +1,20 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../tests/container.html')}`; + +test.describe('Accessibility - calendar', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test.skip('accessibility test', async ({ page }) => { + // TODO: Convert testAccessibility() to Playwright + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/cardView/columnChooser.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/cardView/columnChooser.spec.ts new file mode 100644 index 000000000000..6ae90f686182 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/cardView/columnChooser.spec.ts @@ -0,0 +1,28 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('Accessibility - CardView columnChooser', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test.skip('select mode', async ({ page }) => { + // TODO: Convert a11yCheck() to Playwright with @axe-core/playwright + }); + + test.skip('dragAndDrop mode', async ({ page }) => { + // TODO: Convert a11yCheck() to Playwright with @axe-core/playwright + }); + + test.skip('cardView with opened columnChooser', async ({ page }) => { + // TODO: Convert a11yCheck() to Playwright with @axe-core/playwright + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/cardView/columnSortable.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/cardView/columnSortable.spec.ts new file mode 100644 index 000000000000..8ee1c4a9910f --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/cardView/columnSortable.spec.ts @@ -0,0 +1,28 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('Accessibility - CardView columnSortable', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test.skip('headerPanel dragging column', async ({ page }) => { + // TODO: Convert a11yCheck() to Playwright with @axe-core/playwright + }); + + test.skip('dropzone in headerPanel from columnChooser', async ({ page }) => { + // TODO: Convert a11yCheck() to Playwright with @axe-core/playwright + }); + + test.skip('dropzone with allowReordering false', async ({ page }) => { + // TODO: Convert a11yCheck() to Playwright with @axe-core/playwright + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/cardView/cover.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/cardView/cover.spec.ts new file mode 100644 index 000000000000..d633a4dc3a82 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/cardView/cover.spec.ts @@ -0,0 +1,20 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('Accessibility - CardView cover', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test.skip('default render', async ({ page }) => { + // TODO: Convert a11yCheck() to Playwright with @axe-core/playwright + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/cardView/editing.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/cardView/editing.spec.ts new file mode 100644 index 000000000000..48521929788f --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/cardView/editing.spec.ts @@ -0,0 +1,28 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('Accessibility - CardView editing', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test.skip('default render', async ({ page }) => { + // TODO: Convert a11yCheck() to Playwright with @axe-core/playwright + }); + + test.skip('render of add card popup', async ({ page }) => { + // TODO: Convert a11yCheck() to Playwright with @axe-core/playwright + }); + + test.skip('render of edit card popup', async ({ page }) => { + // TODO: Convert a11yCheck() to Playwright with @axe-core/playwright + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/cardView/filterPanel.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/cardView/filterPanel.spec.ts new file mode 100644 index 000000000000..2a42828d8ee8 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/cardView/filterPanel.spec.ts @@ -0,0 +1,20 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('Accessibility - CardView filterPanel', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test.skip('FilterPanel and FilterBuilderPopup', async ({ page }) => { + // TODO: Convert a11yCheck() to Playwright with @axe-core/playwright + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/cardView/headerFilter.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/cardView/headerFilter.spec.ts new file mode 100644 index 000000000000..95f417db3562 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/cardView/headerFilter.spec.ts @@ -0,0 +1,28 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('Accessibility - CardView headerFilter', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test.skip('popup with list', async ({ page }) => { + // TODO: Convert a11yCheck() to Playwright with @axe-core/playwright + }); + + test.skip('popup with search', async ({ page }) => { + // TODO: Convert a11yCheck() to Playwright with @axe-core/playwright + }); + + test.skip('popup with tree', async ({ page }) => { + // TODO: Convert a11yCheck() to Playwright with @axe-core/playwright + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/cardView/headerPanel.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/cardView/headerPanel.spec.ts new file mode 100644 index 000000000000..19385f462cf8 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/cardView/headerPanel.spec.ts @@ -0,0 +1,36 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('Accessibility - CardView headerPanel', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test.skip('Default render', async ({ page }) => { + // TODO: Convert a11yCheck() to Playwright with @axe-core/playwright + }); + + test.skip('render with header filter enabled', async ({ page }) => { + // TODO: Convert a11yCheck() to Playwright with @axe-core/playwright + }); + + test.skip('render with single sorting', async ({ page }) => { + // TODO: Convert a11yCheck() to Playwright with @axe-core/playwright + }); + + test.skip('render with multiple sorting', async ({ page }) => { + // TODO: Convert a11yCheck() to Playwright with @axe-core/playwright + }); + + test.skip('headerPanel column chooser link', async ({ page }) => { + // TODO: Convert a11yCheck() to Playwright with @axe-core/playwright + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/cardView/noData.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/cardView/noData.spec.ts new file mode 100644 index 000000000000..c87d7d2d75ee --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/cardView/noData.spec.ts @@ -0,0 +1,20 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('Accessibility - CardView noData', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test.skip('default render', async ({ page }) => { + // TODO: Convert a11yCheck() to Playwright with @axe-core/playwright + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/cardView/pager.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/cardView/pager.spec.ts new file mode 100644 index 000000000000..0700b655330a --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/cardView/pager.spec.ts @@ -0,0 +1,20 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('Accessibility - CardView pager', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test.skip('Runtime filterValue change updates paging', async ({ page }) => { + // TODO: Convert a11yCheck() to Playwright with @axe-core/playwright + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/cardView/search.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/cardView/search.spec.ts new file mode 100644 index 000000000000..7cbf0d15c382 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/cardView/search.spec.ts @@ -0,0 +1,20 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('Accessibility - CardView search', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test.skip('highlighted search text', async ({ page }) => { + // TODO: Convert a11yCheck() to Playwright with @axe-core/playwright + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/cardView/selection.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/cardView/selection.spec.ts new file mode 100644 index 000000000000..178fe6c54755 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/cardView/selection.spec.ts @@ -0,0 +1,36 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('Accessibility - CardView selection', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test.skip('Single mode', async ({ page }) => { + // TODO: Convert a11yCheck() to Playwright with @axe-core/playwright + }); + + test.skip('Multiple mode always', async ({ page }) => { + // TODO: Convert a11yCheck() to Playwright with @axe-core/playwright + }); + + test.skip('Multiple mode onClick', async ({ page }) => { + // TODO: Convert a11yCheck() to Playwright with @axe-core/playwright + }); + + test.skip('Multiple mode onLongTap', async ({ page }) => { + // TODO: Convert a11yCheck() to Playwright with @axe-core/playwright + }); + + test.skip('Multiple mode without Select All', async ({ page }) => { + // TODO: Convert a11yCheck() to Playwright with @axe-core/playwright + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/cardView/sortable.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/cardView/sortable.spec.ts new file mode 100644 index 000000000000..31b00e9b9155 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/cardView/sortable.spec.ts @@ -0,0 +1,28 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('Accessibility - CardView sortable', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test.skip('sortable indicator first', async ({ page }) => { + // TODO: Convert a11yCheck() to Playwright with @axe-core/playwright + }); + + test.skip('sortable indicator middle', async ({ page }) => { + // TODO: Convert a11yCheck() to Playwright with @axe-core/playwright + }); + + test.skip('sortable indicator last', async ({ page }) => { + // TODO: Convert a11yCheck() to Playwright with @axe-core/playwright + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/cardView/sorting.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/cardView/sorting.spec.ts new file mode 100644 index 000000000000..60c635c6e696 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/cardView/sorting.spec.ts @@ -0,0 +1,44 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('Accessibility - CardView sorting', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test.skip('Default render', async ({ page }) => { + // TODO: Convert a11yCheck() to Playwright with @axe-core/playwright + }); + + test.skip('Default multiple sorting render', async ({ page }) => { + // TODO: Convert a11yCheck() to Playwright with @axe-core/playwright + }); + + test.skip('Sort index API', async ({ page }) => { + // TODO: Convert a11yCheck() to Playwright with @axe-core/playwright + }); + + test.skip('ShowSortIndexes API', async ({ page }) => { + // TODO: Convert a11yCheck() to Playwright with @axe-core/playwright + }); + + test.skip('AllowSorting API', async ({ page }) => { + // TODO: Convert a11yCheck() to Playwright with @axe-core/playwright + }); + + test.skip('CalculateSortValue API', async ({ page }) => { + // TODO: Convert a11yCheck() to Playwright with @axe-core/playwright + }); + + test.skip('SortingMethod API', async ({ page }) => { + // TODO: Convert a11yCheck() to Playwright with @axe-core/playwright + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/chat.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/chat.spec.ts new file mode 100644 index 000000000000..99cb4cfd9eab --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/chat.spec.ts @@ -0,0 +1,20 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../tests/container.html')}`; + +test.describe('Accessibility - chat', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test.skip('accessibility test', async ({ page }) => { + // TODO: Convert testAccessibility() to Playwright + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/checkBox.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/checkBox.spec.ts new file mode 100644 index 000000000000..1274b58f5bea --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/checkBox.spec.ts @@ -0,0 +1,20 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../tests/container.html')}`; + +test.describe('Accessibility - checkBox', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test.skip('accessibility test', async ({ page }) => { + // TODO: Convert testAccessibility() to Playwright + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/colorBox.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/colorBox.spec.ts new file mode 100644 index 000000000000..93a16518604b --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/colorBox.spec.ts @@ -0,0 +1,20 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../tests/container.html')}`; + +test.describe('Accessibility - colorBox', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test.skip('accessibility test', async ({ page }) => { + // TODO: Convert testAccessibility() to Playwright + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/contextMenu.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/contextMenu.spec.ts new file mode 100644 index 000000000000..e2d736acbae1 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/contextMenu.spec.ts @@ -0,0 +1,20 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../tests/container.html')}`; + +test.describe('Accessibility - contextMenu', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test.skip('accessibility test', async ({ page }) => { + // TODO: Convert testAccessibility() to Playwright + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/dataGrid/common.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/dataGrid/common.spec.ts new file mode 100644 index 000000000000..e7dd6f58f98c --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/dataGrid/common.spec.ts @@ -0,0 +1,20 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('Accessibility - DataGrid common', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test.skip('accessibility test', async ({ page }) => { + // TODO: Convert testAccessibility() / a11yCheck() to Playwright + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/dataGrid/editing.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/dataGrid/editing.spec.ts new file mode 100644 index 000000000000..065b42175d84 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/dataGrid/editing.spec.ts @@ -0,0 +1,20 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('Accessibility - DataGrid editing', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test.skip('accessibility test', async ({ page }) => { + // TODO: Convert testAccessibility() / a11yCheck() to Playwright + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/dataGrid/fixedColumns.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/dataGrid/fixedColumns.spec.ts new file mode 100644 index 000000000000..eb5cff15f318 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/dataGrid/fixedColumns.spec.ts @@ -0,0 +1,20 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('Accessibility - DataGrid fixedColumns', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test.skip('accessibility test', async ({ page }) => { + // TODO: Convert testAccessibility() / a11yCheck() to Playwright + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/dataGrid/scrolling.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/dataGrid/scrolling.spec.ts new file mode 100644 index 000000000000..5b7a67f6e93b --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/dataGrid/scrolling.spec.ts @@ -0,0 +1,20 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('Accessibility - DataGrid scrolling', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test.skip('accessibility test', async ({ page }) => { + // TODO: Convert testAccessibility() / a11yCheck() to Playwright + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/dataGrid/status.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/dataGrid/status.spec.ts new file mode 100644 index 000000000000..934a65176245 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/dataGrid/status.spec.ts @@ -0,0 +1,20 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('Accessibility - DataGrid status', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test.skip('accessibility test', async ({ page }) => { + // TODO: Convert testAccessibility() / a11yCheck() to Playwright + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/dataGrid/templates.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/dataGrid/templates.spec.ts new file mode 100644 index 000000000000..098f66e57b0a --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/dataGrid/templates.spec.ts @@ -0,0 +1,20 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('Accessibility - DataGrid templates', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test.skip('accessibility test', async ({ page }) => { + // TODO: Convert testAccessibility() / a11yCheck() to Playwright + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/dateBox.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/dateBox.spec.ts new file mode 100644 index 000000000000..8d9fb6ea6722 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/dateBox.spec.ts @@ -0,0 +1,20 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../tests/container.html')}`; + +test.describe('Accessibility - dateBox', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test.skip('accessibility test', async ({ page }) => { + // TODO: Convert testAccessibility() to Playwright + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/dateRangeBox.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/dateRangeBox.spec.ts new file mode 100644 index 000000000000..c4725bf1a4a4 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/dateRangeBox.spec.ts @@ -0,0 +1,20 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../tests/container.html')}`; + +test.describe('Accessibility - dateRangeBox', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test.skip('accessibility test', async ({ page }) => { + // TODO: Convert testAccessibility() to Playwright + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/drawer.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/drawer.spec.ts new file mode 100644 index 000000000000..85a5d30fa8f8 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/drawer.spec.ts @@ -0,0 +1,20 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../tests/container.html')}`; + +test.describe('Accessibility - drawer', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test.skip('accessibility test', async ({ page }) => { + // TODO: Convert testAccessibility() to Playwright + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/dropDownBox.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/dropDownBox.spec.ts new file mode 100644 index 000000000000..ed9cc90d7ef5 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/dropDownBox.spec.ts @@ -0,0 +1,20 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../tests/container.html')}`; + +test.describe('Accessibility - dropDownBox', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test.skip('accessibility test', async ({ page }) => { + // TODO: Convert testAccessibility() to Playwright + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/dropDownButton.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/dropDownButton.spec.ts new file mode 100644 index 000000000000..d438c4c28fb3 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/dropDownButton.spec.ts @@ -0,0 +1,20 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../tests/container.html')}`; + +test.describe('Accessibility - dropDownButton', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test.skip('accessibility test', async ({ page }) => { + // TODO: Convert testAccessibility() to Playwright + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/fileUploader.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/fileUploader.spec.ts new file mode 100644 index 000000000000..403336a4b120 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/fileUploader.spec.ts @@ -0,0 +1,20 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../tests/container.html')}`; + +test.describe('Accessibility - fileUploader', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test.skip('accessibility test', async ({ page }) => { + // TODO: Convert testAccessibility() to Playwright + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/filterBuilder.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/filterBuilder.spec.ts new file mode 100644 index 000000000000..7837693cc5ea --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/filterBuilder.spec.ts @@ -0,0 +1,20 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../tests/container.html')}`; + +test.describe('Accessibility - filterBuilder', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test.skip('accessibility test', async ({ page }) => { + // TODO: Convert testAccessibility() to Playwright + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/floatingActionButton.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/floatingActionButton.spec.ts new file mode 100644 index 000000000000..c22e13c91181 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/floatingActionButton.spec.ts @@ -0,0 +1,20 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../tests/container.html')}`; + +test.describe('Accessibility - floatingActionButton', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test.skip('accessibility test', async ({ page }) => { + // TODO: Convert testAccessibility() to Playwright + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/form.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/form.spec.ts new file mode 100644 index 000000000000..c348ec5dbfa2 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/form.spec.ts @@ -0,0 +1,20 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../tests/container.html')}`; + +test.describe('Accessibility - form', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test.skip('accessibility test', async ({ page }) => { + // TODO: Convert testAccessibility() to Playwright + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/gallery.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/gallery.spec.ts new file mode 100644 index 000000000000..dd51555de4e7 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/gallery.spec.ts @@ -0,0 +1,20 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../tests/container.html')}`; + +test.describe('Accessibility - gallery', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test.skip('accessibility test', async ({ page }) => { + // TODO: Convert testAccessibility() to Playwright + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/htmlEditor.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/htmlEditor.spec.ts new file mode 100644 index 000000000000..fc3833e74b6b --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/htmlEditor.spec.ts @@ -0,0 +1,20 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../tests/container.html')}`; + +test.describe('Accessibility - htmlEditor', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test.skip('accessibility test', async ({ page }) => { + // TODO: Convert testAccessibility() to Playwright + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/list.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/list.spec.ts new file mode 100644 index 000000000000..2a2fb9a552b2 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/list.spec.ts @@ -0,0 +1,20 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../tests/container.html')}`; + +test.describe('Accessibility - list', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test.skip('accessibility test', async ({ page }) => { + // TODO: Convert testAccessibility() to Playwright + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/loadIndicator.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/loadIndicator.spec.ts new file mode 100644 index 000000000000..75d3690a4689 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/loadIndicator.spec.ts @@ -0,0 +1,20 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../tests/container.html')}`; + +test.describe('Accessibility - loadIndicator', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test.skip('accessibility test', async ({ page }) => { + // TODO: Convert testAccessibility() to Playwright + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/loadPanel.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/loadPanel.spec.ts new file mode 100644 index 000000000000..6fc1c1503233 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/loadPanel.spec.ts @@ -0,0 +1,20 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../tests/container.html')}`; + +test.describe('Accessibility - loadPanel', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test.skip('accessibility test', async ({ page }) => { + // TODO: Convert testAccessibility() to Playwright + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/lookup.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/lookup.spec.ts new file mode 100644 index 000000000000..a9edb2205c79 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/lookup.spec.ts @@ -0,0 +1,20 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../tests/container.html')}`; + +test.describe('Accessibility - lookup', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test.skip('accessibility test', async ({ page }) => { + // TODO: Convert testAccessibility() to Playwright + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/menu.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/menu.spec.ts new file mode 100644 index 000000000000..af7100e38df7 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/menu.spec.ts @@ -0,0 +1,20 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../tests/container.html')}`; + +test.describe('Accessibility - menu', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test.skip('accessibility test', async ({ page }) => { + // TODO: Convert testAccessibility() to Playwright + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/multiView.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/multiView.spec.ts new file mode 100644 index 000000000000..4299da538529 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/multiView.spec.ts @@ -0,0 +1,20 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../tests/container.html')}`; + +test.describe('Accessibility - multiView', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test.skip('accessibility test', async ({ page }) => { + // TODO: Convert testAccessibility() to Playwright + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/numberBox.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/numberBox.spec.ts new file mode 100644 index 000000000000..be43d167b61f --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/numberBox.spec.ts @@ -0,0 +1,20 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../tests/container.html')}`; + +test.describe('Accessibility - numberBox', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test.skip('accessibility test', async ({ page }) => { + // TODO: Convert testAccessibility() to Playwright + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/pagination.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/pagination.spec.ts new file mode 100644 index 000000000000..c0d352a35f1f --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/pagination.spec.ts @@ -0,0 +1,20 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../tests/container.html')}`; + +test.describe('Accessibility - pagination', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test.skip('accessibility test', async ({ page }) => { + // TODO: Convert testAccessibility() to Playwright + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/popover.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/popover.spec.ts new file mode 100644 index 000000000000..7f5e75dc597c --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/popover.spec.ts @@ -0,0 +1,20 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../tests/container.html')}`; + +test.describe('Accessibility - popover', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test.skip('accessibility test', async ({ page }) => { + // TODO: Convert testAccessibility() to Playwright + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/popup.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/popup.spec.ts new file mode 100644 index 000000000000..e3e6c7a66a95 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/popup.spec.ts @@ -0,0 +1,20 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../tests/container.html')}`; + +test.describe('Accessibility - popup', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test.skip('accessibility test', async ({ page }) => { + // TODO: Convert testAccessibility() to Playwright + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/progressBar.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/progressBar.spec.ts new file mode 100644 index 000000000000..520328620b17 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/progressBar.spec.ts @@ -0,0 +1,20 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../tests/container.html')}`; + +test.describe('Accessibility - progressBar', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test.skip('accessibility test', async ({ page }) => { + // TODO: Convert testAccessibility() to Playwright + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/radioGroup.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/radioGroup.spec.ts new file mode 100644 index 000000000000..103d83fc4371 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/radioGroup.spec.ts @@ -0,0 +1,20 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../tests/container.html')}`; + +test.describe('Accessibility - radioGroup', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test.skip('accessibility test', async ({ page }) => { + // TODO: Convert testAccessibility() to Playwright + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/rangeSlider.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/rangeSlider.spec.ts new file mode 100644 index 000000000000..a8f1f493ce1e --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/rangeSlider.spec.ts @@ -0,0 +1,20 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../tests/container.html')}`; + +test.describe('Accessibility - rangeSlider', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test.skip('accessibility test', async ({ page }) => { + // TODO: Convert testAccessibility() to Playwright + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/scheduler/appointment.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/scheduler/appointment.spec.ts new file mode 100644 index 000000000000..4164bb2ad46b --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/scheduler/appointment.spec.ts @@ -0,0 +1,20 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('Accessibility - Scheduler appointment', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test.skip('accessibility test', async ({ page }) => { + // TODO: Convert testAccessibility() / a11yCheck() to Playwright + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/scheduler/appointmentForm.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/scheduler/appointmentForm.spec.ts new file mode 100644 index 000000000000..1214eeb2cc15 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/scheduler/appointmentForm.spec.ts @@ -0,0 +1,20 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('Accessibility - Scheduler appointmentForm', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test.skip('accessibility test', async ({ page }) => { + // TODO: Convert testAccessibility() / a11yCheck() to Playwright + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/scheduler/legacyPopup.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/scheduler/legacyPopup.spec.ts new file mode 100644 index 000000000000..8dc4ba95b3a2 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/scheduler/legacyPopup.spec.ts @@ -0,0 +1,20 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('Accessibility - Scheduler legacyPopup', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test.skip('accessibility test', async ({ page }) => { + // TODO: Convert testAccessibility() / a11yCheck() to Playwright + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/scheduler/scheduler.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/scheduler/scheduler.spec.ts new file mode 100644 index 000000000000..bfe0bc554581 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/scheduler/scheduler.spec.ts @@ -0,0 +1,20 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('Accessibility - Scheduler scheduler', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test.skip('accessibility test', async ({ page }) => { + // TODO: Convert testAccessibility() / a11yCheck() to Playwright + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/scheduler/status.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/scheduler/status.spec.ts new file mode 100644 index 000000000000..9b2dc2fa47ff --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/scheduler/status.spec.ts @@ -0,0 +1,20 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('Accessibility - Scheduler status', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test.skip('accessibility test', async ({ page }) => { + // TODO: Convert testAccessibility() / a11yCheck() to Playwright + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/selectBox.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/selectBox.spec.ts new file mode 100644 index 000000000000..5f8eb824991f --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/selectBox.spec.ts @@ -0,0 +1,20 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../tests/container.html')}`; + +test.describe('Accessibility - selectBox', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test.skip('accessibility test', async ({ page }) => { + // TODO: Convert testAccessibility() to Playwright + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/slider.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/slider.spec.ts new file mode 100644 index 000000000000..e07f5dff0043 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/slider.spec.ts @@ -0,0 +1,20 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../tests/container.html')}`; + +test.describe('Accessibility - slider', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test.skip('accessibility test', async ({ page }) => { + // TODO: Convert testAccessibility() to Playwright + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/speechToText.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/speechToText.spec.ts new file mode 100644 index 000000000000..4377fcf5f9d3 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/speechToText.spec.ts @@ -0,0 +1,20 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../tests/container.html')}`; + +test.describe('Accessibility - speechToText', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test.skip('accessibility test', async ({ page }) => { + // TODO: Convert testAccessibility() to Playwright + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/splitter.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/splitter.spec.ts new file mode 100644 index 000000000000..e84af758267d --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/splitter.spec.ts @@ -0,0 +1,20 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../tests/container.html')}`; + +test.describe('Accessibility - splitter', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test.skip('accessibility test', async ({ page }) => { + // TODO: Convert testAccessibility() to Playwright + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/stepper.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/stepper.spec.ts new file mode 100644 index 000000000000..59391fd204b9 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/stepper.spec.ts @@ -0,0 +1,20 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../tests/container.html')}`; + +test.describe('Accessibility - stepper', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test.skip('accessibility test', async ({ page }) => { + // TODO: Convert testAccessibility() to Playwright + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/switch.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/switch.spec.ts new file mode 100644 index 000000000000..765d6dba96fb --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/switch.spec.ts @@ -0,0 +1,20 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../tests/container.html')}`; + +test.describe('Accessibility - switch', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test.skip('accessibility test', async ({ page }) => { + // TODO: Convert testAccessibility() to Playwright + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/tabPanel.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/tabPanel.spec.ts new file mode 100644 index 000000000000..c5b3e17bc194 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/tabPanel.spec.ts @@ -0,0 +1,20 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../tests/container.html')}`; + +test.describe('Accessibility - tabPanel', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test.skip('accessibility test', async ({ page }) => { + // TODO: Convert testAccessibility() to Playwright + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/tabs.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/tabs.spec.ts new file mode 100644 index 000000000000..189701a13525 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/tabs.spec.ts @@ -0,0 +1,20 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../tests/container.html')}`; + +test.describe('Accessibility - tabs', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test.skip('accessibility test', async ({ page }) => { + // TODO: Convert testAccessibility() to Playwright + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/tagBox.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/tagBox.spec.ts new file mode 100644 index 000000000000..8f2b385b3176 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/tagBox.spec.ts @@ -0,0 +1,20 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../tests/container.html')}`; + +test.describe('Accessibility - tagBox', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test.skip('accessibility test', async ({ page }) => { + // TODO: Convert testAccessibility() to Playwright + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/textArea.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/textArea.spec.ts new file mode 100644 index 000000000000..c04d41d235bf --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/textArea.spec.ts @@ -0,0 +1,20 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../tests/container.html')}`; + +test.describe('Accessibility - textArea', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test.skip('accessibility test', async ({ page }) => { + // TODO: Convert testAccessibility() to Playwright + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/textBox.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/textBox.spec.ts new file mode 100644 index 000000000000..fb3ce0d08873 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/textBox.spec.ts @@ -0,0 +1,20 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../tests/container.html')}`; + +test.describe('Accessibility - textBox', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test.skip('accessibility test', async ({ page }) => { + // TODO: Convert testAccessibility() to Playwright + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/tileView.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/tileView.spec.ts new file mode 100644 index 000000000000..8a83b0665d82 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/tileView.spec.ts @@ -0,0 +1,20 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../tests/container.html')}`; + +test.describe('Accessibility - tileView', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test.skip('accessibility test', async ({ page }) => { + // TODO: Convert testAccessibility() to Playwright + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/toast.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/toast.spec.ts new file mode 100644 index 000000000000..a85bf0f35fbc --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/toast.spec.ts @@ -0,0 +1,20 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../tests/container.html')}`; + +test.describe('Accessibility - toast', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test.skip('accessibility test', async ({ page }) => { + // TODO: Convert testAccessibility() to Playwright + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/toolbar.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/toolbar.spec.ts new file mode 100644 index 000000000000..a213b715fed3 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/toolbar.spec.ts @@ -0,0 +1,20 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../tests/container.html')}`; + +test.describe('Accessibility - toolbar', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test.skip('accessibility test', async ({ page }) => { + // TODO: Convert testAccessibility() to Playwright + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/tooltip.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/tooltip.spec.ts new file mode 100644 index 000000000000..aecf068a3737 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/tooltip.spec.ts @@ -0,0 +1,20 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../tests/container.html')}`; + +test.describe('Accessibility - tooltip', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test.skip('accessibility test', async ({ page }) => { + // TODO: Convert testAccessibility() to Playwright + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/treeList/aria.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/treeList/aria.spec.ts new file mode 100644 index 000000000000..6070f2494634 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/treeList/aria.spec.ts @@ -0,0 +1,20 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('Accessibility - TreeList aria', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test.skip('accessibility test', async ({ page }) => { + // TODO: Convert testAccessibility() / a11yCheck() to Playwright + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/treeList/common.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/treeList/common.spec.ts new file mode 100644 index 000000000000..69554a80dd33 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/treeList/common.spec.ts @@ -0,0 +1,20 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('Accessibility - TreeList common', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test.skip('accessibility test', async ({ page }) => { + // TODO: Convert testAccessibility() / a11yCheck() to Playwright + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/treeList/status.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/treeList/status.spec.ts new file mode 100644 index 000000000000..94c9c178efb4 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/treeList/status.spec.ts @@ -0,0 +1,20 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('Accessibility - TreeList status', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test.skip('accessibility test', async ({ page }) => { + // TODO: Convert testAccessibility() / a11yCheck() to Playwright + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/treeView.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/treeView.spec.ts new file mode 100644 index 000000000000..d840b65ebf48 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/treeView.spec.ts @@ -0,0 +1,20 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../tests/container.html')}`; + +test.describe('Accessibility - treeView', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test.skip('accessibility test', async ({ page }) => { + // TODO: Convert testAccessibility() to Playwright + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/accessibility/validationSummary.spec.ts b/e2e/testcafe-devextreme/playwright-tests/accessibility/validationSummary.spec.ts new file mode 100644 index 000000000000..a1c7c972abbb --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/accessibility/validationSummary.spec.ts @@ -0,0 +1,20 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../tests/container.html')}`; + +test.describe('Accessibility - validationSummary', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test.skip('accessibility test', async ({ page }) => { + // TODO: Convert testAccessibility() to Playwright + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/cardView/columnChooser/a11y.functional.spec.ts b/e2e/testcafe-devextreme/playwright-tests/cardView/columnChooser/a11y.functional.spec.ts new file mode 100644 index 000000000000..c4bd6cffc5e8 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/cardView/columnChooser/a11y.functional.spec.ts @@ -0,0 +1,33 @@ +import { test, expect } from '@playwright/test'; +import { createWidget } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('CardView - ColumnChooser.A11y.Functional', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('column chooser popup should have aria-label attribute', async ({ page }) => { + await createWidget(page, 'dxCardView', { + columnChooser: { + enabled: true, + }, + columns: ['Column 1'], + }); + + await page.evaluate(() => { + const instance = ($('#container') as any).dxCardView('instance'); + instance.showColumnChooser(); + }); + + const ariaLabel = await page.locator('.dx-cardview-column-chooser .dx-overlay-content').getAttribute('aria-label'); + expect(ariaLabel).toBeTruthy(); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/cardView/columnChooser/api.functional.spec.ts b/e2e/testcafe-devextreme/playwright-tests/cardView/columnChooser/api.functional.spec.ts new file mode 100644 index 000000000000..f4d8799f42fe --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/cardView/columnChooser/api.functional.spec.ts @@ -0,0 +1,51 @@ +import { test, expect } from '@playwright/test'; +import { createWidget } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('CardView - ColumnChooser.Functional', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('public method showColumnChooser', async ({ page }) => { + await createWidget(page, 'dxCardView', { + columns: ['Column 1'], + columnChooser: { + enabled: true, + }, + }); + + const columnChooser = page.locator('.dx-cardview-column-chooser'); + await expect(columnChooser).not.toBeVisible(); + + await page.evaluate(() => { + ($('#container') as any).dxCardView('instance').showColumnChooser(); + }); + await expect(columnChooser).toBeVisible(); + }); + + test('public method hideColumnChooser', async ({ page }) => { + await createWidget(page, 'dxCardView', { + columns: ['Column 1'], + columnChooser: { + enabled: true, + }, + }); + + await page.locator('.dx-cardview-column-chooser-button').click(); + const columnChooser = page.locator('.dx-cardview-column-chooser'); + await expect(columnChooser).toBeVisible(); + + await page.evaluate(() => { + ($('#container') as any).dxCardView('instance').hideColumnChooser(); + }); + await expect(columnChooser).not.toBeVisible(); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/cardView/columnChooser/functional.spec.ts b/e2e/testcafe-devextreme/playwright-tests/cardView/columnChooser/functional.spec.ts new file mode 100644 index 000000000000..e7e626f22f44 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/cardView/columnChooser/functional.spec.ts @@ -0,0 +1,102 @@ +import { test, expect } from '@playwright/test'; +import { createWidget } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('CardView - ColumnChooser.Functional', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('column chooser in select mode should work after multiple hide/show actions', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: [ + { a: 1, b: 2, c: 3 }, + { a: 1, b: 2, c: 3 }, + { a: 1, b: 2, c: 3 }, + ], + columns: ['a', 'b', 'c'], + columnChooser: { + enabled: true, + mode: 'select', + }, + }); + + await page.evaluate(() => { + ($('#container') as any).dxCardView('instance').showColumnChooser(); + }); + + const columnChooser = page.locator('.dx-cardview-column-chooser'); + const checkboxes = columnChooser.locator('.dx-checkbox'); + + await checkboxes.nth(0).click(); + await expect(checkboxes).toHaveCount(3); + + await checkboxes.nth(0).click(); + await expect(checkboxes).toHaveCount(3); + + await checkboxes.nth(0).click(); + await checkboxes.nth(0).click(); + }); + + test('column chooser in dragAndDrop mode should work after multiple hide/show actions', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: [ + { a: 1, b: 2, c: 3 }, + { a: 1, b: 2, c: 3 }, + { a: 1, b: 2, c: 3 }, + ], + columns: ['a', 'b', 'c'], + columnChooser: { + enabled: true, + mode: 'dragAndDrop', + }, + }); + + await page.evaluate(() => { + ($('#container') as any).dxCardView('instance').showColumnChooser(); + }); + + const headerItems = page.locator('.dx-cardview-headers .dx-cardview-header-item'); + await expect(headerItems).toHaveCount(3); + }); + + test('ColumnChooser should receive and render custom texts', async ({ page }) => { + await page.evaluate(() => { + (window as any).DevExpress.localization.loadMessages({ + en: { + 'dxDataGrid-columnChooserTitle': 'customTitle', + 'dxDataGrid-columnChooserEmptyText': 'customEmptyText', + }, + }); + }); + + await createWidget(page, 'dxCardView', { + dataSource: [], + keyExpr: 'ID', + cardsPerRow: 'auto', + cardMinWidth: 300, + columnChooser: { + enabled: true, + mode: 'dragAndDrop', + height: '340px', + }, + columns: [], + }); + + await page.locator('.dx-cardview-column-chooser-button').click(); + + const columnChooser = page.locator('.dx-cardview-column-chooser'); + const title = columnChooser.locator('.dx-popup-title'); + const emptyMessage = columnChooser.locator('.dx-empty-message'); + + await expect(title).toHaveText('customTitle'); + await expect(emptyMessage).toHaveText('customEmptyText'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/cardView/columnChooser/visual.spec.ts b/e2e/testcafe-devextreme/playwright-tests/cardView/columnChooser/visual.spec.ts new file mode 100644 index 000000000000..f308461a6338 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/cardView/columnChooser/visual.spec.ts @@ -0,0 +1,85 @@ +import { test } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('CardView - ColumnChooser.Visual', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test("column chooser in 'select' mode", async ({ page }) => { + await createWidget(page, 'dxCardView', { + columnChooser: { + enabled: true, + mode: 'select', + height: 400, + width: 400, + search: { enabled: true }, + selection: { allowSelectAll: true }, + }, + columns: [ + { dataField: 'Column 1', visible: false }, + { dataField: 'Column 2', allowHiding: false }, + { dataField: 'Column 3', showInColumnChooser: false }, + { dataField: 'Column 4' }, + ], + }); + + await page.evaluate(() => { + ($('#container') as any).dxCardView('instance').showColumnChooser(); + }); + + await testScreenshot(page, 'card-view_column-chooser_select_mode.png', { + element: page.locator('.dx-cardview-column-chooser .dx-overlay-content'), + }); + }); + + test("column chooser in 'dragAndDrop' mode", async ({ page }) => { + await createWidget(page, 'dxCardView', { + columnChooser: { + enabled: true, + mode: 'dragAndDrop', + height: 400, + width: 400, + search: { enabled: true }, + }, + columns: [ + { dataField: 'Column 1', visible: false }, + { dataField: 'Column 2', visible: false, allowHiding: false }, + { dataField: 'Column 3', visible: false, showInColumnChooser: false }, + { dataField: 'Column 4', visible: false }, + ], + }); + + await page.evaluate(() => { + ($('#container') as any).dxCardView('instance').showColumnChooser(); + }); + + await testScreenshot(page, 'card-view_column-chooser_drag_mode.png', { + element: page.locator('.dx-cardview-column-chooser .dx-overlay-content'), + }); + }); + + test('cardView with opened columnChooser', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: Array.from({ length: 50 }, (_, i) => ({ value: `value_${i}` })), + columnChooser: { enabled: true }, + columns: [{ dataField: 'value' }], + }); + + await page.evaluate(() => { + ($('#container') as any).dxCardView('instance').showColumnChooser(); + }); + + await testScreenshot(page, 'card-view_with_opened_column-chooser.png', { + element: page.locator('#container'), + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/cardView/columnSortable/functional.spec.ts b/e2e/testcafe-devextreme/playwright-tests/cardView/columnSortable/functional.spec.ts new file mode 100644 index 000000000000..d68ea2d32dd7 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/cardView/columnSortable/functional.spec.ts @@ -0,0 +1,44 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, setupTestPage, getContainerUrl } from '../../../playwright-helpers'; + +const containerUrl = getContainerUrl(__dirname, '../../../tests/container.html'); + +test.describe('CardView - ColumnSortable.Functional', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + [ + { allowColumnReordering: false, allowReordering: false, result: false }, + { allowColumnReordering: false, allowReordering: true, result: false }, + { allowColumnReordering: true, allowReordering: false, result: false }, + { allowColumnReordering: true, allowReordering: true, result: true }, + ].forEach(({ allowColumnReordering, allowReordering, result }) => { + test(`header column is draggable: ${result}, when allowColumnReordering: ${allowColumnReordering}, allowReordering: ${allowReordering}`, async ({ page }) => { + await createWidget(page, 'dxCardView', { + allowColumnReordering, + columns: [{ + dataField: 'test', + allowReordering, + }], + }); + + const columnElement = page.locator('.dx-cardview-headers .dx-cardview-header-item').first(); + + await page.evaluate((selector) => { + const element = document.querySelector(selector) as Element; + const left = element.getBoundingClientRect().left + 5; + const top = element.getBoundingClientRect().top + 5; + element.dispatchEvent(new MouseEvent('mousedown', { bubbles: true, cancelable: true, clientX: left, clientY: top })); + element.dispatchEvent(new MouseEvent('mousemove', { bubbles: true, cancelable: true, clientX: left, clientY: top + 30 })); + }, '.dx-cardview-headers .dx-cardview-header-item'); + + const dragging = page.locator('.dx-sortable-dragging'); + if (result) { + await expect(dragging).toBeVisible(); + } else { + await expect(dragging).not.toBeVisible(); + } + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/cardView/columnSortable/visual.spec.ts b/e2e/testcafe-devextreme/playwright-tests/cardView/columnSortable/visual.spec.ts new file mode 100644 index 000000000000..0b00cb6d0e60 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/cardView/columnSortable/visual.spec.ts @@ -0,0 +1,35 @@ +import { test } from '@playwright/test'; +import { createWidget, testScreenshot, setupTestPage, getContainerUrl } from '../../../playwright-helpers'; + +const containerUrl = getContainerUrl(__dirname, '../../../tests/container.html'); + +test.describe('CardView - ColumnSortable.Visual', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + test('headerPanel dragging column when it has sorting and headerFilter', async ({ page }) => { + await createWidget(page, 'dxCardView', { + allowColumnReordering: true, + columnChooser: { enabled: true }, + headerFilter: { visible: true }, + columns: [{ + dataField: 'test', + allowReordering: true, + sortOrder: 'asc', + }], + }); + + await page.evaluate(() => { + const element = document.querySelector('.dx-cardview-headers .dx-cardview-header-item') as Element; + const left = element.getBoundingClientRect().left + 5; + const top = element.getBoundingClientRect().top + 5; + element.dispatchEvent(new MouseEvent('mousedown', { bubbles: true, cancelable: true, clientX: left, clientY: top })); + element.dispatchEvent(new MouseEvent('mousemove', { bubbles: true, cancelable: true, clientX: left, clientY: top + 30 })); + }); + + await testScreenshot(page, 'card-view_column-sortable_header-panel_dragging-column.png', { + element: page.locator('#container'), + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/cardView/common/behavior.functional.spec.ts b/e2e/testcafe-devextreme/playwright-tests/cardView/common/behavior.functional.spec.ts new file mode 100644 index 000000000000..6fe54a8f41c9 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/cardView/common/behavior.functional.spec.ts @@ -0,0 +1,27 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, setupTestPage, getContainerUrl } from '../../../playwright-helpers'; + +const containerUrl = getContainerUrl(__dirname, '../../../tests/container.html'); + +test.describe('CardView - Common Behavior', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + test('cardHeader.visibility property should change on contentReady', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: [{ ID: 1 }], + onContentReady(e) { + e.component.option('cardHeader.visible', true); + }, + }); + + const headerVisible = await page.evaluate(() => { + return ( as any).dxCardView('instance').option('cardHeader.visible'); + }); + expect(headerVisible).toBe(true); + + const cardHeader = page.locator('.dx-cardview-card .dx-cardview-card-header'); + await expect(cardHeader.first()).toBeVisible(); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/cardView/contentView.events.spec.ts b/e2e/testcafe-devextreme/playwright-tests/cardView/contentView.events.spec.ts new file mode 100644 index 000000000000..34a904a52413 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/cardView/contentView.events.spec.ts @@ -0,0 +1,63 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, setupTestPage, getContainerUrl } from '../../playwright-helpers'; + +const containerUrl = getContainerUrl(__dirname, '../../tests/container.html'); + +const CONFIG = { + dataSource: [ + { caption1: 'value11', caption2: 'value21', caption3: 'value31' }, + { caption1: 'value12', caption2: 'value22', caption3: 'value32' }, + { caption1: 'value13', caption2: 'value23', caption3: 'value33' }, + { caption1: 'value14', caption2: 'value24', caption3: 'value34' }, + { caption1: 'value15', caption2: 'value25', caption3: 'value35' }, + ], + onCardClick(e) { + window.dxCardViewEventTest ??= {}; + window.dxCardViewEventTest.onCardClick ??= []; + window.dxCardViewEventTest.onCardClick.push(e); + }, + onCardDblClick(e) { + window.dxCardViewEventTest ??= {}; + window.dxCardViewEventTest.onCardDblClick ??= []; + window.dxCardViewEventTest.onCardDblClick.push(e); + }, + onCardPrepared(e) { + window.dxCardViewEventTest ??= {}; + window.dxCardViewEventTest.onCardPrepared ??= []; + window.dxCardViewEventTest.onCardPrepared.push(e); + }, + onDisposing() { + delete window.dxCardViewEventTest; + }, +}; + +test.describe('CardView - ContentView - events', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + test('onCardClick', async ({ page }) => { + await createWidget(page, 'dxCardView', CONFIG); + + await page.locator('.dx-cardview-card').first().click(); + + const count = await page.evaluate(() => (window as any).dxCardViewEventTest?.onCardClick?.length); + expect(count).toBe(1); + }); + + test('onCardDblClick', async ({ page }) => { + await createWidget(page, 'dxCardView', CONFIG); + + await page.locator('.dx-cardview-card').first().dblclick(); + + const count = await page.evaluate(() => (window as any).dxCardViewEventTest?.onCardDblClick?.length); + expect(count).toBe(1); + }); + + test('onCardPrepared', async ({ page }) => { + await createWidget(page, 'dxCardView', CONFIG); + + const count = await page.evaluate(() => (window as any).dxCardViewEventTest?.onCardPrepared?.length); + expect(count).toBe(5); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/cardView/contextMenu/behavior.visual.spec.ts b/e2e/testcafe-devextreme/playwright-tests/cardView/contextMenu/behavior.visual.spec.ts new file mode 100644 index 000000000000..0d5052b2bb79 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/cardView/contextMenu/behavior.visual.spec.ts @@ -0,0 +1,23 @@ +import { test } from '@playwright/test'; +import { createWidget, testScreenshot, setupTestPage, getContainerUrl } from '../../../playwright-helpers'; + +const containerUrl = getContainerUrl(__dirname, '../../../tests/container.html'); + +test.describe('CardView - ContextMenu Behavior', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + test('Context menu should be shown at the mouse cursor', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: [{ ID: 1 }], + }); + + const headerItem = page.locator('.dx-cardview-headers .dx-cardview-header-item').first(); + await headerItem.click({ button: 'right', position: { x: 10, y: 10 } }); + + await testScreenshot(page, 'card-view_context-menu_mouse-click_position.png', { + element: page.locator('#container'), + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/cardView/cover.visual.spec.ts b/e2e/testcafe-devextreme/playwright-tests/cardView/cover.visual.spec.ts new file mode 100644 index 000000000000..46029a70b496 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/cardView/cover.visual.spec.ts @@ -0,0 +1,31 @@ +import { test } from '@playwright/test'; +import { createWidget, testScreenshot, setupTestPage, getContainerUrl } from '../../playwright-helpers'; + +const containerUrl = getContainerUrl(__dirname, '../../tests/container.html'); + +test.describe('CardView - Cover', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + test('default render', async ({ page }) => { + await createWidget(page, 'dxCardView', { + width: 1000, + height: 600, + columns: ['Customer', 'Order Date'], + cardCover: { + imageExpr: (data) => data.Picture && `../../../apps/demos/${data.Picture}`, + altExpr: 'FirstName', + }, + dataSource: [ + { ID: 1, FirstName: 'John', LastName: 'Heart', Picture: 'images/employees/01.png' }, + { ID: 2, FirstName: 'Olivia', LastName: 'Peyton' }, + { ID: 3, FirstName: 'Robert', LastName: 'Reagan', Picture: 'images/employees/03.png' }, + ], + }); + + await testScreenshot(page, 'cover-default-render.png', { + element: page.locator('#container'), + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/cardView/editing/editing.functional.spec.ts b/e2e/testcafe-devextreme/playwright-tests/cardView/editing/editing.functional.spec.ts new file mode 100644 index 000000000000..e52d3b79aa11 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/cardView/editing/editing.functional.spec.ts @@ -0,0 +1,42 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, setupTestPage, getContainerUrl } from '../../../playwright-helpers'; + +const containerUrl = getContainerUrl(__dirname, '../../../tests/container.html'); + +test.describe('CardView - Editing', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + test('should show default values in popup fields after onInitNewCard', async ({ page }) => { + await createWidget(page, 'dxCardView', { + columns: [ + { dataField: 'id', caption: 'ID' }, + { dataField: 'title', caption: 'Task Title' }, + { dataField: 'status', caption: 'Status' }, + ], + dataSource: [], + keyExpr: 'id', + editing: { + allowAdding: true, + form: { items: ['id', 'title', 'status'] }, + }, + onInitNewCard(e) { + e.data.id = 10; + e.data.status = 'Not Started'; + e.data.title = 'New Task'; + }, + }); + + await page.locator('.dx-cardview-addcard-button').click(); + await page.waitForSelector('.dx-cardview-edit-popup'); + + const idInput = page.locator('.dx-cardview-edit-popup input[name="id"]'); + const titleInput = page.locator('.dx-cardview-edit-popup input[name="title"]'); + const statusInput = page.locator('.dx-cardview-edit-popup input[name="status"]'); + + await expect(idInput).toHaveValue('10'); + await expect(titleInput).toHaveValue('New Task'); + await expect(statusInput).toHaveValue('Not Started'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/cardView/editing/editing.visual.spec.ts b/e2e/testcafe-devextreme/playwright-tests/cardView/editing/editing.visual.spec.ts new file mode 100644 index 000000000000..02a06eb424df --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/cardView/editing/editing.visual.spec.ts @@ -0,0 +1,60 @@ +import { test } from '@playwright/test'; +import { createWidget, testScreenshot, setupTestPage, getContainerUrl } from '../../../playwright-helpers'; + +const containerUrl = getContainerUrl(__dirname, '../../../tests/container.html'); + +const columns = ['id', 'title', 'name', 'lastName']; +const data = [ + { id: 1, title: 'Mr.', name: 'John', lastName: 'Heart' }, + { id: 2, title: 'Mrs.', name: 'Olivia', lastName: 'Peyton' }, + { id: 3, title: 'Mr.', name: 'Robert', lastName: 'Reagan' }, + { id: 4, title: 'Mr.', name: 'Greta', lastName: 'Sims' }, +]; + +const baseConfig = { + columns, + dataSource: data, + keyExpr: 'id', + editing: { + allowUpdating: true, + allowDeleting: true, + allowAdding: true, + }, +}; + +test.describe('CardView - Editing Visual', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + test('default render', async ({ page }) => { + await page.setViewportSize({ width: 1100, height: 700 }); + await createWidget(page, 'dxCardView', baseConfig); + + await testScreenshot(page, 'editing-default-render.png', { + element: page.locator('#container'), + }); + }); + + test('render of add card popup', async ({ page }) => { + await page.setViewportSize({ width: 1100, height: 700 }); + await createWidget(page, 'dxCardView', baseConfig); + + await page.locator('.dx-cardview-addcard-button').click(); + + await testScreenshot(page, 'editing-popup-add.png', { + element: page.locator('#container'), + }); + }); + + test('render of edit card popup', async ({ page }) => { + await page.setViewportSize({ width: 1100, height: 700 }); + await createWidget(page, 'dxCardView', baseConfig); + + await page.locator('.dx-cardview-card').first().locator('.dx-toolbar-item').first().click(); + + await testScreenshot(page, 'editing-popup-edit.png', { + element: page.locator('#container'), + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/cardView/filterPanel/api.filterBuilder.functional.spec.ts b/e2e/testcafe-devextreme/playwright-tests/cardView/filterPanel/api.filterBuilder.functional.spec.ts new file mode 100644 index 000000000000..1a6e29eb15ea --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/cardView/filterPanel/api.filterBuilder.functional.spec.ts @@ -0,0 +1,70 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('CardView - FilterBuilder API', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('filterBuilder.height API', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: [ + { id: 1, title: 'Mr.', name: 'John', lastName: 'Heart' }, + { id: 2, title: 'Mrs.', name: 'Olivia', lastName: 'Peyton' }, + { id: 3, title: 'Mr.', name: 'Robert', lastName: 'Reagan' }, + { id: 4, title: 'Mr.', name: 'Greta', lastName: 'Sims' }, + ], + columns: [{ dataField: 'id' }, { dataField: 'title' }, { dataField: 'name' }, { dataField: 'lastName' }], + filterPanel: { visible: true }, + filterBuilder: { height: 500 }, + }); + + await page.locator('.dx-cardview-filter-panel .dx-icon-filter').click(); + await page.waitForSelector('.dx-filterbuilder-popup'); + + const fbHeight = await page.locator('.dx-filterbuilder').evaluate(el => el.clientHeight); + expect(fbHeight).toBe(500); + + await page.evaluate(() => { + ($('#container') as any).dxCardView('instance').option('filterBuilder.height', 700); + }); + + const newHeight = await page.locator('.dx-filterbuilder').evaluate(el => el.clientHeight); + expect(newHeight).toBe(700); + }); + + test('filterBuilder.hint API', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: [ + { id: 1, title: 'Mr.', name: 'John', lastName: 'Heart' }, + { id: 2, title: 'Mrs.', name: 'Olivia', lastName: 'Peyton' }, + { id: 3, title: 'Mr.', name: 'Robert', lastName: 'Reagan' }, + { id: 4, title: 'Mr.', name: 'Greta', lastName: 'Sims' }, + ], + columns: [{ dataField: 'id' }, { dataField: 'title' }, { dataField: 'name' }, { dataField: 'lastName' }], + filterPanel: { visible: true }, + filterBuilder: { hint: 'Test' }, + }); + + await page.locator('.dx-cardview-filter-panel .dx-icon-filter').click(); + await page.waitForSelector('.dx-filterbuilder-popup'); + + const hint = await page.locator('.dx-filterbuilder').getAttribute('title'); + expect(hint).toBe('Test'); + + await page.evaluate(() => { + ($('#container') as any).dxCardView('instance').option('filterBuilder.hint', 'Test2'); + }); + + const newHint = await page.locator('.dx-filterbuilder').getAttribute('title'); + expect(newHint).toBe('Test2'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/cardView/filterPanel/api.filterBuilderPopup.functional.spec.ts b/e2e/testcafe-devextreme/playwright-tests/cardView/filterPanel/api.filterBuilderPopup.functional.spec.ts new file mode 100644 index 000000000000..347db4d11cac --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/cardView/filterPanel/api.filterBuilderPopup.functional.spec.ts @@ -0,0 +1,50 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('CardView - FilterBuilderPopup API', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('filterBuilderPopup.height API', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: [ + { id: 1, title: 'Mr.', name: 'John', lastName: 'Heart' }, + ], + columns: [{ dataField: 'id' }, { dataField: 'title' }, { dataField: 'name' }, { dataField: 'lastName' }], + filterPanel: { visible: true }, + filterBuilderPopup: { height: 500 }, + }); + + await page.locator('.dx-cardview-filter-panel .dx-icon-filter').click(); + await page.waitForSelector('.dx-filterbuilder-popup'); + + const contentHeight = await page.locator('.dx-filterbuilder-popup .dx-overlay-content').evaluate(el => el.offsetHeight); + expect(contentHeight).toBe(500); + }); + + test('filterBuilderPopup.title API', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: [ + { id: 1, title: 'Mr.', name: 'John', lastName: 'Heart' }, + ], + columns: [{ dataField: 'id' }, { dataField: 'title' }, { dataField: 'name' }, { dataField: 'lastName' }], + filterPanel: { visible: true }, + filterBuilderPopup: { title: 'Test' }, + }); + + await page.locator('.dx-cardview-filter-panel .dx-icon-filter').click(); + await page.waitForSelector('.dx-filterbuilder-popup'); + + const titleText = await page.locator('.dx-filterbuilder-popup .dx-toolbar').innerText(); + expect(titleText).toBe('Test'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/cardView/filterPanel/api.functional.spec.ts b/e2e/testcafe-devextreme/playwright-tests/cardView/filterPanel/api.functional.spec.ts new file mode 100644 index 000000000000..74cad2b46188 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/cardView/filterPanel/api.functional.spec.ts @@ -0,0 +1,59 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('CardView - FilterPanel API', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('filterPanel.visible API', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: [ + { id: 1, title: 'Mr.', name: 'John', lastName: 'Heart' }, + ], + columns: [{ dataField: 'id' }, { dataField: 'title' }, { dataField: 'name' }, { dataField: 'lastName' }], + filterPanel: { visible: false }, + filterValue: ['title', '=', 'Mr.'], + }); + + const filterPanel = page.locator('.dx-cardview-filter-panel'); + await expect(filterPanel).not.toBeVisible(); + + await page.evaluate(() => { + ($('#container') as any).dxCardView('instance').option('filterPanel.visible', true); + }); + + await expect(filterPanel).toBeVisible(); + }); + + test('clearFilter API', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: [ + { id: 1, title: 'Mr.', name: 'John', lastName: 'Heart' }, + { id: 2, title: 'Mrs.', name: 'Olivia', lastName: 'Peyton' }, + { id: 3, title: 'Mr.', name: 'Robert', lastName: 'Reagan' }, + { id: 4, title: 'Mr.', name: 'Greta', lastName: 'Sims' }, + ], + columns: [{ dataField: 'id' }, { dataField: 'title' }, { dataField: 'name' }, { dataField: 'lastName' }], + filterPanel: { visible: true }, + filterValue: ['title', '=', 'Mr.'], + }); + + const cards = page.locator('.dx-cardview-card'); + await expect(cards).toHaveCount(3); + + await page.evaluate(() => { + ($('#container') as any).dxCardView('instance').clearFilter(); + }); + + await expect(cards).toHaveCount(4); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/cardView/filterPanel/behavior.functional.spec.ts b/e2e/testcafe-devextreme/playwright-tests/cardView/filterPanel/behavior.functional.spec.ts new file mode 100644 index 000000000000..cbab595f2aac --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/cardView/filterPanel/behavior.functional.spec.ts @@ -0,0 +1,69 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('CardView - FilterPanel Behavior', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('FilterIcon opens popup by click', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: [ + { id: 1, title: 'Mr.', name: 'John', lastName: 'Heart' }, + ], + columns: [{ dataField: 'id' }, { dataField: 'title' }, { dataField: 'name' }, { dataField: 'lastName' }], + filterPanel: { visible: true }, + }); + + const popup = page.locator('.dx-filterbuilder-popup'); + await expect(popup).not.toBeVisible(); + + await page.locator('.dx-cardview-filter-panel .dx-icon-filter').click(); + await expect(popup).toBeVisible(); + }); + + test('FilterText opens popup by click', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: [ + { id: 1, title: 'Mr.', name: 'John', lastName: 'Heart' }, + ], + columns: [{ dataField: 'id' }, { dataField: 'title' }, { dataField: 'name' }, { dataField: 'lastName' }], + filterPanel: { visible: true }, + }); + + const popup = page.locator('.dx-filterbuilder-popup'); + await expect(popup).not.toBeVisible(); + + await page.locator('.dx-cardview-filter-panel .dx-cardview-filter-panel-text').click(); + await expect(popup).toBeVisible(); + }); + + test('ClearFilter button clears filter by click', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: [ + { id: 1, title: 'Mr.', name: 'John', lastName: 'Heart' }, + { id: 2, title: 'Mrs.', name: 'Olivia', lastName: 'Peyton' }, + { id: 3, title: 'Mr.', name: 'Robert', lastName: 'Reagan' }, + { id: 4, title: 'Mr.', name: 'Greta', lastName: 'Sims' }, + ], + columns: [{ dataField: 'id' }, { dataField: 'title' }, { dataField: 'name' }, { dataField: 'lastName' }], + filterPanel: { visible: true }, + filterValue: ['title', '=', 'Mr.'], + }); + + await page.locator('.dx-cardview-filter-panel .dx-cardview-filter-panel-clear-filter').click(); + + const filterValue = await page.evaluate(() => { + return ($('#container') as any).dxCardView('instance').option('filterValue'); + }); + expect(filterValue).toBeNull(); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/cardView/filterPanel/behavior.themes.spec.ts b/e2e/testcafe-devextreme/playwright-tests/cardView/filterPanel/behavior.themes.spec.ts new file mode 100644 index 000000000000..d099f9858bd0 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/cardView/filterPanel/behavior.themes.spec.ts @@ -0,0 +1,40 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('CardView - FilterPanel Appearance', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('FilterPanel and FilterBuilderPopup screenshots', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: [ + { id: 1, title: 'Mr.', name: 'John', lastName: 'Heart' }, + { id: 2, title: 'Mrs.', name: 'Olivia', lastName: 'Peyton' }, + { id: 3, title: 'Mr.', name: 'Robert', lastName: 'Reagan' }, + { id: 4, title: 'Mr.', name: 'Greta', lastName: 'Sims' }, + ], + columns: [{ dataField: 'id' }, { dataField: 'title' }, { dataField: 'name' }, { dataField: 'lastName' }], + filterPanel: { visible: true }, + filterValue: ['title', '=', 'Mr.'], + }); + + await testScreenshot(page, 'cardView_FilterPanel.png', { + element: page.locator('.dx-cardview-filter-panel'), + }); + + await page.locator('.dx-cardview-filter-panel .dx-icon-filter').click(); + + await testScreenshot(page, 'cardView_FilterBuilderPopup.png', { + element: page.locator('.dx-filterbuilder-popup'), + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/cardView/headerFilter/a11y.functional.spec.ts b/e2e/testcafe-devextreme/playwright-tests/cardView/headerFilter/a11y.functional.spec.ts new file mode 100644 index 000000000000..e2e2fec79df4 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/cardView/headerFilter/a11y.functional.spec.ts @@ -0,0 +1,54 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('HeaderFilter.A11y.Functional', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('should open popup by enter if filter icon in the focused state', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: [{ A: 'A_0' }, { A: 'A_1' }, { A: 'A_2' }], + columns: [{ dataField: 'A', caption: 'LONG_COLUMN_A_CAPTION' }], + headerFilter: { visible: true }, + height: 600, + }); + + const headerItem = page.locator('.dx-cardview-headers .dx-cardview-header-item').first(); + await headerItem.click(); + await page.keyboard.press('Alt+ArrowDown'); + + const list = page.locator('.dx-list'); + await expect(list).toBeVisible(); + }); + + test('should return focus on the same icon after the popup closing', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: [{ A: 'A_0' }, { A: 'A_1' }, { A: 'A_2' }], + columns: [{ dataField: 'A', caption: 'LONG_COLUMN_A_CAPTION' }], + headerFilter: { visible: true }, + height: 600, + }); + + const headerItem = page.locator('.dx-cardview-headers .dx-cardview-header-item').first(); + await headerItem.click(); + await page.keyboard.press('Alt+ArrowDown'); + + const list = page.locator('.dx-list'); + await expect(list).toBeVisible(); + + await page.keyboard.press('Tab'); + await page.keyboard.press('Tab'); + await page.keyboard.press('Enter'); + + await expect(headerItem).toBeFocused(); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/cardView/headerFilter/api.functional.spec.ts b/e2e/testcafe-devextreme/playwright-tests/cardView/headerFilter/api.functional.spec.ts new file mode 100644 index 000000000000..a5d545ccd815 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/cardView/headerFilter/api.functional.spec.ts @@ -0,0 +1,34 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('HeaderFilter.API.Functional', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('headerFilter.visible API', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: [{ A: 'A_0' }, { A: 'A_1' }], + columns: ['A'], + headerFilter: { visible: false }, + height: 600, + }); + + const filterIcon = page.locator('.dx-header-filter'); + await expect(filterIcon).not.toBeVisible(); + + await page.evaluate(() => { + ($('#container') as any).dxCardView('instance').option('headerFilter.visible', true); + }); + + await expect(filterIcon.first()).toBeVisible(); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/cardView/headerFilter/common.functional.spec.ts b/e2e/testcafe-devextreme/playwright-tests/cardView/headerFilter/common.functional.spec.ts new file mode 100644 index 000000000000..4285dd9a4c54 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/cardView/headerFilter/common.functional.spec.ts @@ -0,0 +1,33 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('HeaderFilter.Common.Functional', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('popup should open on header filter icon click', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: [ + { A: 'A_0', B: 'B_0', C: 'C_0' }, + { A: 'A_1', B: 'B_1', C: 'C_1' }, + ], + columns: ['A', 'B', 'C'], + headerFilter: { visible: true }, + height: 600, + }); + + await page.locator('.dx-header-filter').first().click(); + + const popup = page.locator('.dx-header-filter-menu'); + await expect(popup).toBeVisible(); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/cardView/headerFilter/local.functional.spec.ts b/e2e/testcafe-devextreme/playwright-tests/cardView/headerFilter/local.functional.spec.ts new file mode 100644 index 000000000000..c954e844e574 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/cardView/headerFilter/local.functional.spec.ts @@ -0,0 +1,35 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('HeaderFilter.Local.Functional', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('should filter data after selecting item', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: [ + { A: 'A_0', B: 'B_0' }, + { A: 'A_1', B: 'B_1' }, + { A: 'A_2', B: 'B_2' }, + ], + columns: ['A', 'B'], + headerFilter: { visible: true }, + height: 600, + }); + + await page.locator('.dx-header-filter').first().click(); + await page.waitForSelector('.dx-header-filter-menu'); + + const listItems = page.locator('.dx-list-item'); + await expect(listItems).toHaveCount(3); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/cardView/headerFilter/remote.functional.spec.ts b/e2e/testcafe-devextreme/playwright-tests/cardView/headerFilter/remote.functional.spec.ts new file mode 100644 index 000000000000..fb45be35e10d --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/cardView/headerFilter/remote.functional.spec.ts @@ -0,0 +1,20 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('HeaderFilter.Remote.Functional', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test.skip('remote header filter should load grouped data', async ({ page }) => { + // TODO: Convert remote API mock (RequestMock) to Playwright route handling + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/cardView/headerFilter/visual.spec.ts b/e2e/testcafe-devextreme/playwright-tests/cardView/headerFilter/visual.spec.ts new file mode 100644 index 000000000000..30fd27808156 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/cardView/headerFilter/visual.spec.ts @@ -0,0 +1,54 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('HeaderFilter.Visual', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('popup with list', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: [ + { A: 'A_0', B: 'B_0', C: 'C_0' }, + { A: 'A_1', B: 'B_1', C: 'C_1' }, + { A: 'A_2', B: 'B_2', C: 'C_2' }, + ], + columns: ['A', 'B', 'C'], + headerFilter: { visible: true }, + height: 600, + }); + + await page.locator('.dx-header-filter').first().click(); + + await testScreenshot(page, 'card-view_header-filter_popup-with-list.png', { + element: page.locator('#container'), + }); + }); + + test('popup with search', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: [ + { A: 'A_0', B: 'B_0', C: 'C_0' }, + { A: 'A_1', B: 'B_1', C: 'C_1' }, + { A: 'A_2', B: 'B_2', C: 'C_2' }, + ], + columns: ['A', 'B', 'C'], + headerFilter: { visible: true, search: { enabled: true } }, + height: 600, + }); + + await page.locator('.dx-header-filter').first().click(); + + await testScreenshot(page, 'card-view_header-filter_popup-with-search.png', { + element: page.locator('#container'), + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/cardView/headerPanel/sortable.visual.spec.ts b/e2e/testcafe-devextreme/playwright-tests/cardView/headerPanel/sortable.visual.spec.ts new file mode 100644 index 000000000000..2a882e760229 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/cardView/headerPanel/sortable.visual.spec.ts @@ -0,0 +1,20 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('CardView - HeaderPanel Sortable Visual', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test.skip('sortable indicator during dragging', async ({ page }) => { + // TODO: Convert drag-and-drop visual tests with MouseUpEvents helpers to Playwright + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/cardView/headerPanel/visual.spec.ts b/e2e/testcafe-devextreme/playwright-tests/cardView/headerPanel/visual.spec.ts new file mode 100644 index 000000000000..824ace83d479 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/cardView/headerPanel/visual.spec.ts @@ -0,0 +1,65 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('CardView - HeaderPanel Visual', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('default render', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: [{ id: 0, filedA: 'A_0', filedB: 'B_0', fieldC: 'C_0' }], + width: 600, + }); + + await testScreenshot(page, 'default-render.png', { + element: page.locator('.dx-cardview-headers'), + }); + }); + + test('render with header filter enabled', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: [{ id: 0, filedA: 'A_0', filedB: 'B_0', fieldC: 'C_0' }], + headerFilter: { visible: true }, + width: 600, + }); + + await testScreenshot(page, 'header-filter-enabled.png', { + element: page.locator('.dx-cardview-headers'), + }); + }); + + test('render with single sorting', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: [{ id: 0, filedA: 'A_0', filedB: 'B_0', fieldC: 'C_0' }], + columns: ['id', 'filedA', { dataField: 'filedB', sortOrder: 'asc' }, 'fieldC'], + width: 600, + }); + + await testScreenshot(page, 'single-sorting.png', { + element: page.locator('.dx-cardview-headers'), + }); + }); + + test('headerPanel column chooser link opens column chooser on click', async ({ page }) => { + await createWidget(page, 'dxCardView', { + height: 600, + columns: [{ dataField: 'Column 1', visible: false }], + columnChooser: { enabled: true }, + }); + + await page.locator('.dx-cardview-headers .dx-cardview-column-chooser-link').click(); + + await testScreenshot(page, 'card-view-column-chooser-opened-on-empty-header-panel-link-click.png', { + element: page.locator('#container'), + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/cardView/items.functional.spec.ts b/e2e/testcafe-devextreme/playwright-tests/cardView/items.functional.spec.ts new file mode 100644 index 000000000000..fb155fd4968a --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/cardView/items.functional.spec.ts @@ -0,0 +1,27 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, setupTestPage, getContainerUrl } from '../../playwright-helpers'; + +const containerUrl = getContainerUrl(__dirname, '../../tests/container.html'); + +test.describe('CardView - Items functional', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + test("Column should show data from calculateDisplayValue if function's result has other dataType", async ({ page }) => { + await createWidget(page, 'dxCardView', { + columns: [{ + dataField: 'activity', + columnType: 'number', + calculateDisplayValue(e) { + return `activity ${e.activity}`; + }, + }], + dataSource: [{ id: 1, activity: 1 }], + keyExpr: 'id', + }); + + const valueCell = page.locator('.dx-cardview-card .dx-cardview-field-value').first(); + await expect(valueCell).toHaveText('activity 1'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/cardView/keyboardNavigation/api.onFocusedCardChanged.functional.spec.ts b/e2e/testcafe-devextreme/playwright-tests/cardView/keyboardNavigation/api.onFocusedCardChanged.functional.spec.ts new file mode 100644 index 000000000000..57ffbecd9355 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/cardView/keyboardNavigation/api.onFocusedCardChanged.functional.spec.ts @@ -0,0 +1,61 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('KeyboardNavigation.onFocusedCardChanged', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('Should be called on each card focus change', async ({ page }) => { + await page.evaluate(() => { (window as any).onFocusedCardChangedArgs = []; }); + await createWidget(page, 'dxCardView', { + dataSource: new Array(9).fill(undefined).map((_, idx) => ({ id: idx })), + columns: ['id'], + keyExpr: 'id', + paging: { pageSize: 9 }, + onFocusedCardChanged: ({ cardIndex }) => { + (window as any).onFocusedCardChangedArgs.push(cardIndex); + }, + height: 700, + }); + + const card = page.locator('.dx-cardview-card').nth(4); + await card.click(); + + for (const key of ['ArrowDown', 'ArrowRight', 'ArrowUp', 'ArrowLeft']) { + await card.dispatchEvent('keydown', { key }); + } + + const result = await page.evaluate(() => (window as any).onFocusedCardChangedArgs); + expect(result).toEqual([4, 7, 8, 5, 4]); + }); + + test('Should be called on focus change by click', async ({ page }) => { + await page.evaluate(() => { (window as any).onFocusedCardChangedArgs = []; }); + await createWidget(page, 'dxCardView', { + dataSource: new Array(9).fill(undefined).map((_, idx) => ({ id: idx })), + columns: ['id'], + keyExpr: 'id', + paging: { pageSize: 9 }, + onFocusedCardChanged: ({ cardIndex }) => { + (window as any).onFocusedCardChangedArgs.push(cardIndex); + }, + height: 700, + }); + + await page.locator('.dx-cardview-card').nth(5).click(); + await page.locator('.dx-cardview-card').nth(8).click(); + await page.locator('.dx-cardview-card').nth(0).click(); + + const result = await page.evaluate(() => (window as any).onFocusedCardChangedArgs); + expect(result).toEqual([5, 8, 0]); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/cardView/keyboardNavigation/api.onKeyDown.functional.spec.ts b/e2e/testcafe-devextreme/playwright-tests/cardView/keyboardNavigation/api.onKeyDown.functional.spec.ts new file mode 100644 index 000000000000..3d50cdfcfe0a --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/cardView/keyboardNavigation/api.onKeyDown.functional.spec.ts @@ -0,0 +1,54 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('KeyboardNavigation.OnKeyDown', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('Should be called on header item unhandled event', async ({ page }) => { + await page.evaluate(() => { (window as any).onKeyDownArgs = []; }); + await createWidget(page, 'dxCardView', { + dataSource: new Array(6).fill(undefined).map((_, idx) => ({ id: idx })), + columns: ['id'], + keyExpr: 'id', + onKeyDown: ({ handled, event: { key } }) => { + (window as any).onKeyDownArgs.push({ handled, key }); + }, + height: 700, + }); + + const headerItem = page.locator('.dx-cardview-headers .dx-cardview-header-item').first(); + await headerItem.dispatchEvent('keydown', { key: 'a' }); + + const result = await page.evaluate(() => (window as any).onKeyDownArgs); + expect(result).toEqual([{ handled: false, key: 'a' }]); + }); + + test('Should be called on card handled event', async ({ page }) => { + await page.evaluate(() => { (window as any).onKeyDownArgs = []; }); + await createWidget(page, 'dxCardView', { + dataSource: new Array(6).fill(undefined).map((_, idx) => ({ id: idx })), + columns: ['id'], + keyExpr: 'id', + onKeyDown: ({ handled, event: { key } }) => { + (window as any).onKeyDownArgs.push({ handled, key }); + }, + height: 700, + }); + + const card = page.locator('.dx-cardview-card').first(); + await card.dispatchEvent('keydown', { key: 'ArrowRight' }); + + const result = await page.evaluate(() => (window as any).onKeyDownArgs); + expect(result).toEqual([{ handled: true, key: 'ArrowRight' }]); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/cardView/keyboardNavigation/contentView.functional.spec.ts b/e2e/testcafe-devextreme/playwright-tests/cardView/keyboardNavigation/contentView.functional.spec.ts new file mode 100644 index 000000000000..f87c95239f6a --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/cardView/keyboardNavigation/contentView.functional.spec.ts @@ -0,0 +1,74 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('KeyboardNavigation.ContentView', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + [ + { caseName: 'arrows -> left item', keys: 'ArrowLeft', resultIndex: 3 }, + { caseName: 'arrows -> right item', keys: 'ArrowRight', resultIndex: 5 }, + { caseName: 'arrows -> top item', keys: 'ArrowUp', resultIndex: 1 }, + { caseName: 'arrows -> bottom item', keys: 'ArrowDown', resultIndex: 7 }, + ].forEach(({ caseName, keys, resultIndex }) => { + test(`Should move between cards: ${caseName}`, async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: new Array(9).fill(undefined).map((_, idx) => ({ id: idx })), + columns: ['id'], + keyExpr: 'id', + paging: { pageSize: 9 }, + height: 700, + }); + + const card4 = page.locator('.dx-cardview-card').nth(4); + await card4.click(); + await page.keyboard.press(keys); + + const targetCard = page.locator('.dx-cardview-card').nth(resultIndex); + await expect(targetCard).toBeFocused(); + }); + }); + + test('Should change page to the next one and focus first card', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: new Array(9).fill(undefined).map((_, idx) => ({ id: idx })), + columns: ['id'], + keyExpr: 'id', + paging: { pageSize: 3, pageIndex: 1 }, + height: 700, + }); + + const card = page.locator('.dx-cardview-card').nth(1); + await card.click(); + await page.keyboard.press('PageDown'); + + const firstCard = page.locator('.dx-cardview-card').first(); + await expect(firstCard).toBeFocused(); + }); + + test('Should change page to the previous one and focus first card', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: new Array(9).fill(undefined).map((_, idx) => ({ id: idx })), + columns: ['id'], + keyExpr: 'id', + paging: { pageSize: 3, pageIndex: 1 }, + height: 700, + }); + + const card = page.locator('.dx-cardview-card').nth(1); + await card.click(); + await page.keyboard.press('PageUp'); + + const firstCard = page.locator('.dx-cardview-card').first(); + await expect(firstCard).toBeFocused(); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/cardView/keyboardNavigation/header.functional.spec.ts b/e2e/testcafe-devextreme/playwright-tests/cardView/keyboardNavigation/header.functional.spec.ts new file mode 100644 index 000000000000..a61eccfb11a8 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/cardView/keyboardNavigation/header.functional.spec.ts @@ -0,0 +1,46 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('KeyboardNavigation.Header', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('Should navigate between items by arrows', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: [{ id: 0, A: 'A_0', B: 'B_0', C: 'C_0' }], + columns: ['A', 'B', 'C'], + keyExpr: 'id', + height: 700, + }); + + const headerItems = page.locator('.dx-cardview-headers .dx-cardview-header-item'); + await headerItems.nth(0).click(); + await page.keyboard.press('ArrowRight'); + await page.keyboard.press('ArrowRight'); + + await expect(headerItems.nth(2)).toBeFocused(); + }); + + test('Should focus item by click', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: [{ id: 0, A: 'A_0', B: 'B_0', C: 'C_0' }], + columns: ['A', 'B', 'C'], + keyExpr: 'id', + height: 700, + }); + + const headerItems = page.locator('.dx-cardview-headers .dx-cardview-header-item'); + await headerItems.nth(1).click(); + + await expect(headerItems.nth(1)).toBeFocused(); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/cardView/keyboardNavigation/search.functional.spec.ts b/e2e/testcafe-devextreme/playwright-tests/cardView/keyboardNavigation/search.functional.spec.ts new file mode 100644 index 000000000000..cdaa859d433b --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/cardView/keyboardNavigation/search.functional.spec.ts @@ -0,0 +1,33 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('KeyboardNavigation.Search', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('Should focus search text box after ctrl+f if card is focused', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: new Array(6).fill(undefined).map((_, idx) => ({ id: idx })), + columns: ['id'], + keyExpr: 'id', + searchPanel: { visible: true }, + height: 700, + }); + + const card = page.locator('.dx-cardview-card').nth(1); + await card.click(); + await card.dispatchEvent('keydown', { key: 'f', ctrlKey: true }); + + const searchInput = page.locator('.dx-cardview-search .dx-texteditor-input'); + await expect(searchInput).toBeFocused(); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/cardView/keyboardNavigation/selection.functional.spec.ts b/e2e/testcafe-devextreme/playwright-tests/cardView/keyboardNavigation/selection.functional.spec.ts new file mode 100644 index 000000000000..5274c7380496 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/cardView/keyboardNavigation/selection.functional.spec.ts @@ -0,0 +1,66 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('KeyboardNavigation.Selection', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + [ + { caseName: 'card selection', keys: ['Space'], result: [false, true, false] }, + { caseName: 'card cannot be deselected', keys: ['Space', 'Space'], result: [false, true, false] }, + { caseName: 'the next card selection', keys: ['Space', 'ArrowRight', 'Space'], result: [false, false, true] }, + ].forEach(({ caseName, keys, result }) => { + test(`Should handle selection in single mode: ${caseName}`, async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: new Array(3).fill(undefined).map((_, idx) => ({ id: idx })), + columns: ['id'], + keyExpr: 'id', + selection: { mode: 'single' }, + height: 700, + }); + + const card = page.locator('.dx-cardview-card').nth(1); + await card.click(); + + for (const key of keys) { + await page.keyboard.press(key); + } + + for (let i = 0; i < 3; i++) { + const isSelected = await page.locator('.dx-cardview-card').nth(i).evaluate( + el => el.classList.contains('dx-selection') + ); + expect(isSelected).toBe(result[i]); + } + }); + }); + + test('Should select all cards after ctrl+a with selection multiple mode', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: new Array(3).fill(undefined).map((_, idx) => ({ id: idx })), + columns: ['id'], + keyExpr: 'id', + selection: { mode: 'multiple' }, + height: 700, + }); + + const card = page.locator('.dx-cardview-card').nth(1); + await card.dispatchEvent('keydown', { key: 'a', ctrlKey: true }); + + for (let i = 0; i < 3; i++) { + const isSelected = await page.locator('.dx-cardview-card').nth(i).evaluate( + el => el.classList.contains('dx-selection') + ); + expect(isSelected).toBe(true); + } + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/cardView/loadPanel.visual.spec.ts b/e2e/testcafe-devextreme/playwright-tests/cardView/loadPanel.visual.spec.ts new file mode 100644 index 000000000000..0532a95405e4 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/cardView/loadPanel.visual.spec.ts @@ -0,0 +1,42 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, setupTestPage, getContainerUrl } from '../../playwright-helpers'; + +const containerUrl = getContainerUrl(__dirname, '../../tests/container.html'); + +test.describe('CardView - LoadPanel', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + test('Default render', async ({ page }) => { + await page.setViewportSize({ width: 800, height: 800 }); + await createWidget(page, 'dxCardView', { + width: 500, + height: 300, + dataSource: { + key: 'id', + load: () => new Promise(() => {}), + }, + columns: ['A', 'B', 'C', 'D'], + }); + + await testScreenshot(page, 'load-panel.png', { + element: page.locator('#container'), + }); + }); + + test('Default render when CardView has a large height', async ({ page }) => { + await page.setViewportSize({ width: 800, height: 800 }); + await createWidget(page, 'dxCardView', { + width: 500, + height: 3000, + dataSource: { + key: 'id', + load: () => new Promise(() => {}), + }, + columns: ['A', 'B', 'C', 'D'], + }); + + await testScreenshot(page, 'load-panel-with-large-height.png'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/cardView/noData.visual.spec.ts b/e2e/testcafe-devextreme/playwright-tests/cardView/noData.visual.spec.ts new file mode 100644 index 000000000000..04dcb269fc0e --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/cardView/noData.visual.spec.ts @@ -0,0 +1,23 @@ +import { test } from '@playwright/test'; +import { createWidget, testScreenshot, setupTestPage, getContainerUrl } from '../../playwright-helpers'; + +const containerUrl = getContainerUrl(__dirname, '../../tests/container.html'); + +test.describe('CardView - NoData', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + test('default render', async ({ page }) => { + await createWidget(page, 'dxCardView', { + width: 1000, + height: 600, + columns: ['Customer', 'Order Date'], + dataSource: [], + }); + + await testScreenshot(page, 'content-no-data.png', { + element: page.locator('#container'), + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/cardView/pager.spec.ts b/e2e/testcafe-devextreme/playwright-tests/cardView/pager.spec.ts new file mode 100644 index 000000000000..8506b78ac03e --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/cardView/pager.spec.ts @@ -0,0 +1,77 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, setupTestPage, getContainerUrl } from '../../playwright-helpers'; + +const containerUrl = getContainerUrl(__dirname, '../../tests/container.html'); + +async function createCardViewWithPager(page, config = {}) { + const dataSource = Array.from({ length: 20 }, (_, i) => ({ text: i.toString(), value: i })); + return createWidget(page, 'dxCardView', { + dataSource, + columns: ['text', 'value'], + paging: { pageSize: 2, pageIndex: 5 }, + pager: { + showPageSizeSelector: true, + allowedPageSizes: [2, 3, 4], + showInfo: true, + showNavigationButtons: true, + }, + ...config, + }); +} + +test.describe('CardView - Pager', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + test('Page index interaction', async ({ page }) => { + await createCardViewWithPager(page); + + const pagerInfo = page.locator('.dx-info'); + await expect(pagerInfo).toHaveText('Page 6 of 10 (20 items)'); + + await page.locator('.dx-page').filter({ hasText: '7' }).click(); + await expect(pagerInfo).toHaveText('Page 7 of 10 (20 items)'); + + await page.locator('.dx-prev-button').click(); + await expect(pagerInfo).toHaveText('Page 6 of 10 (20 items)'); + }); + + [true, false].forEach((remoteOperation) => { + test(`Runtime filterValue change updates paging when remoteOperations = ${remoteOperation}`, async ({ page }) => { + await createCardViewWithPager(page, { remoteOperations: remoteOperation }); + + await page.evaluate(() => { + ( as any).dxCardView('instance').option('filterValue', [ + ['value', '=', '1'], + 'or', ['value', '=', '2'], + 'or', ['value', '=', '3'], + 'or', ['value', '=', '4'], + ]); + }); + + await testScreenshot(page, `filter-value-edit-paging-update-remoteOperations-${remoteOperation}.png`, { + element: page.locator('#container'), + }); + }); + }); + + test('Paging after resetting filter', async ({ page }) => { + await createCardViewWithPager(page, { filterPanel: { visible: true } }); + + await page.evaluate(() => { + ( as any).dxCardView('instance').option('filterValue', ['text', '=', '0']); + }); + + const pager = page.locator('.dx-pager'); + await expect(pager).not.toBeVisible(); + + await page.evaluate(() => { + ( as any).dxCardView('instance').clearFilter(); + }); + + await expect(pager).toBeVisible(); + const pagerInfo = page.locator('.dx-info'); + await expect(pagerInfo).toHaveText('Page 1 of 10 (20 items)'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/cardView/search/a11y.functional.spec.ts b/e2e/testcafe-devextreme/playwright-tests/cardView/search/a11y.functional.spec.ts new file mode 100644 index 000000000000..aa924de22ec1 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/cardView/search/a11y.functional.spec.ts @@ -0,0 +1,27 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('CardView - Search.A11y.Functional', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('Search field should have aria-label attribute', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: [{ id: 1, title: 'Mr.', name: 'John', lastName: 'Heart' }], + columns: [{ dataField: 'id' }, { dataField: 'title' }, { dataField: 'name' }, { dataField: 'lastName' }], + searchPanel: { visible: true }, + }); + + const ariaLabel = await page.locator('.dx-cardview-search .dx-texteditor-input').getAttribute('aria-label'); + expect(ariaLabel).toBe('Search in the card view'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/cardView/search/api.functional.spec.ts b/e2e/testcafe-devextreme/playwright-tests/cardView/search/api.functional.spec.ts new file mode 100644 index 000000000000..35667de164af --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/cardView/search/api.functional.spec.ts @@ -0,0 +1,53 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('CardView - SearchPanel API', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('searchPanel.visible API', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: [{ id: 1, title: 'Mr.', name: 'John', lastName: 'Heart' }], + columns: [{ dataField: 'id' }, { dataField: 'title' }, { dataField: 'name' }, { dataField: 'lastName' }], + searchPanel: { visible: true }, + }); + + const searchBox = page.locator('.dx-cardview-search'); + await expect(searchBox).toBeVisible(); + + await page.evaluate(() => { + ($('#container') as any).dxCardView('instance').option('searchPanel.visible', false); + }); + await expect(searchBox).not.toBeVisible(); + + await page.evaluate(() => { + ($('#container') as any).dxCardView('instance').option('searchPanel.visible', true); + }); + await expect(searchBox).toBeVisible(); + }); + + test('searchPanel.text API', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: [{ id: 1, title: 'Mr.', name: 'John', lastName: 'Heart' }], + columns: [{ dataField: 'id' }, { dataField: 'title' }, { dataField: 'name' }, { dataField: 'lastName' }], + searchPanel: { visible: true, text: 'rt' }, + }); + + const input = page.locator('.dx-cardview-search .dx-texteditor-input'); + await expect(input).toHaveValue('rt'); + + await page.evaluate(() => { + ($('#container') as any).dxCardView('instance').option('searchPanel.text', ''); + }); + await expect(input).toHaveValue(''); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/cardView/search/behavior.functional.spec.ts b/e2e/testcafe-devextreme/playwright-tests/cardView/search/behavior.functional.spec.ts new file mode 100644 index 000000000000..a9ff25093657 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/cardView/search/behavior.functional.spec.ts @@ -0,0 +1,39 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('CardView - SearchPanel Behavior', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('Search panel should filter cards', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: [ + { id: 1, title: 'Mr.', name: 'John', lastName: 'Heart' }, + { id: 2, title: 'Mrs.', name: 'Olivia', lastName: 'Peyton' }, + { id: 3, title: 'Mr.', name: 'Robert', lastName: 'Reagan' }, + { id: 4, title: 'Mr.', name: 'Greta', lastName: 'Sims' }, + ], + columns: [{ dataField: 'id' }, { dataField: 'title' }, { dataField: 'name' }, { dataField: 'lastName' }], + searchPanel: { visible: true }, + }); + + const cards = page.locator('.dx-cardview-card'); + await expect(cards).toHaveCount(4); + + const input = page.locator('.dx-cardview-search .dx-texteditor-input'); + await input.fill('rt'); + await expect(cards).toHaveCount(2); + + await input.fill(''); + await expect(cards).toHaveCount(4); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/cardView/search/visual.spec.ts b/e2e/testcafe-devextreme/playwright-tests/cardView/search/visual.spec.ts new file mode 100644 index 000000000000..b95415426058 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/cardView/search/visual.spec.ts @@ -0,0 +1,32 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('Search.Visual', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('highlighted search text', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: [ + { id: 1, firstName: 'Darin', lastName: 'Heritege', email: 'dheritege0@jugem.jp', gender: 'Male' }, + { id: 2, firstName: 'Aeriel', lastName: 'Giggs', email: 'agiggs1@hubpages.com', gender: 'Female' }, + ], + columns: ['id', 'firstName', 'lastName', 'email', 'gender'], + searchPanel: { visible: true, text: 'da' }, + height: 600, + }); + + await testScreenshot(page, 'card-view_search_text-highlighting.png', { + element: page.locator('#container'), + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/cardView/security.functional.spec.ts b/e2e/testcafe-devextreme/playwright-tests/cardView/security.functional.spec.ts new file mode 100644 index 000000000000..513ca5aadd80 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/cardView/security.functional.spec.ts @@ -0,0 +1,25 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, setupTestPage, getContainerUrl } from '../../playwright-helpers'; + +const containerUrl = getContainerUrl(__dirname, '../../tests/container.html'); + +const UNSAFE_TEXT = ''; + +test.describe('CardView - Security', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + test('Script inside cell text should not be executed after opening header filter', async ({ page }) => { + await createWidget(page, 'dxCardView', { + columns: ['caption'], + headerFilter: { visible: true }, + dataSource: [{ id: 1, caption: UNSAFE_TEXT }], + }); + + await page.locator('.dx-cardview-headers .dx-header-filter').first().click(); + + const itemText = await page.locator('.dx-list-item').first().textContent(); + expect(itemText).toBe(UNSAFE_TEXT); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/cardView/selection/functional.spec.ts b/e2e/testcafe-devextreme/playwright-tests/cardView/selection/functional.spec.ts new file mode 100644 index 000000000000..e752532ccda3 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/cardView/selection/functional.spec.ts @@ -0,0 +1,96 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +const selectionData = [ + { id: 0, title: 'header1', A: 'A_0', B: 'B_0', C: 'C_0' }, + { id: 1, title: 'header2', A: 'A_1', B: 'B_1', C: 'C_1' }, + { id: 2, title: 'header3', A: 'A_2', B: 'B_2', C: 'C_2' }, + { id: 3, title: 'header4', A: 'A_3', B: 'B_3', C: 'C_3' }, + { id: 4, title: 'header5', A: 'A_4', B: 'B_4', C: 'C_4' }, +]; + +test.describe('Selection.Functional', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('Single mode: select a first card -> select a second card -> deselect a second card', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: selectionData, + cardHeader: { captionExpr: () => 'title' }, + columns: ['A', 'B', 'C'], + keyExpr: 'id', + height: 700, + selection: { mode: 'single' }, + }); + + const firstCard = page.locator('.dx-cardview-card').nth(0); + const secondCard = page.locator('.dx-cardview-card').nth(1); + + await firstCard.click(); + await expect(firstCard).toHaveClass(/dx-selection/); + + await secondCard.click(); + await expect(firstCard).not.toHaveClass(/dx-selection/); + await expect(secondCard).toHaveClass(/dx-selection/); + + await secondCard.click({ modifiers: ['Control'] }); + await expect(secondCard).not.toHaveClass(/dx-selection/); + }); + + test("Multiple mode with showCheckBoxesMode='always': select cards with checkboxes", async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: selectionData, + cardHeader: { captionExpr: () => 'title' }, + columns: ['A', 'B', 'C'], + keyExpr: 'id', + height: 700, + selection: { mode: 'multiple', showCheckBoxesMode: 'always', allowSelectAll: true }, + }); + + const firstCheckbox = page.locator('.dx-cardview-card').nth(0).locator('.dx-checkbox'); + const secondCheckbox = page.locator('.dx-cardview-card').nth(1).locator('.dx-checkbox'); + const firstCard = page.locator('.dx-cardview-card').nth(0); + const secondCard = page.locator('.dx-cardview-card').nth(1); + + await firstCheckbox.click(); + await expect(firstCard).toHaveClass(/dx-selection/); + + await secondCheckbox.click(); + await expect(firstCard).toHaveClass(/dx-selection/); + await expect(secondCard).toHaveClass(/dx-selection/); + + await firstCheckbox.click(); + await expect(firstCard).not.toHaveClass(/dx-selection/); + await expect(secondCard).toHaveClass(/dx-selection/); + + await secondCheckbox.click(); + await expect(secondCard).not.toHaveClass(/dx-selection/); + }); + + test('Select all when selectAllMode = allPages', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: selectionData, + cardHeader: { captionExpr: () => 'title' }, + columns: ['A', 'B', 'C'], + keyExpr: 'id', + height: 700, + selection: { mode: 'multiple', showCheckBoxesMode: 'always', allowSelectAll: true, selectAllMode: 'allPages' }, + }); + + await page.locator('.dx-cardview-select-all-button').click(); + + const selectedKeys = await page.evaluate(() => { + return ($('#container') as any).dxCardView('instance').getSelectedCardKeys(); + }); + expect(selectedKeys).toEqual([0, 1, 2, 3, 4]); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/cardView/selection/visual.spec.ts b/e2e/testcafe-devextreme/playwright-tests/cardView/selection/visual.spec.ts new file mode 100644 index 000000000000..5d172d2b3c62 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/cardView/selection/visual.spec.ts @@ -0,0 +1,70 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +const selectionData = [ + { id: 0, title: 'header1', A: 'A_0', B: 'B_0', C: 'C_0' }, + { id: 1, title: 'header2', A: 'A_1', B: 'B_1', C: 'C_1' }, + { id: 2, title: 'header3', A: 'A_2', B: 'B_2', C: 'C_2' }, + { id: 3, title: 'header4', A: 'A_3', B: 'B_3', C: 'C_3' }, + { id: 4, title: 'header5', A: 'A_4', B: 'B_4', C: 'C_4' }, +]; + +test.describe('Selection.Visual', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('Single mode', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: selectionData, + cardHeader: { captionExpr: () => 'title' }, + columns: ['A', 'B', 'C'], + keyExpr: 'id', + height: 700, + selectedCardKeys: [0], + selection: { mode: 'single' }, + }); + + await testScreenshot(page, 'card-view_single_selection.png', { + element: page.locator('#container'), + }); + }); + + test("Multiple mode with showCheckBoxesMode = 'always'", async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: selectionData, + cardHeader: { captionExpr: () => 'title' }, + columns: ['A', 'B', 'C'], + keyExpr: 'id', + height: 700, + selection: { mode: 'multiple', showCheckBoxesMode: 'always', allowSelectAll: true }, + }); + + await testScreenshot(page, 'card-view_miltiple_selection_with_showCheckBoxesMode_=_always.png', { + element: page.locator('#container'), + }); + }); + + test('Multiple mode without Select All/Deselect All', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: selectionData, + cardHeader: { captionExpr: () => 'title' }, + columns: ['A', 'B', 'C'], + keyExpr: 'id', + height: 700, + selection: { mode: 'multiple', allowSelectAll: false }, + }); + + await testScreenshot(page, 'card-view_miltiple_selection_without_select-all.png', { + element: page.locator('#container'), + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/cardView/sorting/api.themes.spec.ts b/e2e/testcafe-devextreme/playwright-tests/cardView/sorting/api.themes.spec.ts new file mode 100644 index 000000000000..926fe55a0e20 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/cardView/sorting/api.themes.spec.ts @@ -0,0 +1,57 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +const data = [ + { id: 1, title: 'Mr.', name: 'John', lastName: 'Heart' }, + { id: 2, title: 'Mrs.', name: 'Olivia', lastName: 'Peyton' }, + { id: 3, title: 'Mr.', name: 'Robert', lastName: 'Reagan' }, + { id: 4, title: 'Mr.', name: 'Greta', lastName: 'Sims' }, +]; + +test.describe('CardView - Sorting API Themes', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('Sort index API', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: data, + height: 500, + columns: [ + { dataField: 'id' }, + { dataField: 'title', sortOrder: 'desc', sortIndex: 1 }, + { dataField: 'name', sortOrder: 'asc', sortIndex: 0 }, + { dataField: 'lastName' }, + ], + }); + + await testScreenshot(page, 'cardview_sort_index_api.png', { + element: page.locator('#container'), + }); + }); + + test('Default render', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: data, + height: 500, + columns: [ + { dataField: 'id' }, + { dataField: 'title', sortOrder: 'desc' }, + { dataField: 'name' }, + { dataField: 'lastName' }, + ], + }); + + await testScreenshot(page, 'cardview_headers_default_render.png', { + element: page.locator('#container'), + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/cardView/sorting/behavior.functional.spec.ts b/e2e/testcafe-devextreme/playwright-tests/cardView/sorting/behavior.functional.spec.ts new file mode 100644 index 000000000000..5cd401adf89d --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/cardView/sorting/behavior.functional.spec.ts @@ -0,0 +1,79 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +const data = [ + { id: 1, title: 'Mr.', name: 'John', lastName: 'Heart' }, + { id: 2, title: 'Mrs.', name: 'Olivia', lastName: 'Peyton' }, + { id: 3, title: 'Mr.', name: 'Robert', lastName: 'Reagan' }, + { id: 4, title: 'Mr.', name: 'Greta', lastName: 'Sims' }, +]; + +test.describe('CardView - Sorting Behavior - Functional', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('Change sorting by header click in single mode', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: data, + sorting: { mode: 'single' }, + columns: [{ dataField: 'title' }, { dataField: 'name' }], + }); + + const titleHeader = page.locator('.dx-cardview-headers .dx-cardview-header-item').first(); + await titleHeader.click(); + + const sortOrder = await page.evaluate(() => { + return ($('#container') as any).dxCardView('instance').columnOption('title', 'sortOrder'); + }); + expect(sortOrder).toBe('asc'); + }); + + test('Sorting should work with computed columns', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: [{ id: 0 }, { id: 1 }, { id: 2 }, { id: 3 }], + keyExpr: 'id', + columns: [{ + caption: 'Computed', + allowSorting: true, + calculateFieldValue: ({ id }) => `str_${id}`, + }], + }); + + const headerItem = page.locator('.dx-cardview-headers .dx-cardview-header-item').first(); + await headerItem.click(); + + const firstValue = await page.locator('.dx-cardview-card').first().locator('.dx-cardview-field-value').textContent(); + expect(firstValue).toBe('str_0'); + + await headerItem.click(); + + const newFirstValue = await page.locator('.dx-cardview-card').first().locator('.dx-cardview-field-value').textContent(); + expect(newFirstValue).toBe('str_3'); + }); + + test('Change sorting via context menu', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: data, + sorting: { mode: 'single' }, + columns: [{ dataField: 'title' }, { dataField: 'name' }], + }); + + const titleHeader = page.locator('.dx-cardview-headers .dx-cardview-header-item').first(); + await titleHeader.click({ button: 'right' }); + await page.locator('.dx-context-menu .dx-menu-item').nth(0).click(); + + const sortOrder = await page.evaluate(() => { + return ($('#container') as any).dxCardView('instance').columnOption('title', 'sortOrder'); + }); + expect(sortOrder).toBe('asc'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/cardView/sorting/behavior.themes.spec.ts b/e2e/testcafe-devextreme/playwright-tests/cardView/sorting/behavior.themes.spec.ts new file mode 100644 index 000000000000..08f994935d3d --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/cardView/sorting/behavior.themes.spec.ts @@ -0,0 +1,57 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +const data = [ + { id: 1, title: 'Mr.', name: 'John', lastName: 'Heart' }, + { id: 2, title: 'Mrs.', name: 'Olivia', lastName: 'Peyton' }, + { id: 3, title: 'Mr.', name: 'Robert', lastName: 'Reagan' }, + { id: 4, title: 'Mr.', name: 'Greta', lastName: 'Sims' }, +]; + +test.describe('CardView - Sorting Behavior - Themes', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('Default render', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: data, + height: 500, + columns: [ + { dataField: 'id' }, + { dataField: 'title', sortOrder: 'desc' }, + { dataField: 'name' }, + { dataField: 'lastName' }, + ], + }); + + await testScreenshot(page, 'cardview_headers_default_render.png', { + element: page.locator('#container'), + }); + }); + + test('Default multiple sorting render', async ({ page }) => { + await createWidget(page, 'dxCardView', { + dataSource: data, + height: 500, + columns: [ + { dataField: 'id' }, + { dataField: 'title', sortOrder: 'desc' }, + { dataField: 'name', sortOrder: 'asc' }, + { dataField: 'lastName' }, + ], + }); + + await testScreenshot(page, 'cardview_headers_with_multiple_sorting_render.png', { + element: page.locator('#container'), + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/common/draggable.spec.ts b/e2e/testcafe-devextreme/playwright-tests/common/draggable.spec.ts new file mode 100644 index 000000000000..3ec1bcdbec42 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/common/draggable.spec.ts @@ -0,0 +1,94 @@ +import { test, expect } from '@playwright/test'; +import { createWidget } from '../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../tests/container.html')}`; + +test.describe('Draggable', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + const init = async () => page.evaluate(() => { + $('
', { + id: 'scrollview', + width: '400px', + height: '400px', + }) + .css({ + position: 'absolute', + top: 0, + padding: '20px', + background: '#f18787', + }) + .appendTo('#container'); + + $('
', { + id: 'scrollview-content', + height: '500px', + width: '500px', + }).appendTo('#scrollview'); + + $('
', { + id: 'drag-me', + }) + .css({ + 'background-color': 'blue', + display: 'inline-block', + }) + .appendTo('#scrollview-content'); + $('#drag-me').append('DRAG ME!!!'); + }); + + test('dxDraggable element should not loose its position on dragging with auto-scroll inside ScrollView (T1169590)', async ({ page }) => { + + await init(); + await createWidget(page, 'dxScrollView', { + direction: 'both', + }, '#scrollview'); + await createWidget(page, 'dxDraggable', { }, '#drag-me'); + + const draggable = page.locator('#drag-me'); + const scrollable = page.locator('#scrollview'); + + await (async () => { + const box = await draggable.boundingBox(); + if (box) { + await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2); + await page.mouse.down(); + await page.mouse.move(box.x + box.width / 2 + 0, box.y + box.height / 2 + 400, { steps: 10 }); + await page.mouse.up(); + } + })() + + .expect(scrollable.getContainer()().scrollTop) + .gt(60); + + await page.expect((await draggable().boundingClientRect).top) + .gt(400); + + await draggable.scrollIntoViewIfNeeded(); + + await (async () => { + const box = await draggable.boundingBox(); + if (box) { + await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2); + await page.mouse.down(); + await page.mouse.move(box.x + box.width / 2 + 400, box.y + box.height / 2 + 0, { steps: 10 }); + await page.mouse.up(); + } + })() + + .expect(scrollable.getContainer()().scrollLeft) + .gt(60); + + await page.expect((await draggable().boundingClientRect).left) + .gt(400); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/common/eventsEngine.spec.ts b/e2e/testcafe-devextreme/playwright-tests/common/eventsEngine.spec.ts new file mode 100644 index 000000000000..a156cca6dde4 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/common/eventsEngine.spec.ts @@ -0,0 +1,87 @@ +import { test, expect } from '@playwright/test'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../tests/container.html')}`; + +test.describe('Events', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + const init = async () => page.evaluate(() => { + const markup = `
+
+
+
hoverStartTriggerCount
+
0
+
hoverEndTriggerCount
+
0
+
`; + + $('#container').html(markup); + + const { DevExpress } = (window as any); + + let hoverStartTriggerCount = 0; + let hoverEndTriggerCount = 0; + + DevExpress.events.on($('#target'), 'dxhoverstart', () => { + hoverStartTriggerCount += 1; + + $('#hoverStartTriggerCount').text(hoverStartTriggerCount); + }); + + DevExpress.events.on($('#target'), 'dxhoverend', () => { + hoverEndTriggerCount += 1; + + $('#hoverEndTriggerCount').text(hoverEndTriggerCount); + }); + }); + + test('The `dxhoverstart` event should be triggered after dragging and dropping an HTML draggable element (T1260277)', async ({ page }) => { + + await init(); + + const draggable = page.locator('#draggable'); + const target = page.locator('#target'); + const hoverStartTriggerCount = page.locator('#hoverStartTriggerCount'); + const hoverEndTriggerCount = page.locator('#hoverEndTriggerCount'); + + await (async () => { + const box = await draggable.boundingBox(); + if (box) { + await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2); + await page.mouse.down(); + await page.mouse.move(box.x + box.width / 2 + 0, box.y + box.height / 2 + 400, { steps: 10 }); + await page.mouse.up(); + } + })(); + + // `.drag` does not trigger the `pointercancel` event. + // A sequence of `.drag` calls behaves like a single drag&drop operation, + // and each call does not trigger the `pointerup` event. + // Even if it did, the `pointercancel` event would not be triggered as specified in: + // https://www.w3.org/TR/pointerevents/#suppressing-a-pointer-event-stream + // This is a hack to test the event engine's logic. + await draggable.dispatchEvent('pointercancel'); + + await (async () => { + const box = await target.boundingBox(); + if (box) { + await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2); + await page.mouse.down(); + await page.mouse.move(box.x + box.width / 2 + 0, box.y + box.height / 2 + 400, { steps: 10 }); + await page.mouse.up(); + } + })(); + + expect(hoverStartTriggerCount.textContent).toBe('1'); + expect(hoverEndTriggerCount.textContent).toBe('1'); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/common/filterBuilder/filterBuilderEditor.spec.ts b/e2e/testcafe-devextreme/playwright-tests/common/filterBuilder/filterBuilderEditor.spec.ts new file mode 100644 index 000000000000..439e4a821be8 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/common/filterBuilder/filterBuilderEditor.spec.ts @@ -0,0 +1,54 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('Editing events', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + // T1310528 + test('Change value editor to checkbox', async ({ page }) => { + + await createWidget(page, 'dxFilterBuilder', { + fields, + value: filter, + allowHierarchicalFields: true, + onEditorPreparing: (data) => { + data.editorName = 'dxCheckBox'; + }, + }); + + const filterBuilder = page.locator('#container'); + await filterBuilder.getField(0, 'itemValue').element.click(); + + await testScreenshot(page, 'value-editor-checkbox.png', { element: filterBuilder.element }); + + }); + + // T1310528 + test('Change value editor to switch', async ({ page }) => { + + await createWidget(page, 'dxFilterBuilder', { + fields, + value: filter, + allowHierarchicalFields: true, + onEditorPreparing: (data) => { + data.editorName = 'dxSwitch'; + }, + }); + + const filterBuilder = page.locator('#container'); + await filterBuilder.getField(0, 'itemValue').element.click(); + + await testScreenshot(page, 'value-editor-switch.png', { element: filterBuilder.element }); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/common/filterBuilder/filterBuilderNaming.spec.ts b/e2e/testcafe-devextreme/playwright-tests/common/filterBuilder/filterBuilderNaming.spec.ts new file mode 100644 index 000000000000..450369a0d49e --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/common/filterBuilder/filterBuilderNaming.spec.ts @@ -0,0 +1,64 @@ +import { test, expect } from '@playwright/test'; +import { createWidget } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('FilterBuilder - Field naming', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + // T1253754 + test('FilterBuilder - First field uses the dataField property while subsequent fields use the name property in the filter value', async ({ page }) => { + + await createWidget(page, 'dxFilterBuilder', { + value: [ + ['dataField1', '<>', 0], + ], + fields: [ + { dataField: 'dataField1', name: 'name1' }, + { dataField: 'dataField2', name: 'name2' }, + ], + }); + + + const filterBuilder = page.locator('#container'); + + const expectedValues = [ + [ + ['name1', '<>', 0], + 'and', + ['name1', 'contains', 'A'], + ], + [ + ['name1', '<>', 0], + 'and', + ['name2', 'contains', 'A'], + ], + ]; + await page.click(filterBuilder.getAddButton()) + .expect(FilterBuilder.getPopupTreeView().visible).ok() + .click(FilterBuilder.getPopupTreeViewNode(0)) + .click(filterBuilder.getField(1, 'itemValue').element) + .pressKey('A enter'); + + await page.expect(await filterBuilder.option('value')) + .eql(expectedValues[0]); + + await filterBuilder.getField(1, 'item').element.click() + .expect(FilterBuilder.getPopupTreeView().visible).ok() + .click(FilterBuilder.getPopupTreeViewNode(1)) + .click(filterBuilder.getField(1, 'itemValue').element) + .pressKey('A enter'); + + await page.expect(await filterBuilder.option('value')) + .eql(expectedValues[1]); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/common/filterBuilder/filterBuilderScrolling.spec.ts b/e2e/testcafe-devextreme/playwright-tests/common/filterBuilder/filterBuilderScrolling.spec.ts new file mode 100644 index 000000000000..24841b085f0f --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/common/filterBuilder/filterBuilderScrolling.spec.ts @@ -0,0 +1,37 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, insertStylesheetRulesToPage } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('Filter Builder Scrolling Test', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + // T1273328 > T1294239 + test('FilterBuilder - The field drop-down closes with the page scroll', async ({ page }) => { + + await insertStylesheetRulesToPage(page, '#container {height: 150px; overflow: scroll;}'); + + await createWidget(page, 'dxFilterBuilder', { + fields, + value: filter, + }); + + const filterBuilder = page.locator('#container'); + + await filterBuilder.isReady(); + + await page.click(filterBuilder.getItem('operation')) + .scrollIntoView(filterBuilder.getItem('operation', 4)); + + await expect(FilterBuilder.getPopupTreeView().exists).notOk(); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/common/filterBuilder/index.spec.ts b/e2e/testcafe-devextreme/playwright-tests/common/filterBuilder/index.spec.ts new file mode 100644 index 000000000000..8cf1a2fa4c62 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/common/filterBuilder/index.spec.ts @@ -0,0 +1,96 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('Editing events', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('Field dropdown popup', async ({ page }) => { + + await createWidget(page, 'dxFilterBuilder', { + fields, + value: filter, + allowHierarchicalFields: true, + }); + + const filterBuilder = page.locator('#container'); + await filterBuilder.getField(0, 'item').element.click(); + + await testScreenshot(page, 'field-dropdown.png', { element: filterBuilder.element }); + + }); + + test('operation dropdown popup', async ({ page }) => { + + await createWidget(page, 'dxFilterBuilder', { + fields, + value: filter, + allowHierarchicalFields: true, + }); + + const filterBuilder = page.locator('#container'); + await filterBuilder.getField(0, 'itemOperation').element.click(); + + await testScreenshot(page, 'operation-dropdown.png', { element: filterBuilder.element }); + + }); + + // T1222027 + test('Dropdown Treeview should have no empty space', async ({ page }) => { + + await createWidget(page, 'dxFilterBuilder', { + fields, + value: filter, + allowHierarchicalFields: true, + }); + + const filterBuilder = page.locator('#container'); + await filterBuilder.getField(0, 'itemAction').element.click(); + + await testScreenshot(page, 'dropdown-space.png', { element: filterBuilder.element }); + + }); + + [ + { dataType: 'date', value: 1740441600000 }, + { dataType: 'date', value: '2025-02-25T00:00:00.000Z' }, + { dataType: 'date', value: new Date('2025-02-25T00:00:00.000Z') }, + { dataType: 'datetime', value: 1740441600000 }, + { dataType: 'datetime', value: '2025-02-25T00:00:00.000Z' }, + { dataType: 'datetime', value: new Date('2025-02-25T00:00:00.000Z') }, + ].forEach(({ dataType, value }) => { + test(`item value text should be correct for dataType: ${dataType} and valueType: ${typeof value}`, async ({ page }) => { + + await createWidget(page, 'dxFilterBuilder', { + fields: [ + { + dataField: 'field1', + dataType: dataType as DataType, + }, + ], + value: ['field1', '=', value], + }); + + + const filterBuilder = page.locator('#container'); + + const date = new Date(value); + const dateString = date.toLocaleDateString(); + const timeString = date.toLocaleTimeString('en-US', { hour: 'numeric', hour12: true, minute: '2-digit' }); + + const expectedValue = dataType === 'date' ? dateString : `${dateString}, ${timeString}`; + + await expect(filterBuilder.getField(0).getValueText().textContent).eql(expectedValue); + + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/common/gantt/common.spec.ts b/e2e/testcafe-devextreme/playwright-tests/common/gantt/common.spec.ts new file mode 100644 index 000000000000..e682a90300e8 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/common/gantt/common.spec.ts @@ -0,0 +1,158 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, appendElementTo } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container-extended.html')}`; + +test.describe('Gantt', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + const TOOLBAR_ITEM_BUTTON = '.dx-button'; + + const data = { + tasks: [{ + id: 1, + parentId: 0, + title: 'Software Development', + start: new Date('2019-02-21T05:00:00.000Z'), + end: new Date('2019-07-04T12:00:00.000Z'), + progress: 31, + color: 'red', + }, { + id: 2, + parentId: 1, + title: 'Scope', + start: new Date('2019-02-21T05:00:00.000Z'), + end: new Date('2019-02-26T09:00:00.000Z'), + progress: 60, + }, { + id: 3, + parentId: 2, + title: 'Determine project scope', + start: new Date('2019-02-21T05:00:00.000Z'), + end: new Date('2019-02-21T09:00:00.000Z'), + progress: 100, + }, { + id: 4, + parentId: 2, + title: 'Secure project sponsorship', + start: new Date('2019-02-21T10:00:00.000Z'), + end: new Date('2019-02-22T09:00:00.000Z'), + progress: 100, + }, { + id: 5, + parentId: 2, + title: 'Define preliminary resources', + start: new Date('2019-02-22T10:00:00.000Z'), + end: new Date('2019-02-25T09:00:00.000Z'), + progress: 60, + }, { + id: 6, + parentId: 2, + title: 'Secure core resources', + start: new Date('2019-02-25T10:00:00.000Z'), + end: new Date('2019-02-26T09:00:00.000Z'), + progress: 0, + }, { + id: 7, + parentId: 2, + title: 'Scope complete', + start: new Date('2019-02-26T09:00:00.000Z'), + end: new Date('2019-02-26T09:00:00.000Z'), + progress: 0, + }], + + dependencies: [{ + id: 0, + predecessorId: 1, + successorId: 2, + type: 0, + }, { + id: 1, + predecessorId: 2, + successorId: 3, + type: 0, + }, { + id: 2, + predecessorId: 3, + successorId: 4, + type: 0, + }, { + id: 3, + predecessorId: 4, + successorId: 5, + type: 0, + }, { + id: 4, + predecessorId: 5, + successorId: 6, + type: 0, + }, { + id: 5, + predecessorId: 6, + successorId: 7, + type: 0, + }], + + resources: [{ + id: 1, text: 'Management', + }, { + id: 2, text: 'Project Manager', + }, { + id: 3, text: 'Deployment Team', + }], + + resourceAssignments: [{ + id: 0, taskId: 3, resourceId: 1, + }, { + id: 1, taskId: 4, resourceId: 1, + }, { + id: 2, taskId: 5, resourceId: 2, + }, { + id: 3, taskId: 6, resourceId: 2, + }, { + id: 4, taskId: 6, resourceId: 3, + }], + }; + + test('Gantt - show resources button should not have focus state (T1264485)', async ({ page }) => { + + const id = `${new Guid()}`; + await appendElementTo(page, '#container', 'div', id, {}); + await createWidget(page, 'dxGantt', { + tasks: { dataSource: data.tasks }, + toolbar: { items: ['showResources'] }, + dependencies: { dataSource: data.dependencies }, + resources: { dataSource: data.resources }, + resourceAssignments: { dataSource: data.resourceAssignments }, + }, `#${id}`); + + await page.click(Selector(TOOLBAR_ITEM_BUTTON)); + await testScreenshot(page, 'Gantt show resourced.png', { element: '#container' }); + + }); + + test('Gantt - show dependencies button should not have focus state (T1264485)', async ({ page }) => { + + const id = `${new Guid()}`; + await appendElementTo(page, '#container', 'div', id, {}); + await createWidget(page, 'dxGantt', { + tasks: { dataSource: data.tasks }, + toolbar: { items: ['showDependencies'] }, + dependencies: { dataSource: data.dependencies }, + resources: { dataSource: data.resources }, + resourceAssignments: { dataSource: data.resourceAssignments }, + }, `#${id}`); + + await page.click(Selector(TOOLBAR_ITEM_BUTTON)); + await testScreenshot(page, 'Gantt show dependencies.png', { element: '#container' }); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/common/icons.spec.ts b/e2e/testcafe-devextreme/playwright-tests/common/icons.spec.ts new file mode 100644 index 000000000000..e73fb573f188 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/common/icons.spec.ts @@ -0,0 +1,428 @@ +import { test, expect } from '@playwright/test'; +import { testScreenshot, appendElementTo } from '../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../tests/container.html')}`; + +test.describe('Icons', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + const ICON_CLASS = 'dx-icon'; + const iconSet = { + add: '\f00b', + airplane: '\f000', + bookmark: '\f017', + box: '\f018', + car: '\f01b', + card: '\f019', + cart: '\f01a', + chart: '\f01c', + check: '\f005', + clear: '\f008', + clock: '\f01d', + close: '\f00a', + coffee: '\f02a', + comment: '\f01e', + doc: '\f021', + file: '\f021', + download: '\f022', + dragvertical: '\f038', + edit: '\f023', + email: '\f024', + event: '\f026', + eventall: '\f043', + favorites: '\f025', + find: '\f027', + filter: '\f050', + folder: '\f028', + activefolder: '\f028', + food: '\f029', + gift: '\f02b', + globe: '\f02c', + group: '\f02e', + help: '\f02f', + home: '\f030', + image: '\f031', + info: '\f032', + key: '\f033', + like: '\f034', + map: '\f035', + menu: '\f00c', + message: '\f024', + money: '\f036', + music: '\f037', + overflow: '\f00d', + percent: '\f039', + photo: '\f03a', + plus: '\f00b', + minus: '\f074', + preferences: '\f03b', + product: '\f03c', + pulldown: '\f062', + refresh: '\f03d', + remove: '\f00a', + revert: '\f04c', + runner: '\f040', + save: '\f041', + search: '\f027', + tags: '\f009', + tel: '\f003', + tips: '\f004', + todo: '\f005', + toolbox: '\f007', + trash: '\f03e', + user: '\f02d', + upload: '\f006', + floppy: '\f073', + arrowleft: '\f011', + arrowdown: '\f015', + arrowright: '\f00e', + arrowup: '\f013', + spinleft: '\f04f', + spinprev: '\f04f', + spinright: '\f04e', + spinnext: '\f04e', + spindown: '\f001', + spinup: '\f002', + chevronleft: '\f012', + chevronprev: '\f012', + back: '\f012', + chevronright: '\f010', + chevronnext: '\f010', + chevrondown: '\f016', + chevronup: '\f014', + chevrondoubleleft: '\f042', + chevrondoubleright: '\f03f', + equal: '\f044', + notequal: '\f045', + less: '\f046', + greater: '\f047', + lessorequal: '\f048', + greaterorequal: '\f049', + isblank: '\f075', + isnotblank: '\f076', + sortup: '\f051', + sortdown: '\f052', + sortuptext: '\f053', + sortdowntext: '\f054', + sorted: '\f055', + expand: '\f04a', + collapse: '\f04b', + columnfield: '\f057', + rowfield: '\f058', + datafield: '\f101', + fields: '\f059', + fieldchooser: '\f05a', + columnchooser: '\f04d', + pin: '\f05b', + unpin: '\f05c', + pinleft: '\f05d', + pinright: '\f05e', + contains: '\f063', + startswith: '\f064', + endswith: '\f065', + doesnotcontain: '\f066', + range: '\f06a', + export: '\f05f', + exportxlsx: '\f060', + exportpdf: '\f061', + exportselected: '\f06d', + ordersbox: '\f06e', + warning: '\f06b', + taskhelpneeded: '\f06f', + more: '\f06c', + square: '\f067', + clearsquare: '\f068', + repeat: '\f069', + selectall: '\f070', + unselectall: '\f071', + print: '\f072', + bold: '\f077', + italic: '\f078', + underline: '\f079', + strike: '\f07a', + indent: '\f07b', + increaselinespacing: '\f07b', + font: '\f11b', + fontsize: '\f07c', + shrinkfont: '\f07d', + growfont: '\f07e', + color: '\f07f', + background: '\f080', + fill: '\f10d', + palette: '\f120', + superscript: '\f081', + subscript: '\f082', + header: '\f083', + blockquote: '\f084', + formula: '\f056', + codeblock: '\f085', + orderedlist: '\f086', + bulletlist: '\f087', + increaseindent: '\f088', + decreaseindent: '\f089', + decreaselinespacing: '\f106', + alignleft: '\f08a', + alignright: '\f08b', + aligncenter: '\f08c', + alignjustify: '\f08d', + link: '\f08e', + video: '\f08f', + mention: '\f090', + variable: '\f091', + clearformat: '\f092', + accountbox: '\f094', + fullscreen: '\f11a', + hierarchy: '\f124', + docfile: '\f111', + docxfile: '\f110', + pdffile: '\f118', + pptfile: '\f114', + pptxfile: '\f115', + rtffile: '\f112', + txtfile: '\f113', + xlsfile: '\f116', + xlsxfile: '\f117', + copy: '\f107', + cut: '\f10a', + paste: '\f108', + share: '\f11f', + inactivefolder: '\f105', + newfolder: '\f123', + movetofolder: '\f121', + parentfolder: '\f122', + rename: '\f109', + detailslayout: '\f10b', + contentlayout: '\f11e', + smalliconslayout: '\f119', + mediumiconslayout: '\f10c', + undo: '\f04c', + redo: '\f093', + hidepanel: '\f11c', + showpanel: '\f11d', + checklist: '\f141', + verticalaligntop: '\f14f', + verticalaligncenter: '\f14e', + verticalalignbottom: '\f14d', + rowproperties: '\f14c', + columnproperties: '\f14b', + cellproperties: '\f14a', + tableproperties: '\f140', + splitcells: '\f139', + mergecells: '\f138', + deleterow: '\f137', + deletecolumn: '\f136', + insertrowabove: '\f135', + insertrowbelow: '\f134', + insertcolumnleft: '\f133', + insertcolumnright: '\f132', + inserttable: '\f130', + deletetable: '\f131', + edittableheader: '\f142', + addtableheader: '\f143', + pasteplaintext: '\f144', + importselected: '\f145', + import: '\f146', + textdocument: '\f147', + jpgfile: '\f148', + bmpfile: '\f149', + svgfile: '\f150', + attach: '\f151', + return: '\f152', + indeterminatestate: '\f153', + lock: '\f154', + unlock: '\f155', + imgarlock: '\f156', + imgarunlock: '\f157', + bell: '\f158', + sun: '\f159', + arrowback: '\f15a', + taskcomplete: '\f15b', + taskrejected: '\f15c', + taskinprogress: '\f15d', + taskstop: '\f15e', + clearcircle: '\f15f', + send: '\f160', + pinmap: '\f179', + photooutline: '\f162', + panelright: '\f163', + panelleft: '\f164', + optionsgear: '\f165', + moon: '\f166', + login: '\f167', + eyeopen: '\f168', + eyeclose: '\f169', + expandform: '\f170', + description: '\f171', + belloutline: '\f172', + to: '\f173', + errorcircle: '\f174', + datatrending: '\f175', + dataarea: '\f176', + datausage: '\f177', + datapie: '\f178', + handlevertical: '\f161', + handlehorizontal: '\f16a', + triangleup: '\f16b', + triangledown: '\f16c', + triangleright: '\f16d', + triangleleft: '\f16e', + sendfilled: '\f09a', + chat: '\f17e', + fixcolumn: '\f16f', + unfixcolumn: '\f17a', + fixcolumnleft: '\f17b', + stickcolumn: '\f17c', + fixcolumnright: '\f17d', + ratingoutline: '\f17f', + ratingfilled: '\f180', + csv: '\f181', + packagebox: '\f182', + checkmarkcircle: '\f183', + clipboardtasklist: '\f184', + today: '\f185', + daterangepicker: '\f186', + cardcontent: '\f187', + cursormove: '\f188', + cursorprohibition: '\f189', + arrowsortup: '\f190', + arrowsortdown: '\f191', + imagethumbnail: '\f192', + sparkle: '\f193', + servicebell: '\f194', + dropzone: '\f195', + restore: '\f196', + groupbycolumn: '\f197', + ungroupcolumn: '\f198', + ungroupallcolumns: '\f199', + chatadd: '\f200', + colordismiss: '\f201', + clipboardpastesparkle: '\f202', + micoutline: '\f203', + micfilled: '\f204', + stopoutline: '\f205', + stopfilled: '\f206', + optionsoutline: '\f207', + optionsfilled: '\f208', + conferenceroomoutline: '\f209', + conferenceroomfilled: '\f210', + chatsparkleoutline: '\f211', + chatsparklefilled: '\f212', + addcircleoutline: '\f213', + addcirclefilled: '\f214', + zoomoutoutline: '\f215', + zoomoutfilled: '\f216', + zoominoutline: '\f217', + zoominfilled: '\f218', + calendardatestartoutline: '\f219', + calendardatestartfilled: '\f220', + calendardateendoutline: '\f221', + calendardateendfilled: '\f222', + datalineoutline: '\f223', + datalinefilled: '\f224', + dataareaoutline: '\f225', + dataareafilled: '\f226', + databaroutline: '\f227', + databarfilled: '\f228', + datastackedbaroutline: '\f229', + datastackedbarfilled: '\f230', + datapieoutline: '\f231', + datapiefilled: '\f232', + datadoughnutoutline: '\f233', + datadoughnutfilled: '\f234', + checkmarkcirclefilled: '\f235', + botoutline: '\f236', + botfilled: '\f237', + copyfilled: '\f238', + }; + + test('Icon set', async ({ page }) => { + + const icons = Object.entries(iconSet).map(([iconName, glyph]) => ({ + id: `dx-${new Guid()}`, + iconName, + glyph, + })); + + await ClientFunction((icons, ICON_CLASS) => { + const container = document.querySelector('#container'); + if (!container) return; + + const fragment = document.createDocumentFragment(); + + for (const { id, iconName, glyph } of icons) { + const element = document.createElement('div'); + element.id = id; + Object.assign(element.style, { + display: 'inline-flex', + padding: '3px', + border: '1px solid black', + alignItems: 'center', + flexDirection: 'column', + fontSize: '10px', + }); + + const iconDiv = document.createElement('div'); + iconDiv.classList.add(ICON_CLASS, `${ICON_CLASS}-${iconName}`); + + const nameDiv = document.createElement('div'); + nameDiv.textContent = iconName; + + const glyphDiv = document.createElement('div'); + glyphDiv.textContent = glyph.replace('\f', '\\f'); + + element.append(iconDiv, nameDiv, glyphDiv); + fragment.append(element); + } + + container.append(fragment); + }, { dependencies: { iconSet, ICON_CLASS } })(icons, ICON_CLASS); + + await testScreenshot(page, 'Icon set.png'); + + }); + + test('SVG icon set', async ({ page }) => { + + const themeName = (process.env.theme ?? 'fluent.blue.light'); + const icons = Object.keys(iconSet).map((iconName) => ({ + id: `dx-${new Guid()}`, + iconName, + src: `../../../packages/devextreme-scss/images/icons/${themeName}/${iconName}.svg`, + })); + + await ClientFunction((icons, appendElementTo) => { + for (const { id, iconName, src } of icons) { + appendElementTo(page, '#container', 'div', id, { + display: 'inline-flex', + padding: '3px', + border: '1px solid black', + alignItems: 'center', + flexDirection: 'column', + fontSize: '10px', + }); + + const el = document.getElementById(id); + if (el) { + const img = document.createElement('img'); + img.src = src; + + const nameDiv = document.createElement('div'); + nameDiv.textContent = iconName; + + el.append(img, nameDiv); + } + } + }, { dependencies: { appendElementTo } })(icons, appendElementTo); + + await testScreenshot(page, 'SVG icon set.png'); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/common/pagination/accessibility.spec.ts b/e2e/testcafe-devextreme/playwright-tests/common/pagination/accessibility.spec.ts new file mode 100644 index 000000000000..154fdc7afb9d --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/common/pagination/accessibility.spec.ts @@ -0,0 +1,50 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('Pagination', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + ['full', 'compact'].forEach((displayMode) => { + [undefined, 'Total {2} items. Page {0} of {1}'].forEach((infoText) => { + [true, false].forEach((showInfo) => { + [true, false].forEach((showNavigationButtons) => { + [true, false].forEach((showPageSizeSelector) => { + test(`Pagination dm_${displayMode}-` + + `${infoText ? 'has' : 'has_no'}_it-` + + `si_${showInfo.toString()}-` + + `snb_${showNavigationButtons.toString()}-` + + `spss_${showPageSizeSelector.toString()}`, async ({ page }) => { + await createWidget(page, 'dxPagination', { + itemCount: 50, + displayMode, + infoText, + showInfo, + showNavigationButtons, + showPageSizeSelector, + }); + + await testScreenshot(page, + `pagination-dm_${displayMode}-` + + `${infoText ? 'has' : 'has_no'}_it-` + + `si_${showInfo.toString()}-` + + `snb_${showNavigationButtons.toString()}-` + + `spss_${showPageSizeSelector.toString()}` + + '.png', + + }); + }); + }); + }); + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/common/pagination/baseProperties.spec.ts b/e2e/testcafe-devextreme/playwright-tests/common/pagination/baseProperties.spec.ts new file mode 100644 index 000000000000..20ddaefbd828 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/common/pagination/baseProperties.spec.ts @@ -0,0 +1,156 @@ +import { test, expect } from '@playwright/test'; +import { createWidget } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('Pagination Base Properties', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('Pagination width and height property', async ({ page }) => { + await createWidget(page, 'dxPagination', { + width: 270, + height: '95px', + itemCount: 50, + }); + + const pagination = page.locator('#container'); + await page.expect(pagination.element.getStyleProperty('width')) + .eql('270px') + .expect(pagination.element.getStyleProperty('height')) + .eql('95px') + .expect(pagination.element.getAttribute('width')) + .eql(null) + .expect(pagination.element.getAttribute('height')) + .eql(null); + + }); + + test('Pagination elementAttr property', async ({ page }) => { + await createWidget(page, 'dxPagination', { + elementAttr: { + 'aria-label': 'some description', + 'data-test': 'custom data', + }, + }); + + const pagination = page.locator('#container'); + await page.expect(pagination.element.getAttribute('aria-label')) + .eql('some description') + .expect(pagination.element.getAttribute('data-test')) + .eql('custom data'); + + }); + + test('Pagination hint and accessKey properties', async ({ page }) => { + await createWidget(page, 'dxPagination', { + hint: 'Best Pagination', + accessKey: 'F', + itemCount: 50, + focusStateEnabled: true, + }); + + const pagination = page.locator('#container'); + await page.expect(pagination.element.getAttribute('accesskey')) + .eql('F') + .expect(pagination.element.getAttribute('title')) + .eql('Best Pagination'); + + }); + + test('Pagination disabled property', async ({ page }) => { + await createWidget(page, 'dxPagination', { + disabled: true, + itemCount: 50, + }); + + const pagination = page.locator('#container'); + await page.expect(pagination.element.getAttribute('aria-disabled')) + .eql('true') + .expect(pagination.element.hasClass('dx-state-disabled')) + .ok(); + + }); + + test('Pagination tabindex and state properties', async ({ page }) => { + await createWidget(page, 'dxPagination', { + itemCount: 50, + disabled: false, + width: '100%', + focusStateEnabled: true, + hoverStateEnabled: true, + activeStateEnabled: true, + tabIndex: 7, + }); + + const pagination = page.locator('#container'); + await page.expect(pagination.element.getAttribute('tabindex')) + .eql('7') + + .click(pagination.getNavPage('3').element) + .expect(pagination.element.hasClass('dx-state-focused')) + .ok() + .expect(pagination.element.hasClass('dx-state-hover')) + .ok() + .expect(pagination.element.hasClass('dx-state-active')) + .notOk(); + + await ClientFunction((_pagination) => { + const $root = $(_pagination.element()); + + $root.trigger($.Event('dxpointerdown', { + pointers: [{ pointerId: 1 }], + })); + })(pagination); + + await page.expect(pagination.element.hasClass('dx-state-active')) + .ok(); + + }); + + test('Pagination focus method & accessKey propery without focusStateEnabled', async ({ page }) => { + await createWidget(page, 'dxPagination', { + focusStateEnabled: false, + accessKey: 'F', + itemCount: 50, + }); + + const pagination = page.locator('#container'); + await page.expect(pagination.element.getAttribute('accesskey')) + .eql(null) + .expect(pagination.getPageSize(0).element.focused) + .notOk(); + + await page.evaluate(() => { + _pagination.getInstance().focus(); + }); + + await page.expect(pagination.getPageSize(0).element.focused) + .ok(); + + }); + + test('Pagination focus method with focusStateEnabled', async ({ page }) => { + await createWidget(page, 'dxPagination', { + focusStateEnabled: true, + itemCount: 50, + }); + + const pagination = page.locator('#container'); + expect(pagination.element.focused).toBeFalsy(); + + await page.evaluate(() => { + _pagination.getInstance().focus(); + }); + + expect(pagination.element.focused).toBeTruthy(); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/common/pagination/index.spec.ts b/e2e/testcafe-devextreme/playwright-tests/common/pagination/index.spec.ts new file mode 100644 index 000000000000..3a2d53cf0747 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/common/pagination/index.spec.ts @@ -0,0 +1,78 @@ +import { test, expect } from '@playwright/test'; +import { createWidget } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('Pagination Base Properties', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('Pagination visibile property', async ({ page }) => { + await createWidget(page, 'dxPagination', { + itemCount: 50, + visible: false, + }); + + const pagination = page.locator('#container'); + await page.expect(pagination.element.hasClass('dx-state-invisible')) + .ok(); + + }); + + test('PageSize selector test', async ({ page }) => { + await createWidget(page, 'dxPagination', { + itemCount: 50, + pageIndex: 2, + pageSize: 8, // pageCount: 7 + allowedPageSizes: [2, 4, 8], + }); + + const pagination = page.locator('#container'); + + await pagination.getPageSize(1).element.click() + .expect(pagination.option('pageCount')) + .eql(13); + + }); + + test('PageIndex test', async ({ page }) => { + await createWidget(page, 'dxPagination', { + itemCount: 50, + pageIndex: 1, + pageSize: 5, // pageCount: 10 + }); + + const pagination = page.locator('#container'); + + await page.expect(pagination.option('pageIndex')) + .eql(1) + .click(pagination.getNavPage('5').element) + .expect(pagination.option('pageIndex')) + .eql(5); + + }); + + test('PageIndex correction test', async ({ page }) => { + await createWidget(page, 'dxPagination', { + itemCount: 50, + pageIndex: 10, + pageSize: 5, // pageCount: 10 + }); + + const pagination = page.locator('#container'); + + await page.expect(pagination.option('pageIndex')) + .eql(10) + .click(pagination.getPageSize(1).element) + .expect(pagination.option('pageIndex')) + .eql(5); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/common/pivotGrid/contextMenu.spec.ts b/e2e/testcafe-devextreme/playwright-tests/common/pivotGrid/contextMenu.spec.ts new file mode 100644 index 000000000000..56c5feb06528 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/common/pivotGrid/contextMenu.spec.ts @@ -0,0 +1,105 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('PivotGrid_contextMenu', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + const CONTEXT_MENU_CLASS = 'dx-context-menu'; + const FIELD_CHOOSER_AREA_FIELDS_CLASS = 'dx-area-fields'; + + test('ContextMenu width should be adjusted to the width of the item text (T1106236)', async ({ page }) => { + await createWidget(page, 'dxPivotGrid', { + width: 1000, + allowSortingBySummary: true, + allowSorting: true, + allowExpandAll: true, + showBorders: true, + fieldChooser: { + enabled: false, + }, + fieldPanel: { + showFilterFields: false, + allowFieldDragging: false, + visible: true, + }, + onContextMenuPreparing(e) { + if (e.field?.dataField === 'amount') { + const menuItems = [] as any; + + e.items.push({ text: 'Summary Type', items: menuItems }); + ['Sum', 'Avg', 'Min', 'Max'].forEach((summaryType) => { + const summaryTypeValue = summaryType.toLowerCase(); + const text = summaryType === 'Min' + ? 'Min - The box is too narrow, the item text does not fit inside.' + : summaryType; + menuItems.push({ + text, + value: summaryType.toLowerCase(), + selected: e.field.summaryType === summaryTypeValue, + }); + }); + } + }, + dataSource: { + fields: [{ + caption: 'Region', + width: 120, + dataField: 'region', + area: 'row', + }, { + caption: 'City', + dataField: 'city', + width: 150, + area: 'row', + }, { + dataField: 'date', + dataType: 'date', + area: 'column', + }, { + groupName: 'date', + groupInterval: 'year', + expanded: true, + }, { + caption: 'Relative Sales', + dataField: 'amount', + dataType: 'number', + summaryType: 'sum', + area: 'data', + summaryDisplayMode: 'percentOfColumnGrandTotal', + }], + store: [{ + id: 10887, + region: 'Africa', + country: 'Egypt', + city: 'Cairo', + amount: 500, + date: new Date('2015-05-26'), + }, { + id: 10888, + region: 'South America', + country: 'Argentina', + city: 'Buenos Aires', + amount: 780, + date: '2015-05-07', + }], + }, + }); + + await rightClick(page.locator(`.${FIELD_CHOOSER_AREA_FIELDS_CLASS}`).nth(1)); + + await page.locator(`.${CONTEXT_MENU_CLASS}`).hover(); + + await testScreenshot(page, 'PivotGrid contextmenu width.png'); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/common/pivotGrid/export/onExportingOption.spec.ts b/e2e/testcafe-devextreme/playwright-tests/common/pivotGrid/export/onExportingOption.spec.ts new file mode 100644 index 000000000000..ae67e4301276 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/common/pivotGrid/export/onExportingOption.spec.ts @@ -0,0 +1,43 @@ +import { test, expect } from '@playwright/test'; +import { createWidget } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('PivotGrid_fieldChooser', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('Should call \'onExporting\' when export button clicked', async ({ page }) => { + await createWidget(page, 'dxPivotGrid', { + dataSource: { + fields: [{ + caption: 'data A', + dataField: 'data_A', + }], + store: [], + }, + export: { + enabled: true, + }, + onExporting() { + (window as any).__exportCalled = true; + }, + }); + + const pivotGrid = page.locator('#container'); + const exportBtn = pivotGrid.getExportButton(); + + await click(exportBtn); + const exportCalled = await ClientFunction(() => (window as any).__exportCalled as boolean)(); + + expect(exportCalled).toBeTruthy(); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/common/pivotGrid/fieldChooser/T1138119_dragAndDropAreaItems.spec.ts b/e2e/testcafe-devextreme/playwright-tests/common/pivotGrid/fieldChooser/T1138119_dragAndDropAreaItems.spec.ts new file mode 100644 index 000000000000..11319feff91a --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/common/pivotGrid/fieldChooser/T1138119_dragAndDropAreaItems.spec.ts @@ -0,0 +1,135 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('pivotGrid_fieldChooser_drag-and-drop_T1138119 ', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('Drag-n-drop the tree view item in all directions', async ({ page }) => { + + await createWidget(page, 'dxPivotGrid', { + dataSource: { + store: [{ + id: 0, + data_0: 'data_0', + data_1: 'data_1', + data_2: 'data_2', + data_3: 'data_3', + data_4: 'data_4', + data_5: 'data_5', + data_6: 'data_6', + data_7: 'data_7', + data_8: 'data_8', + data_9: 'data_9', + data_10: 'data_10', + data_11: 'data_11', + data_12: 'data_12', + }], + }, + fieldChooser: { + enabled: true, + }, + }); + + const pivotGrid = page.locator('#container'); + await click(pivotGrid.getFieldChooserButton()); + + const fieldChooser = pivotGrid.getFieldChooser(); + const treeView = fieldChooser.getTreeView(); + const treeViewNodeItem = treeView.getNodeItem(); + + await MouseUpEvents.disable(MouseAction.dragToOffset); + + await drag(treeViewNodeItem, 0, -30, DRAG_MOUSE_OPTIONS); + await testScreenshot(page, 'field-chooser_tree-item_dnd_top.png', { element: fieldChooser.element }); + await treeViewNodeItem.dispatchEvent('mouseup'); + + await drag(treeViewNodeItem, 30, 0, DRAG_MOUSE_OPTIONS); + await testScreenshot(page, 'field-chooser_tree-item_dnd_right.png', { element: fieldChooser.element }); + await treeViewNodeItem.dispatchEvent('mouseup'); + + await drag(treeViewNodeItem, 0, 30, DRAG_MOUSE_OPTIONS); + await testScreenshot(page, 'field-chooser_tree-item_dnd_bottom.png', { element: fieldChooser.element }); + await treeViewNodeItem.dispatchEvent('mouseup'); + + await drag(treeViewNodeItem, -30, 0, DRAG_MOUSE_OPTIONS); + await testScreenshot(page, 'field-chooser_tree-item_dnd_left.png', { element: fieldChooser.element }); + await treeViewNodeItem.dispatchEvent('mouseup'); + + await MouseUpEvents.enable(MouseAction.dragToOffset); + + }); + + test('Drag-n-drop the row area item in all directions', async ({ page }) => { + + await createWidget(page, 'dxPivotGrid', { + dataSource: { + fields: [{ + caption: 'Data_0', + dataField: 'data_0', + area: 'row', + }, + { + caption: 'Data_1', + dataField: 'data_1', + area: 'row', + }, + { + caption: 'Data_2', + dataField: 'data_2', + area: 'row', + }, + { + caption: 'Data_3', + dataField: 'data_3', + area: 'row', + }, + { + caption: 'Data_4', + dataField: 'data_4', + area: 'row', + }], + store: [], + }, + fieldChooser: { + enabled: true, + }, + }); + + const pivotGrid = page.locator('#container'); + await click(pivotGrid.getFieldChooserButton()); + + const fieldChooser = pivotGrid.getFieldChooser(); + const rowAreaItem = fieldChooser.getRowAreaItem(); + + await MouseUpEvents.disable(MouseAction.dragToOffset); + + await drag(rowAreaItem, 0, -30, DRAG_MOUSE_OPTIONS); + await testScreenshot(page, 'field-chooser_row-area-item_dnd_top.png', { element: fieldChooser.element }); + await rowAreaItem.dispatchEvent('mouseup'); + + await drag(rowAreaItem, 30, 0, DRAG_MOUSE_OPTIONS); + await testScreenshot(page, 'field-chooser_row-area-item_dnd_right.png', { element: fieldChooser.element }); + await rowAreaItem.dispatchEvent('mouseup'); + + await drag(rowAreaItem, 0, 30, DRAG_MOUSE_OPTIONS); + await testScreenshot(page, 'field-chooser_row-area-item_dnd_bottom.png', { element: fieldChooser.element }); + await rowAreaItem.dispatchEvent('mouseup'); + + await drag(rowAreaItem, -30, 0, DRAG_MOUSE_OPTIONS); + await testScreenshot(page, 'field-chooser_row-area-item_dnd_left.png', { element: fieldChooser.element }); + await rowAreaItem.dispatchEvent('mouseup'); + + await MouseUpEvents.enable(MouseAction.dragToOffset); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/common/pivotGrid/fieldChooser/fieldChooser.spec.ts b/e2e/testcafe-devextreme/playwright-tests/common/pivotGrid/fieldChooser/fieldChooser.spec.ts new file mode 100644 index 000000000000..8c955ddcb73b --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/common/pivotGrid/fieldChooser/fieldChooser.spec.ts @@ -0,0 +1,600 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('PivotGrid_fieldChooser', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('Change dataFiels order with one invisible field (T1079461)', async ({ page }) => { + await createWidget(page, 'dxPivotGrid', { + allowSortingBySummary: true, + allowFiltering: true, + showBorders: true, + showColumnGrandTotals: false, + showRowGrandTotals: false, + showRowTotals: false, + showColumnTotals: false, + fieldChooser: { + enabled: true, + height: 800, + }, + dataSource: { + fields: [{ + caption: 'Region', + width: 120, + dataField: 'region', + area: 'row', + sortBySummaryField: 'Total', + }, { + caption: 'City', + dataField: 'city', + width: 150, + area: 'row', + }, { + dataField: 'date', + dataType: 'date', + area: 'column', + }, { + groupName: 'date', + groupInterval: 'month', + visible: false, + }, { + caption: 'Total', + dataField: 'amount', + dataType: 'number', + summaryType: 'sum', + format: 'currency', + area: 'data', + areaIndex: 0, + }, { + caption: 'Total Hidden', + dataField: 'amount', + dataType: 'number', + summaryType: 'sum', + format: 'currency', + area: 'data', + visible: false, + isMeasure: true, + }, { + caption: 'Total 2', + dataField: 'amount', + dataType: 'number', + summaryType: 'sum', + format: 'currency', + area: 'data', + areaIndex: 1, + }, { + caption: 'Total 3', + dataField: 'amount', + dataType: 'number', + summaryType: 'sum', + format: 'currency', + area: 'data', + areaIndex: 2, + }, { + caption: 'Total 4', + dataField: 'amount', + dataType: 'number', + summaryType: 'sum', + format: 'currency', + visible: true, + isMeasure: true, + }, { + caption: 'Total 5', + dataField: 'amount', + dataType: 'number', + summaryType: 'sum', + format: 'currency', + visible: true, + isMeasure: true, + }], + store: sales, + }, + }); + + const pivotGrid = page.locator('#container'); + + await click(pivotGrid.getFieldChooserButton()); + + const fieldChooser = pivotGrid.getFieldChooser(); + const fieldChooserTreeView = fieldChooser.getTreeView(); + + await fieldChooserTreeView.getCheckBoxByNodeIndex(0).element.click(); + await fieldChooserTreeView.getCheckBoxByNodeIndex(1).element.click(); + + await (async () => { + const box = await fieldChooser.getDataFields().nth(0).boundingBox(); + if (box) { + await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2); + await page.mouse.down(); + await page.mouse.move(box.x + box.width / 2 + 0, box.y + box.height / 2 + 170, { steps: 10 }); + await page.mouse.up(); + } + })(); + + await testScreenshot(page, 'FieldChooser change dataField order with invisible fields.png', { element: '.dx-overlay-content.dx-popup-draggable' }); + + }); + + test('Change dataFiels order with two invisible fields', async ({ page }) => { + await createWidget(page, 'dxPivotGrid', { + allowSortingBySummary: true, + allowFiltering: true, + showBorders: true, + showColumnGrandTotals: false, + showRowGrandTotals: false, + showRowTotals: false, + showColumnTotals: false, + fieldChooser: { + enabled: true, + height: 800, + }, + dataSource: { + fields: [{ + caption: 'Region', + width: 120, + dataField: 'region', + area: 'row', + sortBySummaryField: 'Total', + }, { + caption: 'City', + dataField: 'city', + width: 150, + area: 'row', + }, { + dataField: 'date', + dataType: 'date', + area: 'column', + }, { + groupName: 'date', + groupInterval: 'month', + visible: false, + }, { + caption: 'Total', + dataField: 'amount', + dataType: 'number', + summaryType: 'sum', + format: 'currency', + area: 'data', + areaIndex: 0, + }, { + caption: 'Total Hidden', + dataField: 'amount', + dataType: 'number', + summaryType: 'sum', + format: 'currency', + area: 'data', + visible: false, + isMeasure: true, + }, { + caption: 'Total 2', + dataField: 'amount', + dataType: 'number', + summaryType: 'sum', + format: 'currency', + area: 'data', + areaIndex: 1, + }, { + caption: 'Total Hidden', + dataField: 'amount', + dataType: 'number', + summaryType: 'sum', + format: 'currency', + area: 'data', + visible: false, + isMeasure: true, + }, { + caption: 'Total 3', + dataField: 'amount', + dataType: 'number', + summaryType: 'sum', + format: 'currency', + area: 'data', + areaIndex: 2, + }, { + caption: 'Total 4', + dataField: 'amount', + dataType: 'number', + summaryType: 'sum', + format: 'currency', + visible: true, + isMeasure: true, + }, { + caption: 'Total 5', + dataField: 'amount', + dataType: 'number', + summaryType: 'sum', + format: 'currency', + visible: true, + isMeasure: true, + }], + store: sales, + }, + }); + + const pivotGrid = page.locator('#container'); + + await click(pivotGrid.getFieldChooserButton()); + + const fieldChooser = pivotGrid.getFieldChooser(); + const fieldChooserTreeView = fieldChooser.getTreeView(); + + await fieldChooserTreeView.getCheckBoxByNodeIndex(0).element.click(); + await fieldChooserTreeView.getCheckBoxByNodeIndex(1).element.click(); + + await (async () => { + const box = await fieldChooser.getDataFields().nth(0).boundingBox(); + if (box) { + await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2); + await page.mouse.down(); + await page.mouse.move(box.x + box.width / 2 + 0, box.y + box.height / 2 + 170, { steps: 10 }); + await page.mouse.up(); + } + })(); + + await testScreenshot(page, 'FieldChooser change dataField order with two invisible fields.png', { element: '.dx-overlay-content.dx-popup-draggable' }); + + }); + + test('Change dataFiels order with three invisible fields (T1079461)', async ({ page }) => { + await createWidget(page, 'dxPivotGrid', { + allowSortingBySummary: true, + allowFiltering: true, + showBorders: true, + showColumnGrandTotals: false, + showRowGrandTotals: false, + showRowTotals: false, + showColumnTotals: false, + fieldChooser: { + enabled: true, + height: 800, + }, + onInitialized(e) { + function expand(dataSource) { + setTimeout(() => { + dataSource.expandHeaderItem('row', ['North America']); + dataSource.expandHeaderItem('column', [2013]); + }, 0); + } + + expand(e.component.getDataSource()); + }, + dataSource: { + fields: [{ + caption: 'Region', + width: 120, + dataField: 'region', + area: 'row', + sortBySummaryField: 'Total', + }, { + caption: 'City', + dataField: 'city', + width: 150, + area: 'row', + }, { + dataField: 'date', + dataType: 'date', + area: 'column', + }, { + groupName: 'date', + groupInterval: 'month', + visible: false, + }, { + caption: 'Total', + dataField: 'amount', + dataType: 'number', + summaryType: 'sum', + format: 'currency', + area: 'data', + areaIndex: 0, + }, { + caption: 'Total 2', + dataField: 'amount', + dataType: 'number', + summaryType: 'sum', + format: 'currency', + area: 'data', + areaIndex: 1, + }, { + caption: 'Total 3', + dataField: 'amount', + dataType: 'number', + summaryType: 'sum', + format: 'currency', + area: 'data', + areaIndex: 2, + }, { + caption: 'Total 4', + dataField: 'amount', + dataType: 'number', + summaryType: 'sum', + format: 'currency', + isMeasure: true, + }, { + caption: 'Total 5', + dataField: 'amount', + dataType: 'number', + summaryType: 'sum', + format: 'currency', + isMeasure: true, + }, { + caption: 'Total Hidden', + dataField: 'amount', + dataType: 'number', + summaryType: 'sum', + format: 'currency', + area: 'data', + visible: false, + isMeasure: true, + }, { + caption: 'Total Hidden 2', + dataField: 'amount', + dataType: 'number', + summaryType: 'sum', + format: 'currency', + area: 'data', + visible: false, + isMeasure: true, + }, { + caption: 'Total Hidden 3', + dataField: 'amount', + dataType: 'number', + summaryType: 'sum', + format: 'currency', + area: 'data', + visible: false, + isMeasure: true, + }], + store: sales, + }, + }); + + const pivotGrid = page.locator('#container'); + + await click(pivotGrid.getFieldChooserButton()); + + const fieldChooser = pivotGrid.getFieldChooser(); + const fieldChooserTreeView = fieldChooser.getTreeView(); + + await fieldChooserTreeView.getCheckBoxByNodeIndex(0).element.click(); + + await (async () => { + const box = await fieldChooser.getDataFields().nth(0).boundingBox(); + if (box) { + await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2); + await page.mouse.down(); + await page.mouse.move(box.x + box.width / 2 + 0, box.y + box.height / 2 + 170, { steps: 10 }); + await page.mouse.up(); + } + })(); + + await testScreenshot(page, 'FieldChooser change dataField order with three invisible fields.png', { element: '.dx-overlay-content.dx-popup-draggable' }); + + }); + + test('Change dataFiels order when applyChangesMode is "onDemand" (T1097764)', async ({ page }) => { + await createWidget(page, 'dxPivotGrid', { + allowSortingBySummary: true, + allowFiltering: true, + showBorders: true, + showColumnGrandTotals: false, + showRowGrandTotals: false, + showRowTotals: false, + showColumnTotals: false, + fieldChooser: { + enabled: true, + height: 800, + applyChangesMode: 'onDemand', + }, + onInitialized(e) { + function expand(dataSource) { + setTimeout(() => { + dataSource.expandHeaderItem('row', ['North America']); + dataSource.expandHeaderItem('column', [2013]); + }, 0); + } + + expand(e.component.getDataSource()); + }, + dataSource: { + fields: [{ + caption: 'Region', + width: 120, + dataField: 'region', + area: 'row', + sortBySummaryField: 'Total', + }, { + caption: 'City', + dataField: 'city', + width: 150, + area: 'row', + }, { + dataField: 'date', + dataType: 'date', + area: 'column', + }, { + groupName: 'date', + groupInterval: 'month', + visible: false, + }, { + caption: 'Total', + dataField: 'amount', + dataType: 'number', + summaryType: 'sum', + format: 'currency', + area: 'data', + areaIndex: 0, + }, { + caption: 'Total 2', + dataField: 'amount', + dataType: 'number', + summaryType: 'sum', + format: 'currency', + area: 'data', + areaIndex: 1, + }, { + caption: 'Total 3', + dataField: 'amount', + dataType: 'number', + summaryType: 'sum', + format: 'currency', + area: 'data', + areaIndex: 2, + }, { + caption: 'Total 4', + dataField: 'amount', + dataType: 'number', + summaryType: 'sum', + format: 'currency', + isMeasure: true, + }, { + caption: 'Total 5', + dataField: 'amount', + dataType: 'number', + summaryType: 'sum', + format: 'currency', + isMeasure: true, + }, { + caption: 'Total Hidden', + dataField: 'amount', + dataType: 'number', + summaryType: 'sum', + format: 'currency', + area: 'data', + visible: false, + isMeasure: true, + }, { + caption: 'Total Hidden 2', + dataField: 'amount', + dataType: 'number', + summaryType: 'sum', + format: 'currency', + area: 'data', + visible: false, + isMeasure: true, + }, { + caption: 'Total Hidden 3', + dataField: 'amount', + dataType: 'number', + summaryType: 'sum', + format: 'currency', + area: 'data', + visible: false, + isMeasure: true, + }, { + caption: 'Total Hidden 4', + dataField: 'amount', + dataType: 'number', + summaryType: 'sum', + format: 'currency', + area: 'data', + visible: false, + isMeasure: true, + }, { + caption: 'Total Hidden 5', + dataField: 'amount', + dataType: 'number', + summaryType: 'sum', + format: 'currency', + area: 'data', + visible: false, + isMeasure: true, + }, { + caption: 'Total Hidden 6', + dataField: 'amount', + dataType: 'number', + summaryType: 'sum', + format: 'currency', + area: 'data', + visible: false, + isMeasure: true, + }], + store: sales, + }, + }); + + const pivotGrid = page.locator('#container'); + + await click(pivotGrid.getFieldChooserButton()); + + const fieldChooser = pivotGrid.getFieldChooser(); + const fieldChooserTreeView = fieldChooser.getTreeView(); + + const dataFields = fieldChooser.getDataFields(); + + expect(dataFields.count).toBe(3) + .expect(dataFields.nth(0).textContent) + .eql('Total') + .expect(dataFields.nth(1).textContent) + .eql('Total 2') + .expect(dataFields.nth(2).textContent) + .eql('Total 3'); + + await fieldChooserTreeView.getCheckBoxByNodeIndex(1).element.click(); + + expect(dataFields.count).toBe(4) + .expect(dataFields.nth(0).textContent) + .eql('Total') + .expect(dataFields.nth(1).textContent) + .eql('Total 2') + .expect(dataFields.nth(2).textContent) + .eql('Total 3') + .expect(dataFields.nth(3).textContent) + .eql('Total 5'); + + await (async () => { + const box = await fieldChooser.getDataFields().nth(0).boundingBox(); + if (box) { + await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2); + await page.mouse.down(); + await page.mouse.move(box.x + box.width / 2 + 0, box.y + box.height / 2 + 150, { steps: 10 }); + await page.mouse.up(); + } + })(); + + expect(dataFields.count).toBe(4) + .expect(dataFields.nth(0).textContent) + .eql('Total 2') + .expect(dataFields.nth(1).textContent) + .eql('Total 3') + .expect(dataFields.nth(2).textContent) + .eql('Total 5') + .expect(dataFields.nth(3).textContent) + .eql('Total'); + + }); + + test('Field chooser can be clicked (T1290333)', async ({ page }) => { + await createWidget(page, 'dxPivotGrid', { + showBorders: true, + fieldPanel: { + showFilterFields: false, + visible: true, + }, + dataSource: { + fields: [{ + dataField: 'date', + dataType: 'date', + area: 'column', + }], + store: [], + }, + }); + + const pivotGrid = page.locator('#container'); + + await click(pivotGrid.getFieldChooserButton()); + await page.expect(pivotGrid.getFieldChooser().element.exists) + .ok(); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/common/pivotGrid/fieldPanel/T1283238_OLAP_drag_and_drop_field.spec.ts b/e2e/testcafe-devextreme/playwright-tests/common/pivotGrid/fieldPanel/T1283238_OLAP_drag_and_drop_field.spec.ts new file mode 100644 index 000000000000..811400df2201 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/common/pivotGrid/fieldPanel/T1283238_OLAP_drag_and_drop_field.spec.ts @@ -0,0 +1,63 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('pivotGrid_olap_drag-n-drop', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + const PIVOT_GRID_SELECTOR = '#container'; + + [true, false].forEach((showRowGrandTotals) => { + test(`Empty table has one ${showRowGrandTotals ? 'total' : 'empty'} row after drag-n-drop for paginated data`, async ({ page }) => { + + const pivotGrid = new PivotGrid(PIVOT_GRID_SELECTOR); + const loadPanel = pivotGrid.getLoadPanel(); + + await expect(loadPanel.isInvisible()).ok(); + await pivotGrid.scrollTo({ top: 5000 }); + await page.waitForTimeout(1000) + .expect(loadPanel.isInvisible()).ok(); + await dragToElement( + pivotGrid.getRowHeaderArea().getField(), + pivotGrid.getColumnHeaderArea().element, + + await expect(loadPanel.isInvisible()).ok(); + + await testScreenshot(page, + `empty_table_after_dnd (showRowGrandTotals=${showRowGrandTotals}).png`, + { element: pivotGrid.element }, + + });.before(async ({ page }) => { + await addRequestHooks(OLAPApiMock); + await createWidget(page, 'dxPivotGrid', { + height: 500, + fieldPanel: { visible: true }, + showRowGrandTotals, + scrolling: { mode: 'virtual', useNative: false }, + dataSource: { + paginate: true, + fields: [ + { dataField: '[Customer].[Customer]', area: 'row' }, + { dataField: '[Ship Date].[Calendar Year]', area: 'column' }, + { dataField: '[Measures].[Internet Sales Amount]', area: 'data' }, + ], + store: { + type: 'xmla', + url: 'https://api/data', + catalog: 'Catalog', + cube: 'Cube', + }, + }, + }); + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/common/pivotGrid/fieldPanel/T1287521_fields_aria_label.spec.ts b/e2e/testcafe-devextreme/playwright-tests/common/pivotGrid/fieldPanel/T1287521_fields_aria_label.spec.ts new file mode 100644 index 000000000000..d6104595fac0 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/common/pivotGrid/fieldPanel/T1287521_fields_aria_label.spec.ts @@ -0,0 +1,64 @@ +import { test, expect } from '@playwright/test'; +import { createWidget } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('pivotGrid_fieldPanel_aria_label', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + const PIVOT_GRID_SELECTOR = '#container'; + + test('Header fields should have correct aria-label', async ({ page }) => { + + await createWidget(page, 'dxPivotGrid', { + allowFiltering: true, + fieldPanel: { + visible: true, + }, + dataSource: { + fields: [{ + dataField: 'row1', + area: 'row', + }, { + dataField: 'row2', + area: 'row', + }, { + dataField: 'column1', + area: 'column', + }, { + dataField: 'column2', + area: 'column', + }, { + dataField: 'column3', + area: 'filter', + }], + store: [], + }, + }); + + const pivotGrid = new PivotGrid(PIVOT_GRID_SELECTOR); + const rowHeader = pivotGrid.getRowHeaderArea(); + const columnHeader = pivotGrid.getColumnHeaderArea(); + const filterHeader = pivotGrid.getFilterHeaderArea(); + + await page.expect(rowHeader.getHeaderFilterIcon(0).ariaLabel) + .eql('Show filter options for column \'Row1\'') + .expect(rowHeader.getHeaderFilterIcon(1).ariaLabel) + .eql('Show filter options for column \'Row2\'') + .expect(columnHeader.getHeaderFilterIcon(0).ariaLabel) + .eql('Show filter options for column \'Column1\'') + .expect(columnHeader.getHeaderFilterIcon(1).ariaLabel) + .eql('Show filter options for column \'Column2\'') + .expect(filterHeader.getHeaderFilterIcon(0).ariaLabel) + .eql('Show filter options for column \'Column3\''); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/common/pivotGrid/fieldPanel/dragAndDropFieldItems.spec.ts b/e2e/testcafe-devextreme/playwright-tests/common/pivotGrid/fieldPanel/dragAndDropFieldItems.spec.ts new file mode 100644 index 000000000000..0d40c0a8ff98 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/common/pivotGrid/fieldPanel/dragAndDropFieldItems.spec.ts @@ -0,0 +1,126 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('pivotGrid_fieldPanel_drag-n-drop', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + const PIVOT_GRID_SELECTOR = '#container'; + + test('Field panel items markup in the middle of the drag-n-drop', async ({ page }) => { + + await createWidget(page, 'dxPivotGrid', { + allowSorting: true, + allowFiltering: true, + fieldPanel: { + showColumnFields: true, + showDataFields: true, + showFilterFields: true, + showRowFields: true, + allowFieldDragging: true, + visible: true, + }, + dataSource: { + fields: [{ + dataField: 'date', + dataType: 'date', + area: 'column', + }, { + dataField: 'countA', + area: 'row', + }, { + dataField: 'countB', + area: 'row', + }, { + dataField: 'countC', + area: 'data', + }], + store: [{ + id: 0, + countA: 1, + countB: 1, + countC: 1, + date: '2013/01/13', + }], + }, + }); + + const pivotGrid = new PivotGrid(PIVOT_GRID_SELECTOR); + const columnFirstAction = pivotGrid.getColumnHeaderArea().getField(); + const rowFirstAction = pivotGrid.getRowHeaderArea().getField(); + const dataFirstAction = pivotGrid.getDataHeaderArea().getField(); + + await MouseUpEvents.disable(MouseAction.dragToOffset); + + await drag(columnFirstAction, 30, 30, DRAG_MOUSE_OPTIONS); + await testScreenshot(page, 'field-panel_column-action_dnd.png', { element: pivotGrid.element }); + await columnFirstAction.dispatchEvent('mouseup'); + + await drag(rowFirstAction, 30, 30, DRAG_MOUSE_OPTIONS); + await testScreenshot(page, 'field-panel_row-action_dnd.png', { element: pivotGrid.element }); + await columnFirstAction.dispatchEvent('mouseup'); + + await drag(dataFirstAction, 30, 30, DRAG_MOUSE_OPTIONS); + await testScreenshot(page, 'field-panel_data-action_dnd.png', { element: pivotGrid.element }); + await columnFirstAction.dispatchEvent('mouseup'); + + await MouseUpEvents.enable(MouseAction.dragToOffset); + + }); + + test('Should show d-n-d indicator during drag to first place in columns fields', async ({ page }) => { + + await createWidget(page, 'dxPivotGrid', { + showBorders: true, + fieldPanel: { + visible: true, + }, + dataSource: { + fields: [{ + dataField: 'row1', + area: 'row', + }, { + dataField: 'row2', + area: 'row', + }, { + dataField: 'column1', + area: 'column', + }, { + dataField: 'column2', + area: 'column', + }], + store: [], + }, + }); + + const pivotGrid = new PivotGrid(PIVOT_GRID_SELECTOR); + const rowFirstField = pivotGrid.getRowHeaderArea().getField(); + const columnHeaderAreaElement = pivotGrid.getColumnHeaderArea().element; + + await MouseUpEvents.disable(MouseAction.dragToOffset); + + const rowFirsFieldX = await rowFirstField.offsetLeft; + const rowFirsFieldY = await rowFirstField.offsetTop; + const columnHeaderX = await columnHeaderAreaElement.offsetLeft; + const columnHeaderY = await columnHeaderAreaElement.offsetTop; + const deltaOffsetX = 20; + const dragOffsetX = columnHeaderX - rowFirsFieldX - deltaOffsetX; + const dragOffsetY = rowFirsFieldY - columnHeaderY; + + await drag(rowFirstField, dragOffsetX, dragOffsetY, DRAG_MOUSE_OPTIONS); + + await testScreenshot(page, 'field-panel_column-field_dnd-first.png', { element: pivotGrid.element }); + + await MouseUpEvents.enable(MouseAction.dragToOffset); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/common/pivotGrid/headerFilter.spec.ts b/e2e/testcafe-devextreme/playwright-tests/common/pivotGrid/headerFilter.spec.ts new file mode 100644 index 000000000000..9ff804111d04 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/common/pivotGrid/headerFilter.spec.ts @@ -0,0 +1,100 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('pivotGrid_headerFilter', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + const PIVOT_GRID_SELECTOR = '#container'; + + test('Header filter popup', async ({ page }) => { + + await createWidget(page, 'dxPivotGrid', { + allowSorting: true, + allowFiltering: true, + fieldPanel: { + showColumnFields: true, + showDataFields: true, + showFilterFields: true, + showRowFields: true, + allowFieldDragging: true, + visible: true, + }, + headerFilter: { + allowSearch: true, + }, + dataSource: { + fields: [{ + dataField: 'region', + area: 'column', + }, { + dataField: 'date', + area: 'row', + }, { + dataField: 'amount', + area: 'data', + }], + store: sales, + }, + }); + + const pivotGrid = new PivotGrid(PIVOT_GRID_SELECTOR); + + await pivotGrid.getColumnHeaderArea().getHeaderFilterIcon().element.click(); + + await testScreenshot(page, 'headerFilter - before scroll.png'); + + await scroll(pivotGrid.getColumnHeaderArea().getHeaderFilterScrollable(), 0, 10); + + await testScreenshot(page, 'headerFilter - after scroll.png'); + + }); + + test('[T1284200] Should handle dxList "selectAll" when has unselected items on the second page', async ({ page }) => { + await createWidget(page, 'dxPivotGrid', { + dataSource: { + fields: [ + { + dataField: 'id', + area: 'column', + filterType: 'exclude', + filterValues: [70], + }, + ], + store: new Array(100).fill(null).map((_, idx) => ({ + id: idx, + })), + }, + allowSorting: true, + allowFiltering: true, + fieldPanel: { + visible: true, + }, + }); + + const pivotGrid = new PivotGrid(PIVOT_GRID_SELECTOR); + + const filterIconElement = pivotGrid.getColumnHeaderArea().getHeaderFilterIcon().element; + const headerFilter = new HeaderFilter(); + const list = headerFilter.getList(); + + await page.click(filterIconElement) + .click(list.selectAll.checkBox.element); + + expect(list.selectAll.checkBox.isChecked).toBeTruthy(); + + await list.selectAll.checkBox.element.click(); + + expect(list.selectAll.checkBox.isChecked).toBeFalsy(); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/common/pivotGrid/runningTotal/runningTotal.spec.ts b/e2e/testcafe-devextreme/playwright-tests/common/pivotGrid/runningTotal/runningTotal.spec.ts new file mode 100644 index 000000000000..acfac5d556c4 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/common/pivotGrid/runningTotal/runningTotal.spec.ts @@ -0,0 +1,153 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('PivotGrid: running total', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + const PIVOT_GRID_SELECTOR = '#container'; + + const seamlessData = [ + { + month: 'A', + value: 1, + first_row: '0_0', + second_row: '0_1', + }, + { + month: 'B', + value: 1, + first_row: '0_0', + second_row: '0_1', + }, + { + month: 'C', + value: 1, + first_row: '0_0', + second_row: '0_1', + }, + { + month: 'A', + value: 2, + first_row: '1_0', + second_row: '1_1', + }, + { + month: 'B', + value: 2, + first_row: '1_0', + second_row: '1_1', + }, + { + month: 'C', + value: 2, + first_row: '1_0', + second_row: '1_1', + }, + ]; + + const partialData = [ + { + month: 'A', + value: 1, + first_row: '0_0', + second_row: '0_1', + }, + { + month: 'B', + value: 2, + first_row: '1_0', + second_row: '1_1', + }, + { + month: 'C', + value: 3, + first_row: '2_0', + second_row: '2_1', + }, + ]; + + test('Should correctly sum cells values with runningTotal', async ({ page }) => { + await createWidget(page, 'dxPivotGrid', { + dataSource: { + fields: [ + { + dataField: 'first_row', + area: 'row', + expanded: true, + }, + { + dataField: 'second_row', + area: 'row', + }, + { + dataField: 'value', + dataType: 'number', + summaryType: 'sum', + area: 'data', + runningTotal: 'row', + }, + { + dataField: 'month', + area: 'column', + }, + ], + store: seamlessData, + }, + }); + + const pivotGrid = new PivotGrid(PIVOT_GRID_SELECTOR); + + await testScreenshot(page, 'running-total_seamless-data.png', { element: pivotGrid.element }); + + }); + + test('Should correctly sum cells values with runningTotal with partial data (T1144885)', async ({ page }) => { + await createWidget(page, 'dxPivotGrid', { + dataSource: { + fields: [ + { + dataField: 'first_row', + area: 'row', + expanded: true, + }, + { + dataField: 'second_row', + area: 'row', + }, + { + dataField: 'value', + dataType: 'number', + summaryType: 'sum', + area: 'data', + runningTotal: 'row', + }, + { + dataField: 'month', + area: 'column', + }, + ], + store: partialData, + }, + }); + + const pivotGrid = new PivotGrid(PIVOT_GRID_SELECTOR); + + await testScreenshot(page, 'running-total_partial-data_render-0.png', { element: pivotGrid.element }); + + const rowToCollapse = pivotGrid.getRowsArea().getCell(3); + await click(rowToCollapse); + + await testScreenshot(page, 'running-total_partial-data_render-1.png', { element: pivotGrid.element }); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/common/pivotGrid/scrolling.spec.ts b/e2e/testcafe-devextreme/playwright-tests/common/pivotGrid/scrolling.spec.ts new file mode 100644 index 000000000000..5cbf66ac6391 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/common/pivotGrid/scrolling.spec.ts @@ -0,0 +1,227 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, insertStylesheetRulesToPage, generateOptionMatrix } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('PivotGrid_scrolling', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + [ + { useNative: true, mode: 'standart' }, + { useNative: false, mode: 'standart' }, + ].forEach(({ useNative, mode }) => { + test(`Rows syncronization with vertical scrollbar when scrolling{useNative=${useNative},mode=${mode}} and white-space cell is normal (T1081956)`, async ({ page }) => { + + await insertStylesheetRulesToPage(page, '.dx-pivotgrid .dx-pivotgrid-area-data tbody td { white-space: normal !important; }'); + + await createWidget(page, 'dxPivotGrid', { + dataSource: { + store: virtualData, + retrieveFields: false, + fields: [{ + area: 'data', + dataType: 'string', + summaryType: 'custom', + calculateCustomSummary(options) { + if (options.summaryProcess === 'calculate') { + const item = options.value; + options.totalValue = `
${item.value}
`; + } + }, + }, { + dataField: 'y1path', + area: 'row', + width: 200, + expanded: true, + }, { + dataField: 'y2code', + area: 'row', + width: dataOptions.data.y2.visible ? undefined : 1, + }, { + dataField: 'x1code', + area: 'column', + expanded: true, + }], + }, + encodeHtml: false, + showColumnTotals: false, + height: 400, + width: 1200, + scrolling: { + mode, + useNative, + }, + }); + + + const pivotGrid = page.locator('#container'); + + await pivotGrid.scrollBy({ top: 300000 }); + await pivotGrid.scrollBy({ top: 100000 }); + await pivotGrid.scrollBy({ top: -150 }); + + await testScreenshot(page, `PivotGrid rows sync dir=vertical,useNative=${useNative},mode=${mode}.png`, { element: '#container' }); + + }); + + test(`Rows syncronization with both scrollbars when scrolling{useNative=${useNative},mode=${mode}} and white-space cell is normal (T1081956)`, async ({ page }) => { + + await insertStylesheetRulesToPage(page, '.dx-pivotgrid .dx-pivotgrid-area-data tbody td { white-space: normal !important; }'); + + await createWidget(page, 'dxPivotGrid', { + dataSource: { + store: virtualData, + retrieveFields: false, + fields: [{ + area: 'data', + dataType: 'string', + summaryType: 'custom', + calculateCustomSummary(options) { + if (options.summaryProcess === 'calculate') { + const item = options.value; + options.totalValue = `
${item.value}
`; + } + }, + }, { + dataField: 'y1path', + area: 'row', + width: 200, + expanded: true, + }, { + dataField: 'y2code', + area: 'row', + width: dataOptions.data.y2.visible ? undefined : 1, + }, { + dataField: 'x1code', + area: 'column', + expanded: true, + }], + }, + encodeHtml: false, + showColumnTotals: false, + height: 400, + width: 800, + scrolling: { + mode, + useNative, + }, + }); + + + const pivotGrid = page.locator('#container'); + + await pivotGrid.scrollBy({ top: 300000 }); + await pivotGrid.scrollBy({ top: 100000 }); + await pivotGrid.scrollBy({ top: -150 }); + + await testScreenshot(page, `PivotGrid rows sync dir=both,useNative=${useNative},mode=${mode}.png`, { element: '#container' }); + + }); + }); + + generateOptionMatrix({ + height: [450, 350], + useNative: [false, true], + }).forEach(({ height, useNative }) => { + test(`Rows content dont hide under vertical scrollbar when scrolling{useNative=${useNative}},height=100% (${height}px) (T1290313)`, async ({ page }) => { + + await insertStylesheetRulesToPage(page, `#parentContainer { height: ${height}px; }`); + + await createWidget(page, 'dxPivotGrid', { + height: '100%', + showBorders: true, + scrolling: { + useNative, + mode: 'standard', + }, + dataSource: { + fields: [{ + dataField: 'rowField', + area: 'row', + }, { + dataField: 'dataField', + area: 'data', + }, { + dataField: 'dataField', + area: 'data', + }], + store: Array.from({ length: 9 }).map((_, id) => ({ + id, + rowField: id > 7 ? 'row '.repeat(id) : `row ${id}`, + dataField: 47, + })), + }, + }); + + + await testScreenshot(page, + `PivotGrid rows content height=100%(${height}px),useNative=${useNative}.png`, + { element: '#container' }, + + }); + }); + + generateOptionMatrix({ + rtlEnabled: [false, true], + nativeScrolling: [false, true], + }).forEach(({ rtlEnabled, nativeScrolling }) => { + test('Should set margin for scroll-bar correctly (T1214743)', async ({ page }) => { + await createWidget(page, 'dxPivotGrid', { + height: 400, + scrolling: { useNative: nativeScrolling }, + showBorders: true, + rtlEnabled, + dataSource: { + fields: [{ + caption: 'Region', + width: 120, + dataField: 'region', + area: 'row', + }, { + caption: 'City', + dataField: 'city', + width: 150, + area: 'row', + selector(data) { + return `${data.city} (${data.country})`; + }, + }, { + dataField: 'date', + dataType: 'date', + area: 'column', + }, { + caption: 'Sales', + dataField: 'amount', + dataType: 'number', + summaryType: 'sum', + format: 'currency', + area: 'data', + }], + store: sales, + }, + }); + + const pivotGrid = page.locator('#container'); + + const firstCellToClick = pivotGrid.getRowsArea().getCell(1); + await click(firstCellToClick); + await testScreenshot(page, `scrollbar-margin_step-0_useNative-${nativeScrolling}_rtl-${rtlEnabled}`, { element: pivotGrid.element }); + + const secondCellToClick = pivotGrid.getRowsArea().getCell(6); + await click(secondCellToClick); + await testScreenshot(page, `scrollbar-margin_step-1_useNative-${nativeScrolling}_rtl-${rtlEnabled}`, { element: pivotGrid.element }); + + await click(secondCellToClick); + await testScreenshot(page, `scrollbar-margin_step-2_useNative-${nativeScrolling}_rtl-${rtlEnabled}`, { element: pivotGrid.element }); + + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/common/pivotGrid/sort/localSort_T1150523.spec.ts b/e2e/testcafe-devextreme/playwright-tests/common/pivotGrid/sort/localSort_T1150523.spec.ts new file mode 100644 index 000000000000..186d2a0ab976 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/common/pivotGrid/sort/localSort_T1150523.spec.ts @@ -0,0 +1,193 @@ +import { test, expect } from '@playwright/test'; +import { createWidget } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('pivotGrid_sort', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('Should sort without DataSource reload if scrolling mode isn\'t virtual', async ({ page }) => { + const requestLogger = RequestLogger(/\/api\/data/); + const pivotGrid = page.locator('#container'); + + await addRequestHooks(requestLogger); + + await page.waitForTimeout(500); + + requestLogger.clear(); + const initialRequestCount = await requestLogger.count(() => true); + + await click(pivotGrid.getColumnHeaderArea().getField()); + + await page.waitForTimeout(500); + + const afterSortRequestCount = await requestLogger.count(() => true); + const requestCount = afterSortRequestCount - initialRequestCount; + + expect(requestCount).toBe(0); + + await removeRequestHooks(requestLogger); + + });.before(async ({ page }) => { + const apiRequestMock = RequestMock() + .onRequestTo(/\/api\/data\?skip/) + .respond( + { + data: [ + { id: 0, label: 'A', value: 10 }, + { id: 1, label: 'B', value: 20 }, + { id: 2, label: 'C', value: 30 }, + ], + }, + 200, + { 'access-control-allow-origin': '*' }, + ) + .onRequestTo(/\/api\/data\?group/) + .respond( + { + data: [ + { + key: 'A', + items: null, + summary: [10], + }, + { + key: 'B', + items: null, + summary: [20], + }, + { + key: 'C', + items: null, + summary: [30], + }, + ], + }, + 200, + { 'access-control-allow-origin': '*' }, + + (t.ctx as any).apiRequestMock = apiRequestMock; + await addRequestHooks(apiRequestMock); + + await createWidget(page, 'dxPivotGrid', () => ({ + allowSorting: true, + fieldPanel: { visible: true }, + dataSource: { + remoteOperations: true, + store: (window as any).DevExpress.data.AspNet.createStore({ + key: 'id', + loadUrl: 'https://api/data', + }), + fields: [ + { + dataField: 'label', + area: 'column', + }, + { + dataField: 'value', + dataType: 'number', + area: 'data', + }, + ], + }, + })); + }).after(async ({ page }) => { + await removeRequestHooks((t.ctx as any).apiRequestMock); + }); + + test('Should sort with DataSource reload if scrolling mode is virtual', async ({ page }) => { + const requestLogger = RequestLogger(/\/api\/data/); + const pivotGrid = page.locator('#container'); + + await addRequestHooks(requestLogger); + + await page.waitForTimeout(500); + + requestLogger.clear(); + const initialRequestCount = await requestLogger.count(() => true); + + await click(pivotGrid.getColumnHeaderArea().getField()); + + const afterSortRequestCount = await requestLogger.count(() => true); + const requestCount = afterSortRequestCount - initialRequestCount; + + expect(requestCount).toBe(1); + + await removeRequestHooks(requestLogger); + + });.before(async ({ page }) => { + const apiRequestMock = RequestMock() + .onRequestTo(/\/api\/data\?skip/) + .respond( + { + data: [ + { id: 0, label: 'A', value: 10 }, + { id: 1, label: 'B', value: 20 }, + { id: 2, label: 'C', value: 30 }, + ], + }, + 200, + { 'access-control-allow-origin': '*' }, + ) + .onRequestTo(/\/api\/data\?group/) + .respond( + { + data: [ + { + key: 'A', + items: null, + summary: [10], + }, + { + key: 'B', + items: null, + summary: [20], + }, + { + key: 'C', + items: null, + summary: [30], + }, + ], + }, + 200, + { 'access-control-allow-origin': '*' }, + + (t.ctx as any).apiRequestMock = apiRequestMock; + await addRequestHooks(apiRequestMock); + + await createWidget(page, 'dxPivotGrid', () => ({ + allowSorting: true, + fieldPanel: { visible: true }, + scrolling: { mode: 'virtual' }, + dataSource: { + remoteOperations: true, + store: (window as any).DevExpress.data.AspNet.createStore({ + key: 'id', + loadUrl: 'https://api/data', + }), + fields: [ + { + dataField: 'label', + area: 'column', + }, + { + dataField: 'value', + dataType: 'number', + area: 'data', + }, + ], + }, + })); + }).after(async ({ page }) => { + await removeRequestHooks((t.ctx as any).apiRequestMock); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/common/pivotGrid/sort/sortWithSummaryDisplayMode_T1173442.spec.ts b/e2e/testcafe-devextreme/playwright-tests/common/pivotGrid/sort/sortWithSummaryDisplayMode_T1173442.spec.ts new file mode 100644 index 000000000000..4e44035330bd --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/common/pivotGrid/sort/sortWithSummaryDisplayMode_T1173442.spec.ts @@ -0,0 +1,119 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('pivotGrid_sort', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('Should apply sort changes to the markup if the "summaryDisplayMode" is set', async ({ page }) => { + await createWidget(page, 'dxPivotGrid', { + allowSortingBySummary: true, + allowSorting: true, + fieldPanel: { + showFilterFields: false, + visible: true, + }, + dataSource: { + fields: [{ + dataField: 'row', + area: 'row', + }, { + dataField: 'column', + area: 'column', + }, { + dataField: 'value', + dataType: 'number', + summaryType: 'sum', + area: 'data', + summaryDisplayMode: 'percentVariation', + }], + store: [ + { + row: 'row_A', + column: 'column_A', + value: 100, + }, + { + row: 'row_A', + column: 'column_A', + value: 100, + }, + { + row: 'row_A', + column: 'column_B', + value: 150, + }, + { + row: 'row_A', + column: 'column_B', + value: 150, + }, + { + row: 'row_A', + column: 'column_C', + value: 200, + }, + { + row: 'row_A', + column: 'column_C', + value: 200, + }, + { + row: 'row_B', + column: 'column_A', + value: 100, + }, + { + row: 'row_B', + column: 'column_A', + value: 100, + }, + { + row: 'row_B', + column: 'column_B', + value: 150, + }, + { + row: 'row_B', + column: 'column_B', + date: '2022-01-02', + value: 150, + }, + { + row: 'row_B', + column: 'column_C', + value: 200, + }, + { + row: 'row_B', + column: 'column_C', + date: '2022-01-02', + value: 200, + }, + ], + }, + }); + + const pivotGrid = page.locator('#container'); + + await testScreenshot(page, + 'T1173442_before_sort_with_summary_display_mode.png', + { element: pivotGrid.element }, + + await click(pivotGrid.getColumnHeaderArea().getField()); + await click(pivotGrid.getRowHeaderArea().getField()); + await testScreenshot(page, + 'T1173442_after_sort_with_summary_display_mode.png', + { element: pivotGrid.element }, + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/common/pivotGrid/virtualScrolling_T1210807.spec.ts b/e2e/testcafe-devextreme/playwright-tests/common/pivotGrid/virtualScrolling_T1210807.spec.ts new file mode 100644 index 000000000000..2f74abcf3118 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/common/pivotGrid/virtualScrolling_T1210807.spec.ts @@ -0,0 +1,88 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('PivotGrid_scrolling', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + const createData = (count, innerCount) => { + const result: object[] = []; + + for (let i = 0; i < count; i += 1) { + for (let j = 0; j < innerCount; j += 1) { + result.push({ + item: `item ${i}`, + date: new Date('2024-01-01'), + category: `category ${j}`, + innerA: j, + innerB: j, + }); + } + } + + return result; + }; + + test('Row fields overlap data fields if dataFieldArea is set to "row" and virtual scrolling is enabled (T1210807)', async ({ page }) => { + await createWidget(page, 'dxPivotGrid', { + allowExpandAll: true, + showBorders: true, + rowHeaderLayout: 'tree', + dataFieldArea: 'row', + height: 560, + scrolling: { + mode: 'virtual', + }, + dataSource: { + fields: [ + { + dataField: 'item', + area: 'row', + width: 120, + }, + { + dataField: 'category', + area: 'row', + width: 120, + }, + { + dataField: 'date', + dataType: 'date', + area: 'column', + groupInterval: 'year', + }, + { + dataField: 'innerA', + dataType: 'number', + summaryType: 'sum', + area: 'data', + }, + { + dataField: 'innerB', + dataType: 'number', + summaryType: 'sum', + area: 'data', + }, + ], + store: createData(50, 5), + }, + }); + + const pivotGrid = page.locator('#container'); + const firstHeaderRow = pivotGrid.getRowsArea(2).getCell(0); + await page.click(firstHeaderRow); + await pivotGrid.scrollBy({ top: 30000 }); + + await testScreenshot(page, 'rows_do_not_overlap_data_fields_if_virtual_scrolling_enabled_T1210807.png', { element: pivotGrid.element }); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/common/shadowDOM.spec.ts b/e2e/testcafe-devextreme/playwright-tests/common/shadowDOM.spec.ts new file mode 100644 index 000000000000..7e3ef41d030f --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/common/shadowDOM.spec.ts @@ -0,0 +1,95 @@ +import { test, expect } from '@playwright/test'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../tests/container.html')}`; + +test.describe('Shadow DOM - Adopted DX css styles', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + const dxWidgetHostStyles = '.dx-widget-shadow { font-size: 20px; }'; + const dxWidgetShadowStyles = '.dx-widget-shadow { font-size: 40px; }'; + + const setupShadowDomTest = (copyStylesToShadowDom) => ClientFunction((copyStyles) => { + if (!copyStyles) { + (window as any).DevExpress.config({ copyStylesToShadowDom: copyStyles }); + } + + const container = document.createElement('div'); + container.id = 'shadow-host'; + document.body.appendChild(container); + + const hostStyleElement = document.createElement('style'); + hostStyleElement.innerHTML = dxWidgetHostStyles; + document.head.appendChild(hostStyleElement); + + const shadowRoot = container.attachShadow({ mode: 'open' }); + + const shadowStyleElement = shadowRoot.ownerDocument.createElement('style'); + shadowStyleElement.innerHTML = dxWidgetShadowStyles; + shadowRoot.appendChild(shadowStyleElement); + + const shadowContainerElement = document.createElement('div'); + shadowContainerElement.id = 'shadow-container'; + shadowRoot.appendChild(shadowContainerElement); + + (window as any).testShadowRoot = shadowRoot; + + new (window as any).DevExpress.ui.dxButton(shadowContainerElement, { + text: 'Test button', + }); + }, { + dependencies: { + dxWidgetHostStyles, + dxWidgetShadowStyles, + }, + })(copyStylesToShadowDom); + + const getAdoptedStyleSheets = async () => page.evaluate(() => { + const shadowRoot = (window as any).testShadowRoot; + const { adoptedStyleSheets } = shadowRoot; + + const results: { [key: string]: string[] | null } = { + firstSheetRules: null, + secondSheetRules: null, + }; + + if (adoptedStyleSheets.length > 1) { + results.firstSheetRules = Array + .from(adoptedStyleSheets[0].cssRules).map((rule) => (rule as CSSRule).cssText); + + results.secondSheetRules = Array + .from(adoptedStyleSheets[1].cssRules).map((rule) => (rule as CSSRule).cssText); + } + + return results; + }); + + test('Copies DX css styles from the host to the shadow root when rendering a DX widget', async ({ page }) => { + await setupShadowDomTest(true); + + const { firstSheetRules, secondSheetRules } = await getAdoptedStyleSheets(); + + const hasHostStyle = firstSheetRules?.some((rule) => rule === dxWidgetHostStyles); + await expect(hasHostStyle).ok('First adopted stylesheet contains host styles'); + + const hasShadowStyle = secondSheetRules?.some((rule) => rule === dxWidgetShadowStyles); + await expect(hasShadowStyle).ok('Second adopted stylesheet contains shadow styles'); + + }); + + test('Does not copy DX css styles when copyStylesToShadowDom is disabled', async ({ page }) => { + await setupShadowDomTest(false); + + const { firstSheetRules, secondSheetRules } = await getAdoptedStyleSheets(); + await expect(firstSheetRules === null && secondSheetRules === null) + .ok('No adopted stylesheets should be created when copyStylesToShadowDom is disabled'); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/common/treeList/API.spec.ts b/e2e/testcafe-devextreme/playwright-tests/common/treeList/API.spec.ts new file mode 100644 index 000000000000..005acdd00708 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/common/treeList/API.spec.ts @@ -0,0 +1,83 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('Public methods', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + const getItems = (): Record[] => { + const items: Record[] = []; + + for (let i = 0; i < 100; i += 1) { + items.push({ key: `item_${i}`, parentKey: null }); + + for (let j = 0; j < 100; j += 1) { + items.push({ key: `item_${i}_${j}`, parentKey: `item_${i}` }); + } + } + + return items; + }; + + [true, false].forEach((renderAsync) => { + [true, false].forEach((useNativeScrolling) => { + test(`The renderAsync=${renderAsync} and scrolling.useNative=${useNativeScrolling}: The navigateToRow method should work correctly when there are asynchronous cell templates and virtual scrolling is enabled (T1275775)`, async ({ page }) => { + await createWidget(page, 'dxTreeList', { + dataSource: getItems(), + height: 500, + width: 500, + dataStructure: 'plain', + parentIdExpr: 'parentKey', + keyExpr: 'key', + renderAsync, + scrolling: { + mode: 'virtual', + useNaive: useNativeScrolling, + }, + templatesRenderAsynchronously: true, + columns: [{ + dataField: 'key', + cellTemplate: 'testCellTemplate', + }], + integrationOptions: { + templates: { + testCellTemplate: { + render({ model, container, onRendered }) { + setTimeout(() => { + container.append($('').text(model.value)); + onRendered(); + }, 100); + }, + }, + }, + }, + }); + + // arrange + const treeList = page.locator('#container'); + + await page.expect(treeList.getDataCell(0, 0).element.textContent) + .contains('item_'); + + // act + await treeList.apiNavigateToRow('item_80_50'); + + // assert + await page.expect(treeList.getDataCell(131, 0).element.textContent) + .contains('item_'); + + await testScreenshot(page, `T1275775-navigateToRow-with-async-cell-templates_(renderAsync=${renderAsync}_useNativeScrolling=${useNativeScrolling}).png`, { element: treeList.element }); + + }); + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/common/treeList/adaptiveRow.spec.ts b/e2e/testcafe-devextreme/playwright-tests/common/treeList/adaptiveRow.spec.ts new file mode 100644 index 000000000000..c3611b3bdedd --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/common/treeList/adaptiveRow.spec.ts @@ -0,0 +1,71 @@ +import { test, expect } from '@playwright/test'; +import { createWidget } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('Adaptive Row', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('Should be shown and hidden when the window is resized', async ({ page }) => { + await createWidget(page, 'dxTreeList', { + dataSource: [{ + ID: 1, + Head_ID: -1, + Full_Name: 'John Heart', + Prefix: 'Mr.', + Title: 'CEO', + City: 'Los Angeles', + State: 'California', + Email: 'jheart@dx-email.com', + Skype: 'jheart_DX_skype', + Mobile_Phone: '(213) 555-9392', + Birth_Date: '1964-03-16', + Hire_Date: '1995-01-15', + }], + keyExpr: 'ID', + parentIdExpr: 'Head_ID', + rootValue: -1, + allowColumnResizing: true, + rowDragging: { + allowDropInsideItem: true, + allowReordering: true, + }, + columns: [ + { + dataField: 'Title', + caption: 'Position', + hidingPriority: 0, + fixed: true, + }, + { dataField: 'Full_Name', hidingPriority: 1 }, + { dataField: 'City', hidingPriority: 2 }, + { dataField: 'State', hidingPriority: 3 }, + { dataField: 'Mobile_Phone', hidingPriority: 4 }, + { dataField: 'Hire_Date', dataType: 'date', hidingPriority: 5 }, + ], + }); + + const treeList = page.locator('#container'); + await treeList.isReady(); + + const adaptiveButton = treeList.getAdaptiveButton(); + expect(adaptiveButton.exists).toBeTruthy(); + await click(adaptiveButton); + + await expect(treeList.getAdaptiveRow(0).element.exists).ok(); + + await resizeWindow(1200, 400); + + await expect(treeList.isAdaptiveColumnHidden()).ok(); + await expect(treeList.getAdaptiveRow(0).element.exists).notOk(); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/common/treeList/aiColumn/functional.spec.ts b/e2e/testcafe-devextreme/playwright-tests/common/treeList/aiColumn/functional.spec.ts new file mode 100644 index 000000000000..2b4818e89fa5 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/common/treeList/aiColumn/functional.spec.ts @@ -0,0 +1,515 @@ +import { test, expect } from '@playwright/test'; +import { createWidget } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('Ai Column.Common (TreeList)', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + const TREE_LIST_SELECTOR = '#container'; + const EMPTY_CELL_TEXT = '\u00A0'; + const DROPDOWNMENU_PROMPT_EDITOR_INDEX = 0; + const DROPDOWNMENU_REGENERATE_INDEX = 1; + const DROPDOWNMENU_CLEAR_DATA_INDEX = 2; + + test('Get result from AI and display it in the AI column', async ({ page }) => { + await createWidget(page, 'dxTreeList', () => ({ + dataSource: [ + { + id: 1, parentId: 0, name: 'Name 1', value: 10, + }, + { + id: 2, parentId: 1, name: 'Name 2', value: 20, + }, + { + id: 3, parentId: 1, name: 'Name 3', value: 30, + }, + ], + keyExpr: 'id', + parentIdExpr: 'parentId', + autoExpandAll: true, + columns: [ + { dataField: 'id', caption: 'ID' }, + { dataField: 'name', caption: 'Name' }, + { dataField: 'value', caption: 'Value' }, + { + type: 'ai', + caption: 'AI Column', + name: 'AI Column', + ai: { + prompt: 'first AI column', + aiIntegration: new (window as any).DevExpress.aiIntegration({ + sendRequest(prompt) { + return { + promise: new Promise((resolve) => { + const result: Record = {}; + + Object.entries(prompt.data?.data).forEach(([key, value]) => { + result[key] = `Response ${(value as any).name} for ${prompt.data?.text}`; + }); + + resolve(JSON.stringify(result)); + }), + abort: (): void => {}, + }; + }, + }), + }, + }, + ], + })); + + const treeList = new TreeList(TREE_LIST_SELECTOR); + + await expect(treeList.isReady()).ok(); + + await expect(treeList.getDataCell(0, 3).element.innerText).eql('Response Name 1 for first AI column'); + await expect(treeList.getDataCell(1, 3).element.innerText).eql('Response Name 2 for first AI column'); + await expect(treeList.getDataCell(2, 3).element.innerText).eql('Response Name 3 for first AI column'); + + }); + + test('Get result from AI and display it in two AI columns', async ({ page }) => { + await createWidget(page, 'dxTreeList', () => ({ + dataSource: [ + { + id: 1, parentId: 0, name: 'Name 1', value: 10, + }, + { + id: 2, parentId: 1, name: 'Name 2', value: 20, + }, + { + id: 3, parentId: 1, name: 'Name 3', value: 30, + }, + ], + keyExpr: 'id', + parentIdExpr: 'parentId', + autoExpandAll: true, + columns: [ + { dataField: 'id', caption: 'ID' }, + { dataField: 'name', caption: 'Name' }, + { dataField: 'value', caption: 'Value' }, + { + type: 'ai', + caption: 'AI Column', + name: 'AI Column', + ai: { + prompt: 'first AI column', + aiIntegration: new (window as any).DevExpress.aiIntegration({ + sendRequest(prompt) { + return { + promise: new Promise((resolve) => { + const result: Record = {}; + + Object.entries(prompt.data?.data).forEach(([key, value]) => { + result[key] = `Response ${(value as any).name} for ${prompt.data?.text}`; + }); + + resolve(JSON.stringify(result)); + }), + abort: (): void => {}, + }; + }, + }), + }, + }, + { + type: 'ai', + caption: 'AI Column2', + name: 'AI Column2', + ai: { + prompt: 'second AI column', + aiIntegration: new (window as any).DevExpress.aiIntegration({ + sendRequest(prompt) { + return { + promise: new Promise((resolve) => { + const result: Record = {}; + + Object.entries(prompt.data?.data).forEach(([key, value]) => { + result[key] = `Response ${(value as any).name} for ${prompt.data?.text}`; + }); + + resolve(JSON.stringify(result)); + }), + abort: (): void => {}, + }; + }, + }), + }, + }, + ], + })); + + const treeList = new TreeList(TREE_LIST_SELECTOR); + + await expect(treeList.isReady()).ok(); + + await expect(treeList.getDataCell(0, 3).element.innerText).eql('Response Name 1 for first AI column'); + await expect(treeList.getDataCell(1, 3).element.innerText).eql('Response Name 2 for first AI column'); + await expect(treeList.getDataCell(2, 3).element.innerText).eql('Response Name 3 for first AI column'); + await expect(treeList.getDataCell(0, 4).element.innerText).eql('Response Name 1 for second AI column'); + await expect(treeList.getDataCell(1, 4).element.innerText).eql('Response Name 2 for second AI column'); + await expect(treeList.getDataCell(2, 4).element.innerText).eql('Response Name 3 for second AI column'); + + }); + + test('Regenerate the AI request from DropDownButton menu', async ({ page }) => { + await createWidget(page, 'dxTreeList', () => ({ + dataSource: [ + { + id: 1, parentId: 0, name: 'Name 1', value: 10, + }, + { + id: 2, parentId: 1, name: 'Name 2', value: 20, + }, + { + id: 3, parentId: 1, name: 'Name 3', value: 30, + }, + ], + keyExpr: 'id', + parentIdExpr: 'parentId', + autoExpandAll: true, + columns: [ + { dataField: 'id', caption: 'ID' }, + { dataField: 'name', caption: 'Name' }, + { dataField: 'value', caption: 'Value' }, + { + type: 'ai', + caption: 'AI Column', + name: 'AI Column', + ai: { + mode: 'manual', + prompt: 'first AI column', + aiIntegration: new (window as any).DevExpress.aiIntegration({ + sendRequest(prompt) { + return { + promise: new Promise((resolve) => { + const result: Record = {}; + + Object.entries(prompt.data?.data).forEach(([key, value]) => { + result[key] = `Response ${(value as any).name} for ${prompt.data?.text}`; + }); + resolve(JSON.stringify(result)); + }), + abort: (): void => {}, + }; + }, + }), + }, + }, + ], + })); + + const treeList = new TreeList(TREE_LIST_SELECTOR); + + await expect(treeList.isReady()).ok(); + + await expect(treeList.getDataCell(0, 3).element.innerText).eql(EMPTY_CELL_TEXT); + await expect(treeList.getDataCell(1, 3).element.innerText).eql(EMPTY_CELL_TEXT); + await expect(treeList.getDataCell(2, 3).element.innerText).eql(EMPTY_CELL_TEXT); + + const aiColumnHeaderCell = treeList.getHeaders().getHeaderRow(0).getHeaderCell(3); + + await aiColumnHeaderCell.getAIHeaderButton().element.click(); + const dropDownList = await aiColumnHeaderCell.getAIHeaderButton().getList(); + await dropDownList.getItem(DROPDOWNMENU_REGENERATE_INDEX).element.click(); + + await expect(treeList.getDataCell(0, 3).element.innerText).eql('Response Name 1 for first AI column'); + await expect(treeList.getDataCell(1, 3).element.innerText).eql('Response Name 2 for first AI column'); + await expect(treeList.getDataCell(2, 3).element.innerText).eql('Response Name 3 for first AI column'); + + }); + + test('Regenerate the AI request from Prompt Editor', async ({ page }) => { + await createWidget(page, 'dxTreeList', () => ({ + dataSource: [ + { + id: 1, parentId: 0, name: 'Name 1', value: 10, + }, + { + id: 2, parentId: 1, name: 'Name 2', value: 20, + }, + { + id: 3, parentId: 1, name: 'Name 3', value: 30, + }, + ], + keyExpr: 'id', + parentIdExpr: 'parentId', + autoExpandAll: true, + columns: [ + { dataField: 'id', caption: 'ID' }, + { dataField: 'name', caption: 'Name' }, + { dataField: 'value', caption: 'Value' }, + { + type: 'ai', + caption: 'AI Column', + name: 'AI Column', + ai: { + mode: 'manual', + prompt: 'first AI column', + aiIntegration: new (window as any).DevExpress.aiIntegration({ + sendRequest(prompt) { + return { + promise: new Promise((resolve) => { + const result: Record = {}; + + Object.entries(prompt.data?.data).forEach(([key, value]) => { + result[key] = `Response ${(value as any).name} for ${prompt.data?.text}`; + }); + + resolve(JSON.stringify(result)); + }), + abort: (): void => {}, + }; + }, + }), + }, + }, + ], + })); + + const treeList = new TreeList(TREE_LIST_SELECTOR); + + await expect(treeList.isReady()).ok(); + + await expect(treeList.getDataCell(0, 3).element.innerText).eql(EMPTY_CELL_TEXT); + await expect(treeList.getDataCell(1, 3).element.innerText).eql(EMPTY_CELL_TEXT); + await expect(treeList.getDataCell(2, 3).element.innerText).eql(EMPTY_CELL_TEXT); + const aiColumnHeaderCell = treeList.getHeaders().getHeaderRow(0).getHeaderCell(3); + + await aiColumnHeaderCell.getAIHeaderButton().element.click(); + const dropDownList = await aiColumnHeaderCell.getAIHeaderButton().getList(); + await dropDownList.getItem(DROPDOWNMENU_PROMPT_EDITOR_INDEX).element.click(); + + const promptEditor = treeList.getAIPromptEditor(); + + await promptEditor.getRegenerateButton().element.click(); + + await expect(treeList.getDataCell(0, 3).element.innerText).eql('Response Name 1 for first AI column'); + await expect(treeList.getDataCell(1, 3).element.innerText).eql('Response Name 2 for first AI column'); + await expect(treeList.getDataCell(2, 3).element.innerText).eql('Response Name 3 for first AI column'); + + }); + + test('Clear Data from AI column by DropDownButton menu', async ({ page }) => { + await createWidget(page, 'dxTreeList', () => ({ + dataSource: [ + { + id: 1, parentId: 0, name: 'Name 1', value: 10, + }, + { + id: 2, parentId: 1, name: 'Name 2', value: 20, + }, + { + id: 3, parentId: 1, name: 'Name 3', value: 30, + }, + ], + keyExpr: 'id', + parentIdExpr: 'parentId', + autoExpandAll: true, + columns: [ + { dataField: 'id', caption: 'ID' }, + { dataField: 'name', caption: 'Name' }, + { dataField: 'value', caption: 'Value' }, + { + type: 'ai', + caption: 'AI Column', + name: 'AI Column', + ai: { + prompt: 'first AI column', + aiIntegration: new (window as any).DevExpress.aiIntegration({ + sendRequest(prompt) { + return { + promise: new Promise((resolve) => { + const result: Record = {}; + + Object.entries(prompt.data?.data).forEach(([key, value]) => { + result[key] = `Response ${(value as any).name} for ${prompt.data?.text}`; + }); + + resolve(JSON.stringify(result)); + }), + abort: (): void => {}, + }; + }, + }), + }, + }, + ], + })); + + // arrange, act + const treeList = new TreeList(TREE_LIST_SELECTOR); + + await expect(treeList.isReady()).ok(); + // assert + await expect(treeList.getDataCell(0, 3).element.innerText).eql('Response Name 1 for first AI column'); + await expect(treeList.getDataCell(1, 3).element.innerText).eql('Response Name 2 for first AI column'); + await expect(treeList.getDataCell(2, 3).element.innerText).eql('Response Name 3 for first AI column'); + const aiColumnHeaderCell = treeList.getHeaders().getHeaderRow(0).getHeaderCell(3); + + await aiColumnHeaderCell.getAIHeaderButton().element.click(); + const dropDownList = await aiColumnHeaderCell.getAIHeaderButton().getList(); + await dropDownList.getItem(DROPDOWNMENU_CLEAR_DATA_INDEX).element.click(); + + // assert + await expect(treeList.getDataCell(0, 3).element.innerText).eql(EMPTY_CELL_TEXT); + await expect(treeList.getDataCell(1, 3).element.innerText).eql(EMPTY_CELL_TEXT); + await expect(treeList.getDataCell(2, 3).element.innerText).eql(EMPTY_CELL_TEXT); + + }); + + test('Abort the AI request from Prompt Editor', async ({ page }) => { + await createWidget(page, 'dxTreeList', () => ({ + dataSource: [ + { + id: 1, parentId: 0, name: 'Name 1', value: 10, + }, + { + id: 2, parentId: 1, name: 'Name 2', value: 20, + }, + { + id: 3, parentId: 1, name: 'Name 3', value: 30, + }, + ], + keyExpr: 'id', + parentIdExpr: 'parentId', + autoExpandAll: true, + columns: [ + { dataField: 'id', caption: 'ID' }, + { dataField: 'name', caption: 'Name' }, + { dataField: 'value', caption: 'Value' }, + { + type: 'ai', + caption: 'AI Column', + name: 'AI Column', + ai: { + prompt: 'first AI column', + mode: 'manual', + aiIntegration: new (window as any).DevExpress.aiIntegration({ + sendRequest(prompt) { + return { + promise: new Promise((resolve) => { + const result: Record = {}; + + Object.entries(prompt.data?.data).forEach(([key, value]) => { + result[key] = `Response ${(value as any).name} for ${prompt.data?.text}`; + }); + + setTimeout(() => { + resolve(JSON.stringify(result)); + }, 3000); + }), + abort: (): void => {}, + }; + }, + }), + }, + }, + ], + })); + + const treeList = new TreeList(TREE_LIST_SELECTOR); + + await expect(treeList.isReady()).ok(); + + await expect(treeList.getDataCell(0, 3).element.innerText).eql(EMPTY_CELL_TEXT); + await expect(treeList.getDataCell(1, 3).element.innerText).eql(EMPTY_CELL_TEXT); + await expect(treeList.getDataCell(2, 3).element.innerText).eql(EMPTY_CELL_TEXT); + + const aiColumnHeaderCell = treeList.getHeaders().getHeaderRow(0).getHeaderCell(3); + + await aiColumnHeaderCell.getAIHeaderButton().element.click(); + const dropDownList = await aiColumnHeaderCell.getAIHeaderButton().getList(); + await dropDownList.getItem(DROPDOWNMENU_PROMPT_EDITOR_INDEX).element.click(); + + const promptEditor = treeList.getAIPromptEditor(); + + await promptEditor.getRegenerateButton().element.click() + .click(promptEditor.getStopButton().element); + + await expect(treeList.getDataCell(0, 3).element.innerText).eql(EMPTY_CELL_TEXT); + await expect(treeList.getDataCell(1, 3).element.innerText).eql(EMPTY_CELL_TEXT); + await expect(treeList.getDataCell(2, 3).element.innerText).eql(EMPTY_CELL_TEXT); + + }); + + test('Change the prompt in the AI Prompt Editor', async ({ page }) => { + await createWidget(page, 'dxTreeList', () => ({ + dataSource: [ + { + id: 1, parentId: 0, name: 'Name 1', value: 10, + }, + { + id: 2, parentId: 1, name: 'Name 2', value: 20, + }, + { + id: 3, parentId: 1, name: 'Name 3', value: 30, + }, + ], + keyExpr: 'id', + parentIdExpr: 'parentId', + autoExpandAll: true, + columns: [ + { dataField: 'id', caption: 'ID' }, + { dataField: 'name', caption: 'Name' }, + { dataField: 'value', caption: 'Value' }, + { + type: 'ai', + caption: 'AI Column', + name: 'AI Column', + ai: { + prompt: 'first AI column', + aiIntegration: new (window as any).DevExpress.aiIntegration({ + sendRequest(prompt) { + return { + promise: new Promise((resolve) => { + const result: Record = {}; + + Object.entries(prompt.data?.data).forEach(([key, value]) => { + result[key] = `Response ${(value as any).name} for ${prompt.data?.text}`; + }); + + resolve(JSON.stringify(result)); + }), + abort: (): void => {}, + }; + }, + }), + }, + }, + ], + })); + + const treeList = new TreeList(TREE_LIST_SELECTOR); + + await expect(treeList.isReady()).ok(); + await expect(treeList.getDataCell(0, 3).element.innerText).eql('Response Name 1 for first AI column'); + await expect(treeList.getDataCell(1, 3).element.innerText).eql('Response Name 2 for first AI column'); + await expect(treeList.getDataCell(2, 3).element.innerText).eql('Response Name 3 for first AI column'); + + const aiColumnHeaderCell = treeList.getHeaders().getHeaderRow(0).getHeaderCell(3); + + await aiColumnHeaderCell.getAIHeaderButton().element.click(); + const dropDownList = await aiColumnHeaderCell.getAIHeaderButton().getList(); + await dropDownList.getItem(DROPDOWNMENU_PROMPT_EDITOR_INDEX).element.click(); + + const promptEditor = treeList.getAIPromptEditor(); + + await promptEditor.getTextArea().element.fill('changed prompt') + .click(promptEditor.getApplyButton().element); + + await expect(treeList.getDataCell(0, 3).element.innerText).eql('Response Name 1 for changed prompt'); + await expect(treeList.getDataCell(1, 3).element.innerText).eql('Response Name 2 for changed prompt'); + await expect(treeList.getDataCell(2, 3).element.innerText).eql('Response Name 3 for changed prompt'); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/common/treeList/aiColumn/visual.spec.ts b/e2e/testcafe-devextreme/playwright-tests/common/treeList/aiColumn/visual.spec.ts new file mode 100644 index 000000000000..d466c5a4114e --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/common/treeList/aiColumn/visual.spec.ts @@ -0,0 +1,103 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('Ai Column.Visual', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + const TREE_LIST_SELECTOR = '#container'; + + test('Default render', async ({ page }) => { + await createWidget(page, 'dxTreeList', { + dataSource: [ + { + id: 1, parentId: 0, name: 'Name 1', value: 10, + }, + { + id: 2, parentId: 1, name: 'Name 2', value: 20, + }, + { + id: 3, parentId: 0, name: 'Name 3', value: 30, + }, + { + id: 4, parentId: 3, name: 'Name 4', value: 40, + }, + ], + keyExpr: 'id', + parentIdExpr: 'parentId', + expandedRowKeys: [3], + columns: [ + { dataField: 'id', caption: 'ID' }, + { dataField: 'name', caption: 'Name' }, + { dataField: 'value', caption: 'Value' }, + { + type: 'ai', + caption: 'AI Column', + }, + ], + }); + + // arrange, act + const treeList = new TreeList(TREE_LIST_SELECTOR); + + await expect(treeList.isReady()).ok(); + + await testScreenshot(page, 'treelist__ai-column__default.png', { element: treeList.element }); + + // assert + + }); + + test('AI Column when multiple selection is enabled', async ({ page }) => { + await createWidget(page, 'dxTreeList', { + dataSource: [ + { + id: 1, parentId: 0, name: 'Name 1', value: 10, + }, + { + id: 2, parentId: 1, name: 'Name 2', value: 20, + }, + { + id: 3, parentId: 0, name: 'Name 3', value: 30, + }, + { + id: 4, parentId: 3, name: 'Name 4', value: 40, + }, + ], + keyExpr: 'id', + parentIdExpr: 'parentId', + expandedRowKeys: [3], + selection: { + mode: 'multiple', + }, + columns: [ + { + type: 'ai', + caption: 'AI Column', + }, + { dataField: 'id', caption: 'ID' }, + { dataField: 'name', caption: 'Name' }, + { dataField: 'value', caption: 'Value' }, + ], + }); + + // arrange, act + const treeList = new TreeList(TREE_LIST_SELECTOR); + + await expect(treeList.isReady()).ok(); + + await testScreenshot(page, 'treelist__ai-column__multiple-selection.png', { element: treeList.element }); + + // assert + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/common/treeList/columns.spec.ts b/e2e/testcafe-devextreme/playwright-tests/common/treeList/columns.spec.ts new file mode 100644 index 000000000000..a8bf3fe2a0eb --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/common/treeList/columns.spec.ts @@ -0,0 +1,93 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('Columns', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + // T1054312 + test('CheckBox position with double rows columns', async ({ page }) => { + await createWidget(page, 'dxTreeList', { + dataSource: [{ + ID: 1, + Full_Name: 'John Heart', + City: 'Los Angeles', + State: 'California', + }], + keyExpr: 'ID', + selection: { + mode: 'multiple', + }, + columns: [{ + dataField: 'Full_Name', + }, + { columns: ['City', 'State'] }, + ], + }); + + const treeList = page.locator('#container'); + + await testScreenshot(page, 'T1054312', { element: treeList.getHeaders().element }); + + }); + + // T1053931 + test('Correct display border to last column', async ({ page }) => { + await createWidget(page, 'dxTreeList', { + dataSource: [ + { + ID: 1, + Country: 'Brazil', + Area: 8515767, + Population_Urban: 0.85, + Population_Total: 205809000, + GDP_Agriculture: 0.054, + GDP_Industry: 0.274, + GDP_Services: 0.672, + GDP_Total: 2353025, + }, + ], + keyExpr: 'ID', + columns: [ + 'Country', + { + columns: [{ + dataField: 'GDP_Total', + }, { + columns: [{ + dataField: 'GDP_Agriculture', + }, { + dataField: 'GDP_Industry', + }, { + dataField: 'GDP_Services', + }], + }], + }, { + columns: [{ + dataField: 'Population_Total', + }, { + dataField: 'Population_Urban', + }], + }, { + dataField: 'Area', + }, + ], + width: 600, + height: 300, + }); + + const treeList = page.locator('#container'); + + await testScreenshot(page, 'T1053931', { element: treeList.getHeaders().element }); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/common/treeList/editing/editing.spec.ts b/e2e/testcafe-devextreme/playwright-tests/common/treeList/editing/editing.spec.ts new file mode 100644 index 000000000000..940a3348dfe0 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/common/treeList/editing/editing.spec.ts @@ -0,0 +1,72 @@ +import { test, expect } from '@playwright/test'; +import { createWidget } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('Treelist - Editing', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + // T1247158 + test('TreeList - Insertafterkey doesn\'t work on children nodes', async ({ page }) => { + await createWidget(page, 'dxTreeList', { + dataSource: [ + { + ID: 1, + Head_ID: -1, + Full_Name: 'John Heart', + }, + { + ID: 2, + Head_ID: 1, + Full_Name: 'Samantha Bright', + }, + ], + rootValue: -1, + keyExpr: 'ID', + parentIdExpr: 'Head_ID', + columns: ['Full_Name'], + editing: { + mode: 'batch', + allowAdding: true, + allowUpdating: true, + useIcons: true, + }, + focusedRowEnabled: true, + expandedRowKeys: [1], + onKeyDown(e) { + if (e.event.ctrlKey && e.event.key === 'Enter') { + const currentSelectedParentTaskId = e.component.getNodeByKey( + e.component.option('focusedRowKey'), + )?.parent?.key; + const key = new (window as any).DevExpress.data.Guid().toString(); + const data = { Head_ID: currentSelectedParentTaskId }; + e.component.option('editing.changes', [ + { + key, + type: 'insert', + insertAfterKey: e.component.option('focusedRowKey'), + data, + }, + ]); + } + }, + }); + + const treeList = page.locator('#container'); + const expectedInsertedRowIndex = 2; + + await treeList.getDataCell(1, 0).element.click() + .pressKey('ctrl+enter') + .expect(treeList.getDataRow(expectedInsertedRowIndex).isInserted) + .ok(); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/common/treeList/focus.spec.ts b/e2e/testcafe-devextreme/playwright-tests/common/treeList/focus.spec.ts new file mode 100644 index 000000000000..a69613ae909c --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/common/treeList/focus.spec.ts @@ -0,0 +1,79 @@ +import { test, expect } from '@playwright/test'; +import { createWidget } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('Focus', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + const TREE_LIST_SELECTOR = '#container'; + + // T1294363 + test('Focus method should focus the first data cell', async ({ page }) => { + await createWidget(page, 'dxTreeList', { + dataSource: [ + { id: 1, parentId: 0, name: 'name 1' }, + { id: 2, parentId: 1, name: 'name 2' }, + { id: 3, parentId: 0, name: 'name 3' }, + ], + keyExpr: 'id', + parentId: 'parentId', + columns: [ + 'id', + { + dataField: 'name', + cellTemplate: (_, options) => $('
').attr('tabindex', 0).text(options.text), + }, + ], + }); + + const treeList = new TreeList(TREE_LIST_SELECTOR); + + await expect(treeList.isReady()).ok(); + + await treeList.apiFocus(); + + await page.expect(treeList.getDataCell(0, 0).element.focused) + .ok(); + + }); + + // T1294363 + test('Focus method should focus the first data row when focusedRowEnabled = true', async ({ page }) => { + await createWidget(page, 'dxTreeList', { + dataSource: [ + { id: 1, parentId: 0, name: 'name 1' }, + { id: 2, parentId: 1, name: 'name 2' }, + { id: 3, parentId: 0, name: 'name 3' }, + ], + keyExpr: 'id', + parentId: 'parentId', + focusedRowEnabled: true, + columns: [ + 'id', + { + dataField: 'name', + cellTemplate: (_, options) => $('
').attr('tabindex', 0).text(options.text), + }, + ], + }); + + const treeList = new TreeList(TREE_LIST_SELECTOR); + + await expect(treeList.isReady()).ok(); + + await treeList.apiFocus(); + + await page.expect(treeList.getDataRow(0).element.focused) + .ok(); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/common/treeList/focusedRow.spec.ts b/e2e/testcafe-devextreme/playwright-tests/common/treeList/focusedRow.spec.ts new file mode 100644 index 000000000000..fe15b1b9a5bd --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/common/treeList/focusedRow.spec.ts @@ -0,0 +1,119 @@ +import { test, expect } from '@playwright/test'; +import { createWidget } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('Focused row', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + const clearLocalStorage = async () => page.evaluate(() => { + (window as any).localStorage.removeItem('mystate'); + }); + + const getItems = (): Record[] => { + const items: Record[] = []; + for (let i = 0; i < 100; i += 1) { + items.push({ + ID: i + 1, + Name: `Name ${i + 1}`, + }); + } + return items; + }; + + const getTreeListConfig = (): any => ({ + dataSource: getItems(), + keyExpr: 'ID', + height: 500, + stateStoring: { + enabled: true, + type: 'custom', + customSave: (state) => { + localStorage.setItem('mystate', JSON.stringify(state)); + }, + customLoad: () => { + let state = localStorage.getItem('mystate'); + if (state) { + state = JSON.parse(state); + } + return state; + }, + }, + focusedRowEnabled: true, + focusedRowKey: 90, + }); + + test('Focused row should be shown after reloading the page (T1058983)', async ({ page }) => { + + await clearLocalStorage(); + await createWidget(page, 'dxTreeList', getTreeListConfig()); + + const treeList = page.locator('#container'); + + await page.waitForTimeout(1000); + let scrollTopPosition = await treeList.getScrollTop(); + + // assert + await page.expect(treeList.isFocusedRowInViewport()) + .ok(); + + // act + await treeList.scrollTo(t, { top: 0 }); + scrollTopPosition = await treeList.getScrollTop(); + + // assert + expect(scrollTopPosition).toBe(0); + + await page.eval(() => location.reload()); + await createWidget(page, 'dxTreeList', getTreeListConfig()); + await page.waitForTimeout(1000); + + scrollTopPosition = await treeList.getScrollTop(); + + // assert + await page.expect(treeList.isFocusedRowInViewport()) + .ok(); + + }); + + test('TreeList - Unable to focus a node when deleting the previous node in certain scenarios (T1178893)', async ({ page }) => { + + await clearLocalStorage(); + const config = getTreeListConfig(); + config.editing = { + mode: 'row', + allowUpdating: true, + allowAdding: true, + allowDeleting: true, + }; + config.focusedRowKey = 3; + + await createWidget(page, 'dxTreeList', config); + + const treeList = page.locator('#container'); + + await page.expect(treeList.getFocusedRow().getAttribute('aria-rowindex')) + .eql('3') + + .click(treeList.getDataRow(2).getCommandCell(2).getButton(2)) + .click(treeList.getConfirmDeletionButton()) + .expect(treeList.getFocusedRow().getAttribute('aria-rowindex')) + .eql('3') + + .click(treeList.getDataRow(2).getCommandCell(2).getButton(2)) + .click(treeList.getConfirmDeletionButton()) + .expect(treeList.getFocusedRow().getAttribute('aria-rowindex')) + .eql('3') + .expect(treeList.getDataRow(2).getDataCell(0).element.textContent) + .eql('5'); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/common/treeList/keyboardNavigation/customButtons.functional.spec.ts b/e2e/testcafe-devextreme/playwright-tests/common/treeList/keyboardNavigation/customButtons.functional.spec.ts new file mode 100644 index 000000000000..a8a9f3c48e70 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/common/treeList/keyboardNavigation/customButtons.functional.spec.ts @@ -0,0 +1,146 @@ +import { test, expect } from '@playwright/test'; +import { createWidget } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('Tests', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + fixture + .disablePageReloads`Keyboard Navigation - custom buttons` + .page(url(__dirname, '../../../container.html')); + + const TREE_LIST_SELECTOR = '#container'; + const createTreeList = async () => createWidget(page, 'dxTreeList', { + dataSource: [ + { + id: 1, + parentId: 0, + columnA: 'A_0', + columnB: 'B_0', + }, + { + id: 2, + parentId: 0, + columnA: 'A_1', + columnB: 'B_1', + }, + { + id: 3, + parentId: 0, + columnA: 'A_2', + columnB: 'B_2', + }, + ], + keyExpr: 'id', + parentIdExpr: 'parentId', + columns: [ + { + type: 'buttons', + buttons: [ + { + hint: 'button_1', + icon: 'edit', + onClick: (e) => $(e.event.target).attr('has-been-clicked', 'true'), + }, + { + hint: 'button_2', + icon: 'remove', + }, + ], + }, + 'id', + 'columnA', + 'columnB', + ], + sorting: { + mode: 'none', + }, + }); + + test('Custom buttons cell should be focused before custom buttons on tab navigation', async ({ page }) => { + await createTreeList(); + + const treeList = new TreeList(TREE_LIST_SELECTOR); + const expectedFocusedCell = treeList.getDataCell(0, 0); + const cellToStartNavigation = treeList.getHeaders().getHeaderRow(0).getHeaderCell(3); + + await cellToStartNavigation.click() + .pressKey('tab') + .expect(expectedFocusedCell.isFocused) + .ok(); + + }); + + test('Custom buttons cell should be focused after custom buttons on shift+tab reverse navigation', async ({ page }) => { + await createTreeList(); + + const treeList = new TreeList(TREE_LIST_SELECTOR); + const expectedFocusedCell = treeList.getDataCell(0, 0); + const cellToStartNavigation = treeList.getDataCell(0, 1); + + await cellToStartNavigation.click() + .pressKey('shift+tab') + .pressKey('shift+tab') + .pressKey('shift+tab') + .expect(expectedFocusedCell.isFocused) + .ok(); + + }); + + test('First custom button inside custom buttons cell should be focused on tab navigation', async ({ page }) => { + await createTreeList(); + + const treeList = new TreeList(TREE_LIST_SELECTOR); + const customButtonsCell = treeList.getDataCell(0, 0); + const expectedFocusedButton = customButtonsCell.getIconByTitle('button_1'); + const cellToStartNavigation = treeList.getHeaders().getHeaderRow(0).getHeaderCell(3); + + await cellToStartNavigation.click() + .pressKey('tab') + .pressKey('tab') + .expect(expectedFocusedButton.focused) + .ok(); + + }); + + test('Last custom button inside custom buttons cell should be focused on shift+tab reverse navigation', async ({ page }) => { + await createTreeList(); + + const treeList = new TreeList(TREE_LIST_SELECTOR); + const customButtonsCell = treeList.getDataCell(0, 0); + const expectedFocusedButton = customButtonsCell.getIconByTitle('button_2'); + const cellToStartNavigation = treeList.getDataCell(0, 1); + + await cellToStartNavigation.click() + .pressKey('shift+tab') + .expect(expectedFocusedButton.focused) + .ok(); + + }); + + test('Custom button inside custom buttons cell should be clickable by pressing enter key', async ({ page }) => { + await createTreeList(); + + const treeList = new TreeList(TREE_LIST_SELECTOR); + const customButtonsCell = treeList.getDataCell(0, 0); + const expectedFocusedButton = customButtonsCell.getIconByTitle('button_1'); + const cellToStartNavigation = treeList.getHeaders().getHeaderRow(0).getHeaderCell(3); + + await cellToStartNavigation.click() + .pressKey('tab') + .pressKey('tab') + .pressKey('enter') + .expect(expectedFocusedButton.withAttribute('has-been-clicked').exists) + .ok(); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/common/treeList/keyboardNavigation/keyboardNavigation.functional.spec.ts b/e2e/testcafe-devextreme/playwright-tests/common/treeList/keyboardNavigation/keyboardNavigation.functional.spec.ts new file mode 100644 index 000000000000..9ad26fad6606 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/common/treeList/keyboardNavigation/keyboardNavigation.functional.spec.ts @@ -0,0 +1,152 @@ +import { test, expect } from '@playwright/test'; +import { createWidget } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('Keyboard Navigation - common', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('TreeList - Selection CheckBox in a data row isn\'t navigable with Tab button if this CheckBox was focused manually (T1207467)', async ({ page }) => { + + await createWidget(page, 'dxTreeList', { + dataSource: [ + { + id: 1, parentId: 0, name: 'Name 1', age: 19, + }, + { + id: 2, parentId: 1, name: 'Name 2', age: 11, + }, + { + id: 3, parentId: 0, name: 'Name 3', age: 15, + }, + ], + keyExpr: 'id', + parentIdExpr: 'parentId', + showBorders: true, + selection: { + mode: 'multiple', + recursive: false, + }, + columns: ['id', 'name', 'age'], + }); + + await createWidget(page, 'dxButton', { + text: 'Focus', + onClick() { + const checkbox = $('.dx-checkbox:visible')[1]; + if (checkbox) { + checkbox.focus(); + } + }, + }, '#otherContainer'); + + const treeList = page.locator('#container'); + const focusButton = page.locator('#otherContainer'); + const expectedFocusedCell = treeList.getDataCell(0, 2); + + await focusButton.click() + .pressKey('tab tab') + .expect(expectedFocusedCell.isFocused) + .ok(); + + }); + + test('TreeList - Template button in a data row isn\'t navigable with Tab button if this button was focused manually (T1207467)', async ({ page }) => { + + await createWidget(page, 'dxTreeList', { + dataSource: [ + { + id: 1, parentId: 0, name: 'Name 1', age: 19, + }, + { + id: 2, parentId: 1, name: 'Name 2', age: 11, + }, + { + id: 3, parentId: 0, name: 'Name 3', age: 15, + }, + ], + keyExpr: 'id', + parentIdExpr: 'parentId', + showBorders: true, + selection: { + mode: 'multiple', + recursive: false, + }, + columns: [{ + dataField: 'id', + }, { + dataField: 'name', + cellTemplate(container) { + const button = document.createElement('button'); + button.innerText = 'select'; + container.append(button); + }, + }, 'age'], + }); + + await createWidget(page, 'dxButton', { + text: 'Focus', + onClick() { + const btn = $('button')[0]; + if (btn) { + btn.focus(); + } + }, + }, '#otherContainer'); + + const treeList = page.locator('#container'); + const focusButton = page.locator('#otherContainer'); + const expectedFocusedCell = treeList.getDataCell(0, 2); + + await focusButton.click() + .pressKey('tab') + .expect(expectedFocusedCell.isFocused) + .ok(); + + }); + + test('TreeList - Keyboard navigation on Expand/Collapse buttons is broken if the mouse used before (T1234949)', async ({ page }) => { + await createWidget(page, 'dxTreeList', { + dataSource: [ + { + Task_ID: 1, + Task_Subject: 'Plans 2015', + Task_Parent_ID: 0, + }, + { + Task_ID: 2, + Task_Subject: 'Health Insurance', + Task_Parent_ID: 1, + }, + ], + keyExpr: 'Task_ID', + parentIdExpr: 'Task_Parent_ID', + columns: [ + { + dataField: 'Task_Subject', + }, + { + dataField: 'Task_Assigned_Employee_ID', + }, + ], + }),; + + const treeList = page.locator('#container'); + const target = treeList.getDataRow(0).getDataCell(0); + + await page.click(treeList.getDataRow(0).getDataCell(0).element.child(0)) + .click(treeList.getContainer(), { offsetX: 100, offsetY: 600 }) + .pressKey('tab tab tab') + .expect(target.element.focused) + .ok(); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/common/treeList/keyboardNavigation/markup.screenshots.spec.ts b/e2e/testcafe-devextreme/playwright-tests/common/treeList/keyboardNavigation/markup.screenshots.spec.ts new file mode 100644 index 000000000000..8172dd1d2cda --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/common/treeList/keyboardNavigation/markup.screenshots.spec.ts @@ -0,0 +1,125 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('Tests', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + fixture + .disablePageReloads`Keyboard Navigation - screenshots` + .page(url(__dirname, '../../../container.html')); + + const TREE_LIST_SELECTOR = '#container'; + + test('Focused cells should look correctly', async ({ page }) => { + await createWidget(page, 'dxTreeList', { + dataSource: [ + { + id: 1, + parentId: 0, + columnA: 'A_0', + columnB: 'B_0', + }, + { + id: 2, + parentId: 0, + columnA: 'A_1', + columnB: 'B_1', + }, + { + id: 3, + parentId: 0, + columnA: 'A_2', + columnB: 'B_2', + }, + ], + keyExpr: 'id', + parentIdExpr: 'parentId', + columns: ['id', 'columnA', 'columnB'], + sorting: { + mode: 'none', + }, + }); + + const treeList = new TreeList(TREE_LIST_SELECTOR); + const headerCellToFocus = treeList.getHeaders().getHeaderRow(0).getHeaderCell(0); + const dataCellToFocus = treeList.getDataCell(0, 0); + + await headerCellToFocus.click() + .pressKey('tab'); + await testScreenshot(page, 'tree-list_keyboard-navigation-header-cell-focused.png', { element: treeList.element }); + + await dataCellToFocus.click() + .pressKey('tab'); + await testScreenshot(page, 'tree_list_keyboard-navigation-data-cell-focused.png', { element: treeList.element }); + + }); + + test('Focused custom buttons should look correctly', async ({ page }) => { + await createWidget(page, 'dxTreeList', { + dataSource: [ + { + id: 1, + parentId: 0, + columnA: 'A_0', + columnB: 'B_0', + }, + { + id: 2, + parentId: 0, + columnA: 'A_1', + columnB: 'B_1', + }, + { + id: 3, + parentId: 0, + columnA: 'A_2', + columnB: 'B_2', + }, + ], + keyExpr: 'id', + parentIdExpr: 'parentId', + columns: [ + { + type: 'buttons', + buttons: [ + { + hint: 'button_1', + icon: 'edit', + }, + { + hint: 'button_2', + icon: 'remove', + }, + ], + }, + 'id', + 'columnA', + 'columnB', + ], + sorting: { + mode: 'none', + }, + }); + + const treeList = new TreeList(TREE_LIST_SELECTOR); + const headerCellToFocus = treeList.getHeaders().getHeaderRow(0).getHeaderCell(3); + + await headerCellToFocus.click() + .pressKey('tab'); + await testScreenshot(page, 'tree-list_keyboard-navigation-custom-buttons-header-cell-focused.png', { element: treeList.element }); + + await page.keyboard.press('Tab'); + await testScreenshot(page, 'tree-list_keyboard-navigation-custom-button-focused.png', { element: treeList.element }); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/common/treeList/keyboardNavigation/onClick.functional.spec.ts b/e2e/testcafe-devextreme/playwright-tests/common/treeList/keyboardNavigation/onClick.functional.spec.ts new file mode 100644 index 000000000000..f6b9549bcc55 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/common/treeList/keyboardNavigation/onClick.functional.spec.ts @@ -0,0 +1,65 @@ +import { test, expect } from '@playwright/test'; +import { createWidget } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('Tests', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + fixture + .disablePageReloads`Keyboard Navigation - focus on click` + .page(url(__dirname, '../../../container.html')); + + // T861048 + test('The row should be selected on click if less than half of a row is visible', async ({ page }) => { + await createWidget(page, 'dxTreeList', { + dataSource: [ + { + id: 1, parentId: 0, name: 'Name 1', age: 19, + }, + { + id: 2, parentId: 1, name: 'Name 2', age: 11, + }, + { + id: 3, parentId: 0, name: 'Name 3', age: 15, + }, + { + id: 4, parentId: 3, name: 'Name 4', age: 16, + }, + { + id: 5, parentId: 0, name: 'Name 5', age: 25, + }, + { + id: 6, parentId: 5, name: 'Name 6', age: 18, + }, + { + id: 7, parentId: 0, name: 'Name 7', age: 21, + }, + { + id: 8, parentId: 7, name: 'Name 8', age: 14, + }, + ], + height: 150, + autoExpandAll: true, + columns: ['name', 'age'], + selection: { + mode: 'multiple', + }, + }); + + const treeList = page.locator('#container'); + const dataRow = treeList.getDataRow(3); + + await page.click(dataRow.getSelectCheckBox(), { offsetX: 0, offsetY: 0 }) + .expect(dataRow.isSelected).ok(); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/common/treeList/keyboardNavigation/skipDragCell.functional.spec.ts b/e2e/testcafe-devextreme/playwright-tests/common/treeList/keyboardNavigation/skipDragCell.functional.spec.ts new file mode 100644 index 000000000000..ee71e66879e9 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/common/treeList/keyboardNavigation/skipDragCell.functional.spec.ts @@ -0,0 +1,150 @@ +import { test, expect } from '@playwright/test'; +import { createWidget } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('Tests', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + // T1147695 + fixture + .disablePageReloads`Keyboard Navigation - skip drag cell` + .page(url(__dirname, '../../../container.html')); + + const TREE_LIST_SELECTOR = '#container'; + const DATA_SOURCE = [ + { + id: 1, + parentId: 0, + columnA: 'A_0', + columnB: 'B_0', + }, + { + id: 2, + parentId: 0, + columnA: 'A_1', + columnB: 'B_1', + }, + { + id: 3, + parentId: 0, + columnA: 'A_2', + columnB: 'B_2', + }, + ]; + + const createTreeList = async () => createWidget(page, 'dxTreeList', { + dataSource: DATA_SOURCE, + keyExpr: 'id', + parentIdExpr: 'parentId', + columns: ['id', 'columnA', 'columnB'], + rowDragging: { + allowReordering: true, + }, + sorting: { + mode: 'none', + }, + }); + + const createTreeListRenderAsyncWithButtons = async () => createWidget(page, 'dxTreeList', { + dataSource: DATA_SOURCE, + keyExpr: 'id', + parentIdExpr: 'parentId', + columns: ['id', 'columnA', 'columnB', { type: 'buttons' }], + rowDragging: { + allowReordering: true, + }, + sorting: { + mode: 'none', + }, + renderAsync: true, + }); + + test('The drag cell should be skipped when navigating from the header cell by tab keypress', async ({ page }) => { + await createTreeList(); + + const treeList = new TreeList(TREE_LIST_SELECTOR); + const expectedFocusedCell = treeList.getDataCell(0, 1); + const cellToStartNavigation = treeList.getHeaders().getHeaderRow(0).getHeaderCell(3); + + await cellToStartNavigation.click() + .pressKey('tab') + .expect(expectedFocusedCell.isFocused) + .ok(); + + }); + + test('The drag cell should be skipped when navigating from the header cell by tab keypress' + + ' with buttons column and renderAsync: true', async ({ page }) => { + await createTreeListRenderAsyncWithButtons(); + + const treeList = new TreeList(TREE_LIST_SELECTOR); + const expectedFocusedCell = treeList.getDataCell(0, 1); + const cellToStartNavigation = treeList.getHeaders().getHeaderRow(0).getHeaderCell(4); + + await cellToStartNavigation.click() + .pressKey('tab') + .expect(expectedFocusedCell.isFocused) + .ok(); + + }); + + test('The drag cell should be skipped when navigating to the header cell by shift+tab keypress', async ({ page }) => { + await createTreeList(); + + const treeList = new TreeList(TREE_LIST_SELECTOR); + const expectedFocusedCell = treeList.getHeaders().getHeaderRow(0).getHeaderCell(3); + const cellToStartNavigation = treeList.getDataCell(0, 1); + + await cellToStartNavigation.click() + .pressKey('shift+tab') + .expect(expectedFocusedCell.isFocused).ok(); + + }); + + test('The drag cell should be skipped when navigating to a next row by tab keypress', async ({ page }) => { + await createTreeList(); + + const treeList = new TreeList(TREE_LIST_SELECTOR); + const expectedFocusedCell = treeList.getDataCell(1, 1); + const cellToStartNavigation = treeList.getDataCell(0, 3); + + await cellToStartNavigation.click() + .pressKey('tab') + .expect(expectedFocusedCell.isFocused).ok(); + + }); + + test('The drag cell should be skipped when navigating to a previous row by shift+tab keypress', async ({ page }) => { + await createTreeList(); + + const treeList = new TreeList(TREE_LIST_SELECTOR); + const expectedFocusedCell = treeList.getDataCell(0, 3); + const cellToStartNavigation = treeList.getDataCell(1, 1); + + await cellToStartNavigation.click() + .pressKey('shift+tab') + .expect(expectedFocusedCell.isFocused).ok(); + + }); + + test('The drag cell shouldn\'t be focused when the next cell is focused and the left arrow key pressed', async ({ page }) => { + await createTreeList(); + + const treeList = new TreeList(TREE_LIST_SELECTOR); + const expectedFocusedCell = treeList.getDataCell(0, 1); + + await expectedFocusedCell.click() + .pressKey('left') + .expect(expectedFocusedCell.isFocused).ok(); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/common/treeList/markup.spec.ts b/e2e/testcafe-devextreme/playwright-tests/common/treeList/markup.spec.ts new file mode 100644 index 000000000000..a18c870f19d4 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/common/treeList/markup.spec.ts @@ -0,0 +1,272 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('TreeList - Markup', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + fixture.disablePageReloads`TreeList - Markup` + .disablePageReloads + .page(url(__dirname, '../../container.html')); + + const tasksT1223168 = [{ + Task_ID: 1, + Task_Subject: 'Plans 2015', + Task_Parent_ID: 0, + }, { + Task_ID: 2, + Task_Subject: 'Health Insurance', + Task_Parent_ID: 1, + }, { + Task_ID: 3, + Task_Subject: 'Training', + Task_Parent_ID: 2, + }]; + + test('TreeList - Expand/collapse buttons are too close to column borders if the first column is a boolean column (T1223168)', async ({ page }) => { + + await createWidget(page, 'dxTreeList', { + dataSource: tasksT1223168, + keyExpr: 'Task_ID', + parentIdExpr: 'Task_Parent_ID', + autoExpandAll: true, + wordWrapEnabled: true, + showBorders: true, + columns: [{ + dataField: 'test', + dataType: 'boolean', + }, 'Task_Subject'], + showColumnLines: true, + rowDragging: { + allowReordering: true, + }, + }); + + const treeList = page.locator('#container'); + + await testScreenshot(page, 'T1223168-expandable', { element: treeList.element }); + + }); + + // T1221037 + test('TreeList screenshot when the first cell has a template', async ({ page }) => { + await createWidget(page, 'dxTreeList', { + dataSource: [{ + ID: 1, + Head_ID: 0, + Full_Name: 'John Heart', + Prefix: 'Mr.', + Title: 'CEO', + City: 'Los Angeles', + State: 'California', + Email: 'jheart@dx-email.com', + Skype: 'jheart_DX_skype', + Mobile_Phone: '(213) 555-9392', + Birth_Date: '1964-03-16', + Hire_Date: '1995-01-15', + }, { + ID: 2, + Head_ID: 1, + Full_Name: 'Arthur Miller', + Prefix: 'Mr.', + Title: 'CTO', + City: 'Denver', + State: 'Colorado', + Email: 'arthurm@dx-email.com', + Skype: 'arthurm_DX_skype', + Mobile_Phone: '(310) 555-8583', + Birth_Date: '1972-07-11', + Hire_Date: '2007-12-18', + }, { + ID: 3, + Head_ID: 2, + Full_Name: 'Brett Wade', + Prefix: 'Mr.', + Title: 'IT Manager', + City: 'Reno', + State: 'Nevada', + Email: 'brettw@dx-email.com', + Skype: 'brettw_DX_skype', + Mobile_Phone: '(626) 555-0358', + Birth_Date: '1968-12-01', + Hire_Date: '2009-03-06', + }, { + ID: 4, + Head_ID: 3, + Full_Name: 'Morgan Kennedy', + Prefix: 'Mrs.', + Title: 'Graphic Designer', + City: 'San Fernando Valley', + State: 'California', + Email: 'morgank@dx-email.com', + Skype: 'morgank_DX_skype', + Mobile_Phone: '(818) 555-8238', + Birth_Date: '1984-07-17', + Hire_Date: '2012-01-11', + }, { + ID: 5, + Head_ID: 4, + Full_Name: 'Violet Bailey', + Prefix: 'Ms.', + Title: 'Jr Graphic Designer', + City: 'La Canada', + State: 'California', + Email: 'violetb@dx-email.com', + Skype: 'violetb_DX_skype', + Mobile_Phone: '(818) 555-2478', + Birth_Date: '1985-06-10', + Hire_Date: '2012-01-19', + }], + keyExpr: 'ID', + parentIdExpr: 'Head_ID', + columnAutoWidth: true, + width: 770, + columns: [{ + dataField: 'Title', + caption: 'Position', + cellTemplate(_, cellInfo) { + return $('
').append( + $('').text(cellInfo.data.Title), + + }, + }, 'Full_Name', 'City', 'State', { + dataField: 'Hire_Date', + dataType: 'date', + }], + showRowLines: true, + showBorders: true, + expandedRowKeys: [1, 2, 3, 4], + }); + + const treeList = page.locator('#container'); + + await expect(treeList.isReady()).ok(); + await testScreenshot(page, 'T1221037-cell-with-template', { element: treeList.element }); + + }); + + // T1291705 + test('The shading should alternate correctly after expanding the node when repaintChangesOnly is enabled', async ({ page }) => { + await createWidget(page, 'dxTreeList', { + dataSource: [ + { id: 1, parentId: 0, text: 'item 1' }, + { id: 2, parentId: 0, text: 'item 2' }, + { id: 3, parentId: 2, text: 'item 3' }, + { id: 4, parentId: 0, text: 'item 4' }, + { id: 5, parentId: 4, text: 'item 5' }, + { id: 6, parentId: 0, text: 'item 6' }, + ], + keyExpr: 'id', + parentIdExpr: 'parentId', + rowAlternationEnabled: true, + repaintChangesOnly: true, + }); + + const treeList = page.locator('#container'); + + await treeList.apiExpandRow(4); + await treeList.apiExpandRow(2); + + await testScreenshot(page, 'T1291705-row-alternation-after-expanding-node-when-repaintChangesOnly=true', { element: treeList.element }); + + }); + + test('The shading should alternate correctly after expanding the node when repaintChangesOnly and old fixed columns are enabled', async ({ page }) => { + await createWidget(page, 'dxTreeList', { + dataSource: [ + { id: 1, parentId: 0, text: 'item 1' }, + { id: 2, parentId: 0, text: 'item 2' }, + { id: 3, parentId: 2, text: 'item 3' }, + { id: 4, parentId: 0, text: 'item 4' }, + { id: 5, parentId: 4, text: 'item 5' }, + { id: 6, parentId: 0, text: 'item 6' }, + ], + keyExpr: 'id', + parentIdExpr: 'parentId', + rowAlternationEnabled: true, + repaintChangesOnly: true, + columnFixing: { + legacyMode: true, + }, + columns: [{ dataField: 'id', fixed: true }, 'text'], + }); + + const treeList = page.locator('#container'); + + await treeList.apiExpandRow(4); + await treeList.apiExpandRow(2); + + await testScreenshot(page, 'T1291705-row-alternation-after-expanding-node-when-there-is-fixed-column-and-repaintChangesOnly=true', { element: treeList.element }); + + }); + + ['single', 'multiple'].forEach((selectionMode) => { + ['single-line', 'multiple-line'].forEach((contentType) => { + [false, true].forEach((rtlEnabled) => { + test( + `Markup should be correct [T1291914 & T1294907]:selection=${selectionMode},content=${contentType},rtl=${rtlEnabled}`, + async ({ page }) => { + const treeList = page.locator('#container'); + + await testScreenshot(page, `markup-selection=${selectionMode}-rtl=${rtlEnabled}-content=${contentType}`, { element: treeList.element }); + + });, + ).before(async () => { + await createWidget(page, 'dxTreeList', { + dataSource: [ + { + id: 1, parentId: 0, first: 'Alice', last: 'Blue', age: 30, position: 'CEO', + }, + { + id: 2, parentId: 1, first: 'Bob', last: 'Brown', age: 25, position: 'CTO', + }, + { + id: 3, parentId: 1, first: 'Charlie', last: 'Green', age: 28, position: 'CFO', + }, + { + id: 4, parentId: 1, first: 'David', last: 'White', age: 22, position: 'Developer', + }, + { + id: 5, parentId: 3, first: 'Eve', last: 'Black', age: 26, position: 'Designer', + }, + ], + keyExpr: 'id', + parentIdExpr: 'parentId', + expandedRowKeys: [1, 2], + columns: [ + { + dataField: 'first', + cellTemplate: contentType === 'single-line' + ? undefined + : () => { + const div = document.createElement('div'); + div.innerText = 'Long text that should wrap into multiple lines. Long text that should wrap into multiple lines.'; + div.style.whiteSpace = 'break-spaces'; + + return div; + }, + }, + 'last', + 'age', + 'position', + ], + rtlEnabled, + selection: { + mode: selectionMode, + }, + selectedRowKeys: selectionMode === 'single' ? [3] : [3, 4], + }); + }); + }); + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/common/treeList/rowDragging.spec.ts b/e2e/testcafe-devextreme/playwright-tests/common/treeList/rowDragging.spec.ts new file mode 100644 index 000000000000..b830c44f784f --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/common/treeList/rowDragging.spec.ts @@ -0,0 +1,102 @@ +import { test, expect } from '@playwright/test'; +import { createWidget } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('Row dragging', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + const tasksT1228650 = [{ + Task_ID: 1, + Task_Subject: 'Plans 2015', + Task_Parent_ID: 0, + }, { + Task_ID: 2, + Task_Subject: 'Health Insurance', + Task_Parent_ID: 1, + }, { + Task_ID: 3, + Task_Subject: 'Training', + Task_Parent_ID: 2, + }]; + + test('TreeList - Expand/collapse mechanism breaks after dragging action in the space between the last row and the border (T1228650)', async ({ page }) => { + + await createWidget(page, 'dxTreeList', { + dataSource: tasksT1228650, + keyExpr: 'Task_ID', + parentIdExpr: 'Task_Parent_ID', + height: 200, + wordWrapEnabled: true, + showBorders: true, + columnFixing: { + legacyMode: true, + }, + columns: [ + { + dataField: 'test', + dataType: 'boolean', + }, + { + dataField: 'Task_Subject', + fixed: true, + fixedPosition: 'right', + }, + ], + showColumnLines: true, + rowDragging: { + allowDropInsideItem: true, + allowReordering: false, + showDragIcons: false, + group: 'none', + }, + }); + + const treeList = page.locator('#container'); + const dataRow = treeList.getDataRow(0); + const expandButton = new ExpandableCell(dataRow.getDataCell(0)).getExpandButton(); + const freeSpaceRow = treeList.getFreeSpaceRow(); + await page.dragToElement(freeSpaceRow, dataRow.element) + .click(expandButton) + .expect(treeList.getDataRow(1).element.exists) + .ok(); + + }); + + [undefined, 200].forEach((height) => { + test(`TreeList - The W1025 warning occurs when dragging a row (height: ${height ?? 'not set'}). (T1280519)`, async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + height, + scrolling: { + mode: 'virtual', + }, + dataSource: tasksT1228650, + rowDragging: { + allowReordering: true, + }, + }); + + const treeList = page.locator('#container'); + + await treeList.isReady(); + + await treeList.moveRow(0, 10, 10, true); + + await page.waitForTimeout(100); + + const consoleMessages = await getBrowserConsoleMessages(); + const warningExists = !!consoleMessages?.warn.find((message) => message.startsWith('W1025')); + + expect(warningExists).toBe(height === undefined); + + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/common/treeList/scrolling.spec.ts b/e2e/testcafe-devextreme/playwright-tests/common/treeList/scrolling.spec.ts new file mode 100644 index 000000000000..3f90e8d4dd65 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/common/treeList/scrolling.spec.ts @@ -0,0 +1,154 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('Virtual Scrolling', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + const scrollWindowTo = async (position: object) => { + await ClientFunction( + () => { + (window as any).scroll(position); + }, + { + dependencies: { + position, + }, + }, + )(); + }; + + function generateData(rowCount): Record[] { + const items: Record[] = []; + + for (let i = 0; i < rowCount; i += 1) { + items.push({ + ID: i, + Head_ID: -1, + Full_Name: 'Ken Samuelson Demo Demo Demo Demo Demo Demo Demo Demo Demo Demo', + Prefix: 'Dr. Demo Demo Demo Demo Demo Demo Demo Demo Demo Demo', + Title: 'Ombudsman Demo Demo Demo Demo Demo Demo Demo Demo Demo Demo', + City: 'St. Louis Demo Demo Demo Demo Demo Demo Demo Demo Demo Demo', + State: 'Missouri Demo Demo Demo Demo Demo Demo Demo Demo Demo Demo', + Email: 'kents@dx-email.com Demo Demo Demo Demo Demo Demo Demo Demo Demo Demo', + Skype: 'kents_DX_skype', + Mobile_Phone: '(562) 555-9282', + Birth_Date: '1972-09-11', + Hire_Date: '2009-04-22', + }); + } + + return items; + } + + // T1129106 + test('The vertical scroll bar of the container\'s parent should not be displayed when the grid has no height, virtual scrolling and state storing are enabled', async ({ page }) => { + // arrange, act + const treeList = page.locator('#container'); + + await expect(treeList.isReady()).ok(); + await testScreenshot(page, 'T1129106-treelist-virtual-scrolling-1'); + + // act + await scrollWindowTo({ top: 10000000 }); + + await expect(treeList.isReady()).ok(); + await testScreenshot(page, 'T1129106-treelist-virtual-scrolling-2'); + + // act + await scrollWindowTo({ top: 0 }); + + await expect(treeList.isReady()).ok(); + await testScreenshot(page, 'T1129106-treelist-virtual-scrolling-3'); + + // assert + + });.before(async ({ page }) => { + await page.evaluate(() => { + $('#container').wrap('
'); + }); + + await resizeWindow(550, 700); + + await createWidget(page, 'dxTreeList', { + dataSource: generateData(1000), + rootValue: -1, + columnMinWidth: 80, + wordWrapEnabled: true, + columnAutoWidth: true, + allowColumnResizing: true, + keyExpr: 'ID', + parentIdExpr: 'Head_ID', + showRowLines: true, + showBorders: true, + autoExpandAll: true, + scrolling: { + mode: 'virtual', + }, + stateStoring: { + enabled: true, + type: 'custom', + customSave: () => {}, + customLoad: () => ({ + pageIndex: 50, + }), + }, + columns: ['Title', 'Full_Name', 'City', 'State', 'Mobile_Phone', 'Hire_Date'], + }); + }); + + // T1189118 + test('All items should be selected after select all and scroll down', async ({ page }) => { + await createWidget(page, 'dxTreeList', { + dataSource: generateData(100), + height: 400, + rootValue: -1, + columnMinWidth: 80, + columnAutoWidth: true, + allowColumnResizing: true, + keyExpr: 'ID', + parentIdExpr: 'Head_ID', + showRowLines: true, + showBorders: true, + autoExpandAll: true, + scrolling: { + mode: 'virtual', + }, + selection: { + allowSelectAll: true, + mode: 'multiple', + }, + columns: ['Title', 'Full_Name', 'City'], + }); + + // arrange + const treeList = page.locator('#container'); + + // assert + await page.expect(treeList.isReady()) + .ok(); + + // act + const selectAllCheckBox = new CheckBox( + treeList.getHeaders().getHeaderRow(0).getHeaderCell(0).getEditor().element, + + await selectAllCheckBox.click(); + + await testScreenshot(page, 'T1189118-treelist-select-all-with-virtual-scrolling-1', { element: treeList.element }); + + // act + await treeList.scrollTo(t, { y: 300 }); + + await testScreenshot(page, 'T1189118-treelist-select-all-with-virtual-scrolling-2', { element: treeList.element }); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/common/treeList/searchPanel.spec.ts b/e2e/testcafe-devextreme/playwright-tests/common/treeList/searchPanel.spec.ts new file mode 100644 index 000000000000..e558fde1e6f2 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/common/treeList/searchPanel.spec.ts @@ -0,0 +1,83 @@ +import { test, expect } from '@playwright/test'; +import { createWidget } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('SearchPanel', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('Items are shown in the original order after search is applied - T1274434 - 1', async ({ page }) => { + await createWidget(page, 'dxTreeList', { + showBorders: true, + showRowLines: true, + expandedRowKeys: [1], + searchPanel: { + visible: true, + }, + columns: ['text'], + dataSource: [ + { id: 1, parentId: 0, text: 'parent1' }, + { id: 2, parentId: 0, text: 'test1' }, + { id: 3, parentId: 1, text: 'test2' }, + ], + }); + + const treeList = page.locator('#container'); + await treeList.apiSearchByText('test'); + + await page.expect((await treeList.apiGetVisibleRows()).length) + .eql(3); + + await page.expect(treeList.apiGetCellValue(0, 0)) + .eql('parent1'); + + await page.expect(treeList.apiGetCellValue(1, 0)) + .eql('test2'); + + await page.expect(treeList.apiGetCellValue(2, 0)) + .eql('test1'); + + }); + + test('Items are shown in the original order after search is applied - T1274434 - 2', async ({ page }) => { + await createWidget(page, 'dxTreeList', { + showBorders: true, + showRowLines: true, + expandedRowKeys: [1], + searchPanel: { + visible: true, + }, + columns: ['text'], + dataSource: [ + { id: 1, parentId: 0, text: 'parent1' }, + { id: 2, parentId: 0, text: 'test1' }, + { id: 3, parentId: 1, text: 'test2' }, + { id: 4, parentId: 0, text: 'parent2' }, + ], + }); + + const treeList = page.locator('#container'); + await treeList.apiSearchByText('test'); + + await page.expect((await treeList.apiGetVisibleRows()).length) + .eql(3); + + await page.expect(treeList.apiGetCellValue(0, 0)) + .eql('parent1'); + + await page.expect(treeList.apiGetCellValue(1, 0)) + .eql('test2'); + + await page.expect(treeList.apiGetCellValue(2, 0)) + .eql('test1'); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/common/treeList/selection.spec.ts b/e2e/testcafe-devextreme/playwright-tests/common/treeList/selection.spec.ts new file mode 100644 index 000000000000..bafccce69ea9 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/common/treeList/selection.spec.ts @@ -0,0 +1,134 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('Selection', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + // T1109666 + test('TreeList with selection and boolean data in first column should render right', async ({ page }) => { + await createWidget(page, 'dxTreeList', { + dataSource: [ + { + id: 1, parentId: 0, value: true, value1: 'text', + }, + { + id: 2, parentId: 1, value: true, value1: 'text', + }, + { + id: 3, parentId: 2, value: true, value1: 'text', + }, + { + id: 4, parentId: 3, value: true, value1: 'text', + }, + { + id: 5, parentId: 4, value: true, value1: 'text', + }, + { + id: 6, parentId: 5, value: true, value1: 'text', + }, + { + id: 7, parentId: 6, value: true, value1: 'text', + }, + { + id: 8, parentId: 7, value: true, value1: 'text', + }, + ], + height: 300, + width: 400, + autoExpandAll: true, + columns: [{ + dataField: 'value', + width: 100, + }, { + dataField: 'value1', + }], + selection: { + mode: 'multiple', + }, + }); + + const treeList = page.locator('#container'); + + await testScreenshot(page, 'T1109666-selection', { element: treeList.element }); + + }); + + // T1264312 + test('TreeList restore selection after the search panel has cleared', async ({ page }) => { + const treeList = page.locator('#container'); + const dataRow = treeList.getDataRow(0); + const expandableCell = new ExpandableCell(dataRow.getDataCell(0)); + const searchBox = treeList.getSearchBox(); + + await page.click(dataRow.getSelectCheckBox()) + .expect(dataRow.isSelected).ok(); + await page.click(expandableCell.getExpandButton()) + .expect(expandableCell.isExpanded()).ok(); + await testScreenshot(page, 'T1264312-selection-checked-all', { element: treeList.element }); + + await page.click(expandableCell.getCollapseButton()) + .typeText(searchBox.input, 'google') + .expect(expandableCell.isExpanded()).ok(); + await testScreenshot(page, 'T1264312-selection-checked-searched', { element: treeList.element }); + + await page.click(dataRow.getSelectCheckBox()) + .expect(dataRow.isSelected).notOk(); + await testScreenshot(page, 'T1264312-selection-unchecked-searched', { element: treeList.element }); + + await page.click(searchBox.getClearButton()) + .click(expandableCell.getExpandButton()) + .expect(expandableCell.isExpanded()).ok(); + await testScreenshot(page, 'T1264312-selection-unchecked-all.png', { element: treeList.element }); + + });.before(async ({ page }) => { + await addRequestHooks(tasksApiMock); + await createWidget(page, 'dxTreeList', () => ({ + dataSource: (window as any).DevExpress.data.AspNet.createStore({ + key: 'Task_ID', + loadUrl: 'https://api/data', + }), + selection: { mode: 'multiple', recursive: true, allowSelectAll: false }, + remoteOperations: { filtering: true, sorting: true, grouping: true }, + parentIdExpr: 'Task_Parent_ID', + hasItemsExpr: 'Has_Items', + searchPanel: { + visible: true, + }, + headerFilter: { + visible: true, + }, + showRowLines: true, + showBorders: true, + columnWidth: 180, + columns: [{ + dataField: 'Task_Subject', + width: 300, + }, { + dataField: 'Task_Assigned_Employee_ID', + caption: 'Assigned', + }, { + dataField: 'Task_Status', + caption: 'Status', + }, { + dataField: 'Task_Start_Date', + caption: 'Start Date', + dataType: 'date', + }, { + dataField: 'Task_Due_Date', + caption: 'Due Date', + dataType: 'date', + }, + ], + })); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/common/treeList/stickyColumns/stickyColumns.spec.ts b/e2e/testcafe-devextreme/playwright-tests/common/treeList/stickyColumns/stickyColumns.spec.ts new file mode 100644 index 000000000000..7c1c0ce24bc5 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/common/treeList/stickyColumns/stickyColumns.spec.ts @@ -0,0 +1,111 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('Sticky columns - Drag and Drop', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + const TREE_LIST_SELECTOR = '#container'; + + test('Header hover should display correctly when there are fixed columns', async ({ page }) => { + + await createWidget(page, 'dxTreeList', { + dataSource: new Array(20).fill(null).map((_, index) => { + const item = { + id: index + 1, + parentId: index % 5, + }; + + for (let i = 0; i < 13; i += 1) { + item[`field${i}`] = `test ${i} ${index + 2}`; + } + + return item; + }), + keyExpr: 'id', + columnFixing: { + enabled: true, + }, + width: 850, + autoExpandAll: true, + columnAutoWidth: true, + customizeColumns(columns) { + columns[5].fixed = true; + columns[6].fixed = true; + + columns[8].fixed = true; + columns[8].fixedPosition = 'right'; + columns[9].fixed = true; + columns[9].fixedPosition = 'right'; + }, + }); + + const treeList = new TreeList(TREE_LIST_SELECTOR); + const headerCell = treeList.getHeaders().getHeaderRow(0).getHeaderCell(13); + + await expect(treeList.isReady()).ok(); + + await hover(headerCell.element); + + await expect(headerCell.isHovered()).ok(); + + await testScreenshot(page, 'treelist_header_hover_with_fixed_columns.png', { element: treeList.element }); + + }); + + test('Row hover should display correctly when there are fixed columns', async ({ page }) => { + + await createWidget(page, 'dxTreeList', { + dataSource: new Array(20).fill(null).map((_, index) => { + const item = { + id: index + 1, + parentId: index % 5, + }; + + for (let i = 0; i < 13; i += 1) { + item[`field${i}`] = `test ${i} ${index + 2}`; + } + + return item; + }), + keyExpr: 'id', + columnFixing: { + enabled: true, + }, + width: 850, + autoExpandAll: true, + columnAutoWidth: true, + hoverStateEnabled: true, + customizeColumns(columns) { + columns[5].fixed = true; + columns[6].fixed = true; + + columns[8].fixed = true; + columns[8].fixedPosition = 'right'; + columns[9].fixed = true; + columns[9].fixedPosition = 'right'; + }, + }); + + const treeList = new TreeList(TREE_LIST_SELECTOR); + const dataRow = treeList.getDataRow(1); + + await expect(treeList.isReady()).ok(); + + await hover(dataRow.element); + + expect(dataRow.isHovered).toBeTruthy(); + + await testScreenshot(page, 'treelist_row_hover_with_fixed_columns.png', { element: treeList.element }); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/common/treeList/stickyColumns/withDragAndDrop.spec.ts b/e2e/testcafe-devextreme/playwright-tests/common/treeList/stickyColumns/withDragAndDrop.spec.ts new file mode 100644 index 000000000000..c693f152752d --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/common/treeList/stickyColumns/withDragAndDrop.spec.ts @@ -0,0 +1,52 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('Sticky columns - Drag and Drop', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + const DATA_GRID_SELECTOR = '#container'; + + test('Fixed columns should work when drag and drop rows are enabled', async ({ page }) => { + await createWidget(page, 'dxTreeList', { + dataSource: getData(10, 10), + keyExpr: 'field_0', + width: 500, + columnFixing: { + enabled: true, + }, + showColumnHeaders: true, + columnAutoWidth: true, + rowDragging: { + allowReordering: true, + dropFeedbackMode: 'push', + }, + customizeColumns(columns) { + columns[5].fixed = true; + columns[6].fixed = true; + + columns[8].fixed = true; + columns[8].fixedPosition = 'right'; + columns[9].fixed = true; + columns[9].fixedPosition = 'right'; + }, + }); + + // arrange, act + const treeList = new TreeList(DATA_GRID_SELECTOR); + + await testScreenshot(page, 'treelist_sticky_columns_with_drag_and_drop_before_interaction.png', { element: treeList.element }); + + // assert + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/common/treeList/toast.spec.ts b/e2e/testcafe-devextreme/playwright-tests/common/treeList/toast.spec.ts new file mode 100644 index 000000000000..0c98a62f2e21 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/common/treeList/toast.spec.ts @@ -0,0 +1,30 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('Toasts in TreeList', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('Toast should be visible after calling and should be not visible after default display time', async ({ page }) => { + + createWidget(page, 'dxTreeList', {}); + + const treeList = page.locator('#container'); + await treeList.isReady(); + await treeList.apiShowErrorToast(); + await expect(treeList.getToast().exists).ok(); + + await testScreenshot(page, 'ai-column__toast__at-the-right-position.png', { element: treeList.element }); + await expect(treeList.getToast().exists).notOk(); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/accessibility/bugs.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/accessibility/bugs.spec.ts new file mode 100644 index 000000000000..b46f25921f9e --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/accessibility/bugs.spec.ts @@ -0,0 +1,31 @@ +import { test, expect } from '@playwright/test'; +import { createWidget } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('Accessibility bugs', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + test('T1187314 - DataGrid displays an incorrect row count in "aria-label" if there is no data after filtering', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + keyExpr: 'id', + dataSource: [{ + id: 0, + data: 'A', + }], + filterRow: { visible: true }, + scrolling: { mode: 'infinite' }, + }); + + await dataGrid.apiFilter(['id', '=', '1']); + expect(await dataGrid.getContainer().getAttribute('aria-label')); + await t.eql('Data grid with 0 rows and 2 columns'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/accessibility/common.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/accessibility/common.spec.ts new file mode 100644 index 000000000000..f349ac1931ba --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/accessibility/common.spec.ts @@ -0,0 +1,41 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('Common tests', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + // visual: fluent.blue.light + // visual: fluent.blue.dark + const screenshotCheck = async ( + t: TestController, + screenshotName: string, + ) => { + const { takeScreenshot, compareResults } = createScreenshotsComparer(t); + + await testScreenshot(t, takeScreenshot, `${screenshotName}.png`); + + await t + .expect(compareResults.isValid()) + .ok(compareResults.errorMessages()); + }; + + test('Grid without data', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: [], + }); + + expect(await page.locator('.dx-datagrid').first().isVisible()); + await t.ok(); + + await screenshotCheck(t, 'no-data'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/accessibility/contrast.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/accessibility/contrast.spec.ts new file mode 100644 index 000000000000..2657cb7343c8 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/accessibility/contrast.spec.ts @@ -0,0 +1,74 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('DataGrid - contrast', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + // T1257970 + // visual: generic.light + // visual: fluent.blue.light + // visual: material.blue.light + + test('DataGrid - Contrast between icons in the Filter Row menu and their background doesn\'t comply with WCAG accessibility standards', async ({ page }) => { + const filterCell = page.locator('.dx-datagrid-filter-row td').nth(0); + const searchButton = filterCell.menuButton; + const filterMenu = filterCell.menu; + expect(await page.locator('.dx-datagrid').first().isVisible()); + await t.ok(); + await (searchButton).click(); + expect(await filterMenu.element.exists); + await t.ok(); + + await testScreenshot(page, 'T1257970-datagrid-menu-icon-contrast.png', { element: page.locator('#container') }); + }).before( + async () => { + await createWidget(page, 'dxDataGrid', { + dataSource: getData(5, 5), + filterRow: { + visible: true, + }, + }); + }, + ); + + // T1286345 + // visual: generic.light + // visual: fluent.blue.light + // visual: material.blue.light + test('DataGrid - Filter icon should remain visible when it\'s focused', async ({ page }) => { + expect(await page.locator('.dx-datagrid').first().isVisible()); + await t.ok(); + + const searchIconContainer = dataGrid + .getHeaders() + .getFilterRow() + .getFilterCell(1) + .getSearchIcon() + .element; + + await (page.locator('.dx-datagrid-filter-row td').click().nth(0).element); + await page.keyboard.press('tab'); + expect(await searchIconContainer.focused); + await t.ok(); + + await testScreenshot(page, 'T1286345-datagrid-menu-icon-when-focused.png', { element: page.locator('#container') }); + ); + }).before( + async () => { + await createWidget(page, 'dxDataGrid', { + dataSource: getData(2, 2), + filterRow: { + visible: true, + }, + }); + }, +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/adaptiveRow.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/adaptiveRow.spec.ts new file mode 100644 index 000000000000..c82f2ea83a9b --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/adaptiveRow.spec.ts @@ -0,0 +1,66 @@ +import { test, expect } from '@playwright/test'; +import { createWidget } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('Adaptive Row', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + test('Should be shown and hidden when the window is resized', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: [{ + ID: 1, + Head_ID: -1, + Full_Name: 'John Heart', + Prefix: 'Mr.', + Title: 'CEO', + City: 'Los Angeles', + State: 'California', + Email: 'jheart@dx-email.com', + Skype: 'jheart_DX_skype', + Mobile_Phone: '(213) 555-9392', + Birth_Date: '1964-03-16', + Hire_Date: '1995-01-15', + }], + keyExpr: 'ID', + allowColumnResizing: true, + rowDragging: { + allowDropInsideItem: true, + allowReordering: true, + }, + columns: [ + { + dataField: 'Title', + caption: 'Position', + hidingPriority: 0, + fixed: true, + }, + { dataField: 'Full_Name', hidingPriority: 1 }, + { dataField: 'City', hidingPriority: 2 }, + { dataField: 'State', hidingPriority: 3 }, + { dataField: 'Mobile_Phone', hidingPriority: 4 }, + { dataField: 'Hire_Date', dataType: 'date', hidingPriority: 5 }, + ], + }); + + await page.locator('.dx-datagrid').first().isVisible(); + + const adaptiveButton = dataGrid.getAdaptiveButton(); + expect(await adaptiveButton.exists).toBeTruthy(); + await (adaptiveButton).click(); + + expect(await dataGrid.getAdaptiveRow(0).element.exists).toBeTruthy(); + + await page.setViewportSize({ width: 1200, height: 400 }); + + expect(await dataGrid.isAdaptiveColumnHidden()).toBeTruthy(); + expect(await dataGrid.getAdaptiveRow(0).element.exists).toBeFalsy(); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/aiColumn/adaptivity.functional.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/aiColumn/adaptivity.functional.spec.ts new file mode 100644 index 000000000000..923de819340b --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/aiColumn/adaptivity.functional.spec.ts @@ -0,0 +1,68 @@ +import { test, expect } from '@playwright/test'; +import { createWidget } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('Ai Column.Adaptivity', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + const resolveAIRequest = ClientFunction((): void => { + const { aiResponseData } = (window as any); + const { aiResolve } = (window as any); + + if (aiResponseData && aiResolve) { + aiResolve(aiResponseData); + + (window as any).aiResponseData = null; + (window as any).aiResolve = null; + } + }); + + const deleteGlobalVariables = ClientFunction((): void => { + delete (window as any).aiResponseData; + delete (window as any).aiResolve; + }); + + test('The AI column should be hidden when columnHidingEnabled is true', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: [ + { id: 1, name: 'Name 1', value: 10 }, + { id: 2, name: 'Name 2', value: 20 }, + { id: 3, name: 'Name 3', value: 30 }, + ], + keyExpr: 'id', + width: 350, + columnWidth: 100, + columnHidingEnabled: true, + columns: [ + { dataField: 'id', caption: 'ID' }, + { dataField: 'name', caption: 'Name' }, + { dataField: 'value', caption: 'Value' }, + { + type: 'ai', + caption: 'AI Column', + }, + ], + }); + + // arrange, act + expect(await page.locator('.dx-datagrid').first().isVisible()).toBeTruthy(); + + const fourthHeaderCell = page.locator('.dx-header-row').nth(0).locator('td').nth(3); + + // assert: the AI column is hidden + expect(await fourthHeaderCell.element.textContent).toBe('AI Column'); + expect(await fourthHeaderCell.isHidden).toBeTruthy(); + + // assert: the adaptive button is visible + expect(await page.locator('.dx-data-row').nth(0).locator('.dx-command-edit').nth(4).getAdaptiveButton().visible).toBeTruthy(); + }); + // TODO: .after() block removed +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/aiColumn/columnChooser.functional.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/aiColumn/columnChooser.functional.spec.ts new file mode 100644 index 000000000000..1a8835bcad12 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/aiColumn/columnChooser.functional.spec.ts @@ -0,0 +1,70 @@ +import { test, expect } from '@playwright/test'; +import { createWidget } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('Ai Column - Column Chooser.Functional', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + test('The AI column can be hidden when columnChooser.mode is "dragAndDrop"', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: [ + { id: 1, name: 'Name 1', value: 10 }, + { id: 2, name: 'Name 2', value: 20 }, + { id: 3, name: 'Name 3', value: 30 }, + ], + keyExpr: 'id', + width: 600, + columnWidth: 200, + columnChooser: { + enabled: true, + mode: 'dragAndDrop', + }, + columns: [ + { + type: 'ai', + caption: 'AI Column', + name: 'myAiColumn', + }, + { dataField: 'id', caption: 'ID' }, + { dataField: 'name', caption: 'Name' }, + { dataField: 'value', caption: 'Value' }, + ], + }); + + // arrange + const headerRow = page.locator('.dx-header-row').nth(0); + const columnChooser = page.locator('.dx-datagrid-column-chooser'); + + expect(await page.locator('.dx-datagrid').first().isVisible()).toBeTruthy(); + + // assert + expect(await dataGrid.apiColumnOption('myAiColumn', 'visible')).toBeTruthy(); + expect(await headerRow.getHeaderTexts()).toBe(['AI Column', 'ID', 'Name', 'Value']); + + // act + await page.evaluate(() => ($('#container') as any).dxDataGrid('instance').showColumnChooser()); + + // assert + expect(await columnChooser.isVisible()).toBeTruthy(); + expect(await columnChooser.getColumnTexts()).toBe([]); + + // act + await t.dragToElement( + page.locator('.dx-header-row').nth(0).locator('td').nth(0), + page.locator('.dx-datagrid-column-chooser').content, + ); + + // assert + expect(await dataGrid.apiColumnOption('myAiColumn', 'visible')).toBeFalsy(); + expect(await headerRow.getHeaderTexts()).toBe(['ID', 'Name', 'Value']); + expect(await columnChooser.getColumnTexts()).toBe(['AI Column']); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/aiColumn/columnFixing.functional.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/aiColumn/columnFixing.functional.spec.ts new file mode 100644 index 000000000000..173e0b7a7365 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/aiColumn/columnFixing.functional.spec.ts @@ -0,0 +1,50 @@ +import { test, expect } from '@playwright/test'; +import { createWidget } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('Ai Column - Sticky columns.Functional', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + test('The AI column should not be fixed when the columnFixing.enabled option is true', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: [ + { id: 1, name: 'Name 1', value: 10 }, + { id: 2, name: 'Name 2', value: 20 }, + { id: 3, name: 'Name 3', value: 30 }, + ], + keyExpr: 'id', + width: 600, + columnWidth: 200, + columnFixing: { + enabled: true, + }, + columns: [ + { dataField: 'id', caption: 'ID' }, + { dataField: 'name', caption: 'Name' }, + { dataField: 'value', caption: 'Value' }, + { + type: 'ai', + caption: 'AI Column', + name: 'myAiColumn', + }, + ], + }); + + // arrange, act + expect(await page.locator('.dx-datagrid').first().isVisible()).toBeTruthy(); + + const aiHeader = page.locator('.dx-header-row').nth(0).locator('td').nth(3); + + // assert + expect(await aiHeader.element.textContent).toBe('AI Column'); + expect(await aiHeader.isSticky()).toBeFalsy(); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/aiColumn/columnFixing.visual.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/aiColumn/columnFixing.visual.spec.ts new file mode 100644 index 000000000000..3f2d24798c65 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/aiColumn/columnFixing.visual.spec.ts @@ -0,0 +1,52 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('Ai Column - Sticky columns.Visual', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + test('Check context menu items', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: [ + { id: 1, name: 'Name 1', value: 10 }, + { id: 2, name: 'Name 2', value: 20 }, + { id: 3, name: 'Name 3', value: 30 }, + ], + keyExpr: 'id', + width: 600, + columnWidth: 200, + columnFixing: { + enabled: true, + }, + columns: [ + { + type: 'ai', + caption: 'AI Column', + name: 'myAiColumn', + }, + { dataField: 'id', caption: 'ID' }, + { dataField: 'name', caption: 'Name' }, + { dataField: 'value', caption: 'Value' }, + ], + }); + + // arrange + expect(await page.locator('.dx-datagrid').first().isVisible()).toBeTruthy(); + + // act + await t.rightClick(page.locator('.dx-header-row').nth(0).locator('td').nth(0)); + await (dataGrid.getContextMenu().click().getItemByText('Set Fixed Position')); + + await testScreenshot(page, 'datagrid__ai-column-and-sticky-columns__context-menu.png', { element: page.locator('#container') }); + + // assert + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/aiColumn/columnReordering.functional.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/aiColumn/columnReordering.functional.spec.ts new file mode 100644 index 000000000000..4ecf256bf198 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/aiColumn/columnReordering.functional.spec.ts @@ -0,0 +1,52 @@ +import { test, expect } from '@playwright/test'; +import { createWidget } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('Ai Column.ColumnReordering', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + test('Column reordering should work when allowColumnReordering is true', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: [ + { id: 1, name: 'Name 1', value: 10 }, + { id: 2, name: 'Name 2', value: 20 }, + { id: 3, name: 'Name 3', value: 30 }, + ], + keyExpr: 'id', + allowColumnReordering: true, + columnWidth: 100, + columns: [ + { + type: 'ai', + caption: 'AI Column', + name: 'myAiColumn', + }, + { dataField: 'id', caption: 'ID' }, + { dataField: 'name', caption: 'Name' }, + { dataField: 'value', caption: 'Value' }, + ], + }); + + // arrange + const headerRow = page.locator('.dx-header-row').nth(0); + + expect(await page.locator('.dx-datagrid').first().isVisible()).toBeTruthy(); + + // assert + expect(await headerRow.getHeaderTexts()).toBe(['AI Column', 'ID', 'Name', 'Value']); + + // act + await t.drag(headerRow.locator('td').nth(0).element, 150, 0); + + // assert + expect(await headerRow.getHeaderTexts()).toBe(['ID', 'AI Column', 'Name', 'Value']); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/aiColumn/columnReordering.visual.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/aiColumn/columnReordering.visual.spec.ts new file mode 100644 index 000000000000..574af2cce039 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/aiColumn/columnReordering.visual.spec.ts @@ -0,0 +1,57 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('Ai Column.ColumnReordering.Visual', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + test('The draggable AI column should display correctly', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: [ + { id: 1, name: 'Name 1', value: 10 }, + { id: 2, name: 'Name 2', value: 20 }, + { id: 3, name: 'Name 3', value: 30 }, + ], + keyExpr: 'id', + allowColumnReordering: true, + columnWidth: 200, + columns: [ + { + type: 'ai', + caption: 'AI Column', + name: 'myAiColumn', + }, + { dataField: 'id', caption: 'ID' }, + { dataField: 'name', caption: 'Name' }, + { dataField: 'value', caption: 'Value' }, + ], + }); + + // arrange + expect(await page.locator('.dx-datagrid').first().isVisible()).toBeTruthy(); + + await dataGrid.moveHeader(0, 100, 5, true); + + // assert + expect(await dataGrid.getDraggableHeader().visible).toBeTruthy(); + + await testScreenshot(page, 'datagrid__ai-column__dragging.png', { element: page.locator('#container') }); + + // act + await dataGrid.dropHeader(0); + + // assert + expect(await dataGrid.getDraggableHeader().visible); + await t.notOk(); + expect(await compareResults.isValid()); + await t.ok(compareResults.errorMessages()); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/aiColumn/columnResizing.functional.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/aiColumn/columnResizing.functional.spec.ts new file mode 100644 index 000000000000..d8c41b3b41a9 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/aiColumn/columnResizing.functional.spec.ts @@ -0,0 +1,58 @@ +import { test, expect } from '@playwright/test'; +import { createWidget } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('Ai Column.ColumnResizing.Functional', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + (['nextColumn', 'widget'] as const).forEach((columnResizingMode) => { + + test(`Column resizing should work when allowColumnResizing is true (columnResizingMode = ${columnResizingMode})`, async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: [ + { id: 1, name: 'Name 1', value: 10 }, + { id: 2, name: 'Name 2', value: 20 }, + { id: 3, name: 'Name 3', value: 30 }, + ], + keyExpr: 'id', + allowColumnResizing: true, + columnResizingMode, + columnWidth: 100, + columns: [ + { + type: 'ai', + caption: 'AI Column', + name: 'myAIColumn', + }, + { dataField: 'id', caption: 'ID' }, + { dataField: 'name', caption: 'Name' }, + { dataField: 'value', caption: 'Value' }, + ], + }); + + // arrange + const dataCell = page.locator('.dx-data-row').nth(0).locator('td').nth(0); + + expect(await page.locator('.dx-datagrid').first().isVisible()).toBeTruthy(); + + // assert + expect(await page.locator('.dx-header-row').nth(0).locator('td').nth(0).textContent); + await t.eql('AI Column'); + expect(await dataCell.element.clientWidth); + await t.eql(120); + + // act + await dataGrid.resizeHeader(1, 50); + + // assert + expect(await dataCell.element.clientWidth).toBe(170); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/aiColumn/columnResizing.visual.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/aiColumn/columnResizing.visual.spec.ts new file mode 100644 index 000000000000..84d434b49c3f --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/aiColumn/columnResizing.visual.spec.ts @@ -0,0 +1,51 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('Ai Column.ColumnResizing.Visual', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + test('Resize AI Column when wordWrapEnabled is true', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: [ + { id: 1, name: 'Name 1', value: 10 }, + { id: 2, name: 'Name 2', value: 20 }, + { id: 3, name: 'Name 3', value: 30 }, + ], + keyExpr: 'id', + allowColumnResizing: true, + wordWrapEnabled: true, + columnWidth: 100, + columns: [ + { + type: 'ai', + caption: 'AI Column AI Column', + width: 250, + }, + { dataField: 'id', caption: 'ID' }, + { dataField: 'name', caption: 'Name' }, + { dataField: 'value', caption: 'Value' }, + ], + }); + + // arrange + expect(await page.locator('.dx-datagrid').first().isVisible()).toBeTruthy(); + + await testScreenshot(page, 'datagrid__ai-column__column-resizing(wordWrapEnabled=true)-1.png', { element: page.locator('#container') }); + + // act + await dataGrid.resizeHeader(1, -150); + + await testScreenshot(page, 'datagrid__ai-column__column-resizing(wordWrapEnabled=true)-2.png', { element: page.locator('#container') }); + + // assert + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/aiColumn/functional.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/aiColumn/functional.spec.ts new file mode 100644 index 000000000000..c1c8c7b922dc --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/aiColumn/functional.spec.ts @@ -0,0 +1,47 @@ +import { test, expect } from '@playwright/test'; +import { createWidget } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('Ai Column.Common', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + const EMPTY_CELL_TEXT = '\u00A0'; + const DROPDOWNMENU_PROMPT_EDITOR_INDEX = 0; + const DROPDOWNMENU_REGENERATE_INDEX = 1; + const DROPDOWNMENU_CLEAR_DATA_INDEX = 2; + + test('The AI column with a given width', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: [ + { id: 1, name: 'Name 1', value: 10 }, + { id: 2, name: 'Name 2', value: 20 }, + { id: 3, name: 'Name 3', value: 30 }, + ], + keyExpr: 'id', + columns: [ + { dataField: 'id', caption: 'ID' }, + { dataField: 'name', caption: 'Name' }, + { dataField: 'value', caption: 'Value' }, + { + type: 'ai', + caption: 'AI Column', + width: 175, + }, + ], + }); + + // arrange, act + expect(await page.locator('.dx-datagrid').first().isVisible()).toBeTruthy(); + + // assert + expect(await page.locator('.dx-data-row').nth(0).locator('td').nth(3).clientWidth).toBe(175); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/aiColumn/keyboardNavigation.visual.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/aiColumn/keyboardNavigation.visual.spec.ts new file mode 100644 index 000000000000..11e5f3d99297 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/aiColumn/keyboardNavigation.visual.spec.ts @@ -0,0 +1,66 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('Ai Column.KeyboardNavigation.Visual', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + test('Check keyboard navigation for AI column', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: [ + { id: 1, name: 'Name 1', value: 10 }, + { id: 2, name: 'Name 2', value: 20 }, + { id: 3, name: 'Name 3', value: 30 }, + ], + keyExpr: 'id', + allowColumnReordering: true, + columnWidth: 200, + columns: [ + { dataField: 'id', caption: 'ID' }, + { + type: 'ai', + caption: 'AI Column', + name: 'myAiColumn', + }, + { dataField: 'name', caption: 'Name' }, + { dataField: 'value', caption: 'Value' }, + ], + }); + + // arrange + const headerRow = page.locator('.dx-header-row').nth(0); + expect(await page.locator('.dx-datagrid').first().isVisible()).toBeTruthy(); + + // act + await (headerRow.locator('td').nth(0).click().element); + await page.keyboard.press('tab'); + + // assert + expect(await headerRow.locator('.dx-command-edit').nth(1).element.focused).toBeTruthy(); + + // act + await page.keyboard.press('tab'); + + // assert + expect(await headerRow.locator('.dx-command-edit').nth(1).getAIDropDownButton().isFocused).toBeTruthy(); + + await testScreenshot(page, 'datagrid__ai-column__focused-dropdown-button.png', { element: page.locator('#container') }); + + // act + await page.keyboard.press('tab'); + + // assert + expect(await headerRow.locator('td').nth(2).isFocused); + await t.ok(); + expect(await compareResults.isValid()); + await t.ok(compareResults.errorMessages()); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/aiColumn/virtualScrolling.functional.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/aiColumn/virtualScrolling.functional.spec.ts new file mode 100644 index 000000000000..de4c125dc1ea --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/aiColumn/virtualScrolling.functional.spec.ts @@ -0,0 +1,144 @@ +import { test, expect } from '@playwright/test'; +import { createWidget } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('Ai Column.Virtual Scrolling.Functional', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + const checkAIColumnTexts = async ( + t: TestController, + component: DataGrid, + expectedRowCount: number, + ): Promise => { + const visibleRows: Record[] = await component.apiGetVisibleRows(); + + await t.expect(visibleRows.length).eql(expectedRowCount); + + // eslint-disable-next-line no-restricted-syntax + for (const row of visibleRows) { + await t + .expect(component.locator('td').nth(row.dataIndex, 3).textContent) + .eql(`Response ${row.data.name}`); + } + }; + + const resolveAIRequest = ClientFunction((): void => { + const { aiResponseData } = (window as any); + const { aiResolve } = (window as any); + + if (aiResponseData && aiResolve) { + aiResolve(aiResponseData); + + (window as any).aiResponseData = null; + (window as any).aiResolve = null; + } + }); + + const deleteGlobalVariables = ClientFunction((): void => { + delete (window as any).aiResponseData; + delete (window as any).aiResolve; + }); + + test('DataGrid should send an AI request for rendered rows after scrolling without changing the page index', async ({ page }) => { + await createWidget(page, 'dxDataGrid', () => { + const generateData = (rowCount: number): Record[] => { + const result: Record[] = []; + + for (let i = 0; i < rowCount; i += 1) { + result.push({ id: i + 1, name: `Name ${i + 1}`, value: (i + 1) * 10 }); + } + + return result; + }; + + return { + dataSource: generateData(200), + height: 500, + keyExpr: 'id', + paging: { + pageSize: 50, + }, + scrolling: { + mode: 'virtual', + }, + columns: [ + { dataField: 'id', caption: 'ID' }, + { dataField: 'name', caption: 'Name' }, + { dataField: 'value', caption: 'Value' }, + { + type: 'ai', + caption: 'AI Column', + name: 'myColumn', + ai: { + prompt: 'Initial prompt', + // eslint-disable-next-line new-cap + aiIntegration: new (window as any).DevExpress.aiIntegration({ + sendRequest(prompt) { + return { + promise: new Promise((resolve) => { + const result: Record = {}; + + Object.entries(prompt.data?.data).forEach(([key, value]) => { + result[key] = `Response ${(value as any).name}`; + }); + + (window as any).aiResponseData = JSON.stringify(result); + (window as any).aiResolve = resolve; + }), + abort: (): void => {}, + }; + }, + }), + }, + }, + ], + }; + }); + + // arrange + // assert + expect(await dataGrid.getLoadPanel().isVisible()); + await t.ok(); + + // act + await resolveAIRequest(); + + // assert + expect(await page.locator('.dx-datagrid').first().isVisible()); + await t.ok(); + expect(await dataGrid.getLoadPanel().isVisible()); + await t.notOk(); + await checkAIColumnTexts(t, dataGrid, 11); + + // act + await page.evaluate((opts) => ($('#container') as any).dxDataGrid('instance').getScrollable().scrollTo(opts), { y: 1000 }); + + // assert + expect(await page.evaluate(() => ($('#container') as any).dxDataGrid('instance').getScrollable().scrollTop())); + await t.eql(1000); + expect(await dataGrid.apiPageIndex()); + await t.eql(0); + expect(await page.locator('.dx-data-row').nth(20).locator('td').nth(0).textContent()); + await t.eql('21'); + expect(await dataGrid.getLoadPanel().isVisible()); + await t.ok(); + + // act + await resolveAIRequest(); + + // assert + expect(await page.locator('.dx-datagrid').first().isVisible()); + await t.ok(); + expect(await dataGrid.getLoadPanel().isVisible()); + await t.notOk(); + await checkAIColumnTexts(t, dataGrid, 12); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/aiColumn/visual.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/aiColumn/visual.spec.ts new file mode 100644 index 000000000000..afc6a920d524 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/aiColumn/visual.spec.ts @@ -0,0 +1,43 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('Ai Column.Visual', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + test('Default render', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: [ + { id: 1, name: 'Name 1', value: 10 }, + { id: 2, name: 'Name 2', value: 20 }, + { id: 3, name: 'Name 3', value: 30 }, + ], + keyExpr: 'id', + columns: [ + { dataField: 'id', caption: 'ID' }, + { dataField: 'name', caption: 'Name' }, + { dataField: 'value', caption: 'Value' }, + { + type: 'ai', + caption: 'AI Column', + name: 'myAiColumn', + }, + ], + }); + + // arrange, act + expect(await page.locator('.dx-datagrid').first().isVisible()).toBeTruthy(); + + await testScreenshot(page, 'datagrid__ai-column__default.png', { element: page.locator('#container') }); + + // assert + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/bandColumns/runtimeChange.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/bandColumns/runtimeChange.spec.ts new file mode 100644 index 000000000000..548844f43674 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/bandColumns/runtimeChange.spec.ts @@ -0,0 +1,115 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('Band columns: runtime change', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + const GRID_CONTAINER = '#container'; + + const dataSource = [ + { + id: 0, + A: 'A_0', + B: 0, + }, + { + id: 1, + A: 'A_1', + B: 1, + }, + { + id: 2, + A: 'A_2', + B: 2, + }, + ]; + + const lookUpDataSource = [ + { + id: 0, + text: 'Lookup_value_0', + }, + { + id: 1, + text: 'Lookup_value_1', + }, + { + id: 2, + text: 'Lookup_value_2', + }, + ]; + + const columns = [ + { + dataField: 'A', + }, + { + dataField: 'B', + lookup: { + dataSource: lookUpDataSource, + valueExpr: 'id', + displayExpr: 'text', + }, + }, + ]; + + const nestedColumns = [ + { + dataField: 'A', + }, + { + name: 'Nested', + caption: 'Nested', + columns: [ + { + dataField: 'B', + lookup: { + dataSource: lookUpDataSource, + valueExpr: 'id', + displayExpr: 'text', + }, + }, + ], + }, + ]; + + const changeDataGridColumnsReactWay = ClientFunction(() => { + const dataGridWidget = ($(`${GRID_CONTAINER}`) as any).dxDataGrid('instance'); + + dataGridWidget.beginUpdate(); + + dataGridWidget.option('columns[1].dataField', undefined); + dataGridWidget.option('columns[1].lookup', undefined); + dataGridWidget.option('columns[1].columns', nestedColumns[1].columns); + dataGridWidget.option('columns[1].name', nestedColumns[1].name); + dataGridWidget.option('columns[1].caption', nestedColumns[1].caption); + + dataGridWidget.endUpdate(); + }, { dependencies: { GRID_CONTAINER, nestedColumns } }); + + test('Should change usual columns to band columns without error in React (T1213679)', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: [...dataSource], + columns: [...columns], + keyExpr: 'id', + showBorders: true, + }); + + expect(await page.locator('.dx-datagrid').first().isVisible()).toBeTruthy(); + + await testScreenshot(page, 'band-columns_before-runtime-update.png', { element: page.locator('#container') }); + + await changeDataGridColumnsReactWay(); + + await testScreenshot(page, 'band-columns_after-runtime-update.png', { element: page.locator('#container') }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/builder.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/builder.spec.ts new file mode 100644 index 000000000000..f90bd2db0153 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/builder.spec.ts @@ -0,0 +1,47 @@ +import { test, expect } from '@playwright/test'; +import { createWidget } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('Filter Builder', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + const scrollTo = ClientFunction((x, y) => { + window.scrollTo(x, y); + }); + test('Field menu should be opened on field click if window scroll exists (T852701)', async ({ page }) => { + const filter = [] as any[]; + const fields = [] as any[]; + + for (let i = 1; i <= 50; i += 1) { + if (i > 1) { + filter.push('or'); + } + const name = `Test${i}`; + filter.push([name, '=', 'Test']); + fields.push({ dataField: name }); + } + + return createWidget(page, 'dxFilterBuilder', { + fields, + value: filter, + }); + + const filterBuilder = new FilterBuilder('#container'); + const lastField = filterBuilder.getField(49); + + await scrollTo(0, 10000); + await (lastField.element).click(); + + expect(await lastField.text).toBe('Test 50'); + expect(await FilterBuilder.getPopupTreeView().visible).toBeTruthy(); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/columnChooser.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/columnChooser.spec.ts new file mode 100644 index 000000000000..0a66418be29c --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/columnChooser.spec.ts @@ -0,0 +1,52 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('Column chooser', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + // visual: generic.light + // visual: material.blue.light + // visual: fluent.blue.light + // visual: fluent.blue.dark + ['dragAndDrop', 'select'].forEach((mode: any) => { + + test(`Column chooser screenshot in mode=${mode}`, async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: getData(20, 3), + height: 400, + showBorders: true, + columns: [{ + dataField: 'field_0', + dataType: 'string', + }, { + dataField: 'field_1', + dataType: 'string', + }, { + dataField: 'field_2', + dataType: 'string', + visible: false, + }], + columnChooser: { + enabled: true, + mode, + }, + }); + + await page.evaluate(() => ($('#container') as any).dxDataGrid('instance').showColumnChooser()); + + expect(await page.locator('.dx-datagrid-column-chooser').isVisible()); + await t.ok(); + + await testScreenshot(page, `column-chooser-${mode}-mode.png`, { element: page.locator('#container') }); + }); + // TODO: .after() block removed +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/columnReordering/functional.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/columnReordering/functional.spec.ts new file mode 100644 index 000000000000..243169d5c65a --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/columnReordering/functional.spec.ts @@ -0,0 +1,80 @@ +import { test, expect } from '@playwright/test'; +import { createWidget } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('Column reordering', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + const CLASS = ClassNames; + + const getVisibleColumns = (dataGrid: DataGrid): Promise => { + const { getInstance } = dataGrid; + + return ClientFunction( + () => (getInstance() as any) + .getVisibleColumns() + .map((column: any) => column.dataField ?? column.name), + { dependencies: { getInstance } }, + )(); + }; + const getColumnsSeparatorOffset = ClientFunction(() => $(`.${CLASS.columnsSeparator}`).offset(), { dependencies: { CLASS } }); + // T975549 + + test('The column reordering should work correctly when there is a fixed column with zero width', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + width: 800, + dataSource: [ + { + field1: 'test1', field2: 'test2', field3: 'test3', field4: 'test4', + }, + ], + columnFixing: { + // @ts-expect-error private option + legacyMode: true, + }, + columns: [ + { + dataField: 'field1', + fixed: true, + width: 200, + }, { + name: 'fake', + fixed: true, + width: 0.01, + }, { + dataField: 'field2', + width: 200, + }, { + dataField: 'field3', + width: 200, + }, { + dataField: 'field4', + width: 200, + }, + ], + allowColumnReordering: true, + }); + + expect(await page.locator('.dx-datagrid').first().isVisible()).toBeTruthy(); + + const headers = page.locator('.dx-header-row'); + const headerRow = headers.getHeaderRow(0); + + expect(await headerRow.locator('td').nth(2).element.textContent); + await t.eql('Field 2'); + await t.drag(headerRow.locator('td').nth(3).element, -400, 0); + expect(await headerRow.locator('td').nth(2).element.textContent); + await t.eql('Field 3'); + expect(await getVisibleColumns(dataGrid)); + await t.eql(['field1', 'fake', 'field3', 'field2', 'field4']); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/columnReordering/visual.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/columnReordering/visual.spec.ts new file mode 100644 index 000000000000..c69cdd86511e --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/columnReordering/visual.spec.ts @@ -0,0 +1,57 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('Column reordering.Visual', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + test('column separator should work properly with expand columns', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + width: 800, + dataSource: [ + { + field1: 'test1', field2: 'test2', field3: 'test3', field4: 'test4', + }, + ], + groupPanel: { + visible: true, + }, + columns: [ + { + dataField: 'field1', + width: 200, + groupIndex: 0, + }, { + dataField: 'field2', + width: 200, + groupIndex: 1, + }, { + dataField: 'field3', + width: 200, + }, { + dataField: 'field4', + width: 200, + }, + ], + allowColumnReordering: true, + }); + + expect(await page.locator('.dx-datagrid').first().isVisible()).toBeTruthy(); + await MouseUpEvents.disable(MouseAction.dragToOffset); + + await t.drag(dataGrid.getGroupPanel().getHeader(0).element, 0, 30); + await testScreenshot(page, 'column-separator-with-expand-columns.png'); + ); + + await MouseUpEvents.enable(MouseAction.dragToOffset); + }); + // TODO: .after() block removed +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/columnResizing/functional.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/columnResizing/functional.spec.ts new file mode 100644 index 000000000000..9e13e4f93e43 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/columnResizing/functional.spec.ts @@ -0,0 +1,65 @@ +import { test, expect } from '@playwright/test'; +import { createWidget } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('Column resizing', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + // T1314667 + + test('DataGrid – Resize indicator is moved when resizing a grouped column if showWhenGrouped is set to true', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: [{ + ID: 1, + Country: 'Brazil', + Area: 8515767, + Population_Urban: 0.85, + Population_Rural: 0.15, + Population_Total: 205809000, + }], + keyExpr: 'ID', + allowColumnResizing: true, + columnResizingMode: 'widget', + width: 500, + columns: [ + { + dataField: 'ID', + fixed: true, + allowReordering: false, + width: 50, + }, + + { + caption: 'Population', + columns: [ + { + dataField: 'Country', + showWhenGrouped: true, + width: 100, + groupIndex: 0, + }, + { dataField: 'Area' }, + { dataField: 'Population_Total' }, + { dataField: 'Population_Urban' }, + { dataField: 'Population_Rural' }, + ], + }, + ], + }); + + expect(await page.locator('.dx-datagrid').first().isVisible()).toBeTruthy(); + + await dataGrid.resizeHeader(3, 30, false); + + expect(await page.locator('.dx-header-row').nth(1).locator('td').nth(0).clientWidth); + await t.within(128, 130); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/columnResizing/visual.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/columnResizing/visual.spec.ts new file mode 100644 index 000000000000..93f449deb349 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/columnResizing/visual.spec.ts @@ -0,0 +1,75 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('Column resizing', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + test('column separator should starts from the parent', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: [{ + ID: 1, + Country: 'Brazil', + Area: 8515767, + Population_Urban: 0.85, + Population_Rural: 0.15, + Population_Total: 205809000, + GDP_Agriculture: 0.054, + GDP_Industry: 0.274, + GDP_Services: 0.672, + GDP_Total: 2353025, + }], + keyExpr: 'ID', + columnWidth: 100, + allowColumnResizing: true, + showBorders: true, + editing: { + allowUpdating: true, + }, + columns: ['Country', { + dataField: 'Population_Total', + visible: false, + }, { + caption: 'Population', + columns: ['Population_Rural', { + caption: 'By Sector', + columns: ['GDP_Total', { + caption: 'not resizable', + dataField: 'ID', + allowResizing: false, + }, 'GDP_Agriculture', 'GDP_Industry'], + }], + }, { + caption: 'Nominal GDP', + columns: ['GDP_Total', 'Population_Urban'], + }, 'Area'], + }); + + expect(await page.locator('.dx-datagrid').first().isVisible()).toBeTruthy(); + + async function makeColumnSeparatorScreenshot(index: number) { + await dataGrid.resizeHeader(index, 0, false); + await testScreenshot(page, `column-separator-${index}.png`); + + await t.dispatchEvent(page.locator('#container'), 'mouseup'); + } + + await makeColumnSeparatorScreenshot(1); + await makeColumnSeparatorScreenshot(2); + await makeColumnSeparatorScreenshot(3); + await makeColumnSeparatorScreenshot(4); + await makeColumnSeparatorScreenshot(5); + await makeColumnSeparatorScreenshot(6); + await makeColumnSeparatorScreenshot(7); + await makeColumnSeparatorScreenshot(8); + await makeColumnSeparatorScreenshot(9); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/editing/T1154721_editingCellFocus.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/editing/T1154721_editingCellFocus.spec.ts new file mode 100644 index 000000000000..0e3e43ff5f2e --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/editing/T1154721_editingCellFocus.spec.ts @@ -0,0 +1,88 @@ +import { test, expect } from '@playwright/test'; +import { createWidget } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('Editing - cell focus', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + const apiRequestMock = RequestMock() + .onRequestTo(/\/api\/data/) + .respond( + { + data: [ + { + id: 0, + data: 'A', + }, { + id: 1, + data: 'B', + }, { + id: 2, + data: 'C', + }, + ], + }, + 200, + { 'access-control-allow-origin': '*' }, + ) + .onRequestTo(/\/api\/update/) + .respond( + {}, + 200, + { + 'access-control-allow-origin': '*', + 'access-control-allow-methods': '*', + }, + ); + + // T1154721 + + test('Should allow focus next editor in the same column after save changes with local data source', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + keyExpr: 'id', + dataSource: [{ + id: 0, + data: 'A', + }, { + id: 1, + data: 'B', + }, { + id: 2, + data: 'C', + }], + editing: { + allowUpdating: true, + refreshMode: 'repaint', + mode: 'cell', + }, + columns: [{ + dataField: 'data', + showEditorAlways: true, + }], + repaintChangesOnly: true, + }); + + const firstCell = page.locator('.dx-data-row').nth(0).locator('td').nth(0); + const middleCell = page.locator('.dx-data-row').nth(1).locator('td').nth(0); + const secondCell = page.locator('.dx-data-row').nth(2).locator('td').nth(0); + + await (firstCell.locator('.dx-editor-cell')).fill(' AAA'); + await (secondCell.locator('.dx-editor-cell')).fill(' CCC'); + await (middleCell.element).click(); + + const firstCellValue = await firstCell.locator('.dx-editor-cell')().value; + const secondCellValue = await secondCell.locator('.dx-editor-cell')().value; + + expect(await firstCellValue).toBe('A AAA'); + expect(await secondCellValue).toBe('C CCC'); + }); + // TODO: .after() block removed +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/editing/T1323684_readonlyEditorNewRow.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/editing/T1323684_readonlyEditorNewRow.spec.ts new file mode 100644 index 000000000000..70ae7d56a703 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/editing/T1323684_readonlyEditorNewRow.spec.ts @@ -0,0 +1,64 @@ +import { test, expect } from '@playwright/test'; +import { createWidget } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('Editing - showEditorAlways cell in new row should be editable (T1323684)', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + const READONLY_CLASS = 'dx-datagrid-readonly'; + const CELL_FOCUS_DISABLED_CLASS = 'dx-cell-focus-disabled'; + + (['cell', 'batch'] as GridsEditMode[]).forEach((mode) => { + + test(`showEditorAlways editor should be editable in a new row when allowUpdating is false, ${mode} mode`, async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + keyExpr: 'ID', + dataSource: [ + { ID: 1, FirstName: 'John', LastName: 'Heart' }, + { ID: 2, FirstName: 'Olivia', LastName: 'Peyton' }, + ], + showBorders: true, + editing: { + mode, + allowUpdating: false, + allowAdding: true, + }, + columns: [ + 'LastName', + { dataField: 'FirstName', showEditorAlways: true }, + ], + }); + + const addRowButton = page.locator('.dx-datagrid-header-panel').getAddRowButton(); + + await (addRowButton).click(); + + const newRow = page.locator('.dx-data-row').nth(0); + expect(await newRow.isInserted).toBeTruthy(); + + const cell = page.locator('.dx-data-row').nth(0).locator('td').nth(1); + const editor = cell.locator('.dx-editor-cell'); + + expect(await cell.element.hasClass(READONLY_CLASS)); + await t.notOk('showEditorAlways cell in new row should not have readonly class'); + expect(await cell.element.hasClass(CELL_FOCUS_DISABLED_CLASS)); + await t.notOk('showEditorAlways cell in new row should not have cell-focus-disabled class'); + + await (editor.element).click(); + expect(await cell.isFocused); + await t.ok('showEditorAlways cell should be focused after click'); + expect(await editor.element.focused); + await t.ok('editor should be focused after click'); + await (editor.element).fill('test value'); + expect(await editor.element.value); + await t.eql('test value'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/editing/editing.functional_matrix.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/editing/editing.functional_matrix.spec.ts new file mode 100644 index 000000000000..7535ea2b5554 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/editing/editing.functional_matrix.spec.ts @@ -0,0 +1,615 @@ +import { test, expect } from '@playwright/test'; +import { createWidget } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('Editing.FunctionalMatrix', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + /* eslint-disable @typescript-eslint/init-declarations */ + + ); + + interface ColumnInfo { + columnIndex: number; + dataField: string; + newValue: string; + newMaskValue?: string; + } + + const editingModes: GridsEditMode[] = ['cell', 'batch', 'row', 'form', 'popup']; + + const textColumnInfos: ColumnInfo[] = [ + { columnIndex: 0, dataField: 'text', newValue: 'xxxx' }, + { columnIndex: 5, dataField: 'calculated', newValue: '9' }, + ]; + + const expectedTextColumnResult: ColumnInfo[] = [ + ...textColumnInfos, + { columnIndex: 1, dataField: 'number', newValue: '8' }, + ]; + + const maskedColumnInfos: ColumnInfo[] = [ + { + columnIndex: 0, dataField: 'text', newValue: 'xxxx', newMaskValue: 'xxxxx', + }, + { + columnIndex: 1, dataField: 'number', newValue: '-9', newMaskValue: '9-', + }, + { + columnIndex: 2, dataField: 'date', newValue: '10/1/2020', newMaskValue: '101', + }, + ]; + + const expectedMaskedColumnResult: ColumnInfo[] = [ + ...maskedColumnInfos, + { columnIndex: 5, dataField: 'calculated', newValue: '-8' }, + ]; + + const basicColumnInfos: ColumnInfo[] = [ + { columnIndex: 0, dataField: 'text', newValue: 'xxxx' }, + { columnIndex: 1, dataField: 'number', newValue: '-9' }, + { columnIndex: 2, dataField: 'date', newValue: '10/1/2020' }, + { columnIndex: 3, dataField: 'lookup', newValue: 'lookup 2' }, + { columnIndex: 4, dataField: 'boolean', newValue: 'true' }, + ]; + + const expectedBasicColumnResult: ColumnInfo[] = [ + ...basicColumnInfos, + { columnIndex: 5, dataField: 'calculated', newValue: '-8' }, + ]; + + const dataGrid = new DataGrid('#container'); + + const createDataGrid = ({ + mode, repaintChangesOnly = false, useMask = false, + }) => async (): Promise => createWidget(page, 'dxDataGrid', { + keyExpr: 'id', + dataSource: [ + { + id: 1, text: 'text 1', number: 1, date: '2020-10-27', boolean: false, lookup: 1, + }, + { + id: 2, text: 'text 2', number: 2, date: '2020-10-28', boolean: true, lookup: 2, + }, + ], + repaintChangesOnly, + editing: { + mode, + allowUpdating: true, + }, + columns: [ + { + dataField: 'text', + editorOptions: { + mask: useMask ? 'cccc' : undefined, + }, + }, + { + dataField: 'number', + editorOptions: { + format: '#0', + useMaskBehavior: useMask, + }, + }, + { + dataField: 'date', + dataType: 'date', + editorOptions: { + useMaskBehavior: useMask, + pickerType: 'calendar', + }, + }, + { + dataField: 'lookup', + lookup: { + valueExpr: 'id', + displayExpr: 'text', + dataSource: [ + { id: 1, text: 'lookup 1' }, + { id: 2, text: 'lookup 2' }, + ], + }, + }, + { dataField: 'boolean' }, + { + dataField: 'calculated', + calculateCellValue: (data): number => (data as { number: number }).number + 1, + setCellValue: (newData, value): void => { + newData.number = value - 1; + }, + }, + ], + }); + + const getEditForm = (mode: GridsEditMode): EditForm | null => { + if (mode === 'form') { + return dataGrid.getEditForm(); + } + if (mode === 'popup') { + return dataGrid.getPopupEditForm(); + } + return null; + }; + + const clickEditButtonIfExists = async (t: TestController, form: EditForm | null): Promise => { + const formAlreadyOpened = await form?.element.exists && await form?.element.visible; + + if (formAlreadyOpened) { + return; + } + + const editButton = dataGrid.getDataRow(0).locator('.dx-command-edit').nth(6).getEditButton(); + + if (await editButton.exists) { + await t.click(editButton); + } + }; + + const checkCellFocused = async ( + t: TestController, + mode: GridsEditMode, + { dataField }: ColumnInfo, + cell: DataCell | undefined, + editor: CellEditor | undefined, + ): Promise => { + const isRowBasedMode = mode === 'row'; + const isFormBasedMode = mode === 'form' || mode === 'popup'; + + if (!isFormBasedMode) { + await t.expect(cell?.isFocused).ok(); + } + + if ((isRowBasedMode || isFormBasedMode) && dataField === 'boolean') { + return; + } + + await t + .expect(editor?.element.focused) + .eql(true); + }; + + const clickCellEditor = async ( + t: TestController, + mode: GridsEditMode, + columnInfo: ColumnInfo, + form: EditForm | null, + cell: DataCell, + editor: CellEditor, + ): Promise => { + await clickEditButtonIfExists(t, form); + + const item = !form ? cell.element : editor.getItemLabel(); + await t.click(item, { offsetX: 25 }); + + await checkCellFocused(t, mode, columnInfo, cell, editor); + }; + + const moveToFirstCellEditor = async (t: TestController, mode: GridsEditMode): Promise => { + await t + .pressKey('tab') + .pressKey('ctrl+down') + .pressKey('enter'); + + if (mode === 'popup') { + const columns = await dataGrid.apiOption('columns'); + await t + .pressKey(columns.map(() => 'tab').join(' ')) + .pressKey('enter') + .wait(500); + } + }; + + const setEditorValue = async ( + t: TestController, + mode: GridsEditMode, + { dataField, newMaskValue, newValue }: ColumnInfo, + editor: CellEditor, + useKeyboard = false, + useMask = false, + ): Promise => { + if (dataField === 'boolean') { + if (useKeyboard) { + await t.pressKey('space'); + } else { + await t.click(editor.element); + } + + return; + } + + const value: string = useMask ? newMaskValue ?? '' : newValue; + + if (dataField === 'date' && !useKeyboard && !useMask) { + await t.click(editor.getDropDownButton()); + await t.click(Selector(`.${CLASS.calendarCell}`).withText(value.split('/')[1])); + + return; + } + + if (dataField === 'lookup' && !useKeyboard) { + if (mode === 'cell' || mode === 'batch') { + await t.click(editor.getDropDownButton()); + } + await t.click(Selector(`.${CLASS.listItemContent}`).withText(value)); + + return; + } + + await t.typeText(editor.element, value, { replace: true }); + + if (dataField === 'lookup' && useKeyboard) { + await Selector(`.${CLASS.listItemContent}`).withText(value)(); + await t.pressKey('enter'); + } + }; + + const focusNextCellEditor = async ( + t: TestController, + mode: GridsEditMode, + { columnIndex }: ColumnInfo, + useKeyboard = false, + ): Promise<{ nextColumnIndex: number }> => { + const form = getEditForm(mode); + const nextColumnIndex = columnIndex === 0 ? 1 : columnIndex - 1; + const nextColumnInfo = basicColumnInfos[nextColumnIndex]; + + let nextEditor: CellEditor | undefined; + let nextCell: DataCell | undefined; + + if (form) { + if (nextColumnInfo) { + nextEditor = new CellEditor(form.getItem(nextColumnInfo.dataField)); + if (useKeyboard) { + await t.pressKey(columnIndex === 0 ? 'tab' : 'shift+tab'); + } else { + await t.click(nextEditor.element); + } + } + } else { + nextCell = dataGrid.locator('td').nth(0, nextColumnIndex); + nextEditor = nextCell.locator('.dx-editor-cell'); + + if (useKeyboard) { + await t.pressKey(columnIndex === 0 ? 'tab' : 'shift+tab'); + } else { + const isCellRevertBug = mode === 'cell' && columnIndex < nextColumnIndex; + await t.click(nextCell.element, { offsetX: isCellRevertBug ? 50 : 5 }); + } + } + + await checkCellFocused(t, mode, nextColumnInfo, nextCell, nextEditor); + + return { nextColumnIndex }; + }; + + const getSaveButton = ( + mode: string, + form: EditForm | null, + ): Selector | undefined => { + switch (mode) { + case 'batch': + return dataGrid.getHeaderPanel().getSaveButton(); + case 'row': + return dataGrid.getDataRow(0).locator('.dx-command-edit').nth(6).locator('.dx-link').nth(0); + case 'cell': + return Selector('body'); + default: + return form?.saveButton; + } + }; + + const getCellText = async ( + dataField: string, + cell: DataCell, + ): Promise => { + if (dataField === 'boolean') { + return await cell.locator('.dx-editor-cell').isChecked() ? 'true' : 'false'; + } + + return cell.element.textContent; + }; + + const checkSavedCell = async ( + t: TestController, + { dataField, newValue }: ColumnInfo, + cell: DataCell, + ): Promise => { + await t + .expect(await getCellText(dataField, cell)) + .eql(newValue) + .expect(cell.isEditCell) + .eql(dataField === 'boolean') + .expect(cell.isModified) + .notOk() + .expect(DataCell.getModifiedCells().count) + .eql(0); + }; + + const getEditorValue = async ( + dataField: string, + editor: CellEditor, + ): Promise => { + if (dataField === 'boolean') { + return await editor.isChecked() ? 'true' : 'false'; + } + + return editor.element.value; + }; + + const checkModifiedCell = async ( + t: TestController, + mode: string, + { dataField, newValue }: ColumnInfo, + cell: DataCell, + editor: CellEditor, + modifiedCellsCount: number, + ): Promise => { + const editorText = mode === 'batch' || mode === 'cell' + ? await getCellText(dataField, cell) + : await getEditorValue(dataField, editor); + + await t + .expect(editorText) + .eql(newValue); + + if (mode !== 'form' && mode !== 'popup') { + await t + .expect(cell.isEditCell) + .eql(mode === 'row' || dataField === 'boolean') + .expect(cell.isModified) + .eql(mode === 'batch') + .expect(DataCell.getModifiedCells().count) + .eql(modifiedCellsCount); + } + }; + + editingModes.forEach((mode) => { + const configurations = [ + { + repaintChangesOnly: true, + useMask: false, + columnInfos: textColumnInfos, + expectedResult: expectedTextColumnResult, + }, + { + repaintChangesOnly: false, + useMask: true, + columnInfos: maskedColumnInfos, + expectedResult: expectedMaskedColumnResult, + }, + { + repaintChangesOnly: false, + useMask: false, + columnInfos: basicColumnInfos, + expectedResult: expectedBasicColumnResult, + }, + ]; + + configurations.forEach(({ + repaintChangesOnly, useMask, columnInfos, expectedResult, + }) => { + + test( + `Update cell value, mode: ${mode}, repaintChangesOnly: ${repaintChangesOnly}, useKeyboard: false, useMask: ${useMask}`, + async ({ page }) => { + const form = getEditForm(mode); + + if (mode === 'batch') { + await dataGrid.apiCellValue(0, 0, 'modified'); + } + + // eslint-disable-next-line no-restricted-syntax + for (const columnInfo of columnInfos) { + const cell = page.locator('.dx-data-row').nth(0).locator('td').nth(columnInfo.columnIndex); + const editor = form + ? new CellEditor(form.getItem(columnInfo.dataField)) + : cell.locator('.dx-editor-cell'); + + await clickCellEditor(t, mode, columnInfo, form, cell, editor); + await setEditorValue(t, mode, columnInfo, editor, false, useMask); + } + + const saveButton = getSaveButton(mode, form); + + if (saveButton) { + await (saveButton).click({ offsetX: 5, offsetY: 5 }); + } + + // eslint-disable-next-line no-restricted-syntax + for (const columnInfo of expectedResult) { + await checkSavedCell( + t, + columnInfo, + page.locator('.dx-data-row').nth(0).locator('td').nth(columnInfo.columnIndex), + ); + } + }, + ).before(createDataGrid({ + mode, + useMask, + repaintChangesOnly, + })); + }); + + [true, false].forEach((repaintChangesOnly) => { + test( + `Update calculated cell value, mode: ${mode}, repaintChangesOnly: ${repaintChangesOnly}, useKeyboard: false, useMask:false`, + async ({ page }) => { + const columnInfo = { columnIndex: 5, dataField: 'calculated', newValue: '9' }; + const form = getEditForm(mode); + const cell = page.locator('.dx-data-row').nth(0).locator('td').nth(columnInfo.columnIndex); + const editor = form ? new CellEditor(form.getItem(columnInfo.dataField)) : cell.locator('.dx-editor-cell'); + + await clickCellEditor(t, mode, columnInfo, form, cell, editor); + await setEditorValue(t, mode, columnInfo, editor); + + const saveButton = getSaveButton(mode, form); + + if (saveButton) { + if (!repaintChangesOnly && (mode === 'row' || mode === 'form')) { + await ('body').click(); + } + + await (saveButton).click({ offsetX: 5, offsetY: 5 }); + } + + const expectedColumnResult: ColumnInfo[] = [ + { columnIndex: 1, dataField: 'number', newValue: '8' }, + { columnIndex: 5, dataField: 'calculated', newValue: '9' }, + ]; + + // eslint-disable-next-line no-restricted-syntax + for (const resultColumnInfo of expectedColumnResult) { + await checkSavedCell( + t, + resultColumnInfo, + page.locator('.dx-data-row').nth(0).locator('td').nth(resultColumnInfo.columnIndex), + ); + } + }, + ).before(createDataGrid({ + mode, + repaintChangesOnly, + })); + }); + + const keyboardConfigurations = [ + { + useMask: true, + columnInfos: maskedColumnInfos, + expectedResult: expectedMaskedColumnResult, + }, + { + useMask: false, + columnInfos: basicColumnInfos, + expectedResult: expectedBasicColumnResult, + }, + ]; + + keyboardConfigurations.forEach(({ useMask, columnInfos, expectedResult }) => { + test( + `Update cell value, mode: ${mode}, repaintChangesOnly: false, useKeyboard: true, useMask: ${useMask}`, + async ({ page }) => { + const form = getEditForm(mode); + + expect(await page.locator('.dx-datagrid').first().isVisible()).toBeTruthy(); + await moveToFirstCellEditor(t, mode); + + // eslint-disable-next-line no-restricted-syntax + for (const columnInfo of columnInfos) { + const cell = page.locator('.dx-data-row').nth(0).locator('td').nth(columnInfo.columnIndex); + const editor = form + ? new CellEditor(form.getItem(columnInfo.dataField)) + : cell.locator('.dx-editor-cell'); + + if (columnInfo.columnIndex > 0) { + await page.keyboard.press('tab'); + } + + await checkCellFocused(t, mode, columnInfo, cell, editor); + await setEditorValue(t, mode, columnInfo, editor, true, useMask); + } + + await page.keyboard.press('enter'); + + if (mode === 'batch' || mode === 'popup') { + const saveButton = getSaveButton(mode, form); + + if (saveButton) { + await (saveButton).click({ offsetX: 5, offsetY: 5 }); + } + } + + // eslint-disable-next-line no-restricted-syntax + for (const columnInfo of expectedResult) { + await checkSavedCell( + t, + columnInfo, + page.locator('.dx-data-row').nth(0).locator('td').nth(columnInfo.columnIndex), + ); + } + }, + ).before(createDataGrid({ + mode, + useMask, + })); + }); + + test( + `Update cell value and focus next cell, mode: ${mode}, repaintChangesOnly: false, useKeyboard: true`, + async ({ page }) => { + const form = getEditForm(mode); + let activeColumnIndex = 0; + let modifiedCellCount = mode === 'batch' ? 1 : 0; + + expect(await page.locator('.dx-datagrid').first().isVisible()).toBeTruthy(); + await moveToFirstCellEditor(t, mode); + + // eslint-disable-next-line no-restricted-syntax + for (const columnInfo of textColumnInfos) { + const cell = page.locator('.dx-data-row').nth(0).locator('td').nth(columnInfo.columnIndex); + const editor = form ? new CellEditor(form.getItem(columnInfo.dataField)) : cell.locator('.dx-editor-cell'); + + for (let i = activeColumnIndex; i < columnInfo.columnIndex; i += 1) { + await page.keyboard.press('tab'); + } + + await checkCellFocused(t, mode, columnInfo, cell, editor); + await setEditorValue(t, mode, columnInfo, editor, true); + + const { nextColumnIndex } = await focusNextCellEditor(t, mode, columnInfo, true); + activeColumnIndex = nextColumnIndex; + + if (mode === 'batch') { + modifiedCellCount += 1; + } + + await checkModifiedCell(t, mode, columnInfo, cell, editor, modifiedCellCount); + } + }, + ).before(createDataGrid({ + mode, + })); + + [true, false].forEach((repaintChangesOnly) => { + test( + `Update cell value and focus next cell, mode: ${mode}, repaintChangesOnly: ${repaintChangesOnly}, useKeyboard: false`, + async ({ page }) => { + const form = getEditForm(mode); + let modifiedCellCount = mode === 'batch' ? 1 : 0; + + // eslint-disable-next-line no-restricted-syntax + for (const columnInfo of textColumnInfos) { + const cell = page.locator('.dx-data-row').nth(0).locator('td').nth(columnInfo.columnIndex); + const editor = form + ? new CellEditor(form.getItem(columnInfo.dataField)) + : cell.locator('.dx-editor-cell'); + + await clickCellEditor(t, mode, columnInfo, form, cell, editor); + await setEditorValue(t, mode, columnInfo, editor); + + await focusNextCellEditor(t, mode, columnInfo); + + if (mode === 'batch') { + modifiedCellCount += 1; + } + + await checkModifiedCell(t, mode, columnInfo, cell, editor, modifiedCellCount); + } + }, + ).before(createDataGrid({ + mode, + repaintChangesOnly, + })); + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/editing/editingEvents.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/editing/editingEvents.spec.ts new file mode 100644 index 000000000000..0988f4121ed0 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/editing/editingEvents.spec.ts @@ -0,0 +1,118 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('Editing events', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + /* eslint-disable @typescript-eslint/no-misused-promises */ + + ); + + // T1186997 + const testCases = [{ + caseName: 'e.cancel = promise:true', + expected: true, + + onRowUpdating: ClientFunction((e) => { + e.cancel = new Promise((resolve) => { + resolve(true); + }); + }), + onRowInserting: ClientFunction((e) => { + e.cancel = new Promise((resolve) => { + resolve(true); + }); + }), + onRowRemoving: ClientFunction((e) => { + e.cancel = new Promise((resolve) => { + resolve(true); + }); + }), + }, { + caseName: 'e.cancel = true', + expected: true, + + onRowUpdating: ClientFunction((e) => { + e.cancel = true; + }), + onRowInserting: ClientFunction((e) => { + e.cancel = true; + }), + onRowRemoving: ClientFunction((e) => { + e.cancel = true; + }), + }, { + caseName: 'e.cancel = promise:false', + expected: false, + + onRowUpdating: ClientFunction((e) => { + e.cancel = new Promise((resolve) => { + resolve(false); + }); + }), + onRowInserting: ClientFunction((e) => { + e.cancel = new Promise((resolve) => { + resolve(false); + }); + }), + onRowRemoving: ClientFunction((e) => { + e.cancel = new Promise((resolve) => { + resolve(false); + }); + }), + }, { + caseName: 'e.cancel = false', + expected: false, + + onRowUpdating: ClientFunction((e) => { + e.cancel = false; + }), + onRowInserting: ClientFunction((e) => { + e.cancel = false; + }), + onRowRemoving: ClientFunction((e) => { + e.cancel = false; + }), + }]; + + // onRowUpdating + testCases.forEach(({ caseName, expected, onRowUpdating }) => { + + test(`onRowUpdating event should be work valid in case '${caseName}'`, async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: [{ + ID: 1, + FirstName: 'John', + }], + columns: [{ + dataField: 'FirstName', + caption: 'Firs tName', + }], + height: 300, + editing: { + mode: 'row', + allowUpdating: true, + }, + onRowUpdating, + }); + + const dataRow = page.locator('.dx-data-row').nth(0); + + await (dataRow.locator('td').nth(1).click().getLinkEdit()); + + await (dataRow.locator('td').nth(0).locator('.dx-editor-cell')).fill('test text'); + await (dataRow.locator('td').nth(1).click().getLinkSave()); + + expect(await dataRow.locator('td').nth(1).getLinkSave().exists).toBe(expected); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/editing/editingNewRow.functional_matrix.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/editing/editingNewRow.functional_matrix.spec.ts new file mode 100644 index 000000000000..78c68f420494 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/editing/editingNewRow.functional_matrix.spec.ts @@ -0,0 +1,239 @@ +import { test, expect } from '@playwright/test'; +import { createWidget } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('Editing.NewRow', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + interface ColumnInfo { + columnIndex: number; + dataField: string; + newValue: string; + } + + const createDataGrid = (mode: GridsEditMode, repaintChangesOnly = false) => async (): Promise => createWidget(page, 'dxDataGrid', { + keyExpr: 'id', + dataSource: [ + { + id: 1, text: 'text 1', number: 1, date: '2020-10-27', boolean: false, lookup: 1, + }, + { + id: 2, text: 'text 2', number: 2, date: '2020-10-28', boolean: true, lookup: 2, + }, + ], + repaintChangesOnly, + editing: { + mode, + allowAdding: true, + allowUpdating: true, + }, + columns: [ + { dataField: 'text' }, + { + dataField: 'number', + editorOptions: { + format: '#0', + }, + }, + { + dataField: 'date', + dataType: 'date', + editorOptions: { + pickerType: 'calendar', + }, + }, + { + dataField: 'lookup', + lookup: { + valueExpr: 'id', + displayExpr: 'text', + dataSource: [ + { id: 1, text: 'lookup 1' }, + { id: 2, text: 'lookup 2' }, + ], + }, + }, + { dataField: 'boolean' }, + { + dataField: 'calculated', + calculateCellValue: (data): number => (data as { number: number }).number + 1, + setCellValue: (newData, value): void => { + newData.number = value - 1; + }, + }, + ], + }); + + const expectedCalculatedColumnResult: ColumnInfo[] = [ + { columnIndex: 1, dataField: 'number', newValue: '8' }, + { columnIndex: 5, dataField: 'calculated', newValue: '9' }, + ]; + + const dataGrid = new DataGrid('#container'); + + const getEditForm = (mode: GridsEditMode): EditForm | null => { + if (mode === 'form') { + return dataGrid.getEditForm(); + } + if (mode === 'popup') { + return dataGrid.getPopupEditForm(); + } + return null; + }; + + const addRow = async ( + t: TestController, + { columnIndex }: { columnIndex: number }, + form: EditForm | null, + cell: DataCell, + editor: CellEditor, + ): Promise => { + const addRowButton = dataGrid.getHeaderPanel().getAddRowButton(); + await t.click(addRowButton); + + if (columnIndex > 0) { + const item = !form ? cell.element : editor.getItemLabel(); + await t.click(item, { offsetX: 5 }); + } + }; + + const checkCellFocused = async ( + t: TestController, + mode: GridsEditMode, + { dataField }: ColumnInfo, + cell: DataCell | undefined, + editor: CellEditor | undefined, + ): Promise => { + if (mode !== 'form' && mode !== 'popup') { + await t.expect(cell?.isFocused).ok(); + } + + const isRowBasedMode = mode === 'row'; + const isFormBasedMode = mode === 'form' || mode === 'popup'; + + if ((isRowBasedMode || isFormBasedMode) && dataField === 'boolean') { + return; + } + + await t + .expect(editor?.element.focused) + .eql(true); + }; + + const getSaveButton = ( + mode: string, + form: EditForm | null, + ): Selector | undefined => { + switch (mode) { + case 'batch': + return dataGrid.getHeaderPanel().getSaveButton(); + case 'row': + return dataGrid.getDataRow(0).locator('.dx-command-edit').nth(6).locator('.dx-link').nth(0); + case 'cell': + return Selector('body'); + default: + return form?.saveButton; + } + }; + + const getCellText = async ( + dataField: string, + cell: DataCell, + ): Promise => { + if (dataField === 'boolean') { + return await cell.locator('.dx-editor-cell').isChecked() ? 'true' : 'false'; + } + + return cell.element.textContent; + }; + + const checkSavedCell = async ( + t: TestController, + { dataField, newValue }: ColumnInfo, + cell: DataCell, + ): Promise => { + await t + .expect(await getCellText(dataField, cell)) + .eql(newValue) + .expect(cell.isEditCell) + .eql(dataField === 'boolean') + .expect(cell.isModified) + .notOk() + .expect(DataCell.getModifiedCells().count) + .eql(0); + }; + + const modes: GridsEditMode[] = ['cell', 'batch', 'row', 'form', 'popup']; + + modes.forEach((mode) => { + [true, false].forEach((repaintChangesOnly) => { + + test( + `Update cell value in new row, mode: ${mode}, repaintChangesOnly: ${repaintChangesOnly}`, + async ({ page }) => { + const columnInfo = { columnIndex: 0, dataField: 'text', newValue: 'xxxx' }; + const form = getEditForm(mode); + const cell = page.locator('.dx-data-row').nth(0).locator('td').nth(columnInfo.columnIndex); + const editor = form ? new CellEditor(form.getItem(columnInfo.dataField)) : cell.locator('.dx-editor-cell'); + + await addRow(t, columnInfo, form, cell, editor); + + await checkCellFocused(t, mode, columnInfo, cell, editor); + + await t.typeText(editor.element, columnInfo.newValue, { replace: true }); + + const saveButton = getSaveButton(mode, form); + + if (saveButton) { + await (saveButton).click({ offsetX: 5, offsetY: 5 }); + } + + await checkSavedCell(t, columnInfo, page.locator('.dx-data-row').nth(2).locator('td').nth(columnInfo.columnIndex)); + }, + ).before(createDataGrid(mode, repaintChangesOnly)); + + test( + `Update calculated cell value in new row, mode: ${mode}, repaintChangesOnly: ${repaintChangesOnly}`, + async ({ page }) => { + const columnInfo = { columnIndex: 5, dataField: 'calculated', newValue: '9' }; + const form = getEditForm(mode); + const cell = page.locator('.dx-data-row').nth(0).locator('td').nth(columnInfo.columnIndex); + const editor = form ? new CellEditor(form.getItem(columnInfo.dataField)) : cell.locator('.dx-editor-cell'); + + await addRow(t, columnInfo, form, cell, editor); + + await checkCellFocused(t, mode, columnInfo, cell, editor); + + await t.typeText(editor.element, columnInfo.newValue, { replace: true }); + + if (!repaintChangesOnly && (mode === 'row' || mode === 'form')) { + await ('body').click(); + } + + const saveButton = getSaveButton(mode, form); + + if (saveButton) { + await (saveButton).click({ offsetX: 5, offsetY: 5 }); + } + + // eslint-disable-next-line no-restricted-syntax + for (const resultColumnInfo of expectedCalculatedColumnResult) { + await checkSavedCell( + t, + resultColumnInfo, + page.locator('.dx-data-row').nth(2).locator('td').nth(resultColumnInfo.columnIndex), + ); + } + }, + ).before(createDataGrid(mode, repaintChangesOnly)); + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/editing/functional.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/editing/functional.spec.ts new file mode 100644 index 000000000000..c026d271a5c8 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/editing/functional.spec.ts @@ -0,0 +1,62 @@ +import { test, expect } from '@playwright/test'; +import { createWidget } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('Editing.Functional', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + const getGridConfig = (config): Record => { + const defaultConfig = { + errorRowEnabled: true, + dataSource: [{ + id: 1, name: 'Alex', age: 15, lastName: 'John', + }], + keyExpr: 'id', + legacyRendering: false, + }; + + return config ? { ...defaultConfig, ...config } : defaultConfig; + }; + + test('Focused cell should be switched to the editing mode after onSaving\'s promise is resolved (T1190566)', async ({ page }) => { + await page.evaluate(() => { + (window as any).deferred = $.Deferred(); + }); + + return createWidget(page, 'dxDataGrid', { + dataSource: [ + { id: 1, field1: 'value1' }, + { id: 2, field1: 'value2' }, + { id: 3, field1: 'value3' }, + { id: 4, field1: 'value4' }, + ], + keyExpr: 'id', + showBorders: true, + columns: ['field1'], + editing: { + mode: 'cell', + allowUpdating: true, + }, + onSaving(e) { + e.promise = (window as any).deferred; + }, + }); + + const resolveOnSavingDeferred = ClientFunction(() => (window as any).deferred.resolve()); + + await (page.locator('.dx-data-row').click().nth(0).locator('td').nth(0)); + await (page.locator('.dx-data-row').nth(0).locator('td').nth(0)).fill('new_value'); + await page.keyboard.press('tab tab'); + await resolveOnSavingDeferred(); + expect(await page.locator('.dx-data-row').nth(2).locator('td').nth(0).isEditCell).toBeTruthy(); + }); + // TODO: .after() block removed +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/editing/initNewRow.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/editing/initNewRow.spec.ts new file mode 100644 index 000000000000..a460760cdb3e --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/editing/initNewRow.spec.ts @@ -0,0 +1,49 @@ +import { test, expect } from '@playwright/test'; +import { createWidget } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('initNewRow', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + // T1274123 + + test('No errors should be thrown if inserting new row after cancelling insert on second page', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: [...new Array(40)].map((_, index) => ({ id: index + 1, text: `item ${index + 1}` })), + keyExpr: 'id', + paging: { + pageIndex: 1, + }, + columns: ['id', 'text'], + showBorders: true, + editing: { mode: 'popup', allowAdding: true }, + onInitNewRow(e) { + e.data.id = 0; + e.data.text = 'test'; + }, + height: 300, + }); + + await (; + page.locator('.dx-datagrid-header-panel').click().getAddRowButton(), + ) + .click( + dataGrid.getPopupEditForm().cancelButton, + ); + + await (page.locator('.dx-datagrid-header-panel').click().getAddRowButton(), + ); + + expect(await + dataGrid.getPopupEditForm().element.exists, + ).toBeTruthy(); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/editing/undefinedValues.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/editing/undefinedValues.spec.ts new file mode 100644 index 000000000000..d3dcd2ac331f --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/editing/undefinedValues.spec.ts @@ -0,0 +1,54 @@ +import { test, expect } from '@playwright/test'; +import { createWidget } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('Editing - undefined values', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + fixture.disablePageReloads`Editing - undefined values` + .disablePageReloads + .page(url(__dirname, '../../../container.html')); + + test('Should properly set nested undefined values (T1226946)', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: [{ + id: 0, + value: { + data: 100, + }, + }, { + id: 1, + value: { + data: undefined, + }, + }], + keyExpr: 'id', + columns: [{ + dataField: 'value', + customizeText: (cellInfo) => String(cellInfo.value.data ?? 'undefined'), + }], + showBorders: true, + }); + + const firstCell = page.locator('.dx-data-row').nth(0).locator('td').nth(0); + const secondCell = page.locator('.dx-data-row').nth(1).locator('td').nth(0); + + expect(await firstCell.element().textContent).toBe('100'); + expect(await secondCell.element().textContent).toBe('undefined'); + + await dataGrid.apiCellValue(0, 0, { data: undefined }); + await dataGrid.apiSaveEditData(); + + expect(await firstCell.element().textContent).toBe('undefined'); + expect(await secondCell.element().textContent).toBe('undefined'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/editing/visual.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/editing/visual.spec.ts new file mode 100644 index 000000000000..51a1939e85c8 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/editing/visual.spec.ts @@ -0,0 +1,47 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('Editing.Visual', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + const encodedIcon = 'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iaXNvLTg4NTktMSI/Pg0KPCFET0NUWVBFIHN2ZyBQVUJMSUMgIi0vL1czQy8vRFREIFNWRyAxLjEvL0VOIiAiaHR0cDovL3d3dy53My5vcmcvR3JhcGhpY3MvU1ZHLzEuMS9EVEQvc3ZnMTEuZHRkIj4NCjxzdmcgIHdpZHRoPSIyMHB4IiBoZWlnaHQ9IjIwcHgiIHZpZXdCb3g9IjAgMCAyMCAyMCIgZmlsbD0iIzAwMDAwMCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPg0KCTxyZWN0IHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIC8+DQo8L3N2Zz4NCg=='; + + test('The E0110 should not occur when editing a column with setCellValue in form mode (T1193894)', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: [{ + ID: 1, + Name: 'test', + }], + keyExpr: 'ID', + editing: { + mode: 'form', + allowUpdating: true, + editRowKey: 1, + }, + columns: [{ + dataField: 'Name', + setCellValue(rowData, value) { + rowData.Name = value; + }, + }], + // @ts-expect-error private option + templatesRenderAsynchronously: true, + }); + + // act + await (dataGrid.getFormItemEditor(0)).fill('new'); + await (dataGrid.getEditForm().click().saveButton); + + // assert + await testScreenshot(page, 'grid-form-editing-T1193894.png', { element: page.locator('#container') }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/export/export.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/export/export.spec.ts new file mode 100644 index 000000000000..cce71359c670 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/export/export.spec.ts @@ -0,0 +1,31 @@ +import { test, expect } from '@playwright/test'; +import { createWidget } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('Export', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + const GRID_CONTAINER = '#container'; + + test('Warning should be thrown in console if exporting is enabled, but onExporting is not specified', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: [], + export: { + enabled: true, + }, + }); + + const consoleMessages = await t.getBrowserConsoleMessages(); + const isWarningExist = !!consoleMessages?.warn.find((message) => message.startsWith('W1024')); + + expect(await isWarningExist).toBeTruthy(); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/exportButton.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/exportButton.spec.ts new file mode 100644 index 000000000000..2eb6a3bf0894 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/exportButton.spec.ts @@ -0,0 +1,26 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('Export button', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + test('allowExportSelectedData: false, menu: false', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: [{ id: 1, value: 2 }], + export: { + enabled: true, + }, + }); + + await testScreenshot(page, 'grid-export-one-button.png', { element: page.locator('.dx-datagrid-header-panel') }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/filterPanel/functional.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/filterPanel/functional.spec.ts new file mode 100644 index 000000000000..a815d408268a --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/filterPanel/functional.spec.ts @@ -0,0 +1,157 @@ +import { test, expect } from '@playwright/test'; +import { createWidget } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('Filtering', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + const GRID_CONTAINER = '#container'; + + // T1319193, T1311486 + + test('Proper handle custom filter operations for dates with non-date values', async ({ page }) => { + const dataSource = [{ + ID: 1, + OrderNumber: 35711, + OrderDate: '2017/01/12', + Employee: 'Jim Packard', + }, { + ID: 5, + OrderNumber: 35714, + OrderDate: '2017/01/22', + Employee: 'Harv Mudd', + }, { + ID: 7, + OrderNumber: 35983, + OrderDate: '2017/02/07', + Employee: 'Todd Hoffman', + }, { + ID: 14, + OrderNumber: 39420, + OrderDate: '2017/02/15', + Employee: 'Jim Packard', + }, { + ID: 15, + OrderNumber: 39874, + OrderDate: '2017/02/04', + Employee: 'Harv Mudd', + }]; + + return createWidget(page, 'dxDataGrid', { + dataSource, + keyExpr: 'ID', + filterRow: { visible: true }, + filterPanel: { visible: true }, + headerFilter: { visible: true }, + filterBuilder: { + customOperations: [ + { + name: 'weekends', + caption: 'Weekends', + dataTypes: ['date'], + icon: 'check', + hasValue: false, + calculateFilterExpression() { + function getOrderDay(rowData: { OrderDate: string }) { + return (new Date(rowData.OrderDate)).getDay(); + } + + return [[getOrderDay, '=', 0], 'or', [getOrderDay, '=', 6]]; + }, + }, + ], + }, + columns: [ + 'OrderNumber', + { + dataField: 'OrderDate', + dataType: 'date', + calculateFilterExpression(value, selectedFilterOperations, target) { + if (target === 'headerFilter' && value === 'weekends') { + function getOrderDay(rowData: { OrderDate: string }) { + return (new Date(rowData.OrderDate)).getDay(); + } + + return [[getOrderDay, '=', 0], 'or', [getOrderDay, '=', 6]]; + } + return this.defaultCalculateFilterExpression?.( + value, + selectedFilterOperations, + target, + ) ?? []; + }, + headerFilter: { + dataSource(data) { + if (data.dataSource) { + data.dataSource.postProcess = (results) => { + results.push({ + text: 'Weekends', + value: 'weekends', + }); + return results; + }; + } + }, + }, + }, + 'Employee', + ], + }); + + const filterPanel = dataGrid.getFilterPanel(); + + let filterBuilderPopup = await filterPanel.openFilterBuilderPopup(t); + let filterBuilder = filterBuilderPopup.getFilterBuilder(); + + await (filterBuilder.getAddButton().click()); + expect(await FilterBuilder.getPopupTreeView().visible).toBeTruthy(); + await (FilterBuilder.getPopupTreeViewNodeByText('Add Condition').click()); + await (filterBuilder.getField(0, 'item').click().element); + await (FilterBuilder.getPopupTreeViewNodeByText('Order Date').click()); + await (filterBuilder.getField(0, 'itemOperation').click().element); + await (FilterBuilder.getPopupTreeViewNodeByText('Is any of').click()); + await (filterBuilder.getField(0, 'itemValue').click().element); + await (FilterBuilder.getPopupTreeViewNodeCheckboxByText('Weekends').click()); + await (new Popup(FilterBuilder.getPopupTreeView().click()).getOkButton().element); + await (filterBuilderPopup.asPopup().click().getOkButton().element); + + expect(await dataGrid.getRows().count); + await t.eql(3); + expect(await filterPanel.getFilterText().element.innerText); + await t.eql('[Order Date] Is any of(\'Weekends\')'); + + filterBuilderPopup = await filterPanel.openFilterBuilderPopup(t); + filterBuilder = filterBuilderPopup.getFilterBuilder(); + + await (filterBuilder.getField(0, 'itemOperation').click().element); + await (FilterBuilder.getPopupTreeViewNodeByText('Weekends').click()); + await (filterBuilderPopup.asPopup().click().getOkButton().element); + + expect(await dataGrid.getRows().count); + await t.eql(3); + expect(await filterPanel.getFilterText().element.innerText); + await t.eql('[Order Date] Weekends'); + + const dateFilterCell = page.locator('.dx-datagrid-filter-row td').nth(1); + + await (dateFilterCell.menuButton).click(); + await (dateFilterCell.menu.getItemByText('Between').click()); + expect(await dataGrid.getFilterRangeOverlay().exists).toBeTruthy(); + await (dataGrid.getFilterRangeStartEditor().locator('input')).fill('2/1/2017'); + await (dataGrid.getFilterRangeEndEditor().locator('input')).fill('2/28/2017'); + await page.keyboard.press('enter'); + + expect(await dataGrid.getRows().count); + await t.eql(4); + expect(await filterPanel.getFilterText().element.innerText); + await t.eql('[Order Date] Is between(\'2/1/2017\', \'2/28/2017\')'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/filterPanel/visual.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/filterPanel/visual.spec.ts new file mode 100644 index 000000000000..d93a7219f33d --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/filterPanel/visual.spec.ts @@ -0,0 +1,40 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('filterPanel', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + // T1182854 + + test('editor\'s popup inside filterBuilder is opening & closing right (T1182854)', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: [{ column1: 'first' }], + columns: ['column1'], + filterValue: ['column1', 'anyof', []], + filterPanel: { + visible: true, + }, + }); + + const filterBuilder = ( + await dataGrid.getFilterPanel().openFilterBuilderPopup(t) + ).getFilterBuilder(); + + await testScreenshot(page, 'dataGrid-filterPanel-popup-focused.png'); + await (filterBuilder.getField().click().getValueText()); + await testScreenshot(page, 'dataGrid-filterPanel-popup.-with-editor-popup.png'); + await (filterBuilder.getField().click().getValueText()); + await testScreenshot(page, 'dataGrid-filterPanel-popup.png'); + await (filterBuilder.getField().click().getValueText()); + await testScreenshot(page, 'dataGrid-filterPanel-popup.-with-editor-popup.png'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/filterRow/T1163100_changeFIlterIcon.visual_matrix.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/filterRow/T1163100_changeFIlterIcon.visual_matrix.spec.ts new file mode 100644 index 000000000000..1da600b7bbbb --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/filterRow/T1163100_changeFIlterIcon.visual_matrix.spec.ts @@ -0,0 +1,68 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('Header Filter T1163100 change filter icon', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + const GRID_SELECTOR = '#container'; + + const generateTestData = (rowCount: number) => new Array(rowCount) + .fill(null) + .map((_, idx) => ({ + dataA: `A_${idx}`, + dataB: `B_${idx}`, + dataC: `C_${idx}`, + dataD: `D_${idx}`, + })); + + [ + ['usual', ['dataA', 'dataB']], + ['fixed', [{ dataField: 'dataA', fixed: true }, { dataField: 'dataB', fixed: true }]], + ].forEach(([firstColumnsName, firstColumns]) => { + [ + ['usual', ['dataC', 'dataD']], + ['band', [{ caption: 'Band column', columns: ['dataC', 'dataD'] }]], + ].forEach(([secondColumnsName, secondColumns]) => { + ([ + ['usual', undefined], + ['virtual', { columnRenderingMode: 'virtual', rowRenderingMode: 'virtual' }], + ] as const).forEach(([scrollingName, scrolling]) => { + + test(`Should change filter row icon (columns ${firstColumnsName} ${secondColumnsName}, scrolling ${scrollingName}`, async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: generateTestData(25), + filterRow: { + visible: true, + }, + columnFixing: { + // @ts-expect-error private option + legacyMode: true, + }, + columns: [ + ...firstColumns, + ...secondColumns, + ], + scrolling, + }); + + for (let columnIdx = 0; columnIdx < 4; columnIdx += 1) { + const filterCell = page.locator('.dx-datagrid-filter-row td').nth(columnIdx); + await (filterCell.menuButton).click(); + await (filterCell.menu.getItemByText('Starts with').click()); + } + + await testScreenshot(page, + `T1163100_change-icon_columns-${firstColumnsName}-${secondColumnsName}_scrolling-${scrollingName}.png`, + { element: page.locator('#container') }, + ); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/filterRow/functional.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/filterRow/functional.spec.ts new file mode 100644 index 000000000000..1cbce838b940 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/filterRow/functional.spec.ts @@ -0,0 +1,58 @@ +import { test, expect } from '@playwright/test'; +import { createWidget } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('FilterRow', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + test('Filter should reset if the filter row editor text is cleared (T1257261)', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: [ + { ID: 1, Text: 'Item 1' }, + { ID: 2, Text: '' }, + { ID: 3, Text: 'Item 3' }, + ], + keyExpr: 'ID', + showBorders: true, + remoteOperations: true, + headerFilter: { visible: true }, + filterRow: { visible: true }, + filterPanel: { visible: true }, + filterValue: ['Text', '=', 'i'], + columns: ['ID', { + dataField: 'Text', + selectedFilterOperation: '=', + }], + onEditorPreparing(e: any) { + e.updateValueTimeout = 100; + }, + }); + + const filterEditor = dataGrid.getFilterEditor(1, TextBox); + const filterPanelText = dataGrid.getFilterPanel().getFilterText(); + + // assert + .expect(filterPanelText.element.textContent) + .eql('[Text] Equals \'i\'') + // act + .click(filterEditor.locator('input')) + .pressKey('backspace') + .wait(100) // updateValueTimeout + // assert + .expect(filterPanelText.element.textContent) + .eql('Create Filter') + // act + .click(page.locator('#container')) + // assert + .expect(filterPanelText.element.textContent) + .eql('Create Filter'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/filterRow/visual.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/filterRow/visual.spec.ts new file mode 100644 index 000000000000..a93e33a802f6 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/filterRow/visual.spec.ts @@ -0,0 +1,32 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('FilterRow', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + test('Filter row\'s height should be adjusted by content (T1072609)', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + columns: [{ + dataField: 'Date', + dataType: 'date', + width: 140, + selectedFilterOperation: 'between', + filterValue: [new Date(2022, 2, 28), new Date(2022, 2, 29)], + }], + filterRow: { visible: true }, + wordWrapEnabled: true, + showBorders: true, + }); + + await testScreenshot(page, 'T1072609.png', { element: page.locator('#container') }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/filtering/functional.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/filtering/functional.spec.ts new file mode 100644 index 000000000000..9a44c950ec9f --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/filtering/functional.spec.ts @@ -0,0 +1,58 @@ +import { test, expect } from '@playwright/test'; +import { createWidget } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('Filtering', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + const GRID_CONTAINER = '#container'; + + // T1311818 + + test('Don\'t calculate additional filter when filtering column list is empty', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + keyExpr: 'id', + filterValue: ['id', '>=', 1], + dataSource: null, + columns: [], + showBorders: true, + }); + + // arrange + const consoleMessages = await t.getBrowserConsoleMessages(); + + // act + await dataGrid.option({ + columns: [ + { dataField: 'id', caption: 'ID', dataType: 'number' }, + { dataField: 'name', caption: 'Name', dataType: 'string' }, + ], + dataSource: [ + { id: 1, name: 'Item 1' }, + { id: 2, name: 'Item 2' }, + { id: 3, name: 'Item 3' }, + ], + }); + + // assert + expect(await page.locator('.dx-datagrid').first().isVisible()).toBeTruthy(); + + // act + await dataGrid.option({ + columns: [], + dataSource: undefined, + }); + + // assert + expect(await consoleMessages.error.every((msg) => !msg.includes('E1047'))); + await t.ok(); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/filtering/visual.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/filtering/visual.spec.ts new file mode 100644 index 000000000000..2bb64c10ffd0 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/filtering/visual.spec.ts @@ -0,0 +1,53 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('Filtering', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + const GRID_CONTAINER = '#container'; + + test('Data should be filtered if True is selected via the filter method when case sensitivity is enabled', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: { + store: [ + { ID: 1, text: 'true' }, + { ID: 2, text: 'True' }, + ], + langParams: { + locale: 'en-US', + collatorOptions: { + sensitivity: 'case', + }, + }, + }, + keyExpr: 'ID', + showBorders: true, + }); + + // arrange + // act + await dataGrid.apiFilter(['text', '=', 'true']); + + // assert + expect(await page.locator('.dx-datagrid').first().isVisible()).toBeTruthy(); + + await testScreenshot(page, 'filter-method-with-case-sensitive-1.png', { element: page.locator('#container') }); + + // act + await dataGrid.apiFilter(['text', '=', 'True']); + + // assert + expect(await page.locator('.dx-datagrid').first().isVisible()).toBeTruthy(); + + await testScreenshot(page, 'filter-method-with-case-sensitive-2.png', { element: page.locator('#container') }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/fixedColumns/functional.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/fixedColumns/functional.spec.ts new file mode 100644 index 000000000000..97fd40a9abd9 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/fixedColumns/functional.spec.ts @@ -0,0 +1,138 @@ +import { test, expect } from '@playwright/test'; +import { createWidget } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('FixedColumns', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + // T1156153 + + test('Fixed columns should have same width as not fixed columns with columnAutoWidth: true', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: [ + { + id: 0, + // long group name causes the issue + group: 'VERY LONG GROUP TEXT VERY LONG GROUP TEXT VERY LONG GROUP TEXT', + dataA: 'DATA_A', + dataB: 'DATA_B', + dataC: 'DATA_C', + dataD: 'DATA_D', + dataE: 'DATA_E', + dataF: 'DATA_F', + dataG: 'DATA_G', + dataH: 'DATA_H', + }, { + id: 1, + group: 0, + dataA: 'DATA_A', + dataB: 'DATA_B', + dataC: 'DATA_C', + dataD: 'DATA_D', + dataE: 'DATA_E', + dataF: 'DATA_F', + dataG: 'DATA_G', + dataH: 'DATA_H', + }, + ], + keyExpr: 'id', + allowColumnReordering: true, + showBorders: true, + grouping: { + autoExpandAll: true, + }, + columnAutoWidth: true, + scrolling: { mode: 'standard', useNative: true }, + columnFixing: { + // @ts-expect-error private option + legacyMode: true, + }, + columns: [ + { + dataField: 'dataA', + fixed: true, + }, + 'dataB', + 'dataC', + 'dataD', + 'dataE', + 'dataF', + 'dataG', + 'dataH', + { + dataField: 'group', + groupIndex: 0, + }, + ], + }); + + await createWidget(page, 'dxDataGrid', + { + dataSource: [ + { + id: 0, + group: 'VERY LONG GROUP TEXT VERY LONG GROUP TEXT VERY LONG GROUP TEXT', + dataA: 'DATA_A', + dataB: 'DATA_B', + dataC: 'DATA_C', + dataD: 'DATA_D', + dataE: 'DATA_E', + dataF: 'DATA_F', + dataG: 'DATA_G', + dataH: 'DATA_H', + }, { + id: 1, + group: 0, + dataA: 'DATA_A', + dataB: 'DATA_B', + dataC: 'DATA_C', + dataD: 'DATA_D', + dataE: 'DATA_E', + dataF: 'DATA_F', + dataG: 'DATA_G', + dataH: 'DATA_H', + }, + ], + keyExpr: 'id', + allowColumnReordering: true, + showBorders: true, + grouping: { + autoExpandAll: true, + }, + columnAutoWidth: true, + scrolling: { mode: 'standard', useNative: true }, + columns: [ + 'dataA', + 'dataB', + 'dataC', + 'dataD', + 'dataE', + 'dataF', + 'dataG', + 'dataH', + { + dataField: 'group', + groupIndex: 0, + }, + ], + }, + '#otherContainer', + ); + + const firstFixedCell = dataGridWidthFixedColumns.locator('td').nth(1, 0); + const firstCell = dataGridUsual.locator('td').nth(1, 0); + + const fixedCellWidth = await firstFixedCell.element().clientWidth; + const cellWidth = await firstCell.element().clientWidth; + + expect(await fixedCellWidth).toBe(cellWidth); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/fixedColumns/visual.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/fixedColumns/visual.spec.ts new file mode 100644 index 000000000000..bb6b28dcdfb5 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/fixedColumns/visual.spec.ts @@ -0,0 +1,89 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('FixedColumns', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + // T1148937 + + test('Hovering over a row should work correctly when there is a fixed column and a column with a cellTemplate (React)', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: [...new Array(2)].map((_, index) => ({ id: index, text: `item ${index}` })), + keyExpr: 'id', + renderAsync: false, + hoverStateEnabled: true, + templatesRenderAsynchronously: true, + columns: [ + { dataField: 'id', fixed: true }, + { dataField: 'text', cellTemplate: '#test' }, + ], + columnFixing: { + // @ts-expect-error private option + legacyMode: true, + }, + showBorders: true, + }); + + await page.waitForTimeout(100); + + // simulating async rendering in React + await page.evaluate(() => { + const dataGrid = ($('#container') as any).dxDataGrid('instance'); + + // eslint-disable-next-line no-underscore-dangle + dataGrid.getView('rowsView')._templatesCache = {}; + + // eslint-disable-next-line no-underscore-dangle + dataGrid._getTemplate = () => ({ + render(options) { + setTimeout(() => { + ($(options.container) as any).append(($('
') as any).text(options.model.value)); + options.deferred?.resolve(); + }, 100); + }, + }); + + dataGrid.repaint(); + }); + + await page.waitForTimeout(200); + + // arrange + const firstDataRow = page.locator('.dx-data-row').nth(0); + const firstFixedDataRow = dataGrid.getFixedDataRow(0); + const secondDataRow = page.locator('.dx-data-row').nth(1); + const secondFixedDataRow = dataGrid.getFixedDataRow(1); + // act + await (firstDataRow.element).hover(); + + // assert + await testScreenshot(page, 'T1148937-grid-hover-row-1.png', { element: page.locator('#container') }); + + expect(await firstDataRow.isHovered); + await t.ok(); + expect(await firstFixedDataRow.isHovered); + await t.ok(); + + // act + await (secondFixedDataRow.element).hover(); + + // assert + await testScreenshot(page, 'T1148937-grid-hover-row-2.png', { element: page.locator('#container') }); + + expect(await secondDataRow.isHovered); + await t.ok(); + expect(await secondFixedDataRow.isHovered); + await t.ok(); + expect(await compareResults.isValid()); + await t.ok(compareResults.errorMessages()); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/focus/focus.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/focus/focus.spec.ts new file mode 100644 index 000000000000..12416f501493 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/focus/focus.spec.ts @@ -0,0 +1,44 @@ +import { test, expect } from '@playwright/test'; +import { createWidget } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('Focus', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + const GRID_SELECTOR = '#container'; + const FOCUSED_CLASS = 'dx-focused'; + + test('Should remove dx-focused class on blur event from the cell', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: [ + { A: 0, B: 1, C: 2 }, + { A: 3, B: 4, C: 5 }, + { A: 6, B: 7, C: 8 }, + ], + editing: { + mode: 'batch', + allowUpdating: true, + startEditAction: 'dblClick', + }, + onCellClick: (event) => event.component.focus(event.cellElement), + }); + + const firstCell = page.locator('.dx-data-row').nth(0).locator('td').nth(1); + const secondCell = page.locator('.dx-data-row').nth(1).locator('td').nth(1); + + await (firstCell.element).click(); + await (secondCell.element).click(); + + expect(await firstCell.element().hasClass(FOCUSED_CLASS)).toBeFalsy(); + expect(await secondCell.element().hasClass(FOCUSED_CLASS)).toBeTruthy(); + }); + // TODO: .after() block removed +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/focus/focusEvents/newRows_T1162227.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/focus/focusEvents/newRows_T1162227.spec.ts new file mode 100644 index 000000000000..cb8470a0ad29 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/focus/focusEvents/newRows_T1162227.spec.ts @@ -0,0 +1,147 @@ +import { test, expect } from '@playwright/test'; +import { createWidget } from '../../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../../tests/container.html')}`; + +test.describe('Focused row - new rows T1162227', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + // TODO: Something wrong with test cleanup with 'disablePageReloads' + // old events from previous test still alive on the next test case run + // So, we should disable it for these tests until this problem exists. + ); + + type FocusCellChangingData = [ + [prevRowIdx: number, prevColumnIdx: number], + [rowIdx: number, columnIdx: number], + ]; + type FocusCellChangedData = [rowIdx: number, columnIdx: number]; + type FocusRowChangingData = [prevRowIdx: number, rowIdx: number]; + type FocusRowChangedData = [rowIdx: number]; + + const GRID_SELECTOR = '#container'; + + const initCallbackTesting = async () => { + await CallbackTestHelper.initClientTesting([ + 'cellFocusChanging', + 'cellFocusChanged', + 'rowFocusChanging', + 'rowFocusChanged', + ]); + }; + + const clearCallbackTesting = async () => { + await CallbackTestHelper.clearClientData([ + 'cellFocusChanging', + 'cellFocusChanged', + 'rowFocusChanging', + 'rowFocusChanged', + ]); + }; + + const collectEventsCallbackResults = async () => [ + await CallbackTestHelper.getClientResults('cellFocusChanging'), + await CallbackTestHelper.getClientResults('cellFocusChanged'), + await CallbackTestHelper.getClientResults('rowFocusChanging'), + await CallbackTestHelper.getClientResults('rowFocusChanged'), + ]; + + const getGridDataConfig = (size: number) => ({ + keyExpr: 'id', + dataSource: new Array(size).fill(null).map((_, idx) => ({ + id: idx, + dataA: `dataA_${idx}`, + dataB: `dataB_${idx}`, + dataC: `dataC_${idx}`, + })), + columns: ['dataA', 'dataB', 'dataC'], + }); + + const getGridEventsConfig = () => ({ + onFocusedCellChanging: ({ + prevRowIndex, + prevColumnIndex, + newRowIndex, + newColumnIndex, + }) => { + (window as WindowCallbackExtended) + .clientTesting! + .addCallbackResult('cellFocusChanging', [ + [prevRowIndex, prevColumnIndex], [newRowIndex, newColumnIndex], + ]); + }, + onFocusedCellChanged: ({ rowIndex, columnIndex }) => { + (window as WindowCallbackExtended) + .clientTesting! + .addCallbackResult('cellFocusChanged', [rowIndex, columnIndex]); + }, + onFocusedRowChanging: ({ prevRowIndex, newRowIndex }) => { + (window as WindowCallbackExtended) + .clientTesting! + .addCallbackResult('rowFocusChanging', [prevRowIndex, newRowIndex]); + }, + onFocusedRowChanged: ({ rowIndex }) => { + (window as WindowCallbackExtended) + .clientTesting! + .addCallbackResult('rowFocusChanged', [rowIndex]); + }, + }); + + test('It should fire events after new rows were added', async ({ page }) => { + await initCallbackTesting(); + await createWidget(page, 'dxDataGrid', { + focusedRowEnabled: true, + editing: { + mode: 'batch', + allowAdding: true, + allowUpdating: true, + }, + ...getGridDataConfig(4), + ...getGridEventsConfig(), + }); + + const expectedCellFocusChanging: FocusCellChangingData[] = [ + [[-1, -1], [0, 0]], [[1, 0], [0, 0]], [[1, 0], [0, 0]], + ]; + const expectedCellFocusChanged: FocusCellChangedData[] = [ + [0, 0], [0, 0], [0, 0], + ]; + const expectedRowFocusChanging: FocusRowChangingData[] = [ + [-1, 0], [1, 0], [1, 0], + ]; + const expectedRowFocusChanged: FocusRowChangedData[] = [ + [0], [1], [0], [1], [0], + ]; + + const addRowBtn = page.locator('.dx-toolbar').getItem(); + + await (addRowBtn).click() + .click(addRowBtn) + .click(addRowBtn); + + const [ + cellFocusChanging, + cellFocusChanged, + rowFocusChanging, + rowFocusChanged, + ] = await collectEventsCallbackResults(); + + expect(await cellFocusChanging); + await t.eql(expectedCellFocusChanging); + expect(await cellFocusChanged); + await t.eql(expectedCellFocusChanged); + expect(await rowFocusChanging); + await t.eql(expectedRowFocusChanging); + expect(await rowFocusChanged); + await t.eql(expectedRowFocusChanged); + }); + // TODO: .after() block removed +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/focus/focusShowEditorAlwaysCell.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/focus/focusShowEditorAlwaysCell.spec.ts new file mode 100644 index 000000000000..d89f73cd6641 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/focus/focusShowEditorAlwaysCell.spec.ts @@ -0,0 +1,100 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('Focus - cell with showEditorAlways', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + const SELECTOR = '#container'; + const OVERLAY_SELECTOR = '.dx-overlay-wrapper'; + + const createDataGrid = () => createWidget(page, 'dxDataGrid', { + dataSource: [ + { + A: 'A_0', + B: 'B_0', + C: 0, + D: 'D_0', + }, + { + A: 'A_1', + B: 'B_1', + C: 1, + D: 'D_1', + }, + { + A: 'A_2', + B: 'B_2', + C: 2, + D: 'D_2', + }, + ], + columns: [ + { + dataField: 'A', + showEditorAlways: true, + }, + { + dataField: 'B', + showEditorAlways: true, + }, + { + dataField: 'C', + showEditorAlways: true, + lookup: { + dataSource: [ + { + id: 0, + name: 'LOOKUP_0', + }, + { + id: 1, + name: 'LOOKUP_1', + }, + { + id: 2, + name: 'LOOKUP_2', + }, + ], + displayExpr: 'name', + valueExpr: 'id', + }, + }, + { + dataField: 'D', + showEditorAlways: true, + }, + ], + editing: { + mode: 'cell', + allowUpdating: true, + allowAdding: true, + allowDeleting: true, + }, + }); + + test('Should switch focus after the lookup value change [T1194403]', async ({ page }) => { + await createDataGrid(); + + const editorTextCell = page.locator('.dx-data-row').nth(0).locator('td').nth(1); + const lookupCell = page.locator('.dx-data-row').nth(0).locator('td').nth(2).locator('.dx-editor-cell'); + + await (lookupCell.element).click(); + + const list = new List(OVERLAY_SELECTOR); + const item = list.getItem(2); + + await (item.element).click(); + await (editorTextCell.element).click(); + + await testScreenshot(page, 'focus-edit-cell_after-lookup-change.png', { element: page.locator('#container') }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/focus/focusedRow/focusedRow.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/focus/focusedRow/focusedRow.spec.ts new file mode 100644 index 000000000000..1a7e47669256 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/focus/focusedRow/focusedRow.spec.ts @@ -0,0 +1,46 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../../tests/container.html')}`; + +test.describe('Focused row', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + test('onFocusedRowChanged event should fire once after changing focusedRowKey if paging.enabled = false (T755722)', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: [ + { name: 'Alex', phone: '111111', room: 6 }, + { name: 'Dan', phone: '2222222', room: 5 }, + { name: 'Ben', phone: '333333', room: 4 }, + ], + keyExpr: 'name', + focusedRowEnabled: true, + focusedRowIndex: 1, + paging: { + enabled: false, + }, + onFocusedRowChanged: () => { + const global = window as Window & typeof globalThis & { onFocusedRowChangedCounter: number }; + if (!global.onFocusedRowChangedCounter) { + global.onFocusedRowChangedCounter = 0; + } + global.onFocusedRowChangedCounter += 1; + }, + }); + + expect(await ClientFunction(() => (window as any).onFocusedRowChangedCounter)()).toBe(1); + + await ClientFunction(() => (window as any).widget.option('focusedRowKey', 'Ben'))(); + + expect(await dataGrid.getFocusedRow().exists).toBeTruthy(); + expect(await ClientFunction(() => (window as any).onFocusedRowChangedCounter)()).toBe(2); + }); + // TODO: .after() block removed +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/focus/focusedRow/markup.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/focus/focusedRow/markup.spec.ts new file mode 100644 index 000000000000..be69b22ffb1e --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/focus/focusedRow/markup.spec.ts @@ -0,0 +1,65 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, insertStylesheetRulesToPage } from '../../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../../tests/container.html')}`; + +test.describe('Focused row - markup', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + // TODO: Enable multi-theming testcafe run in the future. + // visual: generic.light + // visual: material.blue.light + + test('markup - generic.light', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + keyExpr: 'id', + focusedRowEnabled: true, + editing: { + mode: 'batch', + allowUpdating: true, + }, + dataSource: [{ + id: 0, + dataA: 'dataA_1', + dataB: 'dataB_1', + dataC: 'dataC_1', + }, { + id: 1, + dataA: 'dataA_2', + dataB: 'dataB_2', + dataC: 'dataC_2', + }], + columns: [{ + dataField: 'dataA', + validationRules: [{ type: 'required' }], + }, { + dataField: 'dataB', + validationRules: [{ type: 'required' }], + }, { + dataField: 'dataC', + validationRules: [{ type: 'required' }], + }], + }); + + const firstCell = page.locator('.dx-data-row').nth(0).locator('td').nth(0); + const secondCell = page.locator('.dx-data-row').nth(0).locator('td').nth(1); + const thirdCell = page.locator('.dx-data-row').nth(0).locator('td').nth(2); + + await (firstCell.element).click(); + await (firstCell.locator('.dx-editor-cell')).fill('TEST'); + + await (secondCell.element).click(); + await (secondCell.locator('.dx-editor-cell')).fill(' '); + + await (thirdCell.element).click(); + + await testScreenshot(page, 'focused-row_markup.png', { element: page.locator('#container') }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/grouping/T1162057_oneGroupOnDifferentPages.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/grouping/T1162057_oneGroupOnDifferentPages.spec.ts new file mode 100644 index 000000000000..88b2f6a40390 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/grouping/T1162057_oneGroupOnDifferentPages.spec.ts @@ -0,0 +1,130 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('Grouping Panel - One group on different pages', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + const GRID_SELECTOR = '#container'; + + const endsOnNextPageApiMock = RequestMock() + .onRequestTo(/\/api\/data\?skip=0&take=5/) + .respond( + { + data: [ + { key: 'KeyA', items: null, count: 6 }, + { key: 'KeyB', items: null, count: 3 }, + ], + groupCount: 2, + totalCount: 11, + }, + 200, + { 'access-control-allow-origin': '*' }, + ) + .onRequestTo(/\/api\/data\?skip=0&take=1/) + .respond( + { + data: [ + { key: 'KeyA', items: null, count: 6 }, + ], + groupCount: 2, + totalCount: 11, + }, + 200, + { 'access-control-allow-origin': '*' }, + ) + .onRequestTo(/\/api\/data\?skip=0&take=3/) + .respond( + { + data: [ + { key: 'KeyA', items: null, count: 6 }, + { key: 'KeyB', items: null, count: 3 }, + ], + groupCount: 2, + totalCount: 11, + }, + 200, + { 'access-control-allow-origin': '*' }, + ) + .onRequestTo(/\/api\/data\?take=4.*&filter=.*KeyA/) + .respond( + { + data: [ + { id: 0, data: 'A' }, + { id: 1, data: 'B' }, + { id: 2, data: 'C' }, + { id: 3, data: 'D' }, + ], + }, + 200, + { 'access-control-allow-origin': '*' }, + ) + .onRequestTo(/\/api\/data\?skip=4.*&filter=.*KeyA/) + .respond( + { + data: [ + { id: 4, data: 'E' }, + { id: 5, data: 'F' }, + ], + }, + 200, + { 'access-control-allow-origin': '*' }, + ); + + test('Group panel restored from cache and ends at the next page', async ({ page }) => { + await t.addRequestHooks(endsOnNextPageApiMock); + await createWidget(page, 'dxDataGrid', () => ({ + dataSource: (window as any).DevExpress.data.AspNet.createStore({ + key: 'id', + loadUrl: 'https://api/data', + }), + columns: [ + 'data', + { + dataField: 'key', + groupIndex: 0, + }, + ], + groupPanel: { + visible: true, + }, + grouping: { + autoExpandAll: false, + }, + remoteOperations: { + groupPaging: true, + }, + pager: { + visible: true, + showInfo: true, + showPageSizeSelector: true, + allowedPageSizes: [5], + displayMode: 'full', + }, + paging: { + pageSize: 5, + }, + })); + + await (page.locator('.dx-group-row').click().nth(0).getCell(0).element); + await testScreenshot(page, 'group-panel_loaded_first-page.png'); + + await (page.locator('.dx-pager').click().locator('.dx-page').filter({hasText: '2'}).element); + await testScreenshot(page, 'group-panel_loaded_second-page.png'); + + await (page.locator('.dx-pager').click().locator('.dx-page').filter({hasText: '1'}).element); + await testScreenshot(page, 'group-panel_restored_first-page.png'); + + await (page.locator('.dx-pager').click().locator('.dx-page').filter({hasText: '2'}).element); + await testScreenshot(page, 'group-panel_restored_second-page.png'); + }); + // TODO: .after() block removed +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/grouping/calculateGroupValueRuntimeChanges.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/grouping/calculateGroupValueRuntimeChanges.spec.ts new file mode 100644 index 000000000000..3cdb35a31b80 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/grouping/calculateGroupValueRuntimeChanges.spec.ts @@ -0,0 +1,224 @@ +import { test, expect } from '@playwright/test'; +import { createWidget } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('Grouping API - calculateGroupValue runtime changes', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + test( + 'One group: should expand grouped section after calculateGroupValue update', + async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: [ + { id: 0, A: 0, B: 'B_0' }, + { id: 1, A: 1, B: 'B_1' }, + { id: 2, A: 2, B: 'B_2' }, + { id: 3, A: 3, B: 'B_3' }, + ], + keyExpr: 'id', + columns: [ + { dataField: 'A', sortOrder: 'desc' }, + 'B', + ], + sorting: { mode: 'single' }, + }); + + await page.locator('.dx-datagrid').first().isVisible(); + await dataGrid.apiColumnOption('group', 'calculateGroupValue', () => 'ALL'); + + expect(await page.locator('.dx-group-row').nth(0).isExpanded); + await t.notOk(); + expect(await dataGrid.getGroupRowSelector().count); + await t.eql(1); + expect(await dataGrid.dataRows.count); + await t.eql(0); + + await (dataGrid + .getGroupRow(0).click() + .getExpandCell()); + + expect(await page.locator('.dx-group-row').nth(0).isExpanded); + await t.ok(); + expect(await dataGrid.getGroupRowSelector().count); + await t.eql(1); + expect(await dataGrid.dataRows.count); + await t.eql(4); + }, + ).before(async () => createWidget(page, 'dxDataGrid', { + dataSource: [ + { id: 0, A: 'A_0', group: 'A' }, + { id: 1, A: 'A_1', group: 'A' }, + { id: 2, A: 'A_2', group: 'B' }, + { id: 3, A: 'A_3', group: 'B' }, + ], + keyExpr: 'id', + columns: [ + { dataField: 'group', groupIndex: 0 }, + 'A', + ], + grouping: { autoExpandAll: false }, + })); + + // NOTE: Intersection with "column configuration from first data source item" feature + // Because one of first item's fields is null and different logic is applied + test( + 'One group: should expand grouped section after calculateGroupValue update if first record contains null value', + async ({ page }) => { + await page.locator('.dx-datagrid').first().isVisible(); + await dataGrid.apiColumnOption('group', 'calculateGroupValue', () => 'ALL'); + + expect(await page.locator('.dx-group-row').nth(0).isExpanded); + await t.notOk(); + expect(await dataGrid.getGroupRowSelector().count); + await t.eql(1); + expect(await dataGrid.dataRows.count); + await t.eql(0); + + await (dataGrid + .getGroupRow(0).click() + .getExpandCell()); + + expect(await page.locator('.dx-group-row').nth(0).isExpanded); + await t.ok(); + expect(await dataGrid.getGroupRowSelector().count); + await t.eql(1); + expect(await dataGrid.dataRows.count); + await t.eql(4); + }, + ).before(async () => createWidget(page, 'dxDataGrid', { + dataSource: [ + { id: 0, A: 'A_0', group: 'A' }, + { id: 1, A: 'A_1', group: 'A' }, + { id: 2, A: 'A_2', group: 'B' }, + { id: 3, A: 'A_3', group: 'B' }, + ], + keyExpr: 'id', + columns: [ + { dataField: 'group', groupIndex: 0 }, + 'A', + ], + grouping: { autoExpandAll: false }, + })); + + test( + 'Multiple groups: should expand grouped section after calculateGroupValue update', + async ({ page }) => { + await page.locator('.dx-datagrid').first().isVisible(); + await dataGrid.apiColumnOption('group', 'calculateGroupValue', () => 'ALL'); + + expect(await page.locator('.dx-group-row').nth(0).isExpanded); + await t.notOk(); + expect(await dataGrid.getGroupRowSelector().count); + await t.eql(1); + expect(await dataGrid.dataRows.count); + await t.eql(0); + + await (dataGrid + .getGroupRow(0).click() + .getExpandCell()); + + expect(await page.locator('.dx-group-row').nth(0).isExpanded); + await t.ok(); + expect(await dataGrid.getGroupRowSelector().count); + await t.eql(5); + expect(await dataGrid.dataRows.count); + await t.eql(0); + }, + ).before(async () => createWidget(page, 'dxDataGrid', { + dataSource: [ + { + id: 0, A: 'A_0', B: 'B_0', group: 'A', + }, + { + id: 1, A: 'A_1', B: 'B_1', group: 'A', + }, + { + id: 2, A: 'A_2', B: 'B_2', group: 'B', + }, + { + id: 3, A: 'A_3', B: 'B_3', group: 'B', + }, + ], + keyExpr: 'id', + columns: [ + { dataField: 'group', groupIndex: 0 }, + { dataField: 'A', groupIndex: 1 }, + 'B', + ], + grouping: { autoExpandAll: false }, + })); + + // NOTE: Intersection with "column configuration from first data source item" feature + // Because one of first item's fields is null and different logic is applied + test( + 'Multiple groups: should expand grouped section after calculateGroupValue update if first record contains null value [T1281192]', + async ({ page }) => { + await page.locator('.dx-datagrid').first().isVisible(); + await dataGrid.apiColumnOption('group', 'calculateGroupValue', () => 'ALL'); + + expect(await page.locator('.dx-group-row').nth(0).isExpanded); + await t.notOk(); + expect(await dataGrid.getGroupRowSelector().count); + await t.eql(1); + expect(await dataGrid.dataRows.count); + await t.eql(0); + + await (dataGrid + .getGroupRow(0).click() + .getExpandCell()); + + expect(await page.locator('.dx-group-row').nth(0).isExpanded); + await t.ok(); + expect(await dataGrid.getGroupRowSelector().count); + await t.eql(5); + expect(await dataGrid.dataRows.count); + await t.eql(0); + }, + ).before(async () => createWidget(page, 'dxDataGrid', { + dataSource: [ + { + id: 0, A: 'A_0', B: null, group: 'A', + }, + { + id: 1, A: 'A_1', B: 'B_1', group: 'A', + }, + { + id: 2, A: 'A_2', B: 'B_2', group: 'B', + }, + { + id: 3, A: 'A_3', B: 'B_3', group: 'B', + }, + ], + keyExpr: 'id', + columns: [ + { dataField: 'group', groupIndex: 0 }, + { dataField: 'A', groupIndex: 1 }, + 'B', + ], + grouping: { autoExpandAll: false }, + })); + + test('Should not reset sorting parameters after calculateGroupValue update [T1298901]', async ({ page }) => { + await page.locator('.dx-datagrid').first().isVisible(); + + expect(await dataGrid.apiColumnOption('A', 'sortOrder')); + await t.eql('desc'); + expect(await dataGrid.apiColumnOption('A', 'sortIndex')); + await t.eql(0); + + await dataGrid.apiColumnOption('A', 'calculateGroupValue', () => 'ALL'); + + expect(await dataGrid.apiColumnOption('A', 'sortOrder')); + await t.eql('desc'); + expect(await dataGrid.apiColumnOption('A', 'sortIndex')); + await t.eql(0); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/grouping/grouping.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/grouping/grouping.spec.ts new file mode 100644 index 000000000000..b168265e6626 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/grouping/grouping.spec.ts @@ -0,0 +1,40 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('Grouping Panel', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + test('Grouping Panel label should not overflow in a narrow grid (T1103925)', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: { + store: [ + { + field1: '1', field2: '2', field3: '3', field4: '4', field5: '5', + }, + { + field1: '11', field2: '22', field3: '33', field4: '44', field5: '55', + }], + }, + width: 200, + groupPanel: { + emptyPanelText: 'Long long long long long long long long long long long text', + visible: true, + }, + editing: { allowAdding: true, mode: 'batch' }, + columnChooser: { + enabled: true, + }, + }); + + await testScreenshot(page, 'groupingPanel.png', { element: page.locator('.dx-toolbar') }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/headerFilter/headerFilter.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/headerFilter/headerFilter.spec.ts new file mode 100644 index 000000000000..8a5e4126caa0 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/headerFilter/headerFilter.spec.ts @@ -0,0 +1,57 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('Header Filter', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + const GRID_CONTAINER = '#container'; + + test('Data should be filtered if (Blank) is selected in the header filter (T1257261)', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: [ + { ID: 1, Text: 'Item 1' }, + { ID: 2, Text: '' }, + { ID: 3, Text: 'Item 3' }, + ], + keyExpr: 'ID', + showBorders: true, + remoteOperations: true, + headerFilter: { visible: true }, + filterRow: { visible: true }, + filterPanel: { visible: true }, + }); + + const result: string[] = []; + const headerCell = page.locator('.dx-header-row').nth(0).locator('td').nth(1); + const dataCell = page.locator('.dx-data-row').nth(0).locator('td').nth(0); + const filterIconElement = headerCell.getFilterIcon(); + const headerFilter = new HeaderFilter(); + const buttons = headerFilter.getButtons(); + const list = headerFilter.getList(); + + await (filterIconElement).click(); + await (list.getItem(1).click().element) // Select second item with value 'Item 1'; + await (buttons.nth(0).click()); // Click OK; + + result[0] = await dataCell.element().innerText; + + await (filterIconElement).click(); + await (list.getItem(1).click().element) // Deselect second item with value 'Item 1'; + await (list.getItem(0).click().element) // Select second item with value '(Blanks)'; + await (buttons.nth(0).click()); // Click OK; + + result[1] = await dataCell.element().innerText; + + expect(await result[0]).toBe('1') + .expect(result[1]).toBe('2'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/headerFilter/headerFilterList.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/headerFilter/headerFilterList.spec.ts new file mode 100644 index 000000000000..a387ee33d47e --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/headerFilter/headerFilterList.spec.ts @@ -0,0 +1,56 @@ +import { test, expect } from '@playwright/test'; +import { createWidget } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('Header Filter - dxList integration', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + const GRID_CONTAINER = '#container'; + + const openHeaderFilterAndGetList = async (t: TestController, dataGrid: DataGrid) => { + const headerCell = dataGrid.getHeaders() + .getHeaderRow(0) + .locator('td').nth(0); + const filterIconElement = headerCell.getFilterIcon(); + const headerFilter = new HeaderFilter(); + const list = headerFilter.getList(); + const firstListItem = list.getItem(0); + const secondListItem = list.getItem(1); + + await t + .click(filterIconElement); + + return { list, firstListItem, secondListItem }; + }; + + test('Should has unchecked "Select all" checkbox state if no values is selected', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: [ + { id: 0 }, + { id: 1 }, + ], + keyExpr: 'id', + columns: [ + { dataField: 'id', filterValues: [] }, + ], + headerFilter: { visible: true }, + }); + + const { list, firstListItem, secondListItem } = await openHeaderFilterAndGetList(t, dataGrid); + + expect(await list.selectAll.checkBox.getCheckBoxState()); + await t.eql('unchecked'); + expect(await firstListItem.isSelected); + await t.notOk(); + expect(await secondListItem.isSelected); + await t.notOk(); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/headerPanel.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/headerPanel.spec.ts new file mode 100644 index 000000000000..10f2d6c19b7a --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/headerPanel.spec.ts @@ -0,0 +1,81 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('Header Panel', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + test('Drop-down window should be positioned correctly after resizing the toolbar (T1037975)', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: [ + { ID: 1, Name: 'Name 1', Category: 'Category 1' }, + { ID: 2, Name: 'Name 2', Category: 'Category 1' }, + ], + keyExpr: 'ID', + columns: ['ID', { dataField: 'Name', groupIndex: 0 }, 'Category'], + toolbar: { + items: [ + { + location: 'before', + locateInMenu: 'always', + widget: 'dxSelectBox', + options: { + width: 200, + items: ['Name', 'Category'], + value: 'Name', + onValueChanged(e) { + const gridInstance = ($('#container') as any).dxDataGrid('instance'); + gridInstance.clearGrouping(); + gridInstance.columnOption(e.value, 'groupIndex', 0); + }, + }, + }, + ], + }, + }); + + const headerPanel = page.locator('.dx-datagrid-header-panel'); + + // act + await (headerPanel.locator('.dx-dropdownmenu-button').click()); + + // assert + const selectPopup = headerPanel.getDropDownSelectPopup(); + const popupContent = selectPopup.menuContent(); + + expect(await popupContent.exists); + await t.ok(); + expect(await popupContent.visible); + await t.ok(); + + // act + await (selectPopup.editButton().click()); + + // assert + const menuItem = selectPopup.getSelectItem(1); + + expect(await menuItem.exists); + await t.ok(); + expect(await menuItem.visible); + await t.ok(); + + // act + await (menuItem).click(); + + const visibleRows = await page.evaluate(() => ($('#container') as any).dxDataGrid('instance').getVisibleRows()); + + // assert + expect(await visibleRows.length); + await t.eql(3); + + await testScreenshot(page, 'grid-toolbar-dropdown-menu.png', { element: 'body' }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/keyboardNavigation/columnReordering.visual.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/keyboardNavigation/columnReordering.visual.spec.ts new file mode 100644 index 000000000000..d94feb8f7615 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/keyboardNavigation/columnReordering.visual.spec.ts @@ -0,0 +1,44 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('DataGrid Tests', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + fixture + .disablePageReloads`Keyboard Navigation - Column Reordering` + .page(url(__dirname, '../../../container.html')); + + // Regular columns + [true, false].forEach((rtlEnabled) => { + + test(`reorder column when ${rtlEnabled ? 'left' : 'right'} arrow is pressed when rtlEnabled = ${rtlEnabled}`, async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + rtlEnabled, + allowColumnReordering: true, + dataSource: [{ + field1: 'test1', + field2: 'test2', + field3: 'test3', + field4: 'test4', + }], + }); + + const firstHeaderCell = page.locator('.dx-header-row').nth(0).locator('td').nth(0); + const shortcut = rtlEnabled ? 'ctrl+left' : 'ctrl+right'; + + await (firstHeaderCell.element).click(); + await t.pressKey(shortcut); + + await testScreenshot(page, `reorder_column_to_${rtlEnabled ? 'left' : 'right'}_when_rtlEnabled_=_${rtlEnabled}.png`, { element: page.locator('#container') }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/keyboardNavigation/customButtons.functional.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/keyboardNavigation/customButtons.functional.spec.ts new file mode 100644 index 000000000000..e22b4425866b --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/keyboardNavigation/customButtons.functional.spec.ts @@ -0,0 +1,75 @@ +import { test, expect } from '@playwright/test'; +import { createWidget } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('DataGrid Tests', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + fixture + .disablePageReloads`Keyboard Navigation - custom buttons` + .page(url(__dirname, '../../../container.html')); + + const createDataGrid = async () => createWidget(page, 'dxDataGrid', { + dataSource: [ + { + id: 1, + columnA: 'A_0', + columnB: 'B_0', + }, + { + id: 2, + columnA: 'A_1', + columnB: 'B_1', + }, + { + id: 3, + columnA: 'A_2', + columnB: 'B_2', + }, + ], + keyExpr: 'id', + columns: [ + { + type: 'buttons', + buttons: [ + { + hint: 'button_1', + icon: 'edit', + onClick: (e) => $(e.event.target).attr('has-been-clicked', 'true'), + }, + { + hint: 'button_2', + icon: 'remove', + }, + ], + }, + 'id', + 'columnA', + 'columnB', + ], + sorting: { + mode: 'none', + }, + }); + + test('Custom buttons cell should be focused before custom buttons on tab navigation', async ({ page }) => { + await createDataGrid(); + + const expectedFocusedCell = page.locator('.dx-data-row').nth(0).locator('td').nth(0); + const cellToStartNavigation = page.locator('.dx-header-row').nth(0).locator('td').nth(3); + + await (cellToStartNavigation.element).click(); + await page.keyboard.press('tab'); + expect(await expectedFocusedCell.isFocused); + await t.ok(); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/keyboardNavigation/editOnKeyPress.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/keyboardNavigation/editOnKeyPress.spec.ts new file mode 100644 index 000000000000..336580cb15ab --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/keyboardNavigation/editOnKeyPress.spec.ts @@ -0,0 +1,64 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('DataGrid Tests', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + fixture + .disablePageReloads`Keyboard Navigation - editOnKeyPress` + .page(url(__dirname, '../../../container.html')); + + [ + { name: 'input', template: () => $('') }, + { name: 'div', template: () => $('
').text('Hi, I\'m the template!') }, + ].forEach(({ name, template }) => { + + test(`should render edit cell template without errors, template: ${name}`, async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: [ + { + data_A: 'data_A', + data_B: 'data_B', + }, + ], + columns: [ + { + dataField: 'data_A', + editCellTemplate: template, + }, + 'data_B', + ], + keyboardNavigation: { + enabled: true, + editOnKeyPress: true, + enterKeyDirection: 'column', + }, + editing: { + mode: 'cell', + allowUpdating: true, + allowAdding: true, + startEditAction: 'dblClick', + }, + // @ts-expect-error private option + templatesRenderAsynchronously: true, + }); + await makeRowsViewTemplatesAsync(DATA_GRID_SELECTOR); + + const dataCell = page.locator('.dx-data-row').nth(0).locator('td').nth(0); + + await (dataCell.element).click() + .pressKey('f'); + + await testScreenshot(page, `edit-cell-keypress-with-custom-cell-template_template-${name}.png`, { element: page.locator('#container') }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/keyboardNavigation/groupColumnReordering.functional.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/keyboardNavigation/groupColumnReordering.functional.spec.ts new file mode 100644 index 000000000000..02bb4fdc44ee --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/keyboardNavigation/groupColumnReordering.functional.spec.ts @@ -0,0 +1,53 @@ +import { test, expect } from '@playwright/test'; +import { createWidget } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('DataGrid Tests', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + fixture + .disablePageReloads`Keyboard Navigation - Column Reordering` + .page(url(__dirname, '../../../container.html')); + + const triggerVisibilityChange = ClientFunction(() => { + document.dispatchEvent(new Event('visibilitychange')); + }); + + test('The column should be grouped when pressing Ctrl + G if grouping.contextMenuEnabled is false', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + width: 550, + columnWidth: 100, + grouping: { + contextMenuEnabled: false, + }, + groupPanel: { + visible: true, + }, + dataSource: [{ + field1: 'test1', + field2: 'test2', + field3: 'test3', + field4: 'test4', + }], + }); + + const firstVisibleHeader = page.locator('.dx-header-row').nth(0).locator('td').nth(0); + + await (firstVisibleHeader.element).click(); + await page.keyboard.press('ctrl+g'); + + expect(await dataGrid.getGroupPanel().getHeadersCount()); + await t.eql(1); + expect(await page.locator('.dx-header-row').nth(0).locator('td').nth(1).isFocused); + await t.ok(); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/keyboardNavigation/groupColumnReordering.visual.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/keyboardNavigation/groupColumnReordering.visual.spec.ts new file mode 100644 index 000000000000..b345bf201a4f --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/keyboardNavigation/groupColumnReordering.visual.spec.ts @@ -0,0 +1,58 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('DataGrid Tests', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + fixture + .disablePageReloads`Keyboard Navigation - Group Column Reordering` + .page(url(__dirname, '../../../container.html')); + + // Move grouped columns + [true, false].forEach((rtlEnabled) => { + + test(`reorder group column when ${rtlEnabled ? 'left' : 'right'} arrow is pressed when rtlEnabled = ${rtlEnabled}`, async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + rtlEnabled, + dataSource: [{ + field1: 'test1', + field2: 'test2', + field3: 'test3', + field4: 'test4', + }], + groupPanel: { + visible: true, + }, + columns: [ + { + dataField: 'field1', + groupIndex: 1, + }, + 'field2', + 'field3', + { + dataField: 'field4', + groupIndex: 0, + }, + ], + }); + + const firstGroupHeader = dataGrid.getGroupPanel().getHeader(0); + const shortcut = rtlEnabled ? 'ctrl+left' : 'ctrl+right'; + + await (firstGroupHeader.element).click(); + await t.pressKey(shortcut); + + await testScreenshot(page, `reorder_group_column_to_${rtlEnabled ? 'left' : 'right'}_when_rtlEnabled_=_${rtlEnabled}.png`, { element: page.locator('#container') }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/keyboardNavigation/keyboardNavigation.functional.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/keyboardNavigation/keyboardNavigation.functional.spec.ts new file mode 100644 index 000000000000..d55de0e8ff30 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/keyboardNavigation/keyboardNavigation.functional.spec.ts @@ -0,0 +1,127 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('Keyboard Navigation - common', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + const CLASS = ClassNames; + + const getOnKeyDownCallCount = ClientFunction(() => (window as any).onKeyDownCallCount); + + ); + + test('Changing keyboardNavigation options should not invalidate the entire content (T1197829)', async ({ page }) => { + await page.evaluate(() => { + (window as any).invalidateCounter = 0; + (window as any).renderTableCounter = 0; + }); + + await createWidget(page, 'dxDataGrid', { + dataSource: [...new Array(5)].map((_, index) => ({ id: index, text: `item ${index}` })), + keyExpr: 'id', + columns: [ + { dataField: 'id' }, + { dataField: 'text' }, + ], + focusedRowEnabled: true, + keyboardNavigation: { + editOnKeyPress: true, + enterKeyAction: 'startEdit', + enterKeyDirection: 'column', + }, + editing: { + mode: 'cell', + allowUpdating: true, + }, + onFocusedRowChanging(e) { + if ((e.newRowIndex + 1) % 2 === 0) { + e.component.option('keyboardNavigation.enterKeyAction', 'moveFocus'); + } else { + e.component.option('keyboardNavigation.enterKeyAction', 'startEdit'); + } + }, + onInitialized(e) { + const dataGrid: any = e.component; + const rowsView = dataGrid.getView('rowsView'); + // eslint-disable-next-line no-underscore-dangle + const defaultInvalidate = rowsView._invalidate; + // eslint-disable-next-line no-underscore-dangle + dataGrid.getView('rowsView')._invalidate = (...args) => { + ((window as any).invalidateCounter as number) += 1; + return defaultInvalidate.apply(rowsView, args); + }; + + // eslint-disable-next-line no-underscore-dangle + const defaultRenderTable = rowsView._renderTable; + // eslint-disable-next-line no-underscore-dangle + dataGrid.getView('rowsView')._renderTable = (...args) => { + ((window as any).renderTableCounter as number) += 1; + return defaultRenderTable.apply(rowsView, args); + }; + }, + }); + + expect(await page.locator('.dx-datagrid').first().isVisible()); + await t.ok(); + + expect(await ClientFunction(() => (window as any).invalidateCounter)()); + await t.eql(0); + expect(await ClientFunction(() => (window as any).renderTableCounter)()); + await t.eql(2); + // test enter key behavior + .click(page.locator('.dx-data-row').nth(1).locator('td').nth(1)) // set initial focus + .expect(page.locator('.dx-data-row').nth(1).locator('td').nth(1).isFocused) + .ok() + .expect(page.locator('.dx-data-row').nth(1).locator('td').nth(1).isEditCell) + .ok() + .pressKey('enter') // move focus to next cell + .expect(page.locator('.dx-data-row').nth(2).locator('td').nth(1).isFocused) + .ok() + .expect(page.locator('.dx-data-row').nth(2).locator('td').nth(1).isEditCell) + .notOk() + .pressKey('enter') // switch cell to the editing state + .expect(page.locator('.dx-data-row').nth(2).locator('td').nth(1).isEditCell) + .ok() + .pressKey('enter') // move focus to next cell + .expect(page.locator('.dx-data-row').nth(3).locator('td').nth(1).isFocused) + .ok() + .expect(page.locator('.dx-data-row').nth(3).locator('td').nth(1).isEditCell) + .notOk() + .pressKey('enter') // move focus to next cell again + .expect(page.locator('.dx-data-row').nth(4).locator('td').nth(1).isFocused) + .ok() + .expect(page.locator('.dx-data-row').nth(4).locator('td').nth(1).isEditCell) + .notOk() + .pressKey('enter') // switch cell to the editing state + .expect(page.locator('.dx-data-row').nth(4).locator('td').nth(1).isEditCell) + .ok() + + // test tab key behavior + .click(page.locator('.dx-data-row').nth(1).locator('td').nth(1)) // set initial focus + .expect(page.locator('.dx-data-row').nth(1).locator('td').nth(1).isFocused) + .ok() + .expect(page.locator('.dx-data-row').nth(1).locator('td').nth(1).isEditCell) + .ok() + .pressKey('tab') + .expect(page.locator('.dx-data-row').nth(2).locator('td').nth(0).isFocused) + .ok() + .expect(page.locator('.dx-data-row').nth(2).locator('td').nth(0).isEditCell) + .ok() + + .expect(ClientFunction(() => (window as any).invalidateCounter)()) + .eql(0) + .expect(ClientFunction(() => (window as any).renderTableCounter)()) + .eql(9); + }); + // TODO: .after() block removed +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/keyboardNavigation/keyboardNavigation.visual.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/keyboardNavigation/keyboardNavigation.visual.spec.ts new file mode 100644 index 000000000000..4519ed52918e --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/keyboardNavigation/keyboardNavigation.visual.spec.ts @@ -0,0 +1,42 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('Keyboard Navigation.Visual', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + // Quick navigation through grid cells via Home and End keys + + test('Focus the last cell in the row that contains focus when pressing the End key', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: getData(20, 7), + columnWidth: 100, + height: 500, + width: 800, + showBorders: true, + scrolling: { + showScrollbar: 'never', + }, + }); + + // arrange + expect(await page.locator('.dx-datagrid').first().isVisible()); + await t.ok(); + + // act + await (page.locator('.dx-data-row').click().nth(0).locator('td').nth(0)); + await page.keyboard.press('end'); + + await testScreenshot(page, 'focus_last_cell_in_row_that_contains_focus_when_pressing_End_key.png', { element: page.locator('#container') }); + + // assert + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/keyboardNavigation/markup.screenshots.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/keyboardNavigation/markup.screenshots.spec.ts new file mode 100644 index 000000000000..2f24277814cd --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/keyboardNavigation/markup.screenshots.spec.ts @@ -0,0 +1,58 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('DataGrid Tests', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + fixture + .disablePageReloads`Keyboard Navigation - screenshots` + .page(url(__dirname, '../../../container.html')); + + test('Focused cells should look correctly', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: [ + { + id: 1, + columnA: 'A_0', + columnB: 'B_0', + }, + { + id: 2, + columnA: 'A_1', + columnB: 'B_1', + }, + { + id: 3, + columnA: 'A_2', + columnB: 'B_2', + }, + ], + keyExpr: 'id', + columns: ['id', 'columnA', 'columnB'], + sorting: { + mode: 'none', + }, + }); + + const headerCellToFocus = page.locator('.dx-header-row').nth(0).locator('td').nth(0); + const dataCellToFocus = page.locator('.dx-data-row').nth(0).locator('td').nth(0); + + await (headerCellToFocus.element).click() + .pressKey('tab'); + await testScreenshot(page, 'data-grid_keyboard-navigation-header-cell-focused.png'); + + await (dataCellToFocus.element).click() + .pressKey('tab'); + await testScreenshot(page, 'data-grid_keyboard-navigation-data-cell-focused.png'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/keyboardNavigation/masterDetail/index.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/keyboardNavigation/masterDetail/index.spec.ts new file mode 100644 index 000000000000..581d15709330 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/keyboardNavigation/masterDetail/index.spec.ts @@ -0,0 +1,37 @@ +import { test, expect } from '@playwright/test'; +import { createWidget } from '../../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../../tests/container.html')}`; + +test.describe('DataGrid Tests', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + fixture + .disablePageReloads`Keyboard Navigation - screenshots` + .page(url(__dirname, '../../../../container.html')); + + test('Focus goes inside master detail on tab', async ({ page }) => { + await createWidget(page, 'dxDataGrid', gridOptions); + + await (page.locator('.dx-data-row').click().nth(0).locator('.dx-command-edit').nth(0), + ); + + const innerDataGrid = new DataGrid(page.locator('.dx-master-detail-row').nth(0).element.find('.dx-datagrid').parent()); + + await (page.locator('.dx-data-row').click().nth(0).locator('td').nth(4), + ) + .pressKey('tab') + .pressKey('tab'); + + expect(await innerDataGrid.getHeaders().getHeaderRow(0).locator('td').nth(0).isFocused); + await t.ok(); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/keyboardNavigation/skipDragCell.functional.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/keyboardNavigation/skipDragCell.functional.spec.ts new file mode 100644 index 000000000000..7a4c153713c4 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/keyboardNavigation/skipDragCell.functional.spec.ts @@ -0,0 +1,75 @@ +import { test, expect } from '@playwright/test'; +import { createWidget } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('DataGrid Tests', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + // T1147695 + fixture + .disablePageReloads`Keyboard Navigation - skip drag cell` + .page(url(__dirname, '../../../container.html')); + + const DATA_SOURCE = [ + { + id: 1, + columnA: 'A_0', + columnB: 'B_0', + }, + { + id: 2, + columnA: 'A_1', + columnB: 'B_1', + }, + { + id: 3, + columnA: 'A_2', + columnB: 'B_2', + }, + ]; + const createDataGrid = async () => createWidget(page, 'dxDataGrid', { + dataSource: DATA_SOURCE, + keyExpr: 'id', + columns: ['id', 'columnA', 'columnB'], + rowDragging: { + allowReordering: true, + }, + sorting: { + mode: 'none', + }, + }); + + const createDataGridRenderAsyncWithButtons = async () => createWidget(page, 'dxDataGrid', { + dataSource: DATA_SOURCE, + keyExpr: 'id', + columns: ['id', 'columnA', 'columnB', { type: 'buttons' }], + rowDragging: { + allowReordering: true, + }, + sorting: { + mode: 'none', + }, + renderAsync: true, + }); + + test('The drag cell should be skipped when navigating from the header cell by tab keypress', async ({ page }) => { + await createDataGrid(); + + const expectedFocusedCell = page.locator('.dx-data-row').nth(0).locator('td').nth(1); + const cellToStartNavigation = page.locator('.dx-header-row').nth(0).locator('td').nth(3); + + await (cellToStartNavigation.element).click() + .pressKey('tab') + .expect(expectedFocusedCell.isFocused) + .ok(); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/keyboardNavigation/startEditing.functional.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/keyboardNavigation/startEditing.functional.spec.ts new file mode 100644 index 000000000000..2f2c31bb3738 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/keyboardNavigation/startEditing.functional.spec.ts @@ -0,0 +1,44 @@ +import { test, expect } from '@playwright/test'; +import { createWidget } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('Keyboard Navigation - editOnKeyPress', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + test('Editing should start by pressing enter after scrolling content with scrolling.mode=virtual', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: [...new Array(50)].map((_, i) => ({ + data1: i * 2, + data2: i * 2 + 1, + })), + columns: [ + 'data1', + 'data2', + ], + editing: { + allowUpdating: true, + }, + scrolling: { + mode: 'virtual', + }, + height: 300, + }); + + expect(await page.locator('.dx-datagrid').first().isVisible()).toBeTruthy(); + + await page.evaluate((opts) => ($('#container') as any).dxDataGrid('instance').getScrollable().scrollBy(opts), { y: 10000 }); + + await (page.locator('.dx-data-row').click().nth(49).locator('td').nth(1)); + await page.keyboard.press('enter'); + + expect(await page.locator('.dx-data-row').nth(49).locator('td').nth(1).locator('.dx-editor-cell').focused).toBeTruthy(); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/keyboardNavigation/virtualColumns.functional.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/keyboardNavigation/virtualColumns.functional.spec.ts new file mode 100644 index 000000000000..4e304ad86736 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/keyboardNavigation/virtualColumns.functional.spec.ts @@ -0,0 +1,72 @@ +import { test, expect } from '@playwright/test'; +import { createWidget } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('Virtual Columns.Functional', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + const generateData = (rowCount: number, columnCount: number): Record[] => { + const items: Record[] = []; + + for (let i = 0; i < rowCount; i += 1) { + const item = {}; + + for (let j = 0; j < columnCount; j += 1) { + item[`field${j + 1}`] = `${i + 1}-${j + 1}`; + } + + items.push(item); + } + + return items; + }; + + test('DataGrid should scroll to the first cell of the next row and focus it when navigating with Tab key', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + width: 500, + dataSource: generateData(10, 20), + columnWidth: 100, + scrolling: { + columnRenderingMode: 'virtual', + }, + }); + + // arrange + // assert + expect(await page.locator('.dx-datagrid').first().isVisible()); + await t.ok(); + + // act + await page.evaluate((opts) => ($('#container') as any).dxDataGrid('instance').getScrollable().scrollTo(opts), { x: 10000 }); + + // assert + expect(await dataGrid.getScrollLeft()); + await t.eql(1500); + expect(await page.locator('.dx-data-row').nth(0).locator('td').nth(19).exists); + await t.ok(); + + // act + await (page.locator('.dx-data-row').click().nth(0).locator('td').nth(19)); + + // assert + expect(await page.locator('.dx-data-row').nth(0).locator('td').nth(19).focused); + await t.ok(); + + // act + await page.keyboard.press('tab'); + + // assert + expect(await dataGrid.getScrollLeft()); + await t.eql(0); + expect(await page.locator('.dx-data-row').nth(1).locator('td').nth(0).focused); + await t.ok(); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/markup/T1163515_alternateRowGroupBorders.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/markup/T1163515_alternateRowGroupBorders.spec.ts new file mode 100644 index 000000000000..76006aeab826 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/markup/T1163515_alternateRowGroupBorders.spec.ts @@ -0,0 +1,372 @@ +import { test, expect } from '@playwright/test'; +import { createWidget } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('Grouping Panel - check borders and backgrounds with various options', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + interface MatrixOptions { + rowAlternationEnabled: boolean; + showColumnLines: boolean; + showRowLines: boolean; + showBorders: boolean; + hasFixedColumn: boolean; + hasMasterDetail: boolean; + } + + const SELECTORS = { + gridContainer: 'container', + masterDetailRowClass: 'dx-master-detail-row', + groupRowClass: 'dx-group-row', + rowLinesClass: 'dx-row-lines', + groupSpaceClass: 'dx-datagrid-group-space', + pointerEventsNoneClass: 'dx-pointer-events-none', + rowAlternativeClass: 'dx-row-alt', + }; + + const BORDER_WIDTH = { + big: 2, + normal: 1, + none: 0, + }; + + const dataSource = [ + { + group: 'A', + label: 'LABEL_A_0', + value: 'VALUE_A_0', + count: 1, + }, + { + group: 'A', + label: 'LABEL_A_1', + value: 'VALUE_A_1', + count: 2, + }, + { + group: 'B', + label: 'LABEL_B_0', + value: 'VALUE_B_0', + count: 3, + }, + { + group: 'B', + label: 'LABEL_B_1', + value: 'VALUE_B_1', + count: 4, + }, + { + group: 'B', + label: 'LABEL_B_2', + value: 'VALUE_B_2', + count: 5, + }, + { + group: 'C', + label: 'LABEL_C_0', + value: 'VALUE_C_0', + count: 6, + }, + { + group: 'C', + label: 'LABEL_C_1', + value: 'VALUE_C_1', + count: 7, + }, + ]; + + const getTestParams = ({ + rowAlternationEnabled, + showColumnLines, + showRowLines, + showBorders, + hasFixedColumn, + hasMasterDetail, + }: MatrixOptions) => [ + `rowAlternationEnabled: ${rowAlternationEnabled}`, + `showColumnLines: ${showColumnLines}`, + `showRowLines: ${showRowLines}`, + `showBorders: ${showBorders}`, + `hasFixedColumn: ${hasFixedColumn}`, + `hasMasterDetail: ${hasMasterDetail}`, + ].join(', '); + + const createDataGrid = async ({ + rowAlternationEnabled, + showColumnLines, + showRowLines, + showBorders, + hasFixedColumn, + hasMasterDetail, + }: MatrixOptions) => { + await createWidget(page, 'dxDataGrid', { + dataSource, + columnFixing: { + // @ts-expect-error private option + legacyMode: true, + }, + columns: [ + { + dataField: 'group', + groupIndex: 0, + }, + { + dataField: 'label', + fixed: hasFixedColumn, + }, + 'value', + 'count', + ], + summary: { + groupItems: [{ + column: 'count', + summaryType: 'sum', + }], + }, + masterDetail: hasMasterDetail + ? { + enabled: true, + autoExpandAll: true, + template: ($container) => { + $('
') + .text('MASTER DETAIL') + .appendTo($container); + }, + } + : undefined, + editing: { + mode: 'row', + allowDeleting: true, + confirmDelete: false, + }, + showBorders, + rowAlternationEnabled, + showRowLines, + showColumnLines, + }); + }; + + const checkShowBordersState = async ( + t: TestController, + dataGrid: DataGrid, + showBorders: boolean, + ) => { + const expectedBorderWidth = showBorders ? BORDER_WIDTH.normal : BORDER_WIDTH.none; + + const gridContainer = dataGrid.getContainer(); + const containerClasses = await gridContainer.getAttribute('class'); + + if (showBorders) { + await t.expect(containerClasses).contains('dx-datagrid-borders'); + } else { + await t.expect(containerClasses).notContains('dx-datagrid-borders'); + } + + const headersContainer = dataGrid.getHeadersContainer(); + + const borderTop = await headersContainer.getStyleProperty('border-top-width'); + const borderLeft = await headersContainer.getStyleProperty('border-left-width'); + const borderRight = await headersContainer.getStyleProperty('border-right-width'); + + await t.expect(parseInt(borderTop, 10)).eql(expectedBorderWidth); + await t.expect(parseInt(borderLeft, 10)).eql(expectedBorderWidth); + await t.expect(parseInt(borderRight, 10)).eql(expectedBorderWidth); + + const rowsView = dataGrid.getRowsView(); + + const rowsViewBorderLeft = await rowsView.getStyleProperty('border-left-width'); + const rowsViewBorderRight = await rowsView.getStyleProperty('border-right-width'); + const rowsViewBorderBottom = await rowsView.getStyleProperty('border-bottom-width'); + + await t.expect(parseInt(rowsViewBorderLeft, 10)).eql(expectedBorderWidth); + await t.expect(parseInt(rowsViewBorderRight, 10)).eql(expectedBorderWidth); + await t.expect(parseInt(rowsViewBorderBottom, 10)).eql(expectedBorderWidth); + }; + + const checkShowRowLinesState = async ( + t: TestController, + dataGrid: DataGrid, + showRowLines: boolean, + showBorders: boolean, + ) => { + const expectedBorderWidth = showRowLines ? BORDER_WIDTH.normal : BORDER_WIDTH.none; + /* + getRows() returns double collection of rows (two tables) when + columnFixing.legacyMode = true AND DataGrid has fixed columns + */ + const filteredRows = dataGrid.getRows().filter(`.${SELECTORS.rowLinesClass}`); + const cells = filteredRows.find('td'); + const cellsCount = await cells.count; + + for (let i = 0; i < cellsCount; i += 1) { + const dataCell = cells.nth(i); + + // Skip checking for last lines if showBorders is enabled + if (showBorders) { + const parentRow = dataCell.parent('tr'); + const nextRow = parentRow.nextSibling('tr.dx-row-lines'); + const isLastRow = await nextRow.count === 0; + + if (isLastRow) { + // eslint-disable-next-line no-continue + continue; + } + } + + const borderBottom = await dataCell.getStyleProperty('border-bottom-width'); + await t.expect(parseInt(borderBottom, 10)).eql(expectedBorderWidth); + } + }; + + const checkShowColumnLinesState = async ( + t: TestController, + dataGrid: DataGrid, + showColumnLines: boolean, + ) => { + const getExpBorderWith = ( + isColumnLinesEnabled: boolean, + hasPointerEventsNoneClass: boolean, + ) => { + if (hasPointerEventsNoneClass) { + return BORDER_WIDTH.big; + } + + if (isColumnLinesEnabled) { + return BORDER_WIDTH.normal; + } + + return BORDER_WIDTH.none; + }; + + /* + getRows() returns double collection of rows (two tables) when + columnFixing.legacyMode = true AND DataGrid has fixed columns + */ + const filteredRows = dataGrid.getRows().filter(`:not(.${SELECTORS.masterDetailRowClass})`); + const cells = filteredRows.find(`td:not(.${SELECTORS.groupSpaceClass})`); + + const cellsCount = await cells.count; + + for (let i = 0; i < cellsCount; i += 1) { + const cell = cells.nth(i); + + const parentRow = cell.parent('tr'); + const rowCells = parentRow.find(`td:not(.${SELECTORS.groupSpaceClass})`); + const rowCellsCount = await rowCells.count; + const indexInRow = await cell.prevSibling(`td:not(.${SELECTORS.groupSpaceClass})`).count; + + const isFirstCellInRow = indexInRow === 0; + const isLastCellInRow = indexInRow === rowCellsCount - 1; + + const cellClasses = await cell.getAttribute('class'); + const hasPointerEventsNoneClass = cellClasses?.includes(SELECTORS.pointerEventsNoneClass); + const expectedBorderWidth = getExpBorderWith(showColumnLines, !!hasPointerEventsNoneClass); + + if (!isFirstCellInRow) { + const borderLeftWidth = await cell.getStyleProperty('border-left-width'); + + await t.expect(parseInt(borderLeftWidth, 10)).eql(expectedBorderWidth); + } + + if (!isLastCellInRow) { + const borderRightWidth = await cell.getStyleProperty('border-right-width'); + + await t.expect(parseInt(borderRightWidth, 10)).eql(expectedBorderWidth); + } + } + }; + + const checkRowAlternationEnabledState = async ( + t: TestController, + dataGrid: DataGrid, + rowAlternationEnabled: boolean, + ) => { + /* + getRows() returns double collection of rows (two tables) when + columnFixing.legacyMode = true AND DataGrid has fixed columns + */ + const filteredRows = dataGrid.getRows().filter(`:not(.${SELECTORS.masterDetailRowClass})`); + const filteredRowsLength = await filteredRows.count; + + let i = 1; + while (i < filteredRowsLength) { + const currentRow = filteredRows.nth(i); + const previousRow = filteredRows.nth(i - 1); + + const currentClasses = await currentRow.getAttribute('class'); + const previousClasses = await previousRow.getAttribute('class'); + + if (currentClasses?.includes(SELECTORS.groupRowClass)) { + i += 2; + // eslint-disable-next-line no-continue + continue; + } + + if (previousClasses?.includes(SELECTORS.groupRowClass)) { + i += 1; + // eslint-disable-next-line no-continue + continue; + } + + const currentHasAltClass = currentClasses?.includes(SELECTORS.rowAlternativeClass); + const previousHasAltClass = previousClasses?.includes(SELECTORS.rowAlternativeClass); + + if (rowAlternationEnabled) { + await t.expect(currentHasAltClass).notEql(previousHasAltClass); + } else { + await t.expect(currentHasAltClass).eql(previousHasAltClass); + } + + const currentFirstCell = currentRow.find('td').nth(0); + const previousFirstCell = previousRow.find('td').nth(0); + + const currentFirstCellBg = await currentFirstCell.getStyleProperty('background-color'); + const previousFirstCellBg = await previousFirstCell.getStyleProperty('background-color'); + + if (rowAlternationEnabled) { + await t.expect(currentFirstCellBg).notEql(previousFirstCellBg); + } else { + await t.expect(currentFirstCellBg).eql(previousFirstCellBg); + } + + i += 1; + } + }; + + const verifyGridStyles = async (t: TestController, dataGrid: DataGrid, { + showBorders, showRowLines, rowAlternationEnabled, showColumnLines, + }: MatrixOptions) => { + await checkShowBordersState(t, dataGrid, showBorders); + await checkShowRowLinesState(t, dataGrid, showRowLines, showBorders); + await checkShowColumnLinesState(t, dataGrid, showColumnLines); + await checkRowAlternationEnabledState(t, dataGrid, rowAlternationEnabled); + }; + + const functionalTest = (matrixOptions: MatrixOptions) => { + + test(`Should have correct applied styles with ${getTestParams(matrixOptions)}`, async ({ page }) => { + await createDataGrid(matrixOptions); + + await page.locator('.dx-datagrid').first().isVisible(); + + await verifyGridStyles(t, dataGrid, matrixOptions); + + const rowIdx = matrixOptions.hasMasterDetail ? 8 : 5; + const colIdx = matrixOptions.hasMasterDetail ? 5 : 4; + const deleteBtn = matrixOptions.hasFixedColumn + ? dataGrid.getFixedDataRow(rowIdx).locator('.dx-command-edit').nth(colIdx).element + : page.locator('.dx-data-row').nth(rowIdx).locator('.dx-command-edit').nth(colIdx); + + await (deleteBtn).click(); + + await verifyGridStyles(t, dataGrid, matrixOptions); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/markup/T1240074_hoveringRows.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/markup/T1240074_hoveringRows.spec.ts new file mode 100644 index 000000000000..175185670438 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/markup/T1240074_hoveringRows.spec.ts @@ -0,0 +1,37 @@ +import { test, expect } from '@playwright/test'; +import { createWidget } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('HoveringRows', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + test('Hover over rows in the middle', async ({ page }) => { + await createWidget(page, 'dxDataGrid', + { + dataSource: getData(20, 3), + hoverStateEnabled: true, + }, + ); + + const firstRow = page.locator('.dx-data-row').nth(10); + const secondRow = page.locator('.dx-data-row').nth(11); + + await (firstRow.element).hover() + .expect(firstRow.isHovered) + .ok(); + + await (secondRow.element).hover() + .expect(firstRow.isHovered) + .notOk() + .expect(secondRow.isHovered) + .ok(); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/markup/T1286265_deletedRowHeight.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/markup/T1286265_deletedRowHeight.spec.ts new file mode 100644 index 000000000000..766bfe9bf37a --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/markup/T1286265_deletedRowHeight.spec.ts @@ -0,0 +1,69 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('DataGrid deleted row height consistency T1286265', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + const ROW_INDEX = 1; + + // visual: generic.light + // visual: generic.light.compact + // visual: material.blue.light + // visual: material.blue.light.compact + // visual: fluent.blue.light + // visual: fluent.blue.light.compact + + test('When DataGrid has fixed column row height should not change when marked as deleted - generic.light', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: [ + { id: 1, name: 'John Smith' }, + { id: 2, name: 'Jane Johnson' }, + { id: 3, name: 'Mike Wilson' }, + ], + keyExpr: 'id', + height: 300, + showBorders: true, + showRowLines: true, + columns: [ + { dataField: 'id', width: 50, fixed: true }, + { dataField: 'name', width: 150 }, + ], + editing: { + mode: 'batch', + allowDeleting: true, + }, + }); + + // Arrange + // Get the initial height of the row at index + const initialRow = page.locator('.dx-data-row').nth(ROW_INDEX); + const initialRowHeight = await initialRow.element.clientHeight; + + // Act - mark the row as deleted + await dataGrid.apiDeleteRow(ROW_INDEX); + + // Assert - check if the row is marked as deleted + expect(await page.locator('.dx-data-row').nth(ROW_INDEX).isRemoved); + await t.ok('Row should be marked as deleted'); + + // Get the height of the deleted row + const deletedRow = page.locator('.dx-data-row').nth(ROW_INDEX); + const deletedRowHeight = await deletedRow.element.clientHeight; + + // Assert - check if the height remains consistent + expect(await deletedRowHeight); + await t.eql(initialRowHeight, 'Row height should not change when marked as deleted'); + + // Take a screenshot for visual verification + await testScreenshot(page, 'datagrid-deleted-row-height-row-lines-and-fixed-column.png', { element: page.locator('#container') }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/markup/T838734_alternateRowSizes.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/markup/T838734_alternateRowSizes.spec.ts new file mode 100644 index 000000000000..8bde81d6a672 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/markup/T838734_alternateRowSizes.spec.ts @@ -0,0 +1,57 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('Grouping Panel - Borders with enabled alternate rows', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + const GRID_SELECTOR = '#container'; + + const generateData = (rowCount) => new Array(rowCount).fill(null).map((_, idx) => ({ + A: `A_${idx}`, + B: `B_${idx}`, + C: `C_${idx}`, + })); + + test('Alternate rows should be the same size', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: generateData(10), + columns: ['A', 'B', { + dataField: 'C', + cellTemplate: ($container, { value }) => { + const $root = $('
'); + $('
') + .text('C template') + .appendTo($root); + $('
') + .text(value) + .appendTo($root); + $root.appendTo($container); + }, + }], + onCellPrepared: ({ cellElement, value }) => { + if (typeof value === 'string' && value.startsWith('B')) { + // @ts-expect-error todo check + cellElement.html(` +
+
B template:
+
${value}
+
+ `); + } + }, + showRowLines: false, + rowAlternationEnabled: true, + }); + + await testScreenshot(page, 'T838734_alternate-rows-same-size.png', { element: page.locator('#container') }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/markup/iconSizes.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/markup/iconSizes.spec.ts new file mode 100644 index 000000000000..62cfd60b0183 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/markup/iconSizes.spec.ts @@ -0,0 +1,58 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('Icon Sizes', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + // visual: fluent.blue.light.compact + + test('Correct icon sizes (T1207612)', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: [...new Array(3)].map((_, index) => ({ id: index, text: `item ${index}`, group: `group ${index % 2}` })), + keyExpr: 'id', + width: 550, + columns: [ + { dataField: 'id' }, + { dataField: 'text', sortOrder: 'asc' }, + { dataField: 'group', groupIndex: 0 }, + { dataField: 'hidden', hidingPriority: 0 }, + ], + editing: { + allowAdding: true, + allowUpdating: true, + allowDeleting: true, + }, + showBorders: true, + filterValue: ['Id', '>=', 0], + filterPanel: { visible: true }, + headerFilter: { visible: true }, + filterRow: { visible: true }, + groupPanel: { visible: true }, + searchPanel: { visible: true }, + selection: { mode: 'multiple' }, + rowDragging: { allowReordering: true }, + columnChooser: { enabled: true }, + columnHidingEnabled: true, + masterDetail: { enabled: true }, + export: { enabled: true }, + pager: { + visible: true, + allowedPageSizes: [5, 10, 'all'], + showPageSizeSelector: true, + showInfo: true, + showNavigationButtons: true, + }, + }); + + await testScreenshot(page, 'icon-sizes.png'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/markup/markup.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/markup/markup.spec.ts new file mode 100644 index 000000000000..461d7a9778b8 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/markup/markup.spec.ts @@ -0,0 +1,37 @@ +import { test, expect } from '@playwright/test'; +import { createWidget } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('Icon Sizes', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + test('Load panel should support string height and width', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: [], + columns: [ + 'field1', 'field2', 'field3', + ], + width: 700, + loadPanel: { + enabled: true, + height: '400px', + width: '330px', + }, + }); + + await dataGrid.apiBeginCustomLoading('test'); + + expect(await dataGrid.getLoadPanel().getContent().getStyleProperty('height')); + await t.eql('400px'); + expect(await dataGrid.getLoadPanel().getContent().getStyleProperty('width')); + await t.eql('330px'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/markup/noDataText.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/markup/noDataText.spec.ts new file mode 100644 index 000000000000..3036c9d0b53f --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/markup/noDataText.spec.ts @@ -0,0 +1,69 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('No Data', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + const GRID_CONTAINER = '#container'; + + test('The noDataText element should be rendered when a lookup column is filtered (T1293839)', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: [ + { ID: 1, Name: 'John', Lookup: 1 }, + { ID: 2, Name: 'Jane', Lookup: 2 }, + ], + keyExpr: 'ID', + columns: ['Name', { + dataField: 'Lookup', + lookup: { + dataSource: [ + { ID: 1, Text: 'Item 1' }, + { ID: 2, Text: 'Item 2' }, + ], + valueExpr: 'ID', + displayExpr: 'Text', + }, + }], + showBorders: true, + filterRow: { visible: true }, + onEditorPreparing(e) { + e.updateValueTimeout = 0; + }, + }); + + // arrange + const nameFilterInput = page.locator('.dx-datagrid-filter-row td').nth(0).getEditorInput().element; + const lookupFilterEditor = dataGrid.getFilterEditor(1, SelectBox); + + // assert + expect(await page.locator('.dx-datagrid').first().isVisible()); + await t.ok(); + + // act + await (lookupFilterEditor.element).click(); + + // assert + expect(await lookupFilterEditor.isVisible()()).toBeTruthy(); + + // act + const lookupList = await lookupFilterEditor.getList(); + const lookupItem = lookupList.getItem(1); + await (lookupItem.element).click(); + await (nameFilterInput).fill('test'); + + // assert + expect(await page.locator('.dx-datagrid').first().isVisible()); + await t.ok(); + + await testScreenshot(page, 'T1293839-grid-no-data-text-rendered.png', { element: page.locator('#container') }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/masterDetail.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/masterDetail.spec.ts new file mode 100644 index 000000000000..d01975ddb5b6 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/masterDetail.spec.ts @@ -0,0 +1,77 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('Master detail', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + // visual: material.blue.light + // visual: generic.light + + test('Checkbox align right in masterdetail (T1045321) generic.light', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: [{ + ID: 1, + Prefix: 'Mr.', + }], + keyExpr: 'ID', + showBorders: true, + selection: { + mode: 'multiple', + }, + columns: [ + { + dataField: 'Prefix', + caption: 'Title', + width: 400, + }, + ], + masterDetail: { + autoExpandAll: true, + enabled: true, + template(container) { + ($('
') as any) + .dxTreeList({ + columnAutoWidth: true, + showBorders: true, + selection: { + mode: 'multiple', + }, + dataSource: [{ + ID: 1, + Title: 'CEO', + Hire_Date: '1995-01-15', + }], + rootValue: -1, + keyExpr: 'ID', + parentIdExpr: 'Head_ID', + columns: [ + { + dataField: 'Title', + caption: 'Position', + width: 200, + }, + { + dataField: 'Hire_Date', + dataType: 'date', + width: 200, + }, + ], + showRowLines: true, + }) + .appendTo(container); + }, + }, + }); + + await testScreenshot(page, 'T1045321.png'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/pager.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/pager.spec.ts new file mode 100644 index 000000000000..119efe76c4ba --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/pager.spec.ts @@ -0,0 +1,70 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('Pager', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + async function createDataGridWithPager(page: any): Promise { + const dataSource = Array.from({ length: 100 }, (_, room) => ({ name: 'Alex', phone: '555555', room })); + return createWidget(page, 'dxDataGrid', { + dataSource, + columns: ['name', 'phone', 'room'], + paging: { + pageSize: 5, + pageIndex: 5, + }, + pager: { + showPageSizeSelector: true, + allowedPageSizes: [5, 10, 20], + showInfo: true, + showNavigationButtons: true, + }, + }); + } + ); + + test('Full size pager', async ({ page }) => { + await createDataGridWithPager(); + + const pager = page.locator('.dx-pager'); + await page.setViewportSize({ width: 900, height: 600 }); + expect(await pager.locator('.dx-page-size').nth(0).evaluate(el => el.classList.contains('dx-selection'))); + await t.ok('page size 5 selected'); + expect(await pager.locator('.dx-page').filter({hasText: '6'}).evaluate(el => el.classList.contains('dx-selection'))); + await t.ok('page 6 selected'); + expect(await pager.locator('.dx-info').textContent); + await t.eql('Page 6 of 20 (100 items)'); + expect(await page.locator('.dx-data-row').nth(29).locator('td').nth(2).textContent()); + await t.eql('29'); + // set page sige to 10 + await (pager.locator('.dx-page-size').nth(1).click().element); + expect(await page.locator('.dx-data-row').nth(10 * 6 - 1).locator('td').nth(2).textContent()); + await t.eql('59'); + // set page index 7 + await (pager.locator('.dx-page').filter({hasText: '7'}).click().element); + expect(await page.locator('.dx-data-row').nth(10 * 7 - 1).locator('td').nth(2).textContent()); + await t.eql('69'); + expect(await pager.locator('.dx-info').textContent); + await t.eql('Page 7 of 10 (100 items)'); + // navigate to prev page (6) + await (pager.locator('.dx-navigate-button.dx-prev-button').click().element); + expect(await pager.locator('.dx-info').textContent); + await t.eql('Page 6 of 10 (100 items)'); + // navigate to next page (7) + await (pager.locator('.dx-navigate-button.dx-next-button').click().element); + expect(await pager.locator('.dx-info').textContent); + await t.eql('Page 7 of 10 (100 items)'); + + await testScreenshot(page, 'pager-full-allpages.png'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/rowDragging/functional.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/rowDragging/functional.spec.ts new file mode 100644 index 000000000000..fc0fb552273f --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/rowDragging/functional.spec.ts @@ -0,0 +1,160 @@ +import { test, expect } from '@playwright/test'; +import { createWidget } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('Row dragging.Functional', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + /* eslint-disable @typescript-eslint/no-misused-promises */ + + const CLASS = { ...DataGridClassNames, ...ClassNames }; + + const isPlaceholderVisible = ClientFunction(() => $(`.${CLASS.sortablePlaceholder}`).is(':visible'), { dependencies: { CLASS } }); + + const getPlaceholderOffset = ClientFunction(() => $(`.${CLASS.sortablePlaceholder}`).offset(), { dependencies: { CLASS } }); + + const getRowsViewLeftOffset = ClientFunction(() => $(`#container .${CLASS.dataGridRowsView}`).offset()?.left, { dependencies: { CLASS } }); + + const getDraggingElementLeftOffset = ClientFunction(() => $(`.${CLASS.sortableDragging}`).offset()?.left, { dependencies: { CLASS } }); + + const getDraggingElementScrollPosition = ClientFunction(() => { + const $dataGrid = $(`.${CLASS.sortableDragging}`).find(`.${CLASS.dataGrid}`).first().parent(); + const dataGridInstance = $dataGrid.data('dxDataGrid'); + const scrollableInstance = dataGridInstance.getScrollable(); + + return { + left: scrollableInstance.scrollLeft(), + top: scrollableInstance.scrollTop(), + }; + }, { dependencies: { CLASS } }); + + const getFreeSpaceRowOffset = ClientFunction(() => { + const $freeSpaceRow = $('#container').find(`.${CLASS.dataGridRowsView} table .${CLASS.freeSpaceRow}`).first(); + + return $freeSpaceRow?.offset(); + }, { dependencies: { CLASS } }); + + const scrollTo = ClientFunction((x, y) => { + window.scrollTo(x, y); + }); + + const generateData = (rowCount, columnCount): Record[] => { + const items: Record[] = []; + + for (let i = 0; i < rowCount; i += 1) { + const item = {}; + + for (let j = 0; j < columnCount; j += 1) { + item[`field${j + 1}`] = `${i + 1}-${j + 1}`; + } + + items.push(item); + } + + return items; + }; + + ); + + // T903351 + + test('The placeholder should appear when a cross-component dragging rows after scrolling the window', async ({ page }) => { + await t.maximizeWindow(); + await page.evaluate(() => { + $('body').css('display', 'flex'); + $('#container, #otherContainer').css({ + display: 'inline-block', + 'margin-top': '800px', + width: '50%', + }); + }); + + return Promise.all([ + createWidget(page, 'dxDataGrid', { + width: 400, + dataSource: [ + { + id: 1, name: 'Name 1', age: 19, + }, + { + id: 2, name: 'Name 2', age: 11, + }, + { + id: 3, name: 'Name 3', age: 15, + }, + { + id: 4, name: 'Name 4', age: 16, + }, + { + id: 5, name: 'Name 5', age: 25, + }, + { + id: 6, name: 'Name 6', age: 18, + }, + { + id: 7, name: 'Name 7', age: 21, + }, + { + id: 8, name: 'Name 8', age: 14, + }, + ], + columns: ['name', 'age'], + rowDragging: { + group: 'shared', + }, + }), + createWidget(page, 'dxTreeList', { + columnAutoWidth: true, + dataSource: [ + { + id: 1, parentId: 0, name: 'Name 1', age: 19, + }, + { + id: 2, parentId: 1, name: 'Name 2', age: 11, + }, + { + id: 3, parentId: 0, name: 'Name 3', age: 15, + }, + { + id: 4, parentId: 3, name: 'Name 4', age: 16, + }, + { + id: 5, parentId: 0, name: 'Name 5', age: 25, + }, + { + id: 6, parentId: 5, name: 'Name 6', age: 18, + }, + { + id: 7, parentId: 0, name: 'Name 7', age: 21, + }, + { + id: 8, parentId: 7, name: 'Name 8', age: 14, + }, + ], + autoExpandAll: true, + columns: ['name', 'age'], + rowDragging: { + group: 'shared', + }, + }, '#otherContainer'), + ]); + + expect(await page.locator('.dx-datagrid').first().isVisible()); + await t.ok(); + + await scrollTo(0, 10000); + await dataGrid.moveRow(6, 500, 0, true); + await dataGrid.moveRow(6, 550, 0); + + expect(await isPlaceholderVisible()).toBeTruthy(); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/rowDragging/visual.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/rowDragging/visual.spec.ts new file mode 100644 index 000000000000..66761f56ae28 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/rowDragging/visual.spec.ts @@ -0,0 +1,62 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('Row dragging.Visual', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + // T1179218 + + test('Rows should appear correctly during dragging when virtual scrolling is enabled and rowDragging.dropFeedbackMode = "push"', async ({ page }) => { + await t.maximizeWindow(); + return createWidget(page, 'dxDataGrid', { + height: 440, + keyExpr: 'id', + scrolling: { + mode: 'virtual', + }, + dataSource: [...new Array(100)].fill(null).map((_, index) => ({ id: index })), + columns: ['id'], + rowDragging: { + allowReordering: true, + dropFeedbackMode: 'push', + }, + }); + + expect(await page.locator('.dx-datagrid').first().isVisible()); + await t.ok(); + + // drag the row down + await dataGrid.moveRow(0, 30, 150, true); + await dataGrid.moveRow(0, 30, await getOffsetToTriggerAutoScroll(0, 1, 'down')); + + // waiting for autoscrolling + await page.waitForTimeout(2000); + + expect(await page.locator('.dx-data-row').nth(99).locator('td').nth(1).textContent()); + await t.eql('99'); + expect(await isScrollAtEnd('vertical')); + await t.ok(); + + // drag the row up + await dataGrid.moveRow(0, 30, await getOffsetToTriggerAutoScroll(0, 1)); + + // waiting for autoscrolling + await page.waitForTimeout(2000); + + expect(await page.locator('.dx-data-row').nth(0).locator('td').nth(1).textContent()); + await t.eql('0'); + expect(await page.evaluate(() => ($('#container') as any).dxDataGrid('instance').getScrollable().scrollTop())); + await t.eql(0); + + await testScreenshot(page, 'T1179218-virtual-scrolling-dragging-row.png', { element: page.locator('#container') }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/scrolling.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/scrolling.spec.ts new file mode 100644 index 000000000000..1f8fc1f0027e --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/scrolling.spec.ts @@ -0,0 +1,73 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, insertStylesheetRulesToPage } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('Scrolling', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + async function getMaxRightOffset(dataGrid: DataGrid): Promise { + const scrollWidth = await dataGrid.getScrollWidth(); + const rowsViewWidth = await dataGrid.getRowsView().clientWidth; + return scrollWidth - rowsViewWidth; + } + + async function getRightScrollOffset(dataGrid: DataGrid): Promise { + const maxHorizontalOffset = await getMaxRightOffset(dataGrid); + const scrollLeft = await dataGrid.getScrollLeft(); + return maxHorizontalOffset - scrollLeft; + } + + function getData(rowCount: number, colCount: number): Record[] { + const items: Record[] = []; + for (let i = 0; i < rowCount; i += 1) { + const item: Record = {}; + for (let j = 0; j < colCount; j += 1) { + item[`field_${j}`] = `val_${i}_${j}`; + } + items.push(item); + } + + return items; + } + + async function getTestLoadCount(page: any): Promise { + return ClientFunction(() => (window as any).testLoadCount as number)(); + } + + ); + + test('DataGrid should set the scrollbar position to the left on resize (T934842)', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: getData(1, 50), + columnWidth: 100, + }); + + // act + await page.setViewportSize({ width: 900, height: 250 }); + + // assert + expect(await dataGrid.getScrollLeft()).toBe(0); + + // act + await page.setViewportSize({ width: 700, height: 250 }); + + // assert + expect(await dataGrid.getScrollLeft()).toBe(0); + + // act + await page.setViewportSize({ width: 600, height: 250 }); + + // assert + expect(await dataGrid.getScrollLeft()).toBe(0); + }); + // TODO: .after() block removed +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/searchPanel.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/searchPanel.spec.ts new file mode 100644 index 000000000000..33bc46d8c284 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/searchPanel.spec.ts @@ -0,0 +1,50 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('Search Panel', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + // T1046688 + // visual: material.blue.light + + test('searchPanel has correct view inside masterDetail', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: [{ column1: 'first' }], + columns: ['column1'], + masterDetail: { + enabled: true, + template(container) { + ($('
') as any) + .dxDataGrid({ + searchPanel: { + visible: true, + width: 240, + placeholder: 'Search...', + }, + columns: ['detail1'], + dataSource: [], + }) + .appendTo(container); + }, + }, + }); + + // act + await (page.locator('.dx-data-row').click().nth(0).locator('.dx-command-edit').nth(0)); + + const masterRow = page.locator('.dx-master-detail-row').nth(0); + const masterGrid = masterRow.getDataGrid(); + + // assert + await testScreenshot(page, 'T1046688.searchPanel.png', { element: masterGrid.element }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/security/xss.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/security/xss.spec.ts new file mode 100644 index 000000000000..6a2dbec0a657 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/security/xss.spec.ts @@ -0,0 +1,39 @@ +import { test, expect } from '@playwright/test'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('XSS', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + .beforeEach(async (t) => { + await t + .setNativeDialogHandler((type) => { + if (type === 'alert') { + throw Error('XSS alert was invoked!'); + } + }) + .navigateTo(url(__dirname, './pages/XSS.html')); + }) + .afterEach(async (t) => { + await t.navigateTo(url(__dirname, '../../../container.html')); + }); + + test('The XSS script does not run when the markup has been replaced with text', async ({ page }) => { + const filterBuilder = new FilterBuilder('#filter-builder'); + const group = filterBuilder.getField(0, 'groupOperation'); + + await (group.element).click(); + expect(await FilterBuilder.getPopupTreeView().visible).toBeTruthy(); + await (FilterBuilder.getPopupTreeViewNode().click()); + expect(await true); + await t.ok(); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/selection.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/selection.spec.ts new file mode 100644 index 000000000000..4ecae689181d --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/selection.spec.ts @@ -0,0 +1,48 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('Selection', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + test('selectAll state should be correct after unselect item if refresh(true) is called inside onSelectionChanged (T1048081)', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: [ + { id: 1 }, + { id: 2 }, + { id: 3 }, + { id: 4 }, + ], + keyExpr: 'id', + selectedRowKeys: [1, 2], + paging: { + pageSize: 3, + }, + selection: { + mode: 'multiple', + }, + onSelectionChanged(e) { + e.component.refresh(true); + }, + }); + + const firstRowSelectionCheckBox = new CheckBox(page.locator('.dx-data-row').nth(0).locator('td').nth(0).locator('.dx-editor-cell')); + const selectAllCheckBox = new CheckBox( + page.locator('.dx-header-row').nth(0).locator('td').nth(0).locator('.dx-editor-cell'), + ); + + // act + await (firstRowSelectionCheckBox.element).click(); + // assert + expect(await selectAllCheckBox.option('value')).toBe(undefined); + expect(await firstRowSelectionCheckBox.option('value')).toBe(false); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/sorting/sorting.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/sorting/sorting.spec.ts new file mode 100644 index 000000000000..226343cdfa86 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/sorting/sorting.spec.ts @@ -0,0 +1,58 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('Sorting', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + test('Filter expression should be valid when sortingMethod, remoteOperations, and autoNavigateToFocusedRow are specified (T1200546)', async ({ page }) => { + await createWidget(page, 'dxDataGrid', () => { + const sampleData = Array.from({ length: 20 }, (_, i) => ({ ID: i + 1, Name: `Name ${i + 1}` })); + const sampleAPI = new (window as any).DevExpress.data.ArrayStore(sampleData); + const store = new (window as any).DevExpress.data.CustomStore({ + key: 'ID', + load(o) { + if (o.filter && typeof o.filter[0] === 'function') { + return Promise.reject(); + } + return Promise.all([sampleAPI.load(o), sampleAPI.totalCount(o)]).then((res) => ({ + data: res[0], + totalCount: res[1], + })); + }, + }); + return { + dataSource: store, + remoteOperations: true, + columns: ['ID', { + dataField: 'Name', + sortOrder: 'asc', + sortingMethod() { + return 1; + }, + }], + paging: { pageSize: 5 }, + scrolling: { mode: 'virtual' }, + height: 200, + showBorders: true, + focusedRowEnabled: true, + focusedRowKey: 18, + autoNavigateToFocusedRow: true, + }; + }); + + // assert + expect(await dataGrid.dataRows.count); + await t.eql(6); + expect(await dataGrid.getErrorRow().exists); + await t.eql(false); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/stateStoring/stateStoring.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/stateStoring/stateStoring.spec.ts new file mode 100644 index 000000000000..f124fba9451e --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/stateStoring/stateStoring.spec.ts @@ -0,0 +1,66 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('State Storing', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + const GRID_CONTAINER = '#container'; + + const makeLocalStorageJsonInvalid = ClientFunction(() => { + window.localStorage.testStorageKey = '{]'; + }); + + const dataGridConfig = { + dataSource: [ + { id: 0, text: 'item 1' }, + { id: 1, text: 'item 2' }, + { id: 2, text: 'item 3' }, + { id: 3, text: 'item 4' }, + ], + columnFixing: { + enabled: true, + }, + keyExpr: 'id', + stateStoring: { + enabled: true, + }, + scrolling: { + mode: 'virtual' as any, + }, + customizeColumns(columns) { + columns[0].fixed = true; + columns[0].fixedPosition = 'sticky'; + }, + }; + + test('The Grid should load if JSON in localStorage is invalid and stateStoring enabled', async ({ page }) => { + await makeLocalStorageJsonInvalid(); + await createWidget(page, 'dxDataGrid', { + dataSource: [ + { A: 1, B: 2, C: 3 }, + { A: 4, B: 5, C: 6 }, + { A: 7, B: 8, C: 9 }, + ], + stateStoring: { + enabled: true, + storageKey: 'testStorageKey', + }, + }); + + const secondCell = page.locator('.dx-data-row').nth(1).locator('td').nth(1); + const consoleMessages = await t.getBrowserConsoleMessages(); + + expect(await secondCell.element().innerText).toBe('5'); + const isWarningExist = !!consoleMessages.warn.find((message) => message.startsWith('W1022')); + expect(await isWarningExist).toBeTruthy(); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/summary.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/summary.spec.ts new file mode 100644 index 000000000000..475e67fcce70 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/summary.spec.ts @@ -0,0 +1,47 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('Summary', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + test('Group footer summary should be focusable', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: [ + { id: 1, value: 1 }, + { id: 2, value: 1 }, + { id: 3, value: 1 }, + { id: 4, value: 1 }, + ], + columns: [ + 'id', + { + dataField: 'value', + groupIndex: 0, + }, + ], + summary: { + groupItems: [ + { + column: 'id', + summaryType: 'count', + showInGroupFooter: true, + }, + ], + }, + }); + + await (page.locator('.dx-data-row').click().nth(4).locator('td').nth(1)); + await page.keyboard.press('tab'); + + await testScreenshot(page, 'group-summary-focused.png', { element: page.locator('#container') }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/tagBox.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/tagBox.spec.ts new file mode 100644 index 000000000000..a79875bd06d7 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/tagBox.spec.ts @@ -0,0 +1,58 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('Tagbox Columns', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + // T1228720 + // visual: generic.light + // visual: material.blue.light + // visual: fluent.blue.light + + test('Datagrid tagbox column should not look broken', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + showBorders: true, + allowColumnResizing: true, + dataSource: [{ id: 1, items: [1, 2, 3, 4, 5] }], + columns: [ + 'id', + { + dataField: 'items', + editCellTemplate(container, cellInfo) { + ($('
') as any) + .dxTagBox({ + dataSource: Array.from({ length: 10 }, (_, index) => ({ + id: index + 1, + text: `item ${index + 1}`, + })), + value: cellInfo.value, + valueExpr: 'id', + displayExpr: 'text', + onValueChanged(e) { + cellInfo.setValue(e.value); + }, + onSelectionChanged() { + cellInfo.component.updateDimensions(); + }, + searchEnabled: true, + }) + .appendTo(container); + }, + }, + ], + editing: { mode: 'batch', allowUpdating: true }, + }); + + await (page.locator('.dx-data-row').click().nth(0).locator('td').nth(1)); + await testScreenshot(page, 'T1228720-grid-tagbox-on-edit.png', { element: page.locator('#container') }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/toast.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/toast.spec.ts new file mode 100644 index 000000000000..8b7974218969 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/toast.spec.ts @@ -0,0 +1,25 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('Toasts in DataGrid', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + test('Toast should be visible after calling and should be not visible after default display time', async ({ page }) => { + createWidget(page, 'dxDataGrid', {}); + + await page.locator('.dx-datagrid').first().isVisible(); + await page.evaluate(() => ($('#container') as any).dxDataGrid('instance').showErrorToast()); + expect(await page.locator('.dx-toast').isVisible()).toBeTruthy(); + await testScreenshot(page, 'ai-column__toast__at-the-right-position.png', { element: page.locator('#container') }); + expect(await page.locator('.dx-toast').isVisible()).toBeFalsy(); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/validation/cellEditing.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/validation/cellEditing.spec.ts new file mode 100644 index 000000000000..adf2c0a014cf --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/validation/cellEditing.spec.ts @@ -0,0 +1,60 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('Validation', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + [true, false].forEach((repaintChangesOnly) => { + + test(`Navigation with tab without saving should not throw an error (repaintChangesOnly: ${repaintChangesOnly})`, async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: [{ + id: 1, + col2: 30, + col3: 240, + }, + { + id: 2, + col2: 15, + col3: 120, + }], + keyExpr: 'id', + repaintChangesOnly, + columnAutoWidth: true, + showBorders: true, + paging: { + enabled: false, + }, + editing: { + mode: 'cell', + allowUpdating: true, + allowAdding: true, + }, + columns: [{ + dataField: 'col2', + validationRules: [{ type: 'required' }], + }, { + dataField: 'col3', + validationRules: [{ type: 'required' }], + }], + }); + + await (grid.locator('td').nth(0, 0).click().element); + + const editor = grid.locator('td').nth(0, 0).locator('.dx-editor-cell'); + + await (editor.element).fill('123'); + await page.keyboard.press('tab'); + + expect(await true).toBeTruthy(); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/validation/validationPopup.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/validation/validationPopup.spec.ts new file mode 100644 index 000000000000..b004dacb1db4 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/validation/validationPopup.spec.ts @@ -0,0 +1,55 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('Validation', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + const GRID_SELECTOR = '#container'; + + ); + + test('Validation popup screenshot', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: getData(20, 2), + height: 400, + showBorders: true, + columns: [{ + dataField: 'field_0', + validationRules: [{ type: 'required' }], + }, { + dataField: 'field_1', + validationRules: [{ type: 'required' }], + }], + editing: { + mode: 'cell', + allowUpdating: true, + allowAdding: true, + }, + }); + + await t.maximizeWindow(); + await (page.locator('.dx-data-row').click().nth(0).locator('td').nth(0)); + await page.keyboard.press('ctrl+a backspace enter'); + + // act + await testScreenshot(page, 'validation-popup.png', { element: page.locator('#container') }); + + // assert + expect(await dataGrid.getRevertTooltip().exists); + await t.ok(); + expect(await dataGrid.getInvalidMessageTooltip().exists); + await t.ok(); + expect(await compareResults.isValid()); + await t.ok(compareResults.errorMessages()); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/virtualColumns/functional.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/virtualColumns/functional.spec.ts new file mode 100644 index 000000000000..7009e5a61802 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/virtualColumns/functional.spec.ts @@ -0,0 +1,55 @@ +import { test, expect } from '@playwright/test'; +import { createWidget } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('Virtual Columns.Functional', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + const generateData = (rowCount: number, columnCount: number): Record[] => { + const items: Record[] = []; + + for (let i = 0; i < rowCount; i += 1) { + const item = {}; + + for (let j = 0; j < columnCount; j += 1) { + item[`field${j + 1}`] = `${i + 1}-${j + 1}`; + } + + items.push(item); + } + + return items; + }; + + test('DataGrid should not scroll back to the focused cell after horizontal scrolling to the right when columnRenderingMode is virtual', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + width: 450, + dataSource: generateData(10, 30), + columnWidth: 100, + scrolling: { + columnRenderingMode: 'virtual', + }, + }); + + await (page.locator('.dx-data-row').click().nth(0).locator('td').nth(0)); + expect(await page.locator('.dx-data-row').nth(0).locator('td').nth(0).focused); + await t.ok(); + + await page.evaluate((opts) => ($('#container') as any).dxDataGrid('instance').getScrollable().scrollTo(opts), { x: 50 }); + + expect(await dataGrid.getScrollLeft()).toBe(50); + + await page.evaluate((opts) => ($('#container') as any).dxDataGrid('instance').getScrollable().scrollTo(opts), { x: 100 }); + + expect(await dataGrid.getScrollLeft()); + await t.eql(100); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/virtualColumns/visual.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/virtualColumns/visual.spec.ts new file mode 100644 index 000000000000..4b20faf34aef --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/virtualColumns/visual.spec.ts @@ -0,0 +1,73 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('Virtual Columns.Visual', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + const showDataGrid = ClientFunction(() => { + $('#wrapperContainer').css('display', ''); + }); + const generateData = (rowCount: number, columnCount: number): Record[] => { + const items: Record[] = []; + + for (let i = 0; i < rowCount; i += 1) { + const item = {}; + + for (let j = 0; j < columnCount; j += 1) { + item[`field${j + 1}`] = `${i + 1}-${j + 1}`; + } + + items.push(item); + } + + return items; + }; + + const generateColumns = (columnCount: number): Column[] => [...new Array(columnCount)] + .map((_, index) => ({ + dataField: `field${index + 1}`, + })); + + // T1090735 + + test('The updateDimensions method should render the grid if a container was hidden and columnRenderingMode is virtual', async ({ page }) => { + await t.maximizeWindow(); + + await page.evaluate(() => { + $('#container').wrap('
'); + }); + + return createWidget(page, 'dxDataGrid', { + height: 440, + dataSource: generateData(150, 500), + columnWidth: 100, + scrolling: { + columnRenderingMode: 'virtual', + }, + }); + + expect(await page.locator('#wrapperContainer').isVisible()); + await t.notOk(); + + await showDataGrid(); + + await page.waitForTimeout(200); + expect(await page.locator('#wrapperContainer').isVisible()); + await t.ok(); + + await dataGrid.apiUpdateDimensions(); + + await testScreenshot(page, 'T1090735-grid-virtual-columns.png', { element: '#wrapperContainer' }); + }); + // TODO: .after() block removed +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/appearance.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/appearance.spec.ts new file mode 100644 index 000000000000..263503a11312 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/appearance.spec.ts @@ -0,0 +1,71 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('FixedColumns - appearance', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + // visual: generic.light + // visual: generic.light.compact + // visual: material.blue + // visual: material.blue.compact + // visual: fluent.blue + // visual: fluent.blue.compact + [false, true].forEach( + (showRowLines) => { + // T1268664 + const showRowLinesState = `showRowLines=${showRowLines ? 'true' : 'false'}`; + + test(`Row height for selected, focus and edit state should not differ from the default one if ${showRowLinesState}`, async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: getData(13, 40), + keyExpr: 'field_0', + columnFixing: { + enabled: true, + }, + groupPanel: { + visible: true, + }, + editing: { + allowUpdating: true, + mode: 'row', + }, + showColumnHeaders: true, + columnAutoWidth: true, + allowColumnReordering: true, + allowColumnResizing: true, + focusedRowEnabled: true, + showRowLines, + selection: { + mode: 'multiple', + }, + customizeColumns(columns) { + columns[5].fixed = true; + columns[6].fixed = true; + + columns[11].fixed = true; + columns[11].fixedPosition = 'right'; + columns[12].fixed = true; + columns[12].fixedPosition = 'right'; + }, + }); + + expect(await page.locator('.dx-datagrid').first().isVisible()).toBeTruthy(); + + await testScreenshot(page, `datagrid_default_state_with_${showRowLinesState}.png`, { element: page.locator('#container') }); + + await (page.locator('.dx-data-row').click().nth(2).locator('.dx-command-edit').nth(41).locator('.dx-link').nth(0)); + await (page.locator('.dx-data-row').click().nth(3).locator('.dx-command-edit').nth(0)); + await (page.locator('.dx-data-row').click().nth(4).locator('td').nth(4)); + + await testScreenshot(page, `datagrid_selected_focused_edit_state_with_${showRowLinesState}.png`, { element: page.locator('#container') }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/columnFixingIcons.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/columnFixingIcons.spec.ts new file mode 100644 index 000000000000..f4a3d740e6e0 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/columnFixingIcons.spec.ts @@ -0,0 +1,35 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('Column Fixing', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + // visual: generic.light + // visual: material.blue + // visual: fluent.blue + + test('Fixed columns: Check context menu items', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: getData(5, 5), + width: '100%', + columnFixing: { + enabled: true, + }, + }); + + expect(await page.locator('.dx-datagrid').first().isVisible()).toBeTruthy(); + + await t.rightClick(page.locator('.dx-header-row').nth(0).element); + await (dataGrid.getContextMenu().click().getItemByText('Set Fixed Position')); + await testScreenshot(page, 'sticky_columns_context_menu.png'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/focusOverlay.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/focusOverlay.spec.ts new file mode 100644 index 000000000000..404258b67729 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/focusOverlay.spec.ts @@ -0,0 +1,116 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('FixedColumns - Focus Overlay', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + test('Focus overlay should be displayed correctly if sticky columns are turned on', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: getData(20, 40), + columnFixing: { + enabled: true, + }, + groupPanel: { + visible: true, + }, + width: 800, + showColumnHeaders: true, + columnAutoWidth: true, + allowColumnReordering: true, + allowColumnResizing: true, + summary: { + totalItems: [{ + column: 'field_1', + summaryType: 'count', + }, { + column: 'field_6', + summaryType: 'count', + }], + groupItems: [{ + column: 'field_0', + summaryType: 'count', + showInGroupFooter: false, + alignByColumn: true, + }, + { + column: 'field_11', + summaryType: 'count', + showInGroupFooter: false, + alignByColumn: true, + }, { + column: 'field_6', + summaryType: 'count', + showInGroupFooter: true, + }], + }, + customizeColumns(columns) { + columns[5].fixed = true; + columns[6].fixed = true; + + columns[11].fixed = true; + columns[11].fixedPosition = 'right'; + columns[12].fixed = true; + columns[12].fixedPosition = 'right'; + + columns.splice(15, 5, { + caption: 'Band column 1', + columns: [{ + caption: 'Nested column 1', + columns: ['field_15', 'field_16'], + }, + 'field_17', + { + caption: 'Nested column 2', + columns: ['field_18', 'field_19'], + }], + }); + + columns.splice(25, 4, { + caption: 'Band column 2', + columns: [ + 'field_29', + { + caption: 'Nested column 3', + columns: ['field_30', 'field_31'], + }, + 'field_32', + ], + }); + + columns[0].hidingPriority = 0; + columns[columns.length - 1].hidingPriority = 1; + columns[columns.length - 2].hidingPriority = 2; + columns[columns.length - 3].hidingPriority = 3; + + columns[1].groupIndex = 0; + columns[2].groupIndex = 1; + }, + }); + + expect(await page.locator('.dx-datagrid').first().isVisible()).toBeTruthy(); + + await (page.locator('.dx-group-row').click().nth(0).getCell(1).element); + await page.keyboard.press('tab'); + + await testScreenshot(page, 'datagrid_group_row_focused.png', { element: page.locator('#container') }); + + await (page.locator('.dx-data-row').click().nth(2).locator('.dx-command-edit').nth(40).getAdaptiveButton()); + await page.keyboard.press('tab'); + + await testScreenshot(page, 'datagrid_adaptive_item_focused.png', { element: page.locator('#container') }); + + await (dataGrid.getGroupFooterRow().click().nth(0), { offsetX: 5, offsetY: 5 }); + await page.keyboard.press('tab'); + + await testScreenshot(page, 'datagrid_group_footer_row_focused.png', { element: page.locator('#container') }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/stickyColumnReordering.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/stickyColumnReordering.spec.ts new file mode 100644 index 000000000000..0a97e82258c3 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/stickyColumnReordering.spec.ts @@ -0,0 +1,42 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('Reorder columns', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + test('Move left fixed column to the right', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: getData(5, 25), + columnAutoWidth: true, + allowColumnReordering: true, + columnWidth: 100, + customizeColumns: (columns) => { + columns[5].fixed = true; + columns[5].fixedPosition = 'left'; + columns[6].fixed = true; + columns[6].fixedPosition = 'left'; + columns[7].fixed = true; + columns[7].fixedPosition = 'left'; + }, + }); + + // arrange + expect(await page.locator('.dx-datagrid').first().isVisible()).toBeTruthy(); + + // act + await t.drag(page.locator('.dx-header-row').nth(0).locator('td').nth(0), 400, 0); + + await testScreenshot(page, 'move_left_fixed_column_to_right.png', { element: page.locator('#container') }); + + // assert + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/stickyColumnResizing.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/stickyColumnResizing.spec.ts new file mode 100644 index 000000000000..6b0c301073e2 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/stickyColumnResizing.spec.ts @@ -0,0 +1,57 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('Resize columns - nextColumn mode', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + const scrollTo = ClientFunction((x = 0, y = 0) => { + window.scrollTo(x, y); + }); + [false, true].forEach((rtlEnabled) => { + + test(`Resize first fixed column width with left position (rtlEnabled = ${rtlEnabled})`, async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: getData(5, 25), + rtlEnabled, + columnAutoWidth: true, + allowColumnResizing: true, + columnWidth: 200, + columnResizingMode: 'nextColumn', + customizeColumns: (columns) => { + columns[5].fixed = true; + columns[5].fixedPosition = 'left'; + columns[6].fixed = true; + columns[6].fixedPosition = 'left'; + }, + }); + + // arrange + const columnIndex = rtlEnabled ? 23 : 1; + const scrollLeft = rtlEnabled ? -10000 : 10000; + + expect(await page.locator('.dx-datagrid').first().isVisible()).toBeTruthy(); + + // act + await dataGrid.resizeHeader(columnIndex, 100); + + await testScreenshot(page, `resize_first_fixed_column_with_left_position_1_(nextColumn_mode_and_rtl_=_${rtlEnabled}).png`, { element: page.locator('#container') }); + + // act + await page.evaluate((opts) => ($('#container') as any).dxDataGrid('instance').getScrollable().scrollTo(opts), { x: scrollLeft }); + + await testScreenshot(page, `resize_first_fixed_column_with_left_position_2_(nextColumn_mode_and_rtl_=_${rtlEnabled}).png`, { element: page.locator('#container') }); + + // assert + }); + // TODO: .after() block removed +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/stickyColumns.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/stickyColumns.spec.ts new file mode 100644 index 000000000000..bc0ecc7b11b5 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/stickyColumns.spec.ts @@ -0,0 +1,51 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('FixedColumns', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + test('The simulated scrollbar should display correctly when there are sticky columns', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: getData(5, 25), + width: 984, + columnAutoWidth: true, + scrolling: { + useNative: false, + }, + customizeColumns: (columns) => { + columns[5].fixed = true; + columns[5].fixedPosition = 'left'; + columns[6].fixed = true; + columns[6].fixedPosition = 'left'; + + columns[8].fixed = true; + columns[8].fixedPosition = 'right'; + columns[9].fixed = true; + columns[9].fixedPosition = 'right'; + }, + }); + + // arrange + const scrollbarVerticalThumbTrack = page.locator('.dx-scrollbar-horizontal .dx-scrollable-scroll'); + + expect(await page.locator('.dx-datagrid').first().isVisible()).toBeTruthy(); + + await (scrollbarVerticalThumbTrack).hover(); + await testScreenshot(page, 'simulated_scrollbar_with_sticky_columns_1.png', { element: page.locator('#container') }); + + // act + await page.evaluate((opts) => ($('#container') as any).dxDataGrid('instance').getScrollable().scrollTo(opts), { x: 1500 }); + + await testScreenshot(page, 'simulated_scrollbar_with_sticky_columns_2.png', { element: page.locator('#container') }); + }); + // TODO: .after() block removed +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/withAdaptability.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/withAdaptability.spec.ts new file mode 100644 index 000000000000..f60a01ff9e7a --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/withAdaptability.spec.ts @@ -0,0 +1,47 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('Sticky columns - Adaptability', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + [false, true].forEach((rtlEnabled) => { + + test(`Sticky columns with adaptive detail row (rtlEnabled = ${rtlEnabled})`, async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + ...defaultConfig, + width: 800, + rtlEnabled, + customizeColumns(columns) { + columns.forEach((column, index) => { + if (index < 3) { + column.hidingPriority = index; + } + + column.width = 200; + }); + }, + columnHidingEnabled: true, + }); + + const scrollLeft = rtlEnabled ? -10000 : 10000; + + expect(await page.locator('.dx-datagrid').first().isVisible()).toBeTruthy(); + + await dataGrid.apiExpandAdaptiveDetailRow(1); + + await testScreenshot(page, `adaptability_sticky_columns_with_adaptive_detail_row_1_(rtlEnabled_=_${rtlEnabled}).png`, { element: page.locator('#container') }); + + await page.evaluate((opts) => ($('#container') as any).dxDataGrid('instance').getScrollable().scrollTo(opts), { x: scrollLeft }); + + await testScreenshot(page, `adaptability_sticky_columns_with_adaptive_detail_row_2_(rtlEnabled_=_${rtlEnabled}).png`, { element: page.locator('#container') }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/withBandColumns.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/withBandColumns.spec.ts new file mode 100644 index 000000000000..4a57dbc8203d --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/withBandColumns.spec.ts @@ -0,0 +1,52 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('Band sticky columns', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + [false, true].forEach((rtlEnabled) => { + // T1279722 + + test(`Headers and filter row should display correctly after scrolling to the max right position when there is a grouped column (rtl=${rtlEnabled})`, async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: [ + { + field0: 1, field1: 1, field2: 1, field3: 1, field4: 1, field5: 1, field6: 1, field7: 1, + }, + ], + keyExpr: 'field0', + width: 500, + columnWidth: 100, + columns: [{ + dataField: 'field0', + fixed: true, + fixedPosition: rtlEnabled ? 'right' : 'left', + }, { + caption: 'Band', + fixed: true, + fixedPosition: rtlEnabled ? 'right' : 'left', + columns: [{ + dataField: 'field1', + groupIndex: 0, + }, 'field2'], + }, 'field3', 'field4', 'field5', 'field6', 'field7'], + showBorders: true, + filterRow: { visible: true }, + rtlEnabled, + }); + + expect(await page.locator('.dx-datagrid').first().isVisible()).toBeTruthy(); + + await page.evaluate((opts) => ($('#container') as any).dxDataGrid('instance').getScrollable().scrollTo(opts), { x: rtlEnabled ? 0 : 10000 }); + await testScreenshot(page, `T1279722_band_sticky_columns-headers_with_filter_row_and_grouped_column_(rtl=${rtlEnabled}).png`, { element: page.locator('#container') }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/withDragAndDrop.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/withDragAndDrop.spec.ts new file mode 100644 index 000000000000..2c018ef16ded --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/withDragAndDrop.spec.ts @@ -0,0 +1,48 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('Sticky columns - Drag and Drop', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + test('Fixed columns should work when drag and drop rows are enabled', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: getData(10, 10), + keyExpr: 'field_0', + width: 500, + columnFixing: { + enabled: true, + }, + showColumnHeaders: true, + columnAutoWidth: true, + rowDragging: { + allowReordering: true, + dropFeedbackMode: 'push', + }, + customizeColumns(columns) { + columns[5].fixed = true; + columns[6].fixed = true; + + columns[8].fixed = true; + columns[8].fixedPosition = 'right'; + columns[9].fixed = true; + columns[9].fixedPosition = 'right'; + }, + }); + + // arrange, act + expect(await page.locator('.dx-datagrid').first().isVisible()).toBeTruthy(); + + await testScreenshot(page, 'datagrid_sticky_columns_with_drag_and_drop.png', { element: page.locator('#container') }); + + // assert + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/withEditing.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/withEditing.spec.ts new file mode 100644 index 000000000000..7e715b1d373f --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/withEditing.spec.ts @@ -0,0 +1,39 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('Sticky columns - Editing', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + test('The row edit mode: Edit row when there are sticky columns', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + ...defaultConfig, + editing: { + mode: 'row', + allowUpdating: true, + }, + scrolling: { + showScrollbar: 'never', + }, + }); + + expect(await page.locator('.dx-datagrid').first().isVisible()).toBeTruthy(); + + await dataGrid.apiEditRow(1); + await (page.locator('.dx-data-row').click().nth(1).locator('td').nth(1)); + + await testScreenshot(page, 'edit_row_with_sticky_columns_1.png', { element: page.locator('#container') }); + + await page.evaluate((opts) => ($('#container') as any).dxDataGrid('instance').getScrollable().scrollTo(opts), { x: 10000 }); + + await testScreenshot(page, 'edit_row_with_sticky_columns_2.png', { element: page.locator('#container') }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/withFilterRow.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/withFilterRow.spec.ts new file mode 100644 index 000000000000..5f1e33ae699e --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/withFilterRow.spec.ts @@ -0,0 +1,38 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('Sticky columns - Filter row', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + // visual: generic.light + // visual: material.blue.light + // visual: fluent.blue.light + + test('Filter row with sticky columns (generic.light theme)', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + ...defaultConfig, + filterRow: { + visible: true, + }, + }); + + expect(await page.locator('.dx-datagrid').first().isVisible()).toBeTruthy(); + + await (page.locator('.dx-header-row').click().getFilterRow().getFilterCell(1).element); + + await testScreenshot(page, 'filter_row_with_sticky_columns_1.png', { element: page.locator('#container') }); + + await page.evaluate((opts) => ($('#container') as any).dxDataGrid('instance').getScrollable().scrollTo(opts), { x: 10000 }); + + await testScreenshot(page, 'filter_row_with_sticky_columns_2.png', { element: page.locator('#container') }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/withGrouping.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/withGrouping.spec.ts new file mode 100644 index 000000000000..cc0b3b86f935 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/withGrouping.spec.ts @@ -0,0 +1,81 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('FixedColumns - Grouping', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + [false, true].forEach((rtlEnabled) => { + + test(`Sticky columns with grouping & summary (rtl=${rtlEnabled})`, async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + ...defaultConfig, + rtlEnabled, + customizeColumns(columns) { + columns[2].groupIndex = 0; + }, + summary: { + groupItems: [{ + column: 'OrderNumber', + summaryType: 'count', + displayFormat: '{0} orders', + }, { + column: 'City', + summaryType: 'max', + valueFormat: 'currency', + showInGroupFooter: false, + alignByColumn: true, + }, { + column: 'TotalAmount', + summaryType: 'max', + valueFormat: 'currency', + showInGroupFooter: false, + alignByColumn: true, + }, { + column: 'TotalAmount', + summaryType: 'sum', + valueFormat: 'currency', + displayFormat: 'Total: {0}', + showInGroupFooter: true, + }], + totalItems: [{ + column: 'OrderNumber', + summaryType: 'count', + displayFormat: '{0} orders', + }, { + column: 'SaleAmount', + summaryType: 'max', + valueFormat: 'currency', + }, { + column: 'TotalAmount', + summaryType: 'max', + valueFormat: 'currency', + }, { + column: 'TotalAmount', + summaryType: 'sum', + valueFormat: 'currency', + displayFormat: 'Total: {0}', + }], + }, + }); + + expect(await page.locator('.dx-datagrid').first().isVisible()).toBeTruthy(); + + await testScreenshot(page, `grouping-scroll-begin-rtl=${rtlEnabled}.png`, { element: page.locator('#container') }); + + await page.evaluate((opts) => ($('#container') as any).dxDataGrid('instance').getScrollable().scrollTo(opts), { x: rtlEnabled ? 500 : 100 }); + await testScreenshot(page, `grouping-scroll-center=${rtlEnabled}.png`, { element: page.locator('#container') }); + + await page.evaluate((opts) => ($('#container') as any).dxDataGrid('instance').getScrollable().scrollTo(opts), { x: rtlEnabled ? 0 : 10000 }); + await testScreenshot(page, `grouping-scroll-end=${rtlEnabled}.png`, { element: page.locator('#container') }); + }); + // TODO: .after() block removed +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/withKeyboardNavigation.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/withKeyboardNavigation.spec.ts new file mode 100644 index 000000000000..f9ca72140b94 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/withKeyboardNavigation.spec.ts @@ -0,0 +1,83 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('Fixed Columns - keyboard navigation', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + const navigateToNextCell = async (t, $headerCell) => { + // act + await t + .pressKey('tab'); + + // assert + await t + .expect($headerCell.isFocused) + .ok(); + }; + + const navigateToPrevCell = async (t, $headerCell) => { + // act + await t + .pressKey('shift+tab'); + + // assert + await t + .expect($headerCell.isFocused) + .ok(); + }; + + ); + + test('Headers navigation by Tab key when there are fixed columns', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + ...defaultConfig, + width: 600, + customizeColumns(columns) { + columns[4].width = 125; + columns[4].fixed = true; + columns[4].fixedPosition = 'sticky'; + }, + }); + + // arrange + const headers = page.locator('.dx-header-row'); + const headerRow = headers.getHeaderRow(0); + + expect(await page.locator('.dx-datagrid').first().isVisible()).toBeTruthy(); + + // act + await (headerRow.locator('td').nth(0).click().element); + + // assert + expect(await headerRow.locator('td').nth(0).isFocused); + await t.ok(); + + // act + await navigateToNextCell(t, headerRow.locator('td').nth(1)); + await navigateToNextCell(t, headerRow.locator('td').nth(2)); + await navigateToNextCell(t, headerRow.locator('td').nth(3)); + + await testScreenshot(page, 'fixed_columns_headers_navigation_by_tab_1.png', { element: page.locator('#container') }); + + // act + await navigateToNextCell(t, headerRow.locator('td').nth(4)); + await navigateToNextCell(t, headerRow.locator('td').nth(5)); + + await testScreenshot(page, 'fixed_columns_headers_navigation_by_tab_2.png', { element: page.locator('#container') }); + + // act + await navigateToNextCell(t, headerRow.locator('td').nth(6)); + + // assert + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/withMasterDetail.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/withMasterDetail.spec.ts new file mode 100644 index 000000000000..e0657f39fb5d --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/withMasterDetail.spec.ts @@ -0,0 +1,40 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('FixedColumns - MasterDetail', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + test('Sticky columns with master-detail', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + ...defaultConfig, + masterDetail: { + enabled: true, + template(container) { + $(container) + .text('Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.'); + }, + }, + }); + + expect(await page.locator('.dx-datagrid').first().isVisible()).toBeTruthy(); + + await dataGrid.apiExpandRow(1); + + await testScreenshot(page, 'masterdetail-scroll-begin.png', { element: page.locator('#container') }); + + await page.evaluate((opts) => ($('#container') as any).dxDataGrid('instance').getScrollable().scrollTo(opts), { x: 100 }); + await testScreenshot(page, 'masterdetail-scroll-center.png', { element: page.locator('#container') }); + + await page.evaluate((opts) => ($('#container') as any).dxDataGrid('instance').getScrollable().scrollTo(opts), { x: 10000 }); + await testScreenshot(page, 'masterdetail-scroll-end.png', { element: page.locator('#container') }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/withMultiRow.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/withMultiRow.spec.ts new file mode 100644 index 000000000000..d674332ceaf2 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/withMultiRow.spec.ts @@ -0,0 +1,45 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('Sticky columns - Multi Row Header Columns', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + // visual: generic.light + // visual: material.blue.light + // visual: fluent.blue.light + + test('The multi row header columns should have vertical borders when a column is fixed (generic.light theme) (T1282595)', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + ...defaultConfig, + columns: [ + { + dataField: 'ID', + fixed: true, + }, + { + caption: 'Order', + columns: [ + 'OrderNumber', + 'OrderDate', + ], + }, + 'SaleAmount', + 'Terms', + ], + showBorders: true, + }); + + expect(await page.locator('.dx-datagrid').first().isVisible()).toBeTruthy(); + + await testScreenshot(page, 'multi_row_header_columns.png', { element: page.locator('#container') }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/withRowSelection.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/withRowSelection.spec.ts new file mode 100644 index 000000000000..6cae93687e72 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/withRowSelection.spec.ts @@ -0,0 +1,37 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('Sticky columns - Row Selection', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + // visual: generic.light + // visual: material.blue.light + // visual: fluent.blue.light + + test('The selected row should be displayed correctly when there are sticky columns (generic.light theme)', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + ...defaultConfig, + selection: { + mode: 'multiple', + }, + selectedRowKeys: [4], + }); + + expect(await page.locator('.dx-datagrid').first().isVisible()).toBeTruthy(); + + await testScreenshot(page, 'row_selection_with_sticky_columns_1.png', { element: page.locator('#container') }); + + await page.evaluate((opts) => ($('#container') as any).dxDataGrid('instance').getScrollable().scrollTo(opts), { x: 10000 }); + + await testScreenshot(page, 'row_selection_with_sticky_columns_2.png', { element: page.locator('#container') }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/withVirtualColumns.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/withVirtualColumns.spec.ts new file mode 100644 index 000000000000..f251b8f03d20 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/withVirtualColumns.spec.ts @@ -0,0 +1,45 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('Sticky columns - Virtual Columns', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + test('Fixed columns with sticky position should not work', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: getData(10, 100), + columnWidth: 100, + showColumnLines: true, + scrolling: { + columnRenderingMode: 'virtual', + }, + customizeColumns(columns) { + columns[0].fixed = true; + columns[1].fixed = true; + + columns[3].fixed = true; + columns[3].fixedPosition = 'sticky'; + }, + }); + + // arrange, act + expect(await page.locator('.dx-datagrid').first().isVisible()).toBeTruthy(); + + await testScreenshot(page, 'virtual_columns_with_sticky_columns_1.png', { element: page.locator('#container') }); + + // act + await page.evaluate((opts) => ($('#container') as any).dxDataGrid('instance').getScrollable().scrollTo(opts), { x: 150 }); + + await testScreenshot(page, 'virtual_columns_with_sticky_columns_2.png', { element: page.locator('#container') }); + + // assert + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/withVirtualScrolling.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/withVirtualScrolling.spec.ts new file mode 100644 index 000000000000..d8f590c89d0e --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/withVirtualScrolling.spec.ts @@ -0,0 +1,57 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('Sticky columns - Virtual Scrolling', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + test('Fixed columns should display correctly when scrolling vertically quickly', async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: getData(400, 15), + height: 700, + columnWidth: 100, + showColumnLines: true, + scrolling: { + mode: 'virtual', + // @ts-expect-error private option + updateTimeout: 3000, + }, + customizeColumns(columns) { + columns[0].fixed = true; + + columns[1].fixed = true; + columns[1].fixedPosition = 'right'; + columns[2].fixed = true; + columns[2].fixedPosition = 'right'; + }, + }); + + // arrange + expect(await page.locator('.dx-datagrid').first().isVisible()).toBeTruthy(); + + // act + await page.evaluate((opts) => ($('#container') as any).dxDataGrid('instance').getScrollable().scrollTo(opts), { y: 500 }); + await page.waitForTimeout(100); + await page.evaluate((opts) => ($('#container') as any).dxDataGrid('instance').getScrollable().scrollTo(opts), { y: 1000 }); + await page.waitForTimeout(100); + await page.evaluate((opts) => ($('#container') as any).dxDataGrid('instance').getScrollable().scrollTo(opts), { y: 1500 }); + await page.waitForTimeout(100); + + await testScreenshot(page, 'fixed_columns_with_virtual_scrolling_1.png', { element: page.locator('#container') }); + + // waiting for size update + await page.waitForTimeout(3000); + + await testScreenshot(page, 'fixed_columns_with_virtual_scrolling_2.png', { element: page.locator('#container') }); + + // assert + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/fixed/bandColumnFirstCases.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/fixed/bandColumnFirstCases.spec.ts new file mode 100644 index 000000000000..aa6d2e6af163 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/fixed/bandColumnFirstCases.spec.ts @@ -0,0 +1,74 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('FixedColumns', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + borderConfigs.forEach(({ showColumnLines, showBorders }) => { + [true, false].forEach((rtlEnabled) => { + + test(`Band sticky columns: left and right positions (showColumnLines = ${showColumnLines}, showBorders = ${showBorders})`, async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: getData(5, 25), + width: 984, + showColumnLines, + showBorders, + rtlEnabled, + columnAutoWidth: true, + customizeColumns: (columns) => { + columns.push({ + caption: 'Band column 1', + fixed: true, + fixedPosition: 'left', + columns: [{ + caption: 'Nested band column 1', + columns: [ + { dataField: 'field_11', name: 'child_1' }, + { dataField: 'field_12', name: 'child_2' }, + ], + }, { dataField: 'field_13', name: 'child_3' }, { + caption: 'Nested band column 2', + columns: [ + { dataField: 'field_14', name: 'child_4' }, + { dataField: 'field_15', name: 'child_5' }, + ], + }], + }, { + caption: 'Band column 2', + fixed: true, + fixedPosition: 'right', + columns: [ + { dataField: 'field_16', name: 'child_6' }, + { + caption: 'Nested band column 3', + columns: [ + { dataField: 'field_17', name: 'child_7' }, + { dataField: 'field_18', name: 'child_8' }, + ], + }, + { dataField: 'field_19', name: 'child_9' }, + ], + }); + }, + }); + + // arrange + expect(await page.locator('.dx-datagrid').first().isVisible()).toBeTruthy(); + + await testScreenshot(page, `band-columns-1-(case-1)(cLines_=_${showColumnLines}_borders_=_${showBorders}_rtl_=_${rtlEnabled}).png`, { element: page.locator('#container') }); + + // act + await page.evaluate((opts) => ($('#container') as any).dxDataGrid('instance').getScrollable().scrollTo(opts), { x: rtlEnabled ? 0 : 10000 }); + + await testScreenshot(page, `band-columns-2-(case-1)(cLines_=_${showColumnLines}_borders_=_${showBorders}_rtl_=_${rtlEnabled}).png`, { element: page.locator('#container') }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/fixed/bandColumnSecondCases.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/fixed/bandColumnSecondCases.spec.ts new file mode 100644 index 000000000000..b16ab13c971b --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/fixed/bandColumnSecondCases.spec.ts @@ -0,0 +1,65 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('FixedColumns', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + borderConfigs.forEach(({ showColumnLines, showBorders }) => { + [true, false].forEach((rtlEnabled) => { + + test(`Sticky column + Band sticky column + Sticky column: sticky positions (showColumnLines = ${showColumnLines}, showBorders = ${showBorders})`, async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: getData(5, 25), + width: 984, + showColumnLines, + showBorders, + rtlEnabled, + columnAutoWidth: true, + customizeColumns: (columns) => { + columns[1].fixed = true; + columns[1].fixedPosition = 'sticky'; + + columns.splice(2, 0, { + caption: 'Band column 1', + fixed: true, + fixedPosition: 'sticky', + columns: [{ + caption: 'Nested band column 1', + columns: [ + { dataField: 'field_11', name: 'child_1' }, + { dataField: 'field_12', name: 'child_2' }, + ], + }, { dataField: 'field_13', name: 'child_3' }, { + caption: 'Nested band column 2', + columns: [ + { dataField: 'field_14', name: 'child_4' }, + { dataField: 'field_15', name: 'child_5' }, + ], + }], + }); + + columns[3].fixed = true; + columns[3].fixedPosition = 'sticky'; + }, + }); + + // arrange + expect(await page.locator('.dx-datagrid').first().isVisible()).toBeTruthy(); + + await testScreenshot(page, `band-columns-1-(case-8)(cLines_=_${showColumnLines}_borders_=_${showBorders}_rtl_=_${rtlEnabled}).png`, { element: page.locator('#container') }); + + // act + await page.evaluate((opts) => ($('#container') as any).dxDataGrid('instance').getScrollable().scrollTo(opts), { x: rtlEnabled ? 0 : 10000 }); + + await testScreenshot(page, `band-columns-2-(case-8)(cLines_=_${showColumnLines}_borders_=_${showBorders}_rtl_=_${rtlEnabled}).png`, { element: page.locator('#container') }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/fixed/positions.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/fixed/positions.spec.ts new file mode 100644 index 000000000000..9658506cb8b6 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/fixed/positions.spec.ts @@ -0,0 +1,45 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('FixedColumns', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + borderConfigs.forEach(({ showColumnLines, showBorders }) => { + [true, false].forEach((rtlEnabled) => { + + test(`Sticky columns with left position (showColumnLines = ${showColumnLines}, showBorders = ${showBorders}, rtlEnabled = ${rtlEnabled})`, async ({ page }) => { + await createWidget(page, 'dxDataGrid', { + dataSource: getData(5, 25), + width: 884, + showColumnLines, + showBorders, + rtlEnabled, + columnAutoWidth: true, + customizeColumns: (columns) => { + columns[5].fixed = true; + columns[5].fixedPosition = 'left'; + columns[6].fixed = true; + columns[6].fixedPosition = 'left'; + }, + }); + + // arrange + expect(await page.locator('.dx-datagrid').first().isVisible()).toBeTruthy(); + + await testScreenshot(page, `left-position-1(cLines_=_${showColumnLines}_borders_=_${showBorders}_rtl_=_${rtlEnabled}).png`, { element: page.locator('#container') }); + + // act + await page.evaluate((opts) => ($('#container') as any).dxDataGrid('instance').getScrollable().scrollTo(opts), { x: rtlEnabled ? 0 : 10000 }); + + await testScreenshot(page, `left-position-2(cLines_=_${showColumnLines}_borders_=_${showBorders}_rtl_=_${rtlEnabled}).png`, { element: page.locator('#container') }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/editors/autocomplete/common.spec.ts b/e2e/testcafe-devextreme/playwright-tests/editors/autocomplete/common.spec.ts new file mode 100644 index 000000000000..18c7e111b8ce --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/editors/autocomplete/common.spec.ts @@ -0,0 +1,34 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, appendElementTo, setStyleAttribute } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('Autocomplete_placeholder', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('Placeholder is visible after items option change when value is not chosen (T1099804)', async ({ page }) => { + + await appendElementTo(page, '#container', 'div', 'autocomplete'); + await setStyleAttribute(page, '#container', 'box-sizing: border-box; width: 300px; height: 100px; padding: 8px;'); + + await createWidget(page, 'dxAutocomplete', { + width: '100%', + placeholder: 'Choose a value', + }, '#autocomplete'); + + const autocomplete = page.locator('#autocomplete'); + + await autocomplete.option('items', [1, 2, 3]); + + await testScreenshot(page, 'Autocomplete placeholder if value is not choosen.png', { element: '#container' }); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/editors/calendar/common.spec.ts b/e2e/testcafe-devextreme/playwright-tests/editors/calendar/common.spec.ts new file mode 100644 index 000000000000..a1d463add5da --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/editors/calendar/common.spec.ts @@ -0,0 +1,364 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, appendElementTo, setStyleAttribute, setClassAttribute } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('Calendar', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + const STATE_HOVER_CLASS = 'dx-state-hover'; + const STATE_ACTIVE_CLASS = 'dx-state-active'; + const CALENDAR_CELL_CLASS = 'dx-calendar-cell'; + const CALENDAR_TODAY_CLASS = 'dx-calendar-today'; + const CALENDAR_SELECTED_DATE_CLASS = 'dx-calendar-selected-date'; + const CALENDAR_EMPTY_CELL_CLASS = 'dx-calendar-empty-cell'; + const CALENDAR_OTHER_VIEW_CLASS = 'dx-calendar-other-view'; + const CALENDAR_CONTOURED_DATE_CLASS = 'dx-calendar-contoured-date'; + + const GESTURE_COVER_CLASS = 'dx-gesture-cover'; + + test('Caption button text should be ellipsis when width is limit', async ({ page }) => { + await createWidget(page, 'dxCalendar', { + width: 150, + value: new Date(2021, 9, 17), + }); + + await testScreenshot(page, 'Calendar with limit width.png', { element: '#container' }); + + }); + + test('Grabbing cursor should be shown during swipe', async ({ page }) => { + await createWidget(page, 'dxCalendar', { + value: new Date(2021, 9, 17), + }); + + const calendar = page.locator('#container'); + + await calendar.showGestureCover(); + + const gestureCover = page.locator(`.${GESTURE_COVER_CLASS}`); + + await page.expect(gestureCover.getStyleProperty('cursor')) + .eql('auto'); + + await calendar.swipeStart(); + + await page.expect(gestureCover.getStyleProperty('cursor')) + .eql('grabbing'); + + await calendar.swipe(0.4); + + await page.expect(gestureCover.getStyleProperty('cursor')) + .eql('grabbing'); + + await calendar.swipeEnd(); + + await page.expect(gestureCover.getStyleProperty('cursor')) + .eql('auto'); + + }); + + test('Cells on month view should have hover state class after hover when zoomLevel has been changed from "year" to "month" by click on cell', async ({ page }) => { + await createWidget(page, 'dxCalendar', { + zoomLevel: 'year', + value: new Date(2021, 9, 17), + }); + + const calendar = page.locator('#container'); + + await page.click(calendar.getView().getMonthCellByDate(new Date(2021, 9, 17))); + + const targetCell = calendar.getView().getCellByDate(new Date(2021, 9, 19)); + await targetCell.hover() + .expect(targetCell.hasClass(STATE_HOVER_CLASS)) + .eql(true); + + }); + + test('Calendar with showWeekNumbers rendered correct', async ({ page }) => { + await createWidget(page, 'dxCalendar', { + value: new Date(2022, 0, 1), + showWeekNumbers: true, + }); + + await testScreenshot(page, 'Calendar with showWeekNumbers.png', { element: '#container' }); + + }); + + test('Calendar with showWeekNumbers rendered correct for last week of year value', async ({ page }) => { + await createWidget(page, 'dxCalendar', { + value: new Date(2021, 11, 31), + showWeekNumbers: true, + weekNumberRule: 'firstDay', + }); + + await testScreenshot(page, 'Calendar with showWeekNumbers last week.png', { element: '#container' }); + + }); + + test('Calendar with showWeekNumbers rendered correct with rtlEnabled', async ({ page }) => { + await createWidget(page, 'dxCalendar', { + value: new Date(2022, 0, 1), + showWeekNumbers: true, + rtlEnabled: true, + }); + + await testScreenshot(page, 'Calendar with showWeekNumbers rtl=true.png', { element: '#container' }); + + }); + + test('Calendar with showWeekNumbers rendered correct with cellTemplate', async ({ page }) => { + await createWidget(page, 'dxCalendar', { + value: new Date(2022, 0, 1), + showWeekNumbers: true, + cellTemplate(cellData, cellIndex) { + const italic = $('').css('font-style', 'italic') + .text(cellData.text); + const bold = $('').css('font-weight', '900') + .text(cellData.text); + return cellIndex === -1 ? bold : italic; + }, + }); + + await testScreenshot(page, 'Calendar with showWeekNumbers and cell template.png', { element: '#container' }); + + }); + + ['multiple', 'range'].forEach((selectionMode) => { + test(`Calendar with ${selectionMode} selectionMode rendered correct`, async ({ page }) => { + await createWidget(page, 'dxCalendar', { + value: [new Date(2023, 0, 5), new Date(2023, 0, 17), new Date(2023, 1, 2)], + selectionMode, + }); + + await testScreenshot(page, `Calendar with ${selectionMode} selectionMode.png`, { element: '#container' }); + + }); + + test(`Week cell click selection (selectionMode=${selectionMode})`, async ({ page }) => { + await createWidget(page, 'dxCalendar', { + value: [new Date(2023, 0, 5), new Date(2023, 0, 17), new Date(2023, 1, 2)], + selectionMode, + showWeekNumbers: true, + firstDayOfWeek: 1, + disabledDates: ({ date }) => { + const day = date.getDay(); + return day === 1 || day === 4 || day === 0; + }, + }); + + const calendar = page.locator('#container'); + + await page.click(calendar.getView().getWeekNumberCellByIndex(3)); + + await testScreenshot(page, `Week cell click selection (selectionMode=${selectionMode}).png`, { element: '#container' }); + + }); + }); + + test('Calendar with multiview rendered correct', async ({ page }) => { + await createWidget(page, 'dxCalendar', { + value: [new Date(2023, 0, 5), new Date(2023, 1, 14)], + selectionMode: 'range', + viewsCount: 2, + }); + + await testScreenshot(page, 'Calendar with multiview.png', { element: '#container' }); + + }); + + ['month', 'year', 'decade', 'century'].forEach((zoomLevel) => { + test(`Calendar ${zoomLevel} view rendered correct`, async ({ page }) => { + + await setStyleAttribute(page, '#container', 'width: 400px; height: 400px;'); + await appendElementTo(page, '#container', 'div', 'calendar'); + + await createWidget(page, 'dxCalendar', { + value: new Date(2021, 9, 17), + zoomLevel, + _todayDate: () => new Date(2023, 9, 17), + }, '#calendar'); + + + await testScreenshot(page, `Calendar ${zoomLevel} view.png`, { element: '#container' }); + + }); + + test(`Calendar ${zoomLevel} view rendered correct in RTL`, async ({ page }) => { + + await setStyleAttribute(page, '#container', 'width: 400px; height: 400px;'); + await appendElementTo(page, '#container', 'div', 'calendar'); + + await createWidget(page, 'dxCalendar', { + value: new Date(2021, 9, 17), + zoomLevel, + rtlEnabled: true, + _todayDate: () => new Date(2023, 9, 17), + }, '#calendar'); + + + await testScreenshot(page, `Calendar ${zoomLevel} view in RTL mode.png`, { element: '#container' }); + + }); + + test(`Calendar ${zoomLevel} view with today button rendered correct`, async ({ page }) => { + + await setStyleAttribute(page, '#container', 'width: 600px; height: 800px;'); + await appendElementTo(page, '#container', 'div', 'calendar'); + + await createWidget(page, 'dxCalendar', { + value: new Date(2021, 9, 17), + width: 450, + height: 450, + zoomLevel, + showTodayButton: true, + _todayDate: () => new Date(2023, 9, 17), + }, '#calendar'); + + + await testScreenshot(page, `Calendar ${zoomLevel} view with today button.png`, { element: '#container' }); + + }); + }); + + test('Calendar with disabled dates rendered correct', async ({ page }) => { + await createWidget(page, 'dxCalendar', { + value: new Date(2021, 9, 17), + showTodayButton: true, + showWeekNumbers: true, + min: new Date(2021, 9, 10), + disabledDates: [new Date(2021, 9, 18)], + }); + + await testScreenshot(page, 'Calendar with disabled dates.png', { element: '#container' }); + + }); + + [CALENDAR_CELL_CLASS, CALENDAR_TODAY_CLASS].forEach((cellClass) => { + const testName = `Calendar ${cellClass === CALENDAR_TODAY_CLASS ? 'today ' : ''}cell styles`; + + test(testName, async ({ page }) => { + + await setStyleAttribute(page, '#container', 'width: 600px; height: 800px;'); + await appendElementTo(page, '#container', 'div', 'calendar'); + + await createWidget(page, 'dxCalendar', { + currentDate: new Date(2021, 9, 15), + }, '#calendar'); + + + const calendar = page.locator('#calendar'); + + const startCellDate = new Date(2021, 9, 3); + const view = calendar.getView(); + + let cellOffset = 0; + + for (const cellTypeClass of [ + cellClass, + `${cellClass} ${CALENDAR_OTHER_VIEW_CLASS}`, + `${cellClass} ${CALENDAR_OTHER_VIEW_CLASS} ${CALENDAR_SELECTED_DATE_CLASS}`, + `${cellClass} ${CALENDAR_OTHER_VIEW_CLASS} ${CALENDAR_EMPTY_CELL_CLASS}`, + `${cellClass} ${CALENDAR_EMPTY_CELL_CLASS}`, + `${cellClass} ${CALENDAR_EMPTY_CELL_CLASS} ${CALENDAR_SELECTED_DATE_CLASS}`, + `${cellClass} ${CALENDAR_CONTOURED_DATE_CLASS}`, + `${cellClass} ${CALENDAR_CONTOURED_DATE_CLASS} ${CALENDAR_SELECTED_DATE_CLASS}`, + `${cellClass} ${CALENDAR_SELECTED_DATE_CLASS}`, + ]) { + for (const stateClass of [ + '', + STATE_HOVER_CLASS, + STATE_ACTIVE_CLASS, + ]) { + const cellClasses = `${cellTypeClass} ${stateClass}`; + + await setClassAttribute(page, view.getCellByOffset(startCellDate, cellOffset), cellClasses); + + const cellNumber = startCellDate.getDate() + cellOffset; + const cellId = `cell-${cellNumber}`; + await appendElementTo(page, '#container', 'div', cellId); + + await page.evaluate(() => { + $(`#${cellId}`).text(`${cellNumber} - ${cellClasses}`); + }); + + cellOffset += 1; + } + } + + await testScreenshot(page, `${testName}.png`, { element: '#container' }); + + }); + }); + + ['year', 'decade', 'century'].forEach((zoomLevel) => { + const testName = `Calendar ${zoomLevel} view cell styles`; + + test(testName, async ({ page }) => { + + await setStyleAttribute(page, '#container', 'width: 600px; height: 800px;'); + await appendElementTo(page, '#container', 'div', 'calendar'); + + await createWidget(page, 'dxCalendar', { + currentDate: new Date(2021, 9, 17), + zoomLevel, + _todayDate: () => new Date(2023, 9, 17), + }, '#calendar'); + + + const calendar = page.locator('#calendar'); + + const startCellDate = new Date(2021, 9, 3); + const view = calendar.getView(); + + let cellOffset = 0; + + for (const cellTypeClass of [ + STATE_HOVER_CLASS, + STATE_ACTIVE_CLASS, + CALENDAR_TODAY_CLASS, + CALENDAR_OTHER_VIEW_CLASS, + CALENDAR_EMPTY_CELL_CLASS, + CALENDAR_CONTOURED_DATE_CLASS, + CALENDAR_SELECTED_DATE_CLASS, + `${CALENDAR_CONTOURED_DATE_CLASS} ${CALENDAR_SELECTED_DATE_CLASS}`, + ]) { + const cellClasses = `${cellTypeClass}`; + + await setClassAttribute(page, view.getCellByIndex(cellOffset), cellClasses); + + const cellNumber = startCellDate.getDate() + cellOffset; + const cellId = `cell-${cellNumber}`; + await appendElementTo(page, '#container', 'div', cellId); + + await page.evaluate(() => { + $(`#${cellId}`).text(`${cellNumber} - ${cellClasses}`); + }); + + cellOffset += 1; + } + + await testScreenshot(page, `${testName}.png`, { element: '#container' }); + + }); + + test(`Calendar with range selectionMode rendered correct (maxZoomLevel=${zoomLevel})`, async ({ page }) => { + await createWidget(page, 'dxCalendar', { + value: [new Date(1023, 0, 5), new Date(1023, 0, 17), new Date(1099, 1, 2)], + selectionMode: 'range', + maxZoomLevel: zoomLevel, + }); + + await testScreenshot(page, `Calendar with range selection (maxZoomLevel=${zoomLevel}).png`, { element: '#container' }); + + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/editors/calendar/keyboard.spec.ts b/e2e/testcafe-devextreme/playwright-tests/editors/calendar/keyboard.spec.ts new file mode 100644 index 000000000000..86d0bfd9db07 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/editors/calendar/keyboard.spec.ts @@ -0,0 +1,147 @@ +import { test, expect } from '@playwright/test'; +import { createWidget } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('Calendar keyboard navigation', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + const CALENDAR_SELECTED_DATE_CLASS = 'dx-calendar-selected-date'; + const CALENDAR_CONTOURED_DATE_CLASS = 'dx-calendar-contoured-date'; + + test('Tab navigation order prevButton-caption-nextButton-viewdWrapper-todayButton', async ({ page }) => { + await createWidget(page, 'dxCalendar', { + value: new Date(2021, 9, 17), + showTodayButton: true, + }); + + const calendar = page.locator('#container'); + + await page.locator('body').click() + .pressKey('tab'); + + await page.expect(calendar.getNavigatorPrevButton().isFocused) + .ok() + .pressKey('tab') + .expect(calendar.getNavigatorCaption().isFocused) + .ok() + .pressKey('tab') + .expect(calendar.getNavigatorNextButton().isFocused) + .ok() + .pressKey('tab'); + + const cell = calendar.getView().getCellByDate(new Date(2021, 9, 17)); + await page.expect(cell.hasClass(CALENDAR_CONTOURED_DATE_CLASS)) + .ok() + .expect(cell.hasClass(CALENDAR_SELECTED_DATE_CLASS)) + .ok(); + + await page.keyboard.press('Tab'); + + await page.expect(calendar.getTodayButton().isFocused) + .ok(); + + await page.keyboard.press('Enter'); + + const currentDate = await calendar.option('value') as Date; + const today = new Date(); + + currentDate.setHours(0, 0, 0, 0); + today.setHours(0, 0, 0, 0); + + expect(currentDate).toBe(today); + + const todayCell = calendar.getView().getCellByDate(today); + + await page.expect(todayCell.hasClass(CALENDAR_SELECTED_DATE_CLASS)) + .ok(); + + }); + + test('focusin and focusout event handlers should not be called on tab navigate inside calendar', async ({ page }) => { + + await page.evaluate(() => { + (window as any).onFocusInCounter = 0; + (window as any).onFocusOutCounter = 0; + }); + + await createWidget(page, 'dxCalendar', { + value: new Date(2021, 9, 17), + showTodayButton: true, + onFocusIn() { + ((window as any).onFocusInCounter as number) += 1; + }, + onFocusOut() { + ((window as any).onFocusOutCounter as number) += 1; + }, + }); + + const calendar = page.locator('#container'); + + await page.locator('body').click() + .pressKey('tab'); + + await page.expect(calendar.getNavigatorPrevButton().isFocused) + .ok() + .expect(ClientFunction(() => (window as any).onFocusInCounter)()) + .eql(1) + .expect(ClientFunction(() => (window as any).onFocusOutCounter)()) + .eql(0); + + await page.keyboard.press('Tab'); + + await page.expect(calendar.getNavigatorCaption().isFocused) + .ok() + .expect(ClientFunction(() => (window as any).onFocusInCounter)()) + .eql(1) + .expect(ClientFunction(() => (window as any).onFocusOutCounter)()) + .eql(0); + + await page.keyboard.press('Tab'); + + await page.expect(calendar.getNavigatorNextButton().isFocused) + .ok() + .expect(ClientFunction(() => (window as any).onFocusInCounter)()) + .eql(1) + .expect(ClientFunction(() => (window as any).onFocusOutCounter)()) + .eql(0); + + await page.keyboard.press('Tab'); + + const cell = calendar.getView().getCellByDate(new Date(2021, 9, 17)); + await page.expect(cell.hasClass(CALENDAR_CONTOURED_DATE_CLASS)) + .ok() + .expect(cell.hasClass(CALENDAR_SELECTED_DATE_CLASS)) + .ok() + .expect(ClientFunction(() => (window as any).onFocusInCounter)()) + .eql(1) + .expect(ClientFunction(() => (window as any).onFocusOutCounter)()) + .eql(0); + + await page.keyboard.press('Tab'); + + await page.expect(calendar.getTodayButton().isFocused) + .ok() + .expect(ClientFunction(() => (window as any).onFocusInCounter)()) + .eql(1) + .expect(ClientFunction(() => (window as any).onFocusOutCounter)()) + .eql(0); + + await page.keyboard.press('Tab'); + + expect(calendar.isFocused).toBeFalsy() + .expect(ClientFunction(() => (window as any).onFocusInCounter)()) + .eql(1) + .expect(ClientFunction(() => (window as any).onFocusOutCounter)()) + .eql(1); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/editors/chat/alertList.spec.ts b/e2e/testcafe-devextreme/playwright-tests/editors/chat/alertList.spec.ts new file mode 100644 index 000000000000..55ed67b5077e --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/editors/chat/alertList.spec.ts @@ -0,0 +1,79 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('ChatAlertList', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test.clientScripts([ + { module: 'mockdate' }, + { content: 'window.MockDate = MockDate;' }, + ])('Alertlist appearance', async ({ page }) => { + const chat = page.locator('#container'); + + await testScreenshot(page, 'Alertlist with one error.png', { element: '#container' }); + + await chat.option('alerts', [ + { id: 1, message: 'Error Message 1. Error Description...' }, + { id: 2, message: 'Error Message 2. Message was not sent' }, + { id: 3, message: 'Error Message 3. An unexpected issue occurred while processing your request. Please check your internet connection or contact support for further assistance.' }, + ]); + + await testScreenshot(page, 'Alertlist with long text in error.png', { + element: '#container', + }); + + await chat.option('rtlEnabled', true); + + await testScreenshot(page, 'Alertlist appearance in RTL mode.png', { element: '#container' }); + }).before(async () => { + await page.evaluate(() => { + (window as any).MockDate.set('2024/10/18'); + }); + + const userFirst = createUser(1, 'First'); + const userSecond = createUser(2, 'Second'); + const msInDay = 86400000; + const today = new Date('2024/10/18').setHours(7, 22, 0, 0); + const yesterday = today - msInDay; + + const items = [{ + timestamp: yesterday, + author: userSecond, + text: 'Message text 1', + }, { + timestamp: yesterday, + author: userSecond, + text: 'Message text 2', + }, { + timestamp: today, + author: userFirst, + text: 'Message text 3', + }, { + timestamp: today, + author: userFirst, + text: 'Message text 4', + }]; + + await createWidget(page, 'dxChat', { + items, + user: userFirst, + width: 400, + height: 600, + alerts: [{ id: 1, message: 'Error Message 1. Error Description...' }], + }); + }).after(async () => { + await page.evaluate(() => { + (window as any).MockDate.reset(); + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/editors/chat/avatar.spec.ts b/e2e/testcafe-devextreme/playwright-tests/editors/chat/avatar.spec.ts new file mode 100644 index 000000000000..f372de003e9f --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/editors/chat/avatar.spec.ts @@ -0,0 +1,68 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, appendElementTo } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('ChatAvatar', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('Chat: avatar', async ({ page }) => { + + await appendElementTo(page, '#container', 'div', 'chat'); + + const userFirst = createUser(1, 'First User'); + const userSecond = createUser(2, 'Second User'); + + const items = generateMessages(2, userFirst, userSecond, false, false, 2); + + await createWidget(page, 'dxChat', { + width: 400, + height: 600, + user: userSecond, + items, + }, '#chat'); + + await testScreenshot(page, 'Avatar with two word initials.png', { element: '#chat' }); + + const chat = page.locator('#chat'); + + const userFirst = createUser(1, 'First', avatarUrl); + const userSecond = createUser(2, 'Second', avatarUrl); + + const items = generateMessages(2, userFirst, userSecond, false, false, 2); + + await chat.option('items', items); + + await testScreenshot(page, 'Avatar with image.png', { element: '#chat' }); + + }); + + test('Chat: showAvatar set to false', async ({ page }) => { + + await appendElementTo(page, '#container', 'div', 'chat'); + + const userFirst = createUser(1, 'First User'); + const userSecond = createUser(2, 'Second User'); + + const items = generateMessages(2, userFirst, userSecond, false, false, 2); + + await createWidget(page, 'dxChat', { + width: 400, + height: 600, + user: userSecond, + items, + showAvatar: false, + }, '#chat'); + + await testScreenshot(page, 'Avatar with showAvatar set to false.png', { element: '#chat' }); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/editors/chat/confirmationPopup.spec.ts b/e2e/testcafe-devextreme/playwright-tests/editors/chat/confirmationPopup.spec.ts new file mode 100644 index 000000000000..dc140419bcfc --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/editors/chat/confirmationPopup.spec.ts @@ -0,0 +1,51 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('ChatConfirmationPopup', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + , + + test('Chat: confirmation popup', async ({ page }) => { + + const userFirst = createUser(1, 'First'); + const userSecond = createUser(2, 'Second'); + + const items = [ + { author: userFirst, text: 'AAA' }, + { author: userFirst, text: 'BBB' }, + { author: userSecond, text: 'CCC' }, + ]; + + await createWidget(page, 'dxChat', { + items, + editing: { + allowDeleting: true, + }, + user: userSecond, + width: 400, + height: 600, + showDayHeaders: false, + rtlEnabled: true, + }); + + const chat = page.locator('#container'); + + await rightClick(chat.getMessage(2)).pressKey('down').pressKey('enter'); + + await testScreenshot(page, 'Confirmation popup is shown.png', { + element: '#container', + }); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/editors/chat/messageBox.spec.ts b/e2e/testcafe-devextreme/playwright-tests/editors/chat/messageBox.spec.ts new file mode 100644 index 000000000000..103b59da94fe --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/editors/chat/messageBox.spec.ts @@ -0,0 +1,107 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, appendElementTo } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('ChatMessageBox', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('Chat: messagebox', async ({ page }) => { + + await appendElementTo(page, '#container', 'div', 'chat'); + + await createWidget(page, 'dxChat', { + width: 400, + height: 600, + }, '#chat'); + + const chat = page.locator('#chat'); + + const shortText = getShortText(); + const longText = getLongText(false, 5); + + await chat.focus(); + await testScreenshot(page, 'Messagebox when chat has focus.png', { element: '#chat' }); + + await typeText(chat.getInput(), shortText); + await testScreenshot(page, 'Messagebox when input contains short text.png', { element: '#chat' }); + + await typeText(chat.getInput(), longText); + await testScreenshot(page, 'Messagebox when input contains long text.png', { element: '#chat' }); + + await page.keyboard.press('Tab'); + await testScreenshot(page, 'Messagebox when send button has focus.png', { element: '#chat' }); + + }); + + test('Chat: messagebox with editing preview', async ({ page }) => { + + await appendElementTo(page, '#container', 'div', 'chat'); + + const userFirst = createUser(1, 'First'); + const userSecond = createUser(2, 'Second'); + + const items = [{ + author: userFirst, + text: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.', + }, { + author: userSecond, + text: 'Short message', + }]; + + await createWidget(page, 'dxChat', { + items, + user: userFirst, + editing: { + allowUpdating: true, + }, + width: 400, + height: 600, + }, '#chat'); + + const chat = page.locator('#chat'); + + await rightClick(chat.getMessage(0)); + await click(chat.getContextMenuItem(0)); + + await testScreenshot(page, 'Messagebox with editing preview.png', { + element: '#chat', + }); + + }); + + test('Chat: messagebox with attachments and informer', async ({ page }) => { + + await appendElementTo(page, '#container', 'div', 'chat'); + + await createWidget(page, 'dxChat', { + width: 812, + height: 600, + }, '#chat'); + + const chat = page.locator('#chat'); + + await typeText(chat.getInput(), getLongText(false, 4)); + await chat.option({ + fileUploaderOptions: { + value: [ + ...attachments, + ...attachments, + ...attachments, + ], + }, + }); + + await chat.focus(); + await testScreenshot(page, 'Messagebox with attachments and informer.png', { element: '#chat' }); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/editors/chat/messageBubble.spec.ts b/e2e/testcafe-devextreme/playwright-tests/editors/chat/messageBubble.spec.ts new file mode 100644 index 000000000000..87df1ab04c8e --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/editors/chat/messageBubble.spec.ts @@ -0,0 +1,84 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, appendElementTo } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('ChatMessageBubble', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('Chat: messagebubble', async ({ page }) => { + + await appendElementTo(page, '#container', 'div', 'chat'); + + await createWidget(page, 'dxChat', { + width: 400, + height: 650, + }, '#chat'); + + const chat = page.locator('#chat'); + + const userFirst = createUser(1, 'First'); + const userSecond = createUser(2, 'Second'); + + let items = generateMessages(2, userFirst, userSecond, true, false, 2); + + await chat.option({ items, user: userSecond }); + await testScreenshot(page, 'Bubbles with long text.png', { element: '#chat' }); + + items = generateMessages(2, userFirst, userSecond, true, true, 2); + + await chat.option({ items }); + await testScreenshot(page, 'Bubbles with long text with line breaks.png', { element: '#chat' }); + + }); + + test('Chat: messagebubble with images and files', async ({ page }) => { + + await appendElementTo(page, '#container', 'div', 'chat'); + + await createWidget(page, 'dxChat', { + width: 600, + height: 650, + }, '#chat'); + + const chat = page.locator('#chat'); + + const user = createUser(1, 'ImageUser'); + + const imageMessages = [ + generateImageMessage(user, '../../../apps/demos/images/products/1.png'), + generateImageMessage(user, '../../../apps/demos/images/products/1-small.png'), + ]; + + await chat.option({ items: imageMessages }); + await testScreenshot(page, 'Bubbles with images.png', { element: '#chat' }); + + const fileMessages = [ + generateFileMessage(user), + generateFileMessage(user, true), + ]; + + await chat.option({ + width: 700, + height: 720, + items: fileMessages, + }); + await testScreenshot(page, 'Bubbles with files.png', { element: '#chat' }); + + await chat.option({ + width: 600, + height: 600, + items: [generateFileMessageWithoutText(user)], + }); + await testScreenshot(page, 'Bubble with files without text.png', { element: '#chat' }); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/editors/chat/messageGroup.spec.ts b/e2e/testcafe-devextreme/playwright-tests/editors/chat/messageGroup.spec.ts new file mode 100644 index 000000000000..053fdcd051fc --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/editors/chat/messageGroup.spec.ts @@ -0,0 +1,174 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, appendElementTo, setStyleAttribute } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('ChatMessageGroup', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + const AVATAR_SELECTOR = '.dx-avatar'; + const CHAT_WRAPPER_STYLES = 'display: flex; flex-wrap: wrap; gap: 2px; width: 1270px; padding: 20px; transform: scale(0.9);'; + + test('Chat: messagegroup, avatar rendering', async ({ page }) => { + + await appendElementTo(page, '#container', 'div', 'chat'); + + const userFirst = createUser(1, 'First'); + const items = generateMessages(3, userFirst); + + await createWidget(page, 'dxChat', { + width: 400, + height: 600, + items, + }, '#chat'); + + await testScreenshot(page, 'Avatar has correct position.png', { element: '#chat' }); + + await setStyleAttribute(page, Selector(AVATAR_SELECTOR), 'width: 64px; height: 64px'); + await testScreenshot(page, 'Avatar sizes do not affect indentation between bubbles.png', { element: '#chat' }); + + }); + + test('Chat: messagegroup, information', async ({ page }) => { + + await appendElementTo(page, '#container', 'div', 'chat'); + + const userFirst = createUser(1, getLongText()); + const userSecond = createUser(2, getLongText()); + + const items = generateMessages(2, userFirst, userSecond, false, false, 2); + + await createWidget(page, 'dxChat', { + width: 400, + height: 600, + user: userSecond, + items, + }, '#chat'); + + await testScreenshot(page, 'Information row with long user name.png', { element: '#chat' }); + + }); + + test('Chat: messagegroup, bubbles', async ({ page }) => { + + await appendElementTo(page, '#container', 'div', 'chat'); + + await createWidget(page, 'dxChat', { + width: 400, + height: 600, + }, '#chat'); + + const chat = page.locator('#chat'); + + const userFirst = createUser(1, 'First'); + const userSecond = createUser(2, 'Second'); + + let items = generateMessages(1, userFirst, userSecond, false, false, 4, 2); + + await chat.option({ items, user: userSecond }); + await testScreenshot(page, 'Messagegroup with 1 bubble.png', { element: '#chat' }); + + items = generateMessages(2, userFirst, userSecond, false, false, 4, 2); + + await chat.option({ items }); + await testScreenshot(page, 'Messagegroup with 2 bubbles.png', { element: '#chat' }); + + items = generateMessages(3, userFirst, userSecond, false, false, 4, 2); + + await chat.option({ items }); + await testScreenshot(page, 'Messagegroup with 3 bubbles.png', { element: '#chat' }); + + items = generateMessages(4, userFirst, userSecond, false, false, 4, 2); + + await chat.option({ items }); + await testScreenshot(page, 'Messagegroup with 4 bubbles.png', { element: '#chat' }); + + }); + + test('Messagegroup scenarios in disabled state', async ({ page }) => { + + await appendElementTo(page, '#container', 'div', 'chat-wrapper'); + await setStyleAttribute(page, '#chat-wrapper', CHAT_WRAPPER_STYLES); + + const userFirst = createUser(1, 'First'); + const userSecond = createUser(2, 'Second'); + + await asyncForEach([1, 2, 3, 4], async (bubbleCount, idx) => { + const chatId = `#chat_${idx}`; + await appendElementTo(page, '#chat-wrapper', 'div', `chat_${idx}`); + + const items = generateMessages(bubbleCount, userFirst, userSecond, false, false, 4, 2); + + await createWidget(page, 'dxChat', { + items, + disabled: true, + user: userSecond, + width: 250, + height: 400, + }, chatId); + + const chat = new Chat(chatId); + await chat.repaint(); + }); + + await testScreenshot(page, 'Messagegroup appearance in disabled state.png', { element: '#chat-wrapper' }); + + }); + + test('Messagegroup scenarios in RTL mode', async ({ page }) => { + + await appendElementTo(page, '#container', 'div', 'chat-wrapper'); + await setStyleAttribute(page, '#chat-wrapper', CHAT_WRAPPER_STYLES); + + const userFirst = createUser(1, 'First'); + const userSecond = createUser(2, 'Second'); + + await asyncForEach([1, 2, 3, 4], async (bubbleCount, idx) => { + const chatId = `#chat_${idx}`; + await appendElementTo(page, '#chat-wrapper', 'div', `chat_${idx}`); + + const items = generateMessages(bubbleCount, userFirst, userSecond, false, false, 4, 2); + + await createWidget(page, 'dxChat', { + items, + rtlEnabled: true, + user: userSecond, + width: 250, + height: 400, + }, chatId); + + const chat = new Chat(chatId); + await chat.repaint(); // NOTE: WA to make it stable in Material theme. + }); + + await testScreenshot(page, 'Messagegroup appearance in RTL mode.png', { element: '#chat-wrapper' }); + + }); + + test('MessageGroup with edited messages', async ({ page }) => { + + await appendElementTo(page, '#container', 'div', 'chat'); + + const userFirst = createUser(1, 'First'); + const userSecond = createUser(2, 'Second'); + const items = generateMessages(2, userFirst, userSecond, false, false, 2, 2, true); + + await createWidget(page, 'dxChat', { + width: 400, + height: 600, + user: userSecond, + items, + }, '#chat'); + + await testScreenshot(page, 'MessageGroup with edited messages.png', { element: '#chat' }); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/editors/chat/messageList.spec.ts b/e2e/testcafe-devextreme/playwright-tests/editors/chat/messageList.spec.ts new file mode 100644 index 000000000000..24d63275c864 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/editors/chat/messageList.spec.ts @@ -0,0 +1,362 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('ChatMessageList', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('Messagelist empty view scenarios', async ({ page }) => { + await createWidget(page, 'dxChat', { + width: 400, + height: 600, + }); + + const chat = page.locator('#container'); + + await testScreenshot(page, 'Messagelist empty state.png', { element: '#container' }); + + await chat.option('rtlEnabled', true); + + await testScreenshot(page, 'Messagelist empty in RTL mode.png', { element: '#container' }); + + await chat.option({ + disabled: true, + rtlEnabled: false, + }); + + await testScreenshot(page, 'Messagelist empty in disabled state.png', { element: '#container' }); + + await chat.option({ + width: 200, + height: 400, + disabled: false, + }); + + await testScreenshot(page, 'Messagelist empty with limited dimensions.png', { element: '#container' }); + + }); + + test('Messagelist appearance with scrollbar', async ({ page }) => { + + const userFirst = createUser(1, 'First'); + const userSecond = createUser(2, 'Second'); + + const items = generateMessages(17, userFirst, userSecond, true, false, 2); + + await createWidget(page, 'dxChat', { + items, + user: userSecond, + width: 400, + height: 600, + showDayHeaders: false, + onMessageEntered: (e) => { + const { component, message } = e; + + component.renderMessage(message); + }, + }); + + const chat = page.locator('#container'); + + await page.hover(chat.messageList); + + await testScreenshot(page, 'Messagelist with a lot of messages.png', { element: '#container' }); + + await ClientFunction( + () => { + const instance = chat.getInstance(); + instance.renderMessage({ + author: instance.option('user') as User, + text: 'Lorem ipsum dolor sit amet, \nconsectetur adipiscing elit. Sed do eiusmod tempor \nincididunt ut labore et dolore magna aliqua. Ut enim ad minim \nveniam, quis nostrud exercitation ullamco laboris nisi ut aliquip \nnex ea commodo consequat.', + }); + }, + { dependencies: { chat } }, + )(); + + await testScreenshot(page, 'Messagelist scrollbar position after call renderMessage().png', { element: '#container' }); + + await page.typeText(chat.getInput(), getLongText()) + .pressKey('shift+enter'); + + await testScreenshot(page, 'Messagelist scrollbar position after typing in textarea.png', { element: '#container' }); + + await page.keyboard.press('Enter'); + + await testScreenshot(page, 'Messagelist scrollbar position after send.png', { element: '#container' }); + + const scrollable = chat.getScrollable(); + const topOffset = (await scrollable.scrollOffset()).top; + + await scrollable.scrollTo({ top: topOffset - 100 }); + + await page.typeText(chat.getInput(), getLongText()); + + await testScreenshot(page, 'Messagelist scrollbar middle position after typing in textarea.png', { element: '#container' }); + + }); + + test('Messagelist should scrolled to the latest messages after being rendered inside an invisible element', async ({ page }) => { + + const userFirst = createUser(1, 'First'); + const userSecond = createUser(2, 'Second'); + + const items = generateMessages(17, userFirst, userSecond, true, false, 2); + + await createWidget(page, 'dxTabPanel', { + width: 400, + height: 600, + deferRendering: true, + templatesRenderAsynchronously: true, + dataSource: [{ + title: 'Tab_1', + collapsible: true, + text: 'Tab_1 content', + }, { + title: 'Tab_2', + collapsible: true, + template: ClientFunction(() => ($('
') as any).dxChat({ + items, + user: userSecond, + }), { dependencies: { items, userSecond } }), + }], + }); + + const tabPanel = page.locator('#container'); + + await tabPanel.tabs.getItem(1).element.click(); + + await testScreenshot(page, 'Messagelist scroll position after rendering in invisible container.png', { element: '#container' }); + + }); + + test('Messagelist with deleted items', async ({ page }) => { + + const userFirst = createUser(1, 'First'); + const userSecond = createUser(2, 'Second'); + const items = [{ + author: userFirst, + text: 'AAA', + }, { + author: userFirst, + text: 'BBB', + isDeleted: true, + }, { + author: userSecond, + text: 'CCC', + isDeleted: true, + }]; + + await createWidget(page, 'dxChat', { + items, + user: userFirst, + width: 400, + height: 600, + showDayHeaders: false, + }); + + await testScreenshot(page, 'Messagelist without message template and with deleted messages.png', { element: '#container' }); + + }); + + test('Messagelist with deleted items and custom template', async ({ page }) => { + + const userFirst = createUser(1, 'First'); + const userSecond = createUser(2, 'Second'); + const items = [{ + author: userFirst, + text: 'AAA', + }, { + author: userFirst, + text: 'BBB', + isDeleted: true, + }, { + author: userSecond, + text: 'CCC', + isDeleted: true, + }]; + + await createWidget(page, 'dxChat', { + items, + user: userFirst, + width: 400, + height: 600, + showDayHeaders: false, + messageTemplate: ({ message }, container) => { + if (message.isDeleted) { + $('
').text(`${message.author.name} deleted this message`).appendTo(container); + return; + } + $('
').text(message.text).appendTo(container); + }, + }); + + await testScreenshot(page, 'Messagelist with message template and deleted messages.png', { element: '#container' }); + + }); + + test('Messagelist with messageTemplate', async ({ page }) => { + + const userFirst = createUser(1, 'First'); + const userSecond = createUser(2, 'Second'); + const items = [{ + author: userFirst, + text: 'AAA', + }, { + author: userFirst, + text: 'BBB', + }, { + author: userSecond, + text: 'CCC', + }]; + + await createWidget(page, 'dxChat', { + items, + user: userFirst, + width: 400, + height: 600, + showDayHeaders: false, + onMessageEntered: ({ component, message }) => { + message.timestamp = undefined; + component.renderMessage(message); + }, + messageTemplate: ({ message }, container) => { + $('
').text(`${message.author.name} says: ${message.text}`).appendTo(container); + }, + }); + + const chat = page.locator('#container'); + + await testScreenshot(page, 'Messagelist with message template.png', { element: '#container' }); + + await chat.getInput().fill('New last message') + .pressKey('enter'); + + await testScreenshot(page, 'Messagelist with message template after new message add.png', { element: '#container' }); + + }); + + test('Messagelist options showDayHeaders, showUserName and showMessageTimestamp set to false work', async ({ page }) => { + + const userFirst = createUser(1, 'First'); + const userSecond = createUser(2, 'Second'); + const items = [{ + author: userFirst, + text: 'AAA', + }, { + author: userFirst, + text: 'BBB', + }, { + author: userSecond, + text: 'CCC', + }]; + + await createWidget(page, 'dxChat', { + items, + user: userFirst, + width: 400, + height: 600, + showDayHeaders: false, + showUserName: false, + showMessageTimestamp: false, + }); + + await testScreenshot(page, + 'Messagelist with showDayHeaders, showUserName and showMessageTimestamp options set to false.png', + { element: '#container' }, + + }); + + test('Message list with editing context menu', async ({ page }) => { + + const userFirst = createUser(1, 'First'); + const userSecond = createUser(2, 'Second'); + + const items = [ + { author: userFirst, text: 'AAA' }, + { author: userFirst, text: 'BBB' }, + { author: userSecond, text: 'CCC' }, + ]; + + await createWidget(page, 'dxChat', { + items, + editing: { + allowUpdating: true, + allowDeleting: true, + }, + user: userSecond, + width: 400, + height: 600, + showDayHeaders: false, + }); + + const chat = page.locator('#container'); + + await page.rightClick(chat.getMessage(2)) + .pressKey('down') + .pressKey('down'); + + await testScreenshot(page, 'Messagelist with editing context menu.png', { element: '#container' }); + + }); + + test.clientScripts([ + { module: 'mockdate' }, + { content: 'window.MockDate = MockDate;' }, + ])('Messagelist with date headers', async ({ page }) => { + + await testScreenshot(page, 'Messagelist with date headers.png', { element: '#container' }); + }).before(async () => { + await page.evaluate(() => { + (window as any).MockDate.set('2024/10/27'); + }); + + const userFirst = createUser(1, 'First'); + const userSecond = createUser(2, 'Second'); + const msInDay = 86400000; + const today = new Date('2024/10/27').setHours(7, 22, 0, 0); + const yesterday = today - msInDay; + + const items = [{ + timestamp: new Date('05.01.2024'), + author: userFirst, + text: 'AAA', + }, { + timestamp: new Date('06.01.2024'), + author: userFirst, + text: 'BBB', + }, { + timestamp: new Date('06.01.2024'), + author: userSecond, + text: 'CCC', + }, { + timestamp: yesterday, + author: userSecond, + text: 'DDD', + }, { + timestamp: today, + author: userFirst, + text: 'EEE', + }]; + + await createWidget(page, 'dxChat', { + items, + user: userSecond, + width: 400, + height: 600, + }); + }).after(async () => { + await page.evaluate(() => { + (window as any).MockDate.reset(); + delete (window as any).MockDate; + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/editors/chat/typingIndicator.spec.ts b/e2e/testcafe-devextreme/playwright-tests/editors/chat/typingIndicator.spec.ts new file mode 100644 index 000000000000..123baef94ec3 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/editors/chat/typingIndicator.spec.ts @@ -0,0 +1,117 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, appendElementTo, insertStylesheetRulesToPage } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('ChatTypingIndicator', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + const CHAT_TYPINGINDICATOR_CIRCLE_CLASS = 'dx-chat-typingindicator-circle'; + const waitFont = async () => page.evaluate(() => (window as any).DevExpress.ui.themes.waitWebFont('Item123somevalu*op ', 400)); + + test('Chat: typing indicator with emptyview', async ({ page }) => { + + await insertStylesheetRulesToPage(page, `.${CHAT_TYPINGINDICATOR_CIRCLE_CLASS} { animation: none !important; }`); + + const typingUsers = [ + { name: 'Elodie Montclair' }, + ]; + + await waitFont(); + + await createWidget(page, 'dxChat', { + width: 400, + height: 600, + typingUsers, + }); + + await testScreenshot(page, 'Typing indicator with emptyview.png', { + element: '#container', + }); + + }); + + test('Chat: typing indicator with a lot of items', async ({ page }) => { + + await appendElementTo(page, '#container', 'div', 'chat'); + await insertStylesheetRulesToPage(page, `.${CHAT_TYPINGINDICATOR_CIRCLE_CLASS} { animation: none !important; }`); + + const userFirst = createUser(1, 'Marie-Claire Dubois'); + const userSecond = createUser(2, 'Jean-Pierre Martin'); + + const items = generateMessages(27, userFirst, userSecond); + + const typingUsers = [userFirst]; + + await createWidget(page, 'dxChat', { + user: userSecond, + width: 400, + height: 600, + items, + typingUsers, + }, '#chat'); + + const chat = page.locator('#chat'); + + await chat.repaint(); + + await testScreenshot(page, 'Typing indicator with a lot of items.png', { element: '#chat' }); + + }); + + test('Chat: typing indicator', async ({ page }) => { + + await appendElementTo(page, '#container', 'div', 'chat'); + await insertStylesheetRulesToPage(page, `.${CHAT_TYPINGINDICATOR_CIRCLE_CLASS} { animation: none !important; }`); + + const userFirst = createUser(1, 'Elise Moreau'); + const userSecond = createUser(2, 'Pierre Martin'); + + const items = generateMessages(5, userFirst, userSecond); + + const typingUsers = [userFirst]; + + await waitFont(); + + await createWidget(page, 'dxChat', { + user: userSecond, + width: 400, + height: 600, + items, + typingUsers, + }, '#chat'); + + await testScreenshot(page, 'Typing indicator with 1 user.png', { element: '#chat' }); + + const chat = page.locator('#chat'); + + const userFirst = createUser(1, 'Camille'); + const userSecond = createUser(2, 'Sophie'); + const userThird = createUser(3, 'Antoine'); + const userFourth = createUser(4, 'Julien'); + + await chat.option('typingUsers', [userFirst, userSecond]); + await testScreenshot(page, 'Typing indicator with 2 users.png', { element: '#chat' }); + + await chat.option('typingUsers', [userFirst, userSecond, userThird]); + await testScreenshot(page, 'Typing indicator with 3 users.png', { element: '#chat' }); + + await chat.option('typingUsers', [userFirst, userSecond, userThird, userFourth]); + await testScreenshot(page, 'Typing indicator with 4 users.png', { element: '#chat' }); + + await chat.option('typingUsers', [{ name: 'Marie-Francoise Isabelle Antoinette de La Rochefoucauld' }]); + await testScreenshot(page, 'Typing indicator with long name.png', { element: '#chat' }); + + await chat.option('typingUsers', [{}]); + await testScreenshot(page, 'Typing indicator without name.png', { element: '#chat' }); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/editors/checkBox/common.spec.ts b/e2e/testcafe-devextreme/playwright-tests/editors/checkBox/common.spec.ts new file mode 100644 index 000000000000..22cca87c5b28 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/editors/checkBox/common.spec.ts @@ -0,0 +1,122 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, appendElementTo, setStyleAttribute, setClassAttribute, insertStylesheetRulesToPage } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('CheckBox', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + const valueModes = [false, true, undefined]; + + const CHECKBOX_CLASS = 'dx-checkbox'; + const READONLY_STATE_CLASS = 'dx-state-readonly'; + const DEFAULT_STATE_CLASS = ''; + const ACTIVE_STATE_CLASS = 'dx-state-active'; + const HOVER_STATE_CLASS = 'dx-state-hover'; + const FOCUSED_STATE_CLASS = 'dx-state-focused'; + const DISABLED_STATE_CLASS = 'dx-state-disabled'; + const INVALID_STATE_CLASS = 'dx-invalid'; + + [false, true].forEach((isColumnCountStyle) => { + test(`Render ${!isColumnCountStyle ? 'default' : 'with column-count style on container'}`, async ({ page }) => { + + await setStyleAttribute(page, '#container', `padding: 5px; width: 300px; height: 200px; ${isColumnCountStyle ? 'column-count: 2' : ''}`); + + await insertStylesheetRulesToPage(page, `.${CHECKBOX_CLASS} { display: block; }`); + + await appendElementTo(page, '#container', 'div', 'checked'); + await createWidget(page, 'dxCheckBox', { value: true, text: 'checked' }, '#checked'); + + await appendElementTo(page, '#container', 'div', 'unchecked'); + await createWidget(page, 'dxCheckBox', { value: false, text: 'unchecked' }, '#unchecked'); + + await appendElementTo(page, '#container', 'div', 'indeterminate'); + await createWidget(page, 'dxCheckBox', { value: undefined, text: 'indeterminate' }, '#indeterminate'); + + // rtl + await appendElementTo(page, '#container', 'div', 'checkedRTL'); + await createWidget(page, 'dxCheckBox', { value: true, text: 'checked', rtlEnabled: true }, '#checkedRTL'); + + await appendElementTo(page, '#container', 'div', 'uncheckedRTL'); + await createWidget(page, 'dxCheckBox', { value: false, text: 'unchecked', rtlEnabled: true }, '#uncheckedRTL'); + + await appendElementTo(page, '#container', 'div', 'indeterminateRTL'); + await createWidget(page, 'dxCheckBox', { value: undefined, text: 'indeterminate', rtlEnabled: true }, '#indeterminateRTL'); + + + await testScreenshot(page, `Checkbox states${isColumnCountStyle ? ' with column count style' : ''}.png`, { element: '#container' }); + + }); + }); + + test('Checkbox appearance', async ({ page }) => { + + for (const state of [ + DEFAULT_STATE_CLASS, + READONLY_STATE_CLASS, + DISABLED_STATE_CLASS, + HOVER_STATE_CLASS, + ACTIVE_STATE_CLASS, + FOCUSED_STATE_CLASS, + `${FOCUSED_STATE_CLASS} ${HOVER_STATE_CLASS}`, + INVALID_STATE_CLASS, + `${INVALID_STATE_CLASS} ${FOCUSED_STATE_CLASS}`, + ] as string[]) { + await page.evaluate(() => { + $('#container').append($('
').text(`State: ${state}`).css('fontSize', '10px')); + }); + + for (const iconSize of [undefined, 25]) { + for (const text of [undefined, 'Label text']) { + for (const rtlEnabled of [false, true]) { + for (const value of valueModes) { + const id = `dx${new Guid()}`; + await appendElementTo(page, '#container', 'div', id, {}); + + await createWidget(page, 'dxCheckBox', { + text, + value, + rtlEnabled, + iconSize, + }, `#${id}`); + await setClassAttribute(page, `#${id}`, state); + } + } + } + + for (const rtlEnabled of [false, true]) { + const id = `dx${new Guid()}`; + await appendElementTo(page, '#container', 'div', id, {}); + + await createWidget(page, 'dxCheckBox', { + text: 'Label text', + width: 50, + rtlEnabled, + }, `#${id}`); + await setClassAttribute(page, `#${id}`, state); + } + } + } + + await insertStylesheetRulesToPage(page, '.dx-checkbox.dx-widget { display: inline-flex; vertical-align: middle; margin-inline: 10px; }'); + + await testScreenshot(page, 'CheckBox appearance.png'); + + for (const scale of [1.15, 0.67]) { + await page.evaluate(() => { + $('#container').css('transform', `scale(${scale})`); + }); + + await testScreenshot(page, `CheckBox appearance in scaled container, scale=${scale}.png`); + } + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/editors/checkBox/validationMessage.spec.ts b/e2e/testcafe-devextreme/playwright-tests/editors/checkBox/validationMessage.spec.ts new file mode 100644 index 000000000000..3da0d3d3b675 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/editors/checkBox/validationMessage.spec.ts @@ -0,0 +1,82 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('CheckBox_ValidationMessage', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('ValidationMessage integrated in editor should not raise any errors when it is placed inside of form and has name "style" (T941581)', async ({ page }) => { + + await createWidget(page, 'dxCheckBox', { + name: 'style', + }); + + await createWidget(page, 'dxValidator', { + validationRules: [{ + type: 'required', + message: 'it is required', + }], + }); + + const checkBox = page.locator('#container'); + await checkBox.click() + .click(checkBox.element) + .expect(true).ok(); + + }); + + test('ValidationMessage integrated in editor should not raise any errors when it is placed inside of form that has inline style with scale (T941581)', async ({ page }) => { + + await createWidget(page, 'dxCheckBox', {}); + + await createWidget(page, 'dxValidator', { + validationRules: [{ + type: 'required', + message: 'it is required', + }], + }); + + const checkBox1 = page.locator('#container'); + await checkBox1.click() + .click(checkBox1.element) + .expect(true).ok(); + + }); + + const positions = ['top', 'right', 'bottom', 'left']; + positions.forEach((position) => { + test(`CheckBox ValidationMessage position is correct (${position})`, async ({ page }) => { + + await createWidget(page, 'dxCheckBox', { + text: 'Click me!', + elementAttr: { style: 'margin: 50px 0 0 100px;' }, + validationMessagePosition: position, + }); + + await createWidget(page, 'dxValidator', { + validationRules: [{ + type: 'required', + message: 'it is required', + }], + }); + + + const checkBox1 = page.locator('#container'); + await checkBox1.click() + .click(checkBox1.element) + .expect(true).ok(); + + await testScreenshot(page, `Checkbox validation message with ${position} position.png`); + + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/editors/colorbox/colorbox.spec.ts b/e2e/testcafe-devextreme/playwright-tests/editors/colorbox/colorbox.spec.ts new file mode 100644 index 000000000000..9b173100a471 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/editors/colorbox/colorbox.spec.ts @@ -0,0 +1,53 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, appendElementTo, setStyleAttribute } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('Colorbox', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('Colorbox should display full placeholder', async ({ page }) => { + + await appendElementTo(page, '#container', 'div', 'colorBox'); + await setStyleAttribute(page, '#container', 'box-sizing: border-box; width: 300px; height: 100px; padding: 8px;'); + + await createWidget(page, 'dxColorBox', { + width: '100%', + placeholder: 'I am a very long placeholder', + }, '#colorBox'); + + await testScreenshot(page, 'Colorbox with placeholder.png', { element: '#container' }); + + }); + + ['#00ffff', 'rgb(0,255,255)', 'rgba(0,255,255,1)', 'aqua'].forEach((inputText) => { + ['enter', 'tab'].forEach((key) => { + test(`input value=${inputText} should be formatted to rgba after apply on ${key} key press`, async ({ page }) => { + await createWidget(page, 'dxColorBox', { + editAlphaChannel: true, + }, '#container'); + + const colorBox = page.locator('#container'); + const expectedValue = 'rgba(0, 255, 255, 1)'; + + await page.click(colorBox.input); + + await page.typeText(colorBox.input, inputText) + .pressKey(key) + .expect(colorBox.option('text')) + .eql(expectedValue) + .expect(colorBox.option('value')) + .eql(expectedValue); + + }); + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/editors/dateBox/common.spec.ts b/e2e/testcafe-devextreme/playwright-tests/editors/dateBox/common.spec.ts new file mode 100644 index 000000000000..21094db6fe6e --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/editors/dateBox/common.spec.ts @@ -0,0 +1,82 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, appendElementTo, setClassAttribute, insertStylesheetRulesToPage } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('DateBox render', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + const DATEBOX_CLASS = 'dx-datebox'; + const DROP_DOWN_EDITOR_ACTIVE_CLASS = 'dx-dropdowneditor-active'; + const FOCUSED_STATE_CLASS = 'dx-state-focused'; + + const stylingModes: EditorStyle[] = ['outlined', 'underlined', 'filled']; + const pickerTypes: DatePickerType[] = ['calendar', 'list', 'native', 'rollers']; + const types: DateType[] = ['date', 'datetime', 'time']; + + const createDateBox = async (options?: Properties, state?: string): Promise => { + const id = `${`dx${new Guid()}`}`; + + await appendElementTo(page, '#container', 'div', id, {}); + await createWidget(page, 'dxDateBox', { + width: 220, + label: 'label text', + showClearButton: true, + value: new Date(2021, 9, 17, 16, 34), + ...options, + }, `#${id}`); + + if (state) { + await setClassAttribute(page, `#${id}`, state); + } + + return id; + }; + + test('DateBox styles', async ({ page }) => { + + await testScreenshot(page, 'Datebox.png'); + + for (const state of [DROP_DOWN_EDITOR_ACTIVE_CLASS, FOCUSED_STATE_CLASS] as any[]) { + for (const id of t.ctx.ids) { + await setClassAttribute(page, `#${id}`, state); + } + + await testScreenshot(page, `Datebox ${state.replaceAll('dx-', '').replaceAll('dropdowneditor-', '').replaceAll('state-', '')}.png`); + + for (const id of t.ctx.ids) { + await removeClassAttribute(page.locator(`#${id}`), state); + } + } + + });.before(async ({ page }) => { + t.ctx.ids = []; + + await insertStylesheetRulesToPage(page, `.${DATEBOX_CLASS} { display: inline-block; margin: 5px; }`); + + for (const stylingMode of stylingModes) { + for (const type of types) { + const options = { + stylingMode, + type, + }; + for (const pickerType of pickerTypes) { + const id = await createDateBox({ ...options, pickerType }); + + t.ctx.ids.push(id); + } + + const id = await createDateBox({ ...options, rtlEnabled: true }); + t.ctx.ids.push(id); + } + } + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/editors/dateBox/dateBox.spec.ts b/e2e/testcafe-devextreme/playwright-tests/editors/dateBox/dateBox.spec.ts new file mode 100644 index 000000000000..6cd3da9b1de4 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/editors/dateBox/dateBox.spec.ts @@ -0,0 +1,94 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, isMaterialBased } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('DateBox', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + const ITEM_HEIGHT = 40; + + const TIME_VIEW_FIELD_CLASS = 'dx-timeview-field'; + const SELECT_BOX_CONTAINER_CLASS = 'dx-selectbox-container'; + + if (!isMaterialBased()) { + [[11, 12, 1925], [10, 23, 2001]].forEach(([month, day, year]) => { + test(`Rollers should be scrolled correctly when value is changed to ${day}/${month}/${year} using kbn and valueChangeEvent=keyup (T948310)`, async ({ page }) => { + await createWidget(page, 'dxDateBox', { + pickerType: 'rollers', + openOnFieldClick: false, + useMaskBehavior: true, + valueChangeEvent: 'keyup', + }); + + const dateBox = page.locator('#container'); + const { dropDownEditorButton } = dateBox; + + await page.click(dropDownEditorButton); + + await page.click(DateBox.getDoneButton()); + + await page.typeText(dateBox.input, `${month}${day}${year}`); + + await page.click(dropDownEditorButton); + + const views = { + month: month - 1, + day: day - 1, + year: year - 1900, + }; + for (const viewName of Object.keys(views)) { + const scrollTop = await DateBox.getRollerScrollTop(viewName); + + expect(scrollTop).toBe(views[viewName] * ITEM_HEIGHT, `${viewName} view is scrolled correctly`); + }); + + }); + }); + } + + test('DateBox with datetime and root element as container (T1193495)', async ({ page }) => { + await createWidget(page, 'dxDateBox', { + value: new Date(2022, 10, 23, 17, 23), + type: 'datetime', + pickerType: 'calendar', + opened: true, + width: 300, + dropDownOptions: { + container: '#container', + }, + }, '#container'); + + await testScreenshot(page, 'DateBox with datetime and root element as container.png', { element: '#container' }); + + }); + + test('DateBox with datetime and opened AM/PM select (T1312677)', async ({ page }) => { + await createWidget(page, 'dxDateBox', { + value: new Date(2022, 10, 23, 17, 23), + type: 'datetime', + pickerType: 'calendar', + opened: true, + dropDownOptions: { + container: '#container', + }, + }, '#container'); + + const timeViewSelect = page.locator(`#container .${TIME_VIEW_FIELD_CLASS} .${SELECT_BOX_CONTAINER_CLASS}`); + + await page.click(timeViewSelect); + + await testScreenshot(page, + 'DateBox with datetime and opened AMPM select.png', + { element: '#container' }, + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/editors/dateBox/dateBoxGeometry.spec.ts b/e2e/testcafe-devextreme/playwright-tests/editors/dateBox/dateBoxGeometry.spec.ts new file mode 100644 index 000000000000..71a9a26f05d5 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/editors/dateBox/dateBoxGeometry.spec.ts @@ -0,0 +1,54 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('DateBox (datetime) geometry (T896846)', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + const waitFont = async () => page.evaluate(() => (window as any).DevExpress.ui.themes.waitWebFont('1234567890APM/:', 400)); + + test('Geometry is good', async ({ page }) => { + + await waitFont(); + + await createWidget(page, 'dxDateBox', { + pickerType: 'calendar', + width: 200, + value: new Date(1.5e12), + }); + + const dateBox = page.locator('#container'); + + await dateBox.option('opened', true); + + await testScreenshot(page, 'Datebox with calendar.png'); + + await dateBox.option('opened', false); + await dateBox.option('type', 'datetime'); + await dateBox.option('opened', true); + + await testScreenshot(page, 'Datebox with datetime.png'); + + await dateBox.option('opened', false); + await dateBox.option({ showAnalogClock: false }); + await dateBox.option('opened', true); + + await testScreenshot(page, 'Datebox with datetime without analog clock.png'); + + await dateBox.option('opened', false); + await dateBox.option({ displayFormat: 'HH:mm', calendarOptions: { visible: false }, showAnalogClock: true }); + await dateBox.option('opened', true); + + await testScreenshot(page, 'Datebox with datetime without calendar.png'); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/editors/dateBox/keyboard.spec.ts b/e2e/testcafe-devextreme/playwright-tests/editors/dateBox/keyboard.spec.ts new file mode 100644 index 000000000000..924d9a5818c0 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/editors/dateBox/keyboard.spec.ts @@ -0,0 +1,624 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, appendElementTo } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('DateBox keyboard navigation', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('DateBox should be closed by press esc key when navigator element in popup is focused, applyValueMode is useButtons', async ({ page }) => { + await createWidget(page, 'dxDateBox', { + openOnFieldClick: true, + applyValueMode: 'useButtons', + }); + + const dateBox = page.locator('#container'); + + await page.click(dateBox.input); + + await page.expect(dateBox.option('opened')) + .eql(true); + + await page.keyboard.press('Tab'); + + await page.expect(dateBox.getPopup().getNavigatorPrevButton().isFocused) + .eql(true); + + await page.keyboard.press('esc'); + + await page.expect(dateBox.option('opened')) + .eql(false); + + await page.click(dateBox.input); + + await page.expect(dateBox.option('opened')) + .eql(true); + + await page.keyboard.press('Tab') + .pressKey('tab'); + + await page.expect(dateBox.getPopup().getNavigatorCaption().isFocused) + .eql(true); + + await page.keyboard.press('esc'); + + await page.expect(dateBox.option('opened')) + .eql(false); + + await page.click(dateBox.input); + + await page.expect(dateBox.option('opened')) + .eql(true); + + await page.keyboard.press('Tab') + .pressKey('tab') + .pressKey('tab'); + + await page.expect(dateBox.getPopup().getNavigatorNextButton().isFocused) + .eql(true); + + await page.keyboard.press('esc'); + + await page.expect(dateBox.option('opened')) + .eql(false); + + }); + + test('DateBox should be closed by press esc key when views wrapper in popup is focused, applyValueMode is useButtons', async ({ page }) => { + await createWidget(page, 'dxDateBox', { + openOnFieldClick: true, + applyValueMode: 'useButtons', + }); + + const dateBox = page.locator('#container'); + + await page.click(dateBox.input); + + await page.expect(dateBox.option('opened')) + .eql(true); + + await page.keyboard.press('Tab') + .pressKey('tab') + .pressKey('tab') + .pressKey('tab'); + + await page.expect(dateBox.getPopup().getViewsWrapper().focused) + .eql(true); + + await page.keyboard.press('esc'); + + await page.expect(dateBox.option('opened')) + .eql(false); + + }); + + test('DateBox should be closed by press esc key when today/cancel/apply button in popup is focused, applyValueMode is useButtons', async ({ page }) => { + await createWidget(page, 'dxDateBox', { + openOnFieldClick: true, + applyValueMode: 'useButtons', + }); + + const dateBox = page.locator('#container'); + + await page.click(dateBox.input); + + await page.expect(dateBox.option('opened')) + .eql(true); + + await page.keyboard.press('Tab') + .pressKey('tab') + .pressKey('tab') + .pressKey('tab') + .pressKey('tab'); + + await page.expect(dateBox.getPopup().getTodayButton().isFocused) + .eql(true); + + await page.keyboard.press('esc'); + + await page.expect(dateBox.option('opened')) + .eql(false); + + await page.click(dateBox.input); + + await page.expect(dateBox.option('opened')) + .eql(true); + + await page.keyboard.press('Tab') + .pressKey('tab') + .pressKey('tab') + .pressKey('tab') + .pressKey('tab') + .pressKey('tab'); + + await page.expect(dateBox.getPopup().getApplyButton().isFocused) + .eql(true); + + await page.keyboard.press('esc'); + + await page.expect(dateBox.option('opened')) + .eql(false); + + await page.click(dateBox.input); + + await page.expect(dateBox.option('opened')) + .eql(true); + + await page.keyboard.press('Tab') + .pressKey('tab') + .pressKey('tab') + .pressKey('tab') + .pressKey('tab') + .pressKey('tab') + .pressKey('tab'); + + await page.expect(dateBox.getPopup().getCancelButton().isFocused) + .eql(true); + + await page.keyboard.press('esc'); + + await page.expect(dateBox.option('opened')) + .eql(false); + + }); + + test('dateBox keyboard navigation via `tab` key if applyValueMode is useButtons, input -> prev -> caption -> next -> views -> today -> apply -> cancel -> input', async ({ page }) => { + + await appendElementTo(page, '#container', 'div', 'firstFocusableElement'); + await appendElementTo(page, '#container', 'div', 'dateBox'); + await appendElementTo(page, '#container', 'div', 'lastFocusableElement'); + + await createWidget(page, 'dxButton', { + text: 'First Focusable Element', + }, '#firstFocusableElement'); + + await createWidget(page, 'dxButton', { + text: 'Last Focusable Element', + }, '#lastFocusableElement'); + + await createWidget(page, 'dxDateBox', { + openOnFieldClick: true, + applyValueMode: 'useButtons', + opened: true, + dropDownOptions: { + hideOnOutsideClick: false, + }, + }, '#dateBox'); + + const dateBox = page.locator('#dateBox'); + + await page.locator('#firstFocusableElement').click() + .pressKey('tab'); + + await page.expect(dateBox.option('opened')) + .eql(true) + .expect(dateBox.isFocused) + .ok() + .expect(dateBox.input().focused) + .ok(); + + await page.keyboard.press('Tab'); + + await page.expect(dateBox.option('opened')) + .eql(true) + .expect(dateBox.getPopup().getNavigatorPrevButton().isFocused) + .ok(); + + await page.keyboard.press('Tab'); + + await page.expect(dateBox.option('opened')) + .eql(true) + .expect(dateBox.getPopup().getNavigatorCaption().isFocused) + .ok(); + + await page.keyboard.press('Tab'); + + await page.expect(dateBox.option('opened')) + .eql(true) + .expect(dateBox.getPopup().getNavigatorNextButton().isFocused) + .ok(); + + await page.keyboard.press('Tab'); + + await page.expect(dateBox.option('opened')) + .eql(true) + .expect(dateBox.getPopup().getViewsWrapper().focused) + .ok(); + + await page.keyboard.press('Tab'); + + await page.expect(dateBox.option('opened')) + .eql(true) + .expect(dateBox.getPopup().getTodayButton().isFocused) + .ok(); + + await page.keyboard.press('Tab'); + + await page.expect(dateBox.option('opened')) + .eql(true) + .expect(dateBox.getPopup().getApplyButton().isFocused) + .ok(); + + await page.keyboard.press('Tab'); + + await page.expect(dateBox.option('opened')) + .eql(true) + .expect(dateBox.getPopup().getCancelButton().isFocused) + .ok(); + + await page.keyboard.press('Tab'); + + await page.expect(dateBox.option('opened')) + .eql(true) + .expect(dateBox.input.focused) + .ok(); + + }); + + test('dateBox keyboard navigation via `shift+tab` key if applyValueMode is useButtons, input -> cancel -> apply -> today -> views -> next -> caption -> prev -> input', async ({ page }) => { + + await appendElementTo(page, '#container', 'div', 'firstFocusableElement'); + await appendElementTo(page, '#container', 'div', 'dateBox'); + await appendElementTo(page, '#container', 'div', 'lastFocusableElement'); + + await createWidget(page, 'dxButton', { + text: 'First Focused Element', + }, '#firstFocusableElement'); + + await createWidget(page, 'dxButton', { + text: 'Last Focused Element', + }, '#lastFocusableElement'); + + await createWidget(page, 'dxDateBox', { + openOnFieldClick: true, + applyValueMode: 'useButtons', + opened: false, + }, '#dateBox'); + + const dateBox = page.locator('#dateBox'); + + await page.click(dateBox.input); + + await page.expect(dateBox.option('opened')) + .eql(true) + .expect(dateBox.isFocused) + .ok() + .expect(dateBox.input.focused) + .ok(); + + await page.keyboard.press('shift+tab'); + + await page.expect(dateBox.option('opened')) + .eql(true) + .expect(dateBox.getPopup().getCancelButton().isFocused) + .ok(); + + await page.keyboard.press('shift+tab'); + + await page.expect(dateBox.option('opened')) + .eql(true) + .expect(dateBox.getPopup().getApplyButton().isFocused) + .ok(); + + await page.keyboard.press('shift+tab'); + + await page.expect(dateBox.option('opened')) + .eql(true) + .expect(dateBox.getPopup().getTodayButton().isFocused) + .ok(); + + await page.keyboard.press('shift+tab'); + + await page.expect(dateBox.option('opened')) + .eql(true) + .expect(dateBox.getPopup().getViewsWrapper().focused) + .ok(); + + await page.keyboard.press('shift+tab'); + + await page.expect(dateBox.option('opened')) + .eql(true) + .expect(dateBox.getPopup().getNavigatorNextButton().isFocused) + .ok(); + + await page.keyboard.press('shift+tab'); + + await page.expect(dateBox.option('opened')) + .eql(true) + .expect(dateBox.getPopup().getNavigatorCaption().isFocused) + .ok(); + + await page.keyboard.press('shift+tab'); + + await page.expect(dateBox.option('opened')) + .eql(true) + .expect(dateBox.getPopup().getNavigatorPrevButton().isFocused) + .ok(); + + await page.keyboard.press('shift+tab'); + + await page.expect(dateBox.option('opened')) + .eql(true) + .expect(dateBox.isFocused) + .ok() + .expect(dateBox.input().focused) + .ok(); + + }); + + test('dateBox keyboard navigation via `tab` key if applyValueMode is instantly, input -> prev -> caption -> next -> views -> input', async ({ page }) => { + + await appendElementTo(page, '#container', 'div', 'firstFocusableElement'); + await appendElementTo(page, '#container', 'div', 'dateBox'); + await appendElementTo(page, '#container', 'div', 'lastFocusableElement'); + + await createWidget(page, 'dxButton', { + text: 'First Focusable Element', + }, '#firstFocusableElement'); + + await createWidget(page, 'dxButton', { + text: 'Last Focusable Element', + }, '#lastFocusableElement'); + + await createWidget(page, 'dxDateBox', { + openOnFieldClick: true, + applyValueMode: 'instantly', + opened: true, + dropDownOptions: { + hideOnOutsideClick: false, + }, + }, '#dateBox'); + + const dateBox = page.locator('#dateBox'); + + await page.locator('#firstFocusableElement').click() + .pressKey('tab'); + + await page.expect(dateBox.option('opened')) + .eql(true) + .expect(dateBox.isFocused) + .ok() + .expect(dateBox.input().focused) + .ok(); + + await page.keyboard.press('Tab'); + + await page.expect(dateBox.option('opened')) + .eql(true) + .expect(dateBox.getPopup().getNavigatorPrevButton().isFocused) + .ok(); + + await page.keyboard.press('Tab'); + + await page.expect(dateBox.option('opened')) + .eql(true) + .expect(dateBox.getPopup().getNavigatorCaption().isFocused) + .ok(); + + await page.keyboard.press('Tab'); + + await page.expect(dateBox.option('opened')) + .eql(true) + .expect(dateBox.getPopup().getNavigatorNextButton().isFocused) + .ok(); + + await page.keyboard.press('Tab'); + + await page.expect(dateBox.option('opened')) + .eql(true) + .expect(dateBox.getPopup().getViewsWrapper().focused) + .ok(); + + await page.keyboard.press('Tab'); + + await page.expect(dateBox.option('opened')) + .eql(true) + .expect(dateBox.input.focused) + .ok(); + + }); + + test('dateBox keyboard navigation via `shift+tab` key if applyValueMode is instantly, input -> views -> next -> caption -> prev -> input', async ({ page }) => { + + await appendElementTo(page, '#container', 'div', 'firstFocusableElement'); + await appendElementTo(page, '#container', 'div', 'dateBox'); + await appendElementTo(page, '#container', 'div', 'lastFocusableElement'); + + await createWidget(page, 'dxButton', { + text: 'First Focused Element', + }, '#firstFocusableElement'); + + await createWidget(page, 'dxButton', { + text: 'Last Focused Element', + }, '#lastFocusableElement'); + + await createWidget(page, 'dxDateBox', { + openOnFieldClick: true, + applyValueMode: 'instantly', + opened: false, + }, '#dateBox'); + + const dateBox = page.locator('#dateBox'); + + await page.click(dateBox.input); + + await page.expect(dateBox.option('opened')) + .eql(true) + .expect(dateBox.isFocused) + .ok() + .expect(dateBox.input.focused) + .ok(); + + await page.keyboard.press('shift+tab'); + + await page.expect(dateBox.option('opened')) + .eql(true) + .expect(dateBox.getPopup().getViewsWrapper().focused) + .ok(); + + await page.keyboard.press('shift+tab'); + + await page.expect(dateBox.option('opened')) + .eql(true) + .expect(dateBox.getPopup().getNavigatorNextButton().isFocused) + .ok(); + + await page.keyboard.press('shift+tab'); + + await page.expect(dateBox.option('opened')) + .eql(true) + .expect(dateBox.getPopup().getNavigatorCaption().isFocused) + .ok(); + + await page.keyboard.press('shift+tab'); + + await page.expect(dateBox.option('opened')) + .eql(true) + .expect(dateBox.getPopup().getNavigatorPrevButton().isFocused) + .ok(); + + await page.keyboard.press('shift+tab'); + + await page.expect(dateBox.option('opened')) + .eql(true) + .expect(dateBox.isFocused) + .ok() + .expect(dateBox.input().focused) + .ok(); + + }); + + test('dateBox keyboard navigation via `tab` and `shift+tab` when calendar is not focusable', async ({ page }) => { + + await appendElementTo(page, '#container', 'div', 'firstFocusableElement'); + await appendElementTo(page, '#container', 'div', 'dateBox'); + await appendElementTo(page, '#container', 'div', 'lastFocusableElement'); + + await createWidget(page, 'dxButton', { + text: 'First Focused Element', + }, '#firstFocusableElement'); + + await createWidget(page, 'dxButton', { + text: 'Last Focused Element', + }, '#lastFocusableElement'); + + await createWidget(page, 'dxDateBox', { + openOnFieldClick: true, + applyValueMode: 'useButtons', + calendarOptions: { + focusStateEnabled: false, + }, + }, '#dateBox'); + + const dateBox = page.locator('#dateBox'); + + await page.click(dateBox.input); + + await page.expect(dateBox.option('opened')) + .eql(true) + .expect(dateBox.isFocused) + .ok() + .expect(dateBox.input.focused) + .ok(); + + await page.keyboard.press('Tab'); + + await page.expect(dateBox.option('opened')) + .eql(true) + .expect(dateBox.getPopup().getTodayButton().isFocused) + .ok(); + + await page.keyboard.press('shift+tab'); + + await page.expect(dateBox.option('opened')) + .eql(true) + .expect(dateBox.isFocused) + .ok() + .expect(dateBox.input().focused) + .ok(); + + }); + + test('dateBox keyboard navigation via `tab` and `shift+tab` when navigator prev button is not focusable', async ({ page }) => { + + await appendElementTo(page, '#container', 'div', 'firstFocusableElement'); + await appendElementTo(page, '#container', 'div', 'dateBox'); + await appendElementTo(page, '#container', 'div', 'lastFocusableElement'); + + await createWidget(page, 'dxButton', { + text: 'First Focused Element', + }, '#firstFocusableElement'); + + await createWidget(page, 'dxButton', { + text: 'Last Focused Element', + }, '#lastFocusableElement'); + + await createWidget(page, 'dxDateBox', { + openOnFieldClick: true, + min: new Date(), + }, '#dateBox'); + + const dateBox = page.locator('#dateBox'); + + await page.click(dateBox.input); + + await page.expect(dateBox.option('opened')) + .eql(true) + .expect(dateBox.isFocused) + .ok() + .expect(dateBox.input.focused) + .ok(); + + await page.keyboard.press('Tab'); + + await page.expect(dateBox.option('opened')) + .eql(true) + .expect(dateBox.getPopup().getNavigatorCaption().isFocused) + .ok(); + + await page.keyboard.press('shift+tab'); + + await page.expect(dateBox.option('opened')) + .eql(true) + .expect(dateBox.isFocused) + .ok() + .expect(dateBox.input().focused) + .ok(); + + }); + + test('dateBox keyboard navigation via `tab` should close popup when there is no focusable elements', async ({ page }) => { + await createWidget(page, 'dxDateBox', { + openOnFieldClick: true, + applyValueMode: 'instantly', + calendarOptions: { + focusStateEnabled: false, + }, + }, '#container'); + + const dateBox = page.locator('#container'); + + await page.click(dateBox.input); + + await page.expect(dateBox.option('opened')) + .eql(true) + .expect(dateBox.isFocused) + .ok() + .expect(dateBox.input.focused) + .ok(); + + await page.keyboard.press('Tab'); + + await page.expect(dateBox.option('opened')) + .eql(false); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/editors/dateBox/label.spec.ts b/e2e/testcafe-devextreme/playwright-tests/editors/dateBox/label.spec.ts new file mode 100644 index 000000000000..bb7bf8793f1d --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/editors/dateBox/label.spec.ts @@ -0,0 +1,79 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, appendElementTo, setStyleAttribute, insertStylesheetRulesToPage, removeStylesheetRulesFromPage } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('DateBox_Label', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + const DATEBOX_CLASS = 'dx-datebox'; + + const stylingModes = ['outlined', 'underlined', 'filled']; + const visibleLabelModes = ['floating', 'static', 'outside']; + + test('Symbol parts in label should not be cropped', async ({ page }) => { + + await appendElementTo(page, '#container', 'div', 'dateBox'); + await setStyleAttribute(page, '#container', 'box-sizing: border-box; width: 300px; height: 600px; padding: 8px;'); + + for (const stylingMode of stylingModes) { + for (const labelMode of visibleLabelModes) { + const id = `${`dx${new Guid()}`}`; + + await appendElementTo(page, '#container', 'div', id, { }); + + await createWidget(page, 'dxDateBox', { + label: 'qwerty QWERTY 1234567890', + stylingMode, + labelMode, + value: new Date(1900, 0, 1), + }, `#${id}`); + } + } + + await testScreenshot(page, 'Datebox label symbols.png', { element: '#container' }); + + }); + + test('DateBox with buttons container', async ({ page }) => { + + for (const stylingMode of stylingModes) { + for (const buttons of [ + ['clear'], + ['clear', 'dropDown'], + [{ name: 'custom', location: 'after', options: { icon: 'home' } }, 'clear', 'dropDown'], + ['clear', { name: 'custom', location: 'after', options: { icon: 'home' } }, 'dropDown'], + ['clear', 'dropDown', { name: 'custom', location: 'after', options: { icon: 'home' } }], + ]) { + for (const isValid of [true, false]) { + const id = `${`dx${new Guid()}`}`; + + await appendElementTo(page, '#container', 'div', id, { }); + + await createWidget(page, 'dxDateBox', { + value: new Date(2021, 9, 17), + stylingMode, + buttons, + showClearButton: true, + isValid, + }, `#${id}`); + } + } + } + + await insertStylesheetRulesToPage(page, `#container { display: flex; flex-wrap: wrap; } .${DATEBOX_CLASS} { width: 220px; margin: 2px; }`); + + await testScreenshot(page, 'DateBox render with buttons container.png'); + + await removeStylesheetRulesFromPage(page, ); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/editors/dateBox/validationMessage.spec.ts b/e2e/testcafe-devextreme/playwright-tests/editors/dateBox/validationMessage.spec.ts new file mode 100644 index 000000000000..f54857a0d5ce --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/editors/dateBox/validationMessage.spec.ts @@ -0,0 +1,54 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, appendElementTo } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('DateBox ValidationMessagePosition', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + const positions = ['top', 'right', 'bottom', 'left']; + + test('DateBox ValidationMessage position is correct', async ({ page }) => { + + for (const id of t.ctx.ids) { + const dateBox = page.locator(`#${id}`); + await dateBox.option('value', new Date(2022, 6, 14)); + } + + await testScreenshot(page, 'Datebox validation message.png'); + + });.before(async ({ page }) => { + t.ctx.ids = []; + + for (const position of positions) { + const id = `${`dx${new Guid()}`}`; + + await appendElementTo(page, '#container', 'div', id, {}); + + t.ctx.ids.push(id); + await createWidget(page, 'dxDateBox', { + elementAttr: { style: 'display: inline-block; margin: 50px 100px 0 0;' }, + width: 150, + height: 40, + validationMessageMode: 'always', + validationMessagePosition: position, + }, `#${id}`); + + await createWidget(page, 'dxValidator', { + validationRules: [{ + type: 'range', + max: new Date(1), + message: 'out of range', + }], + }, `#${id}`); + } + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/editors/dateRangeBox/behavior.spec.ts b/e2e/testcafe-devextreme/playwright-tests/editors/dateRangeBox/behavior.spec.ts new file mode 100644 index 000000000000..eb48e2e7d6ce --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/editors/dateRangeBox/behavior.spec.ts @@ -0,0 +1,1453 @@ +import { test, expect } from '@playwright/test'; +import { createWidget } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('DateRangeBox behavior (applyValueMode=\'instantly\')', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('Open by click on startDate input and select date in calendar, value: [null, null]', async ({ page }) => { + + await page.evaluate(() => { + (window as any).onValueChangedCounter = 0; + }); + + await createWidget(page, 'dxDateRangeBox', { + value: [null, null], + openOnFieldClick: true, + width: 500, + calendarOptions: { + currentDate: new Date(2021, 9, 19), + }, + onValueChanged() { + ((window as any).onValueChangedCounter as number) += 1; + }, + }); + + const dateRangeBox = page.locator('#container'); + + await page.click(dateRangeBox.getStartDateBox().input); + + await page.expect(dateRangeBox.option('opened')) + .eql(true); + + await page.click(dateRangeBox.getCalendarCell(10)) + .expect(dateRangeBox.option('value')) + .eql([new Date(2021, 9, 6), null]) + .expect(ClientFunction(() => (window as any).onValueChangedCounter)()) + .eql(1); + + await page.expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.getEndDateBox().isFocused) + .ok(); + + await page.click(dateRangeBox.getCalendarCell(21)) + .expect(dateRangeBox.option('value')) + .eql([new Date(2021, 9, 6), new Date(2021, 9, 17)]) + .expect(ClientFunction(() => (window as any).onValueChangedCounter)()) + .eql(2) + .expect(dateRangeBox.getEndDateBox().isFocused) + .ok(); + + await page.expect(dateRangeBox.option('opened')) + .eql(false) + .expect(dateRangeBox.getEndDateBox().isFocused) + .ok(); + + }); + + test('Open by click on startDate input and reselect start date in calendar, value: ["2021/09/17", null]', async ({ page }) => { + + await page.evaluate(() => { + (window as any).onValueChangedCounter = 0; + }); + + await createWidget(page, 'dxDateRangeBox', { + value: [null, null], + openOnFieldClick: true, + width: 500, + calendarOptions: { + currentDate: new Date(2021, 9, 19), + }, + onValueChanged() { + ((window as any).onValueChangedCounter as number) += 1; + }, + }); + + const dateRangeBox = page.locator('#container'); + + await page.click(dateRangeBox.getStartDateBox().input); + + await page.expect(dateRangeBox.option('opened')) + .eql(true); + + await page.click(dateRangeBox.getCalendarCell(10)) + .expect(dateRangeBox.option('value')) + .eql([new Date(2021, 9, 6), null]) + .expect(ClientFunction(() => (window as any).onValueChangedCounter)()) + .eql(1); + + await page.expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.getEndDateBox().isFocused) + .ok(); + + await page.click(dateRangeBox.getStartDateBox().input); + + await page.expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.getStartDateBox().isFocused) + .ok(); + + await page.click(dateRangeBox.getCalendarCell(21)) + .expect(dateRangeBox.option('value')) + .eql([new Date(2021, 9, 17), null]) + .expect(ClientFunction(() => (window as any).onValueChangedCounter)()) + .eql(2) + .expect(dateRangeBox.getEndDateBox().isFocused) + .ok(); + + await page.expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.getEndDateBox().isFocused) + .ok(); + + await page.click(dateRangeBox.getCalendarCell(25)) + .expect(dateRangeBox.option('value')) + .eql([new Date(2021, 9, 17), new Date(2021, 9, 21)]) + .expect(ClientFunction(() => (window as any).onValueChangedCounter)()) + .eql(3) + .expect(dateRangeBox.getEndDateBox().isFocused) + .ok(); + + await page.expect(dateRangeBox.option('opened')) + .eql(false) + .expect(dateRangeBox.getEndDateBox().isFocused) + .ok(); + + }); + + test('Open by click on endDate input and select date in calendar, value: [null, null]', async ({ page }) => { + + await page.evaluate(() => { + (window as any).onValueChangedCounter = 0; + }); + + await createWidget(page, 'dxDateRangeBox', { + value: [null, null], + openOnFieldClick: true, + width: 500, + calendarOptions: { + currentDate: new Date(2021, 9, 19), + }, + onValueChanged() { + ((window as any).onValueChangedCounter as number) += 1; + }, + }); + + const dateRangeBox = page.locator('#container'); + + await page.click(dateRangeBox.getEndDateBox().input); + + await page.expect(dateRangeBox.option('opened')) + .eql(true); + + await page.click(dateRangeBox.getCalendarCell(21)) + .expect(dateRangeBox.option('value')) + .eql([null, new Date(2021, 9, 17)]) + .expect(ClientFunction(() => (window as any).onValueChangedCounter)()) + .eql(1); + + await page.expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.getStartDateBox().isFocused) + .ok(); + + await page.click(dateRangeBox.getCalendarCell(10)) + .expect(dateRangeBox.option('value')) + .eql([new Date(2021, 9, 6), new Date(2021, 9, 17)]) + .expect(ClientFunction(() => (window as any).onValueChangedCounter)()) + .eql(2); + + await page.expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.getEndDateBox().isFocused) + .ok(); + + await page.click(dateRangeBox.getCalendarCell(27)) + .expect(dateRangeBox.option('value')) + .eql([new Date(2021, 9, 6), new Date(2021, 9, 23)]) + .expect(ClientFunction(() => (window as any).onValueChangedCounter)()) + .eql(3); + + await page.expect(dateRangeBox.option('opened')) + .eql(false) + .expect(dateRangeBox.getEndDateBox().isFocused) + .ok(); + + }); + + test('Open by click on startDate input and select date in calendar < endDate, value: ["2021/09/17", "2021/09/24"]', async ({ page }) => { + + await page.evaluate(() => { + (window as any).onValueChangedCounter = 0; + }); + + await createWidget(page, 'dxDateRangeBox', { + value: [new Date(2021, 9, 17), new Date(2021, 9, 24)], + openOnFieldClick: true, + width: 500, + calendarOptions: { + currentDate: new Date(2021, 9, 19), + }, + onValueChanged() { + ((window as any).onValueChangedCounter as number) += 1; + }, + }); + + const dateRangeBox = page.locator('#container'); + + await page.click(dateRangeBox.getStartDateBox().input); + + await page.expect(dateRangeBox.option('opened')) + .eql(true); + + await page.click(dateRangeBox.getCalendarCell(10)) + .expect(dateRangeBox.option('value')) + .eql([new Date(2021, 9, 6), new Date(2021, 9, 24)]) + .expect(ClientFunction(() => (window as any).onValueChangedCounter)()) + .eql(1); + + await page.expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.getEndDateBox().isFocused) + .ok(); + + }); + + test('Open by click on startDate input and select date in calendar > startDate, value: ["2021/09/17", "2021/09/28"]', async ({ page }) => { + + await page.evaluate(() => { + (window as any).onValueChangedCounter = 0; + }); + + await createWidget(page, 'dxDateRangeBox', { + value: [new Date(2021, 9, 17), new Date(2021, 9, 28)], + openOnFieldClick: true, + width: 500, + calendarOptions: { + currentDate: new Date(2021, 9, 19), + }, + onValueChanged() { + ((window as any).onValueChangedCounter as number) += 1; + }, + }); + + const dateRangeBox = page.locator('#container'); + + await page.click(dateRangeBox.getStartDateBox().input); + + await page.expect(dateRangeBox.option('opened')) + .eql(true); + + await page.click(dateRangeBox.getCalendarCell(25)) + .expect(dateRangeBox.option('value')) + .eql([new Date(2021, 9, 21), new Date(2021, 9, 28)]) + .expect(ClientFunction(() => (window as any).onValueChangedCounter)()) + .eql(1); + + await page.expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.getEndDateBox().isFocused) + .ok(); + + }); + + test('Open by click on startDate input and select date in calendar > endDate, value: ["2021/09/17", "2021/09/21"]', async ({ page }) => { + + await page.evaluate(() => { + (window as any).onValueChangedCounter = 0; + }); + + await createWidget(page, 'dxDateRangeBox', { + value: [new Date(2021, 9, 17), new Date(2021, 9, 21)], + openOnFieldClick: true, + width: 500, + calendarOptions: { + currentDate: new Date(2021, 9, 19), + }, + onValueChanged() { + ((window as any).onValueChangedCounter as number) += 1; + }, + }); + + const dateRangeBox = page.locator('#container'); + + await page.click(dateRangeBox.getStartDateBox().input); + + await page.expect(dateRangeBox.option('opened')) + .eql(true); + + await page.click(dateRangeBox.getCalendarCell(30)) + .expect(dateRangeBox.option('value')) + .eql([new Date(2021, 9, 26), null]) + .expect(ClientFunction(() => (window as any).onValueChangedCounter)()) + .eql(1); + + await page.expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.getEndDateBox().isFocused) + .ok(); + + await page.click(dateRangeBox.getStartDateBox().input); + + await page.click(dateRangeBox.getCalendarCell(31)) + .expect(dateRangeBox.option('value')) + .eql([new Date(2021, 9, 27), null]) + .expect(ClientFunction(() => (window as any).onValueChangedCounter)()) + .eql(2); + + await page.expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.getEndDateBox().isFocused) + .ok(); + + await page.click(dateRangeBox.getStartDateBox().input); + + await page.click(dateRangeBox.getCalendarCell(32)) + .expect(dateRangeBox.option('value')) + .eql([new Date(2021, 9, 28), null]) + .expect(ClientFunction(() => (window as any).onValueChangedCounter)()) + .eql(3); + + await page.expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.getEndDateBox().isFocused) + .ok(); + + }); + + test('Open by click on endDate input and select date in calendar > endDate, value: ["2021/09/17", "2021/09/24"]', async ({ page }) => { + + await page.evaluate(() => { + (window as any).onValueChangedCounter = 0; + }); + + await createWidget(page, 'dxDateRangeBox', { + value: [new Date(2021, 9, 17), new Date(2021, 9, 24)], + openOnFieldClick: true, + width: 500, + calendarOptions: { + currentDate: new Date(2021, 9, 19), + }, + onValueChanged() { + ((window as any).onValueChangedCounter as number) += 1; + }, + }); + + const dateRangeBox = page.locator('#container'); + + await page.click(dateRangeBox.getEndDateBox().input); + + await page.expect(dateRangeBox.option('opened')) + .eql(true); + + await page.click(dateRangeBox.getCalendarCell(30)) + .expect(dateRangeBox.option('value')) + .eql([new Date(2021, 9, 17), new Date(2021, 9, 26)]) + .expect(ClientFunction(() => (window as any).onValueChangedCounter)()) + .eql(1); + + await page.expect(dateRangeBox.option('opened')) + .eql(false) + .expect(dateRangeBox.getEndDateBox().isFocused) + .ok(); + + }); + + test('Open by click on endDate input and select date in calendar < endDate, value: ["2021/09/17", "2021/09/24"]', async ({ page }) => { + + await page.evaluate(() => { + (window as any).onValueChangedCounter = 0; + }); + + await createWidget(page, 'dxDateRangeBox', { + value: [new Date(2021, 9, 17), new Date(2021, 9, 24)], + openOnFieldClick: true, + width: 500, + calendarOptions: { + currentDate: new Date(2021, 9, 19), + }, + onValueChanged() { + ((window as any).onValueChangedCounter as number) += 1; + }, + }); + + const dateRangeBox = page.locator('#container'); + + await page.click(dateRangeBox.getEndDateBox().input); + + await page.expect(dateRangeBox.option('opened')) + .eql(true); + + await page.click(dateRangeBox.getCalendarCell(25)) + .expect(dateRangeBox.option('value')) + .eql([new Date(2021, 9, 17), new Date(2021, 9, 21)]) + .expect(ClientFunction(() => (window as any).onValueChangedCounter)()) + .eql(1); + + await page.expect(dateRangeBox.option('opened')) + .eql(false) + .expect(dateRangeBox.getEndDateBox().isFocused) + .ok(); + + }); + + test('Open by click on endDate input and select date in calendar < startDate, value: ["2021/09/17", "2021/09/24"]', async ({ page }) => { + + await page.evaluate(() => { + (window as any).onValueChangedCounter = 0; + }); + + await createWidget(page, 'dxDateRangeBox', { + value: [new Date(2021, 9, 17), new Date(2021, 9, 24)], + openOnFieldClick: true, + width: 500, + calendarOptions: { + currentDate: new Date(2021, 9, 19), + }, + onValueChanged() { + ((window as any).onValueChangedCounter as number) += 1; + }, + }); + + const dateRangeBox = page.locator('#container'); + + await page.click(dateRangeBox.getEndDateBox().input); + + await page.expect(dateRangeBox.option('opened')) + .eql(true); + + await page.click(dateRangeBox.getCalendarCell(10)) + .expect(dateRangeBox.option('value')) + .eql([new Date(2021, 9, 6), null]) + .expect(ClientFunction(() => (window as any).onValueChangedCounter)()) + .eql(1); + + await page.expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.getEndDateBox().isFocused) + .ok(); + + await page.click(dateRangeBox.getCalendarCell(9)) + .expect(dateRangeBox.option('value')) + .eql([new Date(2021, 9, 5), null]) + .expect(ClientFunction(() => (window as any).onValueChangedCounter)()) + .eql(2); + + await page.expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.getEndDateBox().isFocused) + .ok(); + + await page.click(dateRangeBox.getCalendarCell(8)) + .expect(dateRangeBox.option('value')) + .eql([new Date(2021, 9, 4), null]) + .expect(ClientFunction(() => (window as any).onValueChangedCounter)()) + .eql(3); + + await page.expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.getEndDateBox().isFocused) + .ok(); + + await page.click(dateRangeBox.getCalendarCell(10)) + .expect(dateRangeBox.option('value')) + .eql([new Date(2021, 9, 4), new Date(2021, 9, 6)]) + .expect(ClientFunction(() => (window as any).onValueChangedCounter)()) + .eql(4); + + await page.expect(dateRangeBox.option('opened')) + .eql(false) + .expect(dateRangeBox.getEndDateBox().isFocused) + .ok(); + + }); + + test('Open by click on endDate input and select date in calendar = endDate, value: ["2021/09/17", "2021/09/24"]', async ({ page }) => { + + await page.evaluate(() => { + (window as any).onValueChangedCounter = 0; + }); + + await createWidget(page, 'dxDateRangeBox', { + value: [new Date(2021, 9, 17), new Date(2021, 9, 24)], + openOnFieldClick: true, + width: 500, + calendarOptions: { + currentDate: new Date(2021, 9, 19), + }, + onValueChanged() { + ((window as any).onValueChangedCounter as number) += 1; + }, + }); + + const dateRangeBox = page.locator('#container'); + + await page.click(dateRangeBox.getEndDateBox().input); + + await page.expect(dateRangeBox.option('opened')) + .eql(true); + + await page.click(dateRangeBox.getCalendarCell(28)) + .expect(dateRangeBox.option('value')) + .eql([new Date(2021, 9, 17), new Date(2021, 9, 24)]) + .expect(ClientFunction(() => (window as any).onValueChangedCounter)()) + .eql(0); + + await page.expect(dateRangeBox.option('opened')) + .eql(false) + .expect(dateRangeBox.getEndDateBox().isFocused) + .ok(); + + }); + + test('Open by click on endDate input and select date in calendar = startDate, value: ["2021/09/17", "2021/09/24"]', async ({ page }) => { + + await page.evaluate(() => { + (window as any).onValueChangedCounter = 0; + }); + + await createWidget(page, 'dxDateRangeBox', { + value: [new Date(2021, 9, 17), new Date(2021, 9, 24)], + openOnFieldClick: true, + width: 500, + calendarOptions: { + currentDate: new Date(2021, 9, 19), + }, + onValueChanged() { + ((window as any).onValueChangedCounter as number) += 1; + }, + }); + + const dateRangeBox = page.locator('#container'); + + await page.click(dateRangeBox.getEndDateBox().input); + + await page.expect(dateRangeBox.option('opened')) + .eql(true); + + await page.click(dateRangeBox.getCalendarCell(21)) + .expect(dateRangeBox.option('value')) + .eql([new Date(2021, 9, 17), new Date(2021, 9, 17)]) + .expect(ClientFunction(() => (window as any).onValueChangedCounter)()) + .eql(1); + + await page.expect(dateRangeBox.option('opened')) + .eql(false) + .expect(dateRangeBox.getEndDateBox().isFocused) + .ok(); + + }); + + test('Open by click on startDate input and select date in calendar = startDate -> endDate, value: ["2021/09/17", "2021/09/24"]', async ({ page }) => { + + await page.evaluate(() => { + (window as any).onValueChangedCounter = 0; + }); + + await createWidget(page, 'dxDateRangeBox', { + value: [new Date(2021, 9, 17), new Date(2021, 9, 24)], + openOnFieldClick: true, + width: 500, + calendarOptions: { + currentDate: new Date(2021, 9, 19), + }, + onValueChanged() { + ((window as any).onValueChangedCounter as number) += 1; + }, + }); + + const dateRangeBox = page.locator('#container'); + + await page.click(dateRangeBox.getStartDateBox().input); + + await page.expect(dateRangeBox.option('opened')) + .eql(true); + + await page.click(dateRangeBox.getCalendarCell(21)) + .expect(dateRangeBox.option('value')) + .eql([new Date(2021, 9, 17), new Date(2021, 9, 24)]) + .expect(ClientFunction(() => (window as any).onValueChangedCounter)()) + .eql(0); + + await page.expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.getEndDateBox().isFocused) + .ok(); + + await page.click(dateRangeBox.getCalendarCell(28)) + .expect(dateRangeBox.option('value')) + .eql([new Date(2021, 9, 17), new Date(2021, 9, 24)]) + .expect(ClientFunction(() => (window as any).onValueChangedCounter)()) + .eql(0); + + await page.expect(dateRangeBox.option('opened')) + .eql(false) + .expect(dateRangeBox.getEndDateBox().isFocused) + .ok(); + + }); + + test('Value in calendar should be updated by click on clear button if popup is open', async ({ page }) => { + + await page.evaluate(() => { + (window as any).onValueChangedCounter = 0; + }); + + await createWidget(page, 'dxDateRangeBox', { + value: [new Date(2021, 9, 17), new Date(2021, 9, 24)], + opened: true, + width: 500, + calendarOptions: { + currentDate: new Date(2021, 9, 19), + }, + showClearButton: true, + onValueChanged() { + ((window as any).onValueChangedCounter as number) += 1; + }, + }); + + const dateRangeBox = page.locator('#container'); + + await page.expect(dateRangeBox.option('opened')) + .eql(true); + + await page.expect(dateRangeBox.option('opened')) + .eql(true) + .click(dateRangeBox.clearButton) + .expect(dateRangeBox.option('value')) + .eql([null, null]) + .expect(dateRangeBox.getCalendar().option('value')) + .eql([null, null]) + .expect(ClientFunction(() => (window as any).onValueChangedCounter)()) + .eql(1); + + }); + + test('Value in calendar should be updated after change start date value by keyboard and click on endDate input if popup is open', async ({ page }) => { + + await page.evaluate(() => { + (window as any).onValueChangedCounter = 0; + }); + + await createWidget(page, 'dxDateRangeBox', { + value: [new Date(2021, 9, 17), new Date(2021, 9, 24)], + opened: false, + width: 500, + calendarOptions: { + currentDate: new Date(2021, 9, 19), + }, + showClearButton: true, + onValueChanged() { + ((window as any).onValueChangedCounter as number) += 1; + }, + }); + + const dateRangeBox = page.locator('#container'); + + await page.click(dateRangeBox.getStartDateBox().input); + + await page.expect(dateRangeBox.option('opened')) + .eql(true); + + await page.keyboard.press('Backspace') + .typeText(dateRangeBox.getStartDateBox().input, '0'); + + await page.expect(dateRangeBox.option('value')) + .eql([new Date(2021, 9, 17), new Date(2021, 9, 24)]) + .expect(dateRangeBox.getCalendar().option('value')) + .eql([new Date(2021, 9, 17), new Date(2021, 9, 24)]) + .expect(ClientFunction(() => (window as any).onValueChangedCounter)()) + .eql(0); + + await page.click(dateRangeBox.getEndDateBox().input); + + await page.expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.option('value')) + .eql([new Date(2020, 9, 17), new Date(2021, 9, 24)]) + .expect(dateRangeBox.getCalendar().option('value')) + .eql([new Date(2020, 9, 17), new Date(2021, 9, 24)]) + .expect(ClientFunction(() => (window as any).onValueChangedCounter)()) + .eql(1); + + }); + + test('Value in calendar should be updated after change start date value by keyboard and press `tab` if popup is open', async ({ page }) => { + + await page.evaluate(() => { + (window as any).onValueChangedCounter = 0; + }); + + await createWidget(page, 'dxDateRangeBox', { + value: [new Date(2021, 9, 17), new Date(2021, 9, 24)], + opened: false, + width: 500, + calendarOptions: { + currentDate: new Date(2021, 9, 19), + }, + showClearButton: true, + onValueChanged() { + ((window as any).onValueChangedCounter as number) += 1; + }, + }); + + const dateRangeBox = page.locator('#container'); + + await page.click(dateRangeBox.getStartDateBox().input); + + await page.expect(dateRangeBox.option('opened')) + .eql(true); + + await page.keyboard.press('Backspace') + .pressKey('backspace') + .typeText(dateRangeBox.getStartDateBox().input, '19'); + + await page.expect(dateRangeBox.option('value')) + .eql([new Date(2021, 9, 17), new Date(2021, 9, 24)]) + .expect(dateRangeBox.getCalendar().option('value')) + .eql([new Date(2021, 9, 17), new Date(2021, 9, 24)]) + .expect(ClientFunction(() => (window as any).onValueChangedCounter)()) + .eql(0); + + await page.keyboard.press('Tab'); + + await page.expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.option('value')) + .eql([new Date(2019, 9, 17), new Date(2021, 9, 24)]) + .expect(dateRangeBox.getCalendar().option('value')) + .eql([new Date(2019, 9, 17), new Date(2021, 9, 24)]) + .expect(ClientFunction(() => (window as any).onValueChangedCounter)()) + .eql(1); + + await dateRangeBox.getEndDateBox().input.fill('10/24/2023') + .click(dateRangeBox.getStartDateBox().input); + + await page.expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.option('value')) + .eql([new Date(2019, 9, 17), new Date(2023, 9, 24)]) + .expect(dateRangeBox.getCalendar().option('value')) + .eql([new Date(2019, 9, 17), new Date(2023, 9, 24)]) + .expect(ClientFunction(() => (window as any).onValueChangedCounter)()) + .eql(2); + + }); + + test('Value should be saved after select range in calendar and click on apply button, value: [null, null]', async ({ page }) => { + + await page.evaluate(() => { + (window as any).onValueChangedCounter = 0; + }); + + await createWidget(page, 'dxDateRangeBox', { + value: [null, null], + openOnFieldClick: true, + applyValueMode: 'useButtons', + width: 500, + calendarOptions: { + currentDate: new Date(2021, 9, 19), + }, + onValueChanged() { + ((window as any).onValueChangedCounter as number) += 1; + }, + }); + + const dateRangeBox = page.locator('#container'); + + await page.click(dateRangeBox.getStartDateBox().input); + + await page.expect(dateRangeBox.option('opened')) + .eql(true); + + await page.click(dateRangeBox.getCalendarCell(10)) + .expect(dateRangeBox.option('value')) + .eql([null, null]) + .expect(ClientFunction(() => (window as any).onValueChangedCounter)()) + .eql(0); + + await page.expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.getEndDateBox().isFocused) + .ok(); + + await page.click(dateRangeBox.getCalendarCell(21)) + .expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.option('value')) + .eql([null, null]) + .expect(ClientFunction(() => (window as any).onValueChangedCounter)()) + .eql(0) + .expect(dateRangeBox.getStartDateBox().isFocused) + .ok(); + + await dateRangeBox.getPopup().getApplyButton().element.click() + .expect(dateRangeBox.option('value')) + .eql([new Date(2021, 9, 6), new Date(2021, 9, 17)]) + .expect(ClientFunction(() => (window as any).onValueChangedCounter)()) + .eql(1) + .expect(dateRangeBox.getStartDateBox().isFocused) + .ok(); + + }); + + test('Value should not be saved after select range and click on cancel button', async ({ page }) => { + + await page.evaluate(() => { + (window as any).onValueChangedCounter = 0; + }); + + await createWidget(page, 'dxDateRangeBox', { + value: [null, null], + openOnFieldClick: true, + applyValueMode: 'useButtons', + width: 500, + calendarOptions: { + currentDate: new Date(2021, 9, 19), + }, + onValueChanged() { + ((window as any).onValueChangedCounter as number) += 1; + }, + }); + + const dateRangeBox = page.locator('#container'); + + await page.click(dateRangeBox.getStartDateBox().input); + + await page.click(dateRangeBox.getCalendarCell(10)) + .click(dateRangeBox.getCalendarCell(21)) + .expect(ClientFunction(() => (window as any).onValueChangedCounter)()) + .eql(0); + + await dateRangeBox.getPopup().getCancelButton().element.click() + .expect(dateRangeBox.option('opened')) + .eql(false) + .expect(dateRangeBox.option('value')) + .eql([null, null]) + .expect(ClientFunction(() => (window as any).onValueChangedCounter)()) + .eql(0); + + }); + + test('Open by click on startDate input and reselect start date in calendar, value: [null, null]', async ({ page }) => { + + await page.evaluate(() => { + (window as any).onValueChangedCounter = 0; + }); + + await createWidget(page, 'dxDateRangeBox', { + value: [null, null], + openOnFieldClick: true, + applyValueMode: 'useButtons', + width: 500, + calendarOptions: { + currentDate: new Date(2021, 9, 19), + }, + onValueChanged() { + ((window as any).onValueChangedCounter as number) += 1; + }, + }); + + const dateRangeBox = page.locator('#container'); + + await page.click(dateRangeBox.getStartDateBox().input); + + await page.expect(dateRangeBox.option('opened')) + .eql(true); + + await page.click(dateRangeBox.getCalendarCell(10)) + .expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.getEndDateBox().isFocused) + .ok() + .expect(dateRangeBox.option('value')) + .eql([null, null]) + .expect(ClientFunction(() => (window as any).onValueChangedCounter)()) + .eql(0); + + await dateRangeBox.getPopup().getApplyButton().element.click() + .expect(dateRangeBox.option('opened')) + .eql(false) + .expect(dateRangeBox.option('value')) + .eql([new Date(2021, 9, 6), null]) + .expect(ClientFunction(() => (window as any).onValueChangedCounter)()) + .eql(1) + .expect(dateRangeBox.getStartDateBox().isFocused) + .ok(); + + await page.click(dateRangeBox.getStartDateBox().input); + + await page.click(dateRangeBox.getCalendarCell(21)) + .expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.getEndDateBox().isFocused) + .ok() + .expect(dateRangeBox.option('value')) + .eql([new Date(2021, 9, 6), null]) + .expect(ClientFunction(() => (window as any).onValueChangedCounter)()) + .eql(1); + + await dateRangeBox.getPopup().getApplyButton().element.click() + .expect(dateRangeBox.option('opened')) + .eql(false) + .expect(dateRangeBox.option('value')) + .eql([new Date(2021, 9, 17), null]) + .expect(ClientFunction(() => (window as any).onValueChangedCounter)()) + .eql(2) + .expect(dateRangeBox.getStartDateBox().isFocused) + .ok(); + + }); + + test('Open by click on endDate input and select date in calendar, value: [null, null]', async ({ page }) => { + + await page.evaluate(() => { + (window as any).onValueChangedCounter = 0; + }); + + await createWidget(page, 'dxDateRangeBox', { + value: [null, null], + openOnFieldClick: true, + applyValueMode: 'useButtons', + width: 500, + calendarOptions: { + currentDate: new Date(2021, 9, 19), + }, + onValueChanged() { + ((window as any).onValueChangedCounter as number) += 1; + }, + }); + + const dateRangeBox = page.locator('#container'); + + await page.click(dateRangeBox.getEndDateBox().input); + + await page.expect(dateRangeBox.option('opened')) + .eql(true); + + await page.click(dateRangeBox.getCalendarCell(21)) + .expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.getStartDateBox().isFocused) + .ok() + .expect(dateRangeBox.option('value')) + .eql([null, null]) + .expect(ClientFunction(() => (window as any).onValueChangedCounter)()) + .eql(0); + + await page.click(dateRangeBox.getCalendarCell(10)) + .expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.getEndDateBox().isFocused) + .ok() + .expect(dateRangeBox.option('value')) + .eql([null, null]) + .expect(ClientFunction(() => (window as any).onValueChangedCounter)()) + .eql(0); + + await page.click(dateRangeBox.getCalendarCell(27)) + .expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.getStartDateBox().isFocused) + .ok() + .expect(dateRangeBox.option('value')) + .eql([null, null]) + .expect(ClientFunction(() => (window as any).onValueChangedCounter)()) + .eql(0); + + await dateRangeBox.getPopup().getApplyButton().element.click() + .expect(dateRangeBox.option('opened')) + .eql(false) + .expect(dateRangeBox.getStartDateBox().isFocused) + .ok() + .expect(dateRangeBox.option('value')) + .eql([new Date(2021, 9, 6), new Date(2021, 9, 23)]) + .expect(ClientFunction(() => (window as any).onValueChangedCounter)()) + .eql(1); + + }); + + test('Open by click on startDate input and select date in calendar < endDate, value: ["2021/09/17", "2021/09/24"]', async ({ page }) => { + + await page.evaluate(() => { + (window as any).onValueChangedCounter = 0; + }); + + await createWidget(page, 'dxDateRangeBox', { + value: [new Date(2021, 9, 17), new Date(2021, 9, 24)], + openOnFieldClick: true, + applyValueMode: 'useButtons', + width: 500, + calendarOptions: { + currentDate: new Date(2021, 9, 19), + }, + onValueChanged() { + ((window as any).onValueChangedCounter as number) += 1; + }, + }); + + const dateRangeBox = page.locator('#container'); + + await page.click(dateRangeBox.getStartDateBox().input); + + await page.expect(dateRangeBox.option('opened')) + .eql(true); + + await page.click(dateRangeBox.getCalendarCell(10)) + .expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.getEndDateBox().isFocused) + .ok() + .expect(dateRangeBox.option('value')) + .eql([new Date(2021, 9, 17), new Date(2021, 9, 24)]) + .expect(ClientFunction(() => (window as any).onValueChangedCounter)()) + .eql(0); + + await dateRangeBox.getPopup().getApplyButton().element.click() + .expect(dateRangeBox.option('opened')) + .eql(false) + .expect(dateRangeBox.getStartDateBox().isFocused) + .ok() + .expect(dateRangeBox.option('value')) + .eql([new Date(2021, 9, 6), new Date(2021, 9, 24)]) + .expect(ClientFunction(() => (window as any).onValueChangedCounter)()) + .eql(1); + + }); + + test('Open by click on startDate input and select date in calendar > startDate, value: ["2021/09/17", "2021/09/28"]', async ({ page }) => { + + await page.evaluate(() => { + (window as any).onValueChangedCounter = 0; + }); + + await createWidget(page, 'dxDateRangeBox', { + value: [new Date(2021, 9, 17), new Date(2021, 9, 28)], + openOnFieldClick: true, + applyValueMode: 'useButtons', + width: 500, + calendarOptions: { + currentDate: new Date(2021, 9, 19), + }, + onValueChanged() { + ((window as any).onValueChangedCounter as number) += 1; + }, + }); + + const dateRangeBox = page.locator('#container'); + + await page.click(dateRangeBox.getStartDateBox().input); + + await page.click(dateRangeBox.getCalendarCell(25)) + .expect(dateRangeBox.getEndDateBox().isFocused) + .ok() + .expect(dateRangeBox.option('value')) + .eql([new Date(2021, 9, 17), new Date(2021, 9, 28)]) + .expect(ClientFunction(() => (window as any).onValueChangedCounter)()) + .eql(0); + + await dateRangeBox.getPopup().getApplyButton().element.click() + .expect(dateRangeBox.option('opened')) + .eql(false) + .expect(dateRangeBox.getStartDateBox().isFocused) + .ok() + .expect(dateRangeBox.option('value')) + .eql([new Date(2021, 9, 21), new Date(2021, 9, 28)]) + .expect(ClientFunction(() => (window as any).onValueChangedCounter)()) + .eql(1); + + }); + + test('Open by click on startDate input and select date in calendar > endDate, value: ["2021/09/17", "2021/09/21"]', async ({ page }) => { + + await page.evaluate(() => { + (window as any).onValueChangedCounter = 0; + }); + + await createWidget(page, 'dxDateRangeBox', { + value: [new Date(2021, 9, 17), new Date(2021, 9, 21)], + openOnFieldClick: true, + width: 500, + applyValueMode: 'useButtons', + calendarOptions: { + currentDate: new Date(2021, 9, 19), + }, + onValueChanged() { + ((window as any).onValueChangedCounter as number) += 1; + }, + }); + + const dateRangeBox = page.locator('#container'); + + await page.click(dateRangeBox.getStartDateBox().input); + + await page.click(dateRangeBox.getCalendarCell(30)) + .expect(dateRangeBox.getEndDateBox().isFocused) + .ok() + .expect(dateRangeBox.option('value')) + .eql([new Date(2021, 9, 17), new Date(2021, 9, 21)]) + .expect(ClientFunction(() => (window as any).onValueChangedCounter)()) + .eql(0); + + await page.click(dateRangeBox.getStartDateBox().input); + + await page.click(dateRangeBox.getCalendarCell(31)) + .expect(dateRangeBox.getEndDateBox().isFocused) + .ok() + .expect(dateRangeBox.option('value')) + .eql([new Date(2021, 9, 17), new Date(2021, 9, 21)]) + .expect(ClientFunction(() => (window as any).onValueChangedCounter)()) + .eql(0); + + await page.click(dateRangeBox.getStartDateBox().input); + + await page.click(dateRangeBox.getCalendarCell(32)) + .expect(dateRangeBox.getEndDateBox().isFocused) + .ok() + .expect(dateRangeBox.option('value')) + .eql([new Date(2021, 9, 17), new Date(2021, 9, 21)]) + .expect(ClientFunction(() => (window as any).onValueChangedCounter)()) + .eql(0); + + await dateRangeBox.getPopup().getApplyButton().element.click() + .expect(dateRangeBox.option('opened')) + .eql(false) + .expect(dateRangeBox.getStartDateBox().isFocused) + .ok() + .expect(dateRangeBox.option('value')) + .eql([new Date(2021, 9, 28), null]) + .expect(ClientFunction(() => (window as any).onValueChangedCounter)()) + .eql(1); + + }); + + test('Open by click on endDate input and select date in calendar > endDate, value: ["2021/09/17", "2021/09/24"]', async ({ page }) => { + + await page.evaluate(() => { + (window as any).onValueChangedCounter = 0; + }); + + await createWidget(page, 'dxDateRangeBox', { + value: [new Date(2021, 9, 17), new Date(2021, 9, 24)], + openOnFieldClick: true, + applyValueMode: 'useButtons', + width: 500, + calendarOptions: { + currentDate: new Date(2021, 9, 19), + }, + onValueChanged() { + ((window as any).onValueChangedCounter as number) += 1; + }, + }); + + const dateRangeBox = page.locator('#container'); + + await page.click(dateRangeBox.getEndDateBox().input); + + await page.click(dateRangeBox.getCalendarCell(30)) + .expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.getStartDateBox().isFocused) + .ok() + .expect(dateRangeBox.option('value')) + .eql([new Date(2021, 9, 17), new Date(2021, 9, 24)]) + .expect(ClientFunction(() => (window as any).onValueChangedCounter)()) + .eql(0); + + await dateRangeBox.getPopup().getApplyButton().element.click() + .expect(dateRangeBox.option('opened')) + .eql(false) + .expect(dateRangeBox.getStartDateBox().isFocused) + .ok() + .expect(dateRangeBox.option('value')) + .eql([new Date(2021, 9, 17), new Date(2021, 9, 26)]) + .expect(ClientFunction(() => (window as any).onValueChangedCounter)()) + .eql(1); + + }); + + test('Open by click on endDate input and select date in calendar < endDate, value: ["2021/09/17", "2021/09/24"]', async ({ page }) => { + + await page.evaluate(() => { + (window as any).onValueChangedCounter = 0; + }); + + await createWidget(page, 'dxDateRangeBox', { + value: [new Date(2021, 9, 17), new Date(2021, 9, 24)], + openOnFieldClick: true, + applyValueMode: 'useButtons', + width: 500, + calendarOptions: { + currentDate: new Date(2021, 9, 19), + }, + onValueChanged() { + ((window as any).onValueChangedCounter as number) += 1; + }, + }); + + const dateRangeBox = page.locator('#container'); + + await page.click(dateRangeBox.getEndDateBox().input); + + await page.click(dateRangeBox.getCalendarCell(25)) + .expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.getStartDateBox().isFocused) + .ok() + .expect(dateRangeBox.option('value')) + .eql([new Date(2021, 9, 17), new Date(2021, 9, 24)]) + .expect(ClientFunction(() => (window as any).onValueChangedCounter)()) + .eql(0); + + await dateRangeBox.getPopup().getApplyButton().element.click() + .expect(dateRangeBox.option('opened')) + .eql(false) + .expect(dateRangeBox.getStartDateBox().isFocused) + .ok() + .expect(dateRangeBox.option('value')) + .eql([new Date(2021, 9, 17), new Date(2021, 9, 21)]) + .expect(ClientFunction(() => (window as any).onValueChangedCounter)()) + .eql(1); + + }); + + test('Open by click on endDate input and select date in calendar < startDate, value: ["2021/09/17", "2021/09/24"]', async ({ page }) => { + + await page.evaluate(() => { + (window as any).onValueChangedCounter = 0; + }); + + await createWidget(page, 'dxDateRangeBox', { + value: [new Date(2021, 9, 17), new Date(2021, 9, 24)], + openOnFieldClick: true, + applyValueMode: 'useButtons', + width: 500, + calendarOptions: { + currentDate: new Date(2021, 9, 19), + }, + onValueChanged() { + ((window as any).onValueChangedCounter as number) += 1; + }, + }); + + const dateRangeBox = page.locator('#container'); + + await page.click(dateRangeBox.getEndDateBox().input); + + await page.click(dateRangeBox.getCalendarCell(10)) + .expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.getEndDateBox().isFocused) + .ok() + .expect(dateRangeBox.option('value')) + .eql([new Date(2021, 9, 17), new Date(2021, 9, 24)]) + .expect(ClientFunction(() => (window as any).onValueChangedCounter)()) + .eql(0); + + await page.click(dateRangeBox.getCalendarCell(9)) + .expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.getEndDateBox().isFocused) + .ok() + .expect(dateRangeBox.option('value')) + .eql([new Date(2021, 9, 17), new Date(2021, 9, 24)]) + .expect(ClientFunction(() => (window as any).onValueChangedCounter)()) + .eql(0); + + await page.click(dateRangeBox.getCalendarCell(8)) + .expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.getEndDateBox().isFocused) + .ok() + .expect(dateRangeBox.option('value')) + .eql([new Date(2021, 9, 17), new Date(2021, 9, 24)]) + .expect(ClientFunction(() => (window as any).onValueChangedCounter)()) + .eql(0); + + await page.click(dateRangeBox.getCalendarCell(10)) + .expect(dateRangeBox.option('value')) + .eql([new Date(2021, 9, 17), new Date(2021, 9, 24)]) + .expect(ClientFunction(() => (window as any).onValueChangedCounter)()) + .eql(0); + + await dateRangeBox.getPopup().getApplyButton().element.click() + .expect(dateRangeBox.option('opened')) + .eql(false) + .expect(dateRangeBox.getStartDateBox().isFocused) + .ok() + .expect(dateRangeBox.option('value')) + .eql([new Date(2021, 9, 4), new Date(2021, 9, 6)]) + .expect(ClientFunction(() => (window as any).onValueChangedCounter)()) + .eql(1); + + }); + + test('Open by click on endDate input and select date in calendar = endDate, value: ["2021/09/17", "2021/09/24"]', async ({ page }) => { + + await page.evaluate(() => { + (window as any).onValueChangedCounter = 0; + }); + + await createWidget(page, 'dxDateRangeBox', { + value: [new Date(2021, 9, 17), new Date(2021, 9, 24)], + openOnFieldClick: true, + applyValueMode: 'useButtons', + width: 500, + calendarOptions: { + currentDate: new Date(2021, 9, 19), + }, + onValueChanged() { + ((window as any).onValueChangedCounter as number) += 1; + }, + }); + + const dateRangeBox = page.locator('#container'); + + await page.click(dateRangeBox.getEndDateBox().input); + + await page.click(dateRangeBox.getCalendarCell(28)) + .expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.getStartDateBox().isFocused) + .ok() + .expect(dateRangeBox.option('value')) + .eql([new Date(2021, 9, 17), new Date(2021, 9, 24)]) + .expect(ClientFunction(() => (window as any).onValueChangedCounter)()) + .eql(0); + + await dateRangeBox.getPopup().getApplyButton().element.click() + .expect(dateRangeBox.option('opened')) + .eql(false) + .expect(dateRangeBox.getStartDateBox().isFocused) + .ok() + .expect(dateRangeBox.option('value')) + .eql([new Date(2021, 9, 17), new Date(2021, 9, 24)]) + .expect(ClientFunction(() => (window as any).onValueChangedCounter)()) + .eql(0); + + }); + + test('Open by click on endDate input and select date in calendar = startDate, value: ["2021/09/17", "2021/09/24"]', async ({ page }) => { + + await page.evaluate(() => { + (window as any).onValueChangedCounter = 0; + }); + + await createWidget(page, 'dxDateRangeBox', { + value: [new Date(2021, 9, 17), new Date(2021, 9, 24)], + openOnFieldClick: true, + applyValueMode: 'useButtons', + width: 500, + calendarOptions: { + currentDate: new Date(2021, 9, 19), + }, + onValueChanged() { + ((window as any).onValueChangedCounter as number) += 1; + }, + }); + + const dateRangeBox = page.locator('#container'); + + await page.click(dateRangeBox.getEndDateBox().input); + + await page.expect(dateRangeBox.option('opened')) + .eql(true); + + await page.click(dateRangeBox.getCalendarCell(21)) + .expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.getStartDateBox().isFocused) + .ok() + .expect(dateRangeBox.option('value')) + .eql([new Date(2021, 9, 17), new Date(2021, 9, 24)]) + .expect(ClientFunction(() => (window as any).onValueChangedCounter)()) + .eql(0); + + await dateRangeBox.getPopup().getApplyButton().element.click() + .expect(dateRangeBox.option('opened')) + .eql(false) + .expect(dateRangeBox.getStartDateBox().isFocused) + .ok() + .expect(dateRangeBox.option('value')) + .eql([new Date(2021, 9, 17), new Date(2021, 9, 17)]) + .expect(ClientFunction(() => (window as any).onValueChangedCounter)()) + .eql(1); + + }); + + test('Open by click on startDate input and select date in calendar = startDate -> endDate, value: ["2021/09/17", "2021/09/24"]', async ({ page }) => { + + await page.evaluate(() => { + (window as any).onValueChangedCounter = 0; + }); + + await createWidget(page, 'dxDateRangeBox', { + value: [new Date(2021, 9, 17), new Date(2021, 9, 24)], + openOnFieldClick: true, + applyValueMode: 'useButtons', + width: 500, + calendarOptions: { + currentDate: new Date(2021, 9, 19), + }, + onValueChanged() { + ((window as any).onValueChangedCounter as number) += 1; + }, + }); + + const dateRangeBox = page.locator('#container'); + + await page.click(dateRangeBox.getStartDateBox().input); + + await page.click(dateRangeBox.getCalendarCell(21)) + .expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.getEndDateBox().isFocused) + .ok() + .expect(dateRangeBox.option('value')) + .eql([new Date(2021, 9, 17), new Date(2021, 9, 24)]) + .expect(ClientFunction(() => (window as any).onValueChangedCounter)()) + .eql(0); + + await page.click(dateRangeBox.getCalendarCell(28)) + .expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.getStartDateBox().isFocused) + .ok() + .expect(dateRangeBox.option('value')) + .eql([new Date(2021, 9, 17), new Date(2021, 9, 24)]) + .expect(ClientFunction(() => (window as any).onValueChangedCounter)()) + .eql(0); + + await dateRangeBox.getPopup().getApplyButton().element.click() + .expect(dateRangeBox.option('opened')) + .eql(false) + .expect(dateRangeBox.getStartDateBox().isFocused) + .ok() + .expect(dateRangeBox.option('value')) + .eql([new Date(2021, 9, 17), new Date(2021, 9, 24)]) + .expect(ClientFunction(() => (window as any).onValueChangedCounter)()) + .eql(0); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/editors/dateRangeBox/calendar.spec.ts b/e2e/testcafe-devextreme/playwright-tests/editors/dateRangeBox/calendar.spec.ts new file mode 100644 index 000000000000..1c3c94d84da5 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/editors/dateRangeBox/calendar.spec.ts @@ -0,0 +1,673 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, appendElementTo, setAttribute } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('DateRangeBox range selection', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + const STATE_HOVER_CLASS = 'dx-state-hover'; + + test('DateRangeBox calendar appearance after change rtl mode in runtime', async ({ page }) => { + + await appendElementTo(page, '#container', 'div', 'dateRangeBox'); + await setAttribute(page, '#container', 'style', 'width: 800px; height: 500px; padding-top: 10px;'); + + await createWidget(page, 'dxDateRangeBox', { + value: [new Date(2021, 9, 17), new Date(2021, 10, 30)], + openOnFieldClick: true, + opened: true, + }, '#dateRangeBox'); + + const dateRangeBox = page.locator('#dateRangeBox'); + + await dateRangeBox.option('rtlEnabled', true); + + await testScreenshot(page, 'DRB appearance after change rtl mode in runtime.png', { element: '#container' }); + + }); + + test('Cells classes after hover cells, value: [null, null]', async ({ page }) => { + + await appendElementTo(page, '#container', 'div', 'dateRangeBox'); + await setAttribute(page, '#container', 'style', 'width: 800px; height: 500px;'); + + await createWidget(page, 'dxDateRangeBox', { + value: [null, null], + openOnFieldClick: true, + width: 500, + calendarOptions: { + currentDate: new Date(2021, 9, 19), + }, + }, '#dateRangeBox'); + + const dateRangeBox = page.locator('#dateRangeBox'); + + await page.click(dateRangeBox.getStartDateBox().input); + + const calendar = dateRangeBox.getCalendar(); + + await page.expect(calendar.getSelectedRangeCells().count) + .eql(0) + .expect(calendar.getSelectedRangeStartCell().count) + .eql(0) + .expect(calendar.getSelectedRangeEndCell().count) + .eql(0) + .expect(calendar.getHoveredRangeCells().count) + .eql(0) + .expect(calendar.getHoveredRangeStartCell().count) + .eql(0) + .expect(calendar.getHoveredRangeEndCell().count) + .eql(0); + + await page.hover(calendar.getCellByDate('2021/10/12')); + + await page.expect(calendar.getSelectedRangeCells().count) + .eql(0) + .expect(calendar.getSelectedRangeStartCell().count) + .eql(0) + .expect(calendar.getSelectedRangeEndCell().count) + .eql(0) + .expect(calendar.getHoveredRangeCells().count) + .eql(0) + .expect(calendar.getHoveredRangeStartCell().count) + .eql(0) + .expect(calendar.getHoveredRangeEndCell().count) + .eql(0); + + await page.hover(calendar.getCellByDate('2021/10/25')); + + await page.expect(calendar.getSelectedRangeCells().count) + .eql(0) + .expect(calendar.getSelectedRangeStartCell().count) + .eql(0) + .expect(calendar.getSelectedRangeEndCell().count) + .eql(0) + .expect(calendar.getHoveredRangeCells().count) + .eql(0) + .expect(calendar.getHoveredRangeStartCell().count) + .eql(0) + .expect(calendar.getHoveredRangeEndCell().count) + .eql(0); + + }); + + test('Cells classes after hover date < startDate & date > startDate, currentSelection: startDate, value: [new Date(2021, 9, 17), null]', async ({ page }) => { + + await appendElementTo(page, '#container', 'div', 'dateRangeBox'); + await setAttribute(page, '#container', 'style', 'width: 800px; height: 500px;'); + + await createWidget(page, 'dxDateRangeBox', { + value: [new Date(2021, 9, 17), null], + openOnFieldClick: true, + width: 500, + calendarOptions: { + currentDate: new Date(2021, 9, 19), + }, + }, '#dateRangeBox'); + + const dateRangeBox = page.locator('#dateRangeBox'); + + await page.click(dateRangeBox.getStartDateBox().input); + + const calendar = dateRangeBox.getCalendar(); + + await page.expect(calendar.getSelectedRangeCells().count) + .eql(0) + .expect(calendar.getSelectedRangeStartCell().count) + .eql(1) + .expect(calendar.getSelectedRangeEndCell().count) + .eql(0) + .expect(calendar.getHoveredRangeCells().count) + .eql(0) + .expect(calendar.getHoveredRangeStartCell().count) + .eql(0) + .expect(calendar.getHoveredRangeEndCell().count) + .eql(0); + + await page.hover(calendar.getCellByDate('2021/10/12')); + + await page.expect(calendar.getSelectedRangeCells().count) + .eql(0) + .expect(calendar.getSelectedRangeStartCell().count) + .eql(1) + .expect(calendar.getSelectedRangeEndCell().count) + .eql(0) + .expect(calendar.getHoveredRangeCells().count) + .eql(0) + .expect(calendar.getHoveredRangeStartCell().count) + .eql(0) + .expect(calendar.getHoveredRangeEndCell().count) + .eql(0); + + await page.hover(calendar.getCellByDate('2021/10/25')); + + await page.expect(calendar.getSelectedRangeCells().count) + .eql(0) + .expect(calendar.getSelectedRangeStartCell().count) + .eql(1) + .expect(calendar.getSelectedRangeEndCell().count) + .eql(0) + .expect(calendar.getHoveredRangeCells().count) + .eql(0) + .expect(calendar.getHoveredRangeStartCell().count) + .eql(0) + .expect(calendar.getHoveredRangeEndCell().count) + .eql(0); + + }); + + test('Cells classes after hover date < startDate, currentSelection: endDate, value: [new Date(2021, 9, 17), null]', async ({ page }) => { + + await appendElementTo(page, '#container', 'div', 'dateRangeBox'); + await setAttribute(page, '#container', 'style', 'width: 800px; height: 500px;'); + + await createWidget(page, 'dxDateRangeBox', { + value: [new Date(2021, 9, 17), null], + openOnFieldClick: true, + width: 500, + calendarOptions: { + currentDate: new Date(2021, 9, 19), + }, + }, '#dateRangeBox'); + + const dateRangeBox = page.locator('#dateRangeBox'); + + await page.click(dateRangeBox.getEndDateBox().input); + + const calendar = dateRangeBox.getCalendar(); + + await page.expect(calendar.getSelectedRangeCells().count) + .eql(0) + .expect(calendar.getSelectedRangeStartCell().count) + .eql(1) + .expect(calendar.getSelectedRangeEndCell().count) + .eql(0) + .expect(calendar.getHoveredRangeCells().count) + .eql(0) + .expect(calendar.getHoveredRangeStartCell().count) + .eql(0) + .expect(calendar.getHoveredRangeEndCell().count) + .eql(0); + + await page.hover(calendar.getCellByDate('2021/10/12')); + + await page.expect(calendar.getSelectedRangeCells().count) + .eql(0) + .expect(calendar.getSelectedRangeStartCell().count) + .eql(1) + .expect(calendar.getSelectedRangeEndCell().count) + .eql(0) + .expect(calendar.getHoveredRangeCells().count) + .eql(0) + .expect(calendar.getHoveredRangeStartCell().count) + .eql(0) + .expect(calendar.getHoveredRangeEndCell().count) + .eql(0); + + }); + + test('Cells classes after hover and select date > startDate, currentSelection: endDate, value: [new Date(2021, 9, 17), null]', async ({ page }) => { + + await appendElementTo(page, '#container', 'div', 'dateRangeBox'); + await setAttribute(page, '#container', 'style', 'width: 800px; height: 500px; padding-top: 10px;'); + + await createWidget(page, 'dxDateRangeBox', { + value: [new Date(2021, 9, 17), null], + openOnFieldClick: true, + width: 500, + calendarOptions: { + currentDate: new Date(2021, 9, 19), + }, + }, '#dateRangeBox'); + + const dateRangeBox = page.locator('#dateRangeBox'); + + await page.click(dateRangeBox.getEndDateBox().input); + + const calendar = dateRangeBox.getCalendar(); + + await page.expect(calendar.getSelectedRangeCells().count) + .eql(0) + .expect(calendar.getSelectedRangeStartCell().count) + .eql(1) + .expect(calendar.getSelectedRangeEndCell().count) + .eql(0) + .expect(calendar.getHoveredRangeCells().count) + .eql(0) + .expect(calendar.getHoveredRangeStartCell().count) + .eql(0) + .expect(calendar.getHoveredRangeEndCell().count) + .eql(0); + + await page.hover(calendar.getCellByDate('2021/10/24')); + + await page.expect(calendar.getSelectedRangeCells().count) + .eql(0) + .expect(calendar.getSelectedRangeStartCell().count) + .eql(1) + .expect(calendar.getSelectedRangeEndCell().count) + .eql(0) + .expect(calendar.getHoveredRangeCells().count) + .eql(8) + .expect(calendar.getHoveredRangeStartCell().count) + .eql(1) + .expect(calendar.getHoveredRangeEndCell().count) + .eql(1); + + }); + + test('Selected range if endDate = startDate, currentSelection: startDate', async ({ page }) => { + + await appendElementTo(page, '#container', 'div', 'dateRangeBox'); + await setAttribute(page, '#container', 'style', 'width: 800px; height: 500px; padding-top: 10px;'); + + await createWidget(page, 'dxDateRangeBox', { + value: [new Date(2021, 9, 18), new Date(2021, 9, 18)], + openOnFieldClick: true, + width: 500, + calendarOptions: { + currentDate: new Date(2021, 9, 19), + }, + }, '#dateRangeBox'); + + const dateRangeBox = page.locator('#dateRangeBox'); + + await page.click(dateRangeBox.getEndDateBox().input); + + const calendar = dateRangeBox.getCalendar(); + + await page.expect(calendar.getSelectedRangeCells().count) + .eql(1) + .expect(calendar.getSelectedRangeStartCell().count) + .eql(1) + .expect(calendar.getSelectedRangeEndCell().count) + .eql(1) + .expect(calendar.getHoveredRangeCells().count) + .eql(0) + .expect(calendar.getHoveredRangeStartCell().count) + .eql(0) + .expect(calendar.getHoveredRangeEndCell().count) + .eql(0); + + await testScreenshot(page, 'DRB range, endDate = startDate.png', { element: '#container' }); + + await page.hover(calendar.getCellByDate('2021/10/18')); + + await page.expect(calendar.getSelectedRangeCells().count) + .eql(1) + .expect(calendar.getSelectedRangeStartCell().count) + .eql(1) + .expect(calendar.getSelectedRangeEndCell().count) + .eql(1) + .expect(calendar.getHoveredRangeCells().count) + .eql(0) + .expect(calendar.getHoveredRangeStartCell().count) + .eql(0) + .expect(calendar.getHoveredRangeEndCell().count) + .eql(0); + + }); + + test('Start date cell in selected range', async ({ page }) => { + + await appendElementTo(page, '#container', 'div', 'dateRangeBox'); + await setAttribute(page, '#container', 'style', 'width: 800px; height: 500px; padding-top: 10px;'); + + await createWidget(page, 'dxDateRangeBox', { + value: [new Date(2021, 9, 17), new Date(2021, 10, 6)], + openOnFieldClick: true, + width: 500, + }, '#dateRangeBox'); + + const dateRangeBox = page.locator('#dateRangeBox'); + + await page.click(dateRangeBox.getStartDateBox().input); + + const calendar = dateRangeBox.getCalendar(); + + await page.hover(calendar.getCellByDate('2021/10/01')); + + await testScreenshot(page, 'DRB range, startDate is start in row, hover is start in view.png', { element: '#container' }); + + await page.click(calendar.getCellByDate('2021/10/31')) + .click(dateRangeBox.getStartDateBox().input) + .hover(calendar.getCellByDate('2021/10/16')); + + await testScreenshot(page, 'DRB range, startDate is end in view & start row, hover is end row.png', { element: '#container' }); + + await page.click(calendar.getCellByDate('2021/10/23')) + .click(dateRangeBox.getStartDateBox().input) + .hover(calendar.getCellByDate('2021/10/03')); + + await testScreenshot(page, 'DRB range, startDate is end cell row, hover is start in row.png', { element: '#container' }); + + await dateRangeBox.getCalendar().option('currentDate', new Date(2021, 8, 1)); + + await page.click(calendar.getCellByDate('2021/10/01')) + .click(dateRangeBox.getStartDateBox().input); + + await dateRangeBox.getCalendar().option('currentDate', new Date(2021, 8, 1)); + + await page.hover(calendar.getCellByDate('2021/09/30')); + + await testScreenshot(page, 'DRB range, startDate is start in view, hover is end in view.png', { element: '#container' }); + + await dateRangeBox.getCalendar().option('currentDate', new Date(2021, 8, 1)); + + await page.click(calendar.getCellByDate('2021/09/30')) + .click(dateRangeBox.getStartDateBox().input) + .hover(calendar.getCellByDate('2021/09/15')); + + await testScreenshot(page, 'DRB range, startDate is end in view, hover inside row.png', { element: '#container' }); + + await dateRangeBox.getCalendar().option('currentDate', new Date(2021, 7, 1)); + + await page.click(calendar.getCellByDate('2021/09/15')) + .click(dateRangeBox.getStartDateBox().input); + + await dateRangeBox.getCalendar().option('currentDate', new Date(2021, 7, 1)); + + await page.hover(calendar.getCellByDate('2021/08/01')); + + await testScreenshot(page, 'DRB range, startDate inside row, hover is start in view & row.png', { element: '#container' }); + + await dateRangeBox.getCalendar().option('currentDate', new Date(2021, 6, 1)); + + await page.click(calendar.getCellByDate('2021/08/01')) + .click(dateRangeBox.getStartDateBox().input); + + await dateRangeBox.getCalendar().option('currentDate', new Date(2021, 6, 1)); + + await page.hover(calendar.getCellByDate('2021/07/31')); + + await testScreenshot(page, 'DRB range, startDate is start view & row, hover is end view & row.png', { element: '#container' }); + + await page.click(calendar.getCellByDate('2021/07/31')) + .click(dateRangeBox.getStartDateBox().input); + + await dateRangeBox.getCalendar().option('currentDate', new Date(2021, 6, 1)); + + await page.hover(calendar.getCellByDate('2021/07/02')); + + await testScreenshot(page, 'DRB range, startDate is end in view & row, hover inside row.png', { element: '#container' }); + + await dateRangeBox.getCalendar().option('currentDate', new Date(2021, 4, 1)); + + await page.hover(calendar.getCellByDate('2021/05/01')); + + await testScreenshot(page, 'DRB range, hover is start in view & end cell row.png', { element: '#container' }); + + await page.click(calendar.getCellByDate('2021/05/01')) + .click(dateRangeBox.getStartDateBox().input); + + await testScreenshot(page, 'DRB range, startDate cell is start in view & end cell row.png', { element: '#container' }); + + await dateRangeBox.getCalendar().option('currentDate', new Date(2021, 1, 1)); + + await page.hover(calendar.getCellByDate('2021/02/28')); + + await testScreenshot(page, 'DRB range, hover is end in view & start in row.png', { element: '#container' }); + + }); + + test('End date cell in selected range', async ({ page }) => { + + await appendElementTo(page, '#container', 'div', 'dateRangeBox'); + await setAttribute(page, '#container', 'style', 'width: 800px; height: 500px; padding-top: 10px;'); + + await createWidget(page, 'dxDateRangeBox', { + value: [new Date(2021, 9, 17), new Date(2021, 9, 23)], + openOnFieldClick: true, + width: 500, + }, '#dateRangeBox'); + + const dateRangeBox = page.locator('#dateRangeBox'); + + await page.click(dateRangeBox.getEndDateBox().input); + + const calendar = dateRangeBox.getCalendar(); + + await page.click(calendar.getCellByDate('2021/10/24')) + .click(dateRangeBox.getEndDateBox().input) + .hover(calendar.getCellByDate('2021/10/31')); + + await testScreenshot(page, 'DRB range, endDate is start in row, hover is end view & start row.png', { element: '#container' }); + + await page.click(calendar.getCellByDate('2021/10/25')) + .click(dateRangeBox.getEndDateBox().input) + .hover(calendar.getCellByDate('2021/11/01')); + + await testScreenshot(page, 'DRB range, endDate is cell inside row, hover is start in view.png', { element: '#container' }); + + await page.click(calendar.getCellByDate('2021/10/30')) + .click(dateRangeBox.getEndDateBox().input) + .hover(calendar.getCellByDate('2021/11/30')); + + await testScreenshot(page, 'DRB range, endDate is end cell row, hover is end in view.png', { element: '#container' }); + + await page.click(calendar.getCellByDate('2021/10/31')) + .click(dateRangeBox.getEndDateBox().input) + .hover(calendar.getCellByDate('2021/11/21')); + + await testScreenshot(page, 'DRB range, endDate is end in view & start row, hover is start row.png', { element: '#container' }); + + await page.click(calendar.getCellByDate('2021/11/01')) + .click(dateRangeBox.getEndDateBox().input) + .hover(calendar.getCellByDate('2021/11/21')); + + await testScreenshot(page, 'DRB range, endDate is start in view, hover is end in row.png', { element: '#container' }); + + await dateRangeBox.getCalendar().option('currentDate', new Date(2021, 11, 15)); + + await page.click(calendar.getCellByDate('2021/12/31')) + .click(dateRangeBox.getEndDateBox().input); + + await dateRangeBox.getCalendar().option('currentDate', new Date(2021, 12, 15)); + + await page.hover(calendar.getCellByDate('2022/01/01')); + + await testScreenshot(page, 'DRB range, endDate is end in view, hover is start view & end row.png', { element: '#container' }); + + await page.click(calendar.getCellByDate('2022/01/01')) + .click(dateRangeBox.getEndDateBox().input); + + await dateRangeBox.getCalendar().option('currentDate', new Date(2021, 12, 15)); + + await page.hover(calendar.getCellByDate('2022/01/25')); + + await dateRangeBox.getCalendar().option('currentDate', new Date(2021, 12, 15)); + + await testScreenshot(page, 'DRB range, endDate is start view & end cell row, hover inside row.png', { element: '#container' }); + + await dateRangeBox.getCalendar().option('currentDate', new Date(2022, 3, 15)); + + await page.hover(calendar.getCellByDate('2022/04/30')); + + await testScreenshot(page, 'DRB range, hover is end in view & end cell row.png', { element: '#container' }); + + await page.click(calendar.getCellByDate('2022/04/30')) + .click(dateRangeBox.getEndDateBox().input); + + await dateRangeBox.getCalendar().option('currentDate', new Date(2022, 2, 15)); + await testScreenshot(page, 'DRB range, endDate is end in view & end cell row.png', { element: '#container' }); + + await dateRangeBox.getCalendar().option('currentDate', new Date(2022, 3, 15)); + + await page.hover(calendar.getCellByDate('2022/05/01')); + + await testScreenshot(page, 'DRB range, hover is start in view & start in row.png', { element: '#container' }); + + await page.click(calendar.getCellByDate('2022/05/01')) + .click(dateRangeBox.getEndDateBox().input); + + await dateRangeBox.getCalendar().option('currentDate', new Date(2022, 3, 15)); + + await testScreenshot(page, 'DRB range, endDate is start in view & start in row.png', { element: '#container' }); + + }); + + test('Cell in range', async ({ page }) => { + + await appendElementTo(page, '#container', 'div', 'dateRangeBox'); + await setAttribute(page, '#container', 'style', 'width: 800px; height: 500px; padding-top: 10px;'); + + await createWidget(page, 'dxDateRangeBox', { + value: [new Date(2020, 2, 2), new Date(2024, 2, 2)], + openOnFieldClick: true, + opened: true, + width: 500, + }, '#dateRangeBox'); + + const dateRangeBox = page.locator('#dateRangeBox'); + + await dateRangeBox.getCalendar().option('currentDate', new Date(2023, 2, 1)); + + await testScreenshot(page, 'DRB range cells, start in view and end in row & vise versa.png', { element: '#container' }); + + await dateRangeBox.getCalendar().option('currentDate', new Date(2021, 6, 1)); + + await testScreenshot(page, 'DRB range cells, start in view and in row & end in view and in row.png', { element: '#container' }); + + }); + + test('Disabled dates on start date select (disableOutOfRangeSelection: true)', async ({ page }) => { + + await appendElementTo(page, '#container', 'div', 'dateRangeBox'); + await setAttribute(page, '#container', 'style', 'width: 800px; height: 500px; padding-top: 10px;'); + + await createWidget(page, 'dxDateRangeBox', { + width: 500, + disableOutOfRangeSelection: true, + calendarOptions: { + currentDate: new Date('2020/02/20'), + }, + }, '#dateRangeBox'); + + const dateRangeBox = page.locator('#dateRangeBox'); + + await page.click(dateRangeBox.getStartDateBox().input); + + const calendar = dateRangeBox.getCalendar(); + + await page.click(calendar.getCellByDate('2020/02/20')); + + await testScreenshot(page, 'DRB disabled dates before start date select.png', { element: '#container' }); + + }); + + test('Disabled dates on end date select (disableOutOfRangeSelection: true)', async ({ page }) => { + + await appendElementTo(page, '#container', 'div', 'dateRangeBox'); + await setAttribute(page, '#container', 'style', 'width: 800px; height: 500px; padding-top: 10px;'); + + await createWidget(page, 'dxDateRangeBox', { + width: 500, + disableOutOfRangeSelection: true, + calendarOptions: { + currentDate: new Date('2020/02/20'), + }, + }, '#dateRangeBox'); + + const dateRangeBox = page.locator('#dateRangeBox'); + + await page.click(dateRangeBox.getEndDateBox().input); + + const calendar = dateRangeBox.getCalendar(); + + await page.click(calendar.getCellByDate('2020/02/22')); + + await testScreenshot(page, 'DRB disabled dates after end date select.png', { element: '#container' }); + + }); + + test('Disabled dates on inputs focus (disableOutOfRangeSelection: true)', async ({ page }) => { + + await appendElementTo(page, '#container', 'div', 'dateRangeBox'); + await setAttribute(page, '#container', 'style', 'width: 800px; height: 500px; padding-top: 10px;'); + + await createWidget(page, 'dxDateRangeBox', { + value: [new Date('2020/02/20'), new Date('2020/02/22')], + width: 500, + disableOutOfRangeSelection: true, + }, '#dateRangeBox'); + + const dateRangeBox = page.locator('#dateRangeBox'); + + await page.click(dateRangeBox.getStartDateBox().input) + .hover(dateRangeBox.getStartDateBox().input); + + await testScreenshot(page, 'DRB disabled dates on popup opening.png', { element: '#container' }); + + await page.click(dateRangeBox.getEndDateBox().input) + .hover(dateRangeBox.getEndDateBox().input); + + await testScreenshot(page, 'DRB disabled dates on end date input focus.png', { element: '#container' }); + + await page.click(dateRangeBox.getStartDateBox().input) + .hover(dateRangeBox.getStartDateBox().input); + + await testScreenshot(page, 'DRB disabled dates on start date input focus.png', { element: '#container' }); + + }); + + test(`Hovered cell should have "${STATE_HOVER_CLASS}" class after one date selected (disableOutOfRangeSelection=true)`, async ({ page }) => { + await createWidget(page, 'dxDateRangeBox', { + disableOutOfRangeSelection: true, + calendarOptions: { + currentDate: new Date('2020/02/20'), + }, + }, '#container'); + + const dateRangeBox = page.locator('#container'); + + await page.click(dateRangeBox.getStartDateBox().input); + + const calendar = dateRangeBox.getCalendar(); + + await page.click(calendar.getCellByDate('2020/02/20')); + + const targetCell = calendar.getView().getCellByDate(new Date('2020/02/22')); + await targetCell.hover() + .expect(targetCell.hasClass(STATE_HOVER_CLASS)) + .eql(true); + + }); + + test('Dates selection with focusStateEnabled=false', async ({ page }) => { + + await appendElementTo(page, '#container', 'div', 'dateRangeBox'); + + await createWidget(page, 'dxDateRangeBox', { + value: [new Date('2020/02/20'), new Date('2020/02/22')], + width: 500, + focusStateEnabled: false, + }, '#dateRangeBox'); + + const dateRangeBox = page.locator('#dateRangeBox'); + const calendar = dateRangeBox.getCalendar(); + + await page.click(dateRangeBox.getStartDateBox().input); + + await page.click(calendar.getCellByDate('2020/02/10')); + + await page.click(calendar.getCellByDate('2020/02/25')); + + const expectedStartDate = new Date('2020/02/10'); + const expectedEndDate = new Date('2020/02/25'); + + await page.expect(dateRangeBox.option('opened')) + .eql(false) + .expect(dateRangeBox.option('value')) + .eql([expectedStartDate, expectedEndDate]); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/editors/dateRangeBox/common.spec.ts b/e2e/testcafe-devextreme/playwright-tests/editors/dateRangeBox/common.spec.ts new file mode 100644 index 000000000000..64edf65b36d6 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/editors/dateRangeBox/common.spec.ts @@ -0,0 +1,136 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, appendElementTo, setAttribute, setClassAttribute, insertStylesheetRulesToPage } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('DateRangeBox render', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + const DATERANGEBOX_CLASS = 'dx-daterangebox'; + const DROP_DOWN_EDITOR_ACTIVE_CLASS = 'dx-dropdowneditor-active'; + const FOCUSED_STATE_CLASS = 'dx-state-focused'; + const HOVER_STATE_CLASS = 'dx-state-hover'; + const READONLY_STATE_CLASS = 'dx-state-readonly'; + const DISABLED_STATE_CLASS = 'dx-state-disabled'; + + const stylingModes: EditorStyle[] = ['outlined', 'underlined', 'filled']; + const labelModes: LabelMode[] = ['static', 'floating', 'hidden', 'outside']; + + const TEST_VALUE = [new Date(2021, 9, 17, 16, 34), new Date(2021, 9, 18, 16, 34)]; + + const createDateRangeBox = async ( + options?: DateRangeBoxProperties, + state?: string, + ): Promise => { + const id = `${`dx${new Guid()}`}`; + + await appendElementTo(page, '#container', 'div', id, { }); + + const config: any = { + width: 500, + labelMode: 'static', + endDateLabel: 'static', + startDateLabel: 'qwertyQWERTYg', + showClearButton: true, + ...options, + }; + + await createWidget(page, 'dxDateRangeBox', config, `#${id}`); + + if (state) { + await setClassAttribute(page, `#${id}`, state); + await setClassAttribute(page, `#${id} .dx-start-datebox`, state); + } + + return id; + }; + + test('DateRangeBox styles', async ({ page }) => { + + await insertStylesheetRulesToPage(page, `.${DATERANGEBOX_CLASS} { display: inline-flex; margin: 5px; }`); + + for (const stylingMode of stylingModes) { + for (const state of [ + DROP_DOWN_EDITOR_ACTIVE_CLASS, + FOCUSED_STATE_CLASS, + HOVER_STATE_CLASS, + READONLY_STATE_CLASS, + DISABLED_STATE_CLASS, + ] as any[] + ) { + await createDateRangeBox({ value: TEST_VALUE, stylingMode }, state); + } + } + + await createDateRangeBox({ value: TEST_VALUE, rtlEnabled: true }); + await createDateRangeBox({ value: TEST_VALUE, isValid: false }); + + await testScreenshot(page, 'DateRangeBox styles.png', { element: '#container' }); + + }); + + test('DateRangeBox with buttons container', async ({ page }) => { + + await insertStylesheetRulesToPage(page, '#container { display: flex; flex-wrap: wrap; gap: 4px; }'); + + const testButtons: DropDownEditorProperties['buttons'][] = [ + ['clear'], + [{ name: 'custom', location: 'after', options: { icon: 'home' } }, 'clear', 'dropDown'], + ['clear', { name: 'custom', location: 'after', options: { icon: 'home' } }, 'dropDown'], + [{ name: 'custom', location: 'before', options: { icon: 'home' } }, 'clear', 'dropDown'], + ]; + + for (const buttons of testButtons) { + await createDateRangeBox({ + value: TEST_VALUE, + buttons, + }); + await createDateRangeBox({ + value: TEST_VALUE, + buttons, + rtlEnabled: true, + }); + } + + await testScreenshot(page, 'DateRangeBox with buttons container.png', { element: '#container' }); + + }); + + labelModes.forEach((labelMode) => { + test('Custom placeholders and labels appearance', async ({ page }) => { + const dateRangeBox = page.locator(`#${t.ctx.id}`); + + await testScreenshot(page, `Placeholder and label by default labelMode=${labelMode}.png`, { element: '#container' }); + + await page.click(dateRangeBox.getStartDateBox().input); + + await testScreenshot(page, `Placeholder and label on start date input focus labelMode=${labelMode}.png`, { element: '#container' }); + + await page.click(dateRangeBox.getEndDateBox().input); + + await testScreenshot(page, `Placeholder and label on end date input labelMode=${labelMode} focus.png`, { element: '#container' }); + + });.before(async ({ page }) => { + await setAttribute(page, '#container', 'style', 'width: 800px; height: 300px; padding-top: 10px;'); + + t.ctx.id = await createDateRangeBox({ + labelMode, + width: 600, + openOnFieldClick: false, + startDateLabel: 'first date', + endDateLabel: 'second date', + startDatePlaceholder: 'enter start date', + endDatePlaceholder: 'enter end date', + }); + await appendElementTo(page, '#container', 'div', t.ctx.id); + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/editors/dateRangeBox/focus.spec.ts b/e2e/testcafe-devextreme/playwright-tests/editors/dateRangeBox/focus.spec.ts new file mode 100644 index 000000000000..2c86c12ec545 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/editors/dateRangeBox/focus.spec.ts @@ -0,0 +1,674 @@ +import { test, expect } from '@playwright/test'; +import { createWidget } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('DateRangeBox focus state', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('DateRangeBox & DateBoxes should have focus class if inputs are focused by tab', async ({ page }) => { + await createWidget(page, 'dxDateRangeBox', { + value: ['2021/09/17', '2021/10/24'], + openOnFieldClick: false, + width: 500, + }); + + const dateRangeBox = page.locator('#container'); + + await page.click(dateRangeBox.getStartDateBox().input) + .expect(dateRangeBox.isFocused) + .ok() + .expect(dateRangeBox.getStartDateBox().isFocused) + .ok() + .expect(dateRangeBox.getEndDateBox().isFocused) + .notOk(); + + await page.keyboard.press('Tab') + .expect(dateRangeBox.isFocused) + .ok() + .expect(dateRangeBox.getStartDateBox().isFocused) + .notOk() + .expect(dateRangeBox.getEndDateBox().isFocused) + .ok(); + + await page.keyboard.press('Tab') + .expect(dateRangeBox.isFocused) + .notOk() + .expect(dateRangeBox.getStartDateBox().isFocused) + .notOk() + .expect(dateRangeBox.getEndDateBox().isFocused) + .notOk(); + + }); + + test('DateRangeBox & DateBoxes should have focus class if inputs are focused by click', async ({ page }) => { + await createWidget(page, 'dxDateRangeBox', { + value: ['2021/09/17', '2021/10/24'], + openOnFieldClick: false, + width: 500, + }); + + const dateRangeBox = page.locator('#container'); + + await page.click(dateRangeBox.getStartDateBox().input) + .expect(dateRangeBox.isFocused) + .ok() + .expect(dateRangeBox.getStartDateBox().isFocused) + .ok() + .expect(dateRangeBox.getEndDateBox().isFocused) + .notOk(); + + await page.click(dateRangeBox.getEndDateBox().input) + .expect(dateRangeBox.isFocused) + .ok() + .expect(dateRangeBox.getStartDateBox().isFocused) + .notOk() + .expect(dateRangeBox.getEndDateBox().isFocused) + .ok(); + + await page.click(page.locator('body'), { offsetX: -50 }) + .expect(dateRangeBox.isFocused) + .notOk() + .expect(dateRangeBox.getStartDateBox().isFocused) + .notOk() + .expect(dateRangeBox.getEndDateBox().isFocused) + .notOk(); + + }); + + test('DateRangeBox & Start DateBox should have focus class after click on drop down button', async ({ page }) => { + await createWidget(page, 'dxDateRangeBox', { + value: ['2021/09/17', '2021/10/24'], + openOnFieldClick: false, + }); + + const dateRangeBox = page.locator('#container'); + + await page.click(dateRangeBox.dropDownButton) + .expect(dateRangeBox.isFocused) + .ok() + .expect(dateRangeBox.getStartDateBox().isFocused) + .ok() + .expect(dateRangeBox.getEndDateBox().isFocused) + .notOk(); + + }); + + test('DateRangeBox & StartDateBox should be focused if dateRangeBox open by click on drop down button and endDateBox was focused', async ({ page }) => { + await createWidget(page, 'dxDateRangeBox', { + value: ['2021/09/17', '2021/10/24'], + openOnFieldClick: false, + }); + + const dateRangeBox = page.locator('#container'); + + await dateRangeBox.getEndDateBox().element.click(); + + expect(dateRangeBox.isFocused).toBeTruthy() + .expect(dateRangeBox.getStartDateBox().isFocused) + .notOk() + .expect(dateRangeBox.getEndDateBox().isFocused) + .ok(); + + await page.click(dateRangeBox.dropDownButton); + + expect(dateRangeBox.isFocused).toBeTruthy() + .expect(dateRangeBox.getStartDateBox().isFocused) + .ok() + .expect(dateRangeBox.getEndDateBox().isFocused) + .notOk(); + + }); + + test('onFocusIn should be called only after first click on drop down button', async ({ page }) => { + + await page.evaluate(() => { + (window as any).onFocusInCounter = 0; + (window as any).onFocusOutCounter = 0; + }); + + await createWidget(page, 'dxDateRangeBox', { + value: ['2021/09/17', '2021/10/24'], + openOnFieldClick: false, + onFocusIn() { + ((window as any).onFocusInCounter as number) += 1; + }, + onFocusOut() { + ((window as any).onFocusOutCounter as number) += 1; + }, + }); + + const dateRangeBox = page.locator('#container'); + + await page.click(dateRangeBox.element, { offsetX: -20, offsetY: -20 }); + + await page.expect(dateRangeBox.option('opened')) + .ok(); + + await page.expect(ClientFunction(() => (window as any).onFocusInCounter)()) + .eql(1) + .expect(ClientFunction(() => (window as any).onFocusOutCounter)()) + .eql(0); + + await page.click(dateRangeBox.dropDownButton); + + await page.expect(dateRangeBox.option('opened')) + .notOk() + .expect(ClientFunction(() => (window as any).onFocusInCounter)()) + .eql(1) + .expect(ClientFunction(() => (window as any).onFocusOutCounter)()) + .eql(0); + + await page.click(dateRangeBox.element, { offsetX: -20, offsetY: -20 }); + + await page.expect(dateRangeBox.option('opened')) + .ok() + .expect(ClientFunction(() => (window as any).onFocusInCounter)()) + .eql(1) + .expect(ClientFunction(() => (window as any).onFocusOutCounter)()) + .eql(0); + + }); + + test('onFocusIn should be called only on focus of startDate input', async ({ page }) => { + + await page.evaluate(() => { + (window as any).onFocusInCounter = 0; + (window as any).onFocusOutCounter = 0; + }); + + await createWidget(page, 'dxDateRangeBox', { + value: [new Date('2021/09/17'), new Date('2021/10/24')], + openOnFieldClick: true, + width: 500, + onFocusIn() { + ((window as any).onFocusInCounter as number) += 1; + }, + onFocusOut() { + ((window as any).onFocusOutCounter as number) += 1; + }, + }); + + const dateRangeBox = page.locator('#container'); + + await page.click(dateRangeBox.getStartDateBox().input); + + await page.expect(ClientFunction(() => (window as any).onFocusInCounter)()) + .eql(1) + .expect(ClientFunction(() => (window as any).onFocusOutCounter)()) + .eql(0); + + await page.keyboard.press('Tab'); + + await page.expect(ClientFunction(() => (window as any).onFocusInCounter)()) + .eql(1) + .expect(ClientFunction(() => (window as any).onFocusOutCounter)()) + .eql(0); + + await page.click(dateRangeBox.getStartDateBox().input); + + await page.expect(dateRangeBox.option('opened')) + .ok() + .click(dateRangeBox.getCalendarCell(10)) + .expect(dateRangeBox.option('value')) + .eql([new Date(2021, 8, 8), new Date(2021, 9, 24)]) + .expect(ClientFunction(() => (window as any).onFocusInCounter)()) + .eql(1) + .expect(ClientFunction(() => (window as any).onFocusOutCounter)()) + .eql(0); + + await page.expect(dateRangeBox.option('opened')) + .ok() + .click(dateRangeBox.getCalendarCell(20)) + .expect(dateRangeBox.option('value')) + .eql([new Date(2021, 8, 8), new Date(2021, 8, 18)]) + .expect(ClientFunction(() => (window as any).onFocusInCounter)()) + .eql(1) + .expect(ClientFunction(() => (window as any).onFocusOutCounter)()) + .eql(0); + + await page.keyboard.press('shift+tab') + .wait(100) + .expect(dateRangeBox.isFocused) + .ok() + .expect(dateRangeBox.getStartDateBox().isFocused) + .ok() + .expect(dateRangeBox.getEndDateBox().isFocused) + .notOk() + .expect(ClientFunction(() => (window as any).onFocusInCounter)()) + .eql(1) + .expect(ClientFunction(() => (window as any).onFocusOutCounter)()) + .eql(0); + + await page.keyboard.press('shift+tab') + .wait(100) + .expect(dateRangeBox.isFocused) + .notOk() + .expect(dateRangeBox.getStartDateBox().isFocused) + .notOk() + .expect(dateRangeBox.getEndDateBox().isFocused) + .notOk() + .expect(ClientFunction(() => (window as any).onFocusInCounter)()) + .eql(1) + .expect(ClientFunction(() => (window as any).onFocusOutCounter)()) + .eql(1); + + }); + + test('Click by separator element should focus DateRangeBox or leave active input focused without call onFocusIn event handler', async ({ page }) => { + + await page.evaluate(() => { + (window as any).onFocusInCounter = 0; + (window as any).onFocusOutCounter = 0; + }); + + await createWidget(page, 'dxDateRangeBox', { + value: ['2021/09/17', '2021/10/24'], + openOnFieldClick: true, + width: 500, + onFocusIn() { + ((window as any).onFocusInCounter as number) += 1; + }, + onFocusOut() { + ((window as any).onFocusOutCounter as number) += 1; + }, + }); + + const dateRangeBox = page.locator('#container'); + + await page.click(dateRangeBox.separator); + + await page.expect(dateRangeBox.option('opened')) + .notOk() + .expect(dateRangeBox.isFocused) + .ok() + .expect(dateRangeBox.getStartDateBox().isFocused) + .ok() + .expect(dateRangeBox.getEndDateBox().isFocused) + .notOk() + .expect(ClientFunction(() => (window as any).onFocusInCounter)()) + .eql(1) + .expect(ClientFunction(() => (window as any).onFocusOutCounter)()) + .eql(0); + + await page.click(dateRangeBox.separator); + + await page.expect(dateRangeBox.option('opened')) + .notOk() + .expect(dateRangeBox.isFocused) + .ok() + .expect(dateRangeBox.getStartDateBox().isFocused) + .ok() + .expect(dateRangeBox.getEndDateBox().isFocused) + .notOk() + .expect(ClientFunction(() => (window as any).onFocusInCounter)()) + .eql(1) + .expect(ClientFunction(() => (window as any).onFocusOutCounter)()) + .eql(0); + + await page.click(dateRangeBox.getEndDateBox().input); + + await page.expect(dateRangeBox.option('opened')) + .ok() + .expect(dateRangeBox.isFocused) + .ok() + .expect(dateRangeBox.getStartDateBox().isFocused) + .notOk() + .expect(dateRangeBox.getEndDateBox().isFocused) + .ok() + .expect(ClientFunction(() => (window as any).onFocusInCounter)()) + .eql(1) + .expect(ClientFunction(() => (window as any).onFocusOutCounter)()) + .eql(0); + + await page.click(dateRangeBox.separator); + + await page.expect(dateRangeBox.option('opened')) + .ok() + .expect(dateRangeBox.isFocused) + .ok() + .expect(dateRangeBox.getStartDateBox().isFocused) + .notOk() + .expect(dateRangeBox.getEndDateBox().isFocused) + .ok() + .expect(ClientFunction(() => (window as any).onFocusInCounter)()) + .eql(1) + .expect(ClientFunction(() => (window as any).onFocusOutCounter)()) + .eql(0); + + await page.click(page.locator('body'), { offsetX: -50 }) + .expect(dateRangeBox.option('opened')) + .notOk() + .expect(dateRangeBox.isFocused) + .notOk() + .expect(dateRangeBox.getStartDateBox().isFocused) + .notOk() + .expect(dateRangeBox.getEndDateBox().isFocused) + .notOk() + .expect(ClientFunction(() => (window as any).onFocusInCounter)()) + .eql(1) + .expect(ClientFunction(() => (window as any).onFocusOutCounter)()) + .eql(1); + + }); + + test('EndDateBox should be stay focused after close popup by click on drop down button', async ({ page }) => { + await createWidget(page, 'dxDateRangeBox', { + value: [new Date('2021/09/17'), new Date('2021/10/24')], + openOnFieldClick: false, + }); + + const dateRangeBox = page.locator('#container'); + + await page.click(dateRangeBox.element, { offsetX: -20, offsetY: -20 }); + + await page.expect(dateRangeBox.option('opened')) + .ok(); + + await page.click(dateRangeBox.getCalendarCell(10)) + .expect(dateRangeBox.option('value')) + .eql([new Date(2021, 8, 8), new Date(2021, 9, 24)]) + .expect(dateRangeBox.isFocused) + .ok() + .expect(dateRangeBox.getStartDateBox().isFocused) + .notOk() + .expect(dateRangeBox.getEndDateBox().isFocused) + .ok(); + + await page.click(dateRangeBox.dropDownButton); + + await page.expect(dateRangeBox.option('opened')) + .notOk() + .expect(dateRangeBox.isFocused) + .ok() + .expect(dateRangeBox.getStartDateBox().isFocused) + .notOk() + .expect(dateRangeBox.getEndDateBox().isFocused) + .ok(); + + }); + + test('DateRangeBox & StartDateBox should be focused after click on clear button', async ({ page }) => { + await createWidget(page, 'dxDateRangeBox', { + showClearButton: true, + value: ['2021/09/17', '2021/10/24'], + openOnFieldClick: false, + }); + + const dateRangeBox = page.locator('#container'); + + await dateRangeBox.getEndDateBox().element.click(); + + expect(dateRangeBox.isFocused).toBeTruthy() + .expect(dateRangeBox.getStartDateBox().isFocused) + .notOk() + .expect(dateRangeBox.getEndDateBox().isFocused) + .ok(); + + await page.click(dateRangeBox.clearButton); + + expect(dateRangeBox.isFocused).toBeTruthy() + .expect(dateRangeBox.getStartDateBox().isFocused) + .ok() + .expect(dateRangeBox.getEndDateBox().isFocused) + .notOk(); + + }); + + test('DateRangeBox & StartDateBox should be focused and stay opened after click on clear button when popup is opened', async ({ page }) => { + await createWidget(page, 'dxDateRangeBox', { + showClearButton: true, + value: ['2021/09/17', '2021/10/24'], + openOnFieldClick: true, + }, '#container'); + + const dateRangeBox = page.locator('#container'); + + await dateRangeBox.getStartDateBox().element.click(); + + await page.expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.isFocused) + .ok() + .expect(dateRangeBox.getStartDateBox().isFocused) + .ok() + .expect(dateRangeBox.getEndDateBox().isFocused) + .notOk(); + + await page.click(dateRangeBox.clearButton); + + await page.waitForTimeout(500); + + await page.expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.isFocused) + .ok() + .expect(dateRangeBox.getStartDateBox().isFocused) + .ok() + .expect(dateRangeBox.getEndDateBox().isFocused) + .notOk(); + + }); + + test('DateRangeBox & StartDateBox should be focused after click on clear button', async ({ page }) => { + await createWidget(page, 'dxDateRangeBox', { + showClearButton: true, + value: [null, '2021/10/24'], + openOnFieldClick: false, + opened: true, + }); + + const dateRangeBox = page.locator('#container'); + + await page.click(dateRangeBox.getEndDateBox().input) + .click(dateRangeBox.dropDownButton); + + expect(dateRangeBox.isFocused).toBeTruthy() + .expect(dateRangeBox.getStartDateBox().isFocused) + .notOk() + .expect(dateRangeBox.getEndDateBox().isFocused) + .ok(); + + await page.click(dateRangeBox.clearButton); + + expect(dateRangeBox.isFocused).toBeTruthy() + .expect(dateRangeBox.getStartDateBox().isFocused) + .ok() + .expect(dateRangeBox.getEndDateBox().isFocused) + .notOk(); + + }); + + test('DateRangeBox & StartDateBox should be focused if startDateBox open by keyboard, alt+down, alt+up', async ({ page }) => { + await createWidget(page, 'dxDateRangeBox', { + value: ['2021/09/17', '2021/10/24'], + openOnFieldClick: false, + }); + + const dateRangeBox = page.locator('#container'); + + await page.click(dateRangeBox.getStartDateBox().input); + + await page.expect(dateRangeBox.option('opened')) + .eql(false) + .expect(dateRangeBox.isFocused) + .ok() + .expect(dateRangeBox.getStartDateBox().isFocused) + .ok() + .expect(dateRangeBox.getEndDateBox().isFocused) + .notOk(); + + await page.keyboard.press('alt+down'); + + await page.expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.isFocused) + .ok() + .expect(dateRangeBox.getStartDateBox().isFocused) + .ok() + .expect(dateRangeBox.getEndDateBox().isFocused) + .notOk(); + + await page.keyboard.press('alt+up'); + + await page.expect(dateRangeBox.option('opened')) + .eql(false) + .expect(dateRangeBox.isFocused) + .ok() + .expect(dateRangeBox.getStartDateBox().isFocused) + .ok() + .expect(dateRangeBox.getEndDateBox().isFocused) + .notOk(); + + }); + + test('DateRangeBox & StartDateBox should be focused if endDateBox open and close by keyboard, alt+down, alt+up', async ({ page }) => { + await createWidget(page, 'dxDateRangeBox', { + value: ['2021/09/17', '2021/10/24'], + openOnFieldClick: false, + }); + + const dateRangeBox = page.locator('#container'); + + await page.click(dateRangeBox.getEndDateBox().input); + + await page.expect(dateRangeBox.option('opened')) + .eql(false) + .expect(dateRangeBox.isFocused) + .ok() + .expect(dateRangeBox.getStartDateBox().isFocused) + .notOk() + .expect(dateRangeBox.getEndDateBox().isFocused) + .ok(); + + await page.keyboard.press('alt+down'); + + await page.expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.isFocused) + .ok() + .expect(dateRangeBox.getStartDateBox().isFocused) + .notOk() + .expect(dateRangeBox.getEndDateBox().isFocused) + .ok(); + + await page.keyboard.press('alt+up'); + + await page.expect(dateRangeBox.option('opened')) + .eql(false) + .expect(dateRangeBox.isFocused) + .ok() + .expect(dateRangeBox.getStartDateBox().isFocused) + .notOk() + .expect(dateRangeBox.getEndDateBox().isFocused) + .ok(); + + }); + + test('Opened dateRangeBox should not be closed after click on inputs, openOnFieldClick: true', async ({ page }) => { + await createWidget(page, 'dxDateRangeBox', { + value: ['2021/09/17', '2021/10/24'], + openOnFieldClick: true, + opened: true, + }); + + const dateRangeBox = page.locator('#container'); + + await page.click(dateRangeBox.getStartDateBox().input); + + await page.expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.isFocused) + .ok() + .expect(dateRangeBox.getStartDateBox().isFocused) + .ok() + .expect(dateRangeBox.getEndDateBox().isFocused) + .notOk(); + + await page.click(dateRangeBox.getEndDateBox().input); + + await page.expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.isFocused) + .ok() + .expect(dateRangeBox.getStartDateBox().isFocused) + .notOk() + .expect(dateRangeBox.getEndDateBox().isFocused) + .ok(); + + await page.click(dateRangeBox.getStartDateBox().input); + + await page.expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.isFocused) + .ok() + .expect(dateRangeBox.getStartDateBox().isFocused) + .ok() + .expect(dateRangeBox.getEndDateBox().isFocused) + .notOk(); + + }); + + test('Opened dateRangeBox should be closed after outside click, openOnFieldClick: true', async ({ page }) => { + await createWidget(page, 'dxDateRangeBox', { + value: ['2021/09/17', '2021/10/24'], + width: 500, + openOnFieldClick: true, + opened: true, + }); + + const dateRangeBox = page.locator('#container'); + + await page.click(page.locator('body'), { offsetX: -50 }); + + await page.expect(dateRangeBox.option('opened')) + .eql(false) + .expect(dateRangeBox.isFocused) + .notOk() + .expect(dateRangeBox.getStartDateBox().isFocused) + .notOk() + .expect(dateRangeBox.getEndDateBox().isFocused) + .notOk(); + + await page.click(dateRangeBox.dropDownButton); + + await page.expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.isFocused) + .ok() + .expect(dateRangeBox.getStartDateBox().isFocused) + .ok() + .expect(dateRangeBox.getEndDateBox().isFocused) + .notOk(); + + }); + + // TODO: find way to reproduce focus using accessKey accessKey + test.skip('DateRangeBox and StartDateBox should have focus class after focus via accessKey', async ({ page }) => { + await createWidget(page, 'dxDateRangeBox', { + accessKey: 'x', + openOnFieldClick: false, + }); + + const dateRangeBox = page.locator('#container'); + + await page.click(dateRangeBox.getEndDateBox().input); + + await page.keyboard.press('alt+x'); + + expect(dateRangeBox.isFocused).toBeTruthy() + .expect(dateRangeBox.getStartDateBox().isFocused) + .ok() + .expect(dateRangeBox.getEndDateBox().isFocused) + .notOk(); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/editors/dateRangeBox/keyboard.spec.ts b/e2e/testcafe-devextreme/playwright-tests/editors/dateRangeBox/keyboard.spec.ts new file mode 100644 index 000000000000..4d7a5a3a4e16 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/editors/dateRangeBox/keyboard.spec.ts @@ -0,0 +1,1260 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, appendElementTo } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('DateRangeBox keyboard navigation', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + const initialValue = [new Date('2021/10/17'), new Date('2021/11/24')]; + + const getDateByOffset = (date: Date | string, offset: number) => { + const resultDate = new Date(date); + + return new Date(resultDate.setDate(resultDate.getDate() + offset)); + }; + + test('DateRangeBox should be opened and close by press alt+down and alt+up respectively when startDateBox is focused', async ({ page }) => { + await createWidget(page, 'dxDateRangeBox', { + value: ['2021/09/17', '2021/10/24'], + openOnFieldClick: false, + }); + + const dateRangeBox = page.locator('#container'); + + await page.click(dateRangeBox.getStartDateBox().input); + + await page.expect(dateRangeBox.option('opened')) + .eql(false) + .expect(dateRangeBox.getStartDateBox().option('opened')) + .eql(false) + .expect(dateRangeBox.getEndDateBox().option('opened')) + .eql(false); + + await page.keyboard.press('alt+down'); + + await page.expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.getStartDateBox().option('opened')) + .eql(true) + .expect(dateRangeBox.getEndDateBox().option('opened')) + .eql(true); + + await page.keyboard.press('alt+up'); + + await page.expect(dateRangeBox.option('opened')) + .eql(false) + .expect(dateRangeBox.getStartDateBox().option('opened')) + .eql(false) + .expect(dateRangeBox.getEndDateBox().option('opened')) + .eql(false); + + await page.keyboard.press('alt+down'); + + await page.expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.getStartDateBox().option('opened')) + .eql(true) + .expect(dateRangeBox.getEndDateBox().option('opened')) + .eql(true); + + await page.keyboard.press('alt+up'); + + await page.expect(dateRangeBox.option('opened')) + .eql(false) + .expect(dateRangeBox.getStartDateBox().option('opened')) + .eql(false) + .expect(dateRangeBox.getEndDateBox().option('opened')) + .eql(false); + + }); + + test('DateRangeBox should be opened and close by press alt+down and alt+up respectively when endDateBox is focused', async ({ page }) => { + await createWidget(page, 'dxDateRangeBox', { + value: ['2021/09/17', '2021/10/24'], + openOnFieldClick: false, + }); + + const dateRangeBox = page.locator('#container'); + + await page.click(dateRangeBox.getEndDateBox().input); + + await page.expect(dateRangeBox.option('opened')) + .eql(false) + .expect(dateRangeBox.getStartDateBox().option('opened')) + .eql(false) + .expect(dateRangeBox.getEndDateBox().option('opened')) + .eql(false); + + await page.keyboard.press('alt+down'); + + await page.expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.getStartDateBox().option('opened')) + .eql(true) + .expect(dateRangeBox.getEndDateBox().option('opened')) + .eql(true); + + await page.keyboard.press('alt+up'); + + await page.expect(dateRangeBox.option('opened')) + .eql(false) + .expect(dateRangeBox.getStartDateBox().option('opened')) + .eql(false) + .expect(dateRangeBox.getEndDateBox().option('opened')) + .eql(false); + + await page.keyboard.press('alt+down'); + + await page.expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.getStartDateBox().option('opened')) + .eql(true) + .expect(dateRangeBox.getEndDateBox().option('opened')) + .eql(true); + + await page.keyboard.press('alt+up'); + + await page.expect(dateRangeBox.option('opened')) + .eql(false) + .expect(dateRangeBox.getStartDateBox().option('opened')) + .eql(false) + .expect(dateRangeBox.getEndDateBox().option('opened')) + .eql(false); + + }); + + test('DateRangeBox should be opened by press alt+down if startDate input is focused and close by press alt+up if endDateBox is focused', async ({ page }) => { + await createWidget(page, 'dxDateRangeBox', { + value: ['2021/09/17', '2021/10/24'], + openOnFieldClick: false, + }); + + const dateRangeBox = page.locator('#container'); + + await page.click(dateRangeBox.getStartDateBox().input); + + await page.expect(dateRangeBox.option('opened')) + .eql(false) + .expect(dateRangeBox.getStartDateBox().option('opened')) + .eql(false) + .expect(dateRangeBox.getEndDateBox().option('opened')) + .eql(false); + + await page.keyboard.press('alt+down'); + + await page.expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.getStartDateBox().option('opened')) + .eql(true) + .expect(dateRangeBox.getEndDateBox().option('opened')) + .eql(true); + + await page.click(dateRangeBox.getEndDateBox().input); + + await page.expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.getStartDateBox().option('opened')) + .eql(true) + .expect(dateRangeBox.getEndDateBox().option('opened')) + .eql(true); + + await page.keyboard.press('alt+up'); + + await page.expect(dateRangeBox.option('opened')) + .eql(false) + .expect(dateRangeBox.getStartDateBox().option('opened')) + .eql(false) + .expect(dateRangeBox.getEndDateBox().option('opened')) + .eql(false); + + }); + + test('DateRangeBox should be closed by press esc key when startDateBox is focused', async ({ page }) => { + await createWidget(page, 'dxDateRangeBox', { + value: ['2021/09/17', '2021/10/24'], + openOnFieldClick: true, + }); + + const dateRangeBox = page.locator('#container'); + + await page.click(dateRangeBox.getStartDateBox().input); + + await page.expect(dateRangeBox.option('opened')) + .eql(true); + + await page.keyboard.press('esc'); + + await page.expect(dateRangeBox.option('opened')) + .eql(false); + + }); + + test('DateRangeBox should be closed by press esc key when endDateBox is focused', async ({ page }) => { + await createWidget(page, 'dxDateRangeBox', { + value: ['2021/09/17', '2021/10/24'], + openOnFieldClick: true, + }); + + const dateRangeBox = page.locator('#container'); + + await page.click(dateRangeBox.getEndDateBox().input); + + await page.expect(dateRangeBox.option('opened')) + .eql(true); + + await page.keyboard.press('esc'); + + await page.expect(dateRangeBox.option('opened')) + .eql(false); + + }); + + test('DateRangeBox should be closed by press esc key when today/cancel/apply button in popup is focused, applyValueMode is useButtons', async ({ page }) => { + await createWidget(page, 'dxDateRangeBox', { + value: ['2021/09/17', '2021/10/24'], + openOnFieldClick: true, + applyValueMode: 'useButtons', + }); + + const dateRangeBox = page.locator('#container'); + + await page.click(dateRangeBox.getEndDateBox().input); + + await page.expect(dateRangeBox.option('opened')) + .eql(true); + + await page.keyboard.press('Tab') + .pressKey('tab') + .pressKey('tab') + .pressKey('tab') + .pressKey('tab'); + + await page.expect(dateRangeBox.getPopup().getTodayButton().isFocused) + .eql(true); + + await page.keyboard.press('esc'); + + await page.expect(dateRangeBox.option('opened')) + .eql(false); + + await page.click(dateRangeBox.getEndDateBox().input); + + await page.expect(dateRangeBox.option('opened')) + .eql(true); + + await page.keyboard.press('Tab') + .pressKey('tab') + .pressKey('tab') + .pressKey('tab') + .pressKey('tab') + .pressKey('tab'); + + await page.expect(dateRangeBox.getPopup().getApplyButton().isFocused) + .eql(true); + + await page.keyboard.press('esc'); + + await page.expect(dateRangeBox.option('opened')) + .eql(false); + + await page.click(dateRangeBox.getEndDateBox().input); + + await page.expect(dateRangeBox.option('opened')) + .eql(true); + + await page.keyboard.press('Tab') + .pressKey('tab') + .pressKey('tab') + .pressKey('tab') + .pressKey('tab') + .pressKey('tab') + .pressKey('tab'); + + await page.expect(dateRangeBox.getPopup().getCancelButton().isFocused) + .eql(true); + + await page.keyboard.press('esc'); + + await page.expect(dateRangeBox.option('opened')) + .eql(false); + + }); + + test('DateRangeBox should be closed by press esc key when navigator element in popup is focused, applyValueMode is useButtons', async ({ page }) => { + await createWidget(page, 'dxDateRangeBox', { + value: ['2021/09/17', '2021/10/24'], + openOnFieldClick: true, + applyValueMode: 'useButtons', + }); + + const dateRangeBox = page.locator('#container'); + + await page.click(dateRangeBox.getEndDateBox().input); + + await page.expect(dateRangeBox.option('opened')) + .eql(true); + + await page.keyboard.press('Tab'); + + await page.expect(dateRangeBox.getPopup().getNavigatorPrevButton().isFocused) + .eql(true); + + await page.keyboard.press('esc'); + + await page.expect(dateRangeBox.option('opened')) + .eql(false); + + await page.click(dateRangeBox.getEndDateBox().input); + + await page.expect(dateRangeBox.option('opened')) + .eql(true); + + await page.keyboard.press('Tab') + .pressKey('tab'); + + await page.expect(dateRangeBox.getPopup().getNavigatorCaption().isFocused) + .eql(true); + + await page.keyboard.press('esc'); + + await page.expect(dateRangeBox.option('opened')) + .eql(false); + + await page.click(dateRangeBox.getEndDateBox().input); + + await page.expect(dateRangeBox.option('opened')) + .eql(true); + + await page.keyboard.press('Tab') + .pressKey('tab') + .pressKey('tab'); + + await page.expect(dateRangeBox.getPopup().getNavigatorNextButton().isFocused) + .eql(true); + + await page.keyboard.press('esc'); + + await page.expect(dateRangeBox.option('opened')) + .eql(false); + + }); + + test('DateRangeBox should be closed by press esc key when views wrapper in popup is focused, applyValueMode is useButtons', async ({ page }) => { + await createWidget(page, 'dxDateRangeBox', { + value: ['2021/09/17', '2021/10/24'], + openOnFieldClick: true, + applyValueMode: 'useButtons', + }); + + const dateRangeBox = page.locator('#container'); + + await page.click(dateRangeBox.getEndDateBox().input); + + await page.expect(dateRangeBox.option('opened')) + .eql(true); + + await page.keyboard.press('Tab') + .pressKey('tab') + .pressKey('tab') + .pressKey('tab'); + + await page.expect(dateRangeBox.getPopup().getViewsWrapper().focused) + .eql(true); + + await page.keyboard.press('esc'); + + await page.expect(dateRangeBox.option('opened')) + .eql(false); + + }); + + test('DateRangeBox should not be closed by press tab key on startDate input', async ({ page }) => { + await createWidget(page, 'dxDateRangeBox', { + value: ['2021/09/17', '2021/10/24'], + openOnFieldClick: true, + opened: true, + width: 500, + dropDownOptions: { + hideOnOutsideClick: false, + }, + calendarOptions: { + focusStateEnabled: false, + }, + }); + + const dateRangeBox = page.locator('#container'); + + await page.click(dateRangeBox.getStartDateBox().input); + + await page.expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.getStartDateBox().isFocused) + .ok(); + + await page.keyboard.press('Tab'); + + await page.expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.getEndDateBox().isFocused) + .ok(); + + await page.keyboard.press('Tab'); + + await page.expect(dateRangeBox.option('opened')) + .eql(false) + .expect(dateRangeBox.isFocused) + .notOk(); + + }); + + test('DateRangeBox keyboard navigation via `tab` key if applyValueMode is useButtons, start -> end -> prev -> caption -> next -> views -> today -> apply -> cancel -> start -> end', async ({ page }) => { + + await appendElementTo(page, '#container', 'div', 'firstFocusableElement'); + await appendElementTo(page, '#container', 'div', 'dateRangeBox'); + await appendElementTo(page, '#container', 'div', 'lastFocusableElement'); + + await createWidget(page, 'dxButton', { + text: 'First Focusable Element', + }, '#firstFocusableElement'); + + await createWidget(page, 'dxButton', { + text: 'Last Focusable Element', + }, '#lastFocusableElement'); + + await createWidget(page, 'dxDateRangeBox', { + value: ['2021/09/17', '2021/10/24'], + openOnFieldClick: true, + applyValueMode: 'useButtons', + opened: true, + width: 500, + dropDownOptions: { + hideOnOutsideClick: false, + }, + }, '#dateRangeBox'); + + const dateRangeBox = page.locator('#dateRangeBox'); + + await page.locator('#firstFocusableElement').click() + .pressKey('tab'); + + await page.expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.isFocused) + .ok() + .expect(dateRangeBox.getStartDateBox().isFocused) + .ok() + .expect(dateRangeBox.getEndDateBox().isFocused) + .notOk(); + + await page.keyboard.press('Tab'); + + await page.expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.isFocused) + .ok() + .expect(dateRangeBox.getStartDateBox().isFocused) + .notOk() + .expect(dateRangeBox.getEndDateBox().isFocused) + .ok() + .expect(dateRangeBox.getPopup().getNavigatorPrevButton().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getNavigatorCaption().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getNavigatorNextButton().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getApplyButton().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getCancelButton().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getTodayButton().isFocused) + .notOk(); + + await page.keyboard.press('Tab'); + + await page.expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.isFocused) + .notOk() + .expect(dateRangeBox.getStartDateBox().isFocused) + .notOk() + .expect(dateRangeBox.getEndDateBox().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getNavigatorPrevButton().isFocused) + .ok() + .expect(dateRangeBox.getPopup().getNavigatorCaption().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getNavigatorNextButton().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getApplyButton().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getCancelButton().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getTodayButton().isFocused) + .notOk(); + + await page.keyboard.press('Tab'); + + await page.expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.isFocused) + .notOk() + .expect(dateRangeBox.getStartDateBox().isFocused) + .notOk() + .expect(dateRangeBox.getEndDateBox().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getNavigatorPrevButton().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getNavigatorCaption().isFocused) + .ok() + .expect(dateRangeBox.getPopup().getNavigatorNextButton().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getApplyButton().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getCancelButton().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getTodayButton().isFocused) + .notOk(); + + await page.keyboard.press('Tab'); + + await page.expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.isFocused) + .notOk() + .expect(dateRangeBox.getStartDateBox().isFocused) + .notOk() + .expect(dateRangeBox.getEndDateBox().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getNavigatorPrevButton().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getNavigatorCaption().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getNavigatorNextButton().isFocused) + .ok() + .expect(dateRangeBox.getPopup().getApplyButton().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getCancelButton().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getTodayButton().isFocused) + .notOk(); + + await page.keyboard.press('Tab'); + + await page.expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.isFocused) + .notOk() + .expect(dateRangeBox.getStartDateBox().isFocused) + .notOk() + .expect(dateRangeBox.getEndDateBox().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getNavigatorPrevButton().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getNavigatorCaption().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getNavigatorNextButton().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getViewsWrapper().focused) + .ok() + .expect(dateRangeBox.getPopup().getApplyButton().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getCancelButton().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getTodayButton().isFocused) + .notOk(); + + await page.keyboard.press('Tab'); + + await page.expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.isFocused) + .notOk() + .expect(dateRangeBox.getStartDateBox().isFocused) + .notOk() + .expect(dateRangeBox.getEndDateBox().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getNavigatorPrevButton().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getNavigatorCaption().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getNavigatorNextButton().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getApplyButton().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getCancelButton().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getTodayButton().isFocused) + .ok(); + + await page.keyboard.press('Tab'); + + await page.expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.isFocused) + .notOk() + .expect(dateRangeBox.getStartDateBox().isFocused) + .notOk() + .expect(dateRangeBox.getEndDateBox().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getApplyButton().isFocused) + .ok() + .expect(dateRangeBox.getPopup().getCancelButton().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getTodayButton().isFocused) + .notOk(); + + await page.keyboard.press('Tab'); + + await page.expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.isFocused) + .notOk() + .expect(dateRangeBox.getStartDateBox().isFocused) + .notOk() + .expect(dateRangeBox.getEndDateBox().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getApplyButton().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getCancelButton().isFocused) + .ok() + .expect(dateRangeBox.getPopup().getTodayButton().isFocused) + .notOk(); + + await page.keyboard.press('Tab'); + + await page.expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.isFocused) + .ok() + .expect(dateRangeBox.getStartDateBox().isFocused) + .ok() + .expect(dateRangeBox.getEndDateBox().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getApplyButton().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getCancelButton().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getTodayButton().isFocused) + .notOk(); + + await page.keyboard.press('Tab'); + + await page.expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.isFocused) + .ok() + .expect(dateRangeBox.getStartDateBox().isFocused) + .notOk() + .expect(dateRangeBox.getEndDateBox().isFocused) + .ok() + .expect(dateRangeBox.getPopup().getApplyButton().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getCancelButton().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getTodayButton().isFocused) + .notOk(); + + }); + + test('DateRangeBox keyboard navigation via `shift+tab` key if applyValueMode is useButtons, end -> start -> cancel -> apply -> today -> views -> next -> caption -> prev -> end -> start', async ({ page }) => { + + await appendElementTo(page, '#container', 'div', 'firstFocusableElement'); + await appendElementTo(page, '#container', 'div', 'dateRangeBox'); + await appendElementTo(page, '#container', 'div', 'lastFocusableElement'); + + await createWidget(page, 'dxButton', { + text: 'First Focused Element', + }, '#firstFocusableElement'); + + await createWidget(page, 'dxButton', { + text: 'Last Focused Element', + }, '#lastFocusableElement'); + + await createWidget(page, 'dxDateRangeBox', { + value: ['2021/09/17', '2021/10/24'], + openOnFieldClick: true, + applyValueMode: 'useButtons', + opened: false, + width: 500, + }, '#dateRangeBox'); + + const dateRangeBox = page.locator('#dateRangeBox'); + + await page.click(dateRangeBox.getEndDateBox().input); + + await page.expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.isFocused) + .ok() + .expect(dateRangeBox.getStartDateBox().isFocused) + .notOk() + .expect(dateRangeBox.getEndDateBox().isFocused) + .ok(); + + await page.keyboard.press('shift+tab'); + + await page.expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.isFocused) + .ok() + .expect(dateRangeBox.getStartDateBox().isFocused) + .ok() + .expect(dateRangeBox.getEndDateBox().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getNavigatorPrevButton().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getNavigatorCaption().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getNavigatorNextButton().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getApplyButton().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getCancelButton().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getTodayButton().isFocused) + .notOk(); + + await page.keyboard.press('shift+tab'); + + await page.expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.isFocused) + .notOk() + .expect(dateRangeBox.getStartDateBox().isFocused) + .notOk() + .expect(dateRangeBox.getEndDateBox().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getNavigatorPrevButton().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getNavigatorCaption().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getNavigatorNextButton().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getApplyButton().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getCancelButton().isFocused) + .ok() + .expect(dateRangeBox.getPopup().getTodayButton().isFocused) + .notOk(); + + await page.keyboard.press('shift+tab'); + + await page.expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.isFocused) + .notOk() + .expect(dateRangeBox.getStartDateBox().isFocused) + .notOk() + .expect(dateRangeBox.getEndDateBox().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getNavigatorPrevButton().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getNavigatorCaption().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getNavigatorNextButton().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getApplyButton().isFocused) + .ok() + .expect(dateRangeBox.getPopup().getCancelButton().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getTodayButton().isFocused) + .notOk(); + + await page.keyboard.press('shift+tab'); + + await page.expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.isFocused) + .notOk() + .expect(dateRangeBox.getStartDateBox().isFocused) + .notOk() + .expect(dateRangeBox.getEndDateBox().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getNavigatorPrevButton().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getNavigatorCaption().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getNavigatorNextButton().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getApplyButton().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getCancelButton().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getTodayButton().isFocused) + .ok(); + + await page.keyboard.press('shift+tab'); + + await page.expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.isFocused) + .notOk() + .expect(dateRangeBox.getStartDateBox().isFocused) + .notOk() + .expect(dateRangeBox.getEndDateBox().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getNavigatorPrevButton().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getNavigatorCaption().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getNavigatorNextButton().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getViewsWrapper().focused) + .ok() + .expect(dateRangeBox.getPopup().getApplyButton().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getCancelButton().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getTodayButton().isFocused) + .notOk(); + + await page.keyboard.press('shift+tab'); + + await page.expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.isFocused) + .notOk() + .expect(dateRangeBox.getStartDateBox().isFocused) + .notOk() + .expect(dateRangeBox.getEndDateBox().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getNavigatorPrevButton().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getNavigatorCaption().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getNavigatorNextButton().isFocused) + .ok() + .expect(dateRangeBox.getPopup().getApplyButton().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getCancelButton().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getTodayButton().isFocused) + .notOk(); + + await page.keyboard.press('shift+tab'); + + await page.expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.isFocused) + .notOk() + .expect(dateRangeBox.getStartDateBox().isFocused) + .notOk() + .expect(dateRangeBox.getEndDateBox().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getNavigatorPrevButton().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getNavigatorCaption().isFocused) + .ok() + .expect(dateRangeBox.getPopup().getNavigatorNextButton().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getApplyButton().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getCancelButton().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getTodayButton().isFocused) + .notOk(); + + await page.keyboard.press('shift+tab'); + + await page.expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.isFocused) + .notOk() + .expect(dateRangeBox.getStartDateBox().isFocused) + .notOk() + .expect(dateRangeBox.getEndDateBox().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getNavigatorPrevButton().isFocused) + .ok() + .expect(dateRangeBox.getPopup().getNavigatorCaption().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getNavigatorNextButton().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getApplyButton().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getCancelButton().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getTodayButton().isFocused) + .notOk(); + + await page.keyboard.press('shift+tab'); + + await page.expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.isFocused) + .ok() + .expect(dateRangeBox.getStartDateBox().isFocused) + .notOk() + .expect(dateRangeBox.getEndDateBox().isFocused) + .ok() + .expect(dateRangeBox.getPopup().getNavigatorPrevButton().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getNavigatorCaption().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getNavigatorNextButton().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getApplyButton().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getCancelButton().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getTodayButton().isFocused) + .notOk(); + + await page.keyboard.press('shift+tab'); + + await page.expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.isFocused) + .ok() + .expect(dateRangeBox.getStartDateBox().isFocused) + .ok() + .expect(dateRangeBox.getEndDateBox().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getNavigatorPrevButton().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getNavigatorCaption().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getNavigatorNextButton().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getApplyButton().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getCancelButton().isFocused) + .notOk() + .expect(dateRangeBox.getPopup().getTodayButton().isFocused) + .notOk(); + + }); + + test('DateRangeBox keyboard navigation via `tab` key if applyValueMode is instantly, start -> end -> prev -> caption -> next -> views -> start -> end', async ({ page }) => { + + await appendElementTo(page, '#container', 'div', 'firstFocusableElement'); + await appendElementTo(page, '#container', 'div', 'dateRangeBox'); + await appendElementTo(page, '#container', 'div', 'lastFocusableElement'); + + await createWidget(page, 'dxButton', { + text: 'First Focusable Element', + }, '#firstFocusableElement'); + + await createWidget(page, 'dxButton', { + text: 'Last Focusable Element', + }, '#lastFocusableElement'); + + await createWidget(page, 'dxDateRangeBox', { + value: ['2021/09/17', '2021/10/24'], + openOnFieldClick: true, + applyValueMode: 'instantly', + opened: true, + width: 500, + dropDownOptions: { + hideOnOutsideClick: false, + }, + }, '#dateRangeBox'); + + const dateRangeBox = page.locator('#dateRangeBox'); + + await page.locator('#firstFocusableElement').click() + .pressKey('tab'); + + await page.expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.isFocused) + .ok() + .expect(dateRangeBox.getStartDateBox().isFocused) + .ok() + .expect(dateRangeBox.getEndDateBox().isFocused) + .notOk(); + + await page.keyboard.press('Tab'); + + await page.expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.isFocused) + .ok() + .expect(dateRangeBox.getStartDateBox().isFocused) + .notOk() + .expect(dateRangeBox.getEndDateBox().isFocused) + .ok(); + + await page.keyboard.press('Tab'); + + await page.expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.getPopup().getNavigatorPrevButton().isFocused) + .ok(); + + await page.keyboard.press('Tab'); + + await page.expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.getPopup().getNavigatorCaption().isFocused) + .ok(); + + await page.keyboard.press('Tab'); + + await page.expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.getPopup().getNavigatorNextButton().isFocused) + .ok(); + + await page.keyboard.press('Tab'); + + await page.expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.getPopup().getViewsWrapper().focused) + .ok(); + + await page.keyboard.press('Tab'); + + await page.expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.isFocused) + .ok() + .expect(dateRangeBox.getStartDateBox().isFocused) + .ok() + .expect(dateRangeBox.getEndDateBox().isFocused) + .notOk(); + + await page.keyboard.press('Tab'); + + await page.expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.isFocused) + .ok() + .expect(dateRangeBox.getStartDateBox().isFocused) + .notOk() + .expect(dateRangeBox.getEndDateBox().isFocused) + .ok(); + + }); + + test('DateRangeBox keyboard navigation via `shift+tab` key if applyValueMode is instantly, end -> start -> views -> next -> caption -> prev -> end -> start', async ({ page }) => { + + await appendElementTo(page, '#container', 'div', 'firstFocusableElement'); + await appendElementTo(page, '#container', 'div', 'dateRangeBox'); + await appendElementTo(page, '#container', 'div', 'lastFocusableElement'); + + await createWidget(page, 'dxButton', { + text: 'First Focused Element', + }, '#firstFocusableElement'); + + await createWidget(page, 'dxButton', { + text: 'Last Focused Element', + }, '#lastFocusableElement'); + + await createWidget(page, 'dxDateRangeBox', { + value: ['2021/09/17', '2021/10/24'], + openOnFieldClick: true, + applyValueMode: 'instantly', + opened: false, + width: 500, + }, '#dateRangeBox'); + + const dateRangeBox = page.locator('#dateRangeBox'); + + await page.click(dateRangeBox.getEndDateBox().input); + + await page.expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.isFocused) + .ok() + .expect(dateRangeBox.getStartDateBox().isFocused) + .notOk() + .expect(dateRangeBox.getEndDateBox().isFocused) + .ok(); + + await page.keyboard.press('shift+tab'); + + await page.expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.isFocused) + .ok() + .expect(dateRangeBox.getStartDateBox().isFocused) + .ok() + .expect(dateRangeBox.getEndDateBox().isFocused) + .notOk(); + + await page.keyboard.press('shift+tab'); + + await page.expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.getPopup().getViewsWrapper().focused) + .ok(); + + await page.keyboard.press('shift+tab'); + + await page.expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.getPopup().getNavigatorNextButton().isFocused) + .ok(); + + await page.keyboard.press('shift+tab'); + + await page.expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.getPopup().getNavigatorCaption().isFocused) + .ok(); + + await page.keyboard.press('shift+tab'); + + await page.expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.getPopup().getNavigatorPrevButton().isFocused) + .ok(); + + await page.keyboard.press('shift+tab'); + + await page.expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.getEndDateBox().isFocused) + .ok(); + + await page.keyboard.press('shift+tab'); + + await page.expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.isFocused) + .ok() + .expect(dateRangeBox.getStartDateBox().isFocused) + .ok() + .expect(dateRangeBox.getEndDateBox().isFocused) + .notOk(); + + }); + + test('DateRangeBox should not be closed by press shift+tab key on endDate input', async ({ page }) => { + await createWidget(page, 'dxDateRangeBox', { + value: ['2021/09/17', '2021/10/24'], + openOnFieldClick: true, + opened: true, + width: 500, + dropDownOptions: { + hideOnOutsideClick: false, + }, + calendarOptions: { + focusStateEnabled: false, + }, + }); + + const dateRangeBox = page.locator('#container'); + + await page.click(dateRangeBox.getEndDateBox().input); + + await page.expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.getEndDateBox().isFocused) + .ok(); + + await page.keyboard.press('shift+tab') + .wait(100); + + await page.expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.getStartDateBox().isFocused) + .ok(); + + await page.keyboard.press('shift+tab') + .wait(100); + + await page.expect(dateRangeBox.option('opened')) + .eql(false); + + }); + + [ + { key: 'left', offsetInDays: -1 }, + { key: 'right', offsetInDays: 1 }, + { key: 'up', offsetInDays: -7 }, + { key: 'down', offsetInDays: 7 }, + ].forEach(({ key, offsetInDays }) => { + test(`DateRangeBox start value should be changed after after opening and navigation by '${key}' key and click on 'enter' key`, async ({ page }) => { + await createWidget(page, 'dxDateRangeBox', { + value: initialValue, + openOnFieldClick: false, + }); + + const dateRangeBox = page.locator('#container'); + + await page.click(dateRangeBox.dropDownButton); + + await page.expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.option('value')) + .eql(initialValue); + + await page.pressKey(key) + .pressKey('enter'); + + const expectedStartDate = getDateByOffset(initialValue[0], offsetInDays); + + await page.expect(dateRangeBox.option('opened')) + .eql(true) + .expect(dateRangeBox.option('value')) + .eql([expectedStartDate, new Date(initialValue[1])]); + + }); + + test('Selection in calendar should be started with current startDate value after select startDate if endDate is not specified', async ({ page }) => { + await createWidget(page, 'dxDateRangeBox', { + value: [initialValue[0], null], + }); + + const dateRangeBox = page.locator('#container'); + + await page.click(dateRangeBox.dropDownButton); + + await page.pressKey(key) + .pressKey('enter'); + + await page.keyboard.press('ArrowRight') + .pressKey('right') + .pressKey('right') + .pressKey('right') + .pressKey('right') + .pressKey('enter'); + + const expectedStartDate = getDateByOffset(initialValue[0], offsetInDays); + const expectedEndDate = getDateByOffset(expectedStartDate, 5); + + await page.expect(dateRangeBox.option('opened')) + .eql(false) + .expect(dateRangeBox.option('value')) + .eql([expectedStartDate, expectedEndDate]); + + }); + + test('Selection in calendar should be started with endDate value after select startDate if endDate is specified', async ({ page }) => { + await createWidget(page, 'dxDateRangeBox', { + value: initialValue, + openOnFieldClick: false, + }); + + const dateRangeBox = page.locator('#container'); + + await page.click(dateRangeBox.dropDownButton); + + await page.keyboard.press('ArrowLeft') + .pressKey('enter'); + + await page.pressKey(key) + .pressKey('enter'); + + const expectedStartDate = getDateByOffset(initialValue[0], -1); + const expectedEndDate = getDateByOffset(initialValue[1], offsetInDays); + + await page.expect(dateRangeBox.option('opened')) + .eql(false) + .expect(dateRangeBox.option('value')) + .eql([expectedStartDate, expectedEndDate]); + + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/editors/dateRangeBox/validationMessage.spec.ts b/e2e/testcafe-devextreme/playwright-tests/editors/dateRangeBox/validationMessage.spec.ts new file mode 100644 index 000000000000..aab8ad799033 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/editors/dateRangeBox/validationMessage.spec.ts @@ -0,0 +1,89 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, appendElementTo, insertStylesheetRulesToPage } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('DateRangeBox validation message position', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + const DATERANGEBOX_CLASS = 'dx-daterangebox'; + + const validationMessagePositions = ['auto', 'bottom', 'left', 'right', 'top']; + + const createFormWithDateRangeBox = async (validationMessagePosition: string): Promise => { + const id = `${`dx${new Guid()}`}`; + await appendElementTo(page, '#container', 'div', id, { }); + + const config: any = { + width: '100%', + labelLocation: 'top', + formData: { + DateRange: ['2021/09/17', null], + }, + colCount: 1, + items: [{ + dataField: 'DateRange', + editorType: 'dxDateRangeBox', + label: { + text: 'Date Range', + }, + validationRules: [{ + type: 'required', + message: 'Some message', + }], + editorOptions: { + startDatePlaceholder: 'Start Date', + endDatePlaceholder: 'End Date', + validationMessageMode: 'always', + validationMessagePosition, + }, + }], + }; + + await createWidget(page, 'dxForm', config, `#${id}`); + + return id; + }; + + test('The validation message overlay for DateRangeBox should be correctly positioned before and after opening', async ({ page }) => { + + for (const id of t.ctx.ids) { + await new Form(`#${id}`).validate(); + } + + await testScreenshot(page, 'The validation message overlay position for DateRangeBox before opening.png', { element: '#container' }); + + for (const id of t.ctx.ids) { + const form = page.locator(`#${id}`); + const dateRangeBox = page.locator(`#${id} .${DATERANGEBOX_CLASS}`); + + await form.validate(); + + await page.click(dateRangeBox.dropDownButton) + .click(dateRangeBox.dropDownButton); + } + + await testScreenshot(page, 'The validation message overlay position for DateRangeBox after opening.png', { element: '#container' }); + + });.before(async ({ page }) => { + t.ctx.ids = []; + + await insertStylesheetRulesToPage(page, ` + #container { width: 900px; height: 800px; display: flex; flex-direction: column; padding: 50px; } + .dx-form { margin: 25px 50px; } + `); + + for (const validationMessagePosition of validationMessagePositions) { + const id = await createFormWithDateRangeBox(validationMessagePosition); + t.ctx.ids.push(id); + } + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/editors/dropDownBox/T1245111_dropDownBox_height.spec.ts b/e2e/testcafe-devextreme/playwright-tests/editors/dropDownBox/T1245111_dropDownBox_height.spec.ts new file mode 100644 index 000000000000..3c68e6a5d232 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/editors/dropDownBox/T1245111_dropDownBox_height.spec.ts @@ -0,0 +1,43 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('Grid on Drop Down Box', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + , + + // T1245111 + test('DataGrid on dropDownBox should appear correctly on window resize', async ({ page }) => { + await createWidget(page, 'dxDropDownBox', { + dataSource: Array.from({ length: 100 }, (_, index) => ({ + Value: index + 1, + Text: `item ${index + 1}`, + })), + dropDownOptions: { + width: 'auto', + }, + contentTemplate: (e) => ($('
') as any).dxDataGrid({ + dataSource: e.component.getDataSource(), + }), + }); + + const dropDownBox = page.locator('#container'); + const overlay = page.locator('.dx-overlay-content'); + + await click(dropDownBox); + await overlay.hover(); + await resizeWindow(800, 800); + await testScreenshot(page, 'T1245111-dropDownBox-resize.png'); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/editors/dropDownBox/popup.spec.ts b/e2e/testcafe-devextreme/playwright-tests/editors/dropDownBox/popup.spec.ts new file mode 100644 index 000000000000..929446ca54dd --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/editors/dropDownBox/popup.spec.ts @@ -0,0 +1,32 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('Drop Down Box\'s Popup', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + const BUTTON_CLASS = 'dx-dropdowneditor-button'; + + test('Popup should have correct height when DropDownBox is opened first time (T1130045)', async ({ page }) => { + await createWidget(page, 'dxDropDownBox', { + dropDownOptions: { + templatesRenderAsynchronously: true, + }, + contentTemplate: '
', + }); + + await page.locator(`.${BUTTON_CLASS}`).click(); + + await testScreenshot(page, 'Popup has correct height on the first opening.png'); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/editors/dropDownButton/common.spec.ts b/e2e/testcafe-devextreme/playwright-tests/editors/dropDownButton/common.spec.ts new file mode 100644 index 000000000000..3a2f15f32e6c --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/editors/dropDownButton/common.spec.ts @@ -0,0 +1,125 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, appendElementTo, insertStylesheetRulesToPage } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('Drop Down Button', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + const DROP_DOWN_BUTTON_CLASS = 'dx-dropdownbutton'; + const BUTTON_GROUP_CLASS = 'dx-buttongroup'; + + const stylingModes = ['text', 'outlined', 'contained']; + const types = ['normal', 'default', 'danger', 'success']; + + test('Item collection should be updated after direct option changing (T817436)', async ({ page }) => { + + await appendElementTo(page, '#container', 'div', 'dropDownButton1', { }); + await appendElementTo(page, '#container', 'div', 'dropDownButton2', { }); + + await createWidget(page, 'dxDropDownButton', { + items: [{ text: 'text1' }, { text: 'text2' }], + displayExpr: 'text', + }, '#dropDownButton1'); + + await createWidget(page, 'dxDropDownButton', { + dataSource: [{ text: 'text1' }, { text: 'text2' }], + displayExpr: 'text', + }, '#dropDownButton2'); + + const dropDownButton1 = page.locator('#dropDownButton1'); + const dropDownButton2 = page.locator('#dropDownButton2'); + + await dropDownButton1.click(); + const list1 = await dropDownButton1.getList(); + await dropDownButton2.click(); + const list2 = await dropDownButton2.getList(); + + await page.expect(list1.getItem().isDisabled).notOk() + .expect(list2.getItem().isDisabled).notOk(); + + await dropDownButton1.option('items[0].disabled', true); + await dropDownButton2.option('dataSource[0].disabled', true); + + await dropDownButton1.click() + .expect(list1.getItem().isDisabled) + .ok() + .click(dropDownButton2.element) + .expect(list2.getItem().isDisabled) + .ok(); + + }); + + [undefined, 120].forEach((width) => { + types.forEach((type) => { + if (width && type !== 'normal') { + return; + } + + test('DropDownButton renders correctly', async ({ page }) => { + + await insertStylesheetRulesToPage(page, `.${DROP_DOWN_BUTTON_CLASS}.dx-widget { display: inline-flex; vertical-align: middle; margin: 2px; } .${BUTTON_GROUP_CLASS} { vertical-align: middle; }`); + + await testScreenshot(page, `DropDownButton render${width ? ' with fixed width' : ''}${type !== 'normal' ? `, type=${type}` : ''}.png`); + + });.before(async ({ page }) => { + t.ctx.ids = []; + + for (const rtlEnabled of [false, true]) { + for (const stylingMode of stylingModes) { + await appendElementTo(page, '#container', 'div', `${stylingMode}-${rtlEnabled}`, { fontSize: '10px' }); + await page.evaluate(() => { + $(`#${stylingMode}-${rtlEnabled}`).text(`StylingMode: ${stylingMode}, rtlEnabled: ${rtlEnabled}`); + }); + + for (const splitButton of [true, false]) { + for (const showArrowIcon of [true, false]) { + for (const icon of ['image', '']) { + for (const text of ['', 'Text']) { + const id = `${`dx${new Guid()}`}`; + + t.ctx.ids.push(id); + await appendElementTo(page, '#container', 'div', id, { }); + await createWidget(page, 'dxDropDownButton', { + width, + rtlEnabled, + items: [{ text: 'text1' }, { text: 'text2' }], + displayExpr: 'text', + type, + text, + icon, + stylingMode, + showArrowIcon, + splitButton, + }, `#${id}`); + } + } + } + } + } + } + }); + }); + }); + + [false, true].forEach((splitButton) => { + test('Button template', async ({ page }) => { + await createWidget(page, 'dxDropDownButton', { + splitButton, + width: 200, + template: () => $('
Custom text
'), + }); + + await testScreenshot(page, `Button template, splitButton=${splitButton}.png`, { element: '#container' }); + + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/editors/dropDownButton/popup.spec.ts b/e2e/testcafe-devextreme/playwright-tests/editors/dropDownButton/popup.spec.ts new file mode 100644 index 000000000000..6a5360569ca3 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/editors/dropDownButton/popup.spec.ts @@ -0,0 +1,42 @@ +import { test, expect } from '@playwright/test'; +import { createWidget } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('Drop Down Button\'s Popup', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('Popup should have correct position when DropDownButton is placed in the right bottom(T1034931)', async ({ page }) => { + await createWidget(page, 'dxDropDownButton', { + items: [1, 2, 3, 4, 5, 6, 7], + elementAttr: { style: 'position: absolute; right: 10px; bottom: 10px;' }, + opened: true, + }); + + const dropDownButton = page.locator('#container'); + const dropDownButtonRect = { + top: await dropDownButton.element.getBoundingClientRectProperty('top'), + left: await dropDownButton.element.getBoundingClientRectProperty('left'), + }; + + const popupContent = page.locator('.dx-overlay-content'); + const popupContentRect = { + bottom: await popupContent.getBoundingClientRectProperty('bottom'), + left: await popupContent.getBoundingClientRectProperty('left'), + }; + + await page.expect(Math.abs(dropDownButtonRect.left - popupContentRect.left)) + .lt(1) + .expect(Math.abs(dropDownButtonRect.left - popupContentRect.left)) + .lt(1); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/editors/fileManager/common.spec.ts b/e2e/testcafe-devextreme/playwright-tests/editors/fileManager/common.spec.ts new file mode 100644 index 000000000000..f5e271d73600 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/editors/fileManager/common.spec.ts @@ -0,0 +1,31 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('FileManager', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('Custom DropDown width for Material and Fluent themes', async ({ page }) => { + await createWidget(page, 'dxFileManager', { + name: 'fileManager', + fileSystemProvider: [], + height: 450, + }); + + const viewModeButton = page.locator('.dx-filemanager-toolbar-viewmode-item'); + + await page.click(viewModeButton); + + await testScreenshot(page, 'drop down width.png', { element: '#container' }); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/editors/fileUploader/index.spec.ts b/e2e/testcafe-devextreme/playwright-tests/editors/fileUploader/index.spec.ts new file mode 100644 index 000000000000..b239cde24e8c --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/editors/fileUploader/index.spec.ts @@ -0,0 +1,35 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('FileUploader - file list visibility', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + const TEST_FILE = './images/test-image-1.png'; + + [true, false].forEach((showFileList) => { + test(`FileUploader with showFileList: ${showFileList} - after file selected`, async ({ page }) => { + await createWidget(page, 'dxFileUploader', { showFileList }); + + const fileUploader = page.locator('#container'); + + await fileUploader.input.setInputFiles([TEST_FILE]); + + await testScreenshot(page, `fileuploader-show-filelist-${showFileList}.png`, { + element: '#container', + }); + + await clearUpload(fileUploader.input); + + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/editors/htmlEditor/common.spec.ts b/e2e/testcafe-devextreme/playwright-tests/editors/htmlEditor/common.spec.ts new file mode 100644 index 000000000000..321cd7141e1c --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/editors/htmlEditor/common.spec.ts @@ -0,0 +1,78 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, appendElementTo, setStyleAttribute } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container-extended.html')}`; + +test.describe('HtmlEditor', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + const MENU_ITEM_CLASS = 'dx-menu-item'; + const SUBMENU_CLASS = 'dx-submenu'; + + [false, true].forEach((toolbar) => { + const selector = toolbar ? '#otherContainer' : '#container'; + const clickTarget = toolbar ? '#otherContainer .dx-bold-format' : '#container'; + const baseScreenName = toolbar ? 'htmleditor-with-toolbar' : 'htmleditor-without-toolbar'; + + test(`T1025549 - ${baseScreenName}`, async ({ page }) => { + + await setStyleAttribute(page, '#container', 'box-sizing: border-box; height: 200px; width: 200px'); + await setStyleAttribute(page, '#otherContainer', 'box-sizing: border-box; height: 200px; width: 200px'); + await appendElementTo(page, '#container', 'div', 'editor'); + await appendElementTo(page, '#otherContainer', 'div', 'editorWithToolbar'); + + await createWidget(page, 'dxHtmlEditor', { + height: 200, + width: '100%', + value: Array(100).fill('string').join('\n'), + }, '#editor'); + + await createWidget(page, 'dxHtmlEditor', { + height: 200, + width: '100%', + value: Array(100).fill('string').join('\n'), + toolbar: { + items: ['bold', 'color'], + }, + }, '#editorWithToolbar'); + + + await testScreenshot(page, `${baseScreenName}.png`, { element: selector }); + + await page.click(Selector(clickTarget)); + + await testScreenshot(page, `${baseScreenName}-focused.png`, { element: selector }); + + }); + }); + + test('AI toolbar item', async ({ page }) => { + + await createWidget(page, 'dxHtmlEditor', { + height: 500, + width: 350, + aiIntegration: {}, + toolbar: { + items: ['ai'], + }, + }); + + const htmlEditor = page.locator('#container'); + + await testScreenshot(page, 'htmleditor-ai-toolbar-item.png', { element: '#container' }); + + await page.click(htmlEditor.toolbar.getItemByName('ai')) + .click(page.locator(`.${SUBMENU_CLASS}`).find(`.${MENU_ITEM_CLASS}`).nth(5)); + + await testScreenshot(page, 'htmleditor-ai-toolbar-item-expanded.png', { element: '#container' }); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/editors/htmlEditor/dialogs/addImage/addImageFromDevice.spec.ts b/e2e/testcafe-devextreme/playwright-tests/editors/htmlEditor/dialogs/addImage/addImageFromDevice.spec.ts new file mode 100644 index 000000000000..bd9f312fad11 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/editors/htmlEditor/dialogs/addImage/addImageFromDevice.spec.ts @@ -0,0 +1,123 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../../tests/container-extended.html')}`; + +test.describe('HtmlEditor - upload image from device', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + const TEST_IMAGE_PATH_1 = './images/test-image-1.png'; + const TEST_IMAGE_PATH_2 = './images/test-image-2.png'; + + test('Image from device should be inserted', async ({ page }) => { + + await createWidget(page, 'dxHtmlEditor', { + height: 600, + width: 800, + imageUpload: { + tabs: ['file'], + }, + toolbar: { items: ['image'] }, + }); + + const htmlEditor = page.locator('#container'); + + await click(htmlEditor.toolbar.getItemByName('image')); + + expect(htmlEditor.dialog.footerToolbar.addButton.isDisabled).toBe(true); + + const { fileUploader } = htmlEditor.dialog.addImageFileForm; + + await fileUploader.input.setInputFiles([TEST_IMAGE_PATH_1]); + + const file = fileUploader.getFile(); + + expect(file.fileName).toBe('test-image-1.png') + + .expect(file.fileSize) + .eql('7 KB') + + .expect(file.statusMessage) + .eql('Ready to upload'); + + await fileUploader.getFile().cancelButton.element.click(); + expect(fileUploader.fileCount).toBe(0); + expect(htmlEditor.dialog.footerToolbar.addButton.isDisabled).toBe(true); + + await fileUploader.input.setInputFiles([TEST_IMAGE_PATH_2]); + + await testScreenshot(page, 'editor-before-click-add-button-from-device.png'); + + expect(htmlEditor.dialog.footerToolbar.addButton.isDisabled).toBe(false); + + await htmlEditor.dialog.footerToolbar.addButton.element.click(); + + await testScreenshot(page, 'editor-after-add-image-from-device.png', { element: htmlEditor.content }); + + }); + + test('Image should be validated and inserted from device', async ({ page }) => { + + await createWidget(page, 'dxHtmlEditor', { + height: 600, + width: 800, + imageUpload: { + tabs: ['file'], + fileUploaderOptions: { + maxFileSize: 8500, + }, + }, + toolbar: { items: ['image'] }, + }); + + const htmlEditor = page.locator('#container'); + + await click(htmlEditor.toolbar.getItemByName('image')); + + const { fileUploader } = htmlEditor.dialog.addImageFileForm; + + await fileUploader.input.setInputFiles([TEST_IMAGE_PATH_2]); + + const file = fileUploader.getFile(); + + expect(file.fileName).toBe('test-image-2.png') + + .expect(file.fileSize) + .eql('10 KB') + + .expect(file.validationMessage) + .eql('File is too large'); + + await fileUploader.getFile().cancelButton.element.click() + .expect(fileUploader.fileCount) + .eql(0); + + expect(htmlEditor.dialog.footerToolbar.addButton.isDisabled).toBe(true); + + await fileUploader.input.setInputFiles([TEST_IMAGE_PATH_1]); + expect(file.fileName).toBe('test-image-1.png') + + .expect(file.fileSize) + .eql('7 KB') + + .expect(file.statusMessage) + .eql('Ready to upload'); + + await testScreenshot(page, 'editor-before-click-add-button-and-validation.png'); + + expect(htmlEditor.dialog.footerToolbar.addButton.isDisabled).toBe(false); + + await htmlEditor.dialog.footerToolbar.addButton.element.click(); + + await testScreenshot(page, 'editor-after-click-add-button-and-validation.png', { element: htmlEditor.content }); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/editors/htmlEditor/dialogs/addImage/addImageUrl.spec.ts b/e2e/testcafe-devextreme/playwright-tests/editors/htmlEditor/dialogs/addImage/addImageUrl.spec.ts new file mode 100644 index 000000000000..fd6ffe71f40e --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/editors/htmlEditor/dialogs/addImage/addImageUrl.spec.ts @@ -0,0 +1,132 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, isMaterial } from '../../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../../tests/container-extended.html')}`; + +test.describe('HtmlEditor - add image url', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + const ADD_IMAGE_POPUP_CONTENT_SELECTOR = '.dx-htmleditor-add-image-popup .dx-overlay-content'; + + test('Image uploader from url appearance', async ({ page }) => { + await createWidget(page, 'dxHtmlEditor', { + height: 600, + width: 800, + toolbar: { items: ['image'] }, + }); + + const htmlEditor = page.locator('#container'); + + await page.click(htmlEditor.toolbar.getItemByName('image')); + + await htmlEditor.dialog.addImageUrlForm.lockButton.element.click(); + + await htmlEditor.dialog.addImageUrlForm.url.element.click(); + + await testScreenshot(page, 'Image uploader from url appearance.png', { element: ADD_IMAGE_POPUP_CONTENT_SELECTOR }); + + }); + + test('Image url should be validate before wil be inserted by add button click', async ({ page }) => { + + await createWidget(page, 'dxHtmlEditor', { + height: 600, + width: 800, + imageUpload: { + tabs: ['url'], + }, + toolbar: { items: ['image'] }, + }); + + const htmlEditor = page.locator('#container'); + + await page.click(htmlEditor.toolbar.getItemByName('image')) + .click(htmlEditor.dialog.footerToolbar.addButton.element); + + expect(htmlEditor.dialog.addImageUrlForm.url.isInvalid).toBe(true); + + await page.typeText(htmlEditor.dialog.addImageUrlForm.url.element, BASE64_IMAGE_1, { + paste: true, + }) + .click(htmlEditor.dialog.footerToolbar.addButton.element); + + await testScreenshot(page, 'add-validated-url-image-by-click.png', { element: htmlEditor.content }); + + }); + + test('Image url should be validate before wil be inserted by add enter press', async ({ page }) => { + + await createWidget(page, 'dxHtmlEditor', { + height: 600, + width: 800, + imageUpload: { + tabs: ['url'], + }, + toolbar: { items: ['image'] }, + }); + + const htmlEditor = page.locator('#container'); + + await page.click(htmlEditor.toolbar.getItemByName('image')); + + await page.keyboard.press('Enter') + .expect(htmlEditor.dialog.addImageUrlForm.url.isInvalid) + .eql(true); + + await page.typeText(htmlEditor.dialog.addImageUrlForm.url.element, BASE64_IMAGE_1, { + paste: true, + }) + .pressKey('enter'); + + await testScreenshot(page, 'editor-add-validated-url-image-by-enter.png', { element: htmlEditor.content }); + + }); + + test('Image url should be updated', async ({ page }) => { + + await createWidget(page, 'dxHtmlEditor', { + height: 600, + width: 800, + imageUpload: { + tabs: ['url'], + }, + toolbar: { items: ['image'] }, + }); + + const htmlEditor = page.locator('#container'); + + await page.click(htmlEditor.toolbar.getItemByName('image')) + + .expect(htmlEditor.dialog.footerToolbar.addButton.text) + .eql(isMaterial() ? 'ADD' : 'Add'); + + await page.typeText(htmlEditor.dialog.addImageUrlForm.url.element, BASE64_IMAGE_1, { + paste: true, + }) + .click(htmlEditor.dialog.footerToolbar.addButton.element); + + await testScreenshot(page, 'editor-add-url-image-before-updated.png', { element: htmlEditor.content }); + + await page.click(htmlEditor.toolbar.getItemByName('image')) + + .expect(htmlEditor.dialog.footerToolbar.addButton.text) + .eql(isMaterial() ? 'UPDATE' : 'Update'); + + await page.typeText(htmlEditor.dialog.addImageUrlForm.url.element, BASE64_IMAGE_2, { + paste: true, + replace: true, + }) + .click(htmlEditor.dialog.footerToolbar.addButton.element); + + await testScreenshot(page, 'editor-add-url-image-after-updated.png', { element: htmlEditor.content }); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/editors/htmlEditor/dialogs/addImage/common.spec.ts b/e2e/testcafe-devextreme/playwright-tests/editors/htmlEditor/dialogs/addImage/common.spec.ts new file mode 100644 index 000000000000..874d5c1c0bb5 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/editors/htmlEditor/dialogs/addImage/common.spec.ts @@ -0,0 +1,152 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../../tests/container-extended.html')}`; + +test.describe('HtmlEditor - common', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + const TEST_IMAGE_PATH_1 = './images/test-image-1.png'; + + const ADD_IMAGE_POPUP_CONTENT_SELECTOR = '.dx-htmleditor-add-image-popup .dx-overlay-content'; + + test('TabPanel in HtmlEditor must have correct borders', async ({ page }) => { + + await createWidget(page, 'dxHtmlEditor', { + height: 600, + width: 800, + imageUpload: { + tabs: ['file', 'url'], + }, + toolbar: { items: ['image'] }, + }); + + const htmlEditor = page.locator('#container'); + + await click(htmlEditor.toolbar.getItemByName('image')); + + await testScreenshot(page, 'tabpanel-in-htmleditor.png', { + element: ADD_IMAGE_POPUP_CONTENT_SELECTOR, + }); + + }); + + test('Add button should be enabled after switch to url form', async ({ page }) => { + + await createWidget(page, 'dxHtmlEditor', { + height: 600, + width: 800, + imageUpload: { + tabs: ['file', 'url'], + }, + toolbar: { items: ['image'] }, + }); + + const htmlEditor = page.locator('#container'); + + await page.click(htmlEditor.toolbar.getItemByName('image')) + + .expect(htmlEditor.dialog.footerToolbar.addButton.isDisabled) + .eql(true); + + await htmlEditor.dialog.tabs.getItem(1).element.click(); + + expect(htmlEditor.dialog.footerToolbar.addButton.isDisabled).toBe(false) + + .typeText(htmlEditor.dialog.addImageUrlForm.url.element, BASE64_IMAGE_1, { + paste: true, + }) + .click(htmlEditor.dialog.footerToolbar.addButton.element); + + }); + + test('Add button should be disable after switch to image upload form', async ({ page }) => { + + await createWidget(page, 'dxHtmlEditor', { + height: 600, + width: 800, + imageUpload: { + tabs: ['url', 'file'], + }, + toolbar: { items: ['image'] }, + }); + + const htmlEditor = page.locator('#container'); + + await page.click(htmlEditor.toolbar.getItemByName('image')) + + .expect(htmlEditor.dialog.footerToolbar.addButton.isDisabled) + .notOk() + + .click(htmlEditor.dialog.footerToolbar.addButton.element) + .expect(htmlEditor.dialog.addImageUrlForm.url.isInvalid) + .ok(); + + await htmlEditor.dialog.tabs.getItem(1).element.click(); + + expect(htmlEditor.dialog.footerToolbar.addButton.isDisabled).toBeTruthy(); + + const { fileUploader } = htmlEditor.dialog.addImageFileForm; + + await fileUploader.input.setInputFiles([TEST_IMAGE_PATH_1]) + + .expect(htmlEditor.dialog.footerToolbar.addButton.isDisabled) + .notOk() + + .click(htmlEditor.dialog.footerToolbar.addButton.element); + + }); + + test('AddImage form shouldn\'t lead to side effects in other forms', async ({ page }) => { + + await createWidget(page, 'dxHtmlEditor', { + height: 600, + width: 800, + imageUpload: { + tabs: ['file', 'url'], + }, + toolbar: { items: ['image', 'link', 'color'] }, + }); + + const htmlEditor = page.locator('#container'); + + await page.click(htmlEditor.toolbar.getItemByName('image')) + + .expect(htmlEditor.dialog.footerToolbar.addButton.isDisabled) + .ok() + + .expect(htmlEditor.dialog.footerToolbar.cancelButton.isDisabled) + .notOk() + + .click(htmlEditor.dialog.footerToolbar.cancelButton.element); + + await page.click(htmlEditor.toolbar.getItemByName('link')) + + .expect(htmlEditor.dialog.footerToolbar.addButton.isDisabled) + .notOk() + + .expect(htmlEditor.dialog.footerToolbar.cancelButton.isDisabled) + .notOk() + + .click(htmlEditor.dialog.footerToolbar.addButton.element); + + await page.click(htmlEditor.toolbar.getItemByName('color')) + + .expect(htmlEditor.dialog.footerToolbar.addButton.isDisabled) + .notOk() + + .expect(htmlEditor.dialog.footerToolbar.cancelButton.isDisabled) + .notOk() + + .click(htmlEditor.dialog.footerToolbar.cancelButton.element); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/editors/htmlEditor/dialogs/aiDialog/common.spec.ts b/e2e/testcafe-devextreme/playwright-tests/editors/htmlEditor/dialogs/aiDialog/common.spec.ts new file mode 100644 index 000000000000..0c7fb16f04e5 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/editors/htmlEditor/dialogs/aiDialog/common.spec.ts @@ -0,0 +1,286 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, insertStylesheetRulesToPage } from '../../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../../tests/container-extended.html')}`; + +test.describe('HtmlEditor: AIDialog', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + const MENU_ITEM_CLASS = 'dx-menu-item'; + const SUBMENU_CLASS = 'dx-submenu'; + const LOADINDICATOR_SEGMENT_CLASS = 'dx-loadindicator-segment'; + const LOADINDICATOR_CONTENT_CLASS = 'dx-loadindicator-content'; + const LOADINDICATOR_ICON_CLASS = 'dx-loadindicator-icon'; + const LOADINDICATOR_SEGMENT_INNER_CLASS = 'dx-loadindicator-segment-inner'; + + const longResult = getLongText(false, 10); + + export async function openAIDialog( + t: TestController, + command: number, + option?: number, + ): Promise { + const htmlEditor = page.locator('#container'); + await page.click(htmlEditor.toolbar.getItemByName('ai')) + .click(page.locator(`.${SUBMENU_CLASS} .${MENU_ITEM_CLASS}`).nth(command)); + + if (option !== undefined) { + await page.click(page.locator(`.${SUBMENU_CLASS} .${MENU_ITEM_CLASS}`).nth(command) + .find(`.${SUBMENU_CLASS} .${MENU_ITEM_CLASS}`).nth(0)); + } + + return htmlEditor; + } + + [ + { name: 'with-no-options', command: 0, option: undefined }, + { name: 'with-options', command: 4, option: 0 }, + ].forEach(({ name, command, option }) => { + test(`initial state ${name}`, async ({ page }) => { + + await createWidget(page, 'dxHtmlEditor', { + height: 600, + width: 900, + aiIntegration: {}, + toolbar: { + items: ['ai'], + }, + }); + + + await openAIDialog(t, command, option); + await testScreenshot(page, `htmleditor-ai-dialog-initial-state-${name}.png`, { element: '#container' }); + + }); + }); + + test('resize window when initial state', async ({ page }) => { + + await createWidget(page, 'dxHtmlEditor', { + height: 600, + aiIntegration: {}, + toolbar: { + items: ['ai'], + }, + }); + + const htmlEditor = await openAIDialog(t, 0); + + await resizeWindow(400, 600); + + const aiDialog = htmlEditor.getAIDialog(); + const menuButton = aiDialog.getMenuButton(); + + await menuButton.click(); + await testScreenshot(page, 'htmleditor-ai-dialog-initial-state-resize-window.png', { element: '#container' }); + + }); + + test('generating state', async ({ page }) => { + + await insertStylesheetRulesToPage(page, ` + .${LOADINDICATOR_SEGMENT_CLASS}, + .${LOADINDICATOR_CONTENT_CLASS}, + .${LOADINDICATOR_ICON_CLASS}, + .${LOADINDICATOR_SEGMENT_INNER_CLASS} { + animation: none !important; + opacity: 1 !important; + } + + .${LOADINDICATOR_SEGMENT_CLASS} { + transform: scale(1) !important; + } + `); + + await createWidget(page, 'dxHtmlEditor', { + height: 600, + width: 900, + aiIntegration: { + changeStyle() {}, + }, + toolbar: { + items: ['ai'], + }, + }); + + await openAIDialog(t, 4, 0); + await testScreenshot(page, 'htmleditor-ai-dialog-generating-state.png', { element: '#container' }); + + }); + + [ + { + name: 'short-result', + result: 'result', + }, + { + name: 'long-result', + result: longResult, + }, + ].forEach(({ name, result }) => { + test(`resultReady state with ${name}`, async ({ page }) => { + + await createWidget(page, 'dxHtmlEditor', { + height: 600, + width: 900, + aiIntegration: { + result, + changeStyle(_, { onComplete }) { onComplete(this.result); }, + }, + toolbar: { + items: ['ai'], + }, + }); + + + const htmlEditor = await openAIDialog(t, 4, 0); + const aiDialog = htmlEditor.getAIDialog(); + const splitButton = aiDialog.getSplitButton(); + + await splitButton.click(); + + await testScreenshot(page, `htmleditor-ai-dialog-result-ready-state-with-${name}.png`, { element: '#container' }); + + }); + }); + + test('asking state', async ({ page }) => { + + await createWidget(page, 'dxHtmlEditor', { + height: 600, + width: 900, + aiIntegration: {}, + toolbar: { + items: ['ai'], + }, + }); + + await openAIDialog(t, 7); + await testScreenshot(page, 'htmleditor-ai-dialog-asking-state.png', { element: '#container' }); + + }); + + test('askAI result ready state', async ({ page }) => { + + await createWidget(page, 'dxHtmlEditor', { + height: 600, + width: 900, + aiIntegration: { + result: longResult, + execute(_, { onComplete }) { onComplete(this.result); }, + }, + toolbar: { + items: ['ai'], + }, + }); + + const htmlEditor = await openAIDialog(t, 7); + const aiDialog = htmlEditor.getAIDialog(); + const promptTextArea = aiDialog.getPromptTextArea(); + const generateButton = aiDialog.getGenerateButton(); + + await promptTextArea.getInput().fill('request'); + await generateButton.click(); + + await testScreenshot(page, 'htmleditor-ai-dialog-ask-ai-result-ready-state.png', { element: '#container' }); + + }); + + test('result ready after canceletion', async ({ page }) => { + + await createWidget(page, 'dxHtmlEditor', { + height: 600, + width: 900, + aiIntegration: { + summarize() { return () => {}; }, + }, + toolbar: { + items: ['ai'], + }, + }); + + const htmlEditor = await openAIDialog(t, 0); + const aiDialog = htmlEditor.getAIDialog(); + const cancelButton = aiDialog.getCancelButton(); + + await cancelButton.click(); + + await testScreenshot(page, 'htmleditor-ai-dialog-result-ready-after-canceletion.png', { element: '#container' }); + + }); + + test('error state', async ({ page }) => { + + await createWidget(page, 'dxHtmlEditor', { + height: 600, + width: 900, + aiIntegration: { + summarize(_, { onError }) { onError(); }, + }, + toolbar: { + items: ['ai'], + }, + }); + + await openAIDialog(t, 0); + await testScreenshot(page, 'htmleditor-ai-dialog-error-state.png', { element: '#container' }); + + }); + + [ + { state: 'initial', configuration: {} }, + { + state: 'generating', + configuration: { + summarize() {}, + }, + }, + { + state: 'result-ready', + configuration: { + result: longResult, + summarize(_, { onComplete }) { onComplete(this.result); }, + }, + }, + { + state: 'error', + configuration: { + summarize(_, { onError }) { onError(); }, + }, + }, + ].forEach(({ state, configuration }) => { + test(`${state} state on small screen`, async ({ page }) => { + + await insertStylesheetRulesToPage(page, ` + .${LOADINDICATOR_SEGMENT_CLASS}, + .${LOADINDICATOR_CONTENT_CLASS}, + .${LOADINDICATOR_ICON_CLASS}, + .${LOADINDICATOR_SEGMENT_INNER_CLASS} { + animation: none !important; + opacity: 1 !important; + } + `); + + await createWidget(page, 'dxHtmlEditor', { + height: 700, + aiIntegration: { ...configuration }, + toolbar: { + items: ['ai'], + }, + }); + + + await openAIDialog(t, 0); + await testScreenshot(page, `htmleditor-ai-dialog-${state}-state-on-small-screen.png`, { element: '#container' }); + + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/editors/htmlEditor/format.spec.ts b/e2e/testcafe-devextreme/playwright-tests/editors/htmlEditor/format.spec.ts new file mode 100644 index 000000000000..e87c9cb5aa97 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/editors/htmlEditor/format.spec.ts @@ -0,0 +1,48 @@ +import { test, expect } from '@playwright/test'; +import { createWidget } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container-extended.html')}`; + +test.describe('HtmlEditor - formats', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('HtmlEditor should keep actual format after "enter" key pressed (T922236)', async ({ page }) => { + + await createWidget(page, 'dxHtmlEditor', { + height: 400, + width: 200, + toolbar: { + items: [ + 'bold', + { + name: 'font', + acceptedValues: ['Arial', 'Terminal'], + }, + ], + }, + }); + + const selectBox = page.locator('.dx-font-format'); + + await selectBox.click(); + + const list = await selectBox.getList(); + + await list.getItem().element.click(); + + expect(selectBox.value).toBe('Arial') + .pressKey('k') + .pressKey('enter') + .expect(selectBox.value) + .eql('Arial'); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/editors/htmlEditor/list.spec.ts b/e2e/testcafe-devextreme/playwright-tests/editors/htmlEditor/list.spec.ts new file mode 100644 index 000000000000..76df09aaf6b6 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/editors/htmlEditor/list.spec.ts @@ -0,0 +1,79 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container-extended.html')}`; + +test.describe('HtmlEditor - lists', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + const orderedListMarkup = ` +
    +
  1. Item 1 +
      +
    1. +
        +
      1. +
      +
    +
  2. +
  3. Item 2 +
      +
    1. +
        +
      1. +
      +
    +
  4. +
+ `; + + const orderedListWithTextMarkup = ` +

Text

+
    +
  1. Text +
      +
    1. 1
    2. +
    3. 2
    4. +
    +
  2. +
  3. Text +
      +
    1. 1
    2. +
    3. 2
    4. +
    +
  4. +
+ `; + + test('ordered list numbering sequence should reset for each list item (T1220554)', async ({ page }) => { + + await createWidget(page, 'dxHtmlEditor', { + height: 200, + width: 200, + value: orderedListMarkup, + }); + + await testScreenshot(page, 'htmleditor-ordered-list-appearance.png', { element: '#container' }); + + }); + + test('should reset nested ordered list counters when preceded by text (T1320286)', async ({ page }) => { + + await createWidget(page, 'dxHtmlEditor', { + height: 200, + width: 200, + value: orderedListWithTextMarkup, + }); + + await testScreenshot(page, 'htmleditor-ordered-list-text-appearance.png', { element: '#container' }); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/editors/loadIndIcator/common.spec.ts b/e2e/testcafe-devextreme/playwright-tests/editors/loadIndIcator/common.spec.ts new file mode 100644 index 000000000000..ad3a366b2a8a --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/editors/loadIndIcator/common.spec.ts @@ -0,0 +1,56 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, insertStylesheetRulesToPage } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('LoadIndicator', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + const LOADINDICATOR_SEGMENT_CLASS = 'dx-loadindicator-segment'; + const LOADINDICATOR_CONTENT_CLASS = 'dx-loadindicator-content'; + const LOADINDICATOR_ICON_CLASS = 'dx-loadindicator-icon'; + const LOADINDICATOR_SEGMENT_INNER_CLASS = 'dx-loadindicator-segment-inner'; + + ['circle', 'sparkle'].forEach((animationType) => { + test(`LoadIndicator: start stage of the ${animationType} animation`, async ({ page }) => { + + await insertStylesheetRulesToPage(page, ` + .${LOADINDICATOR_SEGMENT_CLASS}, + .${LOADINDICATOR_CONTENT_CLASS}, + .${LOADINDICATOR_ICON_CLASS}, + .${LOADINDICATOR_SEGMENT_INNER_CLASS} { + animation: none !important; + opacity: 1 !important; + } + `); + + if (animationType === 'sparkle') { + await insertStylesheetRulesToPage(page, ` + .${LOADINDICATOR_SEGMENT_CLASS} { + transform: scale(1) !important; + } + `); + } + + await createWidget(page, 'dxLoadIndicator', { + width: 128, + height: 128, + animationType, + }); + + + await testScreenshot(page, `LoadIndicator with ${animationType} animation.png`, { + element: '#container', + }); + + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/editors/lookup/common.spec.ts b/e2e/testcafe-devextreme/playwright-tests/editors/lookup/common.spec.ts new file mode 100644 index 000000000000..057b5479b719 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/editors/lookup/common.spec.ts @@ -0,0 +1,169 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, appendElementTo, setStyleAttribute, insertStylesheetRulesToPage, isMaterial, isMaterialBased } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('Lookup', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + const LOOKUP_FIELD_CLASS = 'dx-lookup-field'; + const OVERLAY_CLASS = 'dx-overlay-content'; + + const stylingModes = ['outlined', 'underlined', 'filled']; + const labelModes = ['static', 'floating', 'hidden', 'outside']; + + test('Popup should not be closed if lookup is placed at the page bottom (T1018037)', async ({ page }) => { + await createWidget(page, 'dxLookup', { + items: [1, 2, 3], + usePopover: false, + }); + + const lookup = page.locator('#container'); + + const { getInstance } = lookup; + await page.evaluate(() => { + const $element = (getInstance() as any).$element(); + $element.css({ top: window.innerHeight - $element.height() }); + }); + + await lookup.open(); + + await page.expect(await lookup.isOpened()) + .ok(); + + }); + + if (isMaterial()) { + test('Popup should be flipped if lookup is placed at the page bottom', async ({ page }) => { + + await ClientFunction(() => { + const $element = $('#container'); + $element.css({ top: $(window).height() - $element.height() }); + })(); + + await createWidget(page, 'dxLookup', { + items: [1, 2, 3], + usePopover: false, + opened: true, + dropDownOptions: { + hideOnParentScroll: false, + }, + }); + + + const popupWrapper = page.locator('.dx-overlay-wrapper'); + const popupContent = page.locator('.dx-overlay-content'); + + const popupWrapperTop = await popupWrapper.getBoundingClientRectProperty('top'); + const popupContentTop = await popupContent.getBoundingClientRectProperty('top'); + + expect(popupContentTop).toBeLessThan(popupWrapperTop); + + }); + } + + if (!isMaterialBased()) { + test('Popover should have correct vertical position (T1048128)', async ({ page }) => { + await createWidget(page, 'dxLookup', { + items: Array.from(Array(100).keys()), + }); + + const lookup = page.locator('#container'); + await lookup.open(); + + const popoverArrow = page.locator('.dx-popover-arrow'); + + const lookupElementBottom = await lookup.element.getBoundingClientRectProperty('bottom'); + const popoverArrowTop = await popoverArrow.getBoundingClientRectProperty('top'); + + expect(lookupElementBottom).toBe(popoverArrowTop); + + }); + } + + test('Check popup height with no found data option', async ({ page }) => { + await createWidget(page, 'dxLookup', { dataSource: [], searchEnabled: true }); + + await page.locator(`.${LOOKUP_FIELD_CLASS}`).click(); + await hover(`.${OVERLAY_CLASS}`); + + await testScreenshot(page, 'Lookup with no found data.png'); + + }); + + test('Check popup height in loading state', async ({ page }) => { + await createWidget(page, 'dxLookup', { + dataSource: { + load() { + return new Promise((resolve) => { + setTimeout(() => { + resolve([1, 2, 3]); + }, 5000); + }); + }, + }, + valueExpr: 'id', + displayExpr: 'text', + }); + + await page.locator(`.${LOOKUP_FIELD_CLASS}`).click(); + await hover(`.${OVERLAY_CLASS}`); + + await testScreenshot(page, 'Lookup in loading.png'); + + }); + + test('Lookup appearance', async ({ page }) => { + + await testScreenshot(page, 'Lookup appearance.png'); + + for (const id of t.ctx.ids) { + await setStyleAttribute(page, `#${id}`, 'width: fit-content;'); + } + + await testScreenshot(page, 'Lookup width adjust to fit its content.png'); + + for (const id of t.ctx.ids) { + await setStyleAttribute(page, `#${id}`, 'width: 100px;'); + } + + await testScreenshot(page, 'Lookup appearance with limited width.png'); + + });.before(async ({ page }) => { + t.ctx.ids = []; + + await insertStylesheetRulesToPage(page, '#container { display: grid; align-items: end; grid-template-columns: 1fr 1fr 1fr 1fr 1fr; gap: 5px; }'); + + for (const stylingMode of stylingModes) { + for (const labelMode of labelModes) { + for (const rtlEnabled of [true, false]) { + for (const value of [null, 'Item_text_2']) { + const id = `${`dx${new Guid()}`}`; + + t.ctx.ids.push(id); + await appendElementTo(page, '#container', 'div', id, { }); + + const options: any = { + items: ['Item_text_1', 'Item_text_2'], + label: 'label text', + labelMode, + stylingMode, + rtlEnabled, + value, + }; + + await createWidget(page, 'dxLookup', options, `#${id}`); + } + } + } + } + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/editors/numberBox/label.spec.ts b/e2e/testcafe-devextreme/playwright-tests/editors/numberBox/label.spec.ts new file mode 100644 index 000000000000..59cf8d8c8f89 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/editors/numberBox/label.spec.ts @@ -0,0 +1,85 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, appendElementTo, insertStylesheetRulesToPage, isMaterial } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('NumberBox_Label', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + const NUMBERBOX_CLASS = 'dx-numberbox'; + + const stylingModes: EditorStyle[] = ['outlined', 'underlined', 'filled']; + const buttonsList: (TextEditorButton | NumberBoxPredefinedButton)[][] = [ + ['clear'], + [{ name: 'custom', location: 'after', options: { icon: 'home' } }, 'clear', 'spins'], + ['clear', { name: 'custom', location: 'after', options: { icon: 'home' } }, 'spins'], + ['clear', 'spins', { name: 'custom', location: 'after', options: { icon: 'home' } }], + [{ name: 'custom', location: 'before', options: { icon: 'home' } }, 'clear', 'spins'], + ]; + + const createNumberBox = async (options?: Properties): Promise => { + const id = `${`dx${new Guid()}`}`; + + await appendElementTo(page, '#container', 'div', id, {}); + await createWidget(page, 'dxNumberBox', { + value: Math.PI, + showClearButton: true, + showSpinButtons: true, + ...options, + }, `#${id}`); + + return id; + }; + test('Label for dxNumberBox', async ({ page }) => { + + await insertStylesheetRulesToPage(page, '#container { display: flex; flex-direction: column; width: 300px; height: 400px; gap: 8px; }'); + if (isMaterial()) { + await insertStylesheetRulesToPage(page, '#container .dx-widget, #container .dx-widget input { font-family: sans-serif; }'); + } + + for (const stylingMode of stylingModes) { + const options = { + width: '100%', + label: 'label text', + stylingMode, + }; + await createNumberBox({ + ...options, + // @ts-expect-error string instead of number + value: 'text', + }); + await createNumberBox({ + ...options, + value: 123, + }); + } + + await testScreenshot(page, 'NumberBox label.png'); + + }); + + test('NumberBox with buttons container', async ({ page }) => { + + await insertStylesheetRulesToPage(page, `#container { display: flex; flex-wrap: wrap; } .${NUMBERBOX_CLASS} { width: 220px; margin: 2px; }`); + + for (const stylingMode of stylingModes) { + for (const buttons of buttonsList) { + await createNumberBox({ stylingMode, buttons }); + } + + await createNumberBox({ stylingMode, rtlEnabled: true }); + await createNumberBox({ stylingMode, isValid: false }); + } + + await testScreenshot(page, 'NumberBox render with buttons container.png'); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/editors/overlays/dialog.spec.ts b/e2e/testcafe-devextreme/playwright-tests/editors/overlays/dialog.spec.ts new file mode 100644 index 000000000000..941965c753bb --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/editors/overlays/dialog.spec.ts @@ -0,0 +1,49 @@ +import { test, expect } from '@playwright/test'; +import { testScreenshot } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('Dialog', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + const DX_DIALOG_CLASS = 'dx-dialog'; + + [ + 'alert', + 'confirm', + 'custom', + ].forEach((dialogType) => { + test(`Dialog appearance (${dialogType})`, async ({ page }) => { + + const dialogArgs = dialogType === 'custom' + ? { title: 'custom', messageHtml: 'message', buttons: [{ text: 'Custom button' }] } + : dialogType; + + await page.evaluate(() => { + const dialogFunction = (window as any).DevExpress.ui.dialog[dialogType]; + + if (dialogType === 'custom') { + dialogFunction(dialogArgs).show(); + } else { + dialogFunction(dialogArgs); + } + }); + + + await testScreenshot(page, `Dialog appearance (${dialogType}).png`); + + await page.evaluate(() => { + $(`.${DX_DIALOG_CLASS}`).remove(); + }); + + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/editors/overlays/popup.drag.spec.ts b/e2e/testcafe-devextreme/playwright-tests/editors/overlays/popup.drag.spec.ts new file mode 100644 index 000000000000..bfeeaa50c089 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/editors/overlays/popup.drag.spec.ts @@ -0,0 +1,162 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, appendElementTo } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('Popup', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('Popup can not be dragged outside of the container (window)', async ({ page }) => { + await createWidget(page, 'dxPopup', { + width: 100, + height: 100, + visible: true, + dragEnabled: true, + animation: undefined, + }); + + const popup = page.locator('#container'); + + const content = popup.getContent(); + const toolbar = popup.getToolbar(); + + const popupRect: { bottom: number; top: number; left: number; right: number } = { + bottom: 0, top: 0, left: 0, right: 0, + }; + + await (async () => { + const box = await toolbar.boundingBox(); + if (box) { + await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2); + await page.mouse.down(); + await page.mouse.move(box.x + box.width / 2 + -10000, box.y + box.height / 2 + -10000, { steps: 10 }); + await page.mouse.up(); + } + })(); + + await asyncForEach(['bottom', 'left', 'top', 'right'], async (prop) => { + popupRect[prop] = await content.getBoundingClientRectProperty(prop); + }); + + expect(popupRect.top).toBe(0); + + expect(popupRect.left).toBe(0); + + await (async () => { + const box = await toolbar.boundingBox(); + if (box) { + await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2); + await page.mouse.down(); + await page.mouse.move(box.x + box.width / 2 + 10000, box.y + box.height / 2 + 10000, { steps: 10 }); + await page.mouse.up(); + } + })(); + + await asyncForEach(['bottom', 'left', 'top', 'right'], async (prop) => { + popupRect[prop] = await content.getBoundingClientRectProperty(prop); + }); + + expect(popupRect.bottom).toBe(700); + + expect(popupRect.right).toBe(700); + + }); + + test('Popup can not be dragged if content bigger than container', async ({ page }) => { + + await appendElementTo(page, '#container', 'div', 'popup', {}); + await appendElementTo(page, '#container', 'div', 'popupContainer', { width: '99px', height: '99px' }); + + await createWidget(page, 'dxPopup', { + position: { of: '#popupContainer' }, + container: '#popupContainer', + visible: true, + width: 100, + height: 100, + animation: undefined, + }, '#popup'); + + const popup = page.locator('#popup'); + + const content = popup.getContent(); + const toolbar = popup.getToolbar(); + + const popupPosition: { top: number; left: number } = { + top: 0, left: 0, + }; + + const newPopupPosition: { top: number; left: number } = { + top: 0, left: 0, + }; + + await asyncForEach(['left', 'top'], async (prop) => { + popupPosition[prop] = await content.getBoundingClientRectProperty(prop); + }); + + await (async () => { + const box = await toolbar.boundingBox(); + if (box) { + await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2); + await page.mouse.down(); + await page.mouse.move(box.x + box.width / 2 + 50, box.y + box.height / 2 + 50, { steps: 10 }); + await page.mouse.up(); + } + })(); + + await asyncForEach(['left', 'top'], async (prop) => { + newPopupPosition[prop] = await content.getBoundingClientRectProperty(prop); + }); + + expect(popupPosition.top).toBe(newPopupPosition.top); + + expect(popupPosition.left).toBe(newPopupPosition.left); + + }); + + test('Popup can be dragged outside of the container if dragOutsideBoundary is enabled', async ({ page }) => { + await createWidget(page, 'dxPopup', { + width: 100, + height: 100, + visible: true, + dragEnabled: true, + dragOutsideBoundary: true, + animation: undefined, + }); + + const popup = page.locator('#container'); + + const content = popup.getContent(); + const toolbar = popup.getToolbar(); + + const popupPosition: { top: number; left: number } = { + top: 0, left: 0, + }; + + await (async () => { + const box = await toolbar.boundingBox(); + if (box) { + await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2); + await page.mouse.down(); + await page.mouse.move(box.x + box.width / 2 + -10000, box.y + box.height / 2 + -10000, { steps: 10 }); + await page.mouse.up(); + } + })(); + + await asyncForEach(['left', 'top'], async (prop) => { + popupPosition[prop] = await content.getBoundingClientRectProperty(prop); + }); + + expect(popupPosition.top).toBeLessThan(0); + + expect(popupPosition.left).toBeLessThan(0); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/editors/overlays/popup.spec.ts b/e2e/testcafe-devextreme/playwright-tests/editors/overlays/popup.spec.ts new file mode 100644 index 000000000000..f4f1a34d90b7 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/editors/overlays/popup.spec.ts @@ -0,0 +1,167 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, appendElementTo, setStyleAttribute } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('Popup', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('Popup should be centered regarding the container even if container is animated (T920408)', async ({ page }) => { + await page.waitForTimeout(500); + + const wrapper = page.locator('#content .dx-overlay-wrapper'); + const content = wrapper.find('.dx-overlay-content'); + + const wrapperRect: { bottom: number; top: number; left: number; right: number } = { + bottom: 0, top: 0, left: 0, right: 0, + }; + const contentRect: { bottom: number; top: number; left: number; right: number } = { + bottom: 0, top: 0, left: 0, right: 0, + }; + + await asyncForEach(['bottom', 'left', 'right', 'top'], async (prop) => { + wrapperRect[prop] = await wrapper.getBoundingClientRectProperty(prop); + contentRect[prop] = await content.getBoundingClientRectProperty(prop); + }); + + const wrapperVerticalCenter = (wrapperRect.bottom + wrapperRect.top) / 2; + const wrapperHorizontalCenter = (wrapperRect.left + wrapperRect.right) / 2; + const contentVerticalCenter = (contentRect.bottom + contentRect.top) / 2; + const contentHorizontalCenter = (contentRect.left + contentRect.right) / 2; + + await page.expect(wrapperVerticalCenter) + .within(contentVerticalCenter - 0.5, contentVerticalCenter + 0.5); + + await page.expect(wrapperHorizontalCenter) + .within(contentHorizontalCenter - 0.5, contentHorizontalCenter + 0.5); + + });.before(async ({ page }) => { + await appendElementTo(page, '#container', 'div', 'content', {}); + await setStyleAttribute(page, '#content', 'width: 100%; height: 100%;'); + await createWidget(page, 'dxPopup', { + width: 600, + height: 400, + visible: true, + }, undefined, { disableFxAnimation: false }); + + await appendElementTo(page, '#container', 'div', 'innerContainer', {}); + await page.waitForTimeout(500); + + await createWidget(page, 'dxPopup', { + position: { of: '#content' }, + container: '#content', + visible: true, + width: 100, + height: 100, + }, '#innerContainer', { disableFxAnimation: false }); + }); + + test('Popup wrapper left top corner should be the same as the container right left corner even if container is animated', async ({ page }) => { + await page.waitForTimeout(500); + + const wrapper = page.locator('#content .dx-overlay-wrapper'); + const container = wrapper.parent(); + + const wrapperRect: { top: number; left: number } = { top: 0, left: 0 }; + const containerRect: { top: number; left: number } = { top: 0, left: 0 }; + + await asyncForEach(['left', 'top'], async (prop) => { + wrapperRect[prop] = await wrapper.getBoundingClientRectProperty(prop); + containerRect[prop] = await container.getBoundingClientRectProperty(prop); + }); + + await page.expect(wrapperRect.top) + .within(containerRect.top - 0.5, containerRect.top + 0.5); + + await page.expect(wrapperRect.left) + .within(containerRect.left - 0.5, containerRect.left + 0.5); + + });.before(async ({ page }) => { + await appendElementTo(page, '#container', 'div', 'content', {}); + await setStyleAttribute(page, '#content', 'width: 100%; height: 100%;'); + await createWidget(page, 'dxPopup', { + width: 600, + height: 400, + visible: true, + }, undefined, { disableFxAnimation: false }); + + await appendElementTo(page, '#container', 'div', 'innerContainer', {}); + await page.waitForTimeout(500); + + await createWidget(page, 'dxPopup', { + position: { of: '#content' }, + container: '#content', + visible: true, + width: 100, + height: 100, + }, '#innerContainer', { disableFxAnimation: false }); + }); + + test('There should not be any errors when position.of is html (T946851)', async ({ page }) => { + await createWidget(page, 'dxPopup', { + position: { of: 'html' }, + visible: true, + }); + + expect(true).toBeTruthy(); + + }); + + test('Popup should be centered regarding the window after position.boundary is set to window', async ({ page }) => { + await createWidget(page, 'dxPopup', { + width: 300, + height: 200, + visible: true, + animation: undefined, + position: { + boundary: '#otherContainer', + }, + onShown: ClientFunction((e) => { + e.component.option('position.boundary', window); + }), + }, undefined, { disableFxAnimation: false }); + + const popup = page.locator('#container'); + const initialRect: { + bottom: number; + top: number; + left: number; + right: number; + } = { + bottom: 0, + top: 0, + left: 0, + right: 0, + }; + const wrapperRect = initialRect; + const contentRect = initialRect; + + await asyncForEach(['bottom', 'left', 'right', 'top'], async (prop) => { + wrapperRect[prop] = await popup + .getWrapper() + .getBoundingClientRectProperty(prop); + contentRect[prop] = await popup.getContent() + .getBoundingClientRectProperty(prop); + }); + + const wrapperVerticalCenter = (wrapperRect.bottom + wrapperRect.top) / 2; + const wrapperHorizontalCenter = (wrapperRect.left + wrapperRect.right) / 2; + const contentVerticalCenter = (contentRect.bottom + contentRect.top) / 2; + const contentHorizontalCenter = (contentRect.left + contentRect.right) / 2; + + await page.expect(wrapperVerticalCenter) + .within(contentVerticalCenter - 0.5, contentVerticalCenter + 0.5); + + await page.expect(wrapperHorizontalCenter) + .within(contentHorizontalCenter - 0.5, contentHorizontalCenter + 0.5); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/editors/overlays/resizeObserverIntegration.spec.ts b/e2e/testcafe-devextreme/playwright-tests/editors/overlays/resizeObserverIntegration.spec.ts new file mode 100644 index 000000000000..ff5fbb902da6 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/editors/overlays/resizeObserverIntegration.spec.ts @@ -0,0 +1,242 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, setStyleAttribute } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('Popup', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('Popup should be centered regarding the container even if content dimension is changed during animation', async ({ page }) => { + await createWidget(page, 'dxPopup', { + width: 'auto', + height: 'auto', + contentTemplate: () => $('
').attr({ id: 'content' }).css({ width: '100px', height: '100px' }), + }, undefined, { disableFxAnimation: false }); + + const popup = page.locator('#container'); + + await popup.show(); + await setStyleAttribute(page, '#content', 'width: 300px; height: 300px;'); + await page.waitForTimeout(100); + + const wrapper = popup.getWrapper(); + const content = popup.getContent(); + + const wrapperRect: { bottom: number; top: number; left: number; right: number } = { + bottom: 0, top: 0, left: 0, right: 0, + }; + const contentRect: { bottom: number; top: number; left: number; right: number } = { + bottom: 0, top: 0, left: 0, right: 0, + }; + + await asyncForEach(['bottom', 'left', 'right', 'top'], async (prop) => { + wrapperRect[prop] = await wrapper.getBoundingClientRectProperty(prop); + contentRect[prop] = await content.getBoundingClientRectProperty(prop); + }); + + const wrapperVerticalCenter = (wrapperRect.bottom + wrapperRect.top) / 2; + const wrapperHorizontalCenter = (wrapperRect.left + wrapperRect.right) / 2; + const contentVerticalCenter = (contentRect.bottom + contentRect.top) / 2; + const contentHorizontalCenter = (contentRect.left + contentRect.right) / 2; + + await page.expect(wrapperVerticalCenter) + .within(contentVerticalCenter - 0.5, contentVerticalCenter + 0.5); + + await page.expect(wrapperHorizontalCenter) + .within(contentHorizontalCenter - 0.5, contentHorizontalCenter + 0.5); + + }); + + test('Popup should be centered regarding the container even if popup dimension option is changed during animation', async ({ page }) => { + await createWidget(page, 'dxPopup', { + width: 'auto', + height: 'auto', + contentTemplate: () => $('
').attr({ id: 'content' }).css({ width: '100px', height: '100px' }), + }, undefined, { disableFxAnimation: false }); + + const popup = page.locator('#container'); + + await popup.show(); + await setStyleAttribute(page, '#content', 'width: 300px; height: 300px;'); + await page.waitForTimeout(100); + + const wrapper = popup.getWrapper(); + const content = popup.getContent(); + + const wrapperRect: { bottom: number; top: number; left: number; right: number } = { + bottom: 0, top: 0, left: 0, right: 0, + }; + const contentRect: { bottom: number; top: number; left: number; right: number } = { + bottom: 0, top: 0, left: 0, right: 0, + }; + + await asyncForEach(['bottom', 'left', 'right', 'top'], async (prop) => { + wrapperRect[prop] = await wrapper.getBoundingClientRectProperty(prop); + contentRect[prop] = await content.getBoundingClientRectProperty(prop); + }); + + const wrapperVerticalCenter = (wrapperRect.bottom + wrapperRect.top) / 2; + const wrapperHorizontalCenter = (wrapperRect.left + wrapperRect.right) / 2; + const contentVerticalCenter = (contentRect.bottom + contentRect.top) / 2; + const contentHorizontalCenter = (contentRect.left + contentRect.right) / 2; + + await page.expect(wrapperVerticalCenter) + .within(contentVerticalCenter - 0.5, contentVerticalCenter + 0.5); + + await page.expect(wrapperHorizontalCenter) + .within(contentHorizontalCenter - 0.5, contentHorizontalCenter + 0.5); + + }); + + test('Popup should be centered regarding the container even if content dimension is changed', async ({ page }) => { + await createWidget(page, 'dxPopup', { + width: 'auto', + height: 'auto', + contentTemplate: () => $('
').attr({ id: 'content' }).css({ width: '100px', height: '100px' }), + animation: null, + }, undefined, { disableFxAnimation: false }); + + const popup = page.locator('#container'); + + await popup.show(); + await setStyleAttribute(page, '#content', 'width: 300px; height: 300px;'); + await page.waitForTimeout(100); + + const wrapper = popup.getWrapper(); + const content = popup.getContent(); + + const wrapperRect: { bottom: number; top: number; left: number; right: number } = { + bottom: 0, top: 0, left: 0, right: 0, + }; + const contentRect: { bottom: number; top: number; left: number; right: number } = { + bottom: 0, top: 0, left: 0, right: 0, + }; + + await asyncForEach(['bottom', 'left', 'right', 'top'], async (prop) => { + wrapperRect[prop] = await wrapper.getBoundingClientRectProperty(prop); + contentRect[prop] = await content.getBoundingClientRectProperty(prop); + }); + + const wrapperVerticalCenter = (wrapperRect.bottom + wrapperRect.top) / 2; + const wrapperHorizontalCenter = (wrapperRect.left + wrapperRect.right) / 2; + const contentVerticalCenter = (contentRect.bottom + contentRect.top) / 2; + const contentHorizontalCenter = (contentRect.left + contentRect.right) / 2; + + await page.expect(wrapperVerticalCenter) + .within(contentVerticalCenter - 0.5, contentVerticalCenter + 0.5); + + await page.expect(wrapperHorizontalCenter) + .within(contentHorizontalCenter - 0.5, contentHorizontalCenter + 0.5); + + }); + + test('popup should be repositioned after window resize', async ({ page }) => { + await createWidget(page, 'dxPopup', { + animation: null, + visible: true, + width: 100, + height: 100, + }, undefined, { disableFxAnimation: false }); + + const popup = page.locator('#container'); + + const wrapper = popup.getWrapper(); + const content = popup.getContent(); + + const wrapperRect: { bottom: number; top: number; left: number; right: number } = { + bottom: 0, top: 0, left: 0, right: 0, + }; + const contentRect: { bottom: number; top: number; left: number; right: number } = { + bottom: 0, top: 0, left: 0, right: 0, + }; + + await asyncForEach(['bottom', 'left', 'right', 'top'], async (prop) => { + wrapperRect[prop] = await wrapper.getBoundingClientRectProperty(prop); + contentRect[prop] = await content.getBoundingClientRectProperty(prop); + }); + + const wrapperVerticalCenter = (wrapperRect.bottom + wrapperRect.top) / 2; + const wrapperHorizontalCenter = (wrapperRect.left + wrapperRect.right) / 2; + const contentVerticalCenter = (contentRect.bottom + contentRect.top) / 2; + const contentHorizontalCenter = (contentRect.left + contentRect.right) / 2; + + await page.expect(wrapperVerticalCenter) + .within(contentVerticalCenter - 0.5, contentVerticalCenter + 0.5); + + await page.expect(wrapperHorizontalCenter) + .within(contentHorizontalCenter - 0.5, contentHorizontalCenter + 0.5); + + }); + + test('Popup dimensions should be correct after width or height animation', async ({ page }) => { + await createWidget(page, 'dxPopup', { + visible: true, + animation: { + show: { + from: { width: '10px', height: '10px' }, + to: { width: '300px', height: '300px' }, + }, + }, + }, undefined, { disableFxAnimation: false }); + + const popup = page.locator('#container'); + const content = popup.getContent(); + + await page.waitForTimeout(500); + + const contentRect: { width: number; height: number } = { + width: 0, height: 0, + }; + + await asyncForEach(['width', 'height'], async (prop) => { + contentRect[prop] = await content.getBoundingClientRectProperty(prop); + }); + + expect(contentRect.width).toBe(300); + + expect(contentRect.height).toBe(300); + + }); + + test('Showing and shown events should be raised only once even after resize during animation', async ({ page }) => { + await createWidget(page, 'dxPopup', { + width: 'auto', + height: 'auto', + contentTemplate: () => $('
').attr({ id: 'content' }).css({ width: '100px', height: '100px' }), + }, undefined, { disableFxAnimation: false }); + + const popup = page.locator('#container'); + + await page.evaluate(() => { + (window as any).shownCallCount = 0; + (window as any).showingCallCount = 0; + }); + + const incShown = async () => page.evaluate(() => { ((window as any).shownCallCount as number) += 1; }); + const incShowing = async () => page.evaluate(() => { ((window as any).showingCallCount as number) += 1; }); + + const getShownCounter = async () => page.evaluate(() => (window as any).shownCallCount); + const getShowingCounter = async () => page.evaluate(() => (window as any).shownCallCount); + + await popup.option({ + onShown: incShown, + onShowing: incShowing, + }); + + await popup.show(); + + await page.expect(await getShownCounter()) + .eql(1); + await page.expect(await getShowingCounter()) + .eql(1); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/editors/overlays/scrolling.spec.ts b/e2e/testcafe-devextreme/playwright-tests/editors/overlays/scrolling.spec.ts new file mode 100644 index 000000000000..81d26162ed25 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/editors/overlays/scrolling.spec.ts @@ -0,0 +1,143 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, appendElementTo, insertStylesheetRulesToPage, isMaterialBased } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('Popup scrolling', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + const POPUP_CONTENT_CLASS = 'dx-popup-content'; + + if (!isMaterialBased()) { + [false, true].forEach((shading) => { + [false, true].forEach((enableBodyScroll) => { + [false, true].forEach((fullScreen) => { + test(`Popup native scrolling, shading: ${shading}, enableBodyScroll: ${enableBodyScroll}, fullScreen: ${fullScreen}`, async ({ page }) => { + + await appendElementTo(page, '#container', 'div', 'scrollable-container', { height: '2000px', overflowY: 'auto' }); + await appendElementTo(page, '#scrollable-container', 'div', 'scrollable-content', { height: '3000px' }); + + await appendElementTo(page, '#scrollable-content', 'div', 'inner-container', { + width: '500px', height: '500px', border: '1px solid black', overflow: 'auto', + }); + + await ClientFunction(() => { + const $content = $('
'); + + for (let i = 0; i < 100; i += 1) { + $content.append(`
${i}
`); + } + + $('#scrollable-content').append($content); + })(); + + await appendElementTo(page, '#inner-container', 'div', 'inner-content', { width: '2000px', height: '2000px' }); + await appendElementTo(page, '#scrollable-container', 'div', 'popup', {}); + + await createWidget(page, 'dxPopup', { + width: 400, + height: 400, + shading, + enableBodyScroll, + fullScreen, + contentTemplate: ($content) => { + const popupContent = '\ +
Description
\ +
In the heart of LA\'s business district, the Downtown Inn has a welcoming staff and award winning restaurants that remain open 24 hours a day. Use our conference room facilities to conduct meetings and have a drink at our beautiful rooftop bar.
\ +
\ +
\ +
\ +
Features
\ +
\ +
Concierge
\ +
Restaurant
\ +
Valet Parking
\ +
Fitness Center
\ +
Sauna
\ +
Airport Shuttle
\ +
\ +
\ +
\ +
\ +
Rooms
\ +
\ +
Climate control
\ +
Air conditioning
\ +
Coffee/tea maker
\ +
Iron/ironing
\ +
\ +
\ +
\ +
\ +
In the heart of LA\'s business district, the Downtown Inn has a welcoming staff and award winning restaurants that remain open 24 hours a day. Use our conference room facilities to conduct meetings and have a drink at our beautiful rooftop bar.
\ + '; + + $content.html(popupContent); + }, + }, '#popup'); + + + const popup = page.locator('#popup'); + + const checkBodyStyles = async ({ paddingRight, overflow }) => { + await page.expect(getComputedPropertyValue('body', 'padding-right')) + .eql(paddingRight) + .expect(getComputedPropertyValue('body', 'overflow')) + .eql(overflow) + .expect(getComputedPropertyValue('body', 'position')) + .eql('static') + .expect(getComputedPropertyValue('body', 'top')) + .eql('auto') + .expect(getComputedPropertyValue('body', 'left')) + .eql('auto'); + }; + + const checkPopupStyles = async ({ overflow, overScrollBehavior }) => { + await page.expect(getComputedPropertyValue(`.${POPUP_CONTENT_CLASS}`, 'overflow')) + .eql(overflow) + .expect(getComputedPropertyValue(`.${POPUP_CONTENT_CLASS}`, 'overscroll-behavior')) + .eql(overScrollBehavior); + }; + + await checkBodyStyles({ paddingRight: '0px', overflow: 'visible' }); + + await insertStylesheetRulesToPage(page, 'body { padding-right: 10px; overflow: auto; }'); + + await page.evaluate(() => { + window.scrollTo(0, 300); + }); + + await page.expect(getDocumentScrollTop()) + .eql(300); + + await checkBodyStyles({ paddingRight: '10px', overflow: 'auto' }); + await page.expect(getDocumentScrollTop()) + .eql(300); + + await popup.show(); + + await checkPopupStyles({ overflow: 'auto', overScrollBehavior: 'contain' }); + await checkBodyStyles({ paddingRight: enableBodyScroll ? '10px' : '25px', overflow: enableBodyScroll ? 'auto' : 'hidden' }); + await page.expect(getDocumentScrollTop()) + .eql(300); + + await popup.hide(); + + await checkBodyStyles({ paddingRight: '10px', overflow: 'auto' }); + await page.expect(getDocumentScrollTop()) + .eql(300); + + }); + }); + }); + }); + } +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/editors/overlays/toast.spec.ts b/e2e/testcafe-devextreme/playwright-tests/editors/overlays/toast.spec.ts new file mode 100644 index 000000000000..2e81baa5ea2a --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/editors/overlays/toast.spec.ts @@ -0,0 +1,55 @@ +import { test, expect } from '@playwright/test'; +import { testScreenshot, setClassAttribute, insertStylesheetRulesToPage } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('Toast', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + const types = ['info', 'warning', 'error', 'success']; + const STACK_CONTAINER_SELECTOR = '.dx-toast-stack'; + + const showToast = ClientFunction( + (type) => { + (window as any).DevExpress.ui.notify( + { + message: `Toast ${type}`, + type, + displayTime: 35000000, + animation: { + show: { + type: 'fade', duration: 0, + }, + hide: { type: 'fade', duration: 0 }, + }, + }, + { + position: 'top center', + direction: 'down-push', + }, + + }, + + const hideAllToasts = async () => page.evaluate(() => { + (window as any).DevExpress.ui.hideToasts(); + }); + + test('Toasts', async ({ page }) => { + + await Promise.all(types.map((type) => showToast(type))); + + await insertStylesheetRulesToPage(page, `${STACK_CONTAINER_SELECTOR} { padding: 20px; }`); + await setClassAttribute(page, Selector(STACK_CONTAINER_SELECTOR), `dx-theme-${(process.env.theme ?? 'fluent.blue.light')}-typography`); + + await testScreenshot(page, 'Toasts.png', { element: STACK_CONTAINER_SELECTOR }); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/editors/overlays/toolbarIntegration.spec.ts b/e2e/testcafe-devextreme/playwright-tests/editors/overlays/toolbarIntegration.spec.ts new file mode 100644 index 000000000000..5251548c9df2 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/editors/overlays/toolbarIntegration.spec.ts @@ -0,0 +1,242 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, insertStylesheetRulesToPage, isMaterial } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('Popup_toolbar', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + const COMPONENT_SELECTOR = '#container'; + const CLOSE_BUTTON_SELECTOR = '.dx-closebutton'; + const ANIMATION_DELAY = 500; + + [ + { name: 'dxPopup', Class: Popup }, + { name: 'dxPopover', Class: Popover }, + ].forEach(({ name, Class }) => { + ['bottom', 'top'].forEach((toolbar) => { + [true, false].forEach((rtlEnabled) => { + test(`Extended toolbar should be used in ${name},rtlEnabled=${rtlEnabled},toolbar=${toolbar}`, async ({ page }) => { + + if (isMaterial()) { + await insertStylesheetRulesToPage(page, '.dx-overlay-content, .dx-overlay-content input { font-family: sans-serif !important; }'); + } + await createWidget(page, name as 'dxPopup' | 'dxPopover', { + showCloseButton: true, + contentTemplate: () => $('
').text('\ + Lorem Ipsum is simply dummy text of the printing and typesetting industry.\ + Lorem Ipsum has been the industrys standard dummy text ever since the 1500s,\ + when an unknown printer took a galley of type and scrambled it to make a type specimen book.\ + '), + width: '60%', + height: 300, + showTitle: true, + rtlEnabled, + visible: true, + animation: undefined, + target: COMPONENT_SELECTOR, + hideOnOutsideClick: true, + toolbarItems: [{ + location: 'before', + widget: 'dxButton', + options: { + icon: 'back', + }, + toolbar, + }, { + location: 'before', + widget: 'dxButton', + locateInMenu: 'auto', + options: { + icon: 'refresh', + }, + toolbar, + }, { + location: 'center', + locateInMenu: 'never', + template() { + return $('
Popup\'s title
'); + }, + toolbar, + }, { + location: 'after', + widget: 'dxSelectBox', + locateInMenu: 'auto', + options: { + width: 140, + items: [1, 2, 3, 4, 5], + value: 3, + }, + toolbar, + }, { + location: 'after', + widget: 'dxButton', + locateInMenu: 'auto', + options: { + icon: 'plus', + }, + toolbar, + }, { + locateInMenu: 'always', + widget: 'dxButton', + options: { + icon: 'save', + text: 'Save', + }, + toolbar, + }, { + widget: 'dxButton', + toolbar: toolbar === 'top' + ? 'bottom' + : 'top', + location: 'before', + options: { + icon: 'email', + }, + }, { + widget: 'dxButton', + toolbar: toolbar === 'top' + ? 'bottom' + : 'top', + location: 'after', + options: { + text: 'Close', + }, + }], + }); + + + const instance = new Class(COMPONENT_SELECTOR); + + if (toolbar === 'top') { + const topToolbar = new Toolbar(instance.getToolbar()); + await topToolbar.option('overflowMenuVisible', true); + } else { + const bottomToolbar = new Toolbar(instance.getBottomToolbar()); + await bottomToolbar.option('overflowMenuVisible', true); + } + + await hover(Selector(CLOSE_BUTTON_SELECTOR)); + + await testScreenshot(page, `${name.replace('dx', '')}_${toolbar}_toolbar_menu,rtlEnabled=${rtlEnabled}.png`); + + }); + }); + }); + }); + + function getItemConfig( + text: string, + toolbar: 'top' | 'bottom' = 'top', + location: 'before' | 'center' | 'after' = 'after', + locateInMenu: 'auto' | 'none' = 'none', + ) { + return { + text, + toolbar, + locateInMenu, + location, + }; + } + + const toolbarItems = [ + getItemConfig('First Item'), + getItemConfig('Second Item', 'top', 'after', 'auto'), + getItemConfig('Third Item', 'top', 'after', 'auto'), + getItemConfig('!@#$%^&*()-+=[]{}<>|:;.,!?~^*_(){}<>[]:-=+', 'bottom', 'before'), + getItemConfig('First Item', 'bottom'), + getItemConfig('Second Item', 'bottom', 'after', 'auto'), + getItemConfig('Third Item', 'bottom', 'after', 'auto'), + ]; + + const baseConfiguration = { + title: '!@#$%^&*()-+=[]{}<>|:;.,!?~^*_(){}<>[]:-=+', + width: 'auto', + height: 'auto', + showCloseButton: false, + contentTemplate: () => $('
') + .width(300) + .height(300), + }; + + test('Popup toolbars with wide elements and overflow menu if hidden on init with toolbar items', async ({ page }) => { + await createWidget(page, 'dxPopup', { + ...baseConfiguration, + toolbarItems, + visible: false, + }); + + const instance = new Popup(COMPONENT_SELECTOR); + await instance.option({ visible: true }); + + await page.wait(ANIMATION_DELAY) + .click(instance.getOverflowButton().element); + + await testScreenshot(page, 'Popup toolbars with wide elements and overflow menu before items rebinding.png'); + + const items = await instance.option('toolbarItems'); + items[2].visible = false; + await instance.option('toolbarItems', [...items]); + + await instance.getOverflowButton().element.click(); + + await testScreenshot(page, 'Popup toolbars with wide elements and overflow menu after items rebinding.png'); + + }); + + test('Popup toolbars with wide elements and overflow menu if hidden on init with no toolbar items', async ({ page }) => { + await createWidget(page, 'dxPopup', { + ...baseConfiguration, + toolbarItems: [], + visible: false, + }); + + const instance = new Popup(COMPONENT_SELECTOR); + await instance.option({ visible: true, toolbarItems }); + + await page.wait(ANIMATION_DELAY) + .click(instance.getOverflowButton().element); + + await testScreenshot(page, 'Toolbar before items rebinding if it was hidden without items on init.png'); + + const items = await instance.option('toolbarItems'); + items[2].visible = false; + await instance.option('toolbarItems', [...items]); + + await instance.getOverflowButton().element.click(); + + await testScreenshot(page, 'Toolbar after items rebinding if it was hidden without items on init.png'); + + }); + + test('Popup toolbars with wide elements and overflow menu if shown on init with toolbar items', async ({ page }) => { + await createWidget(page, 'dxPopup', { + ...baseConfiguration, + toolbarItems, + visible: true, + }); + + const instance = new Popup(COMPONENT_SELECTOR); + + await instance.getOverflowButton().element.click(); + + await testScreenshot(page, 'Toolbar before items rebinding if it was visible with items on init.png'); + + const items = await instance.option('toolbarItems'); + items[2].visible = false; + await instance.option('toolbarItems', [...items]); + + await instance.getOverflowButton().element.click(); + + await testScreenshot(page, 'Toolbar after items rebinding if it was visible with items on init.png'); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/editors/radioGroup/common.spec.ts b/e2e/testcafe-devextreme/playwright-tests/editors/radioGroup/common.spec.ts new file mode 100644 index 000000000000..682e19800b9c --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/editors/radioGroup/common.spec.ts @@ -0,0 +1,101 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, appendElementTo, setStyleAttribute } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('Radio Group', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('Radio buttons placed into the template should not be selected after clicking the parent radio button (T816449)', async ({ page }) => { + await createWidget(page, 'dxRadioGroup', { + items: [{}, {}, {}], + itemTemplate: () => ($('
') as any).dxRadioGroup({ + dataSource: [{}, {}, {}], + layout: 'horizontal', + }), + }); + + const parentGroup = page.locator('#container'); + const firstChildGroup = new RadioGroup(parentGroup.getItem().content.child().nth(0)); + const secondChildGroup = new RadioGroup(parentGroup.getItem(1).content.child()); + const thirdChildGroup = new RadioGroup(parentGroup.getItem(2).content.child()); + + const checkGroup = async ( + group: RadioGroup, + firstChecked = false, + secondChecked = false, + thirdChecked = false, + ): Promise => { + await page.expect(group.getItem().radioButton.isChecked).eql(firstChecked) + .expect(group.getItem(1).radioButton.isChecked).eql(secondChecked) + .expect(group.getItem(2).radioButton.isChecked) + .eql(thirdChecked); + }; + + await checkGroup(parentGroup); + await checkGroup(firstChildGroup); + await checkGroup(secondChildGroup); + await checkGroup(thirdChildGroup); + + await parentGroup.getItem().radioButton.element.click(); + await checkGroup(parentGroup, true); + await checkGroup(firstChildGroup); + await checkGroup(secondChildGroup); + await checkGroup(thirdChildGroup); + + await parentGroup.getItem(1).radioButton.element.click(); + await checkGroup(parentGroup, false, true); + await checkGroup(firstChildGroup); + await checkGroup(secondChildGroup); + await checkGroup(thirdChildGroup); + + await parentGroup.getItem(2).radioButton.element.click(); + await checkGroup(parentGroup, false, false, true); + await checkGroup(firstChildGroup); + await checkGroup(secondChildGroup); + await checkGroup(thirdChildGroup); + + await firstChildGroup.getItem().radioButton.element.click(); + await checkGroup(parentGroup, false, false, true); + await checkGroup(firstChildGroup, true); + await checkGroup(secondChildGroup); + await checkGroup(thirdChildGroup); + + await secondChildGroup.getItem(1).radioButton.element.click(); + await checkGroup(parentGroup, false, false, true); + await checkGroup(firstChildGroup, true); + await checkGroup(secondChildGroup, false, true); + await checkGroup(thirdChildGroup); + + await thirdChildGroup.getItem(2).radioButton.element.click(); + await checkGroup(parentGroup, false, false, true); + await checkGroup(firstChildGroup, true); + await checkGroup(secondChildGroup, false, true); + await checkGroup(thirdChildGroup, false, false, true); + + }); + + test('Dot of Radio button placed in scaled container should have valid centering(T1165339)', async ({ page }) => { + + await setStyleAttribute(page, '#container', 'width: 600px; height: 100px;'); + + await appendElementTo(page, '#container', 'div', 'radioGroup'); + await setStyleAttribute(page, '#radioGroup', 'transform: scale(0.7);'); + + await createWidget(page, 'dxRadioGroup', { + items: ['One', 'Two', 'Three'], + value: 'Two', + }, '#radioGroup'); + + await testScreenshot(page, 'RadioGroup in scaled container.png', { element: '#container' }); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/editors/radioGroup/validationMessage.spec.ts b/e2e/testcafe-devextreme/playwright-tests/editors/radioGroup/validationMessage.spec.ts new file mode 100644 index 000000000000..4bdd66814d3c --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/editors/radioGroup/validationMessage.spec.ts @@ -0,0 +1,57 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('Radio Group Validation Message', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + const RADIO_GROUP_CLASS = 'dx-radiogroup'; + + test('message position is right (T1020449)', async ({ page }) => { + await createWidget(page, 'dxForm', { + width: 300, + height: 400, + items: [{ + itemType: 'simple', + dataField: 'PropertyNameId', + editorOptions: { + dataSource: ['HR Manager', 'IT Manager'], + layout: 'horizontal', + }, + editorType: 'dxRadioGroup', + validationRules: [{ + type: 'required', + message: 'The PropertyNameId field is required.', + }], + }, { + itemType: 'button', + horizontalAlignment: 'left', + buttonOptions: { + text: 'Register', + type: 'success', + useSubmitBehavior: true, + }, + }], + }); + + const form = page.locator('#container'); + + await form.validate(); + + const radioGroup = page.locator(`.${RADIO_GROUP_CLASS}`); + + await radioGroup.focus(); + + await testScreenshot(page, 'RadioGroup horizontal validation.png', { element: '#container' }); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/editors/selectBox/actionButton.spec.ts b/e2e/testcafe-devextreme/playwright-tests/editors/selectBox/actionButton.spec.ts new file mode 100644 index 000000000000..4dc64e88e723 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/editors/selectBox/actionButton.spec.ts @@ -0,0 +1,189 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, appendElementTo } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('SelectBox', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + const purePressKey = async (t, key): Promise => { + await page.pressKey(key) + .wait(100); + }; + + test('Click on action button should correctly work with SelectBox containing the field template (T811890)', async ({ page }) => { + await createWidget(page, 'dxSelectBox', { + items: ['item1', 'item2'], + fieldTemplate: (value) => ($('
') as any).dxTextBox({ value }), + }); + + const selectBox = page.locator('#container'); + const { getInstance } = selectBox; + + await ClientFunction( + () => { + (getInstance() as any).option('buttons', [{ + name: 'test', + options: { + icon: 'home', + onClick: () => { + (getInstance() as any).option('value', 'item2'); + (getInstance() as any).focus(); // NOTE: need because of editor input rerendering + }, + }, + }]); + }, + { dependencies: { getInstance } }, + )(); + + await selectBox.click(); + await purePressKey(t, 'alt+up'); + expect(selectBox.isFocused).toBeTruthy() + .expect(await selectBox.isOpened()) + .notOk(); + + const actionButton = selectBox.getButton(0); + await actionButton.click() + .expect(selectBox.isFocused).ok() + .expect(selectBox.value) + .eql('item2'); + + }); + + test('Click on action button after typing should correctly work with SelectBox containing the field template (T811890)', async ({ page }) => { + await createWidget(page, 'dxSelectBox', { + items: ['item1', 'item2'], + fieldTemplate: (value) => ($('
') as any).dxTextBox({ value }), + }); + + const selectBox = page.locator('#container'); + const { getInstance } = selectBox; + + await ClientFunction( + () => { + (getInstance() as any).option('buttons', [{ + name: 'test', + options: { + icon: 'home', + onClick: () => { + (getInstance() as any).option('value', 'item2'); + (getInstance() as any).focus(); // NOTE: need because of editor input rerendering + }, + }, + }]); + }, + { dependencies: { getInstance } }, + )(); + + await selectBox.click(); + await purePressKey(t, 'alt+up'); + expect(selectBox.isFocused).toBeTruthy() + .expect(await selectBox.isOpened()) + .notOk(); + + const actionButton = selectBox.getButton(0); + + await selectBox.input.fill('tt'); + await actionButton.click() + .expect(selectBox.isFocused).ok() + .expect(selectBox.value) + .eql('item2'); + + }); + + test('editor can be focused out after click on action button', async ({ page }) => { + await createWidget(page, 'dxSelectBox', { + items: ['item1', 'item2'], + }); + + const selectBox = page.locator('#container'); + const { getInstance } = selectBox; + + await ClientFunction( + () => { + (getInstance() as any).option('buttons', [{ + name: 'test', + options: { + icon: 'home', + onClick: () => { + (getInstance() as any).option('value', 'item2'); + }, + }, + }]); + }, + { dependencies: { getInstance } }, + )(); + + await selectBox.click(); + expect(selectBox.isFocused).toBeTruthy(); + + const actionButton = selectBox.getButton(0); + await actionButton.click() + .expect(selectBox.isFocused).ok(); + + await purePressKey(t, 'tab'); + expect(selectBox.isFocused).toBeFalsy(); + + }); + + test('selectbox should not be opened after click on disabled action button (T1117453)', async ({ page }) => { + await createWidget(page, 'dxSelectBox', { + items: ['item1', 'item2'], + value: 'item1', + }); + + const selectBox = page.locator('#container'); + const { getInstance } = selectBox; + + await ClientFunction( + () => { + (getInstance() as any).option('buttons', [{ + name: 'test', + options: { + icon: 'home', + type: 'default', + disabled: true, + onClick: () => { + (getInstance() as any).option('value', 'item2'); + }, + }, + }]); + }, + { dependencies: { getInstance } }, + )(); + + const actionButton = selectBox.getButton(0); + await actionButton.click() + .expect(selectBox.isFocused) + .notOk() + .expect(await selectBox.isOpened()) + .notOk() + .expect(selectBox.value) + .eql('item1'); + + }); + + test('SelectBox: positioning content in the custom dropdown button', async ({ page }) => { + + await appendElementTo(page, '#container', 'div', 'selectBox'); + + await createWidget(page, 'dxSelectBox', { + items: ['item1', 'item2'], + value: 'item1', + dropDownButtonTemplate() { + return 'X'; + }, + }, '#container'); + + await testScreenshot(page, 'SelectBox Customize DropDown Button.png', { element: '#container' }); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/editors/selectBox/common.spec.ts b/e2e/testcafe-devextreme/playwright-tests/editors/selectBox/common.spec.ts new file mode 100644 index 000000000000..de52779b9a96 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/editors/selectBox/common.spec.ts @@ -0,0 +1,123 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, appendElementTo, setStyleAttribute } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('SelectBox placeholder', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('Placeholder is visible after items option change when value is not chosen (T1099804)', async ({ page }) => { + + await appendElementTo(page, '#container', 'div', 'selectBox'); + await setStyleAttribute(page, '#container', 'box-sizing: border-box; width: 300px; height: 100px; padding: 8px;'); + + await createWidget(page, 'dxSelectBox', { + width: '100%', + placeholder: 'Choose a value', + }, '#selectBox'); + + const selectBox = page.locator('#selectBox'); + + await selectBox.option('items', [1, 2, 3]); + await testScreenshot(page, 'SelectBox placeholder after items change if value is not choosen.png', { element: '#container' }); + + }); + + test('Pages should be loaded consistently after closing the dropdown popup and filtering the data (T1274576)', async ({ page }) => { + + await appendElementTo(page, '#container', 'div', 'selectBox'); + await setStyleAttribute(page, '#container', 'box-sizing: border-box; width: 300px; height: 100px; padding: 8px;'); + + await createWidget(page, 'dxSelectBox', () => { + const data: { id: number; text: string; anotherId: number }[] = []; + + for (let index = 0; index < 100; index += 1) { + data.push({ + id: index + 1, + text: `item ${index + 1}`, + anotherId: index % 2 === 0 ? 1 : 2, + }); + } + + const sampleAPI = new (window as any).DevExpress.data.ArrayStore({ key: 'id', data }); + const store = new (window as any).DevExpress.data.CustomStore({ + key: 'id', + load(loadOptions) { + return new Promise((resolve) => { + setTimeout(() => { + sampleAPI.load(loadOptions).done((items) => { + resolve(items); + }); + }, 100); + }); + }, + totalCount(loadOptions) { + return sampleAPI.totalCount(loadOptions); + }, + byKey(key) { + return sampleAPI.byKey(key); + }, + }); + + return { + dataSource: { + store, + paginate: true, + pageSize: 6, + }, + valueExpr: 'id', + displayExpr: 'text', + }; + }, '#selectBox'); + + const selectBox = page.locator('#selectBox'); + + await selectBox.option('opened', true); + + const list = await selectBox.getList(); + const items = list.getItems(); + + expect(items.count).toBe(12) + .expect(items.nth(0).textContent) + .eql('item 1') + .expect(items.nth(11).textContent) + .eql('item 12'); + + const scrollingDistance = 50; + await list.scrollTo(scrollingDistance); + await page.waitForTimeout(500); + + await page.locator('body').click(); + + const { getInstance } = selectBox; + + await ClientFunction( + () => { + const dataSource = (getInstance() as any).getDataSource(); + dataSource.filter(['anotherId', '=', 2]); + dataSource.load(); + }, + { dependencies: { getInstance } }, + )(); + await page.waitForTimeout(500); + + await selectBox.option('opened', true); + + await page.waitForTimeout(500); + + expect(items.count).toBe(12) + .expect(items.nth(0).textContent) + .eql('item 2') + .expect(items.nth(11).textContent) + .eql('item 24'); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/editors/selectBox/label.spec.ts b/e2e/testcafe-devextreme/playwright-tests/editors/selectBox/label.spec.ts new file mode 100644 index 000000000000..23ba23e39e0d --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/editors/selectBox/label.spec.ts @@ -0,0 +1,57 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, appendElementTo, setStyleAttribute } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('Label', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + const labelMods = ['floating', 'static', 'outside']; + const stylingModes = ['outlined', 'underlined', 'filled']; + + stylingModes.forEach((stylingMode) => { + labelMods.forEach((labelMode) => { + test(`Label for dxSelectBox labelMode=${labelMode} stylingMode=${stylingMode}`, async ({ page }) => { + + await setStyleAttribute(page, '#container', 'box-sizing: border-box; width: 300px; height: 400px; padding: 8px;'); + + await appendElementTo(page, '#container', 'div', 'selectBox1'); + await appendElementTo(page, '#container', 'div', 'selectBox2'); + + await createWidget(page, 'dxSelectBox', { + width: 100, + label: 'label', + text: '', + labelMode, + stylingMode, + }, '#selectBox1'); + + await createWidget(page, 'dxSelectBox', { + label: `this label is ${'very '.repeat(10)}long`, + text: `this content is ${'very '.repeat(10)}long`, + items: ['item1', 'item2'], + labelMode, + stylingMode, + }, '#selectBox2'); + + + const selectBox2 = page.locator('#selectBox2'); + + await page.locator('#selectBox2').click(); + + await testScreenshot(page, `SelectBox with label-labelMode=${labelMode}-stylingMode=${stylingMode}.png`, { element: '#container' }); + + await click(await selectBox2.getPopup()); + + }); + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/editors/selectBox/popup.spec.ts b/e2e/testcafe-devextreme/playwright-tests/editors/selectBox/popup.spec.ts new file mode 100644 index 000000000000..2bd12c665a33 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/editors/selectBox/popup.spec.ts @@ -0,0 +1,138 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('popup height after load', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('SelectBox without data', async ({ page }) => { + await createWidget(page, 'dxSelectBox', { + dataSource: { + store: [], + paginate: true, + pageSize: 3, + }, + }); + + const selectBox = page.locator('#container'); + + await selectBox.click(); + + await testScreenshot(page, 'SelectBox no data.png'); + + await click(await selectBox.getPopup()); + + }); + + test('SelectBox has a correct popup height for the first opening if the pageSize is equal to dataSource length (T942881)', async ({ page }) => { + await createWidget(page, 'dxSelectBox', { + dataSource: { + store: [], + paginate: true, + pageSize: 3, + }, + }); + + const selectBox = page.locator('#container'); + + await selectBox.click(); + + await selectBox.option('dataSource', { + store: [1, 2, 3], + paginate: true, + pageSize: 3, + }); + + await testScreenshot(page, 'SelectBox pagesize equal datasource items count.png'); + + await click(await selectBox.getPopup()); + + }); + + test('SelectBox has a correct popup height for the first opening if the pageSize is less than dataSource items count', async ({ page }) => { + await createWidget(page, 'dxSelectBox', { + dataSource: { + store: [], + paginate: true, + pageSize: 3, + }, + }); + + const selectBox = page.locator('#container'); + + await selectBox.click(); + + await selectBox.option('dataSource', { + store: [1, 2, 3], + paginate: true, + pageSize: 2, + }); + + await testScreenshot(page, 'SelectBox pagesize less datasource items count.png'); + + await click(await selectBox.getPopup()); + + }); + + test('SelectBox has a correct popup height for the first opening if the pageSize is more than dataSource items count', async ({ page }) => { + await createWidget(page, 'dxSelectBox', { + dataSource: { + store: [], + paginate: true, + pageSize: 3, + }, + }); + + const selectBox = page.locator('#container'); + + await selectBox.click(); + + await selectBox.option('dataSource', { + store: [1, 2, 3], + paginate: true, + pageSize: 5, + }); + + await testScreenshot(page, 'SelectBox pagesize more datasource items count.png'); + + await click(await selectBox.getPopup()); + + }); + + test('SelectBox does not change a popup height after load the last page', async ({ page }) => { + await createWidget(page, 'dxSelectBox', { + dataSource: { + store: [], + paginate: true, + pageSize: 3, + }, + }); + + const selectBox = page.locator('#container'); + + await selectBox.click(); + + await selectBox.option('dataSource', { + store: [1, 2, 3, 4, 5], + paginate: true, + pageSize: 2, + }); + + const list = await selectBox.getList(); + await list.scrollTo(100); + + await testScreenshot(page, 'SelectBox popup height after last page load.png'); + + await click(await selectBox.getPopup()); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/editors/selectBox/toolbarIntegration.spec.ts b/e2e/testcafe-devextreme/playwright-tests/editors/selectBox/toolbarIntegration.spec.ts new file mode 100644 index 000000000000..bd7ae52b3897 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/editors/selectBox/toolbarIntegration.spec.ts @@ -0,0 +1,47 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, isMaterial } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('SelectBox as Toolbar item', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('SelectBox should correctly render its buttons if editor is rendered as a Toolbar item with fieldTemplate (T949859)', async ({ page }) => { + await createWidget(page, 'dxToolbar', { + items: [ + { + widget: 'dxSelectBox', + options: { + buttons: [ + { + name: 'test', + options: { + text: 'test', + }, + }, + ], + fieldTemplate: (_, wrapper) => { + ($('
').appendTo(wrapper) as any).dxTextBox(); + }, + items: [1, 2, 3, 4], + }, + }, + ], + }); + + const selectBox = page.locator('#container'); + const actionButton = selectBox.getButton(0); + + await page.expect(actionButton.getText().innerText) + .eql(isMaterial() ? 'TEST' : 'test'); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/editors/slider/slider.spec.ts b/e2e/testcafe-devextreme/playwright-tests/editors/slider/slider.spec.ts new file mode 100644 index 000000000000..8118350c0ef4 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/editors/slider/slider.spec.ts @@ -0,0 +1,29 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('Slider', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('Slider appearance', async ({ page }) => { + await createWidget(page, 'dxSlider', { + tooltip: { + enabled: true, + showMode: 'always', + position: 'bottom', + }, + }); + + await testScreenshot(page, 'slider-appearance.png', { element: '#container' }); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/editors/tagBox/common.spec.ts b/e2e/testcafe-devextreme/playwright-tests/editors/tagBox/common.spec.ts new file mode 100644 index 000000000000..8a9173b90058 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/editors/tagBox/common.spec.ts @@ -0,0 +1,159 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('TagBox', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('Keyboard navigation should work then tagBox is focused or list is focused', async ({ page }) => { + await createWidget(page, 'dxTagBox', { + items: ['item1', 'item2', 'item3'], + showSelectionControls: true, + selectionMode: 'all', + applyValueMode: 'useButtons', + }); + + const tagBox = page.locator('#container'); + + await tagBox.click(); + + expect(tagBox.isFocused).toBeTruthy() + .expect(await tagBox.isOpened()) + .ok(); + + const list = await tagBox.getList(); + const { selectAll } = list; + const selectAllCheckBox = selectAll.checkBox; + const firstItemCheckBox = list.getItem().checkBox; + const secondItemCheckBox = list.getItem(1).checkBox; + const thirdItemCheckBox = list.getItem(2).checkBox; + + await t + // List is focused + .pressKey('tab') + .expect(selectAllCheckBox.isFocused).ok() + .pressKey('down down down') + .expect(thirdItemCheckBox.isFocused) + .ok() + .pressKey('down') + .expect(selectAllCheckBox.isFocused) + .ok() + .pressKey('up up up') + .expect(firstItemCheckBox.isFocused) + .ok() + .expect(firstItemCheckBox.isChecked) + .notOk() + .pressKey('space') + .expect(firstItemCheckBox.isChecked) + .ok() + .pressKey('enter') + .expect(firstItemCheckBox.isChecked) + .notOk() + + // TagBox is focused + .pressKey('shift+tab') + .expect(tagBox.isFocused) + .ok() + .pressKey('down') + .expect(secondItemCheckBox.isFocused) + .ok() + .pressKey('down down') + .expect(selectAllCheckBox.isFocused) + .ok() + .pressKey('up up up') + .expect(firstItemCheckBox.isFocused) + .ok() + .expect(firstItemCheckBox.isChecked) + .notOk() + .pressKey('space') + .expect(firstItemCheckBox.isChecked) + .ok() + .pressKey('enter') + .expect(firstItemCheckBox.isChecked) + .notOk(); + + }); + + test('Select all checkbox should be focused by tab and closed by escape (T389453)', async ({ page }) => { + await createWidget(page, 'dxTagBox', { + items: ['item1', 'item2', 'item3'], + showSelectionControls: true, + selectionMode: 'all', + applyValueMode: 'useButtons', + }); + + const tagBox = page.locator('#container'); + + await tagBox.click(); + + expect(tagBox.isFocused).toBeTruthy() + .expect(await tagBox.isOpened()) + .ok(); + + const list = await tagBox.getList(); + const { selectAll } = list; + const selectAllCheckBox = selectAll.checkBox; + + await page.keyboard.press('Tab') + .expect(tagBox.isFocused).notOk() + .expect(selectAllCheckBox.isFocused) + .ok() + + .pressKey('shift+tab') + .expect(tagBox.isFocused) + .ok() + .expect(selectAllCheckBox.isFocused) + .notOk() + + .pressKey('tab') + .expect(tagBox.isFocused) + .notOk() + .expect(selectAllCheckBox.isFocused) + .ok(); + + await page.keyboard.press('esc'); + + expect(tagBox.isFocused).toBeTruthy() + .expect(await tagBox.isOpened()) + .notOk(); + + }); + + test('TagBox with selection controls', async ({ page }) => { + await createWidget(page, 'dxTagBox', { + items: [1, 2, 3, 4, 5, 6, 7], + showSelectionControls: true, + width: 300, + }); + + const tagBox = page.locator('#container'); + + await tagBox.click(); + + await testScreenshot(page, 'TagBox with selection controls.png'); + + }); + + test('Placeholder is visible after items option change when value is not chosen (T1099804)', async ({ page }) => { + await createWidget(page, 'dxTagBox', { + width: 300, + placeholder: 'Choose a value', + }); + + const tagBox = page.locator('#container'); + + await tagBox.option('items', [1, 2, 3]); + + await testScreenshot(page, 'TagBox placeholder if value is not choosen.png', { element: '#container' }); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/editors/tagBox/label.spec.ts b/e2e/testcafe-devextreme/playwright-tests/editors/tagBox/label.spec.ts new file mode 100644 index 000000000000..bac1230be689 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/editors/tagBox/label.spec.ts @@ -0,0 +1,95 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, appendElementTo, setStyleAttribute, insertStylesheetRulesToPage, isMaterial } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('TagBox_Label', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + const stylingModes = ['outlined', 'underlined', 'filled']; + const labelModes = ['static', 'floating', 'hidden', 'outside']; + + stylingModes.forEach((stylingMode) => { + test(`Label for dxTagBox stylingMode=${stylingMode}`, async ({ page }) => { + + const componentOptions = { + label: 'label text', + items: [...Array(10)].map((_, i) => `item${i}`), + value: [...Array(5)].map((_, i) => `item${i}`), + stylingMode, + }; + + if (isMaterial()) { + await insertStylesheetRulesToPage(page, '#container .dx-widget { font-family: sans-serif }'); + } + + await appendElementTo(page, '#container', 'div', 'tagBox1', { }); + await appendElementTo(page, '#container', 'div', 'tagBox2', { }); + + await createWidget(page, 'dxTagBox', { + ...componentOptions, + multiline: false, + }, '#tagBox1'); + + await createWidget(page, 'dxTagBox', { + ...componentOptions, + multiline: true, + }, '#tagBox2'); + + + await page.locator('#tagBox2').click(); + + await testScreenshot(page, `TagBox label with stylingMode=${stylingMode}.png`); + + }); + + labelModes.forEach((labelMode) => { + test(`Label shouldn't be cutted for dxTagBox in stylingMode=${stylingMode}, labelMode=${labelMode} (T1104913)`, async ({ page }) => { + + await setStyleAttribute(page, '#container', 'top: 250px;'); + + await createWidget(page, 'dxTagBox', { + width: 200, + label: 'Label text', + labelMode, + stylingMode, + dataSource: { + load() { + return new Promise((resolve) => { + resolve([ + { text: 'item_1' }, + { text: 'item_2' }, + { text: 'item_3' }, + { text: 'item_4' }, + ]); + }); + }, + paginate: true, + pageSize: 20, + }, + }); + + + const tagBox = page.locator('#container'); + + await tagBox.click(); + + const screenshotName = `TagBox label with stylingMode=${stylingMode},labelMode=${labelMode}.png`; + + await tagBox.click(); + await tagBox.click(); + + await testScreenshot(page, screenshotName); + + }); + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/editors/textArea/index.spec.ts b/e2e/testcafe-devextreme/playwright-tests/editors/textArea/index.spec.ts new file mode 100644 index 000000000000..c37716ba7ff9 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/editors/textArea/index.spec.ts @@ -0,0 +1,182 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, appendElementTo, setAttribute } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('TextArea_Height', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + const text = 'Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry\'s standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged.'; + + test('TextArea should have correct height when height is 7em & maxHeight is 5em', async ({ page }) => { + + await setAttribute(page, '#container', 'style', 'width: 300px; height: 400px;'); + + const config = { + maxHeight: '5em', + height: '7em', + width: '100%', + value: text, + }; + + await appendElementTo(page, '#container', 'div', 'textArea1'); + await appendElementTo(page, '#container', 'div', 'textArea2'); + + await createWidget(page, 'dxTextArea', { + ...config, + autoResizeEnabled: true, + }, '#textArea1'); + + await createWidget(page, 'dxTextArea', { + ...config, + autoResizeEnabled: false, + }, '#textArea2'); + + await testScreenshot(page, 'TextArea appearance, height=7em & maxHeight=5em.png', { element: '#container' }); + + }); + + test('TextArea should have correct height when height is 5em & maxHeight is 7em', async ({ page }) => { + + await setAttribute(page, '#container', 'style', 'width: 300px; height: 400px;'); + + const config = { + maxHeight: '7em', + height: '5em', + width: '100%', + value: text, + }; + + await appendElementTo(page, '#container', 'div', 'textArea1'); + await appendElementTo(page, '#container', 'div', 'textArea2'); + + await createWidget(page, 'dxTextArea', { + ...config, + autoResizeEnabled: true, + }, '#textArea1'); + + await createWidget(page, 'dxTextArea', { + ...config, + autoResizeEnabled: false, + }, '#textArea2'); + + await testScreenshot(page, 'TextArea appearance, height=5em & maxHeight=7em.png', { element: '#container' }); + + }); + + test('TextArea should have correct height when maxHeight is 5em', async ({ page }) => { + + await setAttribute(page, '#container', 'style', 'width: 300px; height: 400px;'); + + const config = { + maxHeight: '5em', + width: '100%', + value: text, + }; + + await appendElementTo(page, '#container', 'div', 'textArea1'); + await appendElementTo(page, '#container', 'div', 'textArea2'); + + await createWidget(page, 'dxTextArea', { + ...config, + autoResizeEnabled: true, + }, '#textArea1'); + + await createWidget(page, 'dxTextArea', { + ...config, + autoResizeEnabled: false, + }, '#textArea2'); + + await testScreenshot(page, 'TextArea appearance, maxHeight=5em.png', { element: '#container' }); + + }); + + test('TextArea with font-size style has correct height when maxHeight option is 5em', async ({ page }) => { + + await setAttribute(page, '#container', 'style', 'width: 300px; height: 400px; font-size: 12px;'); + + const config = { + maxHeight: '5em', + width: '100%', + value: text, + }; + + await appendElementTo(page, '#container', 'div', 'textArea1'); + await appendElementTo(page, '#container', 'div', 'textArea2'); + + await createWidget(page, 'dxTextArea', { + ...config, + autoResizeEnabled: true, + }, '#textArea1'); + + await createWidget(page, 'dxTextArea', { + ...config, + autoResizeEnabled: false, + }, '#textArea2'); + + await testScreenshot(page, 'TextArea appearance, maxHeight=5em, font-size=12px.png', { element: '#container' }); + + }); + + test('TextArea has correct height when maxHeight is not defined', async ({ page }) => { + + await setAttribute(page, '#container', 'style', 'width: 300px;'); + + const config = { + width: '100%', + value: text, + autoResizeEnabled: true, + }; + + await appendElementTo(page, '#container', 'div', 'textArea1'); + await appendElementTo(page, '#container', 'div', 'textArea2'); + + await createWidget(page, 'dxTextArea', { + ...config, + }, '#textArea1'); + + await createWidget(page, 'dxTextArea', { + ...config, + value: text + text, + }, '#textArea2'); + + await testScreenshot(page, 'TextArea appearance, maxHeight is not defined.png', { element: '#container' }); + + }); + + test('Height of TextArea input should have the correct height when the maxHeight option is set to 80px (T1221869)', async ({ page }) => { + + await setAttribute(page, '#container', 'style', 'width: 300px; height: 400px;'); + + const config = { + value: text, + width: '100%', + maxHeight: 80, + autoResizeEnabled: true, + }; + + await appendElementTo(page, '#container', 'div', 'textArea1'); + await appendElementTo(page, '#container', 'div', 'textArea2'); + + await createWidget(page, 'dxTextArea', { + ...config, + autoResizeEnabled: true, + }, '#textArea1'); + + await createWidget(page, 'dxTextArea', { + ...config, + autoResizeEnabled: false, + }, '#textArea2'); + + await testScreenshot(page, 'TextArea appearance, maxHeight=80px.png', { element: '#container' }); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/editors/textArea/label.spec.ts b/e2e/testcafe-devextreme/playwright-tests/editors/textArea/label.spec.ts new file mode 100644 index 000000000000..9dd02751ca0a --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/editors/textArea/label.spec.ts @@ -0,0 +1,67 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, appendElementTo } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('Label', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + const labelModes = ['floating', 'static', 'outside']; + const stylingModes = ['outlined', 'underlined', 'filled']; + + test('Label scroll input dxTextArea', async ({ page }) => { + await createWidget(page, 'dxTextArea', { + height: 50, + width: 200, + text: `this content is ${'very '.repeat(10)}long`, + label: 'label text', + }); + + const textArea = page.locator('#container'); + + await scroll(textArea.getInput(), 0, 20); + + await testScreenshot(page, 'TextArea label after scroll.png', { element: '#container' }); + + }); + + stylingModes.forEach((stylingMode) => { + labelModes.forEach((labelMode) => { + test(`Label for dxTextArea labelMode=${labelMode} stylingMode=${stylingMode}`, async ({ page }) => { + + await appendElementTo(page, '#container', 'div', 'textArea1', { }); + await appendElementTo(page, '#container', 'div', 'textArea2', { }); + + await createWidget(page, 'dxTextArea', { + width: 100, + label: 'label', + text: '', + labelMode, + stylingMode, + }, '#textArea1'); + + await createWidget(page, 'dxTextArea', { + label: `this label is ${'very '.repeat(10)}long`, + text: `this content is ${'very '.repeat(10)}long`, + items: ['item1', 'item2'], + labelMode, + stylingMode, + }, '#textArea2'); + + + await page.locator('#textArea2').click(); + + await testScreenshot(page, `TextArea with label-labelMode=${labelMode}-stylingMode=${stylingMode}.png`); + + }); + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/editors/textBox/label.spec.ts b/e2e/testcafe-devextreme/playwright-tests/editors/textBox/label.spec.ts new file mode 100644 index 000000000000..db3197e78796 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/editors/textBox/label.spec.ts @@ -0,0 +1,204 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, appendElementTo, setStyleAttribute, setClassAttribute, insertStylesheetRulesToPage, removeStylesheetRulesFromPage, isMaterial } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('TextBox_Label', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + const visibleLabelModes: LabelMode[] = ['floating', 'static', 'outside']; + const stylingModes: EditorStyle[] = ['outlined', 'underlined', 'filled']; + const buttonsList: (string | TextEditorButton)[][] = [ + ['clear'], + ['clear', { name: 'custom', location: 'after', options: { icon: 'home' } }], + [{ name: 'custom', location: 'after', options: { icon: 'home' } }, 'clear'], + ['clear', { name: 'custom', location: 'before', options: { icon: 'home' } }], + ]; + + const TEXTBOX_CLASS = 'dx-textbox'; + const HOVER_STATE_CLASS = 'dx-state-hover'; + const FOCUSED_STATE_CLASS = 'dx-state-focused'; + const READONLY_STATE_CLASS = 'dx-state-readonly'; + const INVALID_STATE_CLASS = 'dx-invalid'; + + const createTextBox = async (options?: Properties, state?: string): Promise => { + const id = `${`dx${new Guid()}`}`; + + await appendElementTo(page, '#container', 'div', id, {}); + await createWidget(page, 'dxTextBox', { + labelMode: 'floating', + stylingMode: 'outlined', + text: 'Text', + label: 'Label Text', + ...options, + }, `#${id}`); + + if (state) { + await setClassAttribute(page, `#${id}`, state); + } + + return id; + }; + + [ + { labelMode: 'static', expectedWidths: { generic: 82, material: 68, fluent: 74 } }, + { labelMode: 'floating', expectedWidths: { generic: 82, material: 68, fluent: 74 } }, + { labelMode: 'outside', expectedWidths: { generic: 'none', material: 'none', fluent: 'none' } }, + ].forEach(({ labelMode, expectedWidths }) => { + test(`Label max-width should be changed after container width was changed, labelMode is ${labelMode}`, async ({ page }) => { + const textBox = page.locator('#container'); + + const expectedWidth = expectedWidths[(process.env.theme ?? 'fluent.blue.light')]; + + await page.expect(textBox.getLabel().getStyleProperty('max-width')) + .eql(expectedWidth === 'none' ? 'none' : `${expectedWidth}px`); + + await setStyleAttribute(page, page.locator(`#${await textBox.element.getAttribute('id')}`), `width: ${t.ctx.initialWidth + t.ctx.deltaWidth}px;`); + + await page.expect(textBox.getLabel().getStyleProperty('max-width')) + .eql(expectedWidth === 'none' ? 'none' : `${expectedWidth + t.ctx.deltaWidth}px`); + + });.before(async ({ page }) => { + t.ctx.initialWidth = 100; + t.ctx.deltaWidth = 300; + + await createWidget(page, 'dxTextBox', { + width: t.ctx.initialWidth, + label: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.', + labelMode, + }); + }); + }); + + test('Textbox render', async ({ page }) => { + + for (const stylingMode of stylingModes) { + for (const labelMode of visibleLabelModes) { + for (const placeholder of ['Placeholder', '']) { + await createTextBox({ + text: undefined, + placeholder, + stylingMode, + labelMode, + }); + } + + await createTextBox({ text: 'Text value' }); + await createTextBox({ rtlEnabled: true }); + } + for (const placeholder of ['Placeholder', '']) { + await createTextBox({ + text: undefined, + placeholder, + stylingMode, + label: undefined, + }); + } + await createTextBox({ label: undefined, text: 'Text value' }); + await createTextBox({ label: undefined, rtlEnabled: true }); + } + + await insertStylesheetRulesToPage(page, `.${TEXTBOX_CLASS} { display: inline-block; vertical-align: middle; width: 60px; margin: 5px; }`); + + await testScreenshot(page, 'Textbox render with limited width.png', { element: '#container' }); + + await removeStylesheetRulesFromPage(page, ); + + await insertStylesheetRulesToPage(page, `.${TEXTBOX_CLASS} { display: inline-block; vertical-align: middle; width: 260px; margin: 5px; }`); + + await testScreenshot(page, 'Textbox render.png'); + + }); + + test('Textbox states', async ({ page }) => { + + const states = [ + HOVER_STATE_CLASS, + FOCUSED_STATE_CLASS, + READONLY_STATE_CLASS, + INVALID_STATE_CLASS, + `${INVALID_STATE_CLASS} ${FOCUSED_STATE_CLASS}`, + ]; + for (const state of states) { + for (const placeholder of ['Placeholder', '']) { + await createTextBox({ + text: undefined, + placeholder, + }, state); + } + + await createTextBox({ text: 'Text value' }, state); + await createTextBox({ rtlEnabled: true }, state); + } + + await insertStylesheetRulesToPage(page, `.${TEXTBOX_CLASS} { display: inline-block; vertical-align: middle; width: 260px; margin: 5px; }`); + + await testScreenshot(page, 'Textbox states.png', { element: '#container' }); + + }); + + test('Textbox with buttons container', async ({ page }) => { + + if (isMaterial()) { + await insertStylesheetRulesToPage(page, '#container .dx-widget { font-family: sans-serif }'); + } + + for (const stylingMode of stylingModes) { + for (const buttons of buttonsList) { + await createTextBox({ stylingMode, buttons, showClearButton: true }); + await createTextBox({ + stylingMode, buttons, showClearButton: true, isValid: false, + }); + } + } + + await insertStylesheetRulesToPage(page, '#container { display: flex; flex-wrap: wrap; gap: 4px; }'); + + await testScreenshot(page, 'Textbox with buttons container.png'); + + }); + + stylingModes.forEach((stylingMode) => { + test(`TextBox should not be hovered after hover of outside label, stylingMode=${stylingMode}`, async ({ page }) => { + await createWidget(page, 'dxTextBox', { + value: 'text', + label: 'Label text', + labelMode: 'outside', + stylingMode, + width: 500, + }); + + const textBox = page.locator('#container'); + + await page.hover(textBox.getLabelSpan()) + .expect(textBox.isHovered) + .notOk(); + + }); + + test(`TextBox should be focused after click on outside label, stylingMode=${stylingMode}`, async ({ page }) => { + await createWidget(page, 'dxTextBox', { + value: 'text', + label: 'Label text', + labelMode: 'outside', + stylingMode, + width: 500, + }); + + const textBox = page.locator('#container'); + + await page.click(textBox.getLabelSpan()) + .expect(textBox.isFocused) + .ok(); + + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/editors/textBox/mask.spec.ts b/e2e/testcafe-devextreme/playwright-tests/editors/textBox/mask.spec.ts new file mode 100644 index 000000000000..e5074ad7ed78 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/editors/textBox/mask.spec.ts @@ -0,0 +1,32 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, appendElementTo } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('TextBox_mask', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('"!" character should not be accepted if mask restricts it (T1156419)', async ({ page }) => { + + await appendElementTo(page, '#container', 'div', 'textBox', { }); + + await createWidget(page, 'dxTextBox', { + mask: '9', + }, '#textBox'); + + const textBox = page.locator('#textBox'); + const { input } = textBox; + + await input.fill('!') + .expect(input.value).eql('_'); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/editors/textBox/validationMessage.spec.ts b/e2e/testcafe-devextreme/playwright-tests/editors/textBox/validationMessage.spec.ts new file mode 100644 index 000000000000..e2a6290617ad --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/editors/textBox/validationMessage.spec.ts @@ -0,0 +1,49 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, appendElementTo, setAttribute } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('ValidationMessage', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + const TEXTEDITOR_INPUT_CLASS = 'dx-texteditor-input'; + + test('Validation Message position should be correct after change visibility of parent container (T1095900)', async ({ page }) => { + + await appendElementTo(page, '#container', 'div', 'textbox', {}); + + await createWidget(page, 'dxTextBox', { + value: 'a', + validationMessageMode: 'always', + }, '#textbox'); + + await createWidget(page, 'dxValidator', { + validationRules: [ + { + type: 'required', + }, + ], + }, '#textbox'); + + await addFocusableElementBefore('#container'); + + await page.locator(`.${TEXTEDITOR_INPUT_CLASS}`).click() + .pressKey('backspace') + .pressKey('enter') + .click(page.locator('#focusable-start')); + + await setAttribute(page, '#container', 'hidden', 'true'); + await removeAttribute('#container', 'hidden'); + + await testScreenshot(page, 'Textbox validation message.png'); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/navigation/accordion/common.spec.ts b/e2e/testcafe-devextreme/playwright-tests/navigation/accordion/common.spec.ts new file mode 100644 index 000000000000..81f6f5ebfe99 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/navigation/accordion/common.spec.ts @@ -0,0 +1,52 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, appendElementTo, setAttribute } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('Accordion_common', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('Accordion items render (T865742)', async ({ page }) => { + + await appendElementTo(page, '#container', 'div', 'accordion'); + await appendElementTo(page, '#container', 'div', 'accordion2'); + + await setAttribute(page, '#container', 'style', 'display: flex; gap: 50px;'); + + const items: any[] = [ + { title: 'Some text 1', icon: 'coffee' }, + { title: 'Some text 2' }, + { title: 'Some text 3' }, + ]; + + await createWidget(page, 'dxAccordion', { items, width: 500 }, '#accordion'); + await createWidget(page, 'dxAccordion', { items, rtlEnabled: true, width: 500 }, '#accordion2'); + + const screenshotName = 'Accordion items render.png'; + + await testScreenshot(page, screenshotName, { element: '#container' }); + + }); + + test('Icon-only button should be rendered correctly (T851081)', async ({ page }) => { + + await appendElementTo(page, '#container', 'div', 'accordion'); + + const itemTitleTemplate = () => ($('
') as any).dxButton({ icon: 'coffee' }); + + await createWidget(page, 'dxAccordion', { dataSource: [{}], itemTitleTemplate }, '#accordion'); + + const screenshotName = 'Accordion with icon-only button.png'; + + await testScreenshot(page, screenshotName, { element: '#container' }); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/navigation/button/common.spec.ts b/e2e/testcafe-devextreme/playwright-tests/navigation/button/common.spec.ts new file mode 100644 index 000000000000..0958c340ad20 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/navigation/button/common.spec.ts @@ -0,0 +1,134 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, appendElementTo, setAttribute, setStyleAttribute, setClassAttribute, insertStylesheetRulesToPage } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('Button', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + ['text', 'outlined', 'contained'].forEach((stylingMode) => { + const testName = `Buttons, stylingMode=${stylingMode}`; + test(testName, async ({ page }) => { + + const typedButtons = ['danger', 'default', 'normal', 'success'].map((type: any) => ({ + type, + text: `${type[0].toUpperCase()}${type.slice(1)}`, + })); + const iconButtons = [ + { icon: 'find', text: 'Find' }, + { icon: 'find' }, + { + icon: ` + + `, + }, + ]; + const buttons = [ + ...typedButtons, + ...iconButtons, + ]; + + await setAttribute(page, '#container', 'class', 'dx-theme-generic-typography'); + await setAttribute(page, '#container', 'style', 'width: fit-content; padding: 8px;'); + + const states = ['default', 'focused', 'hover', 'active', 'selected', 'disabled']; + + for (const state of states) { + await appendElementTo(page, '#container', 'div', `mode${state}`, {}); + await setAttribute(page, `#mode${state}`, 'style', 'display: flex; gap: 8px; margin-bottom: 16px;'); + await addCaptionTo(`#mode${state}`, state); + + await Promise.all(buttons.map( + (_, index) => appendElementTo(page, `#mode${state}`, 'div', `button-${state}-${index}`, {}), + )); + + await Promise.all(buttons.map( + (defaultConfig, index) => createWidget(page, 'dxButton', { + ...defaultConfig, + stylingMode, + disabled: state === 'disabled', + }, `#button-${state}-${index}`), + )); + + if (state !== 'default' && state !== 'disabled') { + await Promise.all( + buttons.map((_, index) => setClassAttribute(page, `#button-${state}-${index}`, `dx-state-${state}`)), + + } + } + + + await testScreenshot(page, `${testName}.png`, { + element: '#container', + }); + + }); + }); + + test('Button in rtl modes', async ({ page }) => { + + await setAttribute(page, '#container', 'style', 'width: fit-content; padding: 8px; display: grid; grid-template-columns: repeat(3, auto); grid-gap: 16px;'); + + const buttons = [ + { icon: 'find', text: 'Button text' }, + { icon: 'find', text: 'Long button text' }, + { icon: 'find', text: 'Long button text', width: 150 }, + { icon: 'find', text: 'Button text', rtlEnabled: true }, + { icon: 'find', text: 'Long button text', rtlEnabled: true }, + { + icon: 'find', text: 'Long button text', width: 150, rtlEnabled: true, + }, + ]; + + await Promise.all(buttons.map( + (_, index) => appendElementTo(page, '#container', 'div', `button-${index}`, {}), + )); + + await Promise.all(buttons.map( + (config, index) => createWidget(page, 'dxButton', { + ...config, + }, `#button-${index}`), + )); + + await testScreenshot(page, 'Button in rtl modes.png', { + element: '#container', + }); + + }); + + test('Button: svg icon as background should be fit within icon element (T1178813)', async ({ page }) => { + + await insertStylesheetRulesToPage(page, '.dx-icon-custom { background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAACXBIWXMAAAsTAAALEwEAmpwYAAAB4klEQVR4nO3WTYiPURQG8N/M+ByEjQUxRQpJKSkhFigLo8jGQqGURDZY2CMpliI2PhZslLKxJBaUCCkUMxELja8pwoxuncW7mZn3fe9fb2meuqt7n+c8995z7zmMojVox1G81RDOYhBvmgi+LIKncakJA5cj+ABWNGHgQxg4pyG8xxmM/VcBVuMCXuIbenELe9GFiViH47iLd+jHq0jOBXUDT8a1QoINNX6OMP8Lx+KplkYnHoTARxyOnUzCHOzATXzHHzzHSWzATEzHRtwuGEnzpXExSA8xY5h1bZgwzHwHHodWStZSWBy76sMseTgQwb9gVVnSqSCle8vBuLi+pLWtCvFRkJZnGlgTOk+qEj8FcWqmgd2hc74q8UcQx2ca2BU66Q+phN4gzs40sDJ0XlT9A+4FcX2mgY4oz0nrSBXiiTofxxDYElVyIOpF+shGxNow0IMxLTCxJ37MpHm6DKENz4KwXWtwJ/T2lyXsLJzClMzg3YWaUlqrHfeDeCVOpQ464xUknX1VyYvwtXB3lZ5SrL8a/Kd1G5Zu/A6RVFqXxjFujl4w7e4zXkfvsBXTMA83gpeakyUysClEBmuM/uiWstEVPUJPXEvKj0NYGDVjPg5GtvdF+b2Oua0IPor/G38BnW+XcSzQwtUAAAAASUVORK5CYII="); }'); + + await setStyleAttribute(page, '#container', 'width: 300px; height: 200px;'); + await appendElementTo(page, '#container', 'div', 'button'); + await appendElementTo(page, '#container', 'div', 'fixedWidthButton'); + await appendElementTo(page, '#container', 'div', 'iconOnlyButton'); + + await createWidget(page, 'dxButton', { + text: 'svg icon', + icon: 'custom', + }, '#button'); + + await createWidget(page, 'dxButton', { + text: 'fixed width + svg icon', + icon: 'custom', + width: 200, + }, '#fixedWidthButton'); + + await createWidget(page, 'dxButton', { + icon: 'custom', + }, '#iconOnlyButton'); + + await testScreenshot(page, 'Button with svg icon as background.png', { element: '#container' }); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/navigation/button/floatingAction.spec.ts b/e2e/testcafe-devextreme/playwright-tests/navigation/button/floatingAction.spec.ts new file mode 100644 index 000000000000..cdd377a86196 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/navigation/button/floatingAction.spec.ts @@ -0,0 +1,126 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, appendElementTo, setStyleAttribute, insertStylesheetRulesToPage, isMaterial } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('FloatingAction - default theme', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + const OVERLAY_CONTENT_CLASS = 'dx-overlay-content'; + const FA_MAIN_BUTTON_CLASS = 'dx-fa-button-main'; + + const setGlobalConfig = async () => page.evaluate(() => { + (window as any).DevExpress.config({ + floatingActionButtonConfig: { + icon: 'edit', + shading: false, + position: { + of: '#container', + my: 'right bottom', + at: 'right bottom', + offset: '-16 -16', + }, + }, + }); + }); + + for (const label of ['Add Row', '']) { + for (const icon of ['home', '']) { + test(`FAB with two speed dial action buttons after opening, label: ${label}, icon: ${icon}`, async ({ page }) => { + + await setStyleAttribute(page, '#container', 'width: 300px; height: 300px;'); + await appendElementTo(page, '#container', 'div', 'speed-dial-action'); + await appendElementTo(page, '#container', 'div', 'speed-dial-action-trash'); + + await setGlobalConfig(); + if (isMaterial()) { + await insertStylesheetRulesToPage(page, '.dx-overlay-wrapper { font-family: sans-serif !important; }'); + } + + await createWidget(page, 'dxSpeedDialAction', { + label, + icon, + index: 1, + visible: true, + }, '#speed-dial-action'); + + await createWidget(page, 'dxSpeedDialAction', { + label: 'Remove Row', + icon: 'trash', + index: 2, + visible: true, + }, '#speed-dial-action-trash'); + + + await page.locator('body').click() + .click(page.locator(`.${FA_MAIN_BUTTON_CLASS} .${OVERLAY_CONTENT_CLASS}`)); + + await testScreenshot(page, `FAB is opened with two speed dial actions,label='${label}',icon='${icon}'.png`, { + element: '#container', + }); + + }); + + test(`FAB with one speed dial action button, label: ${label}, icon: ${icon}`, async ({ page }) => { + + await setStyleAttribute(page, '#container', 'width: 300px; height: 300px;'); + if (isMaterial()) { + await insertStylesheetRulesToPage(page, '.dx-overlay-wrapper { font-family: sans-serif !important; }'); + } + await appendElementTo(page, '#container', 'div', 'speed-dial-action'); + + await setGlobalConfig(); + + await createWidget(page, 'dxSpeedDialAction', { + label, + icon, + visible: true, + }, '#speed-dial-action'); + + + await testScreenshot(page, `FAB with one speed dial action button,label='${label}',icon='${icon}'.png`, { element: '#container' }); + + }); + } + } + + test('FAB with two speed dial action buttons', async ({ page }) => { + + await setStyleAttribute(page, '#container', 'width: 300px; height: 300px;'); + if (isMaterial()) { + await insertStylesheetRulesToPage(page, '.dx-overlay-wrapper { font-family: sans-serif !important; }'); + } + + await appendElementTo(page, '#container', 'div', 'speed-dial-action'); + await appendElementTo(page, '#container', 'div', 'speed-dial-action-trash'); + + await setGlobalConfig(); + + await createWidget(page, 'dxSpeedDialAction', { + label: 'Add row', + icon: 'plus', + index: 1, + visible: true, + }, '#speed-dial-action'); + + await createWidget(page, 'dxSpeedDialAction', { + label: 'Remove Row', + icon: 'trash', + index: 2, + visible: true, + }, '#speed-dial-action-trash'); + + await testScreenshot(page, 'FAB with two speed dial action buttons.png', { + element: '#container', + }); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/navigation/button/floatingActionInGrid.spec.ts b/e2e/testcafe-devextreme/playwright-tests/navigation/button/floatingActionInGrid.spec.ts new file mode 100644 index 000000000000..e85e52e1972b --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/navigation/button/floatingActionInGrid.spec.ts @@ -0,0 +1,92 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, appendElementTo } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('FloatingAction with Grid', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + const scrollWindowTo = async (position: object) => { + await ClientFunction( + () => { + (window as any).scroll(position); + }, + { + dependencies: { + position, + }, + }, + )(); + }; + + const generateData = (count) => { + const items: Record[] = []; + + for (let i = 0; i < count; i += 1) { + items.push({ + ID: i, + NAME: 'Name', + Full_Name: 'Full name', + }); + } + + return items; + }; + + [undefined, '#grid'].forEach((positionOf) => { + test(`FAB with grid, position.of is ${positionOf}`, async ({ page }) => { + + const dataGrid = page.locator('#grid'); + + await page.expect(dataGrid.isReady()) + .ok(); + + await page.evaluate(() => { + (window as any).DevExpress.ui.repaintFloatingActionButton(); + }); + + await testScreenshot(page, `FAB with grid, position.of is ${positionOf}, before scrolling.png`); + + await scrollWindowTo({ top: 10000000 }); + + await page.expect(dataGrid.isReady()) + .ok(); + + await testScreenshot(page, `FAB with grid, position.of is ${positionOf}, after scrolling.png`); + + });.before(async ({ page }) => { + await page.evaluate(() => { + $('#container').wrap('
'); + }); + + await resizeWindow(1000, 400); + + await appendElementTo(page, '#container', 'div', 'grid'); + await appendElementTo(page, '#container', 'div', 'speed-dial-action'); + + await createWidget(page, 'dxDataGrid', { + dataSource: generateData(20), + }, '#grid'); + + await createWidget(page, 'dxSpeedDialAction', { + label: 'Add row', + icon: 'plus', + position: { + of: positionOf, + }, + }, '#speed-dial-action'); + }).after(async () => { + await page.evaluate(() => { + $('#container').unwrap(); + }); + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/navigation/buttonGroup/common.spec.ts b/e2e/testcafe-devextreme/playwright-tests/navigation/buttonGroup/common.spec.ts new file mode 100644 index 000000000000..9abf0c9fe93d --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/navigation/buttonGroup/common.spec.ts @@ -0,0 +1,44 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, appendElementTo, setAttribute, setStyleAttribute } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('ButtonGroup', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + const typedItems: any[] = ['danger', 'default', 'normal', 'success'].map((type: any) => ({ type, text: type })); + const iconItems: any[] = [ + { icon: 'find', text: 'find' }, + { icon: 'find' }, + ]; + const items: any[] = [ + ...typedItems, + ...iconItems, + ]; + + test('ButtonGroup styling', async ({ page }) => { + + await setStyleAttribute(page, '#container', 'width: fit-content; padding: 8px; display: flex; gap: 16px; flex-direction: column;'); + await setAttribute(page, '#container', 'class', 'dx-theme-generic-typography'); + + const stylingModes = ['text', 'outlined', 'contained']; + + await Promise.all(stylingModes.map((mode) => appendElementTo(page, '#container', 'div', `buttongroup-${mode}`, {}))); + await Promise.all(stylingModes.map((stylingMode) => createWidget(page, 'dxButtonGroup', { + items, + stylingMode, + selectionMode: 'none', + }, `#buttongroup-${stylingMode}`))); + + await testScreenshot(page, 'ButtonGroup styling.png', { element: '#container' }); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/navigation/buttonGroup/selection.spec.ts b/e2e/testcafe-devextreme/playwright-tests/navigation/buttonGroup/selection.spec.ts new file mode 100644 index 000000000000..499dc491ba2e --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/navigation/buttonGroup/selection.spec.ts @@ -0,0 +1,80 @@ +import { test, expect } from '@playwright/test'; +import { createWidget } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('ButtonGroup_Selection', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('selected class should not be added to the button after hovering (T1222079)', async ({ page }) => { + await createWidget(page, 'dxButtonGroup', { + items: [ + { text: 'Button_1' }, + { text: 'Button_2' }, + ], + selectedItemKeys: ['Button_1'], + disabled: true, + }); + + const buttonGroup = page.locator('#container'); + + await buttonGroup.option('disabled', false); + + await buttonGroup.getItem(1).element.click(); + + await page.expect(buttonGroup.getItem(1).isSelected) + .ok() + .expect(buttonGroup.isItemSelected(1)) + .ok(); + + await page.hover(buttonGroup.getItem(0).element); + + await page.expect(buttonGroup.getItem(0).isSelected) + .notOk() + .expect(buttonGroup.isItemSelected(0)) + .notOk(); + + }); + test('selected class should be set after reenabling (T1308601)', async ({ page }) => { + await createWidget(page, 'dxButtonGroup', { + items: [ + { text: 'Button_1' }, + { text: 'Button_2' }, + ], + selectedItemKeys: ['Button_1'], + }); + + const buttonGroup = page.locator('#container'); + + await buttonGroup.option('disabled', true); + await buttonGroup.option('disabled', false); + + await buttonGroup.getItem(1).element.click(); + + await buttonGroup.option('disabled', true); + await buttonGroup.option('disabled', false); + + await buttonGroup.getItem(0).element.click(); + + await page.expect(buttonGroup.getItem(0).isSelected) + .ok() + .expect(buttonGroup.isItemSelected(0)) + .ok(); + + await page.hover(buttonGroup.getItem(1).element); + + await page.expect(buttonGroup.getItem(0).isSelected) + .ok() + .expect(buttonGroup.isItemSelected(0)) + .ok(); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/navigation/contextMenu/common.spec.ts b/e2e/testcafe-devextreme/playwright-tests/navigation/contextMenu/common.spec.ts new file mode 100644 index 000000000000..9eb08a6c0a99 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/navigation/contextMenu/common.spec.ts @@ -0,0 +1,80 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, appendElementTo, setStyleAttribute, insertStylesheetRulesToPage } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('ContextMenu_common', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('ContextMenu items render', async ({ page }) => { + + await appendElementTo(page, '#container', 'div', 'contextMenu'); + await setStyleAttribute(page, '#container', 'width: 300px; height: 200px;'); + + await insertStylesheetRulesToPage(page, '.custom-class { box-shadow: 0 0 0 2px green !important; }'); + + const menuItems: any[] = [ + { text: 'remove', icon: 'remove', items: [{ text: 'item_1' }, { text: 'item_2' }] }, + { text: 'user', icon: 'user' }, + { text: 'coffee', icon: 'coffee' }, + ]; + + await createWidget(page, 'dxContextMenu', { + cssClass: 'custom-class', + items: menuItems, + target: 'body', + position: { + offset: '10 10', + }, + }, '#contextMenu'); + + const contextMenu = page.locator('#contextMenu'); + + await contextMenu.show(); + await click(contextMenu.items.nth(0)); + + const screenshotName = 'ContextMenu items render.png'; + await testScreenshot(page, screenshotName, { element: '#container' }); + + }); + + test('ContextMenu selected focused item', async ({ page }) => { + + await appendElementTo(page, '#container', 'div', 'contextMenu'); + await setStyleAttribute(page, '#container', 'width: 150px; height: 200px;'); + + await insertStylesheetRulesToPage(page, '.custom-class { border: 2px solid green !important; }'); + + const menuItems: any[] = [ + { text: 'remove', icon: 'remove', selected: true }, + { text: 'user', icon: 'user' }, + { text: 'coffee', icon: 'coffee' }, + ]; + + await createWidget(page, 'dxContextMenu', { + cssClass: 'custom-class', + items: menuItems, + target: 'body', + position: { + offset: '10 10', + }, + }, '#contextMenu'); + + const contextMenu = page.locator('#contextMenu'); + + await contextMenu.show(); + await page.keyboard.press('ArrowDown'); + + const screenshotName = 'ContextMenu selected focused item.png'; + await testScreenshot(page, screenshotName, { element: '#container' }); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/navigation/contextMenu/contextMenu.spec.ts b/e2e/testcafe-devextreme/playwright-tests/navigation/contextMenu/contextMenu.spec.ts new file mode 100644 index 000000000000..b6abb53005ea --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/navigation/contextMenu/contextMenu.spec.ts @@ -0,0 +1,57 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, appendElementTo } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('ContextMenu', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('Context menu should be shown in the same position when item was added in runtime (T755681)', async ({ page }) => { + + const menuTargetID = 'menuTarget'; + await appendElementTo(page, '#container', 'div', 'contextMenu'); + await appendElementTo(page, '#container', 'button', menuTargetID, { + width: '150px', height: '50px', backgroundColor: 'steelblue', + }); + + await createWidget(page, 'dxContextMenu', { + items: [{ text: 'item1' }], + showEvent: 'dxclick', + target: `#${menuTargetID}`, + onShowing: (e) => { + if (!(window as any).isItemAdded) { + setTimeout(() => { + (window as any).isItemAdded = true; + const items = e.component.option('items'); + items.push({ text: 'item 2' }); + e.component.option('items', items); + }, 1000); + } + }, + }, '#contextMenu'); + + const contextMenu = page.locator('#contextMenu'); + const target = page.locator('#menuTarget'); + + await page.click(target) + .expect(page.locator('.dx-context-menu').exists).ok('Context menu element should exist') + .expect(contextMenu.overlay.getContent().getStyleProperty('visibility')) + .eql('visible'); + + const initialOverlayOffset = await contextMenu.overlay.getOverlayOffset(); + + await page.expect(contextMenu.getItemCount()).eql(1); + + await page.expect(contextMenu.getItemCount()).eql(2) + .expect(contextMenu.overlay.getOverlayOffset()).eql(initialOverlayOffset); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/navigation/contextMenu/scrolling.spec.ts b/e2e/testcafe-devextreme/playwright-tests/navigation/contextMenu/scrolling.spec.ts new file mode 100644 index 000000000000..37b297c164b6 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/navigation/contextMenu/scrolling.spec.ts @@ -0,0 +1,42 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, appendElementTo } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('ContextMenu_common', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('ContextMenu items render', async ({ page }) => { + + await appendElementTo(page, '#container', 'div', 'contextMenu'); + + const items: any[] = new Array(99).fill(null).map((_, idx) => ({ text: `item ${idx}` })); + + items[98].items = new Array(99).fill(null).map((_, idx) => ({ text: `item ${idx}` })); + + await createWidget(page, 'dxContextMenu', { + items, + target: 'body', + }, '#contextMenu'); + + const contextMenu = page.locator('#contextMenu'); + + await contextMenu.show(); + + await page.keyboard.press('ArrowDown') + .pressKey('up') + .pressKey('right') + .pressKey('up'); + + await testScreenshot(page, 'ContextMenu scrolling.png'); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/navigation/drawer/common.spec.ts b/e2e/testcafe-devextreme/playwright-tests/navigation/drawer/common.spec.ts new file mode 100644 index 000000000000..4d27993c2bea --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/navigation/drawer/common.spec.ts @@ -0,0 +1,118 @@ +import { test, expect } from '@playwright/test'; +import { testScreenshot } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('Drawer', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + ['overlap', 'shrink', 'push'].forEach((openedStateMode: OpenedStateMode) => { + const testName = `Drawer, openedStateMode=${openedStateMode}, shading=true`; + test(testName, async ({ page }) => { + + await createDrawer({ + options: { openedStateMode }, + }); + + + await testScreenshot(page, `${testName}.png`); + + }); + }); + + ['top', 'bottom', 'left', 'right'].forEach((position: Position) => { + const testName = `Drawer, position=${position}, shading=true`; + test(testName, async ({ page }) => { + + await createDrawer({ + options: { position }, + }); + + + await testScreenshot(page, `${testName}.png`); + + }); + }); + + test('Drawer hidden', async ({ page }) => { + + await createDrawer({ + createOuterContent: ($container) => { + ($('
').appendTo($container) as any).dxButton({ + text: 'Hide Drawer', + onClick: () => ($(`#${$container.attr('id')} #drawer`) as any).dxDrawer('instance').hide(), + }); + }, + }); + + await page.locator('#container #hideDrawerBtn').click(); + + await testScreenshot(page, 'Drawer hidden.png'); + + }); + + [{ + testCase: 'Menu inside drawer', + selector: '.dx-menu-item', + createDrawerContent: ($container: JQuery) => { + ($('
').appendTo($container) as any).dxMenu({ + dataSource: [{ text: 'item1 very long text wider than panel', items: [{ text: 'item1/item1 very long text wider than panel' }, { text: 'item1/item2' }] }], + }); + }, + }, { + testCase: 'SelectBox inside drawer', + selector: '.dx-texteditor-container', + createDrawerContent: ($container: JQuery) => { + ($('
').appendTo($container) as any).dxSelectBox({ + dataSource: ['item1 very long text wider than panel', 'item2'], + }); + }, + }, { + testCase: 'Menu outside drawer', + selector: '.dx-menu-item', + createOuterContent: ($container: JQuery) => { + ($('
').appendTo($container) as any).dxMenu({ + dataSource: [{ text: 'item1 very long text wider than panel', items: [{ text: 'item1/item1 very long text wider than panel' }, { text: 'item1/item2' }] }], + }); + }, + }, { + testCase: 'SelectBox outside drawer', + selector: '.dx-texteditor-container', + createOuterContent: ($container: JQuery) => { + ($('
').appendTo($container) as any).dxSelectBox({ + dataSource: ['item1 very long text wider than panel', 'item2'], + }); + }, + }].forEach(({ + testCase, createDrawerContent, createOuterContent, selector, + }) => { + const testName = `Drawer z-index, ${testCase}, shading=true`; + test(testName, async ({ page }) => { + + await createDrawer({ + createDrawerContent, + createOuterContent, + testInPopup: true, + }); + + + await page.locator(`#container #content ${selector}`).click(); + + await testScreenshot(page, `${testName}_container.png`); + + await page.locator('#showPopupBtn').click(); + await page.locator(`#popup1_template #content ${selector}`).click(); + + await testScreenshot(page, `${testName}_popup.png`); + + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/navigation/form/itemTypes.spec.ts b/e2e/testcafe-devextreme/playwright-tests/navigation/form/itemTypes.spec.ts new file mode 100644 index 000000000000..461be49b64ee --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/navigation/form/itemTypes.spec.ts @@ -0,0 +1,52 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('Form', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('GroupItem', async ({ page }) => { + await createWidget(page, 'dxForm', { + items: [ + { + itemType: 'group', + items: ['item1'], + captionTemplate: () => $('Custom caption template'), + }, + ], + }); + + await testScreenshot(page, 'Group caption template.png', { element: '#container' }); + + }); + + test('TabbedItem', async ({ page }) => { + await createWidget(page, 'dxForm', { + width: 500, + items: [ + { + itemType: 'tabbed', + tabPanelOptions: { deferRendering: false }, + tabs: [ + { + title: 'tab1', + items: ['item1'], + }, + ], + }, + ], + }); + + await testScreenshot(page, 'TabbedItem.png', { element: '#container' }); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/navigation/form/labels.spec.ts b/e2e/testcafe-devextreme/playwright-tests/navigation/form/labels.spec.ts new file mode 100644 index 000000000000..48e4844d0936 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/navigation/form/labels.spec.ts @@ -0,0 +1,252 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, appendElementTo, insertStylesheetRulesToPage, removeStylesheetRulesFromPage } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('Form', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + const waitFont = async () => page.evaluate(() => (window as any).DevExpress.ui.themes.waitWebFont('Item123somevalu*op ', 400)); + + [false, true].forEach((rtlEnabled) => { + ['left', 'right', 'top'].forEach((formLabelLocation) => { + ['outside', 'floating', 'hidden', 'static'].forEach((formLabelMode) => { + const testName = `Form,rtl=${rtlEnabled},lMode=${formLabelMode},lLoc=${formLabelLocation}`; + + test(testName, async ({ page }) => { + + await waitFont(); + + const getGroup = (visible: boolean, alignment: HorizontalAlignment) => ({ + itemType: 'group', + caption: `Label visible: ${visible}, label alignment: ${alignment}`, + colCount: 3, + items: [ + { + dataField: 'field1', + label: { visible, alignment }, + editorType: 'dxTextBox', + }, + { + dataField: 'field2', + label: { visible, alignment }, + editorType: 'dxTextBox', + editorOptions: { + value: 'dxTextBox', + }, + }, + { + dataField: 'field3', + label: { visible, alignment }, + editorType: 'dxCheckBox', + editorOptions: { + value: true, + text: 'dxCheckBox', + }, + }, + ], + }); + + const items = [true, false].flatMap( + (labelVisible) => { + const alignments: HorizontalAlignment[] = labelVisible && formLabelLocation === 'top' + ? ['left', 'center', 'right'] + : ['left']; + + return alignments.map((labelAlignment) => getGroup(labelVisible, labelAlignment)); + }, + + await createWidget(page, 'dxForm', { + rtlEnabled, + width: 1000, + labelMode: formLabelMode, + labelLocation: formLabelLocation, + items, + }); + + + await testScreenshot(page, `${testName}.png`, { element: '#container' }); + + }); + }); + }); + }); + + [true, false].forEach((alignItemLabelsInAllGroups) => { + [true, false].forEach((alignItemLabels) => { + const testName = `Align items,lblMode=outside,alignInAllGrp=${alignItemLabelsInAllGroups},alignInGrp=${alignItemLabels}`; + test(testName, async ({ page }) => { + + const options = { + labelMode: 'outside', + labelLocation: 'left', + alignItemLabelsInAllGroups, + colCount: 2, + width: 1000, + items: [ + { + itemType: 'group', + caption: 'Group1', + colSpan: 1, + alignItemLabels, + items: [ + { dataField: 'field1', label: { text: 'field1' }, editorType: 'dxTextBox' }, + { dataField: 'field2', label: { text: 'field2 long text' }, editorType: 'dxTextBox' }, + { dataField: 'field3', label: { text: 'CheckBox1' }, editorType: 'dxCheckBox' }, + { dataField: 'field4', label: { text: 'CheckBox2 long text' }, editorType: 'dxCheckBox' }, + ], + }, + { + itemType: 'group', + caption: 'Group2', + colSpan: 1, + alignItemLabels, + items: [ + { dataField: 'field5', label: { text: 'short text' }, editorType: 'dxTextBox' }, + { dataField: 'field6', label: { text: 'field2 very long text' }, editorType: 'dxTextBox' }, + { dataField: 'field7', label: { text: 'CheckBox1 text' }, editorType: 'dxCheckBox' }, + { dataField: 'field8', label: { text: 'CheckBox2 very long text' }, editorType: 'dxCheckBox' }, + ], + }, + { + itemType: 'group', + caption: 'Group3', + colSpan: 2, + alignItemLabels, + items: [ + { dataField: 'field9', label: { text: 'short text' }, editorType: 'dxTextBox' }, + { dataField: 'field10', label: { text: 'field2 very long text' }, editorType: 'dxTextBox' }, + { dataField: 'field11', label: { text: 'ChBx1 very very long text' }, editorType: 'dxCheckBox' }, + { dataField: 'field12', label: { text: 'ChBx2 very long text' }, editorType: 'dxCheckBox' }, + ], + }, + ], + }; + + await createWidget(page, 'dxForm', options); + + + await testScreenshot(page, `${testName}.png`, { element: '#container' }); + + }); + }); + }); + + test('Item label position properties, labelMode=outside', async ({ page }) => { + + const options = { + labelMode: 'outside', + width: 500, + items: [ + { dataField: 'Left', label: { location: 'left' }, editorType: 'dxTextBox' }, + { dataField: 'Top left', label: { location: 'top', alignment: 'left' }, editorType: 'dxTextBox' }, + { dataField: 'Top center', label: { location: 'top', alignment: 'center' }, editorType: 'dxTextBox' }, + { dataField: 'Top right', label: { location: 'top', alignment: 'right' }, editorType: 'dxTextBox' }, + { dataField: 'Right', label: { location: 'right' }, editorType: 'dxTextBox' }, + ], + }; + + await createWidget(page, 'dxForm', options); + + await testScreenshot(page, 'Item label position properties, labelMode=outside.png', { element: '#container' }); + + }); + + test('Color of the mark (T882067)', async ({ page }) => { + await createWidget(page, 'dxForm', { + height: 400, + width: 1000, + formData: { + firstName: 'John', + lastName: 'Heart', + position: 'CEO', + }, + items: [ + { dataField: 'firstName', isRequired: true }, + { dataField: 'lastName', isOptional: true }, + 'position', + ], + requiredMark: '!', + optionalMark: 'opt', + showOptionalMark: true, + }); + + const screenshotName = 'Form color of the mark.png'; + + await testScreenshot(page, screenshotName, { element: '#container' }); + + }); + + test('Form labels should have correct width after render in invisible container', async ({ page }) => { + + await appendElementTo(page, '#container', 'div', 'form'); + await insertStylesheetRulesToPage(page, '#container { display: none; }'); + + await createWidget(page, 'dxForm', { + width: 1000, + labelLocation: 'left', + formData: { + ID: 1, + FirstName: 'John', + LastName: 'Heart', + Position: 'CEO', + OfficeNo: '901', + BirthDate: new Date(1964, 2, 16), + HireDate: new Date(1995, 0, 15), + Address: '351 S Hill St.', + City: 'Los Angeles', + State: 'CA', + ZipCode: '90013', + Phone: '+1(213) 555-9392', + Email: 'jheart@dx-email.com', + Skype: 'jheart_DX_skype', + }, + colCount: 2, + items: [{ + itemType: 'group', + caption: 'System Information', + items: ['ID', 'FirstName', 'LastName', 'HireDate', 'Position', 'OfficeNo'], + }, { + itemType: 'group', + caption: 'Personal Data', + items: ['BirthDate', { + itemType: 'group', + caption: 'Home Address', + items: ['Address', 'City', 'State', 'ZipCode'], + }], + }, { + itemType: 'group', + caption: 'Contact Information', + items: [{ + itemType: 'tabbed', + tabPanelOptions: { + deferRendering: true, + }, + tabs: [{ + title: 'Phone', + items: ['Phone'], + }, { + title: 'Skype', + items: ['Skype'], + }, { + title: 'Email', + items: ['Email'], + }], + }], + }], + }, '#form'); + + await removeStylesheetRulesFromPage(page, ); + + await testScreenshot(page, 'Form labels width after render in invisible container.png'); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/navigation/form/layout.spec.ts b/e2e/testcafe-devextreme/playwright-tests/navigation/form/layout.spec.ts new file mode 100644 index 000000000000..f62ef04142cb --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/navigation/form/layout.spec.ts @@ -0,0 +1,275 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, appendElementTo, setAttribute } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('Form', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + const waitFont = async () => page.evaluate(() => (window as any).DevExpress.ui.themes.waitWebFont('Item123somevalu*op ', 400)); + + test('SimpleItem: item1_cSpan_2', async ({ page }) => { + + await waitFont(); + await setAttribute(page, '#container', 'style', 'width: 500px;'); + + for (let colCount = 1; colCount <= 4; colCount += 1) { + const formId = `form${colCount}`; + + await appendElementTo(page, '#container', 'div', formId); + await page.evaluate(({ sel, caption }) => { document.querySelector(sel)?.insertAdjacentText('beforebegin', caption); }, { sel: `#${formId}`, caption: `colCount = ${colCount}` }); + + const formOptions = { + elementAttr: { style: 'margin-bottom: 20px' }, + labelMode: 'static', + colCount, + items: [{ dataField: 'item_1', colSpan: 2 }], + }; + + await createWidget(page, 'dxForm', formOptions, `#${formId}`); + } + + await testScreenshot(page, 'SimpleItem,item1_cSpan_2.png', { element: '#container' }); + + }); + + [[1, 2], [2, 1], [2, 2]].forEach(([colSpan1, colSpan2]) => { + const testName = `SimpleItem,item1_cSpan_${colSpan1},item2_cSpan_${colSpan2}`; + test(testName, async ({ page }) => { + + await waitFont(); + await setAttribute(page, '#container', 'style', 'width: 600px;'); + + for (let colCount = 1; colCount <= 4; colCount += 1) { + const formId = `form${colCount}`; + + await appendElementTo(page, '#container', 'div', formId); + await page.evaluate(({ sel, caption }) => { document.querySelector(sel)?.insertAdjacentText('beforebegin', caption); }, { sel: `#${formId}`, caption: `colCount = ${colCount}` }); + + const formOptions = { + elementAttr: { style: 'margin-bottom: 20px' }, + labelMode: 'static', + colCount, + items: [ + { dataField: `item_1_span_${colSpan1}`, colSpan: colSpan1 }, + { dataField: `item_2_span_${colSpan2}`, colSpan: colSpan2 }, + ], + }; + + await createWidget(page, 'dxForm', formOptions, `#${formId}`); + } + + + await testScreenshot(page, `${testName}.png`, { element: '#container' }); + + }); + }); + + [false, true].forEach((rtlEnabled) => { + [1, 2, 3, 4, 5, 6].forEach((itemsCount) => { + const testName = `colCount,rtl_${rtlEnabled},itemsCount_${itemsCount}`; + test(testName, async ({ page }) => { + + await waitFont(); + const containerStyle = ` + display: grid; + grid-template-columns: repeat(3, 300px); + grid-template-rows: 0px auto; + grid-auto-flow: column; + grid-gap: 30px; + width: 960px;`; + await setAttribute(page, '#container', 'style', containerStyle); + + for (let colCount = 1; colCount <= 3; colCount += 1) { + const formId = `form${colCount + 1}`; + + await appendElementTo(page, '#container', 'div', formId); + await page.evaluate(({ sel, caption }) => { document.querySelector(sel)?.insertAdjacentText('beforebegin', caption); }, { sel: `#${formId}`, caption: `colCount = ${colCount}` }); + + const formOptions = { + colCount, + rtlEnabled, + labelMode: 'static', + items: Array(itemsCount).fill(null).map((_, i) => ({ dataField: `item_${i + 1}` })), + }; + + await createWidget(page, 'dxForm', formOptions, `#${formId}`); + } + + + await testScreenshot(page, `${testName}.png`, { element: '#container' }); + + }); + }); + }); + + ['left', 'right', 'top'].forEach((labelLocation) => { + test('widget alignment (T1086611)', async ({ page }) => { + + await waitFont(); + + await createWidget(page, 'dxForm', { + labelLocation, + colCount: 2, + width: 1000, + formData: {}, + items: [{ + dataField: 'FirstName', + editorType: 'dxTextBox', + }, { + dataField: 'Position', + editorType: 'dxSelectBox', + }, { + dataField: 'BirthDate', + editorType: 'dxDateBox', + }, { + dataField: 'Notes', + editorType: 'dxTextArea', + }], + }); + + + await testScreenshot(page, `Form with labelLocation=${labelLocation}.png`, { element: '#container' }); + + }); + }); + + [() => 'xs', () => 'md', () => 'lg'].forEach((screenByWidth) => { + const testName = `Form item padding with screenByWidth=${screenByWidth()}`; + test(`${testName} (T1088451)`, async ({ page }) => { + await createWidget(page, 'dxForm', { + screenByWidth, + width: 1000, + formData: {}, + items: [ + 'Name1', 'Name2', + { + itemType: 'group', + items: [ + { + itemType: 'group', + items: [ + { + itemType: 'group', + items: [ + { + itemType: 'group', + colCount: 2, + items: [ + { + dataField: 'Name3', + }, + { + dataField: 'Name4', + }, + ], + }, + ], + }, + ], + }, + ], + }, + { + itemType: 'group', + items: [ + { + itemType: 'group', + items: [ + { + itemType: 'group', + items: [ + { + itemType: 'group', + colCount: 2, + items: [ + { + itemType: 'group', + colCount: 2, + items: ['Name7', 'Name8'], + }, + { + itemType: 'group', + colCount: 2, + items: ['Name9', 'Name10'], + }, + ], + }, + ], + }, + ], + }, + ], + }, + 'Name11', 'Name12', + ], + }); + + await waitFont(); + + await testScreenshot(page, `${testName}.png`, { element: '#container' }); + + }); + }); + + test('Validation errors persist after resize', async ({ page }) => { + await createWidget(page, 'dxForm', { + colCountByScreen: { + xs: 1, + sm: 2, + md: 2, + lg: 2, + }, + items: [ + { + dataField: 'name', + editorType: 'dxTextBox', + validationRules: [{ type: 'required' }], + }, + { + dataField: 'birthDate', + editorType: 'dxDateBox', + validationRules: [{ type: 'required' }], + }, + { + dataField: 'role', + editorType: 'dxSelectBox', + editorOptions: { + dataSource: ['Dev', 'QA', 'PM'], + }, + validationRules: [{ type: 'required' }], + }, + { + dataField: 'agree', + editorType: 'dxCheckBox', + editorOptions: { + text: 'I agree', + }, + validationRules: [{ + type: 'custom', + validationCallback: () => false, + message: 'Required', + }], + }, + ], + }); + + const form = page.locator('#container'); + + await waitFont(); + await form.validate(); + + await resizeWindow(400, 800); + + await testScreenshot(page, 'form_validation_errors_after_resize.png', { element: '#container' }); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/navigation/gallery/common.spec.ts b/e2e/testcafe-devextreme/playwright-tests/navigation/gallery/common.spec.ts new file mode 100644 index 000000000000..24ccdb222ec9 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/navigation/gallery/common.spec.ts @@ -0,0 +1,67 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, setAttribute } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('Click on indicator', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + const YELLOW_PIXEL = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAAMSURBVBhXYzi8wA8AA9sBsq0bEHsAAAAASUVORK5CYII='; + const BLACK_PIXEL = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAAMSURBVBhXY1hSWg4AA1EBkakDs38AAAAASUVORK5CYII='; + const RED_PIXEL = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAAMSURBVBhXY/i5aQsABQcCYPaWuk8AAAAASUVORK5CYII='; + + test('click on indicator item should change selected item', async ({ page }) => { + await createWidget(page, 'dxGallery', { + height: 300, + showIndicator: true, + items: [BLACK_PIXEL, RED_PIXEL, YELLOW_PIXEL], + }); + + const gallery = page.locator('#container'); + const secondIndicatorItem = gallery.getIndicatorItem(1); + + await secondIndicatorItem.click() + .expect(secondIndicatorItem.isSelected).ok(); + + }); + + [true, false].forEach((showIndicator) => { + test(`Gallery. Check normal and focus state. showIndicator=${showIndicator}`, async ({ page }) => { + + await createWidget(page, 'dxGallery', { + height: 110, + showIndicator, + items: [BLACK_PIXEL, RED_PIXEL, YELLOW_PIXEL], + itemTemplate(item: string) { + const result = $('
'); + + $('') + .attr({ src: item }) + .height(100) + .width(100) + .appendTo(result); + + return result; + }, + }); + + await setAttribute(page, '#container', 'style', 'width: 120px; height: 120px;'); + + + await testScreenshot(page, `Gallery. showIndicator=${showIndicator}.png`, { element: '#container' }); + + await page.locator('#container').click(); + + await testScreenshot(page, `Focused gallery. showIndicator=${showIndicator}.png`, { element: '#container' }); + + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/navigation/list/common.spec.ts b/e2e/testcafe-devextreme/playwright-tests/navigation/list/common.spec.ts new file mode 100644 index 000000000000..b1350ba6dd63 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/navigation/list/common.spec.ts @@ -0,0 +1,376 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, isMaterialBased, isFluent } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('List', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('Should focus first item after changing selection mode (T811770)', async ({ page }) => { + await createWidget(page, 'dxList', { + items: ['item1', 'item2', 'item3'], + showSelectionControls: true, + selectionMode: 'all', + }); + + const list = page.locator('#container'); + const { selectAll } = list; + const firstItemRadioButton = list.getItem().radioButton; + + await list.focus(); + + expect(selectAll.checkBox.isFocused).toBeTruthy(); + + await list.option('selectionMode', 'single'); + + await list.focus(); + + expect(firstItemRadioButton.isFocused).toBeTruthy(); + + }); + + test('There is hover class in hovered list item (T1110076)', async ({ page }) => { + await createWidget(page, 'dxList', { + items: ['item1', 'item2', 'item3'], + selectionMode: 'single', + }); + + const list = page.locator('#container'); + + const firstItem = list.getItem(0); + + await dispatchEvent(firstItem.element, 'mousedown'); + await list.repaint(); + await dispatchEvent(firstItem.element, 'mouseup'); + + const secondItem = list.getItem(1); + + expect(secondItem.isHovered).toBeFalsy() + .hover(secondItem.element) + .expect(secondItem.isHovered) + .ok(); + + }); + + test('List selection should work with keyboard arrows (T718398)', async ({ page }) => { + await createWidget(page, 'dxList', { + items: ['item1', 'item2', 'item3'], + showSelectionControls: true, + selectionMode: 'all', + }); + + const list = page.locator('#container'); + const firstItemCheckBox = list.getItem().checkBox; + const secondItemCheckBox = list.getItem(1).checkBox; + const thirdItemCheckBox = list.getItem(2).checkBox; + const { selectAll } = list; + const selectAllCheckBox = selectAll.checkBox; + + await list.focus(); + + expect(selectAllCheckBox.isFocused).toBeTruthy() + + .pressKey('down') + .expect(selectAllCheckBox.isFocused) + .notOk() + .expect(firstItemCheckBox.isFocused) + .ok() + + .pressKey('down') + .expect(firstItemCheckBox.isFocused) + .notOk() + .expect(secondItemCheckBox.isFocused) + .ok() + + .pressKey('down') + .expect(secondItemCheckBox.isFocused) + .notOk() + .expect(thirdItemCheckBox.isFocused) + .ok() + + .pressKey('down') + .expect(thirdItemCheckBox.isFocused) + .notOk() + .expect(selectAllCheckBox.isFocused) + .ok() + + .pressKey('down') + .expect(selectAllCheckBox.isFocused) + .notOk() + .expect(firstItemCheckBox.isFocused) + .ok() + + .pressKey('up') + .expect(firstItemCheckBox.isFocused) + .notOk() + .expect(selectAll.isFocused) + .ok() + + .pressKey('up') + .expect(selectAllCheckBox.isFocused) + .notOk() + .expect(thirdItemCheckBox.isFocused) + .ok() + + .pressKey('up') + .expect(thirdItemCheckBox.isFocused) + .notOk() + .expect(secondItemCheckBox.isFocused) + .ok() + + .pressKey('tab') + .expect(selectAllCheckBox.isFocused) + .notOk() + .expect(secondItemCheckBox.isFocused) + .notOk(); + + }); + + test('Should save focused checkbox', async ({ page }) => { + await createWidget(page, 'dxList', { + items: ['item1', 'item2', 'item3'], + showSelectionControls: true, + selectionMode: 'all', + }); + + const list = page.locator('#container'); + const secondItemCheckBox = list.getItem(1).checkBox; + const { selectAll } = list; + const selectAllCheckBox = selectAll.checkBox; + + await list.focus(); + + expect(selectAllCheckBox.isFocused).toBeTruthy() + + .pressKey('down down') + .expect(secondItemCheckBox.isFocused) + .ok() + .expect(selectAllCheckBox.isFocused) + .notOk() + + .pressKey('shift+tab') + .expect(secondItemCheckBox.isFocused) + .notOk() + .expect(selectAllCheckBox.isFocused) + .notOk() + + .pressKey('tab') + .expect(secondItemCheckBox.isFocused) + .ok() + .expect(selectAllCheckBox.isFocused) + .notOk() + + .pressKey('up up') + .expect(selectAllCheckBox.isFocused) + .ok() + .expect(secondItemCheckBox.isFocused) + .notOk() + + .pressKey('shift+tab') + .expect(secondItemCheckBox.isFocused) + .notOk() + .expect(selectAllCheckBox.isFocused) + .notOk() + + .pressKey('tab') + .expect(selectAllCheckBox.isFocused) + .ok() + .expect(secondItemCheckBox.isFocused) + .notOk(); + + }); + + test('Grouped list can not reorder items (T727360)', async ({ page }) => { + + const data = [ + { group: 'group1', value: '11' }, + { group: 'group1', value: '12' }, + { group: 'group1', value: '13' }, + { group: 'group2', value: '21' }, + { group: 'group2', value: '22' }, + { group: 'group2', value: '23' }, + { group: 'group2', value: '24' }, + { group: 'group2', value: '25' }, + { group: 'group2', value: '26' }, + { group: 'group2', value: '27' }, + { group: 'group2', value: '28' }, + { group: 'group2', value: '29' }, + { group: 'group2', value: '20' }, + { group: 'group3', value: '31' }, + { group: 'group3', value: '32' }, + { group: 'group3', value: '33' }, + { group: 'group3', value: '34' }, + { group: 'group3', value: '35' }, + { group: 'group3', value: '36' }, + { group: 'group3', value: '37' }, + { group: 'group3', value: '38' }, + { group: 'group3', value: '39' }, + { group: 'group3', value: '30' }, + ]; + + await createWidget(page, 'dxList', { + dataSource: { + store: data, + group: 'group', + }, + itemDragging: { + allowReordering: true, + }, + collapsibleGroups: true, + grouped: true, + itemTemplate: ({ value }, _, el) => el.append($('').text(value)), + }); + + const list = page.locator('#container'); + const firstGroup = list.getGroup(); + const secondGroup = list.getGroup(1); + const thirdGroup = list.getGroup(2); + + await page.click(secondGroup.header) + .click(thirdGroup.header) + + .dragToElement(firstGroup.getItem().reorderHandle, firstGroup.getItem(1).element) + .expect(firstGroup.getItem().text) + .eql(isFluent() ? '11' : '12') + .expect(firstGroup.getItem(1).text) + .eql(isFluent() ? '12' : '11') + + .click(firstGroup.header) + .click(secondGroup.header) + + .dragToElement(secondGroup.getItem().reorderHandle, secondGroup.getItem(1).element) + .expect(secondGroup.getItem().text) + .eql(isFluent() ? '21' : '22') + .expect(secondGroup.getItem(1).text) + .eql(isFluent() ? '22' : '21') + + .click(secondGroup.header) + .click(thirdGroup.header) + + .dragToElement(thirdGroup.getItem().reorderHandle, thirdGroup.getItem(1).element) + .expect(thirdGroup.getItem().text) + .eql(isMaterialBased() ? '31' : '32') + .expect(thirdGroup.getItem(1).text) + .eql(isMaterialBased() ? '32' : '31'); + + }); + + test('Grouped List with nested List should able to reorder items (T845082)', async ({ page }) => { + + const data = [ + { group: 'group1', text: 'value11' }, + { + group: 'group1', + text: 'value12', + template: ClientFunction((_data, _index, element) => ($('
').appendTo(element) as any).dxList({ + items: ['value121', 'value122', 'value123'], + itemTemplate: (data, _index, element) => { + $(element) + .text(data) + .parent() + .addClass('nested-item'); + }, + })), + }, + { group: 'group1', text: 'value13' }, + ]; + + await createWidget(page, 'dxList', { + dataSource: { + store: data, + group: 'group', + }, + itemDragging: { + allowReordering: true, + }, + collapsibleGroups: true, + grouped: true, + }); + + const list = page.locator('#container'); + const group = list.getGroup(); + + await page.expect(group.getItem(0).text).eql('value11') + .drag(group.getItem().reorderHandle, 0, await group.getItem(1).element.clientHeight) + .expect(group.getItem(1).text) + .eql('value11'); + + }); + + test('Disabled item should be focused on tab press to match accessibility criteria', async ({ page }) => { + await createWidget(page, 'dxList', { + dataSource: [{ text: 'item1' }, { text: 'item2' }], + searchEnabled: true, + }); + + const list = page.locator('#container'); + const { searchInput } = list; + const firstItem = list.getItem(); + const secondItem = list.getItem(1); + + await page.click(searchInput) + .pressKey('tab') + .expect(firstItem.isFocused).ok() + .expect(secondItem.isFocused) + .notOk(); + + await list.option('items[0].disabled', true); + + expect(firstItem.isDisabled).toBeTruthy() + + .click(searchInput) + .pressKey('tab') + .expect(firstItem.isFocused) + .ok() + .expect(secondItem.isFocused) + .notOk(); + + }); + + test('The delete button should be displayed correctly after the list item focused (T1216108)', async ({ page }) => { + await createWidget(page, 'dxList', { + dataSource: [{ + text: 'item 1', + icon: 'user', + }], + allowItemDeleting: true, + itemDeleteMode: 'static', + }); + + const list = page.locator('#container'); + + await list.focus(); + + await testScreenshot(page, 'List delete button when item is focused.png'); + + }); + + test('The button icon in custom template should be displayed correctly after the list item focused (T1216108)', async ({ page }) => { + await createWidget(page, 'dxList', { + dataSource: [{ text: 'item 1' }], + itemTemplate: (_, __, element) => { + const button = ($('
') as any).dxButton({ + text: 'custom', + icon: 'home', + }); + + element.append(button); + }, + }); + + const list = page.locator('#container'); + + await list.focus(); + + await testScreenshot(page, 'List icon in button when item is focused.png'); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/navigation/list/focus.spec.ts b/e2e/testcafe-devextreme/playwright-tests/navigation/list/focus.spec.ts new file mode 100644 index 000000000000..8314b267b51d --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/navigation/list/focus.spec.ts @@ -0,0 +1,154 @@ +import { test, expect } from '@playwright/test'; +import { createWidget } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('List', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + const LIST_ITEM_DELETE_BUTTON = 'dx-list-static-delete-button'; + + const createList = (selectionMode, allowItemDeleting = false) => createWidget(page, 'dxList', { + items: ['item1', 'item2', 'item3'], + showSelectionControls: true, + selectionMode, + allowItemDeleting, + }); + + [true, false].forEach((focusStateEnabled) => { + test(`Should${focusStateEnabled ? '' : ' not'} focus item when deleting when focusStateEnabled=${focusStateEnabled} (T1226030)`, async ({ page }) => { + await createWidget(page, 'dxList', { + items: ['item1', 'item2', 'item3'], + selectionMode: 'none', + allowItemDeleting: true, + itemDeleteMode: 'static', + focusStateEnabled, + }); + + const list = page.locator('#container'); + const firstItem = list.getItem(0); + const $firstDeleteBtn = firstItem.element.find(`.${LIST_ITEM_DELETE_BUTTON}`); + + await page.click($firstDeleteBtn) + .expect(firstItem.isFocused) + .eql(focusStateEnabled); + + }); + }); + + test('Should apply styles on selectAll checkbox after tab button press', async ({ page }) => { + await createList('all'); + + const list = page.locator('#container'); + + await page.keyboard.press('Tab') + .expect(list.selectAll.checkBox.isFocused) + .ok(); + + }); + + test('Should apply styles on selectAll checkbox after enter button press on it', async ({ page }) => { + await createList('all'); + + const list = page.locator('#container'); + + await page.keyboard.press('Tab') + .pressKey('enter') + .expect(list.selectAll.checkBox.isChecked) + .ok(); + + }); + + ['single', 'multiple'].forEach((selectionMode) => { + test(`Should apply styles on list item after tab button press, ${selectionMode} mode`, async ({ page }) => { + await createList(selectionMode); + + const list = page.locator('#container'); + + await page.keyboard.press('Tab') + .expect(list.getItem(0).isFocused) + .ok(); + + }); + + test(`Should apply styles on list item after enter button press on it, ${selectionMode} mode`, async ({ page }) => { + await createList(selectionMode); + + const list = page.locator('#container'); + + const firstItem = list.getItem(0); + const firstItemType = selectionMode === 'single' ? firstItem.radioButton : firstItem.checkBox; + + await page.keyboard.press('Tab') + .pressKey('enter') + .expect(firstItemType.isChecked) + .ok(); + + }); + }); + + test('Should select next item after delete by keyboard', async ({ page }) => { + await createList('none', true); + + const list = page.locator('#container'); + const firstItem = list.getItem(0); + + await page.expect(list.getVisibleItems().count).eql(3) + .click(firstItem.element) + .pressKey('delete'); + + const item = list.getItem(0); + + expect(item.isFocused).toBeTruthy(); + expect(item.text).toBe('item2'); + await expect(list.getItems().count).eql(2); + + }); + + test('Should select previous item after delete last item', async ({ page }) => { + await createList('none', true); + + const list = page.locator('#container'); + const lastItem = list.getItem(2); + + await page.expect(list.getVisibleItems().count).eql(3) + .click(lastItem.element) + .pressKey('delete'); + + const item = list.getItem(1); + + expect(item.isFocused).toBeTruthy(); + expect(item.text).toBe('item2'); + await expect(list.getItems().count).eql(2); + + }); + + [[2, 0], [1, 2]].forEach(([selectItemIdx, deleteItemIdx]) => { + test(`Should not change selection after delete another (not selected) item (${selectItemIdx}, ${deleteItemIdx})`, async ({ page }) => { + await createList('none', true); + + const list = page.locator('#container'); + const itemToSelect = list.getItem(selectItemIdx); + const itemToDelete = list.getItem(deleteItemIdx); + + await page.expect(list.getVisibleItems().count).eql(3) + .click(itemToSelect.element) + .click(itemToDelete.element.find('.dx-button')); + + const item = list.getItem(deleteItemIdx > selectItemIdx ? selectItemIdx : selectItemIdx - 1); + + expect(item.isFocused).toBeTruthy(); + expect(item.text).toBe(`item${selectItemIdx + 1}`); + await expect(list.getItems().count).eql(2); + + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/navigation/list/grouping.spec.ts b/e2e/testcafe-devextreme/playwright-tests/navigation/list/grouping.spec.ts new file mode 100644 index 000000000000..6b001c8bfa74 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/navigation/list/grouping.spec.ts @@ -0,0 +1,110 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, appendElementTo, setAttribute } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('Grouping', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('Grouped list appearance', async ({ page }) => { + await createWidget(page, 'dxList', { + width: 300, + dataSource: [ + { + key: 'group_1', + items: ['item_1_1', 'item_1_2', 'item_1_3'], + expanded: false, + }, + { + key: 'group_2', + items: [ + { text: 'item_2_1', disabled: true }, + { text: 'item_2_2', icon: 'home' }, + { text: 'item_2_3', showChevron: true, badge: 'item_2_3' }, + { text: 'item_2_4', badge: 'item_2_4' }, + 'item_2_5', + ], + }, + { + key: 'group_3', + items: ['item_3_1', 'item_3_2', 'item_3_3'], + expanded: false, + }, + ], + collapsibleGroups: true, + grouped: true, + allowItemDeleting: true, + itemDeleteMode: 'static', + itemDragging: { + allowReordering: true, + }, + }); + + const list = page.locator('#container'); + + await list.getItem(2).element.click() + .pressKey('down'); + + await testScreenshot(page, 'Grouped list appearance, header focused.png', { element: '#container' }); + + await page.click(list.getGroup(0).header) + .click(list.getGroup(2).header) + .click(list.getItem(4).element) + .hover(list.getGroup(1).header); + + await testScreenshot(page, 'Grouped list appearance, item focused, header hovered.png', { element: '#container' }); + + await list.option('collapsibleGroups', false); + + await testScreenshot(page, 'Grouped list appearance,collapsibleGroups=false.png', { element: '#container' }); + + }); + + test('Grouped list appearance with template', async ({ page }) => { + + await setAttribute(page, '#container', 'style', 'display: flex; gap: 40px; padding: 8px; width: fit-content;'); + + const dataSource = [ + { key: 'One', items: ['1_1', '1_2', '1_3'] }, + { key: 'Two', items: ['2_1', '2_2', '2_3'] }, + { key: 'Three', items: ['3_1', '3_2', '3_3'] }, + ]; + + await Promise.all([false, true].map((rtlEnabled) => appendElementTo(page, '#container', 'div', `list-rtl-${rtlEnabled}`))); + await Promise.all([false, true].map((rtlEnabled) => createWidget(page, 'dxList', { + dataSource, + width: 300, + groupTemplate(data) { + const wrapper = $('
'); + + $(`${data.key}`).appendTo(wrapper); + $('
second row
').appendTo(wrapper); + + return wrapper; + }, + collapsibleGroups: true, + grouped: true, + rtlEnabled, + }, `#list-rtl-${rtlEnabled}`))); + + const list = page.locator('#list-rtl-false'); + const list2 = page.locator('#list-rtl-true'); + + await page.click(list.getGroup(0).header) + .click(list.getGroup(2).header) + .click(list2.getGroup(0).header) + .click(list2.getGroup(2).header) + .click('#container'); + + await testScreenshot(page, 'Grouped list appearance with template.png', { element: '#container' }); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/navigation/list/paging.spec.ts b/e2e/testcafe-devextreme/playwright-tests/navigation/list/paging.spec.ts new file mode 100644 index 000000000000..3b935cac0312 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/navigation/list/paging.spec.ts @@ -0,0 +1,222 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, isMaterial } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('List', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + function generateData(count) { + const items: { id: number }[] = []; + + for (let i = 0; i < count; i += 1) { + items.push({ id: i + 1 }); + } + return items; + } + + test('Should initiate load next pages if items on the first pages are invisible', async ({ page }) => { + + const sampleData = generateData(12).map((data) => ({ + ...data, + visible: data.id > 8, + })); + + await createWidget(page, 'dxList', { + dataSource: { + store: sampleData, + paginate: true, + pageSize: 2, + }, + height: 100, + width: 200, + pageLoadMode: 'scrollBottom', + valueExpr: 'id', + displayExpr: 'id', + }); + + const list = page.locator('#container'); + + await page.expect(list.getItems().count) + .eql(isMaterial() ? 10 : 12) + .expect(list.getVisibleItems().count) + .eql(isMaterial() ? 2 : 4); + + await testScreenshot(page, 'List loading with first items invisible.png', { element: '#container' }); + + }); + + test('Should initiate load next page if all items in the current load are invisible, pageLoadMode: scrollBottom (T1092746)', async ({ page }) => { + + const sampleData = generateData(12).map((data) => ({ + ...data, + visible: data.id <= 4 || data.id > 8, + })); + + await createWidget(page, 'dxList', { + dataSource: { + store: sampleData, + paginate: true, + pageSize: 2, + }, + height: 100, + width: 200, + pageLoadMode: 'scrollBottom', + valueExpr: 'id', + displayExpr: 'id', + }); + + const list = page.locator('#container'); + + await list.scrollTo(100); + + await page.expect(list.getItems().count) + .eql(isMaterial() ? 4 : 10) + .expect(list.getVisibleItems().count) + .eql(isMaterial() ? 4 : 6); + + await testScreenshot(page, 'List loading with middle items invisible.png', { element: '#container' }); + + }); + + test('Should initiate load next page if some items in the current load are invisible, pageLoadMode: scrollBottom', async ({ page }) => { + + const sampleData = generateData(12).map((data) => ({ + ...data, + visible: data.id <= 4 || data.id === 8 || data.id === 11, + })); + + await createWidget(page, 'dxList', { + dataSource: { + store: sampleData, + paginate: true, + pageSize: 2, + }, + height: 100, + width: 200, + pageLoadMode: 'scrollBottom', + valueExpr: 'id', + displayExpr: 'id', + }); + + const list = page.locator('#container'); + + await list.scrollTo(100); + + await page.expect(list.getItems().count) + .eql(isMaterial() ? 4 : 12) + .expect(list.getVisibleItems().count) + .eql(isMaterial() ? 4 : 6); + + await testScreenshot(page, 'List loading with part items invisible on loaded page.png', { element: '#container' }); + + }); + + test('Should initiate load next page if all items on next pages are invisible', async ({ page }) => { + + const sampleData = generateData(12).map((data) => ({ + ...data, + visible: data.id <= 4, + })); + + await createWidget(page, 'dxList', { + dataSource: { + store: sampleData, + paginate: true, + pageSize: 2, + }, + height: 100, + width: 200, + pageLoadMode: 'scrollBottom', + valueExpr: 'id', + displayExpr: 'id', + }); + + const list = page.locator('#container'); + + await list.scrollTo(100); + + await page.expect(list.getItems().count) + .eql(isMaterial() ? 4 : 12) + .expect(list.getVisibleItems().count) + .eql(4); + + await testScreenshot(page, 'List loading with last items invisible.png', { element: '#container' }); + + }); + + test('Should not initiate load next page if not reach the bottom when pullRefreshEnabled is true', async ({ page }) => { + + const sampleData = generateData(12).map((data) => ({ + ...data, + })); + + await createWidget(page, 'dxList', { + dataSource: { + store: sampleData, + paginate: true, + pageSize: 2, + }, + pullRefreshEnabled: true, + height: 130, + width: 200, + pageLoadMode: 'scrollBottom', + valueExpr: 'id', + displayExpr: 'id', + }); + + const list = page.locator('#container'); + + await list.scrollTo(1); + + await page.expect(list.getItems().count) + .eql(4); + + }); + + test('Should initiate load next page on select last item by keyboard', async ({ page }) => { + + const sampleData = generateData(12).map((data) => ({ + ...data, + })); + + await createWidget(page, 'dxList', { + dataSource: { + store: sampleData, + paginate: true, + pageSize: 3, + }, + pullRefreshEnabled: true, + height: 160, + width: 200, + pageLoadMode: 'scrollBottom', + valueExpr: 'id', + displayExpr: 'id', + }); + + const list = page.locator('#container'); + + await list.focus(); + + await page.expect(list.getItems().count) + .eql(6); + + await page.keyboard.press('ArrowDown') + .pressKey('down') + .pressKey('down') + .pressKey('down') + .pressKey('down'); + + await page.expect(list.getItems().count) + .eql(9); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/navigation/list/search.spec.ts b/e2e/testcafe-devextreme/playwright-tests/navigation/list/search.spec.ts new file mode 100644 index 000000000000..058d858a85b3 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/navigation/list/search.spec.ts @@ -0,0 +1,37 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, appendElementTo, setAttribute } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('Search', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('List with search bar appearance', async ({ page }) => { + + await setAttribute(page, '#container', 'style', 'display: flex; gap: 40px; padding: 8px; width: fit-content;'); + + const dataSource = Array.from({ length: 8 }, (_, i) => `Item_${i + 1}`); + const selectionModes = ['none', 'single', 'multiple', 'all']; + + await Promise.all(selectionModes.map((mode) => appendElementTo(page, '#container', 'div', `list-${mode}`))); + await Promise.all(selectionModes.map((mode) => createWidget(page, 'dxList', { + dataSource, + height: 400, + width: 200, + searchEnabled: true, + showSelectionControls: true, + selectionMode: mode, + }, `#list-${mode}`))); + + await testScreenshot(page, 'List with search.png', { element: '#container' }); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/navigation/menu/common.spec.ts b/e2e/testcafe-devextreme/playwright-tests/navigation/menu/common.spec.ts new file mode 100644 index 000000000000..92a0502af221 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/navigation/menu/common.spec.ts @@ -0,0 +1,171 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, appendElementTo, setAttribute, insertStylesheetRulesToPage } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('Menu_common', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('Menu items render', async ({ page }) => { + + await appendElementTo(page, '#container', 'div', 'menu'); + await setAttribute(page, '#container', 'style', 'box-sizing: border-box; width: 400px; height: 400px; padding: 8px;'); + await insertStylesheetRulesToPage(page, '.custom-class { border: 2px solid green !important }'); + + const menuItems: any[] = [ + { + text: 'remove', + icon: 'remove', + items: [ + { + text: 'user', + icon: 'user', + disabled: true, + items: [{ text: 'user_1' }], + }, + { + text: 'save', + icon: 'save', + items: [ + { text: 'export', icon: 'export' }, + { text: 'edit', icon: 'edit' }, + ], + }, + ], + }, + { + text: 'user', + icon: 'user', + items: [ + { + text: 'user', + icon: 'user', + selected: true, + }, + { + text: 'save', + icon: 'save', + }, + ], + }, + { + text: 'coffee', + icon: 'coffee', + disabled: true, + }, + ]; + + await createWidget(page, 'dxMenu', { items: menuItems, cssClass: 'custom-class' }, '#menu'); + + const menu = new Menu(); + + await page.click(menu.getItem(0)) + .pressKey('down') + .pressKey('down') + .pressKey('right'); + + await testScreenshot(page, 'Menu render items.png', { element: '#container' }); + + await page.click(menu.getItem(1)) + .pressKey('down'); + + await testScreenshot(page, 'Menu selected focused item.png', { + element: '#container', + }); + + }); + + [true, false].forEach((adaptivityEnabled) => { + test(`Menu item with link, adaptivityEnabled=${adaptivityEnabled}`, async ({ page }) => { + + await appendElementTo(page, '#container', 'div', 'menu'); + await setAttribute(page, '#container', 'style', 'width: 200px; height: 400px;'); + + const items: any[] = [{ + text: 'Items 1', + items: [{ + text: 'Item 1', + }, { + text: 'Item 2', + icon: 'bookmark', + url: 'https://js.devexpress.com/', + }, { + icon: 'more', + url: 'https://js.devexpress.com/', + }, { + text: 'Item 4', + url: 'https://js.devexpress.com/', + }], + }]; + + if (adaptivityEnabled) { + items.push( + { text: 'Items 2' }, + { text: 'Items 3' }, + { text: 'Items 4' }, + + } + + await createWidget(page, 'dxMenu', { + adaptivityEnabled, + items, + }, '#menu'); + + + const menu = new Menu(adaptivityEnabled); + + if (adaptivityEnabled) { + await click(menu.getHamburgerButton()); + } + + await page.click(menu.getItem(0)) + .pressKey('down') + .pressKey('down'); + + await testScreenshot(page, `Menu item with link and icon focused, adaptivityEnabled=${adaptivityEnabled}.png`); + + await page.keyboard.press('ArrowDown') + .pressKey('down'); + + await testScreenshot(page, `Menu item with link focused, adaptivityEnabled=${adaptivityEnabled}.png`); + + }); + }); + + test('Menu scrolling', async ({ page }) => { + + const items: any[] = new Array(99).fill(null).map((_, idx) => ({ text: `item ${idx}` })); + + items[98].items = new Array(99).fill(null).map((_, idx) => ({ text: `item ${idx}` })); + + await createWidget(page, 'dxMenu', { + items: [ + { + text: 'root', + items, + }, + ], + showFirstSubmenuMode: 'onClick', + hideSubmenuOnMouseLeave: true, + }); + + const menu = new Menu(); + + await page.click(menu.getItem(0)) + .pressKey('down') + .pressKey('up') + .pressKey('right') + .pressKey('up'); + + await testScreenshot(page, 'Menu scrolling.png'); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/navigation/menu/delimiter.spec.ts b/e2e/testcafe-devextreme/playwright-tests/navigation/menu/delimiter.spec.ts new file mode 100644 index 000000000000..b1dd709262b4 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/navigation/menu/delimiter.spec.ts @@ -0,0 +1,150 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, appendElementTo, setAttribute } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('Menu_common', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + const items: any[] = [ + { text: 'Category 1' }, + { + text: 'Category 2', + items: [ + { text: 'Item long name 2-1' }, + { text: 'Item long name 2-2' }, + ], + }, + { + text: 'Category 3', + items: [ + { text: 'Item 1' }, + { text: 'Item 2' }, + ], + }, + { + text: 'Category 4', + items: [ + { text: 'Item long name 4-1' }, + { text: 'Item long name 4-2' }, + ], + }, + ]; + + ['horizontal', 'vertical'].forEach((orientation) => { + const testName = `Menu delimiter, orientation=${orientation}`; + test(testName, async ({ page }) => { + await createWidget(page, + 'dxMenu', + { + items, + orientation, + }, + '#container', + + const menu = new Menu(); + + await page.click(menu.getItem(2)) + .pressKey('down'); + + await testScreenshot(page, `${testName}.png`); + + if (orientation === 'horizontal') { + await page.click(menu.getItem(1)) + .pressKey('down'); + + await testScreenshot(page, `${testName}, wide submenu.png`); + } + + }); + }); + + ['horizontal', 'vertical'].forEach((orientation) => { + ['bottom', 'right', 'bottom right'].forEach((collision) => { + const testName = `Menu delimiter ${collision} collision, orientation=${orientation}`; + test(testName, async ({ page }) => { + + await appendElementTo(page, '#container', 'div', 'menu'); + const additionalStyles = { + bottom: 'justify-content: start;', + right: 'align-content: start;', + }; + await setAttribute(page, '#container', 'style', `width: 500px; height: 500px; display: grid; ${additionalStyles[collision] ?? ''}`); + + await createWidget(page, + 'dxMenu', + { + elementAttr: { + style: 'align-self: end; justify-self: end;', + }, + items, + orientation, + }, + '#menu', + + const menu = new Menu(); + + await click(menu.getItem(3)); + + await testScreenshot(page, `${testName}.png`); + + }); + }); + }); + + test('Menu delimiter appearance when the Menu is used as a toolbar item', async ({ page }) => { + + const toolbarItems = [ + { + location: 'before', + widget: 'dxMenu', + options: { + items: [{ + text: 'Video Players', + }, { + text: 'Televisions', + items: [{ + id: '2_1', + text: 'SuperLCD 42', + }, { + id: '2_2', + text: 'SuperLED 42', + }], + }], + }, + }, { + location: 'before', + widget: 'dxButton', + options: { + icon: 'undo', + }, + }, { + location: 'before', + widget: 'dxButton', + options: { + icon: 'redo', + }, + }, + ]; + + await createWidget(page, 'dxToolbar', { + items: toolbarItems, + width: '100%', + }, '#container'); + + const menu = new Menu(); + + await click(menu.getItem(1)); + + await testScreenshot(page, 'Menu delimiter, menu as toolbar item.png'); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/navigation/menu/keyboard.spec.ts b/e2e/testcafe-devextreme/playwright-tests/navigation/menu/keyboard.spec.ts new file mode 100644 index 000000000000..34c03d55e373 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/navigation/menu/keyboard.spec.ts @@ -0,0 +1,137 @@ +import { test, expect } from '@playwright/test'; +import { createWidget } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('Menu_keyboard', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('keyboard navigation should work after click on a root item if showFirstSubmenuMode is "onClick"', async ({ page }) => { + await createWidget(page, 'dxMenu', { + items: [{ + text: 'Item 1', + items: [{ + text: 'item 1_1', + items: [{ + text: 'item_1_1_1', + }], + }], + }], + showFirstSubmenuMode: 'onClick', + hideSubmenuOnMouseLeave: true, + }); + + const menu = new Menu(); + + await page.click(menu.getItem(0)) + .pressKey('down') + .pressKey('right') + .pressKey('down'); + + const focusedElement = menu.getItem(2); + + expect(focusedElement.innerText).toBe('item_1_1_1') + .expect(menu.isElementFocused(focusedElement)) + .eql(true); + + }); + + test('keyboard navigation should work after hover a root item if showFirstSubmenuMode is "onHover"', async ({ page }) => { + await createWidget(page, 'dxMenu', { + items: [{ + text: 'Item 1', + items: [{ + text: 'item 1_1', + items: [{ + text: 'item_1_1_1', + }], + }], + }], + showFirstSubmenuMode: 'onHover', + hideSubmenuOnMouseLeave: true, + }); + + const menu = new Menu(); + + await page.locator('body').click() + .hover(menu.getItem(0)) + .pressKey('down') + .pressKey('right') + .pressKey('down'); + + const focusedElement = menu.getItem(2); + + expect(focusedElement.innerText).toBe('item_1_1_1') + .expect(menu.isElementFocused(focusedElement)) + .eql(true); + + }); + + test('menu should be closed after press on "escape" key when submenu was shown by click, showFirstSubmenuMode="onClick" (T1115916)', async ({ page }) => { + await createWidget(page, 'dxMenu', { + items: [{ + text: 'Item 1', + items: [{ + text: 'item 1_1', + items: [{ + text: 'item_1_1_1', + }], + }], + }], + showFirstSubmenuMode: 'onClick', + hideSubmenuOnMouseLeave: true, + }); + + const menu = new Menu(); + + await page.locator('body').click() + .click(menu.getItem(0)); + + const submenu = menu.getSubMenuInstance(menu.getItem(0)); + + await page.expect(submenu.option('visible')) + .eql(true) + .pressKey('esc') + .expect(submenu.option('visible')) + .eql(false); + + }); + + test('menu should be closed after press on "escape" key when submenu was shown by hover, showFirstSubmenuMode="onHover" (T1115916)', async ({ page }) => { + await createWidget(page, 'dxMenu', { + items: [{ + text: 'Item 1', + items: [{ + text: 'item 1_1', + items: [{ + text: 'item_1_1_1', + }], + }], + }], + showFirstSubmenuMode: 'onHover', + hideSubmenuOnMouseLeave: true, + }); + + const menu = new Menu(); + + await page.locator('body').click() + .hover(menu.getItem(0)); + + const submenu = menu.getSubMenuInstance(menu.getItem(0)); + + await page.expect(submenu.option('visible')) + .eql(true) + .pressKey('esc') + .expect(submenu.option('visible')) + .eql(false); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/navigation/scrollView/scrollView.spec.ts b/e2e/testcafe-devextreme/playwright-tests/navigation/scrollView/scrollView.spec.ts new file mode 100644 index 000000000000..38cb8960eb62 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/navigation/scrollView/scrollView.spec.ts @@ -0,0 +1,54 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, appendElementTo } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('ScrollView', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + type ScrollableDirection = 'both' | 'horizontal' | 'vertical'; + + [150, 300].forEach((scrollableContentSize) => { + (['vertical', 'horizontal'] as ScrollableDirection[]).forEach((direction) => { + ['onHover', 'always', 'onScroll', 'never'].forEach((showScrollbar) => { + const scrollableContainerSize = 200; + const scrollBarVisibleAfterMouseEnter = (showScrollbar === 'always' || showScrollbar === 'onHover') && scrollableContentSize > scrollableContainerSize; + const scrollBarVisibleAfterMouseLeave = showScrollbar === 'always' && scrollableContentSize > scrollableContainerSize; + + test(`Scroll visibility on mouseEnter/mouseLeave, showScrollbar: '${showScrollbar}', direction: '${direction}', content ${scrollableContentSize < scrollableContainerSize ? 'less' : 'more'} than container (T817096)`, async ({ page }) => { + + await appendElementTo(page, '#container', 'div', 'scrollView'); + await appendElementTo(page, '#scrollView', 'div', 'innerScrollViewContent', { + width: `${scrollableContentSize}px`, height: `${scrollableContentSize}px`, backgroundColor: 'steelblue', + }); + + await createWidget(page, 'dxScrollView', { + width: scrollableContainerSize, + height: scrollableContainerSize, + useNative: false, + direction, + showScrollbar, + }, '#scrollView'); + + + const scrollView = new ScrollView('#scrollView', { direction }); + + await expect(scrollView.scrollbar.isScrollVisible()).eql(scrollBarVisibleAfterMouseLeave); + await hover(scrollView.getContainer()); + await expect(scrollView.scrollbar.isScrollVisible()).eql(scrollBarVisibleAfterMouseEnter); + await page.locator('body').click(); + await expect(scrollView.scrollbar.isScrollVisible()).eql(scrollBarVisibleAfterMouseLeave); + + }); + }); + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/navigation/scrollable/integration.spec.ts b/e2e/testcafe-devextreme/playwright-tests/navigation/scrollable/integration.spec.ts new file mode 100644 index 000000000000..f0a9a732e754 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/navigation/scrollable/integration.spec.ts @@ -0,0 +1,53 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, appendElementTo, setAttribute } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('Integration_DataGrid', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + [true, false].forEach((useNative) => { + test(`The rows in the fixed column are not aligned when the grid is encapsulated inside a element, useNative: ${useNative} (T1071725)`, async ({ page }) => { + + await setAttribute(page, '#container', 'style', 'width: 300px; height: 200px;'); + + await appendElementTo(page, '#container', 'table', 'outerTable', {}); + await appendElementTo(page, '#outerTable', 'tr', 'outerTableTR', {}); + await appendElementTo(page, '#outerTableTR', 'td', 'outerTableTD', {}); + await appendElementTo(page, '#outerTableTR', 'div', 'grid', {}); + + await createWidget(page, 'dxDataGrid', { + dataSource: [ + { + field1: 'test1', field2: 'test2', + }, + ], + scrolling: { + useNative, + }, + width: 300, + columnFixing: { + // @ts-expect-error private option + legacyMode: true, + }, + columns: [ + { dataField: 'field1', fixed: true }, + { dataField: 'field2' }, + ], + hoverStateEnabled: true, + }, '#grid'); + + + await testScreenshot(page, `Grid with scrollable wrapped in td,useNative=${useNative}.png`, { element: '#container' }); + + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/navigation/scrollable/scrollable.spec.ts b/e2e/testcafe-devextreme/playwright-tests/navigation/scrollable/scrollable.spec.ts new file mode 100644 index 000000000000..8099e8747848 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/navigation/scrollable/scrollable.spec.ts @@ -0,0 +1,478 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, appendElementTo } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('Scrollable_ScrollToElement', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + type ScrollableDirection = 'both' | 'horizontal' | 'vertical'; + + (['both'] as ScrollableDirection[]).forEach((direction) => { + test(`ScrollToElement, element less container,direction=${direction}`, async ({ page }) => { + + const positions = [ + { initialScrollOffset: { top: 80, left: 80 }, position: 'elementInsideContainer' }, + { initialScrollOffset: { top: 0, left: 0 }, position: 'fromTopLCorner' }, + { initialScrollOffset: { top: 0, left: 80 }, position: 'fromTop' }, + { initialScrollOffset: { top: 0, left: 160 }, position: 'fromTopRCorner' }, + { initialScrollOffset: { top: 80, left: 160 }, position: 'fromR' }, + { initialScrollOffset: { top: 160, left: 160 }, position: 'fromBRCorner' }, + { initialScrollOffset: { top: 160, left: 80 }, position: 'fromB' }, + { initialScrollOffset: { top: 160, left: 0 }, position: 'fromBLCorner' }, + { initialScrollOffset: { top: 80, left: 0 }, position: 'fromL' }, + // part + { initialScrollOffset: { top: 125, left: 125 }, position: 'part-fromTopLCorner' }, + { initialScrollOffset: { top: 125, left: 80 }, position: 'part-fromTop' }, + { initialScrollOffset: { top: 125, left: 45 }, position: 'part-fromTopRCorner' }, + { initialScrollOffset: { top: 80, left: 45 }, position: 'part-fromR' }, + { initialScrollOffset: { top: 45, left: 45 }, position: 'part-fromBRCorner' }, + { initialScrollOffset: { top: 45, left: 80 }, position: 'part-fromB' }, + { initialScrollOffset: { top: 45, left: 125 }, position: 'part-fromBLCorner' }, + { initialScrollOffset: { top: 80, left: 125 }, position: 'part-fromL' }, + ]; + + for (const useNative of [true, false]) { + for (const rtlEnabled of [true, false]) { + for (const { initialScrollOffset } of positions) { + const id = `${`dx${new Guid()}`}`; + + await appendElementTo(page, '#container', 'div', id, { + border: '1px solid black', + display: 'inline-block', + }); + + await appendElementTo(page, `#${id}`, 'div', `${id}scrollableContent`, { + width: '250px', + height: '250px', + border: '1px solid #0b837a', + backgroundColor: 'lightskyblue', + }); + + await appendElementTo(page, `#${id}scrollableContent`, 'div', `${id}element`, { + position: 'absolute', + boxSizing: 'border-box', + left: '100px', + top: '100px', + height: '50px', + width: '50px', + backgroundColor: '#2bb97f', + border: '5px solid red', + margin: '5px', + }); + + await createWidget(page, 'dxScrollable', { + width: 100, + height: 100, + useNative, + direction, + rtlEnabled, + showScrollbar: 'always', + }, `#${id}`); + + const scrollable = new Scrollable(`#${id}`, { useNative, direction }); + + await scrollable.scrollTo(initialScrollOffset); + await scrollable.scrollToElement(`#${id}element`); + } + } + } + + + await testScreenshot(page, `ScrollToElement, element less container direction=${direction}.png`); + + }); + + test(`ScrollToElement, element more container,direction=${direction}`, async ({ page }) => { + + const positions = [ + { initialScrollOffset: { top: 0, left: 0 }, position: 'fromTLCorner' }, + { initialScrollOffset: { top: 0, left: 40 }, position: 'fromTLPart' }, + { initialScrollOffset: { top: 0, left: 120 }, position: 'fromTRPart' }, + { initialScrollOffset: { top: 0, left: 160 }, position: 'fromTRCorner' }, + + { initialScrollOffset: { top: 40, left: 160 }, position: 'fromRTPart' }, + { initialScrollOffset: { top: 120, left: 160 }, position: 'fromRBPart' }, + + { initialScrollOffset: { top: 160, left: 160 }, position: 'fromBRCorner' }, + { initialScrollOffset: { top: 160, left: 120 }, position: 'fromBRPart' }, + { initialScrollOffset: { top: 160, left: 40 }, position: 'fromBLPart' }, + { initialScrollOffset: { top: 160, left: 0 }, position: 'fromBLCorner' }, + + { initialScrollOffset: { top: 120, left: 0 }, position: 'fromLBPart' }, + { initialScrollOffset: { top: 40, left: 0 }, position: 'fromLTPart' }, + + // from inside + + { initialScrollOffset: { top: 40, left: 60 }, position: 'fromInsideTL' }, + { initialScrollOffset: { top: 40, left: 100 }, position: 'fromInsideTR' }, + { initialScrollOffset: { top: 60, left: 120 }, position: 'fromInsideRT' }, + { initialScrollOffset: { top: 100, left: 120 }, position: 'fromInsideRB' }, + { initialScrollOffset: { top: 120, left: 100 }, position: 'fromInsideBR' }, + { initialScrollOffset: { top: 120, left: 60 }, position: 'fromInsideBL' }, + { initialScrollOffset: { top: 100, left: 40 }, position: 'fromInsideLB' }, + { initialScrollOffset: { top: 60, left: 40 }, position: 'fromInsideLT' }, + ]; + + for (const useNative of [true, false]) { + for (const rtlEnabled of [true, false]) { + for (const { initialScrollOffset } of positions) { + const id = `${`dx${new Guid()}`}`; + + await appendElementTo(page, '#container', 'div', id, { + border: '1px solid black', + display: 'inline-block', + }); + + await appendElementTo(page, `#${id}`, 'div', `${id}scrollableContent`, { + width: '250px', + height: '250px', + border: '1px solid #0b837a', + backgroundColor: 'lightskyblue', + }); + + await appendElementTo(page, `#${id}scrollableContent`, 'div', `${id}element`, { + position: 'absolute', + boxSizing: 'border-box', + left: '20px', + top: '20px', + height: '200px', + width: '200px', + backgroundColor: '#2bb97f', + border: '5px solid red', + margin: '5px', + }); + + await createWidget(page, 'dxScrollable', { + width: 100, + height: 100, + useNative, + direction, + showScrollbar: 'always', + rtlEnabled, + }, `#${id}`); + + const scrollable = new Scrollable(`#${id}`, { useNative, direction }); + + await scrollable.scrollTo(initialScrollOffset); + await scrollable.scrollToElement(`#${id}element`); + } + } + } + + + await testScreenshot(page, `ScrollToElement, element more container direction=${direction}.png`); + + }); + + test(`ScrollToElement with scaling scale(1.5),direction=${direction}`, async ({ page }) => { + + const positions = [ + { initialScrollOffset: { top: 0, left: 0 }, position: 'fromTLCorner' }, + { initialScrollOffset: { top: 0, left: 290 }, position: 'fromTRCorner' }, + { initialScrollOffset: { top: 290, left: 290 }, position: 'fromBRCorner' }, + { initialScrollOffset: { top: 290, left: 0 }, position: 'fromBLCorner' }, + + { initialScrollOffset: { top: 0, left: 160 }, position: 'fromT' }, + { initialScrollOffset: { top: 160, left: 290 }, position: 'fromR' }, + { initialScrollOffset: { top: 290, left: 160 }, position: 'fromB' }, + { initialScrollOffset: { top: 160, left: 0 }, position: 'fromL' }, + + // from inside + + { initialScrollOffset: { top: 165, left: 175 }, position: 'fromInsideTLPart' }, + { initialScrollOffset: { top: 140, left: 140 }, position: 'fromInsideRBPart' }, + ]; + + for (const useNative of [true, false]) { + for (const rtlEnabled of [true, false]) { + for (const { initialScrollOffset } of positions) { + const id = `${`dx${new Guid()}`}`; + + await appendElementTo(page, '#container', 'div', id, { + border: '1px solid black', + display: 'inline-block', + }); + + await appendElementTo(page, `#${id}`, 'div', `${id}scrollableContent`, { + width: '250px', + height: '250px', + border: '1px solid #0b837a', + backgroundColor: 'lightskyblue', + transform: 'scale(1.5)', + transformOrigin: '0 0', + }); + + await appendElementTo(page, `#${id}scrollableContent`, 'div', `${id}element`, { + position: 'absolute', + boxSizing: 'border-box', + left: '20px', + top: '20px', + height: '200px', + width: '200px', + backgroundColor: '#2bb97f', + border: '5px solid red', + margin: '5px', + }); + + await createWidget(page, 'dxScrollable', { + width: 100, + height: 100, + useNative, + direction, + showScrollbar: 'always', + rtlEnabled, + }, `#${id}`); + + const scrollable = new Scrollable(`#${id}`, { useNative, direction }); + + await scrollable.scrollTo(initialScrollOffset); + await scrollable.scrollToElement(`#${id}element`); + } + } + } + + + await testScreenshot(page, `ScrollToElement with scaling scale(1.5),direction=${direction}.png`); + + }); + }); + + (['horizontal'] as ScrollableDirection[]).forEach((direction) => { + [false, true].forEach((useNative) => { + [false, true].forEach((useSimulatedScrollbar) => { + test(`Scroll offset after resize, rtlEnabled: true, useNative: '${useNative}', useSimulatedScrollbar: '${useSimulatedScrollbar}, container.width = 75 -> 50 -> 75 -> 100 -> 75`, async ({ page }) => { + + await appendElementTo(page, '#container', 'div', 'scrollable'); + await appendElementTo(page, '#scrollable', 'div', 'content', { + width: '100px', height: '100px', backgroundColor: 'skyblue', + }); + + await createWidget(page, 'dxScrollable', { + width: 50, + height: 50, + useNative, + rtlEnabled: true, + useSimulatedScrollbar, + direction: 'horizontal', + showScrollbar: 'always', + }, '#scrollable'); + + + const scrollable = new Scrollable('#scrollable', { direction, useNative, useSimulatedScrollbar }); + + await scrollable.setContainerCssWidth(75); + + await expect(await scrollable.scrollOffset()).eql({ left: 25, top: 0 }); + if (scrollable.hScrollbar) { + const { top, left } = await scrollable.hScrollbar?.getScrollTranslate(); + expect(top).toBe(0); + await expect(left).within(18, 20); + } + + await scrollable.setContainerCssWidth(50); + + await expect(await scrollable.scrollOffset()).eql({ left: 50, top: 0 }); + if (scrollable.hScrollbar) { + const { top, left } = await scrollable.hScrollbar?.getScrollTranslate(); + expect(top).toBe(0); + await expect(left).within(24, 26); + } + + await scrollable.setContainerCssWidth(75); + + await expect(await scrollable.scrollOffset()).eql({ left: 25, top: 0 }); + if (scrollable.hScrollbar) { + const { top, left } = await scrollable.hScrollbar?.getScrollTranslate(); + expect(top).toBe(0); + await expect(left).within(18, 20); + } + + await scrollable.setContainerCssWidth(100); + + await expect(await scrollable.scrollOffset()).eql({ left: 0, top: 0 }); + if (scrollable.hScrollbar) { + const { top, left } = await scrollable.hScrollbar?.getScrollTranslate(); + expect(top).toBe(0); + expect(left).toBe(0); + } + + await scrollable.setContainerCssWidth(75); + + await expect(await scrollable.scrollOffset()).eql({ left: 25, top: 0 }); + if (scrollable.hScrollbar) { + const { top, left } = await scrollable.hScrollbar?.getScrollTranslate(); + expect(top).toBe(0); + await expect(left).within(18, 20); + } + + }); + + [1, 10, 20].forEach((scrollOffset) => { + test(`Scroll offset after resize, rtlEnabled: true, useNative: '${useNative}', useSimulatedScrollbar: '${useSimulatedScrollbar}, scrollTo(Right - ${scrollOffset}), container.width = 75 -> 50 -> 100 -> 75 -> 50`, async ({ page }) => { + + await appendElementTo(page, '#container', 'div', 'scrollable'); + await appendElementTo(page, '#scrollable', 'div', 'content', { + width: '100px', height: '100px', backgroundColor: 'skyblue', + }); + + await createWidget(page, 'dxScrollable', { + width: 50, + height: 50, + useNative, + rtlEnabled: true, + useSimulatedScrollbar, + direction: 'horizontal', + showScrollbar: 'always', + }, '#scrollable'); + + + const scrollable = new Scrollable('#scrollable', { direction, useNative, useSimulatedScrollbar }); + + await scrollable.scrollTo({ left: 50 - scrollOffset }); + await scrollable.update(); + + await scrollable.setContainerCssWidth(75); + + let expectedScrollOffset = (await scrollable.getMaxScrollOffset()).horizontal - scrollOffset; + await page.expect((await scrollable.scrollOffset()).left) + .within(expectedScrollOffset - 1, expectedScrollOffset + 1); + await expect((await scrollable.scrollOffset()).top).eql(0); + if (scrollable.hScrollbar) { + const { top, left } = await scrollable.hScrollbar?.getScrollTranslate(); + expect(top).toBe(0); + const expectedTranslateValue = expectedScrollOffset * 0.75; + await expect(left).within(expectedTranslateValue - 1, expectedTranslateValue + 1); + } + + await scrollable.setContainerCssWidth(50); + + expectedScrollOffset = (await scrollable.getMaxScrollOffset()).horizontal - scrollOffset; + await page.expect((await scrollable.scrollOffset()).left) + .within(expectedScrollOffset - 1, expectedScrollOffset + 1); + await expect((await scrollable.scrollOffset()).top).eql(0); + if (scrollable.hScrollbar) { + const { top, left } = await scrollable.hScrollbar?.getScrollTranslate(); + expect(top).toBe(0); + const expectedTranslateValue = expectedScrollOffset * 0.5; + await expect(left).within(expectedTranslateValue - 1, expectedTranslateValue + 1); + } + + await scrollable.setContainerCssWidth(100); + + await expect(await scrollable.scrollOffset()).eql({ left: 0, top: 0 }); + if (scrollable.hScrollbar) { + const { top, left } = await scrollable.hScrollbar?.getScrollTranslate(); + expect(top).toBe(0); + expect(left).toBe(0); + } + + await scrollable.setContainerCssWidth(75); + + await expect(await scrollable.scrollOffset()).eql({ left: 25, top: 0 }); + if (scrollable.hScrollbar) { + const { top, left } = await scrollable.hScrollbar?.getScrollTranslate(); + expect(top).toBe(0); + await expect(left).within(18, 20); + } + + await scrollable.setContainerCssWidth(50); + + await expect(await scrollable.scrollOffset()).eql({ left: 50, top: 0 }); + if (scrollable.hScrollbar) { + const { top, left } = await scrollable.hScrollbar?.getScrollTranslate(); + expect(top).toBe(0); + await expect(left).within(24, 26); + } + + }); + }); + + [30, 40, 50].forEach((scrollOffset) => { + test(`Scroll offset after resize, rtlEnabled: true, useNative: '${useNative}', useSimulatedScrollbar: '${useSimulatedScrollbar}, scrollTo(${scrollOffset}), container.width = 75 -> 50 -> 100 -> 75 -> 50`, async ({ page }) => { + + await appendElementTo(page, '#container', 'div', 'scrollable'); + await appendElementTo(page, '#scrollable', 'div', 'content', { + width: '100px', height: '100px', backgroundColor: 'skyblue', + }); + + await createWidget(page, 'dxScrollable', { + width: 50, + height: 50, + useNative, + rtlEnabled: true, + useSimulatedScrollbar, + direction: 'horizontal', + showScrollbar: 'always', + }, '#scrollable'); + + + const scrollable = new Scrollable('#scrollable', { direction, useNative, useSimulatedScrollbar }); + + await scrollable.scrollTo({ left: scrollOffset }); + await scrollable.update(); + + await scrollable.setContainerCssWidth(75); + + const expectedScrollOffset = scrollOffset - 25; + await page.expect((await scrollable.scrollOffset()).left) + .within(expectedScrollOffset - 0.5, expectedScrollOffset + 0.5); + await expect((await scrollable.scrollOffset()).top).eql(0); + if (scrollable.hScrollbar) { + const { top, left } = await scrollable.hScrollbar?.getScrollTranslate(); + expect(top).toBe(0); + const expectedTranslateValue = expectedScrollOffset * 0.75; + await expect(left).within(expectedTranslateValue - 0.5, expectedTranslateValue + 0.5); + } + + await scrollable.setContainerCssWidth(50); + + await expect(await scrollable.scrollOffset()).eql({ left: scrollOffset, top: 0 }); + if (scrollable.hScrollbar) { + const { top, left } = await scrollable.hScrollbar?.getScrollTranslate(); + expect(top).toBe(0); + const expectedTranslateValue = scrollOffset * 0.5; + await expect(left).within(expectedTranslateValue - 0.5, expectedTranslateValue + 0.5); + } + + await scrollable.setContainerCssWidth(100); + + await expect(await scrollable.scrollOffset()).eql({ left: 0, top: 0 }); + if (scrollable.hScrollbar) { + const { top, left } = await scrollable.hScrollbar?.getScrollTranslate(); + expect(top).toBe(0); + expect(left).toBe(0); + } + + await scrollable.setContainerCssWidth(75); + + await expect(await scrollable.scrollOffset()).eql({ left: 25, top: 0 }); + if (scrollable.hScrollbar) { + const { top, left } = await scrollable.hScrollbar?.getScrollTranslate(); + expect(top).toBe(0); + await expect(left).within(18, 20); + } + + await scrollable.setContainerCssWidth(50); + + await expect(await scrollable.scrollOffset()).eql({ left: 50, top: 0 }); + if (scrollable.hScrollbar) { + const { top, left } = await scrollable.hScrollbar?.getScrollTranslate(); + expect(top).toBe(0); + await expect(left).within(24, 26); + } + + }); + }); + }); + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/navigation/scrollable/visibility.spec.ts b/e2e/testcafe-devextreme/playwright-tests/navigation/scrollable/visibility.spec.ts new file mode 100644 index 000000000000..d6601df33b87 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/navigation/scrollable/visibility.spec.ts @@ -0,0 +1,67 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, appendElementTo } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('Scrollable_visibility_integration', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + type ScrollableDirection = 'both' | 'horizontal' | 'vertical'; + + (['both'] as ScrollableDirection[]).forEach((direction) => { + [false, true].forEach((useNative) => { + [false, true].forEach((rtlEnabled) => { + [false, true].forEach((useSimulatedScrollbar) => { + test(`Scroll should save position on dxhiding when scroll is hidden, dir: ${direction}, useNative: ${useNative}, useSimulatedScrollbar: ${useSimulatedScrollbar}, rtlEnabled: ${rtlEnabled}`, async ({ page }) => { + + await appendElementTo(page, '#container', 'div', 'scrollable'); + + await appendElementTo(page, '#scrollable', 'div', 'content', { + width: '200px', height: '200px', backgroundColor: 'skyblue', + }); + + await createWidget(page, 'dxScrollable', { + width: 100, + height: 100, + useNative, + rtlEnabled, + useSimulatedScrollbar, + direction, + showScrollbar: 'always', + }, '#scrollable'); + + + const scrollable = new Scrollable('#scrollable', { direction, useNative, useSimulatedScrollbar }); + await scrollable.scrollTo({ left: 10, top: 20 }); + + const expectedScrollOffsetValue = { left: 10, top: 20 }; + await expect(await scrollable.scrollOffset()).eql(expectedScrollOffsetValue); + + await testScreenshot(page, `Scroll position before hide, useNative=${useNative},rtl=${rtlEnabled},useSimScrollbar=${useSimulatedScrollbar}.png`, { element: page.locator('#scrollable') }); + + await page.expect(compareResults.isValid()) + .ok(); + + await scrollable.triggerHidingEvent(); + await scrollable.hide(); + await scrollable.scrollTo({ left: 0, top: 0 }); + await scrollable.show(); + await scrollable.triggerShownEvent(); + + await expect(await scrollable.scrollOffset()).eql(expectedScrollOffsetValue); + await testScreenshot(page, `Scroll position after show, useNative=${useNative},rtl=${rtlEnabled},useSimScrollbar=${useSimulatedScrollbar}.png`, { element: page.locator('#scrollable') }); + + }); + }); + }); + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/navigation/splitter/common.spec.ts b/e2e/testcafe-devextreme/playwright-tests/navigation/splitter/common.spec.ts new file mode 100644 index 000000000000..60bd7e662e31 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/navigation/splitter/common.spec.ts @@ -0,0 +1,120 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('Splitter_common', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + const getScreenshotName = (state: string) => `Splitter apearance - handle in ${state} state.png`; + + test('ResizeHandle appearance in inactive state, allowKeyboardNavigation', async ({ page }) => { + await createWidget(page, 'dxSplitter', { + width: 600, + height: 300, + dataSource: [{ + text: 'pane_1', + }, { + text: 'pane_2', + resizable: false, + }], + }); + + await testScreenshot(page, getScreenshotName('inactive'), { element: '#container' }); + + }); + + test('ResizeHandle appearance in different states, allowKeyboardNavigation', async ({ page }) => { + await createWidget(page, 'dxSplitter', { + width: 600, + height: 300, + dataSource: [{ + text: 'pane_1', + collapsible: true, + }, { + text: 'pane_2', + collapsible: true, + }], + }); + + const splitter = page.locator('#container'); + + await click(page.locator('body'), { offsetX: -50 }); + + await testScreenshot(page, getScreenshotName('normal'), { element: '#container' }); + + await hover(splitter.resizeHandles.nth(0)); + + await testScreenshot(page, getScreenshotName('hover'), { element: '#container' }); + + await page.dispatchEvent(splitter.resizeHandles.nth(0), 'mousedown') + .wait(500); + + await testScreenshot(page, getScreenshotName('active'), { element: '#container' }); + + await dispatchEvent(splitter.resizeHandles.nth(0), 'mouseup'); + + await click(splitter.resizeHandles.nth(0)); + + await testScreenshot(page, getScreenshotName('focused'), { element: '#container' }); + + }); + + ['horizontal', 'vertical'].forEach((orientation) => { + test(`Splitter appearance, orientation='${orientation}'`, async ({ page }) => { + + await createWidget(page, 'dxSplitter', { + orientation, + width: 600, + height: 300, + dataSource: [{ + text: 'pane_1', collapsible: true, + }, { + text: 'pane_2', collapsible: true, + }, + ], + }); + + + await testScreenshot(page, `Splitter appearance, orientation='${orientation}'.png`, { element: '#container' }); + + }); + + test(`Nested Splitter appearance, orientation='${orientation}'`, async ({ page }) => { + + await createWidget(page, 'dxSplitter', { + orientation, + width: 600, + height: 300, + dataSource: [{ text: 'Pane_1', collapsible: true }, + { + splitter: { + orientation: orientation === 'horizontal' ? 'vertical' : 'horizontal', + dataSource: [{ + text: 'Pane_2_1', collapsible: true, + }, { + text: 'Pane_2_2', collapsible: true, + }, { + text: 'Pane_2_3', collapsible: true, + }], + }, + collapsible: true, + }, + { text: 'Pane_3', collapsible: true }, + ], + }); + + + await testScreenshot(page, `Nested Splitter appearance, orientation='${orientation}'.png`, { element: '#container' }); + + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/navigation/splitter/events.spec.ts b/e2e/testcafe-devextreme/playwright-tests/navigation/splitter/events.spec.ts new file mode 100644 index 000000000000..76044b505f4e --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/navigation/splitter/events.spec.ts @@ -0,0 +1,44 @@ +import { test, expect } from '@playwright/test'; +import { createWidget } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('Splitter_events', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('Panes should not be able to resize when onResizeStart event canceled', async ({ page }) => { + await createWidget(page, 'dxSplitter', { + width: 408, + height: 408, + onResizeStart(e) { + const { event } = e; + event.cancel = true; + }, + dataSource: [{ size: '200px' }, { size: '200px' }], + }); + + const splitter = page.locator('#container'); + + await (async () => { + const box = await splitter.resizeHandles.nth(0).boundingBox(); + if (box) { + await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2); + await page.mouse.down(); + await page.mouse.move(box.x + box.width / 2 + 100, box.y + box.height / 2 + 0, { steps: 10 }); + await page.mouse.up(); + } + })(); + + await expect(splitter.option('items[0].size')).eql(200); + await expect(splitter.option('items[1].size')).eql(200); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/navigation/splitter/integration.spec.ts b/e2e/testcafe-devextreme/playwright-tests/navigation/splitter/integration.spec.ts new file mode 100644 index 000000000000..ae5e0c8c008e --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/navigation/splitter/integration.spec.ts @@ -0,0 +1,69 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('Splitter_integration', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('The splitter pane should be rendered with the correct ratio inside the tab content of TabPanel if pane.size uses pixels', async ({ page }) => { + await createWidget(page, 'dxTabPanel', { + width: '100%', + height: 300, + deferRendering: true, + templatesRenderAsynchronously: true, + dataSource: [{ + title: 'Tab_1', + collapsible: true, + text: 'Tab_1 content', + }, { + title: 'Tab_2', + collapsible: true, + template: () => ($('
') as any).dxSplitter({ + orientation: 'horizontal', + allowKeyboardNavigation: true, + dataSource: [{ + size: '100px', + text: 'Pane_1', + collapsible: true, + template: () => $('
').text('Pane_1'), + }, { + collapsible: true, + splitter: { + orientation: 'vertical', + dataSource: [{ + text: 'Pane_2_1', + collapsible: true, + template: () => $('
').text('Pane_2_1'), + }, { + text: 'Pane_2_2', + collapsible: true, + template: () => $('
').text('Pane_2_2'), + }], + }, + }], + }), + }], + }); + + const tabPanel = page.locator('#container'); + + await tabPanel.tabs.getItem(1).element.click() + .click(tabPanel.multiView.element); + + await testScreenshot(page, 'Splitter in tab content, pane_1.size=`100px`.png', { element: '#container' }); + + await resizeWindow(600, 400); + + await testScreenshot(page, 'Splitter in tab content after window resize, pane_1.size=`100px`.png', { element: '#container' }); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/navigation/splitter/keyboard.spec.ts b/e2e/testcafe-devextreme/playwright-tests/navigation/splitter/keyboard.spec.ts new file mode 100644 index 000000000000..b6ccf162e564 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/navigation/splitter/keyboard.spec.ts @@ -0,0 +1,101 @@ +import { test, expect } from '@playwright/test'; +import { createWidget } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('Splitter_keyboard', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('The next resize handle should be focused after tab press', async ({ page }) => { + await createWidget(page, 'dxSplitter', { + width: 400, + height: 400, + dataSource: [ + { text: 'Pane_1' }, + { text: 'Pane_2' }, + { text: 'Pane_2' }, + ], + }); + + const splitter = page.locator('#container'); + + await page.click(splitter.resizeHandles.nth(0)); + + await page.expect(splitter.getResizeHandle(0).isFocused) + .ok() + .expect(splitter.getResizeHandle(1).isFocused) + .notOk(); + + await page.keyboard.press('Tab'); + + await page.expect(splitter.getResizeHandle(0).isFocused) + .notOk() + .expect(splitter.getResizeHandle(1).isFocused) + .ok(); + + }); + + test('The previous resize handle should be focused after shift+tab press', async ({ page }) => { + await createWidget(page, 'dxSplitter', { + width: 400, + height: 400, + dataSource: [ + { text: 'Pane_1' }, + { text: 'Pane_2' }, + { text: 'Pane_2' }, + ], + }); + + const splitter = page.locator('#container'); + + await page.click(splitter.resizeHandles.nth(1)); + + await page.expect(splitter.getResizeHandle(1).isFocused) + .ok() + .expect(splitter.getResizeHandle(0).isFocused) + .notOk(); + + await page.keyboard.press('shift+tab'); + + await page.expect(splitter.getResizeHandle(1).isFocused) + .notOk() + .expect(splitter.getResizeHandle(0).isFocused) + .ok(); + + }); + + [true, false].forEach((allowKeyboardNavigation) => { + test(`The resize handle should not change its focused state after the pane collapses, allowKeyboardNavigation=${allowKeyboardNavigation}`, async ({ page }) => { + await createWidget(page, 'dxSplitter', { + width: 400, + height: 400, + allowKeyboardNavigation, + dataSource: [ + { text: 'Pane_1', collapsible: true }, + { text: 'Pane_2' }, + ], + }); + + const splitter = page.locator('#container'); + + await page.click(splitter.getResizeHandle(0).getCollapsePrev()); + + if (allowKeyboardNavigation) { + await page.expect(splitter.getResizeHandle(0).isFocused) + .ok(); + } else { + await page.expect(splitter.getResizeHandle(0).isFocused) + .notOk(); + } + + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/navigation/splitter/resize.spec.ts b/e2e/testcafe-devextreme/playwright-tests/navigation/splitter/resize.spec.ts new file mode 100644 index 000000000000..0c299ce9f001 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/navigation/splitter/resize.spec.ts @@ -0,0 +1,43 @@ +import { test, expect } from '@playwright/test'; +import { createWidget } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('Splitter_integration', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('non resizable pane should not change its size during resize', async ({ page }) => { + await createWidget(page, 'dxSplitter', { + width: '100%', + height: 300, + dataSource: [{ + text: 'Pane_1', + }, { + text: 'Pane_1', + }, { + text: 'Pane_3', + size: '300px', + resizable: false, + }], + }); + + const splitter = page.locator('#container'); + + await page.expect(splitter.getItem(2).element.clientWidth) + .eql(300); + + await resizeWindow(400, 400); + + await page.expect(splitter.getItem(2).element.clientWidth) + .eql(145); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/navigation/stepper/common.spec.ts b/e2e/testcafe-devextreme/playwright-tests/navigation/stepper/common.spec.ts new file mode 100644 index 000000000000..e75111c18968 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/navigation/stepper/common.spec.ts @@ -0,0 +1,192 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, appendElementTo, setAttribute } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('Stepper_common', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + const commonItems: any[] = [ + { icon: 'cart', label: 'Cart' }, + { icon: 'clipboardtasklist', label: 'Shipping Info' }, + { icon: 'gift', label: 'Promo Code', optional: true }, + { icon: 'packagebox', label: 'Checkout' }, + { icon: 'checkmarkcircle', label: 'Ordered' }, + ]; + + ['horizontal', 'vertical'].forEach((orientation) => { + test('Stepper common properties', async ({ page }) => { + + await appendElementTo(page, '#container', 'div', 'stepper'); + await appendElementTo(page, '#container', 'div', 'stepper2'); + + const containerStyle = orientation === 'horizontal' ? 'width: 800px; flex-direction: column;' : 'height: 600px; width: 400px'; + await setAttribute(page, '#container', 'style', `display: flex; gap: 40px; ${containerStyle}`); + + const stepperOptions = { + selectedIndex: 4, + orientation, + dataSource: commonItems, + }; + + const stepperRTLOptions = { + ...stepperOptions, + rtlEnabled: true, + }; + + await createWidget(page, 'dxStepper', stepperOptions, '#stepper'); + + await createWidget(page, 'dxStepper', stepperRTLOptions, '#stepper2'); + + + await testScreenshot(page, `Stepper orient=${orientation}.png`, { + element: '#container', + }); + + }); + }); + + test('Stepper text overflow in horizontal orientation', async ({ page }) => { + + await appendElementTo(page, '#container', 'div', 'stepper'); + await setAttribute(page, '#container', 'style', 'width: 200px; height: 150px; overflow: auto;'); + + await appendElementTo(page, '#otherContainer', 'div', 'stepper2'); + await setAttribute(page, '#otherContainer', 'style', 'width: 400px; height: 150px; overflow: auto;'); + + await setAttribute(page, '#parentContainer', 'style', 'width: 400px;'); + + const stepperOptions = { + dataSource: commonItems, + }; + + await createWidget(page, 'dxStepper', stepperOptions, '#stepper'); + + await createWidget(page, 'dxStepper', stepperOptions, '#stepper2'); + + await testScreenshot(page, 'Stepper text overflow orient=horizontal.png', { element: '#parentContainer' }); + + }); + + test('Stepper text overflow in vertical orientation', async ({ page }) => { + + await appendElementTo(page, '#container', 'div', 'stepper'); + await appendElementTo(page, '#container', 'div', 'stepper2'); + await setAttribute(page, '#container', 'style', 'display: flex; gap: 40px; width: 400px'); + + const stepperOptions = { + dataSource: commonItems, + width: 120, + height: 400, + orientation: 'vertical', + }; + + const stepperRTLOptions = { + ...stepperOptions, + rtlEnabled: true, + }; + + await createWidget(page, 'dxStepper', stepperOptions, '#stepper'); + + await createWidget(page, 'dxStepper', stepperRTLOptions, '#stepper2'); + + await testScreenshot(page, 'Stepper text overflow orient=vertical.png', { element: '#container' }); + + }); + + [true, false].forEach((selectOnFocus) => { + test('Stepper item states', async ({ page }) => { + + await appendElementTo(page, '#container', 'div', 'stepper'); + await setAttribute(page, '#container', 'style', 'width: 800px; height: 150px;'); + + const dataSource: any[] = [ + { label: 'Default' }, + { label: 'Valid', isValid: true, optional: true }, + { label: 'Invalid', isValid: false, optional: true }, + { + label: 'Disabled', icon: 'packagebox', disabled: true, optional: true, + }, + { label: 'Disabled Valid', disabled: true, isValid: true }, + { label: 'Disabled Invalid', disabled: true, isValid: false }, + { label: 'With Text', text: 'T', optional: true }, + ]; + + const stepperOptions = { + selectOnFocus, + dataSource, + }; + + await createWidget(page, 'dxStepper', stepperOptions, '#stepper'); + + + const state = selectOnFocus ? 'selected' : 'focused'; + + await page.keyboard.press('Tab'); + await testScreenshot(page, `Stepper 1st step selected,selectOnFocus=${selectOnFocus}.png`, { element: '#stepper' }); + + await page.keyboard.press('ArrowRight'); + await testScreenshot(page, `Stepper valid step ${state},selectOnFocus=${selectOnFocus}.png`, { element: '#stepper' }); + + await page.keyboard.press('ArrowRight'); + await testScreenshot(page, `Stepper invalid step ${state},selectOnFocus=${selectOnFocus}.png`, { element: '#stepper' }); + + await page.keyboard.press('ArrowRight'); + await testScreenshot(page, `Stepper disabled step focused,selectOnFocus=${selectOnFocus}.png`, { element: '#stepper' }); + + await page.keyboard.press('ArrowRight'); + await testScreenshot(page, `Stepper disabled valid step focused,selectOnFocus=${selectOnFocus}.png`, { element: '#stepper' }); + + await page.keyboard.press('ArrowRight'); + await testScreenshot(page, `Stepper disabled invalid step focused,selectOnFocus=${selectOnFocus}.png`, { element: '#stepper' }); + + await page.keyboard.press('ArrowRight'); + await testScreenshot(page, `Stepper last step ${state},selectOnFocus=${selectOnFocus}.png`, { element: '#stepper' }); + + }); + }); + + test('Stepper completed item states', async ({ page }) => { + + await appendElementTo(page, '#container', 'div', 'stepper'); + await setAttribute(page, '#container', 'style', 'width: 800px; height: 150px;'); + + const dataSource: any[] = [ + { label: 'Default' }, + { label: 'Valid', isValid: true, optional: true }, + { label: 'Invalid', isValid: false, optional: true }, + { label: 'With Text', text: 'T', optional: true }, + ]; + + const stepperOptions = { + selectOnFocus: false, + dataSource, + selectedIndex: 3, + }; + + await createWidget(page, 'dxStepper', stepperOptions, '#stepper'); + + const stepper = page.locator('#container'); + await stepper.getItem(3).element.click(); + + await page.keyboard.press('ArrowLeft'); + await testScreenshot(page, 'Completed invalid step focused.png', { element: '#stepper' }); + + await page.keyboard.press('ArrowLeft'); + await testScreenshot(page, 'Completed valid step focused.png', { element: '#stepper' }); + + await page.keyboard.press('ArrowLeft'); + await testScreenshot(page, 'Completed step focused.png', { element: '#stepper' }); + + await page.locator('body').click(); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/navigation/tabPanel/common.spec.ts b/e2e/testcafe-devextreme/playwright-tests/navigation/tabPanel/common.spec.ts new file mode 100644 index 000000000000..ca5bb1375e60 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/navigation/tabPanel/common.spec.ts @@ -0,0 +1,365 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, appendElementTo, setAttribute, insertStylesheetRulesToPage } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('TabPanel_common', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + const TABS_RIGHT_NAV_BUTTON_CLASS = 'dx-tabs-nav-button-right'; + const TABS_LEFT_NAV_BUTTON_CLASS = 'dx-tabs-nav-button-left'; + + ['with scrolling', 'without scrolling'].forEach((mode) => { + const testName = `TabPanel borders ${mode}`; + test(testName, async ({ page }) => { + + const dataSource: any[] = [ + { + title: 'John Heart', + text: 'John Heart', + }, { + title: 'Olivia Peyton', + text: 'Olivia Peyton', + }, { + title: 'Robert Reagan', + text: 'Robert Reagan', + }, { + title: 'Greta Sims', + text: 'Greta Sims', + }, { + title: 'Olivia Peyton', + text: 'Olivia Peyton', + }, + ]; + + const tabPanelOptions = { + dataSource, + itemTemplate: (_, __, itemElement) => { + ($('
').css('marginTop', '10px') as any) + .dxTabs({ + items: [ + { + title: 'John Heart', + text: 'John Heart', + }, { + title: 'Olivia Peyton', + text: 'Olivia Peyton', + }, { + title: 'Robert Reagan', + text: 'Robert Reagan', + }, { + title: 'Greta Sims', + text: 'Greta Sims', + }, { + title: 'Olivia Peyton', + text: 'Olivia Peyton', + }, + ], + width: 300, + showNavButtons: true, + }) + .appendTo(itemElement); + }, + height: 120, + width: mode === 'with scrolling' ? 300 : 900, + showNavButtons: true, + }; + + await createWidget(page, 'dxTabPanel', tabPanelOptions); + + + await testScreenshot(page, `${testName}.png`, { element: '#container' }); + + }); + }); + + test('TabPanel text-overflow with tabsPosition left', async ({ page }) => { + + const dataSource: any[] = [ + { icon: 'user', text: 'John Heart', title: 'John Heart' }, + { icon: 'user', text: 'Marina Elizabeth Thomas Grace Sophia', title: 'Mariya Elizabeth Thomas Grace Sophia' }, + { icon: 'user', text: 'Robert Reagan', title: 'Robert Reagan' }, + { icon: 'user', text: 'Greta Sims', title: 'Greta Sims' }, + ]; + + await createWidget(page, 'dxTabPanel', { + dataSource, + width: 600, + height: 250, + tabsPosition: 'left', + showNavButtons: true, + }); + + await testScreenshot(page, 'TabPanel text-overflow when tabsPosition is left.png', { element: '#container' }); + + await setAttribute(page, '.dx-tabs-wrapper', 'style', 'max-width: 130px;'); + + await testScreenshot(page, 'TabPanel text-overflow when tabs wrapper width is limited.png', { element: '#container' }); + + }); + + test('TabPanel focus borders after change selectedIndex in runtime', async ({ page }) => { + + const dataSource: any[] = [ + { + title: 'John Heart', + text: 'John Heart', + }, { + title: 'Olivia Peyton', + text: 'Olivia Peyton', + }, { + title: 'Robert Reagan', + text: 'Robert Reagan', + }, { + title: 'Greta Sims', + text: 'Greta Sims', + }, { + title: 'Olivia Peyton', + text: 'Olivia Peyton', + }, + ]; + + await createWidget(page, 'dxTabPanel', { + dataSource, + height: 120, + width: 300, + }); + + const tabPanel = page.locator('#container'); + await tabPanel.option('selectedIndex', 1); + + await testScreenshot(page, 'TabPanel focus borders.png', { element: '#container' }); + + }); + + test('TabPanel navigation buttons hover', async ({ page }) => { + + const dataSource: any[] = [ + { + title: 'John Heart', + text: 'John Heart', + }, { + title: 'Olivia Peyton', + text: 'Olivia Peyton', + }, { + title: 'Robert Reagan', + text: 'Robert Reagan', + }, { + title: 'Greta Sims', + text: 'Greta Sims', + }, { + title: 'Olivia Peyton', + text: 'Olivia Peyton', + }, + ]; + + const tabPanelOptions = { + dataSource, + height: 120, + width: 400, + showNavButtons: true, + selectedIndex: 2, + useInkRipple: false, + }; + + await createWidget(page, 'dxTabPanel', tabPanelOptions); + + await page.locator('body').click(); + + const rightNavButton = page.locator(`.${TABS_RIGHT_NAV_BUTTON_CLASS}`); + await page.click(rightNavButton) + .hover(rightNavButton); + + await testScreenshot(page, 'TabPanel right navigation button hovered.png', { element: '#container' }); + + const leftNavButton = page.locator(`.${TABS_LEFT_NAV_BUTTON_CLASS}`); + await leftNavButton.hover(); + + await testScreenshot(page, 'TabPanel left navigation button hovered.png', { element: '#container' }); + + }); + + ['top', 'right', 'bottom', 'left'].forEach((tabsPosition) => { + const testName = `TabPanel without focus,tabsPosition=${tabsPosition}`; + test(testName, async ({ page }) => { + + await appendElementTo(page, '#container', 'div', 'tabpanel'); + await appendElementTo(page, '#container', 'div', 'tabpanel-rtl'); + await setAttribute(page, '#container', 'style', 'display: flex; gap: 40px; flex-direction: column; width: fit-content;'); + + const dataSource: any[] = [ + { + title: 'John Heart', + text: 'John Heart', + }, { + title: 'Olivia Peyton', + text: 'Olivia Peyton', + }, { + title: 'Robert Reagan', + text: 'Robert Reagan', + }, { + title: 'Greta Sims', + text: 'Greta Sims', + }, { + title: 'Olivia Peyton', + text: 'Olivia Peyton', + }, + ]; + + const tabPanelOptions = { + dataSource, + height: 250, + width: 450, + tabsPosition, + useInkRipple: false, + }; + + await createWidget(page, 'dxTabPanel', tabPanelOptions, '#tabpanel'); + await createWidget(page, 'dxTabPanel', { ...tabPanelOptions, rtlEnabled: true }, '#tabpanel-rtl'); + + + await page.locator('body').click(); + + await testScreenshot(page, `${testName}.png`, { element: '#container' }); + + }); + }); + + test('TabPanel item focus when clicking on multiview', async ({ page }) => { + + const dataSource: any[] = [ + { + title: 'John Heart', + text: 'John Heart', + }, { + title: 'Olivia Peyton', + text: 'Olivia Peyton', + }, { + title: 'Robert Reagan', + text: 'Robert Reagan', + }, { + title: 'Greta Sims', + text: 'Greta Sims', + }, { + title: 'Olivia Peyton', + text: 'Olivia Peyton', + }, + ]; + + await createWidget(page, 'dxTabPanel', { + dataSource, + height: 250, + width: 450, + useInkRipple: false, + }); + + const tabPanel = page.locator('#container'); + await tabPanel.multiView.element.click(); + await testScreenshot(page, 'TabPanel item focus when clicking on multiview.png', { element: '#container' }); + + }); + + const positions = ['top', 'left', 'right', 'bottom']; + + positions.forEach((tabsPosition) => { + test(`TabPanel border appearance when it placed inside the content of TabPanel with=${tabsPosition}`, async ({ page }) => { + + await insertStylesheetRulesToPage(page, '.dx-tabpanel { margin: 10px }'); + + const dataSource: any[] = [ + { + title: 'John Heart', + text: 'John Heart', + }, { + title: 'Olivia Peyton', + text: 'Olivia Peyton', + }, + ]; + + await createWidget(page, 'dxTabPanel', { + dataSource, + height: 700, + width: 500, + tabsPosition, + selectedIndex: 1, + deferRendering: true, + itemTemplate: ClientFunction(() => { + const $container = $('
'); + + positions.forEach((position) => { + const $tabPanel = ($('
') as any).dxTabPanel({ + height: 120, + tabsPosition: position, + dataSource, + }); + + $container.append($tabPanel); + + $container.append($('
')); + }); + + return $container; + }, { + dependencies: { dataSource, positions }, + }), + }); + + + await testScreenshot(page, `Nested TabPanel borders appearance,tabsPos=${tabsPosition}.png`, { element: '#container' }); + + }); + }); + + test('TabPanel tabs min-width', async ({ page }) => { + + const dataSource: any[] = [ + { text: 'ok', title: 'ok' }, + { icon: 'comment' }, + { icon: 'user' }, + { icon: 'money' }, + { text: 'ok', title: 'ok', icon: 'search' }, + { text: 'alignright', title: 'alignright', icon: 'alignright' }, + ]; + + await createWidget(page, 'dxTabPanel', { + dataSource, + height: 250, + width: 900, + useInkRipple: false, + }); + + await testScreenshot(page, 'TabPanel tabs min-width.png', { element: '#container' }); + + }); + + ['left', 'right'].forEach((tabsPosition) => { + test('TabPanel should be shown correctly even if there is only one tab', async ({ page }) => { + + const dataSource: any[] = [ + { + title: 'John Heart', + text: 'John Heart', + }, + ]; + + await createWidget(page, 'dxTabPanel', { + dataSource, + height: 120, + width: 300, + tabsPosition, + }); + + + await testScreenshot(page, `TabPanel with single tab, tabPosition=${tabsPosition}.png`, { element: '#container' }); + + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/navigation/tabPanel/focus.spec.ts b/e2e/testcafe-devextreme/playwright-tests/navigation/tabPanel/focus.spec.ts new file mode 100644 index 000000000000..8498da849ec8 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/navigation/tabPanel/focus.spec.ts @@ -0,0 +1,266 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, appendElementTo } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('TabPanel', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + // T821726 + test('[{0: selected}, {1}] -> click to tabs[1] -> click to external button', async () => { + + await appendElementTo(page, '#container', 'div', 'tabPanel'); + + await createWidget(page, 'dxTabPanel', { + items: ['Item 1', 'Item 2'], + }, '#tabPanel'); + + const tabPanel = page.locator('#tabPanel'); + + await tabPanel.tabs.getItem(1).element.click() + .expect(tabPanel.isFocused).ok() + .expect(tabPanel.tabs.isFocused) + .ok() + .expect(tabPanel.tabs.getItem(0).isFocused) + .notOk() + .expect(tabPanel.tabs.getItem(1).isFocused) + .ok() + .expect(tabPanel.multiView.getItem(0).isFocused) + .notOk() + .expect(tabPanel.multiView.getItem(1).isFocused) + .ok(); + + await page.click(page.locator('body'), { offsetY: 400 }) + .expect(tabPanel.isFocused).notOk() + .expect(tabPanel.tabs.isFocused) + .notOk() + .expect(tabPanel.tabs.getItem(0).isFocused) + .notOk() + .expect(tabPanel.tabs.getItem(1).isFocused) + .notOk() + .expect(tabPanel.multiView.getItem(1).isFocused) + .notOk() + .expect(tabPanel.multiView.getItem(0).isFocused) + .notOk(); + + }); + + test('[{0: selected}] -> click to multiView -> click to external button', async () => { + + await appendElementTo(page, '#container', 'div', 'tabPanel'); + + await createWidget(page, 'dxTabPanel', { + items: ['Item 1'], + }, '#tabPanel'); + + const tabPanel = page.locator('#tabPanel'); + + await tabPanel.multiView.getItem(0).element.click() + .expect(tabPanel.isFocused).ok() + .expect(tabPanel.tabs.isFocused) + .ok() + .expect(tabPanel.tabs.getItem(0).isFocused) + .ok() + .expect(tabPanel.multiView.getItem(0).isFocused) + .ok(); + + await page.click(page.locator('body'), { offsetY: 400 }) + .expect(tabPanel.isFocused).notOk() + .expect(tabPanel.tabs.isFocused) + .notOk() + .expect(tabPanel.tabs.getItem(0).isFocused) + .notOk() + .expect(tabPanel.multiView.getItem(0).isFocused) + .notOk(); + + }); + + test('[{0: selected}, {1}, {2}] -> click to tabs[1] -> navigate to tabs[2] -> click to external button', async () => { + + await appendElementTo(page, '#container', 'div', 'tabPanel'); + + await createWidget(page, 'dxTabPanel', { + items: ['Item 1', 'Item 2', 'Item 3'], + }, '#tabPanel'); + + const tabPanel = page.locator('#tabPanel'); + + await tabPanel.tabs.getItem(1).element.click() + .expect(tabPanel.isFocused).ok() + .expect(tabPanel.tabs.isFocused) + .ok() + .expect(tabPanel.tabs.getItem(0).isFocused) + .notOk() + .expect(tabPanel.tabs.getItem(1).isFocused) + .ok() + .expect(tabPanel.tabs.getItem(2).isFocused) + .notOk() + .expect(tabPanel.multiView.getItem(0).isFocused) + .notOk() + .expect(tabPanel.multiView.getItem(1).isFocused) + .ok() + .expect(tabPanel.multiView.getItem(2).isFocused) + .notOk(); + + await page.keyboard.press('ArrowRight') + .expect(tabPanel.isFocused).ok() + .expect(tabPanel.tabs.isFocused) + .ok() + .expect(tabPanel.tabs.getItem(0).isFocused) + .notOk() + .expect(tabPanel.tabs.getItem(1).isFocused) + .notOk() + .expect(tabPanel.tabs.getItem(2).isFocused) + .ok() + .expect(tabPanel.multiView.getItem(0).isFocused) + .notOk() + .expect(tabPanel.multiView.getItem(1).isFocused) + .notOk() + .expect(tabPanel.multiView.getItem(2).isFocused) + .ok(); + + await page.click(page.locator('body'), { offsetY: 400 }) + .expect(tabPanel.isFocused).notOk() + .expect(tabPanel.tabs.isFocused) + .notOk() + .expect(tabPanel.tabs.getItem(0).isFocused) + .notOk() + .expect(tabPanel.tabs.getItem(1).isFocused) + .notOk() + .expect(tabPanel.tabs.getItem(2).isFocused) + .notOk() + .expect(tabPanel.multiView.getItem(0).isFocused) + .notOk() + .expect(tabPanel.multiView.getItem(1).isFocused) + .notOk() + .expect(tabPanel.multiView.getItem(2).isFocused) + .notOk(); + + }); + + test('[{0: selected}, {1}] -> click to multiView -> navigate to tabs[1] -> click to external button', async () => { + + await appendElementTo(page, '#container', 'div', 'tabPanel'); + + await createWidget(page, 'dxTabPanel', { + items: ['Item 1', 'Item 2'], + }, '#tabPanel'); + + const tabPanel = page.locator('#tabPanel'); + + await tabPanel.multiView.getItem(0).element.click() + .expect(tabPanel.isFocused).ok() + .expect(tabPanel.tabs.isFocused) + .ok() + .expect(tabPanel.tabs.getItem(0).isFocused) + .ok() + .expect(tabPanel.tabs.getItem(1).isFocused) + .notOk() + .expect(tabPanel.multiView.getItem(0).isFocused) + .ok() + .expect(tabPanel.multiView.getItem(1).isFocused) + .notOk(); + + await page.keyboard.press('ArrowRight') + .expect(tabPanel.isFocused).ok() + .expect(tabPanel.tabs.isFocused) + .ok() + .expect(tabPanel.tabs.getItem(0).isFocused) + .notOk() + .expect(tabPanel.tabs.getItem(1).isFocused) + .ok() + .expect(tabPanel.multiView.getItem(0).isFocused) + .notOk() + .expect(tabPanel.multiView.getItem(1).isFocused) + .ok(); + + await page.click(page.locator('body'), { offsetY: 400 }) + .expect(tabPanel.isFocused).notOk() + .expect(tabPanel.tabs.isFocused) + .notOk() + .expect(tabPanel.tabs.getItem(0).isFocused) + .notOk() + .expect(tabPanel.tabs.getItem(1).isFocused) + .notOk() + .expect(tabPanel.multiView.getItem(0).isFocused) + .notOk() + .expect(tabPanel.multiView.getItem(1).isFocused) + .notOk(); + + }); + + test('[{0: selected}] -> click to multiView -> press "tab" -> press "tab"', async () => { + await createWidget(page, 'dxTabPanel', { + items: ['Item 1'], + }); + + const tabPanel = page.locator('#container'); + + await tabPanel.multiView.getItem(0).element.click() + .expect(tabPanel.isFocused).ok() + .expect(tabPanel.tabs.isFocused) + .ok() + .expect(tabPanel.tabs.getItem(0).isFocused) + .ok() + .expect(tabPanel.multiView.getItem(0).isFocused) + .ok(); + + await page.keyboard.press('Tab') + .expect(tabPanel.isFocused).ok() + .expect(tabPanel.tabs.isFocused) + .ok() + .expect(tabPanel.tabs.getItem(0).isFocused) + .ok() + .expect(tabPanel.multiView.getItem(0).isFocused) + .notOk(); + + await page.keyboard.press('Tab') + .expect(tabPanel.isFocused).notOk() + .expect(tabPanel.tabs.isFocused) + .notOk() + .expect(tabPanel.tabs.getItem(0).isFocused) + .notOk() + .expect(tabPanel.multiView.getItem(0).isFocused) + .notOk(); + + }); + + test('[{0: selected}] -> focusin by press "tab" -> press "tab"', async () => { + + await appendElementTo(page, '#container', 'div', 'tabPanel'); + + await createWidget(page, 'dxTabPanel', { + items: ['Item 1'], + }, '#tabPanel'); + + const tabPanel = page.locator('#tabPanel'); + + await page.click(page.locator('body'), { offsetY: 400 }) + .pressKey('tab') + .expect(tabPanel.isFocused).ok() + .expect(tabPanel.tabs.isFocused) + .ok() + .expect(tabPanel.tabs.getItem(0).isFocused) + .ok() + .expect(tabPanel.multiView.getItem(0).isFocused) + .ok(); + + await page.keyboard.press('Tab') + .expect(tabPanel.isFocused).notOk() + .expect(tabPanel.tabs.isFocused) + .notOk() + .expect(tabPanel.tabs.getItem(0).isFocused) + .notOk() + .expect(tabPanel.multiView.getItem(0).isFocused) + .notOk(); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/navigation/tabs/common.spec.ts b/e2e/testcafe-devextreme/playwright-tests/navigation/tabs/common.spec.ts new file mode 100644 index 000000000000..2ef152c7c4c0 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/navigation/tabs/common.spec.ts @@ -0,0 +1,201 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, appendElementTo, setAttribute } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('Tabs_common', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + const TAB_CLASS = 'dx-tab'; + + test('Tabs background color', async ({ page }) => { + + await appendElementTo(page, '#container', 'div', 'tabs'); + await setAttribute(page, '#container', 'style', 'width: 400px; background: #fff000 !important;'); + + const dataSource: any[] = [ + { text: 'John Heart' }, + { text: 'Marina Thomas' }, + { text: 'Robert Reagan' }, + { text: 'Greta Sims' }, + ]; + + await createWidget(page, 'dxTabs', { dataSource }, '#tabs'); + + await testScreenshot(page, 'Tabs background color.png', { element: '#container' }); + + }); + + test('Tabs text-overflow with vertical orientation', async ({ page }) => { + + await setAttribute(page, '#container', 'style', 'display: flex; gap: 40px; width: fit-content;'); + + const iconPositions = ['start', 'end', 'top']; + const dataSource: any[] = [ + { icon: 'user', text: 'John Heart' }, + { icon: 'user', text: 'Marina Elizabeth Thomas Grace Sophia Alexander Benjamin Olivia Nicholas Victoria Michael Emily' }, + { icon: 'user', text: 'Robert Reagan' }, + { icon: 'user', text: 'Greta Sims' }, + ]; + + await Promise.all(iconPositions.map((iconPosition) => appendElementTo(page, '#container', 'div', `tabs-${iconPosition}`))); + await Promise.all(iconPositions.map((iconPosition) => createWidget(page, 'dxTabs', { + dataSource, + iconPosition, + width: 130, + orientation: 'vertical', + }, `#tabs-${iconPosition}`))); + + await testScreenshot(page, 'Tabs text-overflow.png', { element: '#container' }); + + }); + + [true, false].forEach((rtlEnabled) => { + test('Tabs icon position', async ({ page }) => { + + await setAttribute(page, '#container', 'style', 'display: flex; flex-direction: column; gap: 20px; width: 800px'); + + const iconPositions = ['start', 'end', 'top', 'bottom']; + const dataSource: any[] = [ + { text: 'user', badge: '1' }, + { text: 'comment', icon: 'comment', badge: 'text' }, + { icon: 'user' }, + { icon: 'money' }, + ]; + + await Promise.all(iconPositions.map((iconPosition) => appendElementTo(page, '#container', 'div', `tabs-${iconPosition}`))); + await Promise.all(iconPositions.map((iconPosition) => createWidget(page, 'dxTabs', { + dataSource, + iconPosition, + rtlEnabled, + }, `#tabs-${iconPosition}`))); + + + await testScreenshot(page, `Tabs icon position,rtl=${rtlEnabled}.png`, { element: '#container' }); + + }); + }); + + test('Tabs with width: auto in flex container', async ({ page }) => { + + await appendElementTo(page, '#container', 'div', 'tabs'); + await setAttribute(page, '#container', 'style', 'display: flex; width: 800px;'); + + const dataSource: any[] = [ + { text: 'ok' }, + { icon: 'comment' }, + { icon: 'user' }, + { icon: 'money' }, + { text: 'ok', icon: 'search' }, + { text: 'alignright', icon: 'alignright' }, + ]; + + await createWidget(page, 'dxTabs', { dataSource, width: 'auto' }, '#tabs'); + + await testScreenshot(page, 'Tabs with width auto.png', { element: '#tabs' }); + + }); + + ['primary', 'secondary'].forEach((stylingMode) => { + ['horizontal', 'vertical'].forEach((orientation) => { + test('Tabs item selected states', async ({ page }) => { + + await appendElementTo(page, '#container', 'div', 'tabs'); + await appendElementTo(page, '#container', 'div', 'tabs-rtl'); + await setAttribute(page, '#container', 'style', `display: flex; gap: 40px; flex-direction: ${orientation === 'horizontal' ? 'column' : 'row'}; width: fit-content;`); + + const dataSource: any[] = [ + { text: 'John Heart' }, + { text: 'Marina Thomas', disabled: true }, + { text: 'Robert Reagan' }, + { text: 'Greta Sims' }, + { text: 'Olivia Peyton' }, + { text: 'Ed Holmes' }, + { text: 'Wally Hobbs' }, + { text: 'Brad Jameson' }, + ]; + + const tabsOptions = { + dataSource, + orientation, + stylingMode, + width: orientation === 'horizontal' ? 450 : 'auto', + height: orientation === 'horizontal' ? 'auto' : 250, + selectedItem: dataSource[2], + showNavButtons: true, + }; + + await createWidget(page, 'dxTabs', tabsOptions, '#tabs'); + await createWidget(page, 'dxTabs', { ...tabsOptions, rtlEnabled: true }, '#tabs-rtl'); + + + await testScreenshot(page, `Tabs item selected, orientation=${orientation}, stylingMode=${stylingMode}.png`, { element: '#container' }); + + }); + }); + }); + + test('Tabs item states', async ({ page }) => { + + await appendElementTo(page, '#container', 'div', 'tabs'); + + const dataSource: any[] = [ + { text: 'John Heart' }, + { text: 'Marina Thomas', disabled: true }, + { text: 'Robert Reagan' }, + { text: 'Greta Sims' }, + { text: 'Olivia Peyton' }, + { text: 'Ed Holmes' }, + { text: 'Wally Hobbs' }, + { text: 'Brad Jameson' }, + ]; + + const tabsOptions = { + dataSource, + selectOnFocus: false, + showNavButtons: true, + width: 600, + useInkRipple: false, + }; + + await createWidget(page, 'dxTabs', tabsOptions, '#tabs'); + + await testScreenshot(page, 'Tabs without focus.png', { element: '#tabs' }); + + await page.keyboard.press('Tab'); + await testScreenshot(page, 'Tabs item focused.png', { element: '#tabs' }); + + await page.keyboard.press('ArrowRight'); + await testScreenshot(page, 'Tabs disabled item focused.png', { element: '#tabs' }); + + const thirdItem = page.locator(`.${TAB_CLASS}:nth-child(3)`); + const fourthItem = page.locator(`.${TAB_CLASS}:nth-child(4)`); + + await page.keyboard.press('ArrowRight') + .dispatchEvent(thirdItem, 'mousedown'); + await testScreenshot(page, 'Tabs item active.png', { element: '#tabs' }); + await thirdItem.dispatchEvent('mouseup'); + + await page.click(thirdItem) + .hover(fourthItem); + await testScreenshot(page, 'Tabs item hovered.png', { element: '#tabs' }); + + await page.locator('body').click(); + + await thirdItem.hover(); + await testScreenshot(page, 'Tabs selected item hovered.png', { element: '#tabs' }); + + await thirdItem.dispatchEvent('mousedown'); + await testScreenshot(page, 'Tabs selected item active.png', { element: '#tabs' }); + await thirdItem.dispatchEvent('mouseup'); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/navigation/toolbar/common.spec.ts b/e2e/testcafe-devextreme/playwright-tests/navigation/toolbar/common.spec.ts new file mode 100644 index 000000000000..b153ac6a8577 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/navigation/toolbar/common.spec.ts @@ -0,0 +1,111 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, appendElementTo, setAttribute, setStyleAttribute } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('Toolbar_common', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + ['never', 'always'].forEach((locateInMenu: LocateInMenuMode) => { + [true, false].forEach((rtlEnabled) => { + test(`Default nested widgets render,items[].locateInMenu=${locateInMenu},rtl=${rtlEnabled}`, async ({ page }) => { + + await appendElementTo(page, '#container', 'div', 'toolbar'); + await setAttribute(page, '#container', 'style', 'width: 1184px;'); + + const supportedWidgets: ToolbarItemComponent[] = ['dxAutocomplete', 'dxButton', 'dxCheckBox', 'dxDateBox', 'dxMenu', 'dxSelectBox', 'dxTabs', 'dxTextBox', 'dxButtonGroup', 'dxDropDownButton']; + const toolbarItems: any[] = supportedWidgets.map((widgetName) => ({ + location: 'before', + locateInMenu, + widget: widgetName, + options: { + value: new Date(2021, 9, 17), + stylingMode: 'contained', + text: `${widgetName}`, + icon: 'refresh', + items: [{ text: `${widgetName}`, icon: 'export' }], + iconPosition: widgetName === 'dxTabs' ? 'start' : undefined, + width: locateInMenu === 'never' ? 115 : undefined, + }, + })); + + toolbarItems.push({ + location: 'before', + locateInMenu, + text: 'Some text', + }); + + await createWidget(page, 'dxToolbar', { + items: toolbarItems, + rtlEnabled, + width: locateInMenu === 'auto' ? 50 : '100%', + }, '#toolbar'); + + + const toolbar = page.locator('#toolbar'); + let targetContainer = page.locator('#container'); + + const overflowMenu = toolbar.getOverflowMenu(); + + if (locateInMenu !== 'never') { + await overflowMenu.click(); + + targetContainer = overflowMenu.getPopup().getContent(); + } + + await setStyleAttribute(page, targetContainer, 'background-color: gold;'); + + await testScreenshot(page, `Toolbar widgets render${rtlEnabled ? ' rtl=true' : ''},items[]locateInMenu=${locateInMenu}.png`, { + element: targetContainer, + }); + + }); + }); + }); + + [true, false].forEach((rtlEnabled) => { + test(`Default nested widgets render, rtlEnabled: ${rtlEnabled}`, async ({ page }) => { + + await setAttribute(page, '#container', 'style', 'box-sizing: border-box; width: 400px; height: 400px; padding: 8px;'); + await appendElementTo(page, '#container', 'div', 'toolbar'); + + const supportedWidgets: ToolbarItemComponent[] = ['dxAutocomplete', 'dxButton', 'dxCheckBox', 'dxDateBox', 'dxMenu', 'dxSelectBox', 'dxTabs', 'dxTextBox', 'dxButtonGroup', 'dxDropDownButton']; + const toolbarItems: any[] = supportedWidgets.map((widgetName) => ({ + location: 'before', + widget: widgetName, + options: { + value: new Date(2021, 9, 17), + stylingMode: 'contained', + text: 1, + items: [{ text: 1 }, { text: 2 }], + showClearButton: true, + }, + })); + + toolbarItems.push({ + location: 'after', + text: 'Lorem Ipsum is simply dummy text of the printing and typesetting industry', + }); + + await createWidget(page, 'dxToolbar', { + multiline: true, + items: toolbarItems, + rtlEnabled, + }, '#toolbar'); + + + await testScreenshot(page, `Toolbar nested widgets render in multiline rtl=${rtlEnabled}.png`, { + element: '#toolbar', + }); + + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/navigation/toolbar/overflowMenu.spec.ts b/e2e/testcafe-devextreme/playwright-tests/navigation/toolbar/overflowMenu.spec.ts new file mode 100644 index 000000000000..77187e097117 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/navigation/toolbar/overflowMenu.spec.ts @@ -0,0 +1,350 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, appendElementTo, setClassAttribute } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('Toolbar_OverflowMenu', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + const BUTTON_CLASS = 'dx-button'; + const ACTIVE_STATE_CLASS = 'dx-state-active'; + const HOVER_STATE_CLASS = 'dx-state-hover'; + const FOCUSED_STATE_CLASS = 'dx-state-focused'; + + const stylingModes = ['text', 'outlined', 'contained']; + const buttonTypes = ['danger', 'default', 'normal', 'success']; + const stateClasses = [FOCUSED_STATE_CLASS, HOVER_STATE_CLASS, ACTIVE_STATE_CLASS]; + + test('Drop down button should lost hover and active state', async ({ page }) => { + + await appendElementTo(page, '#container', 'div', 'toolbar'); + await appendElementTo(page, '#container', 'button', 'button', { + width: '50px', height: '50px', backgroundColor: 'steelblue', marginTop: '400px', + }); + + await createWidget(page, 'dxToolbar', { + items: [ + { text: 'item1', locateInMenu: 'always' }, + { text: 'item2', locateInMenu: 'always' }, + { text: 'item3', locateInMenu: 'always' }], + }, '#toolbar'); + + const toolbar = page.locator('#toolbar'); + const dropDownMenu = toolbar.getOverflowMenu(); + + await page.dispatchEvent(dropDownMenu.element, 'mousedown') + .expect(dropDownMenu.isActive) + .ok() + .expect(dropDownMenu.isFocused) + .notOk() + .expect(dropDownMenu.isHovered) + .notOk() + .dispatchEvent(dropDownMenu.element, 'mouseup') + .expect(dropDownMenu.isActive) + .notOk() + .expect(dropDownMenu.isFocused) + .notOk() + .expect(dropDownMenu.isHovered) + .notOk(); + + await dropDownMenu.click() + .expect(dropDownMenu.isActive) + .notOk() + .expect(dropDownMenu.isHovered) + .ok(); + + await page.hover('#button') + .expect(dropDownMenu.isHovered) + .notOk() + .expect(dropDownMenu.isFocused) + .notOk() + .expect(dropDownMenu.isActive) + .notOk(); + + await page.locator('#button').click() + .expect(dropDownMenu.isHovered) + .notOk() + .expect(dropDownMenu.isFocused) + .notOk() + .expect(dropDownMenu.isActive) + .notOk(); + + }); + + test('ButtonGroup item should not have hover and active state', async ({ page }) => { + await createWidget(page, 'dxToolbar', { + items: [ + { + location: 'before', + locateInMenu: 'always', + widget: 'dxButtonGroup', + options: { + items: [{ + icon: 'alignleft', + text: 'Align left', + }, + { + icon: 'aligncenter', + text: 'Center', + }], + selectionMode: 'single', + }, + }, + ], + }); + + const toolbar = page.locator('#container'); + const overflowMenu = toolbar.getOverflowMenu(); + + await overflowMenu.click(); + + const list = overflowMenu.getList(); + const items = list.getItems(); + + expect(items.count).toBe(1); + + const item = items.nth(0); + const button = item.find(`.${BUTTON_CLASS}`); + + await button.hover() + .dispatchEvent(button, 'mousedown') + .expect(item.hasClass(ACTIVE_STATE_CLASS)) + .notOk() + .expect(item.hasClass(FOCUSED_STATE_CLASS)) + .notOk() + .expect(item.hasClass(HOVER_STATE_CLASS)) + .notOk() + .expect(button.hasClass(ACTIVE_STATE_CLASS)) + .notOk() + .expect(button.hasClass(FOCUSED_STATE_CLASS)) + .ok() + .expect(button.hasClass(HOVER_STATE_CLASS)) + .ok(); + + await button.dispatchEvent('mouseup') + .expect(item.hasClass(ACTIVE_STATE_CLASS)) + .notOk() + .expect(item.hasClass(FOCUSED_STATE_CLASS)) + .notOk() + .expect(item.hasClass(HOVER_STATE_CLASS)) + .notOk() + .expect(button.hasClass(ACTIVE_STATE_CLASS)) + .notOk() + .expect(button.hasClass(FOCUSED_STATE_CLASS)) + .ok() + .expect(button.hasClass(HOVER_STATE_CLASS)) + .ok(); + + await page.expect(overflowMenu.option('opened')) + .eql(true) + .click(button) + .expect(overflowMenu.option('opened')) + .eql(false); + + }); + + test('Click on overflow button should prevent popup\'s hideOnOutsideClick', async ({ page }) => { + await createWidget(page, 'dxToolbar', { + items: [ + { text: 'item1', locateInMenu: 'always' }, + { text: 'item2', locateInMenu: 'always' }, + { text: 'item3', locateInMenu: 'always' }, + ], + }); + + const toolbar = page.locator('#container'); + const menu = toolbar.getOverflowMenu(); + + await menu.click() + .expect(menu.getPopup().getWrapper().count) + .eql(1); + + await menu.click() + .expect(menu.getPopup().getWrapper().count) + .eql(0); + + }); + + test('Toolbar buttons in menu appearance', async ({ page }) => { + + const items: any[] = stylingModes.flatMap((stylingMode) => buttonTypes.map((type) => ({ + widget: 'dxButton', + locateInMenu: 'always', + options: { + stylingMode, + text: `Button ${stylingMode}`, + type, + icon: 'home', + }, + }))); + + await createWidget(page, 'dxToolbar', { + width: 50, + multiline: false, + items, + }); + + const toolbar = page.locator('#container'); + + await toolbar.getOverflowMenu().element.click(); + + const targetContainer = toolbar.getOverflowMenu().getPopup().getContent(); + + await testScreenshot(page, 'Toolbar buttons in menu.png', { element: targetContainer }); + + const items = await toolbar.getOverflowMenu().getList().getItemsAsArray(); + + for (const state of stateClasses) { + await Promise.all(items.map((item) => setClassAttribute(page, item, state))); + + const stateName = state.replace('dx-state-', ''); + await testScreenshot(page, `Toolbar buttons in menu ${stateName}.png`, { element: targetContainer }); + + await Promise.all(items.map((item) => removeClassAttribute(item, state))); + } + + }); + + test('Toolbar buttons as custom template appearance', async ({ page }) => { + + const items: any[] = stylingModes.flatMap((stylingMode) => buttonTypes.map((type) => { + const template = async () => page.evaluate(() => ($('
') as any).dxButton({ + stylingMode, + text: `Button ${stylingMode}`, + type, + icon: 'home', + }), { dependencies: { stylingMode, type } }); + + return { + locateInMenu: 'always', + template, + }; + })); + + await createWidget(page, 'dxToolbar', { + width: 50, + multiline: false, + items, + }); + + const toolbar = page.locator('#container'); + + await toolbar.getOverflowMenu().element.click(); + + const targetContainer = toolbar.getOverflowMenu().getPopup().getContent(); + + await testScreenshot(page, 'Toolbar buttons as custom template in menu.png', { element: targetContainer }); + + const items = await toolbar.getOverflowMenu().getList().getItemsAsArray(); + + for (const state of stateClasses) { + await Promise.all(items.map((item) => setClassAttribute(page, item, state))); + + const stateName = state.replace('dx-state-', ''); + await testScreenshot(page, `Toolbar buttons as custom template in menu ${stateName}.png`, { element: targetContainer }); + + await Promise.all(items.map((item) => removeClassAttribute(item, state))); + } + + }); + + test('Toolbar button group appearance', async ({ page }) => { + + const items: any[] = stylingModes.map((stylingMode) => { + const buttons: ButtonGroupItem[] = buttonTypes.map((type) => ({ + text: `ButtonGroup ${stylingMode}`, + type, + icon: 'home', + })); + + return { + widget: 'dxButtonGroup', + locateInMenu: 'always', + options: { + stylingMode, + items: buttons, + }, + }; + }); + + await createWidget(page, 'dxToolbar', { + width: 50, + items, + }); + + const toolbar = page.locator('#container'); + + await toolbar.getOverflowMenu().element.click(); + + const targetContainer = toolbar.getOverflowMenu().getPopup().getContent(); + + await testScreenshot(page, 'Toolbar buttonGroup in menu.png', { element: targetContainer }); + + const items = await toolbar.getOverflowMenu().getList().getItemsAsArray(); + + for (const state of stateClasses) { + await Promise.all(items.map((item) => setClassAttribute(page, item, state))); + + const stateName = state.replace('dx-state-', ''); + await testScreenshot(page, `Toolbar buttonGroup in menu ${stateName}.png`, { element: targetContainer }); + + await Promise.all(items.map((item) => removeClassAttribute(item, state))); + } + + }); + + test('Toolbar button group as custom template appearance', async ({ page }) => { + + const items: any[] = stylingModes.map((stylingMode) => { + const buttons: ButtonGroupItem[] = buttonTypes.map((type) => ({ + text: `${stylingMode[0].toUpperCase()}${stylingMode.slice(1)}`, + type, + icon: 'home', + })); + + const template = async () => page.evaluate(() => ($('
') as any).dxButtonGroup({ + width: 490, + stylingMode, + items: buttons, + }), { dependencies: { stylingMode, buttons } }); + + return { + locateInMenu: 'always', + template, + }; + }); + + await createWidget(page, 'dxToolbar', { + width: 50, + items, + }); + + const toolbar = page.locator('#container'); + + await toolbar.getOverflowMenu().element.click(); + + const targetContainer = toolbar.getOverflowMenu().getPopup().getContent(); + + await testScreenshot(page, 'Toolbar buttonGroup as custom template in menu.png', { element: targetContainer }); + + const items = await toolbar.getOverflowMenu().getList().getItemsAsArray(); + + for (const state of stateClasses) { + await Promise.all(items.map((item) => setClassAttribute(page, item, state))); + + const stateName = state.replace('dx-state-', ''); + await testScreenshot(page, `Toolbar buttonGroup as custom template in menu ${stateName}.png`, { element: targetContainer }); + + await Promise.all(items.map((item) => removeClassAttribute(item, state))); + } + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/navigation/toolbar/overflowMenuPopup.spec.ts b/e2e/testcafe-devextreme/playwright-tests/navigation/toolbar/overflowMenuPopup.spec.ts new file mode 100644 index 000000000000..d8ca2844c95c --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/navigation/toolbar/overflowMenuPopup.spec.ts @@ -0,0 +1,95 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, setAttribute } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('Toolbar_OverflowMenu_Popup', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + const generateItems = (count) => { + const items: { text: string; locateInMenu: string }[] = []; + + for (let i = 0; i <= count; i += 1) { + items.push({ text: `item${i}`, locateInMenu: 'always' }); + } + + return items; + }; + + test('Popup automatically update its height on window resize', async ({ page }) => { + await createWidget(page, 'dxToolbar', { + items: generateItems(40), + }); + + const toolbar = page.locator('#container'); + const overflowMenu = toolbar.getOverflowMenu(); + + await overflowMenu.click(); + + await testScreenshot(page, 'Toolbar menu popup before window resize.png'); + + await resizeWindow(300, 300); + + await testScreenshot(page, 'Toolbar menu popup after window resize.png'); + + }); + + test('Popup should be position correctly with the window border collision', async ({ page }) => { + await createWidget(page, 'dxToolbar', { + items: generateItems(40), + width: 50, + }); + + const toolbar = page.locator('#container'); + const overflowMenu = toolbar.getOverflowMenu(); + + await overflowMenu.click(); + + await testScreenshot(page, 'Toolbar menu popup collision with window border.png'); + + }); + + [true, false].forEach((rtlEnabled) => { + test(`Popup under container should be limited in height,rtlEnabled=${rtlEnabled}`, async ({ page }) => { + await createWidget(page, 'dxToolbar', { + items: generateItems(40), + rtlEnabled, + }); + + const toolbar = page.locator('#container'); + const overflowMenu = toolbar.getOverflowMenu(); + + await overflowMenu.click(); + + await testScreenshot(page, `Toolbar menu popup under container rtl=${rtlEnabled}.png`); + + }); + + test(`Popup above container should be limited in height,rtlEnabled=${rtlEnabled}`, async ({ page }) => { + + await setAttribute(page, '#container', 'style', 'margin-top: 200px'); + + await createWidget(page, 'dxToolbar', { + items: generateItems(40), + rtlEnabled, + }); + + + const toolbar = page.locator('#container'); + const overflowMenu = toolbar.getOverflowMenu(); + + await overflowMenu.click(); + + await testScreenshot(page, `Toolbar menu popup above container rtl=${rtlEnabled}.png`); + + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/navigation/treeView/common.spec.ts b/e2e/testcafe-devextreme/playwright-tests/navigation/treeView/common.spec.ts new file mode 100644 index 000000000000..9abbf9444398 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/navigation/treeView/common.spec.ts @@ -0,0 +1,264 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, setAttribute } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('TreeView', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('Treeview search, selectAll item and nodes should be focused in DOM elements order when navigating with tab and shift+tab', async ({ page }) => { + await createWidget(page, 'dxTreeView', { + searchEnabled: true, + showCheckBoxesMode: 'selectAll', + items: employees, + }); + + const treeView = page.locator('#container'); + const selectAllItemCheckBox = treeView.getSelectAllCheckBox(); + const searchTextBox = treeView.getSearchTextBox(); + const node = treeView.getNode(0); + + await page.keyboard.press('Tab') + .expect(searchTextBox.isFocused) + .ok() + .pressKey('tab') + .expect(selectAllItemCheckBox.isFocused) + .ok() + .pressKey('tab') + .expect(node.isFocused) + .ok() + .pressKey('shift+tab') + .expect(selectAllItemCheckBox.isFocused) + .ok() + .pressKey('shift+tab') + .expect(searchTextBox.isFocused) + .ok(); + + }); + + test('Treeview items focus order should be correct when changing showCheckBoxesMode from normal to selectAll at runtime', async ({ page }) => { + await createWidget(page, 'dxTreeView', { + showCheckBoxesMode: 'normal', + items: employees, + }); + + const treeView = page.locator('#container'); + const node = treeView.getNode(0); + + await treeView.option('showCheckBoxesMode', 'selectAll'); + + const selectAllItemCheckBox = treeView.getSelectAllCheckBox(); + + await page.keyboard.press('Tab') + .expect(selectAllItemCheckBox.isFocused) + .ok() + .pressKey('tab') + .expect(node.isFocused) + .ok(); + + }); + + test('Treeview items focus order should be correct when changing showCheckBoxesMode from none to selectAll at runtime', async ({ page }) => { + await createWidget(page, 'dxTreeView', { + showCheckBoxesMode: 'none', + items: employees, + }); + + const treeView = page.locator('#container'); + const node = treeView.getNode(0); + + await treeView.option('showCheckBoxesMode', 'selectAll'); + + const selectAllItemCheckBox = treeView.getSelectAllCheckBox(); + + await page.keyboard.press('Tab') + .expect(selectAllItemCheckBox.isFocused) + .ok() + .pressKey('tab') + .expect(node.isFocused) + .ok(); + + }); + + test('Treeview items focus order should be correct when changing showCheckBoxesMode at runtime with search enabled', async ({ page }) => { + await createWidget(page, 'dxTreeView', { + searchEnabled: true, + showCheckBoxesMode: 'normal', + items: employees, + }); + + const treeView = page.locator('#container'); + const searchBar = treeView.getSearchTextBox(); + const node = treeView.getNode(0); + + await treeView.option('showCheckBoxesMode', 'selectAll'); + + const selectAllItemCheckBox = treeView.getSelectAllCheckBox(); + + await page.keyboard.press('Tab') + .expect(searchBar.isFocused) + .ok() + .pressKey('tab') + .expect(selectAllItemCheckBox.isFocused) + .ok() + .pressKey('tab') + .expect(node.isFocused) + .ok(); + + }); + + test('Treeview items focus order should be correct when changing search panel mode at runtime', async ({ page }) => { + await createWidget(page, 'dxTreeView', { + searchEnabled: false, + showCheckBoxesMode: 'selectAll', + items: employees, + }); + + const treeView = page.locator('#container'); + const selectAllItemCheckBox = treeView.getSelectAllCheckBox(); + const node = treeView.getNode(0); + + await treeView.option('searchEnabled', 'true'); + + const searchBar = treeView.getSearchTextBox(); + + await page.keyboard.press('Tab') + .expect(searchBar.isFocused) + .ok() + .pressKey('tab') + .expect(selectAllItemCheckBox.isFocused) + .ok() + .pressKey('tab') + .expect(node.isFocused) + .ok(); + + }); + + test('Treeview node container should be focused after selectAll item when navigating with tab when no search bar is present', async ({ page }) => { + await createWidget(page, 'dxTreeView', { + showCheckBoxesMode: 'selectAll', + items: employees, + }); + + const treeView = page.locator('#container'); + const selectAllItemCheckBox = treeView.getSelectAllCheckBox(); + const node = treeView.getNode(0); + + await page.keyboard.press('Tab') + .expect(selectAllItemCheckBox.isFocused) + .ok() + .pressKey('tab') + .expect(node.isFocused) + .ok(); + + }); + + test('TreeView: height should be calculated correctly when searchEnabled is true (T1138605)', async ({ page }) => { + await createWidget(page, 'dxTreeView', { + width: 300, + height: 350, + searchEnabled: true, + items: employees, + itemTemplate(item) { + return `
${item.fullName} (${item.position})
`; + }, + }); + + const treeView = page.locator('#container'); + const scrollable = treeView.getScrollable(); + + await scrollable.scrollTo({ top: 1000 }); + + await testScreenshot(page, 'TreeView scrollable has correct height.png', { element: '#container' }); + + }); + + [true, false].forEach((rtlEnabled) => { + ['selectAll', 'normal', 'none'].forEach((showCheckBoxesMode) => { + const testName = `TreeView selection showCheckBoxesMode=${showCheckBoxesMode},rtl=${rtlEnabled}`; + test(testName, async ({ page }) => { + + await setAttribute(page, '#container', 'class', 'dx-theme-generic-typography'); + + await createWidget(page, 'dxTreeView', { + items: employees, + width: 300, + selectionMode: 'multiple', + showCheckBoxesMode, + rtlEnabled, + itemTemplate(item) { + return `
${item.fullName} (${item.position})
`; + }, + }); + + + await testScreenshot(page, `${testName}.png`, { element: '#container' }); + + }); + }); + }); + + ['normal', 'none'].forEach((showCheckBoxesMode) => { + const testName = `Treeview with custom icons showCheckBoxesMode=${showCheckBoxesMode}`; + test(testName, async ({ page }) => { + await createWidget(page, 'dxTreeView', { + items: employees, + width: 300, + showCheckBoxesMode, + expandIcon: 'add', + collapseIcon: 'minus', + itemTemplate(item) { + return `
${item.fullName} (${item.position})
`; + }, + }); + + await click(page.locator('.dx-treeview-item').nth(1)); + + await testScreenshot(page, `${testName}.png`, { element: '#container' }); + + }); + }); + + test('TreeView checkBox focus styles', async ({ page }) => { + await createWidget(page, 'dxTreeView', { + items: [{ + ID: '1', + text: 'Item 1', + expanded: true, + items: [ + { + ID: '1_1', + text: 'Item 1_1', + selected: true, + }, { + ID: '1_2', + text: 'Item 1_2', + }, + ], + }], + width: 300, + showCheckBoxesMode: 'normal', + }); + + await page.keyboard.press('Tab'); + + await testScreenshot(page, 'Treeview indeterminate CheckBox focus.png', { element: '#container' }); + + await page.keyboard.press('ArrowDown'); + + await testScreenshot(page, 'Treeview checked CheckBox focus.png', { element: '#container' }); + + await page.keyboard.press('ArrowDown'); + + await testScreenshot(page, 'Treeview unchecked CheckBox focus.png', { element: '#container' }); + + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/a11y/contrast.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/a11y/contrast.spec.ts new file mode 100644 index 000000000000..1f437c928908 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/a11y/contrast.spec.ts @@ -0,0 +1,37 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('a11y - contrast', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + +); + +// visual: generic.light +// visual: generic.dark +// visual: fluent.light +// visual: fluent.dark +test('Scheduler a11y: Insufficient contrast of day numbers in the MonthView', async ({ page }) => { + // --- setup --- +await createWidget(page, 'dxScheduler', { + dataSource: [], + currentView: 'month', + currentDate: new Date(2020, 10, 25), + // --- test --- +// Scheduler on '#container' + await testScreenshot(page, 'month_day_number_contrast.png', { element: page.locator('.dx-scheduler') }); + + expect(compareResults.isValid()) + .ok(compareResults.errorMessages()); +}); +}); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/adaptive.weekView.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/adaptive.weekView.spec.ts new file mode 100644 index 000000000000..842a48beba2d --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/adaptive.weekView.spec.ts @@ -0,0 +1,62 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, getContainerUrl, setupTestPage } from '../../../playwright-helpers'; + +const containerUrl = getContainerUrl(__dirname, '../../../tests/container.html'); + +const sampleData = [ + { text: 'Website Re-Design Plan', startDate: new Date(2017, 4, 22, 9, 30), endDate: new Date(2017, 4, 22, 11, 30) }, + { text: 'Website Re-Design Plan', startDate: new Date(2017, 4, 22, 9, 40), endDate: new Date(2017, 4, 22, 11, 40) }, + { text: 'Book Flights to San Fran for Sales Trip', startDate: new Date(2017, 4, 22, 12, 0), endDate: new Date(2017, 4, 22, 13, 0), allDay: true }, +]; + +const sampleDataNotRoundedMinutes = [ + { text: 'Website Re-Design Plan', startDate: new Date(2017, 4, 22, 9, 10), endDate: new Date(2017, 4, 22, 11, 30) }, + { text: 'Website Re-Design Plan', startDate: new Date(2017, 4, 23, 9, 5), endDate: new Date(2017, 4, 23, 11, 40) }, + { text: 'Book Flights to San Fran for Sales Trip', startDate: new Date(2017, 4, 24, 12, 12), endDate: new Date(2017, 4, 24, 13, 30) }, +]; + +const roughEqual = (actual: number, expected: number): boolean => { + const epsilon = 1.5; + return Math.abs(expected - actual) <= epsilon; +}; + +const createScheduler = async (page, data, width = '100%'): Promise => { + await createWidget(page, 'dxScheduler', { + dataSource: data, + views: ['week'], + currentView: 'week', + adaptivityEnabled: true, + currentDate: new Date(2017, 4, 25), + startDayHour: 9, + height: 600, + width, + }); +}; + +test.describe('Week view in adaptive mode', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + test('Compact appointment should be center by vertical alignment', async ({ page }) => { + await createScheduler(page, sampleDataNotRoundedMinutes); + + const appointmentCount = await page.locator('.dx-scheduler-appointment').count(); + expect(appointmentCount).toBe(0); + + const collectorsCount = await page.locator('.dx-scheduler-appointment-collector').count(); + expect(collectorsCount).toBe(3); + + const firstCollector = page.locator('.dx-scheduler-appointment-collector').nth(0); + const firstBox = await firstCollector.boundingBox(); + expect(roughEqual(firstBox!.y, 150)).toBeTruthy(); + + const secondCollector = page.locator('.dx-scheduler-appointment-collector').nth(1); + const secondBox = await secondCollector.boundingBox(); + expect(roughEqual(secondBox!.y, 150)).toBeTruthy(); + + const thirdCollector = page.locator('.dx-scheduler-appointment-collector').nth(2); + const thirdBox = await thirdCollector.boundingBox(); + expect(roughEqual(thirdBox!.y, 450)).toBeTruthy(); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/agenda/API.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/agenda/API.spec.ts new file mode 100644 index 000000000000..9b53b735d185 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/agenda/API.spec.ts @@ -0,0 +1,48 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, getContainerUrl, setupTestPage } from '../../../../playwright-helpers'; + +const containerUrl = getContainerUrl(__dirname, '../../../../tests/container.html'); + +test.describe('Agenda:API', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + test('Html elements should be absent in Agenda view', async ({ page }) => { + const data = [ + { text: 'Website Re-Design Plan', ownerId: [4, 1, 2], roomId: [1, 2, 3], priorityId: 2, startDate: new Date('2021-05-24T16:30:00.000Z'), endDate: new Date('2021-05-24T18:30:00.000Z'), recurrenceRule: 'FREQ=WEEKLY', allDay: true }, + { text: 'Book Flights to San Fran for Sales Trip', ownerId: 2, roomId: 2, priorityId: 1, startDate: new Date('2021-05-24T19:00:00.000Z'), endDate: new Date('2021-05-24T20:00:00.000Z'), allDay: true }, + { text: 'Final Budget Review', ownerId: 1, roomId: 1, priorityId: 1, startDate: new Date('2021-05-25T19:00:00.000Z'), endDate: new Date('2021-05-25T20:35:00.000Z') }, + { text: 'New Brochures', ownerId: 4, roomId: 3, priorityId: 2, startDate: new Date('2021-05-25T21:30:00.000Z'), endDate: new Date('2021-05-25T22:45:00.000Z') }, + { text: 'Install New Database', ownerId: 2, roomId: 3, priorityId: 1, startDate: new Date('2021-05-26T16:45:00.000Z'), endDate: new Date('2021-05-26T18:15:00.000Z') }, + { text: 'Approve New Online Marketing Strategy', ownerId: 4, roomId: 2, priorityId: 1, startDate: new Date('2021-05-26T19:00:00.000Z'), endDate: new Date('2021-05-26T21:00:00.000Z') }, + { text: 'Upgrade Personal Computers', ownerId: 2, roomId: 2, priorityId: 2, startDate: new Date('2021-05-26T22:15:00.000Z'), endDate: new Date('2021-05-26T23:30:00.000Z') }, + ]; + + await createWidget(page, 'dxScheduler', { + dataSource: data, + views: ['agenda'], + currentView: 'agenda', + currentDate: new Date(2021, 4, 25), + showAllDayPanel: true, + crossScrollingEnabled: true, + focusStateEnabled: true, + height: 600, + }); + + const scheduler = page.locator('#container'); + + await expect(scheduler.locator('.dx-scheduler-all-day-panel')).not.toBeVisible(); + await expect(scheduler.locator('.dx-scheduler-sidebar-scrollable')).not.toBeVisible(); + + const workSpace = scheduler.locator('.dx-scheduler-work-space'); + const hasBothScrollbar = await workSpace.evaluate((el) => el.classList.contains('dx-scheduler-work-space-both-scrollbar')); + expect(hasBothScrollbar).toBe(false); + + const cell0Text = await scheduler.locator('.dx-scheduler-date-table-cell').nth(0).textContent(); + expect(cell0Text).toBe(''); + + await expect(scheduler.locator('.dx-scheduler-fixed-appointments')).not.toBeVisible(); + await expect(scheduler.locator('.dx-scheduler-header-panel')).not.toBeVisible(); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/agenda/adaptive.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/agenda/adaptive.spec.ts new file mode 100644 index 000000000000..321e7c891b77 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/agenda/adaptive.spec.ts @@ -0,0 +1,47 @@ +import { test } from '@playwright/test'; +import { createWidget, testScreenshot, getContainerUrl, setupTestPage } from '../../../../playwright-helpers'; + +const containerUrl = getContainerUrl(__dirname, '../../../../tests/container.html'); + +const createScheduler = async (page, groups: undefined | string[], rtlEnabled: boolean): Promise => { + await createWidget(page, 'dxScheduler', { + dataSource: [ + { text: 'Website Re-Design Plan', priorityId: 2, startDate: new Date(2021, 4, 21, 16, 30), endDate: new Date(2021, 4, 21, 18, 30) }, + { text: 'Approve Personal Computer Upgrade Plan', priorityId: 2, startDate: new Date(2021, 4, 21, 17), endDate: new Date(2021, 4, 21, 18) }, + { text: 'Install New Database', priorityId: 1, startDate: new Date(2021, 4, 21, 16), endDate: new Date(2021, 4, 21, 19, 15) }, + { text: 'Approve New Online Marketing Strategy', priorityId: 1, startDate: new Date(2021, 4, 21, 19), endDate: new Date(2021, 4, 21, 21) }, + ], + views: ['agenda'], + currentView: 'agenda', + currentDate: new Date(2021, 4, 21), + rtlEnabled, + groups, + resources: [{ + fieldExpr: 'priorityId', + allowMultiple: false, + dataSource: [ + { text: 'Low Priority', id: 1, color: '#1e90ff' }, + { text: 'High Priority', id: 2, color: '#ff9747' }, + ], + label: 'Priority', + }], + }); +}; + +test.describe('Agenda:adaptive', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + [false, true].forEach((rtlEnabled) => { + [ + { groups: undefined, text: 'without-groups' }, + { groups: ['priorityId'], text: 'groups' }, + ].forEach((testCase) => { + test(`${testCase.text} adaptive rtl=${rtlEnabled}`, async ({ page }) => { + await createScheduler(page, testCase.groups, rtlEnabled); + await testScreenshot(page, `agenda-${testCase.text}-adaptive-rtl=${rtlEnabled}.png`); + }); + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/agenda/editing.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/agenda/editing.spec.ts new file mode 100644 index 000000000000..6b4e13e8e33f --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/agenda/editing.spec.ts @@ -0,0 +1,34 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, getContainerUrl, setupTestPage } from '../../../../playwright-helpers'; + +const containerUrl = getContainerUrl(__dirname, '../../../../tests/container.html'); + +test.describe('Agenda:Editing', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + test('It should be possible to delete an appointment', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource: [ + { text: 'App 1', startDate: new Date(2021, 1, 1, 12), endDate: new Date(2021, 1, 1, 13) }, + { text: 'App 2', startDate: new Date(2021, 1, 2, 12), endDate: new Date(2021, 1, 2, 13) }, + { text: 'App 3', startDate: new Date(2021, 1, 3, 12), endDate: new Date(2021, 1, 3, 13) }, + { text: 'App 4', startDate: new Date(2021, 1, 4, 12), endDate: new Date(2021, 1, 4, 13) }, + ], + views: ['agenda'], + currentView: 'agenda', + currentDate: new Date(2021, 1, 1), + height: 600, + }); + + const appointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'App 1' }); + await appointment.click(); + + const deleteButton = page.locator('.dx-tooltip-appointment-item-delete-button').first(); + await deleteButton.click(); + + const count = await page.locator('.dx-scheduler-appointment').count(); + expect(count).toBe(3); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/agenda/keyField.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/agenda/keyField.spec.ts new file mode 100644 index 000000000000..2638f5bb4b32 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/agenda/keyField.spec.ts @@ -0,0 +1,164 @@ +import { test, expect } from '@playwright/test'; +import { createWidget } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('Agenda:KeyField', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + +); + +const hasWarningCode = (message) => message.startsWith('W1023'); + +['week', 'agenda'].forEach((currentView) => { + test(`Warning should be thrown in console in case currentView='${currentView}'(T1100758)`, async ({ page }) => { + const messages = await t.getBrowserConsoleMessages(); + + const isWarningExist = !!messages.warn.find(hasWarningCode); + expect(isWarningExist).toBeTruthy(); + }); + +// TODO: .before() block not converted - move to test setup +// { + await createWidget(page, 'dxScheduler', { + dataSource: [], + views: ['week', 'agenda'], + currentView, + currentDate: new Date(2021, 2, 28), + height: 600, + }); + }); +}); + +test('Warning should be thrown in console after set new views(T1100758)', async ({ page }) => { + const messages = await t.getBrowserConsoleMessages(); + const isWarningExist = !!messages.warn.find(hasWarningCode); + expect(isWarningExist).toBeFalsy(); + + // Scheduler on '#container' + await scheduler.option('views', ['week', 'agenda']); + + const messagesAfterChangeViews = await t.getBrowserConsoleMessages(); + const isWarningExistAfterChangeViews = !!messagesAfterChangeViews.warn.find(hasWarningCode); + expect(isWarningExistAfterChangeViews).toBeTruthy(); +}); + +// TODO: .before() block not converted - move to test setup +// { + await createWidget(page, 'dxScheduler', { + dataSource: [], + views: ['week'], + currentView: 'week', + currentDate: new Date(2021, 2, 28), + height: 600, + }); +}); + +test('Warning shouldn\'t be thrown in console in case currentView=\'week\' if keyField exists(T1100758)', async ({ page }) => { + const messages = await t.getBrowserConsoleMessages(); + + const isWarningExist = !!messages.warn.find(hasWarningCode); + expect(isWarningExist).toBeFalsy(); +}); + +// TODO: .before() block not converted - move to test setup +// { + await createWidget(page, 'dxScheduler', () => { + const store = new (window as any).DevExpress.data.CustomStore({ + key: 'id', + load: () => [], + }); + + return { + dataSource: store, + views: ['week', 'agenda'], + currentView: 'week', + currentDate: new Date(2021, 2, 28), + height: 600, + }; + }); +}); + +test('Warning shouldn\'t be thrown in console in case currentView=\'agenda\' if keyField exists(T1100758)', async ({ page }) => { + const messages = await t.getBrowserConsoleMessages(); + + const isWarningExist = !!messages.warn.find(hasWarningCode); + expect(isWarningExist).toBeFalsy(); +}); + +// TODO: .before() block not converted - move to test setup +// { + await createWidget(page, 'dxScheduler', () => { + const store = new (window as any).DevExpress.data.CustomStore({ + key: 'id', + load: () => [], + }); + + return { + dataSource: store, + views: ['agenda'], + currentView: 'agenda', + currentDate: new Date(2021, 2, 28), + height: 600, + }; + }); +}); + +['week', 'agenda'].forEach((currentView) => { + test(`Warning should be thrown in console in case currentView='${currentView}' if keyField not set in Store(T1100758)`, async ({ page }) => { + const messages = await t.getBrowserConsoleMessages(); + + const isWarningExist = !!messages.warn.find(hasWarningCode); + expect(isWarningExist).toBeTruthy(); + }); + +// TODO: .before() block not converted - move to test setup +// { + await createWidget(page, 'dxScheduler', ClientFunction(() => ({ + dataSource: new (window as any).DevExpress.data.CustomStore({ + load: () => [], + }), + views: ['week', 'agenda'], + currentView, + currentDate: new Date(2021, 2, 28), + height: 600, + }), { dependencies: { currentView } })); + }); +}); + +test('Wrong behavior: editing recurrence appointment does not affect to appointment\'s data source(T1100758)', async ({ page }) => { + // Scheduler on '#container' + + await (page.locator('.dx-scheduler-appointment').filter({ hasText: 'Test' }).dblclick().element); + await t + .typeText(scheduler.appointmentPopup.textEditor.element, 'Updated', { replace: true }) + .click(scheduler.appointmentPopup.saveButton.element); + + expect(page.locator('.dx-scheduler-appointment').filter({ hasText: 'Updated' }).element.exists).toBeTruthy(); +}); + +// TODO: .before() block not converted - move to test setup +// { + await createWidget(page, 'dxScheduler', { + dataSource: [{ + text: 'Test', + startDate: new Date('2021-03-29T16:30:00.000Z'), + endDate: new Date('2021-03-29T18:30:00.000Z'), + recurrenceRule: 'FREQ=WEEKLY', + }], + views: ['agenda'], + currentView: 'agenda', + currentDate: new Date(2021, 2, 28), + recurrenceEditMode: 'series', + height: 600, + }, '#container'); +}); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/agenda/layout.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/agenda/layout.spec.ts new file mode 100644 index 000000000000..824265275e72 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/agenda/layout.spec.ts @@ -0,0 +1,183 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('Agenda:layout', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + +); + +const data = [{ + text: 'Website Re-Design Plan', + ownerId: [4, 1, 2], + roomId: [1, 2, 3], + priorityId: 2, + startDate: new Date('2021-05-24T16:30:00.000Z'), + endDate: new Date('2021-05-24T18:30:00.000Z'), + recurrenceRule: 'FREQ=WEEKLY', + allDay: true, +}, { + text: 'Book Flights to San Fran for Sales Trip', + ownerId: 2, + roomId: 2, + priorityId: 1, + startDate: new Date('2021-05-24T19:00:00.000Z'), + endDate: new Date('2021-05-24T20:00:00.000Z'), + allDay: true, +}, { + text: 'Final Budget Review', + ownerId: 1, + roomId: 1, + priorityId: 1, + startDate: new Date('2021-05-25T19:00:00.000Z'), + endDate: new Date('2021-05-25T20:35:00.000Z'), +}, { + text: 'New Brochures', + ownerId: 4, + roomId: 3, + priorityId: 2, + startDate: new Date('2021-05-25T21:30:00.000Z'), + endDate: new Date('2021-05-25T22:45:00.000Z'), +}, { + text: 'Install New Database', + ownerId: 2, + roomId: 3, + priorityId: 1, + startDate: new Date('2021-05-26T16:45:00.000Z'), + endDate: new Date('2021-05-26T18:15:00.000Z'), +}, { + text: 'Approve New Online Marketing Strategy', + ownerId: 4, + roomId: 2, + priorityId: 1, + startDate: new Date('2021-05-26T19:00:00.000Z'), + endDate: new Date('2021-05-26T21:00:00.000Z'), +}, { + text: 'Upgrade Personal Computers', + ownerId: 2, + roomId: 2, + priorityId: 2, + startDate: new Date('2021-05-26T22:15:00.000Z'), + endDate: new Date('2021-05-26T23:30:00.000Z'), +}]; + +const owners = [{ + text: 'Samantha Bright', + id: 1, + color: '#727bd2', +}, { + text: 'John Heart', + id: 2, + color: '#32c9ed', +}, { + text: 'Todd Hoffman', + id: 3, + color: '#2a7ee4', +}, { + text: 'Sandra Johnson', + id: 4, + color: '#7b49d3', +}]; + +const rooms = [{ + text: 'Room 1', + id: 1, + color: '#00af2c', +}, { + text: 'Room 2', + id: 2, + color: '#56ca85', +}, { + text: 'Room 3', + id: 3, + color: '#8ecd3c', +}]; + +const priorities = [{ + text: 'High priority', + id: 1, + color: '#cc5c53', +}, { + text: 'Low priority', + id: 2, + color: '#ff9747', +}]; + +const resourcesData = [{ + fieldExpr: 'roomId', + allowMultiple: true, + dataSource: rooms, + label: 'Room', +}, { + fieldExpr: 'priorityId', + allowMultiple: true, + dataSource: priorities, + label: 'Priority', +}, { + fieldExpr: 'ownerId', + allowMultiple: true, + dataSource: owners, + label: 'Owner', +}]; + +const createScheduler = async ( + rtlEnabled: boolean, + resources: undefined | any[], + groups: undefined | string[], +): Promise => { + await createWidget(page, 'dxScheduler', { + dataSource: data, + views: ['agenda'], + currentView: 'agenda', + currentDate: new Date(2021, 4, 25), + resources, + rtlEnabled, + groups, + height: 600, + }); +}; + +[false, true].forEach((rtlEnabled) => { + [undefined, resourcesData].forEach((resources) => { + test(`Agenda test layout(rtl=${rtlEnabled}, resources=${!!resources}`, async (t) => { + await testScreenshot(page, + `agenda-layout-rtl=${rtlEnabled}-resources=${!!resources}.png`, + ); + + expect(compareResults.isValid()) + .ok(compareResults.errorMessages()); + }) + .before(async () => createScheduler(rtlEnabled, resources, undefined)); + }); +}); + +[false, true].forEach((rtlEnabled) => { + test(`Agenda test layout with groups(rtl=${rtlEnabled}`, async ({ page }) => { + await testScreenshot(page, `agenda-layout-groups-rtl=${rtlEnabled}.png`); + + expect(compareResults.isValid()) + .ok(compareResults.errorMessages()); + }).before(async () => createScheduler(rtlEnabled, resourcesData, ['roomId'])); +}); + +test('Agenda test appointment state', async ({ page }) => { + // Scheduler on '#container' + await (page.locator('.dx-scheduler-appointment').filter({ hasText: 'Final Budget Review' }).hover().element); + await testScreenshot(page, 'agenda-layout-appointment-state-hover.png'); + + await (page.locator('.dx-scheduler-appointment').filter({ hasText: 'New Brochures' }).click().element); + await testScreenshot(page, 'agenda-layout-appointment-state-click.png'); + + expect(compareResults.isValid()) + .ok(compareResults.errorMessages()); +}).before(async () => createScheduler(false, resourcesData, undefined)); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/agenda/switchingToAgenda.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/agenda/switchingToAgenda.spec.ts new file mode 100644 index 000000000000..5a574de218ff --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/agenda/switchingToAgenda.spec.ts @@ -0,0 +1,42 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('Agenda:view switching', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + +); + +test('View switching should work for empty agenda', async ({ page }) => { + // --- setup --- +await createWidget(page, 'dxScheduler', { + dataSource: [{ + startDate: new Date(2021, 4, 25, 0), + endDate: new Date(2021, 4, 25, 1), + text: 'Test Appointment', + }], + views: ['day', 'agenda'], + currentView: 'day', + currentDate: new Date(2021, 4, 25), + height: 600, + // --- test --- +// Scheduler on '#container' + await scheduler.option('currentDate', new Date(2021, 4, 26)); + await scheduler.option('currentView', 'agenda'); + + await testScreenshot(page, 'switch-to-agenda-without-appointments.png'); + + expect(compareResults.isValid()) + .ok(compareResults.errorMessages()); +}); +}); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/agenda/tooltip.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/agenda/tooltip.spec.ts new file mode 100644 index 000000000000..1631f3ccf1c3 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/agenda/tooltip.spec.ts @@ -0,0 +1,51 @@ +import { test, expect } from '@playwright/test'; +import { createWidget } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('Agenda:Tooltip', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + +); + +test('Tooltip\'s date should be equal to date of current appointment(T1037028)', async ({ page }) => { + // Scheduler on '#container' + const appointmentName = 'Text'; + + for (let index = 0; index < 5; index += 1) { + await scheduler.hideAppointmentTooltip(); + + await (scheduler.getAppointment(appointmentName, index).click().element); + + const tooltipDate = await scheduler.appointmentTooltip + .getListItem(appointmentName, 0).date.innerText; + const expectedDate = await scheduler.getAppointment(appointmentName, index).date.time; + + expect(tooltipDate).toBe(expectedDate); + } +}); + +// TODO: .before() block not converted - move to test setup +// { + await createWidget(page, 'dxScheduler', { + dataSource: [{ + text: 'Text', + startDate: new Date(2021, 1, 1, 12), + endDate: new Date(2021, 1, 1, 13), + recurrenceRule: 'FREQ=HOURLY;COUNT=5', + }], + views: ['agenda'], + currentView: 'agenda', + currentDate: new Date(2021, 1, 1), + height: 600, + }); +}); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/api/deleteRecurrence.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/api/deleteRecurrence.spec.ts new file mode 100644 index 000000000000..9a2b5b478dac --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/api/deleteRecurrence.spec.ts @@ -0,0 +1,115 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, getContainerUrl, setupTestPage } from '../../../../playwright-helpers'; + +const containerUrl = getContainerUrl(__dirname, '../../../../tests/container.html'); + +test.describe('Scheduler API - deleteRecurrence', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + test('should delete recurrent appointment if mode is "series"', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + width: 800, + height: 600, + views: [{ type: 'day', intervalCount: 3 }], + currentView: 'day', + currentDate: new Date(2022, 3, 12), + startDayHour: 8, + endDayHour: 13, + onAppointmentDeleting: ((e: any) => { + e.component.deleteRecurrence(e.appointmentData, e.targetedAppointmentData.startDate, 'series'); + e.cancel = true; + }) as any, + dataSource: [{ + text: 'test-appt', + startDate: new Date(2022, 3, 12, 8), + endDate: new Date(2022, 3, 12, 9), + apptColor: 1, + recurrenceRule: 'FREQ=DAILY;COUNT=4', + }], + }); + + const appointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'test-appt' }); + await appointment.click(); + + const deleteButton = page.locator('.dx-tooltip-appointment-item-delete-button').first(); + await deleteButton.click(); + + const appointmentCount = await page.locator('.dx-scheduler-appointment').count(); + expect(appointmentCount).toBe(0); + }); + + test('should exclude from recurrence if mode is "occurrence"', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + width: 800, + height: 600, + views: [{ type: 'day', intervalCount: 3 }], + currentView: 'day', + currentDate: new Date(2022, 3, 12), + startDayHour: 8, + endDayHour: 12, + onAppointmentDeleting: ((e: any) => { + e.component.deleteRecurrence(e.appointmentData, e.targetedAppointmentData.startDate, 'occurrence'); + e.cancel = true; + }) as any, + dataSource: [{ + text: 'test-appt', + startDate: new Date(2022, 3, 12, 8), + endDate: new Date(2022, 3, 12, 9), + apptColor: 1, + recurrenceRule: 'FREQ=DAILY;COUNT=4', + }], + }); + + const appointment0 = page.locator('.dx-scheduler-appointment').filter({ hasText: 'test-appt' }).first(); + await appointment0.click(); + + const deleteButton = page.locator('.dx-tooltip-appointment-item-delete-button').first(); + await deleteButton.click(); + + const appointmentCount = await page.locator('.dx-scheduler-appointment').count(); + expect(appointmentCount).toBe(2); + }); + + test('should show delete recurrence dialog if mode is "dialog"', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + width: 800, + height: 600, + views: [{ type: 'day', intervalCount: 3 }], + currentView: 'day', + currentDate: new Date(2022, 3, 12), + startDayHour: 8, + endDayHour: 13, + onAppointmentDeleting: ((e: any) => { + e.component.deleteRecurrence(e.appointmentData, e.targetedAppointmentData.startDate, 'dialog'); + e.cancel = true; + }) as any, + dataSource: [{ + text: 'test-appt', + startDate: new Date(2022, 3, 12, 8), + endDate: new Date(2022, 3, 12, 9), + apptColor: 1, + recurrenceRule: 'FREQ=DAILY;COUNT=4', + }], + }); + + const appointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'test-appt' }).first(); + await appointment.click(); + + const deleteButton = page.locator('.dx-tooltip-appointment-item-delete-button').first(); + await expect(deleteButton).toBeVisible(); + await deleteButton.click(); + + await page.waitForTimeout(100); + const count1 = await page.locator('.dx-scheduler-appointment').count(); + expect(count1).toBe(3); + + const dialogAppointmentBtn = page.locator('.dx-dialog').locator('.dx-dialog-button').first(); + await dialogAppointmentBtn.click(); + + await page.waitForTimeout(100); + const count2 = await page.locator('.dx-scheduler-appointment').count(); + expect(count2).toBe(2); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/api/resourceRequestCount.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/api/resourceRequestCount.spec.ts new file mode 100644 index 000000000000..2658f159a066 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/api/resourceRequestCount.spec.ts @@ -0,0 +1,30 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, getContainerUrl, setupTestPage } from '../../../../playwright-helpers'; + +const containerUrl = getContainerUrl(__dirname, '../../../../tests/container.html'); + +test.describe('Scheduler API - request counting', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + // TODO: RequestLogger from TestCafe has no direct Playwright equivalent. + // These tests require network interception via page.route() to count requests. + // Skipping for now as they need API mocking infrastructure. + + test.skip('Request should be requested only once for color appointments (week)', async () => { + // Requires page.route() setup for resource API mock + }); + + test.skip('Request should be requested only once for color appointments (agenda)', async () => { + // Requires page.route() setup for resource API mock + }); + + test.skip('Request should be requested only once for grouping', async () => { + // Requires page.route() setup for resource API mock + }); + + test.skip('should be no requests for no grouping and appointments without color', async () => { + // Requires page.route() setup for resource API mock + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/appointmentForm/form.functional.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/appointmentForm/form.functional.spec.ts new file mode 100644 index 000000000000..a064ac39f52c --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/appointmentForm/form.functional.spec.ts @@ -0,0 +1,163 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, setupTestPage } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +const SCHEDULER_SELECTOR = '#container'; + +const openAppointmentPopup = async (page: any, appointment?: any, isRecurring = false) => { + await page.evaluate(({ appt, recurring, sel }) => { + const instance = ($(sel) as any).dxScheduler('instance'); + instance.showAppointmentPopup(appt, !appt, recurring); + }, { appt: appointment, recurring: isRecurring, sel: SCHEDULER_SELECTOR }); + await page.locator('.dx-scheduler-appointment-popup').waitFor({ state: 'visible' }); +}; + +const clickRecurrenceSettingsButton = async (page: any) => { + await page.locator('.dx-recurrence-editor .dx-button').click(); +}; + +const roughEqualClientBoundingRect = ( + a: { width: number; height: number; top: number; left: number }, + b: { width: number; height: number; top: number; left: number }, +): boolean => ( + Math.abs(a.width - b.width) < 1 + && Math.abs(a.height - b.height) < 1 + && Math.abs(a.top - b.top) < 1 + && Math.abs(a.left - b.left) < 1 +); + +test.describe('Appointment Form: Functional', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + test('Subject text editor should have focus after returning from recurrence form', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource: [], + views: ['week'], + currentView: 'week', + currentDate: new Date(2021, 2, 25), + }); + + const appointment = { + text: 'Appointment', + startDate: new Date('2021-04-26T16:30:00.000Z'), + endDate: new Date('2021-04-26T18:30:00.000Z'), + allDay: false, + recurrenceRule: 'FREQ=WEEKLY;BYDAY=MO,TH;COUNT=10', + }; + + await openAppointmentPopup(page, appointment, true); + await clickRecurrenceSettingsButton(page); + + await page.locator('.dx-recurrence-back-button').click(); + + const textInput = page.locator('.dx-scheduler-appointment-popup .dx-texteditor-input').first(); + await expect(textInput).toBeFocused(); + }); + + test('Recurrence start date editor should have focus after opening recurrence settings', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource: [], + views: ['week'], + currentView: 'week', + currentDate: new Date(2021, 2, 25), + }); + + const appointment = { + text: 'Appointment', + startDate: new Date('2021-04-26T16:30:00.000Z'), + endDate: new Date('2021-04-26T18:30:00.000Z'), + allDay: false, + recurrenceRule: 'FREQ=WEEKLY;BYDAY=MO,TH;COUNT=10', + }; + + await openAppointmentPopup(page, appointment, true); + await clickRecurrenceSettingsButton(page); + + const startDateInput = page.locator('.dx-recurrence-editor .dx-datebox .dx-texteditor-input').first(); + await expect(startDateInput).toBeFocused(); + }); + + test('Popup should not change dimensions when switching groups and recurrence group height is larger', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource: [], + views: ['week'], + currentView: 'week', + currentDate: new Date(2021, 2, 25), + editing: { + form: { + items: [ + { + name: 'mainGroup', + items: ['repeatGroup'], + }, + 'recurrenceGroup', + ], + }, + }, + }); + + await openAppointmentPopup(page); + const contentElement = page.locator('.dx-popup-content'); + const boundingClientRect1 = await contentElement.boundingBox(); + + await page.evaluate((sel) => { + const instance = ($(sel) as any).dxScheduler('instance'); + const popup = instance.getAppointmentPopup(); + const form = popup.$content().find('.dx-form').dxForm('instance'); + const repeatEditor = form.getEditor('recurrenceRule'); + repeatEditor.option('value', 'FREQ=WEEKLY'); + }, SCHEDULER_SELECTOR); + + const boundingClientRect2 = await contentElement.boundingBox(); + + await page.locator('.dx-recurrence-back-button').click(); + const boundingClientRect3 = await contentElement.boundingBox(); + + expect(roughEqualClientBoundingRect(boundingClientRect1!, boundingClientRect2!)).toBe(true); + expect(roughEqualClientBoundingRect(boundingClientRect1!, boundingClientRect3!)).toBe(true); + }); + + test('Popup should not change dimensions when switching groups and main group height is larger', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource: [], + views: ['week'], + currentView: 'week', + currentDate: new Date(2021, 2, 25), + editing: { + form: { + items: [ + 'mainGroup', + { + name: 'recurrenceGroup', + items: ['recurrenceStartDateGroup'], + }, + ], + }, + }, + }); + + await openAppointmentPopup(page); + const contentElement = page.locator('.dx-popup-content'); + const boundingClientRect1 = await contentElement.boundingBox(); + + await page.evaluate((sel) => { + const instance = ($(sel) as any).dxScheduler('instance'); + const popup = instance.getAppointmentPopup(); + const form = popup.$content().find('.dx-form').dxForm('instance'); + const repeatEditor = form.getEditor('recurrenceRule'); + repeatEditor.option('value', 'FREQ=WEEKLY'); + }, SCHEDULER_SELECTOR); + + const boundingClientRect2 = await contentElement.boundingBox(); + + await page.locator('.dx-recurrence-back-button').click(); + const boundingClientRect3 = await contentElement.boundingBox(); + + expect(roughEqualClientBoundingRect(boundingClientRect1!, boundingClientRect2!)).toBe(true); + expect(roughEqualClientBoundingRect(boundingClientRect1!, boundingClientRect3!)).toBe(true); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/appointmentForm/form.visual.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/appointmentForm/form.visual.spec.ts new file mode 100644 index 000000000000..2334cd886042 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/appointmentForm/form.visual.spec.ts @@ -0,0 +1,319 @@ +import { test } from '@playwright/test'; +import { createWidget, testScreenshot, setupTestPage } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +const SCHEDULER_SELECTOR = '#container'; + +const getResources = (withIcons = false) => ([ + { + fieldExpr: 'assigneeId', + allowMultiple: true, + label: 'Assignee', + dataSource: [ + { text: 'Samantha Bright', id: 1, color: '#727bd2' }, + { text: 'John Heart', id: 2, color: '#32c9ed' }, + { text: 'Todd Hoffman', id: 3, color: '#2a7ee4' }, + { text: 'Sandra Johnson', id: 4, color: '#7b49d3' }, + ], + icon: withIcons ? 'user' : undefined, + }, + { + fieldExpr: 'roomId', + label: 'Room', + dataSource: [ + { text: 'Room 1', id: 1, color: '#00af2c' }, + ], + icon: withIcons ? 'conferenceroomfilled' : undefined, + }, + { + fieldExpr: 'priorityId', + label: 'Priority', + dataSource: [ + { text: 'High', id: 1, color: '#cc5c53' }, + ], + icon: withIcons ? 'tags' : undefined, + }, +]); + +const openAppointmentPopup = async (page: any, appointment?: any, isRecurring = false) => { + await page.evaluate(({ appt, recurring, sel }) => { + const instance = ($(sel) as any).dxScheduler('instance'); + instance.showAppointmentPopup(appt, !appt, recurring); + }, { appt: appointment, recurring: isRecurring, sel: SCHEDULER_SELECTOR }); + await page.locator('.dx-scheduler-appointment-popup').waitFor({ state: 'visible' }); +}; + +test.describe('Appointment Form: Main Form', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + [ + { isRecurringAppointment: false, isAllDay: true }, + { isRecurringAppointment: false, isAllDay: false }, + { isRecurringAppointment: true, isAllDay: true }, + { isRecurringAppointment: true, isAllDay: false }, + ].forEach(({ isRecurringAppointment, isAllDay }) => { + const appointment = { + text: 'Appointment', + startDate: new Date('2021-04-26T16:30:00.000Z'), + endDate: new Date('2021-04-26T18:30:00.000Z'), + allDay: isAllDay, + recurrenceRule: isRecurringAppointment ? 'FREQ=WEEKLY;BYDAY=MO,TH;COUNT=10' : undefined, + assigneeId: [1, 2], + roomId: 1, + priorityId: 1, + }; + + test(`appointment main form (recurring=${isRecurringAppointment},allDay=${isAllDay})`, async ({ page }) => { + await page.setViewportSize({ width: 1500, height: 1500 }); + + await createWidget(page, 'dxScheduler', { + dataSource: [appointment], + views: ['week'], + currentView: 'week', + currentDate: new Date(2021, 2, 25), + }); + + await openAppointmentPopup(page, appointment, isRecurringAppointment); + + await testScreenshot( + page, + `scheduler__appointment__main-form (recurring=${isRecurringAppointment},allDay=${isAllDay}).png`, + { element: page.locator('.dx-popup-content') }, + ); + }); + + test(`appointment main form with resources and timezones (recurring=${isRecurringAppointment},allDay=${isAllDay})`, async ({ page }) => { + await page.setViewportSize({ width: 1500, height: 1500 }); + + await createWidget(page, 'dxScheduler', { + dataSource: [appointment], + views: ['week'], + currentView: 'week', + currentDate: new Date(2021, 2, 25), + resources: getResources(), + editing: { + allowTimeZoneEditing: true, + }, + }); + + await openAppointmentPopup(page, appointment, isRecurringAppointment); + + await testScreenshot( + page, + `scheduler__appointment__main-form__with-resources-and-timezones (recurring=${isRecurringAppointment},allDay=${isAllDay}).png`, + { element: page.locator('.dx-popup-content') }, + ); + }); + }); + + test('main form with resources that have icons', async ({ page }) => { + await page.setViewportSize({ width: 1500, height: 1500 }); + + const appointment = { + text: 'Appointment', + startDate: new Date('2021-04-26T16:30:00.000Z'), + endDate: new Date('2021-04-26T18:30:00.000Z'), + assigneeId: [1, 2], + roomId: 1, + priorityId: 1, + }; + + await createWidget(page, 'dxScheduler', { + dataSource: [], + views: ['week'], + currentView: 'week', + currentDate: new Date(2021, 2, 25), + resources: getResources(true), + }); + + await openAppointmentPopup(page, appointment, false); + + await testScreenshot( + page, + 'scheduler__appointment__main-form__with-resources-with-icons.png', + { element: page.locator('.dx-popup-content') }, + ); + }); + + test('appointment form readonly state', async ({ page }) => { + await page.setViewportSize({ width: 1500, height: 1500 }); + + const appointment = { + text: 'Appointment', + startDate: new Date('2021-04-26T16:30:00.000Z'), + endDate: new Date('2021-04-26T18:30:00.000Z'), + allDay: false, + recurrenceRule: 'FREQ=WEEKLY;BYDAY=MO,TH;COUNT=10', + assigneeId: [1, 2], + roomId: 1, + priorityId: 1, + }; + + await createWidget(page, 'dxScheduler', { + dataSource: [], + views: ['week'], + currentView: 'week', + currentDate: new Date(2021, 2, 25), + resources: getResources(), + editing: { + allowUpdating: false, + allowTimeZoneEditing: true, + }, + }); + + await openAppointmentPopup(page, appointment, false); + + await testScreenshot( + page, + 'scheduler__appointment__main-form__readonly.png', + { element: page.locator('.dx-popup-content') }, + ); + }); + + test('main form on mobile screen', async ({ page }) => { + await page.setViewportSize({ width: 450, height: 1000 }); + + await createWidget(page, 'dxScheduler', { + dataSource: [], + views: ['week'], + currentView: 'week', + currentDate: new Date(2021, 2, 25), + resources: getResources(true), + editing: { + form: { + iconsShowMode: 'both', + }, + }, + }); + + await openAppointmentPopup(page, undefined, false); + + await testScreenshot( + page, + 'scheduler__appointment__main-form__mobile.png', + ); + }); + + test('appointment form resource with multiple selection', async ({ page }) => { + await page.setViewportSize({ width: 1500, height: 1500 }); + + const appointment = { + text: 'Appointment', + startDate: new Date('2021-04-26T16:30:00.000Z'), + endDate: new Date('2021-04-26T18:30:00.000Z'), + allDay: false, + assigneeId: [1, 2, 3, 4], + roomId: 1, + priorityId: 1, + }; + + await createWidget(page, 'dxScheduler', { + dataSource: [], + views: ['week'], + currentView: 'week', + currentDate: new Date(2021, 2, 25), + resources: getResources(true), + editing: { + allowUpdating: true, + }, + }); + + await openAppointmentPopup(page, appointment, false); + + await testScreenshot( + page, + 'scheduler__appointment__main-form__resource-with-multiple-selection.png', + { element: page.locator('.dx-popup-content') }, + ); + }); + + test('appointment main form with opened startDate calendar', async ({ page }) => { + await page.setViewportSize({ width: 1500, height: 1500 }); + + const appointment = { + text: 'Appointment', + startDate: new Date('2021-04-26T16:30:00.000Z'), + endDate: new Date('2021-04-26T18:30:00.000Z'), + allDay: false, + }; + + await createWidget(page, 'dxScheduler', { + dataSource: [], + views: ['week'], + currentView: 'week', + currentDate: new Date(2021, 2, 25), + }); + + await openAppointmentPopup(page, appointment, false); + + const startDateDropDown = page.locator('.dx-scheduler-appointment-popup .dx-first-row .dx-dropdowneditor-button'); + await startDateDropDown.first().click(); + + await page.locator('.dx-calendar').waitFor({ state: 'visible' }); + + await testScreenshot( + page, + 'scheduler__appointment__main-form__startDate-calendar-opened.png', + ); + }); + + test('Recurrence settings button should have correct focus state', async ({ page }) => { + await page.setViewportSize({ width: 1500, height: 1500 }); + + const appointment = { + text: 'Appointment', + startDate: new Date('2021-04-26T16:30:00.000Z'), + endDate: new Date('2021-04-26T18:30:00.000Z'), + allDay: false, + recurrenceRule: 'FREQ=WEEKLY;BYDAY=MO,TH;COUNT=10', + }; + + await createWidget(page, 'dxScheduler', { + dataSource: [], + views: ['week'], + currentView: 'week', + currentDate: new Date(2021, 2, 25), + }); + + await openAppointmentPopup(page, appointment, true); + + await page.locator('.dx-recurrence-editor').click(); + await page.keyboard.press('Tab'); + + await testScreenshot( + page, + 'scheduler__appointment__recurrence-settings-button__focus-state.png', + { element: page.locator('.dx-popup-content') }, + ); + }); + + test('appointment form with labelMode=static', async ({ page }) => { + await page.setViewportSize({ width: 1500, height: 1500 }); + + await createWidget(page, 'dxScheduler', { + dataSource: [], + views: ['week'], + currentView: 'week', + currentDate: new Date(2021, 2, 25), + resources: getResources(true), + editing: { + allowUpdating: true, + form: { + labelMode: 'static', + }, + }, + }); + + await openAppointmentPopup(page, undefined, false); + + await testScreenshot( + page, + 'scheduler__appointment__main-form__with-labelMode-static.png', + { element: page.locator('.dx-popup-content') }, + ); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/appointmentForm/recurrence-form.visual.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/appointmentForm/recurrence-form.visual.spec.ts new file mode 100644 index 000000000000..f55096888d4e --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/appointmentForm/recurrence-form.visual.spec.ts @@ -0,0 +1,214 @@ +import { test } from '@playwright/test'; +import { createWidget, testScreenshot, setupTestPage } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +const SCHEDULER_SELECTOR = '#container'; + +const openAppointmentPopup = async (page: any, appointment?: any, isRecurring = false) => { + await page.evaluate(({ appt, recurring, sel }) => { + const instance = ($(sel) as any).dxScheduler('instance'); + instance.showAppointmentPopup(appt, !appt, recurring); + }, { appt: appointment, recurring: isRecurring, sel: SCHEDULER_SELECTOR }); + await page.locator('.dx-scheduler-appointment-popup').waitFor({ state: 'visible' }); +}; + +const selectRepeatValue = async (page: any, frequency: string) => { + await page.evaluate(({ sel, freq }) => { + const instance = ($(sel) as any).dxScheduler('instance'); + const popup = instance.getAppointmentPopup(); + const form = popup.$content().find('.dx-form').dxForm('instance'); + const repeatEditor = form.getEditor('recurrenceRule'); + const freqMap: Record = { + Hourly: 'FREQ=HOURLY', + Daily: 'FREQ=DAILY', + Weekly: 'FREQ=WEEKLY', + Monthly: 'FREQ=MONTHLY', + Yearly: 'FREQ=YEARLY', + }; + repeatEditor.option('value', freqMap[freq] || freq); + }, { sel: SCHEDULER_SELECTOR, freq: frequency }); +}; + +const clickRecurrenceSettingsButton = async (page: any) => { + await page.locator('.dx-recurrence-editor .dx-button').click(); +}; + +test.describe('Appointment Form: Recurrence Form', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + ['Hourly', 'Daily', 'Weekly', 'Monthly', 'Yearly'].forEach((frequency) => { + test(`recurrence form in ${frequency} frequency`, async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource: [], + views: ['week'], + currentView: 'week', + currentDate: new Date(2024, 0, 1), + }); + + const appointment = { + text: 'Appointment', + startDate: new Date('2024-01-01T10:00:00'), + endDate: new Date('2024-01-01T11:00:00'), + }; + + await openAppointmentPopup(page, appointment, false); + await selectRepeatValue(page, frequency); + + await testScreenshot( + page, + `scheduler__appointment__recurrence-form__${frequency.toLowerCase()}.png`, + { element: page.locator('.dx-popup-content') }, + ); + }); + }); + + test('recurrence form with icons', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource: [], + views: ['week'], + currentView: 'week', + currentDate: new Date(2021, 2, 25), + editing: { + form: { + iconsShowMode: 'both', + }, + }, + }); + + const appointment = { + text: 'Appointment', + startDate: new Date('2021-04-26T16:30:00.000Z'), + endDate: new Date('2021-04-26T18:30:00.000Z'), + assigneeId: [1, 2], + roomId: 1, + priorityId: 1, + recurrenceRule: 'FREQ=WEEKLY;BYDAY=MO,WE,FR;COUNT=1', + }; + + await openAppointmentPopup(page, appointment, true); + await clickRecurrenceSettingsButton(page); + + await testScreenshot( + page, + 'scheduler__appointment__recurrence-form__with-icons.png', + { element: page.locator('.dx-popup-content') }, + ); + }); + + test('recurrence form readonly state', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource: [], + views: ['week'], + currentView: 'week', + currentDate: new Date(2024, 0, 1), + editing: { + allowUpdating: false, + }, + }); + + const appointment = { + text: 'Readonly Recurrent Appointment', + startDate: new Date('2024-01-01T10:00:00'), + endDate: new Date('2024-01-01T11:00:00'), + recurrenceRule: 'FREQ=WEEKLY;BYDAY=MO,WE,FR;COUNT=10', + }; + + await openAppointmentPopup(page, appointment, false); + await clickRecurrenceSettingsButton(page); + + await testScreenshot( + page, + 'scheduler__appointment__recurrence-form__readonly.png', + { element: page.locator('.dx-popup-content') }, + ); + }); + + test('recurrence form on mobile screen', async ({ page }) => { + await page.setViewportSize({ width: 450, height: 1000 }); + + await createWidget(page, 'dxScheduler', { + dataSource: [], + views: ['week'], + currentView: 'week', + currentDate: new Date(2021, 2, 25), + editing: { + form: { + iconsShowMode: 'both', + }, + }, + }); + + await openAppointmentPopup(page, undefined, false); + await selectRepeatValue(page, 'Weekly'); + + await testScreenshot( + page, + 'scheduler__appointment__recurrence-form__mobile.png', + ); + }); + + test('recurrence form with labelMode=static', async ({ page }) => { + await page.setViewportSize({ width: 1500, height: 1500 }); + + await createWidget(page, 'dxScheduler', { + dataSource: [], + views: ['week'], + currentView: 'week', + currentDate: new Date(2021, 2, 25), + editing: { + allowUpdating: true, + popup: { + width: 420, + height: 500, + }, + form: { + iconsShowMode: 'both', + labelMode: 'static', + items: [ + 'mainGroup', + { + name: 'recurrenceGroup', + items: [ + 'recurrenceStartDateGroup', + 'recurrenceRuleGroup', + { + name: 'recurrenceEndGroup', + items: [ + 'recurrenceEndIcon', + { + name: 'recurrenceEndEditor', + label: { + visible: true, + location: 'top', + }, + }, + ], + }, + ], + }, + ], + }, + }, + }); + + const appointment = { + text: 'Readonly Recurrent Appointment', + startDate: new Date('2024-01-01T10:00:00'), + endDate: new Date('2024-01-01T11:00:00'), + recurrenceRule: 'FREQ=WEEKLY;BYDAY=MO,WE,FR;COUNT=10', + }; + + await openAppointmentPopup(page, appointment, true); + await clickRecurrenceSettingsButton(page); + + await testScreenshot( + page, + 'scheduler__appointment__recurrence-form__with-labelMode-static.png', + { element: page.locator('.dx-popup-content') }, + ); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/appointmentOverlapping/basic.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/appointmentOverlapping/basic.spec.ts new file mode 100644 index 000000000000..a2f4adf3f336 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/appointmentOverlapping/basic.spec.ts @@ -0,0 +1,120 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, setupTestPage } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +const SIMPLE_DATA = [ + { + text: 'Appointment 1', + startDate: new Date(2017, 4, 24, 13, 0), + endDate: new Date(2017, 4, 25, 12, 30), + }, + { + text: 'Appointment 2', + startDate: new Date(2017, 4, 24, 15, 0), + endDate: new Date(2017, 4, 24, 16, 30), + }, + { + text: 'Appointment 3', + startDate: new Date(2017, 4, 25, 9, 0), + endDate: new Date(2017, 4, 25, 10, 30), + }, + { + text: 'Appointment 4', + startDate: new Date(2017, 4, 25, 11, 0), + endDate: new Date(2017, 4, 25, 12, 30), + }, + { + text: 'Appointment 5', + startDate: new Date(2017, 4, 25, 11, 0), + endDate: new Date(2017, 4, 25, 12, 0), + allDay: true, + }, +]; + +const ALL_DAY_DATA = [ + { + text: 'Appointment 1', + startDate: new Date(2017, 4, 21, 9, 0), + endDate: new Date(2017, 4, 24, 10, 30), + allDay: true, + }, + { + text: 'Appointment 2', + startDate: new Date(2017, 4, 22, 11, 0), + endDate: new Date(2017, 4, 22, 12, 0), + allDay: true, + }, + { + text: 'Appointment 3', + startDate: new Date(2017, 4, 25, 9, 0), + endDate: new Date(2017, 4, 25, 10, 30), + }, + { + text: 'Appointment 4', + startDate: new Date(2017, 4, 25, 11, 0), + endDate: new Date(2017, 4, 25, 12, 0), + allDay: true, + }, +]; + +const SCHEDULER_DEFAULT_OPTIONS = { + views: ['week'], + width: 940, + currentView: 'week', + currentDate: new Date(2017, 4, 25), + startDayHour: 9, + height: 900, +}; + +test.describe('Appointment overlapping in Scheduler', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + test('Multi-day appointment should not overlap other appointments when specific width is set, \'auto\' mode (T864456)', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + ...SCHEDULER_DEFAULT_OPTIONS, + dataSource: SIMPLE_DATA, + }); + + const collectorsCount = await page.locator('.dx-scheduler-appointment-collector').count(); + expect(collectorsCount).toBe(3); + + const appointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'Appointment 1' }).nth(1); + const box = await appointment.boundingBox(); + + expect(Math.round(box!.height)).toBe(266); + expect(Math.round(box!.width)).toBe(94); + }); + + test('Simple appointment should not overlap allDay appointment when specific width is set, \'auto\' mode (T864456)', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + ...SCHEDULER_DEFAULT_OPTIONS, + dataSource: ALL_DAY_DATA, + }); + + const collectorsCount = await page.locator('.dx-scheduler-appointment-collector').count(); + expect(collectorsCount).toBe(1); + + const appointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'Appointment 4' }); + const box = await appointment.boundingBox(); + expect(box!.y).toBeCloseTo(138.828125, 0); + }); + + test('Crossing allDay appointments should not overlap each other (T893674)', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + ...SCHEDULER_DEFAULT_OPTIONS, + dataSource: ALL_DAY_DATA, + }); + + const firstAppointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'Appointment 1' }); + const secondAppointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'Appointment 2' }); + + const firstBox = await firstAppointment.boundingBox(); + const secondBox = await secondAppointment.boundingBox(); + + expect(firstBox!.y).not.toBe(secondBox!.y); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/appointments/T1017889.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/appointments/T1017889.spec.ts new file mode 100644 index 000000000000..b5fcaeb2791e --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/appointments/T1017889.spec.ts @@ -0,0 +1,36 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, setupTestPage, getContainerUrl } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = getContainerUrl(__dirname, '../../../../tests/container.html'); + +test.describe('Timeline Appointments', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + test('all-day and ordinary appointments should overlap each other correctly in timeline views (T1017889)', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource: [{ + text: 'Google AdWords Strategy', + startDate: new Date(2021, 1, 1, 10), + endDate: new Date(2021, 1, 1, 11), + allDay: true, + }, { + text: 'Brochure Design Review', + startDate: new Date(2021, 1, 1, 11, 30), + endDate: new Date(2021, 1, 1, 12, 30), + }], + views: ['timelineWeek'], + currentView: 'timelineWeek', + currentDate: new Date(2021, 1, 1), + firstDayOfWeek: 1, + startDayHour: 10, + endDayHour: 20, + cellDuration: 60, + height: 580, + }); + + await testScreenshot(page, 'timeline-overlapping-appointments.png'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/appointments/adaptive.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/appointments/adaptive.spec.ts new file mode 100644 index 000000000000..d9b8320f4d36 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/appointments/adaptive.spec.ts @@ -0,0 +1,138 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, setupTestPage, getContainerUrl } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = getContainerUrl(__dirname, '../../../../tests/container.html'); +const MOBILE_SIZE: [width: number, height: number] = [500, 700]; + +test.describe('Appointments with adaptive', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + ['week', 'month'].forEach((view) => { + test(`should correctly render appointment collectors (view:${view})`, async ({ page }) => { + await page.setViewportSize({ width: MOBILE_SIZE[0], height: MOBILE_SIZE[1] }); + + await createWidget(page, 'dxScheduler', { + dataSource: [ + { + startDate: '2023-12-20T10:00:00', + endDate: '2024-01-20T12:00:00', + allDay: true, + text: 'all-day #0 (long)', + }, + { + startDate: '2023-12-31T10:00:00', + endDate: '2024-01-06T12:00:00', + allDay: true, + text: 'all-day #1 (week-long)', + }, + { + startDate: '2024-01-01T10:00:00', + endDate: '2024-01-05T12:00:00', + allDay: true, + text: 'all-day #2', + }, + { + startDate: '2024-01-02T10:00:00', + endDate: '2024-01-04T12:00:00', + allDay: true, + text: 'all-day #3', + }, + { + startDate: '2024-01-03T10:00:00', + endDate: '2024-01-03T12:00:00', + allDay: true, + text: 'all-day #4 (single-day)', + }, + { + startDate: '2024-12-30T10:00:00', + endDate: '2024-01-20T12:00:00', + text: 'usual #0 (long)', + }, + { + startDate: '2024-01-03T01:30:00', + endDate: '2024-01-03T22:00:00', + text: 'usual #1 (day-long)', + }, + { + startDate: '2024-01-03T01:30:00', + endDate: '2024-01-03T02:30:00', + text: 'usual #2 (short)', + }, + { + startDate: '2024-01-03T02:30:00', + endDate: '2024-01-03T22:00:00', + text: 'usual #3 (day-long)', + }, + ], + adaptivityEnabled: true, + currentView: 'week', + currentDate: '2024-01-01T00:00:00', + }); + + await testScreenshot(page, `adaptive_appts_view-${view}.png`, { + element: page.locator('.dx-scheduler-work-space'), + }); + }); + }); + + test('should correctly render long appointments with disabled allDayPanel ()', async ({ page }) => { + await page.setViewportSize({ width: MOBILE_SIZE[0], height: MOBILE_SIZE[1] }); + + await createWidget(page, 'dxScheduler', { + dataSource: [ + { + startDate: '2023-12-20T00:00:00', + endDate: '2024-01-20T02:00:00', + text: '#0 (long)', + }, + { + startDate: '2023-12-31T00:00:00', + endDate: '2024-01-06T02:00:00', + text: '#1 (week-long)', + }, + { + startDate: '2024-01-01T00:00:00', + endDate: '2024-01-05T02:00:00', + text: '#2', + }, + { + startDate: '2024-01-02T00:00:00', + endDate: '2024-01-04T02:00:00', + text: '#3', + }, + { + startDate: '2024-01-03T00:00:00', + endDate: '2024-01-03T02:00:00', + text: '#4 (single-day)', + }, + { + startDate: '2024-01-03T01:30:00', + endDate: '2024-01-03T22:00:00', + text: '#5', + }, + { + startDate: '2024-01-03T01:30:00', + endDate: '2024-01-03T02:30:00', + text: '#6', + }, + { + startDate: '2024-01-03T02:30:00', + endDate: '2024-01-03T22:00:00', + text: '#7', + }, + ], + adaptivityEnabled: true, + allDayPanelMode: 'hidden', + showAllDayPanel: false, + currentView: 'week', + currentDate: '2024-01-01T00:00:00', + }); + + await testScreenshot(page, 'adaptive_long-appts-without-all-day-panel_view-week.png', { + element: page.locator('.dx-scheduler-work-space'), + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/appointments/allDay/allDay.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/appointments/allDay/allDay.spec.ts new file mode 100644 index 000000000000..20a42365d633 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/appointments/allDay/allDay.spec.ts @@ -0,0 +1,98 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, insertStylesheetRulesToPage, setupTestPage, getContainerUrl } from '../../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = getContainerUrl(__dirname, '../../../../../tests/container.html'); + +const data = [{ + text: '0', + startDate: new Date(2021, 3, 1), + endDate: new Date(2021, 3, 4), +}, { + text: '1', + startDate: new Date(2021, 3, 2), + endDate: new Date(2021, 3, 5, 0, 0, 1), +}, { + text: '2', + startDate: new Date(2021, 3, 2, 1), + endDate: new Date(2021, 3, 4, 23, 59), +}, { + text: '3 - Skip', + startDate: new Date(2021, 3, 3), + endDate: new Date(2021, 3, 4, 23, 59, 59), +}]; + +test.describe('Scheduler - All day appointments', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + test('it should skip weekend days in workWeek', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource: data, + views: [{ + type: 'workWeek', + intervalCount: 2, + startDate: new Date(2021, 2, 4), + }], + maxAppointmentsPerCell: 'unlimited', + currentView: 'workWeek', + currentDate: new Date(2021, 3, 5), + height: 300, + }); + + await testScreenshot(page, 'workweek_all-day_appointments_skip_weekend.png'); + }); + + test('it should skip weekend days in timelineWorkWeek', async ({ page }) => { + await insertStylesheetRulesToPage(page, '#container .dx-scheduler-cell-sizes-horizontal { width: 4px; }'); + + await createWidget(page, 'dxScheduler', { + width: 970, + height: 300, + dataSource: data, + cellDuration: 60, + views: [{ + type: 'timelineWorkWeek', + intervalCount: 2, + }], + maxAppointmentsPerCell: 'unlimited', + currentView: 'timelineWorkWeek', + currentDate: new Date(2021, 3, 2), + }); + + await testScreenshot(page, 'timeline-work-week_all-day_appointments_skip_weekend.png'); + }); + + test('should work correctly for unsorted dataSource', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource: [{ + id: 3, + text: '3', + startDate: new Date('2020-11-23T00:00:00.000'), + endDate: new Date('2020-11-28T00:00:00.000'), + allDay: true, + }, { + id: 5, + text: '5', + startDate: new Date('2020-11-27T00:00:00.000'), + endDate: new Date('2020-11-27T00:00:00.000'), + allDay: true, + }, { + id: 1, + text: '1', + startDate: new Date('2020-11-25T22:20:00.000'), + endDate: new Date('2020-11-26T12:30:00.000'), + }], + views: ['week'], + currentView: 'week', + showAllDayPanel: true, + currentDate: new Date(2020, 10, 25), + height: 600, + }); + + await testScreenshot(page, 'allDay-unsorted-datasource.png', { + element: page.locator('.dx-scheduler-work-space'), + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/appointments/allDay/allDayEndsAtMidnight.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/appointments/allDay/allDayEndsAtMidnight.spec.ts new file mode 100644 index 000000000000..737889ce1aed --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/appointments/allDay/allDayEndsAtMidnight.spec.ts @@ -0,0 +1,110 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, setupTestPage, getContainerUrl } from '../../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = getContainerUrl(__dirname, '../../../../../tests/container.html'); + +const VIEW_RANGE_HOURS = [ + [undefined, undefined], + [6, undefined], + [undefined, 18], + [6, 18], +]; + +const setViewOptions = (startDayHour: number | undefined, endDayHour: number | undefined) => { + const viewOptions: { startDayHour?: number; endDayHour?: number } = {}; + if (startDayHour) viewOptions.startDayHour = startDayHour; + if (endDayHour) viewOptions.endDayHour = endDayHour; + + return viewOptions; +}; + +test.describe('Scheduler - All day appointments ends at midnight', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + ['week', 'month', 'timelineDay', 'timelineMonth'].forEach((view) => { + VIEW_RANGE_HOURS.forEach(([startDayHour, endDayHour]) => { + test( + `all-day appointment ends at midnight. view=${view}, startDayHour=${startDayHour}, endDayHour=${endDayHour} (T1128938)`, + async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource: [ + { + text: 'One day', + startDate: '2023-01-01T00:00:00', + endDate: '2023-01-01T00:00:00', + allDay: true, + }, + { + text: 'Two days', + startDate: '2023-01-01T00:00:00', + endDate: '2023-01-02T00:00:00', + allDay: true, + }, + ], + dateSerializationFormat: 'yyyy-MM-ddTHH:mm:ss', + currentView: view, + currentDate: '2023-01-01T00:00:00', + height: 800, + cellDuration: 360, + maxAppointmentsPerCell: 2, + ...setViewOptions(startDayHour, endDayHour), + }); + + await testScreenshot( + page, + `midnight_all-day-appt_view=${view}_start=${startDayHour}_end=${endDayHour}.png`, + { element: page.locator('.dx-scheduler-work-space') }, + ); + }, + ); + }); + }); + + [ + 'timelineDay', + 'timelineMonth', + ].forEach((view) => { + test(`all-day appointment ends at midnight of the next month. view=${view} (T1122382)`, async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource: [ + { + text: 'Two days', + startDate: '2022-12-31T00:00:00', + endDate: '2023-01-01T00:00:00', + allDay: true, + }, + ], + dateSerializationFormat: 'yyyy-MM-ddTHH:mm:ss', + currentView: view, + currentDate: '2022-12-31T00:00:00', + height: 800, + }); + + await page.evaluate((scrollDate) => { + ($('#container') as any).dxScheduler('scrollTo', new Date(scrollDate)); + }, '2022-12-31T23:59:00'); + + await testScreenshot( + page, + `midnight-next-month_all-day-appt_view=${view}_first.png`, + { element: page.locator('.dx-scheduler-work-space') }, + ); + + await page.locator('.dx-scheduler-navigator-next').click(); + await page.waitForTimeout(100); + + await page.evaluate((scrollDate) => { + ($('#container') as any).dxScheduler('scrollTo', new Date(scrollDate)); + }, '2023-01-01T00:01:00'); + + await testScreenshot( + page, + `midnight-next-month_all-day-appt_view=${view}_second.png`, + { element: page.locator('.dx-scheduler-work-space') }, + ); + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/appointments/appointment_collector.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/appointments/appointment_collector.spec.ts new file mode 100644 index 000000000000..a110a9e9ba66 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/appointments/appointment_collector.spec.ts @@ -0,0 +1,85 @@ +import { test, expect } from '@playwright/test'; +import { setupTestPage, getContainerUrl } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = getContainerUrl(__dirname, '../../../../tests/container.html'); + +const dataSource = [{ + text: 'appointment1', + startDate: new Date('2021-04-02T07:30:00.000Z'), + endDate: new Date('2021-04-02T09:00:00.000Z'), +}, { + text: 'appointment2', + startDate: new Date('2021-04-02T07:35:00.000Z'), + endDate: new Date('2021-04-02T09:05:00.000Z'), +}]; +const config = { + dataSource, + timeZone: 'America/Los_Angeles', + currentDate: new Date(2021, 3, 2), + maxAppointmentsPerCell: 1, +}; + +test.describe('Appointment Editing', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + ['day', 'week', 'month', 'timelineDay', 'timelineWeek', 'timelineMonth'].forEach((view) => { + test(`appointmentCollectorTemplate should render with appointments data on ${view} view`, async ({ page }) => { + await page.evaluate(({ cfg, ds, viewName }) => { + const $scheduler = ($('#container') as any); + const devExpress = (window as any).DevExpress; + + $scheduler.dxScheduler({ + ...cfg, + dataSource: ds, + views: [viewName], + currentView: viewName, + appointmentCollectorTemplate(data: any) { + (window as any).appointmentCollectorArgsData = data; + return document.createElement('div'); + }, + }); + devExpress.fx.off = true; + }, { cfg: config, ds: dataSource, viewName: view }); + + const renderedData = await page.evaluate(() => (window as any).appointmentCollectorArgsData); + + expect(renderedData).toEqual({ + appointmentCount: 1, + isCompact: ['day', 'week'].includes(view), + items: [dataSource[1]], + }); + }); + + test(`appointmentCollectorTemplate in view config should render with appointments data on ${view} view`, async ({ page }) => { + await page.evaluate(({ cfg, ds, viewName }) => { + const $scheduler = ($('#container') as any); + const devExpress = (window as any).DevExpress; + + $scheduler.dxScheduler({ + ...cfg, + dataSource: ds, + views: [{ + type: viewName, + appointmentCollectorTemplate(data: any) { + (window as any).appointmentCollectorArgsData = data; + return document.createElement('div'); + }, + }], + currentView: viewName, + }); + devExpress.fx.off = true; + }, { cfg: config, ds: dataSource, viewName: view }); + + const renderedData = await page.evaluate(() => (window as any).appointmentCollectorArgsData); + + expect(renderedData).toEqual({ + appointmentCount: 1, + isCompact: ['day', 'week'].includes(view), + items: [dataSource[1]], + }); + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/appointments/dependendOptions.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/appointments/dependendOptions.spec.ts new file mode 100644 index 000000000000..e8c2cbe88a5e --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/appointments/dependendOptions.spec.ts @@ -0,0 +1,39 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, setupTestPage, getContainerUrl } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = getContainerUrl(__dirname, '../../../../tests/container.html'); + +test.describe('Appointment dependend options', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + test('cellDuration (T1076138)', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource: [{ + text: 'test-appt', + startDate: new Date(2021, 3, 27, 10), + endDate: new Date(2021, 3, 27, 11, 20), + }], + views: ['day'], + currentView: 'day', + currentDate: new Date(2021, 3, 27), + startDayHour: 9, + endDayHour: 18, + width: 600, + height: 600, + cellDuration: 20, + }); + + const appointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'test-appt' }); + + await page.evaluate(() => { + ($('#container') as any).dxScheduler('option', 'cellDuration', 30); + }); + + const clientHeight = await appointment.evaluate((el) => el.clientHeight); + expect(clientHeight).toBeGreaterThanOrEqual(132); + expect(clientHeight).toBeLessThanOrEqual(133); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/appointments/displayArguments.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/appointments/displayArguments.spec.ts new file mode 100644 index 000000000000..e432f175135e --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/appointments/displayArguments.spec.ts @@ -0,0 +1,65 @@ +import { test, expect } from '@playwright/test'; +import { setupTestPage, getContainerUrl } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = getContainerUrl(__dirname, '../../../../tests/container.html'); + +test.describe('Display* arguments in appointment templates and events', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + [undefined, 'America/Los_Angeles'].forEach((timeZone) => { + test(`displayStartDate and displayEndDate arguments should be right with timeZone='${timeZone}'`, async ({ page }) => { + await page.evaluate(({ tz }) => { + const $scheduler = ($('#container') as any); + const devExpress = (window as any).DevExpress; + + $scheduler.dxScheduler({ + timeZone: tz, + dataSource: [], + views: ['day'], + currentView: 'day', + currentDate: new Date(2021, 1, 15), + startDayHour: 9, + height: 600, + + onAppointmentClick(model: any) { + const { displayStartDate, displayEndDate } = model.targetedAppointmentData; + (window as any).testDisplayValue = `${displayStartDate.toLocaleTimeString('en-US', { hour12: false })} ${displayEndDate.toLocaleTimeString('en-US', { hour12: false })}`; + }, + + appointmentTooltipTemplate(model: any) { + const { displayStartDate, displayEndDate } = model.targetedAppointmentData; + return `${displayStartDate.toLocaleTimeString('en-US', { hour12: false })} ${displayEndDate.toLocaleTimeString('en-US', { hour12: false })}`; + }, + + appointmentTemplate(model: any) { + const { displayStartDate, displayEndDate } = model.targetedAppointmentData; + return `${displayStartDate.toLocaleTimeString('en-US', { hour12: false })} ${displayEndDate.toLocaleTimeString('en-US', { hour12: false })}`; + }, + }); + devExpress.fx.off = true; + }, { tz: timeZone }); + + const etalon = '09:30:00 10:00:00'; + + const cell = page.locator('.dx-scheduler-date-table-row').nth(1).locator('.dx-scheduler-date-table-cell').nth(0); + await cell.dblclick(); + + const textEditor = page.locator('.dx-scheduler-appointment-popup .dx-textbox input'); + await textEditor.fill('text'); + await page.locator('.dx-popup-done').click(); + + const appointmentText = await page.locator('.dx-scheduler-appointment').nth(0).innerText(); + expect(appointmentText).toBe(etalon); + + await page.locator('.dx-scheduler-appointment').nth(0).click(); + const tooltipText = await page.locator('.dx-scheduler-appointment-tooltip-wrapper .dx-list-item').nth(0).innerText(); + expect(tooltipText).toBe(etalon); + + const testDisplayValue = await page.evaluate(() => (window as any).testDisplayValue); + expect(testDisplayValue).toBe(etalon); + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/appointments/legacyEditing.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/appointments/legacyEditing.spec.ts new file mode 100644 index 000000000000..da1c5b6a8df4 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/appointments/legacyEditing.spec.ts @@ -0,0 +1,119 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, setupTestPage, getContainerUrl } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = getContainerUrl(__dirname, '../../../../tests/container.html'); + +const SCHEDULER_SELECTOR = '#container'; +const INITIAL_APPOINTMENT_TITLE = 'appointment'; +const ADDITIONAL_TITLE_TEXT = '-updated'; +const UPDATED_APPOINTMENT_TITLE = `${INITIAL_APPOINTMENT_TITLE}${ADDITIONAL_TITLE_TEXT}`; + +test.describe('Appointment Editing', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + test('Should correctly update appointment if dataSource is a simple array', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource: [{ + id: 1, + text: INITIAL_APPOINTMENT_TITLE, + startDate: new Date(2021, 2, 29, 9, 30), + endDate: new Date(2021, 2, 29, 11, 30), + }], + views: ['day'], + currentView: 'day', + currentDate: new Date(2021, 2, 29), + startDayHour: 9, + endDayHour: 14, + height: 600, + editing: { legacyForm: true }, + }); + + const appointment = page.locator('.dx-scheduler-appointment').filter({ hasText: INITIAL_APPOINTMENT_TITLE }); + await appointment.dblclick(); + + const subjectInput = page.locator('.dx-popup-wrapper .dx-textbox input').first(); + await subjectInput.click(); + await subjectInput.fill(UPDATED_APPOINTMENT_TITLE); + + const inputValue = await subjectInput.inputValue(); + expect(inputValue).toBe(UPDATED_APPOINTMENT_TITLE); + + await page.locator('.dx-popup-done').click(); + + await expect(page.locator('.dx-scheduler-appointment').filter({ hasText: UPDATED_APPOINTMENT_TITLE })).toBeVisible(); + }); + + test('Should correctly update appointment if dataSource is a Store with key array', async ({ page }) => { + await page.evaluate(({ selector, title }) => { + const $scheduler = ($(selector) as any); + const devExpress = (window as any).DevExpress; + + $scheduler.dxScheduler({ + dataSource: new devExpress.data.DataSource({ + store: { + type: 'array', + key: 'id', + data: [{ + id: 1, + text: title, + startDate: new Date(2021, 2, 29, 9, 30), + endDate: new Date(2021, 2, 29, 11, 30), + }], + }, + }), + views: ['day'], + currentView: 'day', + currentDate: new Date(2021, 2, 29), + startDayHour: 9, + endDayHour: 14, + height: 600, + editing: { legacyForm: true }, + }).dxScheduler('instance'); + devExpress.fx.off = true; + }, { selector: SCHEDULER_SELECTOR, title: INITIAL_APPOINTMENT_TITLE }); + + const appointment = page.locator('.dx-scheduler-appointment').filter({ hasText: INITIAL_APPOINTMENT_TITLE }); + await appointment.dblclick(); + + const subjectInput = page.locator('.dx-popup-wrapper .dx-textbox input').first(); + await subjectInput.click(); + await subjectInput.fill(UPDATED_APPOINTMENT_TITLE); + + const inputValue = await subjectInput.inputValue(); + expect(inputValue).toBe(UPDATED_APPOINTMENT_TITLE); + + await page.locator('.dx-popup-done').click(); + + await expect(page.locator('.dx-scheduler-appointment').filter({ hasText: UPDATED_APPOINTMENT_TITLE })).toBeVisible(); + }); + + test('Appointment EditForm screenshot', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource: [{ + id: 1, + text: INITIAL_APPOINTMENT_TITLE, + startDate: new Date(2021, 2, 29, 9, 30), + endDate: new Date(2021, 2, 29, 11, 30), + }], + editing: { legacyForm: true }, + views: ['day'], + currentView: 'day', + currentDate: new Date(2021, 2, 29), + startDayHour: 9, + endDayHour: 14, + height: 600, + }); + + const appointment = page.locator('.dx-scheduler-appointment').filter({ hasText: INITIAL_APPOINTMENT_TITLE }); + await appointment.dblclick(); + + await testScreenshot(page, 'appointment-popup-screenshot.png', { + element: appointment, + }); + + await expect(page.locator('.dx-popup-wrapper')).toBeVisible(); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/appointments/maxAppointmentsPerCell/allDay.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/appointments/maxAppointmentsPerCell/allDay.spec.ts new file mode 100644 index 000000000000..f21ac69442c7 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/appointments/maxAppointmentsPerCell/allDay.spec.ts @@ -0,0 +1,106 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, setupTestPage, getContainerUrl } from '../../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = getContainerUrl(__dirname, '../../../../../tests/container.html'); + +test.describe('Scheduler: max appointments per cell: All day', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + ['auto', 'unlimited', 1, 3, 10].forEach((maxAppointmentsPerCellValue) => { + test(`All day appointments should have correct height in maxAppointmentsPerCell=${maxAppointmentsPerCellValue}`, async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource: [{ + text: 'test_26', + startDate: new Date(2021, 3, 26), + endDate: new Date(2021, 3, 26), + allDay: true, + }, { + text: 'test_27', + startDate: new Date(2021, 3, 27), + endDate: new Date(2021, 3, 27), + allDay: true, + }, { + text: 'test_27', + startDate: new Date(2021, 3, 27), + endDate: new Date(2021, 3, 27), + allDay: true, + }, { + text: 'test_28', + startDate: new Date(2021, 3, 28), + endDate: new Date(2021, 3, 28), + allDay: true, + }, { + text: 'test_28', + startDate: new Date(2021, 3, 28), + endDate: new Date(2021, 3, 28), + allDay: true, + }, { + text: 'test_28', + startDate: new Date(2021, 3, 28), + endDate: new Date(2021, 3, 28), + allDay: true, + }, { + text: 'test_29', + startDate: new Date(2021, 3, 29), + endDate: new Date(2021, 3, 29), + allDay: true, + }, { + text: 'test_29', + startDate: new Date(2021, 3, 29), + endDate: new Date(2021, 3, 29), + allDay: true, + }, { + text: 'test_29', + startDate: new Date(2021, 3, 29), + endDate: new Date(2021, 3, 29), + allDay: true, + }, { + text: 'test_29', + startDate: new Date(2021, 3, 29), + endDate: new Date(2021, 3, 29), + allDay: true, + }, { + text: 'test_30', + startDate: new Date(2021, 3, 30), + endDate: new Date(2021, 3, 30), + allDay: true, + }, { + text: 'test_30', + startDate: new Date(2021, 3, 30), + endDate: new Date(2021, 3, 30), + allDay: true, + }, { + text: 'test_30', + startDate: new Date(2021, 3, 30), + endDate: new Date(2021, 3, 30), + allDay: true, + }, { + text: 'test_30', + startDate: new Date(2021, 3, 30), + endDate: new Date(2021, 3, 30), + allDay: true, + }, { + text: 'test_30', + startDate: new Date(2021, 3, 30), + endDate: new Date(2021, 3, 30), + allDay: true, + }], + maxAppointmentsPerCell: maxAppointmentsPerCellValue, + views: ['week'], + currentView: 'week', + currentDate: new Date(2021, 3, 29), + startDayHour: 9, + allDayPanelMode: 'allDay', + }); + + await testScreenshot( + page, + `all-day-appointment-maxAppointmentsPerCell=${maxAppointmentsPerCellValue}.png`, + { element: page.locator('.dx-scheduler-all-day-appointments') }, + ); + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/appointments/maxAppointmentsPerCell/day.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/appointments/maxAppointmentsPerCell/day.spec.ts new file mode 100644 index 000000000000..1a7f84402e6d --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/appointments/maxAppointmentsPerCell/day.spec.ts @@ -0,0 +1,104 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, setupTestPage, getContainerUrl } from '../../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = getContainerUrl(__dirname, '../../../../../tests/container.html'); + +test.describe('Scheduler: max appointments per cell: Day', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + ['auto', 'unlimited', 3, 10].forEach((maxAppointmentsPerCellValue) => { + test(`Day appointments should have correct height in maxAppointmentsPerCell=${maxAppointmentsPerCellValue}`, async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource: [{ + text: 'test_1', + startDate: new Date(2021, 3, 27, 9), + endDate: new Date(2021, 3, 27, 9, 30), + }, { + text: 'test_2', + startDate: new Date(2021, 3, 27, 9, 30), + endDate: new Date(2021, 3, 27, 10), + }, { + text: 'test_3', + startDate: new Date(2021, 3, 27, 9, 30), + endDate: new Date(2021, 3, 27, 10), + }, { + text: 'test_4', + startDate: new Date(2021, 3, 27, 9, 30), + endDate: new Date(2021, 3, 27, 10), + }, { + text: 'test_5', + startDate: new Date(2021, 3, 27, 9, 30), + endDate: new Date(2021, 3, 27, 10), + }, { + text: 'test_6', + startDate: new Date(2021, 3, 27, 9, 30), + endDate: new Date(2021, 3, 27, 10), + }, { + text: 'test_7', + startDate: new Date(2021, 3, 27, 9, 30), + endDate: new Date(2021, 3, 27, 10), + }, { + text: 'test_8', + startDate: new Date(2021, 3, 27, 9, 30), + endDate: new Date(2021, 3, 27, 10), + }, { + text: 'test_9', + startDate: new Date(2021, 3, 27, 9, 30), + endDate: new Date(2021, 3, 27, 10), + }, { + text: 'test_10', + startDate: new Date(2021, 3, 27, 10), + endDate: new Date(2021, 3, 27, 11), + }, { + text: 'test_1', + startDate: new Date(2021, 3, 27, 9, 30), + endDate: new Date(2021, 3, 27, 10), + }, { + text: 'test_12', + startDate: new Date(2021, 3, 27, 9, 30), + endDate: new Date(2021, 3, 27, 10), + }, { + text: 'test_13', + startDate: new Date(2021, 3, 27, 9, 30), + endDate: new Date(2021, 3, 27, 10), + }, { + text: 'test_14', + startDate: new Date(2021, 3, 27, 9, 30), + endDate: new Date(2021, 3, 27, 10), + }, { + text: 'test_15', + startDate: new Date(2021, 3, 27, 10, 30), + endDate: new Date(2021, 3, 27, 11, 30), + }, { + text: 'test_16', + startDate: new Date(2021, 3, 27, 12), + endDate: new Date(2021, 3, 27, 12, 30), + }, { + text: 'test_17', + startDate: new Date(2021, 3, 27, 12), + endDate: new Date(2021, 3, 27, 14), + }, { + text: 'test_18', + startDate: new Date(2021, 3, 27, 12), + endDate: new Date(2021, 3, 27, 13, 30), + }], + maxAppointmentsPerCell: maxAppointmentsPerCellValue, + views: ['day'], + currentView: 'day', + currentDate: new Date(2021, 3, 27), + startDayHour: 9, + height: 700, + width: 500, + }); + + await testScreenshot( + page, + `day-appointment-maxAppointmentsPerCell=${maxAppointmentsPerCellValue}.png`, + { element: page.locator('.dx-scheduler-work-space') }, + ); + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/appointments/maxAppointmentsPerCell/month.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/appointments/maxAppointmentsPerCell/month.spec.ts new file mode 100644 index 000000000000..8bd02e9e6025 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/appointments/maxAppointmentsPerCell/month.spec.ts @@ -0,0 +1,106 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, setupTestPage, getContainerUrl } from '../../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = getContainerUrl(__dirname, '../../../../../tests/container.html'); + +test.describe('Scheduler: max appointments per cell: Month', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + ['auto', 'unlimited', 1, 3, 10].forEach((maxAppointmentsPerCellValue) => { + test(`Month appointments should have correct height in maxAppointmentsPerCell=${maxAppointmentsPerCellValue}`, async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource: [{ + text: 'test_26', + startDate: new Date(2021, 3, 26), + endDate: new Date(2021, 3, 26), + allDay: true, + }, { + text: 'test_27', + startDate: new Date(2021, 3, 27), + endDate: new Date(2021, 3, 27), + allDay: true, + }, { + text: 'test_27', + startDate: new Date(2021, 3, 27), + endDate: new Date(2021, 3, 27), + allDay: true, + }, { + text: 'test_28', + startDate: new Date(2021, 3, 28), + endDate: new Date(2021, 3, 28), + allDay: true, + }, { + text: 'test_28', + startDate: new Date(2021, 3, 28), + endDate: new Date(2021, 3, 28), + allDay: true, + }, { + text: 'test_28', + startDate: new Date(2021, 3, 28), + endDate: new Date(2021, 3, 28), + allDay: true, + }, { + text: 'test_29', + startDate: new Date(2021, 3, 29), + endDate: new Date(2021, 3, 29), + allDay: true, + }, { + text: 'test_29', + startDate: new Date(2021, 3, 29), + endDate: new Date(2021, 3, 29), + allDay: true, + }, { + text: 'test_29', + startDate: new Date(2021, 3, 29), + endDate: new Date(2021, 3, 29), + allDay: true, + }, { + text: 'test_29', + startDate: new Date(2021, 3, 29), + endDate: new Date(2021, 3, 29), + allDay: true, + }, { + text: 'test_30', + startDate: new Date(2021, 3, 30), + endDate: new Date(2021, 3, 30), + allDay: true, + }, { + text: 'test_30', + startDate: new Date(2021, 3, 30), + endDate: new Date(2021, 3, 30), + allDay: true, + }, { + text: 'test_30', + startDate: new Date(2021, 3, 30), + endDate: new Date(2021, 3, 30), + allDay: true, + }, { + text: 'test_30', + startDate: new Date(2021, 3, 30), + endDate: new Date(2021, 3, 30), + allDay: true, + }, { + text: 'test_30', + startDate: new Date(2021, 3, 30), + endDate: new Date(2021, 3, 30), + allDay: true, + }], + maxAppointmentsPerCell: maxAppointmentsPerCellValue, + views: ['month'], + currentView: 'month', + currentDate: new Date(2021, 3, 29), + startDayHour: 9, + height: 700, + }); + + await testScreenshot( + page, + `month-appointment-maxAppointmentsPerCell=${maxAppointmentsPerCellValue}.png`, + { element: page.locator('.dx-scheduler-work-space') }, + ); + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/appointments/maxAppointmentsPerCell/timeline.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/appointments/maxAppointmentsPerCell/timeline.spec.ts new file mode 100644 index 000000000000..7cb57d4de91e --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/appointments/maxAppointmentsPerCell/timeline.spec.ts @@ -0,0 +1,103 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, setupTestPage, getContainerUrl } from '../../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = getContainerUrl(__dirname, '../../../../../tests/container.html'); + +test.describe('Scheduler: max appointments per cell: Timeline', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + ['auto', 'unlimited', 1, 3, 10, 20].forEach((maxAppointmentsPerCellValue) => { + test(`Timeline appointments should have correct height in maxAppointmentsPerCell=${maxAppointmentsPerCellValue}`, async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource: [{ + text: 'test_1', + startDate: new Date(2021, 3, 27, 9), + endDate: new Date(2021, 3, 27, 9, 30), + }, { + text: 'test_2', + startDate: new Date(2021, 3, 27, 9, 30), + endDate: new Date(2021, 3, 27, 10), + }, { + text: 'test_3', + startDate: new Date(2021, 3, 27, 9, 30), + endDate: new Date(2021, 3, 27, 10), + }, { + text: 'test_4', + startDate: new Date(2021, 3, 27, 9, 30), + endDate: new Date(2021, 3, 27, 10), + }, { + text: 'test_5', + startDate: new Date(2021, 3, 27, 9, 30), + endDate: new Date(2021, 3, 27, 10), + }, { + text: 'test_6', + startDate: new Date(2021, 3, 27, 9, 30), + endDate: new Date(2021, 3, 27, 10), + }, { + text: 'test_7', + startDate: new Date(2021, 3, 27, 9, 30), + endDate: new Date(2021, 3, 27, 10), + }, { + text: 'test_8', + startDate: new Date(2021, 3, 27, 9, 30), + endDate: new Date(2021, 3, 27, 10), + }, { + text: 'test_9', + startDate: new Date(2021, 3, 27, 9, 30), + endDate: new Date(2021, 3, 27, 10), + }, { + text: 'test_10', + startDate: new Date(2021, 3, 27, 10), + endDate: new Date(2021, 3, 27, 11), + }, { + text: 'test_1', + startDate: new Date(2021, 3, 27, 9, 30), + endDate: new Date(2021, 3, 27, 10), + }, { + text: 'test_12', + startDate: new Date(2021, 3, 27, 9, 30), + endDate: new Date(2021, 3, 27, 10), + }, { + text: 'test_13', + startDate: new Date(2021, 3, 27, 9, 30), + endDate: new Date(2021, 3, 27, 10), + }, { + text: 'test_14', + startDate: new Date(2021, 3, 27, 9, 30), + endDate: new Date(2021, 3, 27, 10), + }, { + text: 'test_15', + startDate: new Date(2021, 3, 27, 10, 30), + endDate: new Date(2021, 3, 27, 11, 30), + }, { + text: 'test_16', + startDate: new Date(2021, 3, 27, 12), + endDate: new Date(2021, 3, 27, 12, 30), + }, { + text: 'test_17', + startDate: new Date(2021, 3, 27, 12), + endDate: new Date(2021, 3, 27, 14), + }, { + text: 'test_18', + startDate: new Date(2021, 3, 27, 12), + endDate: new Date(2021, 3, 27, 13, 30), + }], + maxAppointmentsPerCell: maxAppointmentsPerCellValue, + views: ['timelineDay'], + currentView: 'timelineDay', + currentDate: new Date(2021, 3, 27), + startDayHour: 9, + height: 700, + }); + + await testScreenshot( + page, + `timeline-appointment-maxAppointmentsPerCell=${maxAppointmentsPerCellValue}.png`, + { element: page.locator('.dx-scheduler-work-space') }, + ); + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/appointments/maxAppointmentsPerCell/week.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/appointments/maxAppointmentsPerCell/week.spec.ts new file mode 100644 index 000000000000..8fcea5b4d2db --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/appointments/maxAppointmentsPerCell/week.spec.ts @@ -0,0 +1,103 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, setupTestPage, getContainerUrl } from '../../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = getContainerUrl(__dirname, '../../../../../tests/container.html'); + +test.describe('Scheduler: max appointments per cell: Week', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + ['auto', 'unlimited', 3, 10].forEach((maxAppointmentsPerCellValue) => { + test(`Week appointments should have correct height in maxAppointmentsPerCell=${maxAppointmentsPerCellValue}`, async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource: [{ + text: 'test_1', + startDate: new Date(2021, 3, 27, 9), + endDate: new Date(2021, 3, 27, 9, 30), + }, { + text: 'test_2', + startDate: new Date(2021, 3, 27, 9, 30), + endDate: new Date(2021, 3, 27, 10), + }, { + text: 'test_3', + startDate: new Date(2021, 3, 27, 9, 30), + endDate: new Date(2021, 3, 27, 10), + }, { + text: 'test_4', + startDate: new Date(2021, 3, 27, 9, 30), + endDate: new Date(2021, 3, 27, 10), + }, { + text: 'test_5', + startDate: new Date(2021, 3, 27, 9, 30), + endDate: new Date(2021, 3, 27, 10), + }, { + text: 'test_6', + startDate: new Date(2021, 3, 27, 9, 30), + endDate: new Date(2021, 3, 27, 10), + }, { + text: 'test_7', + startDate: new Date(2021, 3, 27, 9, 30), + endDate: new Date(2021, 3, 27, 10), + }, { + text: 'test_8', + startDate: new Date(2021, 3, 27, 9, 30), + endDate: new Date(2021, 3, 27, 10), + }, { + text: 'test_9', + startDate: new Date(2021, 3, 27, 9, 30), + endDate: new Date(2021, 3, 27, 10), + }, { + text: 'test_10', + startDate: new Date(2021, 3, 27, 10), + endDate: new Date(2021, 3, 27, 11), + }, { + text: 'test_1', + startDate: new Date(2021, 3, 27, 9, 30), + endDate: new Date(2021, 3, 27, 10), + }, { + text: 'test_12', + startDate: new Date(2021, 3, 27, 9, 30), + endDate: new Date(2021, 3, 27, 10), + }, { + text: 'test_13', + startDate: new Date(2021, 3, 27, 9, 30), + endDate: new Date(2021, 3, 27, 10), + }, { + text: 'test_14', + startDate: new Date(2021, 3, 27, 9, 30), + endDate: new Date(2021, 3, 27, 10), + }, { + text: 'test_15', + startDate: new Date(2021, 3, 27, 10, 30), + endDate: new Date(2021, 3, 27, 11, 30), + }, { + text: 'test_16', + startDate: new Date(2021, 3, 27, 12), + endDate: new Date(2021, 3, 27, 12, 30), + }, { + text: 'test_17', + startDate: new Date(2021, 3, 27, 12), + endDate: new Date(2021, 3, 27, 14), + }, { + text: 'test_18', + startDate: new Date(2021, 3, 27, 12), + endDate: new Date(2021, 3, 27, 13, 30), + }], + maxAppointmentsPerCell: maxAppointmentsPerCellValue, + views: ['week'], + currentView: 'week', + currentDate: new Date(2021, 3, 29), + startDayHour: 9, + height: 700, + }); + + await testScreenshot( + page, + `week-appointment-maxAppointmentsPerCell=${maxAppointmentsPerCellValue}.png`, + { element: page.locator('.dx-scheduler-work-space') }, + ); + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/appointments/multiday.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/appointments/multiday.spec.ts new file mode 100644 index 000000000000..cd9f5c8ed323 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/appointments/multiday.spec.ts @@ -0,0 +1,297 @@ +import { test, expect } from '@playwright/test'; +import type { Page, Locator } from '@playwright/test'; +import { createWidget, setupTestPage, getContainerUrl } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = getContainerUrl(__dirname, '../../../../tests/container.html'); + +const getAppointment = (page: Page, title: string, index = 0): Locator => page.locator('.dx-scheduler-appointment').filter({ hasText: title }).nth(index); + +const checkAllDayAppointment = async ( + page: Page, + title: string, + index: number, + reduceType: 'head' | 'body' | 'tail' | undefined, + width: number, +): Promise => { + const appointment = getAppointment(page, title, index); + const isReduced = reduceType !== undefined; + + const hasReducedIcon = await appointment.locator('.dx-scheduler-appointment-reduced-icon').count(); + expect(hasReducedIcon > 0).toBe(isReduced); + + const isHead = await appointment.evaluate((el) => el.classList.contains('dx-scheduler-appointment-head')); + expect(isHead).toBe(reduceType === 'head'); + + const isBody = await appointment.evaluate((el) => el.classList.contains('dx-scheduler-appointment-body')); + expect(isBody).toBe(reduceType === 'body'); + + const isTail = await appointment.evaluate((el) => el.classList.contains('dx-scheduler-appointment-tail')); + expect(isTail).toBe(reduceType === 'tail'); + + const isAllDay = await appointment.evaluate((el) => el.classList.contains('dx-scheduler-all-day-appointment')); + expect(isAllDay).toBeTruthy(); + + const clientWidth = await appointment.evaluate((el) => el.clientWidth); + expect(clientWidth).toBeGreaterThanOrEqual(width - 1); + expect(clientWidth).toBeLessThanOrEqual(width + 1); +}; + +const checkRegularAppointment = async ( + page: Page, + title: string, + index: number, + reduceType: 'head' | 'body' | 'tail' | undefined, + height: number, +): Promise => { + const appointment = getAppointment(page, title, index); + const isReduced = reduceType !== undefined; + + const hasReducedIcon = await appointment.locator('.dx-scheduler-appointment-reduced-icon').count(); + expect(hasReducedIcon > 0).toBe(isReduced); + + const isHead = await appointment.evaluate((el) => el.classList.contains('dx-scheduler-appointment-head')); + expect(isHead).toBe(reduceType === 'head'); + + const isBody = await appointment.evaluate((el) => el.classList.contains('dx-scheduler-appointment-body')); + expect(isBody).toBe(reduceType === 'body'); + + const isTail = await appointment.evaluate((el) => el.classList.contains('dx-scheduler-appointment-tail')); + expect(isTail).toBe(reduceType === 'tail'); + + const clientHeight = await appointment.evaluate((el) => el.clientHeight); + expect(clientHeight).toBeGreaterThanOrEqual(height - 1); + expect(clientHeight).toBeLessThanOrEqual(height + 1); +}; + +test.describe('Scheduler - Multiday appointments', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + test('it should render multi-day and multi-view appointments correctly if allDayPanelMode is "hidden"', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + width: 900, + height: 400, + dataSource: [{ + text: 'appt-00', + startDate: new Date(2021, 2, 22, 8), + endDate: new Date(2021, 2, 22, 10, 30), + }, { + text: 'appt-01', + startDate: new Date(2021, 2, 25, 9), + endDate: new Date(2021, 3, 6, 8, 30), + }], + views: ['week', 'month', 'timelineMonth'], + currentView: 'week', + currentDate: new Date(2021, 2, 21), + startDayHour: 8, + endDayHour: 10, + allDayPanelMode: 'hidden', + }); + + let appointmentCount = await page.locator('.dx-scheduler-appointment').count(); + expect(appointmentCount).toBe(4); + + await checkRegularAppointment(page, 'appt-00', 0, undefined, 200); + await checkRegularAppointment(page, 'appt-01', 0, 'head', 100); + for (let i = 1; i < appointmentCount - 2; i += 1) { + await checkRegularAppointment(page, 'appt-01', i, 'body', 200); + } + + await page.locator('.dx-scheduler-navigator-next').click(); + + appointmentCount = await page.locator('.dx-scheduler-appointment').count(); + expect(appointmentCount).toBe(7); + + for (let i = 0; i < appointmentCount; i += 1) { + await checkRegularAppointment(page, 'appt-01', i, 'body', 200); + } + + await page.locator('.dx-scheduler-navigator-next').click(); + + appointmentCount = await page.locator('.dx-scheduler-appointment').count(); + expect(appointmentCount).toBe(3); + await checkRegularAppointment(page, 'appt-01', 0, 'body', 200); + await checkRegularAppointment(page, 'appt-01', 1, 'body', 200); + await checkRegularAppointment(page, 'appt-01', 2, 'tail', 50); + }); + + test('it should render all-day appointments if allDayPanelMode is "all"', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + width: 900, + height: 400, + dataSource: [{ + text: 'appt-00', + startDate: new Date(2021, 2, 22, 8), + endDate: new Date(2021, 2, 22, 10, 30), + allDay: true, + }, { + text: 'appt-01', + startDate: new Date(2021, 2, 25, 9), + endDate: new Date(2021, 3, 6, 8, 30), + }], + views: ['week', 'month', 'timelineMonth'], + currentView: 'week', + currentDate: new Date(2021, 2, 21), + startDayHour: 8, + endDayHour: 10, + allDayPanelMode: 'all', + }); + + let appointmentCount = await page.locator('.dx-scheduler-appointment').count(); + expect(appointmentCount).toBe(2); + await checkAllDayAppointment(page, 'appt-00', 0, undefined, 109); + await checkAllDayAppointment(page, 'appt-01', 0, 'head', 337); + + await page.locator('.dx-scheduler-navigator-next').click(); + + appointmentCount = await page.locator('.dx-scheduler-appointment').count(); + expect(appointmentCount).toBe(1); + await checkAllDayAppointment(page, 'appt-01', 0, 'body', 793); + + await page.locator('.dx-scheduler-navigator-next').click(); + + appointmentCount = await page.locator('.dx-scheduler-appointment').count(); + expect(appointmentCount).toBe(1); + await checkAllDayAppointment(page, 'appt-01', 0, 'tail', 337); + }); + + test('it should render all-day and multi-day appointments if allDayPanelMode is "allDay"', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + width: 900, + height: 400, + dataSource: [{ + text: 'allDay', + startDate: new Date(2021, 2, 22), + allDay: true, + }, { + text: 'multiDay', + startDate: new Date(2021, 2, 22, 8), + endDate: new Date(2021, 2, 25, 9, 30), + }], + views: ['week', 'month', 'timelineMonth'], + currentView: 'week', + currentDate: new Date(2021, 2, 21), + startDayHour: 8, + endDayHour: 10, + allDayPanelMode: 'allDay', + }); + + expect(await page.locator('.dx-scheduler-appointment').count()).toBe(5); + + await checkAllDayAppointment(page, 'allDay', 0, undefined, 117); + await checkRegularAppointment(page, 'multiDay', 0, 'head', 151); + await checkRegularAppointment(page, 'multiDay', 1, 'body', 151); + await checkRegularAppointment(page, 'multiDay', 2, 'body', 151); + await checkRegularAppointment(page, 'multiDay', 3, 'tail', 113); + }); + + test('it should correctly change allDayPanelOption at runtime', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + width: 800, + height: 600, + dataSource: [ + { + text: 'allDay', + startDate: new Date(2021, 2, 22), + allDay: true, + }, + { + text: 'multiDay', + startDate: new Date(2021, 2, 22, 8), + endDate: new Date(2021, 2, 25, 9, 30), + }], + views: ['week', 'workWeek'], + currentView: 'week', + currentDate: new Date(2021, 2, 22), + maxAppointmentsPerCell: 2, + startDayHour: 8, + endDayHour: 12, + }); + + expect(await page.locator('.dx-scheduler-appointment').count()).toBe(2); + await checkAllDayAppointment(page, 'allDay', 0, undefined, 103); + await checkAllDayAppointment(page, 'multiDay', 0, undefined, 417); + + await page.evaluate(() => { + ($('#container') as any).dxScheduler('option', 'allDayPanelMode', 'allDay'); + }); + expect(await page.locator('.dx-scheduler-appointment').count()).toBe(5); + await checkAllDayAppointment(page, 'allDay', 0, undefined, 103); + await checkRegularAppointment(page, 'multiDay', 0, 'head', 303); + await checkRegularAppointment(page, 'multiDay', 1, 'body', 303); + await checkRegularAppointment(page, 'multiDay', 2, 'body', 303); + await checkRegularAppointment(page, 'multiDay', 3, 'tail', 113); + + await page.evaluate(() => { + ($('#container') as any).dxScheduler('option', 'allDayPanelMode', 'hidden'); + }); + expect(await page.locator('.dx-scheduler-appointment').count()).toBe(5); + await expect(page.locator('.dx-scheduler-all-day-table-cell')).toHaveCount(0); + await checkRegularAppointment(page, 'allDay', 0, undefined, 303); + await checkRegularAppointment(page, 'multiDay', 0, 'head', 303); + await checkRegularAppointment(page, 'multiDay', 1, 'body', 303); + await checkRegularAppointment(page, 'multiDay', 2, 'body', 303); + await checkRegularAppointment(page, 'multiDay', 3, 'tail', 113); + + await page.evaluate(() => { + ($('#container') as any).dxScheduler('option', 'allDayPanelMode', 'allDay'); + }); + expect(await page.locator('.dx-scheduler-appointment').count()).toBe(5); + await checkAllDayAppointment(page, 'allDay', 0, undefined, 103); + await checkRegularAppointment(page, 'multiDay', 0, 'head', 303); + await checkRegularAppointment(page, 'multiDay', 1, 'body', 303); + await checkRegularAppointment(page, 'multiDay', 2, 'body', 303); + await checkRegularAppointment(page, 'multiDay', 3, 'tail', 113); + + await page.evaluate(() => { + ($('#container') as any).dxScheduler('option', 'allDayPanelMode', 'all'); + }); + expect(await page.locator('.dx-scheduler-appointment').count()).toBe(2); + await checkAllDayAppointment(page, 'allDay', 0, undefined, 103); + await checkAllDayAppointment(page, 'multiDay', 0, undefined, 417); + }); + + test('it should correctly handle allDayPanelMode for the workspace', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + width: 900, + height: 400, + dataSource: [{ + text: 'allDay', + startDate: new Date(2021, 2, 22), + allDay: true, + }, { + text: 'multiDay', + startDate: new Date(2021, 2, 22, 8), + endDate: new Date(2021, 2, 25, 9, 30), + }], + views: [ + 'week', + { + type: 'week', + name: 'weekAllDay', + allDayPanelMode: 'allDay', + }, + ], + currentView: 'week', + currentDate: new Date(2021, 2, 21), + startDayHour: 8, + endDayHour: 10, + }); + + expect(await page.locator('.dx-scheduler-appointment').count()).toBe(2); + + await checkAllDayAppointment(page, 'allDay', 0, undefined, 109); + await checkAllDayAppointment(page, 'multiDay', 0, undefined, 451); + + await page.locator('.dx-tabs-item').filter({ hasText: 'weekAllDay' }).click(); + expect(await page.locator('.dx-scheduler-appointment').count()).toBe(5); + + await checkAllDayAppointment(page, 'allDay', 0, undefined, 109); + await checkRegularAppointment(page, 'multiDay', 0, 'head', 200); + await checkRegularAppointment(page, 'multiDay', 1, 'body', 200); + await checkRegularAppointment(page, 'multiDay', 2, 'body', 200); + await checkRegularAppointment(page, 'multiDay', 3, 'tail', 150); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/appointments/multiday_screenshot.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/appointments/multiday_screenshot.spec.ts new file mode 100644 index 000000000000..1a471c03c3c0 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/appointments/multiday_screenshot.spec.ts @@ -0,0 +1,41 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, setupTestPage, getContainerUrl } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = getContainerUrl(__dirname, '../../../../tests/container.html'); + +test.describe('Scheduler - Multiday appointments (screenshot)', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + [ + 'week', + 'month', + 'timelineMonth', + ].forEach((currentView) => { + test(`it should not cut multiday appointment in ${currentView} view`, async ({ page }) => { + await createWidget( + page, + 'dxScheduler', + { + width: 900, + height: 400, + dataSource: [{ + text: 'Website Re-Design Plan', + startDate: new Date(2021, 2, 28, 8), + endDate: new Date(2021, 3, 4, 8), + }], + views: ['week', 'month', 'timelineMonth'], + currentView, + currentDate: new Date(2021, 3, 4), + startDayHour: 12, + }, + ); + + await testScreenshot(page, `multiday-appointment_${currentView}.png`, { + element: page.locator('#container'), + }); + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/appointments/onAppointmentDeleting.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/appointments/onAppointmentDeleting.spec.ts new file mode 100644 index 000000000000..289a145bd0e5 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/appointments/onAppointmentDeleting.spec.ts @@ -0,0 +1,91 @@ +import { test, expect } from '@playwright/test'; +import { setupTestPage, getContainerUrl } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = getContainerUrl(__dirname, '../../../../tests/container.html'); + +const data = [ + { + text: 'Brochure Design Review', + startDate: new Date(2021, 3, 27, 1, 30), + endDate: new Date(2021, 3, 27, 2, 30), + }, +]; + +test.describe('onAppointmentDeleting event', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + [{ + cancel: false, + expectedCount: 0, + }, { + cancel: true, + expectedCount: 1, + }].forEach(({ cancel, expectedCount }) => { + test(`UI behaviour should be valid in case argument pass boolean value, e.cancel=${cancel}`, async ({ page }) => { + await page.evaluate(({ appointmentData, cancelValue }) => { + const $scheduler = ($('#container') as any); + const devExpress = (window as any).DevExpress; + + $scheduler.dxScheduler({ + dataSource: appointmentData, + views: ['day'], + currentView: 'day', + currentDate: new Date(2021, 3, 27), + startDayHour: 1, + endDayHour: 7, + height: 600, + cellDuration: 30, + onAppointmentDeleting(e: any) { + e.cancel = cancelValue; + }, + }); + devExpress.fx.off = true; + }, { appointmentData: data, cancelValue: cancel }); + + const appointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'Brochure Design Review' }); + await appointment.click(); + + await expect(page.locator('.dx-scheduler-appointment-tooltip-wrapper')).toBeVisible(); + + await page.locator('.dx-scheduler-appointment-tooltip-wrapper .dx-tooltip-appointment-item-delete-button').click(); + + await expect(page.locator('.dx-scheduler-appointment')).toHaveCount(expectedCount); + }); + + test(`UI behaviour should be valid in case argument pass Promise resolved, e.cancel=${cancel}`, async ({ page }) => { + await page.evaluate(({ appointmentData, cancelValue }) => { + const $scheduler = ($('#container') as any); + const devExpress = (window as any).DevExpress; + + $scheduler.dxScheduler({ + dataSource: appointmentData, + views: ['day'], + currentView: 'day', + currentDate: new Date(2021, 3, 27), + startDayHour: 1, + endDayHour: 7, + height: 600, + cellDuration: 30, + onAppointmentDeleting(e: any) { + e.cancel = new Promise((resolve) => { + resolve(cancelValue); + }); + }, + }); + devExpress.fx.off = true; + }, { appointmentData: data, cancelValue: cancel }); + + const appointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'Brochure Design Review' }); + await appointment.click(); + + await expect(page.locator('.dx-scheduler-appointment-tooltip-wrapper')).toBeVisible(); + + await page.locator('.dx-scheduler-appointment-tooltip-wrapper .dx-tooltip-appointment-item-delete-button').click(); + + await expect(page.locator('.dx-scheduler-appointment')).toHaveCount(expectedCount); + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/appointments/resources.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/appointments/resources.spec.ts new file mode 100644 index 000000000000..382409f411fc --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/appointments/resources.spec.ts @@ -0,0 +1,207 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, setupTestPage, getContainerUrl } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = getContainerUrl(__dirname, '../../../../tests/container.html'); + +const dataSource = [{ + text: 'test-appt-1', + priorityId: 1, + typeId: 2, + startDate: new Date('2021-05-26T06:45:00.000Z'), + endDate: new Date('2021-05-26T09:15:00.000Z'), +}, { + text: 'test-appt-2', + priorityId: 2, + typeId: 1, + startDate: new Date('2021-05-26T06:45:00.000Z'), + endDate: new Date('2021-05-26T09:15:00.000Z'), +}]; + +const priorityData = [{ + text: 'Low Priority', + id: 1, + color: 'rgb(252, 182, 94)', +}, { + text: 'High Priority', + id: 2, + color: 'rgb(225, 142, 146)', +}]; + +test.describe('Appointment resources', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + test('Resource color should be correct if group is set in "views"', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + height: 600, + dataSource, + views: [{ + type: 'workWeek', + startDayHour: 9, + endDayHour: 18, + groups: ['priorityId'], + }], + currentView: 'workWeek', + currentDate: new Date(2021, 4, 25), + resources: [{ + fieldExpr: 'priorityId', + allowMultiple: false, + dataSource: priorityData, + label: 'Priority', + }, { + fieldExpr: 'typeId', + allowMultiple: false, + dataSource: [{ + id: 1, + color: '#b6d623', + }, { + id: 2, + color: '#679ec5', + }], + }], + }); + + const appointment1 = page.locator('.dx-scheduler-appointment').filter({ hasText: 'test-appt-1' }); + const appointment2 = page.locator('.dx-scheduler-appointment').filter({ hasText: 'test-appt-2' }); + + const color1 = await appointment1.evaluate((el) => getComputedStyle(el).backgroundColor); + const color2 = await appointment2.evaluate((el) => getComputedStyle(el).backgroundColor); + + expect(color1).toBe(priorityData[0].color); + expect(color2).toBe(priorityData[1].color); + }); + + test('Scheduler should renders correctly if resource dataSource is not set', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + height: 600, + width: 800, + dataSource: [{ + text: 'Appt-1', + startDate: new Date(2021, 3, 27, 10), + endDate: new Date(2021, 3, 27, 12), + }, { + text: 'Appt-2', + startDate: new Date(2021, 3, 29, 11), + endDate: new Date(2021, 3, 29, 13), + }], + views: ['workWeek'], + currentView: 'workWeek', + currentDate: new Date(2021, 3, 26), + startDayHour: 9, + endDayHour: 14, + resources: [{ + fieldExpr: 'roomId', + label: 'Room', + }], + }); + + await expect(page.locator('.dx-scheduler-appointment').filter({ hasText: 'Appt-1' })).toBeVisible(); + await expect(page.locator('.dx-scheduler-appointment').filter({ hasText: 'Appt-2' })).toBeVisible(); + }); + + test('Resource with allowMultiple should be set correctly for new the appointment (T1075028)', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + views: ['day'], + currentView: 'day', + currentDate: new Date(2021, 3, 27), + startDayHour: 9, + endDayHour: 14, + resources: [{ + fieldExpr: 'test_Id', + allowMultiple: true, + dataSource: [{ + text: 'Test-0', + id: 1, + }, { + text: 'Test-1', + id: 2, + }], + label: 'MultipleResource', + }], + }); + + const cell = page.locator('.dx-scheduler-date-table-row').nth(2).locator('.dx-scheduler-date-table-cell').nth(0); + await cell.dblclick(); + + await expect(page.locator('.dx-scheduler-appointment-popup')).toBeVisible(); + + const resourceTagBox = page.locator('.dx-tagbox'); + await expect(resourceTagBox).toBeVisible(); + await resourceTagBox.click(); + + const tagBoxPopup = page.locator('.dx-tagbox-popup-wrapper'); + await expect(tagBoxPopup).toBeVisible(); + + await tagBoxPopup.locator('.dx-list-item').first().click(); + + const tags = page.locator('.dx-tagbox .dx-tag'); + await expect(tags).toHaveCount(1); + }); + + test('Resource color should be correct for the complex resource id without grouping', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + currentDate: new Date(2015, 6, 10), + views: ['week'], + currentView: 'week', + editing: true, + dataSource: [{ + text: 'a', + allDay: true, + startDate: new Date(2015, 6, 10, 0), + endDate: new Date(2015, 6, 10, 0, 30), + ownerId: { _value: 'guid-1' }, + }, { + text: 'b', + allDay: true, + startDate: new Date(2015, 6, 10, 0), + endDate: new Date(2015, 6, 10, 0, 30), + ownerId: { _value: 'guid-2' }, + }, { + text: 'c', + startDate: new Date(2015, 6, 10, 2), + endDate: new Date(2015, 6, 10, 2, 30), + ownerId: { _value: 'guid-3' }, + }], + resources: [ + { + field: 'ownerId', + dataSource: [ + { + id: { _value: 'guid-1' }, + text: 'one', + color: 'rgb(255, 0, 0)', + }, + { + id: { _value: 'guid-2' }, + text: 'two', + color: 'rgb(0, 128, 0)', + }, + { + id: { _value: 'guid-3' }, + text: 'three', + color: 'rgb(255, 255, 0)', + }, + ], + }, + ], + scrolling: { + orientation: 'vertical', + }, + height: 600, + }); + + const appointmentA = page.locator('.dx-scheduler-appointment').filter({ hasText: 'a' }); + const appointmentB = page.locator('.dx-scheduler-appointment').filter({ hasText: 'b' }); + const appointmentC = page.locator('.dx-scheduler-appointment').filter({ hasText: 'c' }); + + const colorA = await appointmentA.evaluate((el) => getComputedStyle(el).backgroundColor); + const colorB = await appointmentB.evaluate((el) => getComputedStyle(el).backgroundColor); + const colorC = await appointmentC.evaluate((el) => getComputedStyle(el).backgroundColor); + + expect(colorA).toBe('rgb(255, 0, 0)'); + expect(colorB).toBe('rgb(0, 128, 0)'); + expect(colorC).toBe('rgb(255, 255, 0)'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/appointments/timelineMonth.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/appointments/timelineMonth.spec.ts new file mode 100644 index 000000000000..59e8d0e5e6cf --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/appointments/timelineMonth.spec.ts @@ -0,0 +1,75 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, setupTestPage, getContainerUrl } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = getContainerUrl(__dirname, '../../../../tests/container.html'); + +test.describe('Appointments in TimelineMonth', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + test('Appointments should have correct order', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + currentDate: new Date(2016, 1, 2), + dataSource: [ + { + text: 'appt-01', + startDate: new Date(2016, 1, 1, 9, 0), + endDate: new Date(2016, 1, 1, 10, 30), + }, { + text: 'appt-02', + startDate: new Date(2016, 1, 1, 11, 30), + endDate: new Date(2016, 1, 1, 14, 15), + }, { + text: 'appt-03', + startDate: new Date(2016, 1, 1, 15, 15), + endDate: new Date(2016, 1, 1, 17, 15), + }, { + text: 'appt-04', + startDate: new Date(2016, 1, 1, 18, 45), + endDate: new Date(2016, 1, 1, 20, 15), + }, { + text: 'appt-05', + startDate: new Date(2016, 1, 2, 8, 15), + endDate: new Date(2016, 1, 2, 10, 45), + }, { + text: 'appt-06', + startDate: new Date(2016, 1, 2, 12, 0), + endDate: new Date(2016, 1, 2, 13, 45), + }, { + text: 'appt-07', + startDate: new Date(2016, 1, 2, 15, 30), + endDate: new Date(2016, 1, 2, 17, 30), + }, { + text: 'appt-08', + startDate: new Date(2016, 1, 3, 8, 15), + endDate: new Date(2016, 1, 3, 9, 0), + }, { + text: 'appt-09', + startDate: new Date(2016, 1, 3, 10, 0), + endDate: new Date(2016, 1, 3, 11, 15), + }, { + text: 'appt-10', + startDate: new Date(2016, 1, 3, 11, 45), + endDate: new Date(2016, 1, 3, 13, 45), + }, { + text: 'appt-11', + startDate: new Date(2016, 1, 3, 14, 0), + endDate: new Date(2016, 1, 3, 16, 45), + }, + ], + views: ['timelineMonth'], + currentView: 'timelineMonth', + maxAppointmentsPerCell: 'unlimited', + height: 505, + startDayHour: 8, + endDayHour: 20, + cellDuration: 60, + firstDayOfWeek: 0, + width: 800, + }); + + await testScreenshot(page, 'timelineMonth-appt-order.png'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/appointments/timelineWorkWeek.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/appointments/timelineWorkWeek.spec.ts new file mode 100644 index 000000000000..596872f8c1e9 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/appointments/timelineWorkWeek.spec.ts @@ -0,0 +1,102 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, setupTestPage, getContainerUrl } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = getContainerUrl(__dirname, '../../../../tests/container.html'); + +const CELL_WIDTH = 200; + +test.describe('Appointments in TimelineWorkWeek', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + test('Appointments should have correct width', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + currentDate: new Date(2024, 0, 29), + dataSource: [ + { + text: 'appt-01', + startDate: new Date(2024, 1, 1, 13, 0), + endDate: new Date(2024, 1, 6, 14, 0), + }, + { + text: 'appt-02', + startDate: new Date(2024, 1, 1, 13, 0), + endDate: new Date(2024, 1, 4, 14, 0), + }, + { + text: 'appt-03', + startDate: new Date(2024, 1, 1, 13, 0), + endDate: new Date(2024, 1, 6, 10, 0), + }, + { + text: 'appt-04', + startDate: new Date(2024, 1, 1, 13, 0), + endDate: new Date(2024, 1, 6, 18, 0), + }, + { + text: 'appt-05', + startDate: new Date(2024, 1, 1, 13, 0), + endDate: new Date(2024, 1, 3, 10, 0), + }, + { + text: 'appt-06', + startDate: new Date(2024, 1, 1, 13, 0), + endDate: new Date(2024, 1, 3, 18, 0), + }, + { + text: 'appt-07', + startDate: new Date(2024, 1, 4, 13, 0), + endDate: new Date(2024, 1, 6, 14, 0), + }, + { + text: 'appt-08', + startDate: new Date(2024, 1, 1, 10, 0), + endDate: new Date(2024, 1, 6, 14, 0), + }, + { + text: 'appt-09', + startDate: new Date(2024, 1, 1, 19, 0), + endDate: new Date(2024, 1, 6, 14, 0), + }, + { + text: 'appt-10', + startDate: new Date(2024, 1, 4, 10, 0), + endDate: new Date(2024, 1, 6, 14, 0), + }, + { + text: 'appt-11', + startDate: new Date(2024, 1, 4, 17, 0), + endDate: new Date(2024, 1, 6, 14, 0), + }, + ], + views: [{ + type: 'timelineWorkWeek', + intervalCount: 2, + maxAppointmentsPerCell: 'unlimited', + }], + currentView: 'timelineWorkWeek', + startDayHour: 12, + endDayHour: 16, + cellDuration: 60, + }); + + const getApptWidth = (title: string) => + page.locator('.dx-scheduler-appointment').filter({ hasText: title }).evaluate( + (el) => el.style.width, + ); + + expect(await getApptWidth('appt-01')).toBe(`${CELL_WIDTH * (3 + 4 * 2 + 2)}px`); + expect(await getApptWidth('appt-02')).toBe(`${CELL_WIDTH * (3 + 4)}px`); + expect(await getApptWidth('appt-03')).toBe(`${CELL_WIDTH * (3 + 4 * 2)}px`); + expect(await getApptWidth('appt-04')).toBe(`${CELL_WIDTH * (3 + 4 * 3)}px`); + expect(await getApptWidth('appt-05')).toBe(`${CELL_WIDTH * (3 + 4)}px`); + expect(await getApptWidth('appt-06')).toBe(`${CELL_WIDTH * (3 + 4)}px`); + expect(await getApptWidth('appt-07')).toBe(`${CELL_WIDTH * (4 + 2)}px`); + expect(await getApptWidth('appt-08')).toBe(`${CELL_WIDTH * (4 * 3 + 2)}px`); + expect(await getApptWidth('appt-09')).toBe(`${CELL_WIDTH * (4 * 2 + 2)}px`); + expect(await getApptWidth('appt-10')).toBe(`${CELL_WIDTH * (4 + 2)}px`); + expect(await getApptWidth('appt-11')).toBe(`${CELL_WIDTH * (4 + 2)}px`); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/appointments/workWeek/interval.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/appointments/workWeek/interval.spec.ts new file mode 100644 index 000000000000..805f839d4f9a --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/appointments/workWeek/interval.spec.ts @@ -0,0 +1,59 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, setupTestPage, getContainerUrl } from '../../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = getContainerUrl(__dirname, '../../../../../tests/container.html'); + +test.describe('Appointments with adaptive', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + test('Should correctly render scheduler in workWeek view with interval, skipping weekends (T1243027)', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource: [ + { + startDate: '2024-01-05T01:00:00', + endDate: '2024-01-07T01:00:00', + text: 'Ends in weekend', + color: 'red', + }, + { + startDate: '2024-01-07T01:00:00', + endDate: '2024-01-08T01:00:00', + text: 'Starts in weekend', + color: 'blue', + }, + { + startDate: '2024-01-05T01:00:00', + endDate: '2024-01-08T01:00:00', + text: 'Goes over weekend', + color: 'green', + }, + ], + views: [{ + name: 'myView', + type: 'workWeek', + allDayPanelMode: 'allDay', + intervalCount: 2, + maxAppointmentsPerCell: 'unlimited', + }], + currentView: 'myView', + currentDate: '2024-01-01', + height: 600, + resources: [{ + fieldExpr: 'color', + dataSource: [ + { id: 'red', color: 'red' }, + { id: 'blue', color: 'blue' }, + { id: 'green', color: 'green' }, + ], + label: 'Room', + }], + }); + + await testScreenshot(page, 'work_week_interval-2.png', { + element: page.locator('.dx-scheduler-work-space'), + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/cellsSelection/bothDirectionsVirtualScrolling.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/cellsSelection/bothDirectionsVirtualScrolling.spec.ts new file mode 100644 index 000000000000..9c8fd7243b6c --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/cellsSelection/bothDirectionsVirtualScrolling.spec.ts @@ -0,0 +1,94 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, getContainerUrl, setupTestPage } from '../../../../playwright-helpers'; + +const containerUrl = getContainerUrl(__dirname, '../../../../tests/container.html'); + +const SELECTED_CELL_CLASS = 'dx-state-focused'; + +const createSchedulerWidget = async (page: any, options = {}) => { + await createWidget(page, 'dxScheduler', { + dataSource: [], + views: ['week'], + currentView: 'week', + currentDate: new Date(2020, 8, 20), + cellDuration: 60, + height: 300, + width: 400, + scrolling: { mode: 'virtual' }, + resources: [{ + fieldExpr: 'resourceId0', + dataSource: [{ id: 0 }, { id: 1 }], + }], + ...options, + }); +}; + +const scrollTo = async (page: any, x: number, y: number) => { + await page.evaluate(({ scrollX, scrollY }: { scrollX: number; scrollY: number }) => { + const instance = ($('#container') as any).dxScheduler('instance'); + const scrollable = instance.getWorkSpaceScrollable(); + scrollable.scrollTo({ y: scrollY, x: scrollX }); + }, { scrollX: x, scrollY: y }); + await page.waitForTimeout(300); +}; + +const baseConfig = { + scrolling: { mode: 'virtual', orientation: 'both' }, + views: [{ type: 'week', intervalCount: 3 }], + currentView: 'week', +}; + +test.describe('Scheduler: Cells Selection in Both Directions Virtual Scrolling', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + test('Selected cells shouldn\'t disappear on scroll', async ({ page }) => { + await createSchedulerWidget(page, { ...baseConfig }); + + const firstCell = page.locator('.dx-scheduler-date-table-row').nth(0).locator('.dx-scheduler-date-table-cell').nth(0); + const secondCell = page.locator('.dx-scheduler-date-table-row').nth(0).locator('.dx-scheduler-date-table-cell').nth(1); + + await firstCell.dragTo(secondCell); + + let selectedCount = await page.locator(`.dx-scheduler-date-table-cell.${SELECTED_CELL_CLASS}`).count(); + expect(selectedCount).toBeGreaterThan(0); + + await scrollTo(page, 1000, 0); + selectedCount = await page.locator(`.dx-scheduler-date-table-cell.${SELECTED_CELL_CLASS}`).count(); + expect(selectedCount).toBe(0); + + await scrollTo(page, 0, 0); + selectedCount = await page.locator(`.dx-scheduler-date-table-cell.${SELECTED_CELL_CLASS}`).count(); + expect(selectedCount).toBeGreaterThan(0); + }); + + test('Selection should work in month view', async ({ page }) => { + await createSchedulerWidget(page, { + ...baseConfig, + views: [{ type: 'month', groupOrientation: 'horizontal' }], + currentView: 'month', + groups: ['resourceId0'], + resources: [{ + fieldExpr: 'resourceId0', + dataSource: [{ id: 0 }, { id: 1 }, { id: 2 }, { id: 3 }], + }], + }); + + const firstCell = page.locator('.dx-scheduler-date-table-row').nth(0).locator('.dx-scheduler-date-table-cell').nth(0); + const secondCell = page.locator('.dx-scheduler-date-table-row').nth(0).locator('.dx-scheduler-date-table-cell').nth(1); + + await firstCell.dragTo(secondCell); + + let selectedCount = await page.locator(`.dx-scheduler-date-table-cell.${SELECTED_CELL_CLASS}`).count(); + expect(selectedCount).toBeGreaterThan(0); + + await scrollTo(page, 1000, 0); + selectedCount = await page.locator(`.dx-scheduler-date-table-cell.${SELECTED_CELL_CLASS}`).count(); + expect(selectedCount).toBe(0); + + await scrollTo(page, 0, 0); + selectedCount = await page.locator(`.dx-scheduler-date-table-cell.${SELECTED_CELL_CLASS}`).count(); + expect(selectedCount).toBeGreaterThan(0); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/cellsSelection/cellsSelection.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/cellsSelection/cellsSelection.spec.ts new file mode 100644 index 000000000000..5cafcc8821e1 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/cellsSelection/cellsSelection.spec.ts @@ -0,0 +1,30 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, getContainerUrl, setupTestPage } from '../../../../playwright-helpers'; + +const containerUrl = getContainerUrl(__dirname, '../../../../tests/container.html'); + +test.describe('Scheduler: Cells Selection in Virtual Scrolling', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + test('Selection should work correctly with all-day panel appointments', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + views: ['week'], + currentView: 'week', + currentDate: new Date(2021, 11, 9), + dataSource: [{ + startDate: new Date(2021, 11, 9), + endDate: new Date(2021, 11, 9), + allDay: true, + text: 'Appointment', + }], + }); + + await page.locator('.dx-scheduler-appointment').filter({ hasText: 'Appointment' }).click(); + await page.locator('.dx-scheduler-date-table-cell').first().click(); + + const selectedCount = await page.locator('.dx-scheduler-date-table-cell.dx-state-focused').count(); + expect(selectedCount).toBe(1); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/cellsSelection/virtualScrollingCellSelection.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/cellsSelection/virtualScrollingCellSelection.spec.ts new file mode 100644 index 000000000000..ff37edf61d93 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/cellsSelection/virtualScrollingCellSelection.spec.ts @@ -0,0 +1,85 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, getContainerUrl, setupTestPage } from '../../../../playwright-helpers'; + +const containerUrl = getContainerUrl(__dirname, '../../../../tests/container.html'); + +const SELECTED_CELL_CLASS = 'dx-state-focused'; + +const createSchedulerWidget = async (page: any, options = {}) => { + await createWidget(page, 'dxScheduler', { + dataSource: [], + views: ['week'], + currentView: 'week', + currentDate: new Date(2020, 8, 20), + cellDuration: 60, + height: 300, + width: 400, + scrolling: { mode: 'virtual' }, + resources: [{ + fieldExpr: 'resourceId0', + dataSource: [{ id: 0 }, { id: 1 }], + }], + ...options, + }); +}; + +const scrollTo = async (page: any, x: number, y: number) => { + await page.evaluate(({ scrollX, scrollY }: { scrollX: number; scrollY: number }) => { + const instance = ($('#container') as any).dxScheduler('instance'); + const scrollable = instance.getWorkSpaceScrollable(); + scrollable.scrollTo({ y: scrollY, x: scrollX }); + }, { scrollX: x, scrollY: y }); + await page.waitForTimeout(300); +}; + +test.describe('Scheduler: Cells Selection in Virtual Scrolling', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + [true, false].forEach((showAllDayPanel) => { + test(`Selected cells shouldn't disappear on scroll when showAllDayPanel is equal to ${showAllDayPanel}`, async ({ page }) => { + await createSchedulerWidget(page, { showAllDayPanel }); + + const firstCell = page.locator('.dx-scheduler-date-table-row').nth(0).locator('.dx-scheduler-date-table-cell').nth(0); + const secondCell = page.locator('.dx-scheduler-date-table-row').nth(0).locator('.dx-scheduler-date-table-cell').nth(1); + + await firstCell.dragTo(secondCell); + + let selectedCount = await page.locator(`.dx-scheduler-date-table-cell.${SELECTED_CELL_CLASS}`).count(); + expect(selectedCount).toBeGreaterThan(0); + + await scrollTo(page, 0, 500); + + await scrollTo(page, 0, 0); + + selectedCount = await page.locator(`.dx-scheduler-date-table-cell.${SELECTED_CELL_CLASS}`).count(); + expect(selectedCount).toBeGreaterThan(0); + }); + }); + + test('Selection should work in month view', async ({ page }) => { + await createSchedulerWidget(page, { + views: [{ type: 'month', intervalCount: 30 }], + currentView: 'month', + }); + + const firstCell = page.locator('.dx-scheduler-date-table-row').nth(0).locator('.dx-scheduler-date-table-cell').nth(0); + const secondCell = page.locator('.dx-scheduler-date-table-row').nth(0).locator('.dx-scheduler-date-table-cell').nth(1); + + await firstCell.dragTo(secondCell); + + let selectedCount = await page.locator(`.dx-scheduler-date-table-cell.${SELECTED_CELL_CLASS}`).count(); + expect(selectedCount).toBeGreaterThan(0); + + await scrollTo(page, 0, 1500); + + selectedCount = await page.locator(`.dx-scheduler-date-table-cell.${SELECTED_CELL_CLASS}`).count(); + expect(selectedCount).toBe(0); + + await scrollTo(page, 0, 0); + + selectedCount = await page.locator(`.dx-scheduler-date-table-cell.${SELECTED_CELL_CLASS}`).count(); + expect(selectedCount).toBeGreaterThan(0); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dataSource/load.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dataSource/load.spec.ts new file mode 100644 index 000000000000..795ae2b5ed25 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dataSource/load.spec.ts @@ -0,0 +1,64 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, getContainerUrl, setupTestPage } from '../../../../playwright-helpers'; + +const containerUrl = getContainerUrl(__dirname, '../../../../tests/container.html'); + +test.describe('Scheduler - DataSource loading', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + test('it should correctly load items with post processing', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource: { + store: [ + { text: 'appt-0', startDate: new Date(2021, 3, 26, 9, 30), endDate: new Date(2021, 3, 26, 11, 30) }, + { text: 'appt-1', startDate: new Date(2021, 3, 27, 9, 30), endDate: new Date(2021, 3, 27, 11, 30) }, + { text: 'appt-2', startDate: new Date(2021, 3, 28, 9, 30), endDate: new Date(2021, 3, 28, 11, 30) }, + ], + postProcess: ((items: any[]) => [items[0]]) as any, + }, + views: ['workWeek'], + currentView: 'workWeek', + currentDate: new Date(2021, 3, 27), + startDayHour: 9, + endDayHour: 19, + height: 600, + width: 800, + }); + + const appointmentCount = await page.locator('.dx-scheduler-appointment').count(); + expect(appointmentCount).toBe(1); + + const appointment0 = page.locator('.dx-scheduler-appointment').filter({ hasText: 'appt-0' }); + await expect(appointment0).toBeVisible(); + }); + + test('it should not call additional DataSource loads after repaint', async ({ page }) => { + await page.evaluate(() => { + (window as any).testOptions = { loadCount: 0 }; + (window as any).widget = ($('#container') as any) + .dxScheduler({ + dataSource: { + store: new (window as any).DevExpress.data.ArrayStore({ + data: [], + onLoaded: () => { (window as any).testOptions.loadCount! += 1; }, + }), + }, + }).dxScheduler('instance'); + }); + + await page.evaluate(() => { (window as any).widget.repaint(); }); + await page.evaluate(() => { (window as any).widget.repaint(); }); + await page.evaluate(() => { (window as any).widget.repaint(); }); + + await page.evaluate(() => { + const store = (window as any).widget.getDataSource().store(); + store.push([{ type: 'update', key: 0, data: {} }]); + }); + await page.waitForTimeout(200); + + const loadCount = await page.evaluate(() => (window as any).testOptions.loadCount); + expect(loadCount).toBe(2); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/deleteAppointments.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/deleteAppointments.spec.ts new file mode 100644 index 000000000000..e18240615bb1 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/deleteAppointments.spec.ts @@ -0,0 +1,108 @@ +import { test, expect } from '@playwright/test'; +import { createWidget } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('Delete appointments', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + +); + +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(page, '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.meta({ unstable: true })('Recurrence appointments should be deleted by click on \'delete\' button', async (t) => { + // Scheduler on '#container' + + expect(page.locator('.dx-scheduler-appointment').count()).toBe(6) + .click(page.locator('.dx-scheduler-appointment').filter({ hasText: 'Text' }).element) + + .expect(scheduler.appointmentTooltip.element.exists) + .ok() + .click(scheduler.appointmentTooltip.deleteButton) + .click(Scheduler.getDeleteRecurrenceDialog().appointment) + .wait(100) + + .expect(page.locator('.dx-scheduler-appointment').count()) + .eql(5); + + await (page.locator('.dx-scheduler-appointment').filter({ hasText: 'Text' }).click().element) + + .click(scheduler.appointmentTooltip.deleteButton) + .click(Scheduler.getDeleteRecurrenceDialog().series) + + .expect(page.locator('.dx-scheduler-appointment').count()) + .eql(0); +}).before(async () => createScheduler(createRecurrenceData())); + +test.meta({ unstable: true })('Recurrence appointments should be deleted by press \'delete\' key', async (t) => { + // Scheduler on '#container' + + expect(page.locator('.dx-scheduler-appointment').count()).toBe(6) + .click(page.locator('.dx-scheduler-appointment').filter({ hasText: 'Text' }).element) + .pressKey('delete') + .click(Scheduler.getDeleteRecurrenceDialog().appointment) + .wait(100) + .expect(page.locator('.dx-scheduler-appointment').count()) + .eql(5); + + await (page.locator('.dx-scheduler-appointment').filter({ hasText: 'Text' }).click().element) + .pressKey('delete') + .click(Scheduler.getDeleteRecurrenceDialog().series) + .expect(page.locator('.dx-scheduler-appointment').count()) + .eql(0); +}).before(async () => createScheduler(createRecurrenceData())); + +test('Common appointments should be deleted by click on \'delete\' button and press \'delete\' key', async ({ page }) => { + // Scheduler on '#container' + + expect(page.locator('.dx-scheduler-appointment').count()).toBe(2) + .click(page.locator('.dx-scheduler-appointment').filter({ hasText: 'Text' }).element) + .click(scheduler.appointmentTooltip.deleteButton) + .expect(page.locator('.dx-scheduler-appointment').count()) + .eql(1); + + expect(page.locator('.dx-scheduler-appointment').count()).toBe(1) + .click(page.locator('.dx-scheduler-appointment').filter({ hasText: 'Text2' }).element) + .pressKey('delete') + .expect(page.locator('.dx-scheduler-appointment').count()) + .eql(0); +}).before(async () => createScheduler(createSimpleData())); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/DNDToFakeCell.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/DNDToFakeCell.spec.ts new file mode 100644 index 000000000000..37b276166c40 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/DNDToFakeCell.spec.ts @@ -0,0 +1,55 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, appendElementTo } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('Drag-n-drop to fake cell', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + +); + +test('Should not select cells outside the scheduler(T1040795)', async () => { + // Scheduler on '#container' + + const { element } = page.locator('.dx-scheduler-appointment').filter({ hasText: 'app' }); + + await t + .drag(element, 0, 200) + + .expect(Selector('#fake').hasClass('dx-scheduler-date-table-droppable-cell')) + .eql(false); +}); + +// TODO: .before() block not converted - move to test setup +// { + await appendElementTo('#container', 'div', 'scheduler'); + await appendElementTo('#container', 'div', 'fake', { + width: '400px', height: '100px', + }); + await ClientFunction(() => { + $('#fake').addClass('scheduler-date-table-cell'); + })(); + + return createWidget(page, 'dxScheduler', { + dataSource: [ + { + text: 'app', + startDate: new Date(2021, 3, 26, 2), + endDate: new Date(2021, 3, 26, 2, 30), + }, + ], + views: ['day'], + currentDate: new Date(2021, 3, 26), + height: 200, + width: 400, + }, '#scheduler'); +}); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/T1017720.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/T1017720.spec.ts new file mode 100644 index 000000000000..aaf8126a13cd --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/T1017720.spec.ts @@ -0,0 +1,72 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('T1017720', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + +); + +test('Drag-n-drop appointment above SVG element(T1017720)', async ({ page }) => { + // Scheduler on '#scheduler' + const draggableAppointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'text' }); + + await /* TODO: drag */ await (draggableAppointment.element).click() /* drag(330, 0) */; + + await testScreenshot(page, 'drag-n-drop-to-right(T1017720).png', { element: page.locator('.dx-scheduler-work-space') }); + + await /* TODO: drag */ await (draggableAppointment.element).click() /* drag(-330, 70) */; + + await testScreenshot(page, 'drag-n-drop-to-left(T1017720).png', { element: page.locator('.dx-scheduler-work-space') }); + + expect(compareResults.isValid()) + .ok(compareResults.errorMessages()); +}); + +// TODO: .before() block not converted - move to test setup +// { + await createWidget(page, 'dxChart', extend({ + width: '100%', + height: 1300, + series: { + type: 'bar', + color: '#ffaa66', + }, + })); + + await createWidget(page, 'dxPopup', extend({ + width: '90%', + height: '90%', + visible: true, + contentTemplate: ClientFunction(() => { + const scheduler = $('
'); + + (scheduler as any).dxScheduler({ + width: '100%', + height: '100%', + startDayHour: 11, + dataSource: [{ + text: 'text', + startDate: new Date(2021, 6, 27, 11), + endDate: new Date(2021, 6, 27, 14), + allDay: false, + }], + views: ['week'], + currentDate: new Date(2021, 6, 27, 12), + currentView: 'week', + }); + + return scheduler; + }), + })); +}); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/T1080232.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/T1080232.spec.ts new file mode 100644 index 000000000000..938c81ba8119 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/T1080232.spec.ts @@ -0,0 +1,82 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, getContainerUrl, setupTestPage, appendElementTo } from '../../../../playwright-helpers'; + +const containerUrl = getContainerUrl(__dirname, '../../../../tests/container.html'); + +test.describe('Appointment (T1080232)', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + test('it should correctly drag external item to the appointment after drag appointment', async ({ page }) => { + await appendElementTo(page, '#container', 'div', { id: 'list' }); + + await page.evaluate(() => { + $('#list').append('
drag-item
').addClass('drag-item'); + }); + + await appendElementTo(page, '#container', 'div', { id: 'scheduler' }); + + await createWidget(page, 'dxSortable', { + group: 'resourceGroup', + }, '#list'); + + await createWidget(page, 'dxScheduler', { + resources: [ + { + fieldExpr: 'resourceId', + dataSource: [ + { id: 0, color: '#e01e38' }, + { id: 1, color: '#f98322' }, + { id: 2, color: '#1e65e8' }, + ], + label: 'Color', + }, + ], + firstDayOfWeek: 1, + maxAppointmentsPerCell: 5, + currentView: 'day', + dataSource: [{ + text: 'Appt-01', + startDate: new Date(2021, 3, 26, 10), + endDate: new Date(2021, 3, 26, 11), + }, { + text: 'Appt-02', + startDate: new Date(2021, 3, 26, 12), + endDate: new Date(2021, 3, 26, 13), + }], + views: ['day'], + currentDate: new Date(2021, 3, 26), + startDayHour: 9, + width: 800, + height: 600, + appointmentTemplate: new Function('e', '_', 'element', ` + var newData = e.appointmentData; + return element + .text(newData.text) + .dxSortable({ + group: 'resourceGroup', + data: [newData], + onAdd: function() { + element.attr('data-status', 'Added'); + }, + }); + `) as any, + }, '#scheduler'); + + const appt01 = page.locator('#scheduler .dx-scheduler-appointment').filter({ hasText: 'Appt-01' }); + const appt02 = page.locator('#scheduler .dx-scheduler-appointment').filter({ hasText: 'Appt-02' }); + const cell01 = page.locator('#scheduler .dx-scheduler-date-table-row').nth(1).locator('.dx-scheduler-date-table-cell').nth(0); + const dragItem = page.locator('.drag-item'); + + await appt01.dragTo(cell01); + + const appt01Box = await appt01.boundingBox(); + expect(appt01Box!.y).toBeCloseTo(183, 0); + + await dragItem.dragTo(appt02); + + const dataStatus = await appt02.locator('.dx-item-content').getAttribute('data-status'); + expect(dataStatus).toBe('Added'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/T1118059.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/T1118059.spec.ts new file mode 100644 index 000000000000..2af270b08587 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/T1118059.spec.ts @@ -0,0 +1,143 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, getContainerUrl, setupTestPage, setStyleAttribute } from '../../../../playwright-helpers'; + +const containerUrl = getContainerUrl(__dirname, '../../../../tests/container.html'); + +const SCHEDULER_SELECTOR = '#scheduler'; + +const markup = '
' + + '
drag container
' + + '
top right space
' + + '
' + + '
' + + '
left space
' + + '
' + + '
'; + +test.describe('T1118059', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + test('After drag to draggable component, should be called onAppointmentDeleting event only', async ({ page }) => { + await page.evaluate(() => { + (window as any).eventName = ''; + }); + + await setStyleAttribute(page, '#container', 'display: flex; flex-direction: column;'); + + await page.evaluate((m) => { + $('#container').append(m); + }, markup); + + await createWidget(page, 'dxDraggable', { + group: 'appointmentsGroup', + }, '#drag-container'); + + await createWidget(page, 'dxScheduler', { + dataSource: [{ + text: 'All day test app 1', + startDate: new Date(2021, 3, 26), + endDate: new Date(2021, 3, 26), + allDay: true, + }, { + text: 'All day test app 2', + startDate: new Date(2021, 3, 27), + endDate: new Date(2021, 3, 27), + allDay: true, + }, { + text: 'Regular test app', + startDate: new Date(2021, 3, 27, 10, 30), + endDate: new Date(2021, 3, 27, 11), + }], + views: [{ + type: 'day', + intervalCount: 2, + }], + onAppointmentUpdated: new Function(`window.eventName = 'onAppointmentUpdated';`) as any, + onAppointmentUpdating: new Function(`window.eventName = 'onAppointmentUpdating';`) as any, + onAppointmentDeleting: new Function(`window.eventName = 'onAppointmentDeleting';`) as any, + currentDate: new Date(2021, 3, 26), + startDayHour: 9, + height: 600, + width: 500, + appointmentDragging: { + group: 'appointmentsGroup', + onRemove: new Function('e', 'e.component.deleteAppointment(e.itemData);') as any, + }, + }, SCHEDULER_SELECTOR); + + const regularApp = page.locator(`${SCHEDULER_SELECTOR} .dx-scheduler-appointment`).filter({ hasText: 'Regular test app' }); + const dragContainer = page.locator('#drag-container'); + + await regularApp.dragTo(dragContainer); + + await page.waitForTimeout(500); + + const eventName = await page.evaluate(() => (window as any).eventName); + expect(eventName).toBe('onAppointmentDeleting'); + }); + + test('After drag over component area, shouldn\'t called onAppointment* data events and appointment shouldn\'t change position', async ({ page }) => { + await page.evaluate(() => { + (window as any).eventName = ''; + }); + + await setStyleAttribute(page, '#container', 'display: flex; flex-direction: column;'); + + await page.evaluate((m) => { + $('#container').append(m); + }, markup); + + await createWidget(page, 'dxScheduler', { + dataSource: [{ + text: 'All day test app 1', + startDate: new Date(2021, 3, 26), + endDate: new Date(2021, 3, 26), + allDay: true, + }, { + text: 'All day test app 2', + startDate: new Date(2021, 3, 27), + endDate: new Date(2021, 3, 27), + allDay: true, + }, { + text: 'Regular test app', + startDate: new Date(2021, 3, 27, 10, 30), + endDate: new Date(2021, 3, 27, 11), + }], + views: [{ + type: 'day', + intervalCount: 2, + }], + onAppointmentUpdated: new Function(`window.eventName = 'onAppointmentUpdated';`) as any, + onAppointmentUpdating: new Function(`window.eventName = 'onAppointmentUpdating';`) as any, + onAppointmentDeleting: new Function(`window.eventName = 'onAppointmentDeleting';`) as any, + currentDate: new Date(2021, 3, 26), + startDayHour: 9, + height: 600, + width: 500, + }, SCHEDULER_SELECTOR); + + const allDayApp2 = page.locator(`${SCHEDULER_SELECTOR} .dx-scheduler-appointment`).filter({ hasText: 'All day test app 2' }); + const spaceRight = page.locator('#space-right'); + + await allDayApp2.dragTo(spaceRight); + + let eventName = await page.evaluate(() => (window as any).eventName); + expect(eventName).toBe(''); + + const allDayTimeText = await allDayApp2.locator('.dx-scheduler-appointment-content-date').textContent(); + expect(allDayTimeText).toContain('April 27'); + + const regularApp = page.locator(`${SCHEDULER_SELECTOR} .dx-scheduler-appointment`).filter({ hasText: 'Regular test app' }); + const leftRight = page.locator('#left-right'); + + await regularApp.dragTo(leftRight); + + eventName = await page.evaluate(() => (window as any).eventName); + expect(eventName).toBe(''); + + const regularTimeText = await regularApp.locator('.dx-scheduler-appointment-content-date').textContent(); + expect(regularTimeText).toContain('10:30 AM - 11:00 AM'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/T1235433.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/T1235433.spec.ts new file mode 100644 index 000000000000..c42ecc75eb34 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/T1235433.spec.ts @@ -0,0 +1,157 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, getContainerUrl, setupTestPage } from '../../../../playwright-helpers'; + +const containerUrl = getContainerUrl(__dirname, '../../../../tests/container.html'); + +const createScheduler = (view: string) => ({ + timeZone: 'America/Los_Angeles', + dataSource: [ + { + text: 'Book 1', + startDate: new Date('2021-02-02T18:00:00.000Z'), + endDate: new Date('2021-02-02T19:00:00.000Z'), + priority: 1, + }, { + text: 'Book 2', + startDate: new Date('2021-02-03T01:00:00.000Z'), + endDate: new Date('2021-02-03T02:15:00.000Z'), + priority: 1, + }, { + text: 'Book 3', + startDate: new Date('2021-02-09T01:00:00.000Z'), + endDate: new Date('2021-02-09T02:15:00.000Z'), + priority: 1, + }, + ], + views: [view], + currentView: view, + currentDate: new Date('2021-02-02T17:00:00.000Z'), + firstDayOfWeek: 0, + scrolling: { mode: 'virtual' }, + startDayHour: 8, + endDayHour: 20, + cellDuration: 60, + groups: ['priority'], + useDropDownViewSwitcher: false, + resources: [{ + fieldExpr: 'priority', + dataSource: [ + { id: 1, text: 'Low Priority', color: 'green' }, + { id: 2, text: 'High Priority', color: 'blue' }, + ], + label: 'Priority', + }], + height: 580, +}); + +const scrollTo = async (page: any, x: number, y: number) => { + await page.evaluate(({ scrollX, scrollY }: { scrollX: number; scrollY: number }) => { + const instance = ($('#container') as any).dxScheduler('instance'); + const scrollable = instance.getWorkSpaceScrollable(); + scrollable.scrollTo({ y: scrollY, x: scrollX }); + }, { scrollX: x, scrollY: y }); +}; + +const dragAppointmentByCircle = async ( + page: any, + appointmentText: string, + labels: string[], + descriptions: string[], +) => { + const appointment = page.locator('.dx-scheduler-appointment').filter({ hasText: appointmentText }); + + const box0 = await appointment.boundingBox(); + await appointment.hover(); + await page.mouse.down(); + await page.mouse.move(box0!.x + box0!.width / 2 - 200, box0!.y + box0!.height / 2, { steps: 10 }); + await page.mouse.up(); + + let ariaLabel = await appointment.getAttribute('aria-label'); + expect(ariaLabel).toContain(labels[0]); + let ariaDesc = await appointment.getAttribute('aria-description'); + expect(ariaDesc).toContain(descriptions[0]); + + const box1 = await appointment.boundingBox(); + await appointment.hover(); + await page.mouse.down(); + await page.mouse.move(box1!.x + box1!.width / 2, box1!.y + box1!.height / 2 + 200, { steps: 10 }); + await page.mouse.up(); + + ariaLabel = await appointment.getAttribute('aria-label'); + expect(ariaLabel).toContain(labels[1]); + ariaDesc = await appointment.getAttribute('aria-description'); + expect(ariaDesc).toContain(descriptions[1]); + + const box2 = await appointment.boundingBox(); + await appointment.hover(); + await page.mouse.down(); + await page.mouse.move(box2!.x + box2!.width / 2 + 200, box2!.y + box2!.height / 2, { steps: 10 }); + await page.mouse.up(); + + ariaLabel = await appointment.getAttribute('aria-label'); + expect(ariaLabel).toContain(labels[2]); + ariaDesc = await appointment.getAttribute('aria-description'); + expect(ariaDesc).toContain(descriptions[2]); + + const box3 = await appointment.boundingBox(); + await appointment.hover(); + await page.mouse.down(); + await page.mouse.move(box3!.x + box3!.width / 2, box3!.y + box3!.height / 2 - 200, { steps: 10 }); + await page.mouse.up(); + + ariaLabel = await appointment.getAttribute('aria-label'); + expect(ariaLabel).toContain(labels[3]); + ariaDesc = await appointment.getAttribute('aria-description'); + expect(ariaDesc).toContain(descriptions[3]); +}; + +const appointmentDescriptions = ['Group: Low Priority', 'Group: High Priority', 'Group: High Priority', 'Group: Low Priority']; +const appointment1Times = ['9:00 AM - 10:00 AM', '9:00 AM - 10:00 AM', '10:00 AM - 11:00 AM', '10:00 AM - 11:00 AM']; +const appointment2Times = ['4:00 PM - 5:15 PM', '4:00 PM - 5:15 PM', '5:00 PM - 6:15 PM', '5:00 PM - 6:15 PM']; + +test.describe('Scheduler Drag-and-Drop inside Group', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + test('T1235433: Scheduler - Drag-n-Drop works inside the group with virtual scrolling (timelineDay)', async ({ page }) => { + await createWidget(page, 'dxScheduler', createScheduler('timelineDay')); + + await expect(page.locator('.dx-scheduler')).toBeVisible(); + + await dragAppointmentByCircle(page, 'Book 1', appointment1Times, appointmentDescriptions); + await scrollTo(page, 1400, 0); + await dragAppointmentByCircle(page, 'Book 2', appointment2Times, appointmentDescriptions); + }); + + test('T1235433: Scheduler - Drag-n-Drop works inside the group with virtual scrolling (timelineWorkWeek)', async ({ page }) => { + await createWidget(page, 'dxScheduler', createScheduler('timelineWorkWeek')); + + await expect(page.locator('.dx-scheduler')).toBeVisible(); + + await scrollTo(page, 2400, 0); + await dragAppointmentByCircle(page, 'Book 1', appointment1Times, appointmentDescriptions); + await scrollTo(page, 3400, 0); + await dragAppointmentByCircle(page, 'Book 2', appointment2Times, appointmentDescriptions); + }); + + test('T1235433: Scheduler - Drag-n-Drop works inside the group with virtual scrolling (timelineMonth)', async ({ page }) => { + await createWidget(page, 'dxScheduler', createScheduler('timelineMonth')); + + await expect(page.locator('.dx-scheduler')).toBeVisible(); + + await dragAppointmentByCircle(page, 'Book 1', [ + 'February 1, 2021', + 'February 1, 2021', + 'February 2, 2021', + 'February 2, 2021', + ], appointmentDescriptions); + await scrollTo(page, 1000, 0); + await dragAppointmentByCircle(page, 'Book 3', [ + 'February 7, 2021', + 'February 7, 2021', + 'February 8, 2021', + 'February 8, 2021', + ], appointmentDescriptions); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/T1263508.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/T1263508.spec.ts new file mode 100644 index 000000000000..594659aba913 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/T1263508.spec.ts @@ -0,0 +1,81 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, getContainerUrl, setupTestPage } from '../../../../playwright-helpers'; + +const containerUrl = getContainerUrl(__dirname, '../../../../tests/container.html'); + +const DRAGGABLE_ITEM_CLASS = 'dx-card'; +const draggingGroupName = 'appointmentsGroup'; + +test.describe('Scheduler Drag-and-Drop Fix', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + test('Scheduler - The \'Cannot read properties of undefined (reading \'getTime\')\' error is thrown on an attempt to drag an outside element if the previous drag operation was canceled', async ({ page }) => { + const tasks = [{ text: 'Brochures' }]; + + await page.evaluate(() => { + $('
', { id: 'list' }).appendTo('#parentContainer'); + }); + + await page.evaluate((tasksArr) => { + tasksArr.forEach((task: any) => { + $('
', { + class: 'dx-card', + text: task.text, + }).appendTo('#list'); + }); + }, tasks); + + for (const task of tasks) { + await createWidget(page, 'dxDraggable', { + group: draggingGroupName, + data: task, + clone: true, + onDragStart: new Function('e', 'e.itemData = e.fromData;') as any, + }, `.${DRAGGABLE_ITEM_CLASS}:contains(${task.text})`); + } + + await createWidget(page, 'dxScheduler', { + timeZone: 'America/Los_Angeles', + dataSource: [ + { + text: 'Book', + startDate: new Date('2021-04-26T19:00:00.000Z'), + endDate: new Date('2021-04-26T20:00:00.000Z'), + }, + ], + currentDate: new Date(2021, 3, 26), + startDayHour: 9, + height: 600, + editing: true, + appointmentDragging: { + group: draggingGroupName, + onDragEnd: new Function('e', 'e.cancel = e.event.ctrlKey;') as any, + onRemove: new Function('e', 'e.component.deleteAppointment(e.itemData);') as any, + onAdd: new Function('e', 'e.component.addAppointment(e.itemData);') as any, + }, + }); + + const draggableAppointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'Book' }); + const targetCell = page.locator('.dx-scheduler-date-table-row').nth(5).locator('.dx-scheduler-date-table-cell').nth(0); + const draggableItem = page.locator(`.${DRAGGABLE_ITEM_CLASS}`).filter({ hasText: 'Brochures' }); + + await expect(page.locator('.dx-scheduler')).toBeVisible(); + + // TODO: This test requires disabling mouseup events during drag, then pressing escape. + const targetBox = await targetCell.boundingBox(); + await draggableAppointment.hover(); + await page.mouse.down(); + await page.mouse.move(targetBox!.x + targetBox!.width / 2, targetBox!.y + targetBox!.height / 2, { steps: 10 }); + await page.keyboard.press('Escape'); + await page.mouse.up(); + + await expect(draggableItem).toBeVisible(); + + await draggableItem.dragTo(targetCell); + + const newAppointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'Brochures' }); + await expect(newAppointment).toBeVisible(); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/T697037.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/T697037.spec.ts new file mode 100644 index 000000000000..25b15ed5d916 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/T697037.spec.ts @@ -0,0 +1,41 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, getContainerUrl, setupTestPage } from '../../../../playwright-helpers'; + +const containerUrl = getContainerUrl(__dirname, '../../../../tests/container.html'); + +test.describe('T697037', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + test('Recurrence exception date should equal date of appointment, which excluded from recurrence(T697037)', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource: [{ + text: 'Test', + startDate: '2018-11-26T02:00:00Z', + endDate: '2018-11-26T02:15:00Z', + recurrenceRule: 'FREQ=DAILY;COUNT=5', + recurrenceException: '', + }], + views: ['week'], + currentView: 'week', + currentDate: new Date(2018, 10, 26), + dateSerializationFormat: 'yyyy-MM-ddTHH:mm:ssZ', + timeZone: 'Etc/UTC', + showAllDayPanel: false, + recurrenceEditMode: 'occurrence', + onAppointmentUpdating: new Function('e', ` + window.recurrenceException = e.newData.recurrenceException; + `) as any, + }); + + const targetCell = page.locator('.dx-scheduler-date-table-row').nth(3).locator('.dx-scheduler-date-table-cell').nth(3); + const appointments = page.locator('.dx-scheduler-appointment').filter({ hasText: 'Test' }); + const appointment = appointments.nth(2); + + await appointment.dragTo(targetCell); + + const recurrenceException = await page.evaluate(() => (window as any).recurrenceException); + expect(recurrenceException).toBe('20181128T020000Z'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/appointmentCollector.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/appointmentCollector.spec.ts new file mode 100644 index 000000000000..87de53371d84 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/appointmentCollector.spec.ts @@ -0,0 +1,148 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('Drag-and-drop behaviour for the appointment tooltip', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + +); + +test('Drag-n-drop between a scheduler table cell and the appointment tooltip', async ({ page }) => { + // Scheduler on '#container' + const appointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'Approve Personal Computer Upgrade Plan' }); + const collector = scheduler.collectors.find('2'); + const { appointmentTooltip } = scheduler; + const appointmentTooltipItem = appointmentTooltip.getListItem('Approve Personal Computer Upgrade Plan'); + + await (collector.element).click() + .expect(appointmentTooltip.isVisible()).toBeTruthy() + .dragToElement(appointmentTooltipItem.element, page.locator('.dx-scheduler-date-table-row').nth(2).locator('.dx-scheduler-date-table-cell').nth(5), { speed: 0.5 }) + .expect(appointmentTooltipItem.element.exists) + .notOk() + .expect(appointment.element.exists) + .ok() + .expect(appointment.size.height) + .eql('76px') + .expect(appointment.date.time) + .eql('9:30 AM - 10:30 AM') + .dragToElement(appointment.element, page.locator('.dx-scheduler-date-table-row').nth(3).locator('.dx-scheduler-date-table-cell').nth(2), { speed: 0.5 }) + .click(collector.element) + .expect(appointmentTooltip.isVisible()) + .ok() + .expect(appointment.element.exists) + .notOk(); +}).before(async () => createScheduler({ + views: ['week'], + currentView: 'week', + dataSource: appointmentCollectorData, + maxAppointmentsPerCell: 2, + width: 1000, +})); + +test('Drag-n-drop to the cell on the left should work in week view (T1005115)', async ({ page }) => { + // Scheduler on '#container' + const collector = scheduler.collectors.find('1'); + const { appointmentTooltip } = scheduler; + const appointmentTooltipItem = appointmentTooltip.getListItem('Approve Personal Computer Upgrade Plan'); + + await (collector.element).click() + .dragToElement( + appointmentTooltipItem.element, + page.locator('.dx-scheduler-date-table-row').nth(2).locator('.dx-scheduler-date-table-cell').nth(2), + { speed: 0.5 }, + ); + + await testScreenshot(page, 'drag-n-drop-from-tooltip-to-left-cell-in-week.png', { element: page.locator('.dx-scheduler-work-space') }); + + expect(compareResults.isValid()) + .ok(compareResults.errorMessages()); +}).before(async () => createWidget(page, 'dxScheduler', { + currentDate: new Date(2019, 3, 1), + views: ['week'], + currentView: 'week', + dataSource: [{ + text: 'Website Re-Design Plan', + startDate: new Date(2019, 3, 3, 9, 30), + endDate: new Date(2019, 3, 3, 11, 30), + }, { + text: 'Approve Personal Computer Upgrade Plan', + startDate: new Date(2019, 3, 3, 10, 0), + endDate: new Date(2019, 3, 3, 10, 30), + }, { + text: 'Install New Database', + startDate: new Date(2019, 3, 3, 9, 45), + endDate: new Date(2019, 3, 3, 11, 15), + }], + maxAppointmentsPerCell: 2, + height: 800, + startDayHour: 9, +})); + +test('Drag-n-drop in the same table cell', async ({ page }) => { + // Scheduler on '#container' + const { appointmentTooltip } = scheduler; + const appointmentTooltipItem = appointmentTooltip.getListItem('Approve Personal Computer Upgrade Plan'); + + await (scheduler.collectors.find('2').click().element) + .expect(appointmentTooltip.isVisible()).toBeTruthy() + .drag(appointmentTooltipItem.element, 0, -90) + .click(scheduler.collectors.find('2').element) + .expect(appointmentTooltip.isVisible()) + .ok() + .expect(appointmentTooltipItem.element.exists) + .ok(); +}).before(async () => createScheduler({ + views: ['week'], + currentView: 'week', + dataSource: appointmentCollectorData, + maxAppointmentsPerCell: 2, + width: 1000, +})); + +test.meta({ runInTheme: Themes.genericLight })('Drag-n-drop to the cell below should work in month view (T1005115)', async (t) => { + // Scheduler on '#container' + const collector = scheduler.collectors.find('1 more'); + const { appointmentTooltip } = scheduler; + const appointmentTooltipItem = appointmentTooltip.getListItem('Approve Personal Computer Upgrade Plan'); + + await (collector.element).click() + .dragToElement( + appointmentTooltipItem.element, + page.locator('.dx-scheduler-date-table-row').nth(1).locator('.dx-scheduler-date-table-cell').nth(3), + { speed: 0.5 }, + ); + + await testScreenshot(page, 'drag-n-drop-from-tooltip-to-cell-below-in-month.png', { element: page.locator('.dx-scheduler-work-space') }); + + expect(compareResults.isValid()) + .ok(compareResults.errorMessages()); +}).before(async () => createWidget(page, 'dxScheduler', { + currentDate: new Date(2019, 3, 1), + views: ['month'], + currentView: 'month', + dataSource: [{ + text: 'Website Re-Design Plan', + startDate: new Date(2019, 3, 3, 9, 30), + endDate: new Date(2019, 3, 3, 11, 30), + }, { + text: 'Approve Personal Computer Upgrade Plan', + startDate: new Date(2019, 3, 3, 10, 0), + endDate: new Date(2019, 3, 3, 11, 0), + }, { + text: 'Install New Database', + startDate: new Date(2019, 3, 3, 9, 45), + endDate: new Date(2019, 3, 3, 11, 15), + }], + maxAppointmentsPerCell: 2, + height: 800, +})); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/basic.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/basic.spec.ts new file mode 100644 index 000000000000..ef196abb11dd --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/basic.spec.ts @@ -0,0 +1,166 @@ +import { test, expect } from '@playwright/test'; +import { testScreenshot } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('Drag-and-drop appointments in the Scheduler basic views', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + +); + +['day', 'week', 'workWeek'].forEach((view) => test(`Drag-n-drop in the "${view}" view`, async ({ page }) => { + // --- setup --- +await createScheduler({ + timeZone: 'Etc/GMT', + dataSource: [{ + text: 'Test appointment', + startDate: new Date('2022-09-08T10:00:00.000Z'), + endDate: new Date('2022-09-08T10:30:00.000Z'), + }], + views: ['week'], + currentView: 'week', + currentDate: new Date('2022-09-09T10:00:00.000Z'), + startDayHour: 9, + width: 600, + height: 600, + // --- test --- +// Scheduler on '#container' + const draggableAppointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'Brochure Design Review' }); + + await t + .dragToElement(draggableAppointment.element, page.locator('.dx-scheduler-date-table-row').nth(4).locator('.dx-scheduler-date-table-cell').nth(0)) + .expect(draggableAppointment.size.height).toBe('38px') + .expect(draggableAppointment.date.time) + .eql('11:00 AM - 11:30 AM'); +}).before(async () => createScheduler({ + views: [view], + currentView: view, + dataSource, +}))); + +test('Drag-n-drop in the "month" view', async ({ page }) => { + // Scheduler on '#container' + const draggableAppointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'Brochure Design Review' }); + + await t + .dragToElement(draggableAppointment.element, page.locator('.dx-scheduler-date-table-row').nth(0).locator('.dx-scheduler-date-table-cell').nth(4)) + .expect(draggableAppointment.size.height).toBe('23.8281px') + .expect(draggableAppointment.date.time) + .eql('9:00 AM - 9:30 AM'); +}).before(async () => createScheduler({ + views: ['month'], + currentView: 'month', + dataSource, + height: 834, +})); + +test('Drag-n-drop when browser has horizontal scroll', async ({ page }) => { + // Scheduler on '#container' + const draggableAppointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'Staff Productivity Report' }); + + await t + .drag(draggableAppointment.element, 250, -50, { speed: 0.2 }) + .expect(draggableAppointment.isAllDay).toBe(true); +}).before(async () => createScheduler({ + views: ['week'], + currentView: 'week', + dataSource: [{ + text: 'Staff Productivity Report', + startDate: new Date(2019, 3, 6, 9, 0), + endDate: new Date(2019, 3, 6, 10, 30), + resourceId: 2, + }], + width: 1800, +})); + +test('Drag-n-drop when browser has vertical scroll', async ({ page }) => { + // Scheduler on '#container' + const draggableAppointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'Staff Productivity Report' }); + + await t + .dragToElement(draggableAppointment.element, page.locator('.dx-scheduler-date-table-row').nth(25).locator('.dx-scheduler-date-table-cell').nth(0), { speed: 0.5 }) + .expect(draggableAppointment.date.time).toBe('9:30 PM - 10:00 PM'); +}).before(async () => createScheduler({ + views: ['week'], + currentView: 'week', + dataSource: [{ + text: 'Staff Productivity Report', + startDate: new Date(2019, 3, 1, 21, 0), + endDate: new Date(2019, 3, 1, 21, 30), + resourceId: 2, + }], + height: 1800, +})); + +test('Drag recurrent appointment occurrence from collector (T832887)', async ({ page }) => { + // Scheduler on '#container' + const appointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'Recurrence two' }); + const collector = scheduler.collectors.find('2'); + const { appointmentTooltip } = scheduler; + const appointmentTooltipItem = appointmentTooltip.getListItem('Recurrence two'); + const popup = Scheduler.getDeleteRecurrenceDialog(); + + await (collector.element).click() + .expect(appointmentTooltip.isVisible()).toBeTruthy() + .dragToElement(appointmentTooltipItem.element, page.locator('.dx-scheduler-date-table-row').nth(2).locator('.dx-scheduler-date-table-cell').nth(2)) + .expect(appointmentTooltipItem.element.exists) + .notOk() + .click(popup.appointment) + .expect(appointment.element.exists) + .ok() + .expect(appointment.date.time) + .eql('4:00 AM - 6:00 AM') + .expect(collector.element.exists) + .notOk(); +}).before(async () => createScheduler({ + views: ['week'], + currentView: 'week', + firstDayOfWeek: 2, + startDayHour: 4, + maxAppointmentsPerCell: 1, + dataSource: [{ + text: 'Recurrence one', + startDate: new Date(2019, 2, 26, 8, 0), + endDate: new Date(2019, 2, 26, 10, 0), + recurrenceException: '', + recurrenceRule: 'FREQ=DAILY', + }, { + text: 'Non-recurrent appointment', + startDate: new Date(2019, 2, 26, 7, 0), + endDate: new Date(2019, 2, 26, 11, 0), + }, { + text: 'Recurrence two', + startDate: new Date(2019, 2, 26, 8, 0), + endDate: new Date(2019, 2, 26, 10, 0), + recurrenceException: '', + recurrenceRule: 'FREQ=DAILY', + }], + currentDate: new Date(2019, 2, 26), +})); + +test('Drag-n-drop the appointment to the left column to the cell that has the same time', async ({ page }) => { + // Scheduler on '#container' + const draggableAppointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'Test appointment' }); + + await t + .dragToElement( + draggableAppointment.element, + page.locator('.dx-scheduler-date-table-row').nth(2).locator('.dx-scheduler-date-table-cell').nth(2), + { speed: 0.5 }, + ); + + await testScreenshot(page, 'drag-n-drop-appointment-to-left-column.png', { element: page.locator('.dx-scheduler-work-space') }); + + expect(compareResults.isValid()) + .ok(compareResults.errorMessages()); +}); +}); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/betweenSchedulers/dragAppointmentInEqualCellIndexes.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/betweenSchedulers/dragAppointmentInEqualCellIndexes.spec.ts new file mode 100644 index 000000000000..280b7507050c --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/betweenSchedulers/dragAppointmentInEqualCellIndexes.spec.ts @@ -0,0 +1,50 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, getContainerUrl, setupTestPage, setStyleAttribute, appendElementTo } from '../../../../../playwright-helpers'; + +const containerUrl = getContainerUrl(__dirname, '../../../../../tests/container.html'); + +const FIRST_SCHEDULER_SELECTOR = 'scheduler-first'; +const SECOND_SCHEDULER_SELECTOR = 'scheduler-second'; +const EXPECTED_APPOINTMENT_TIME = '1:00 AM - 2:00 AM'; + +const TEST_APPOINTMENT = { + text: 'My appointment', + startDate: new Date(2021, 3, 30, 1), + endDate: new Date(2021, 3, 30, 2), +}; + +const getSchedulerOptions = (dataSource: any[]) => ({ + dataSource, + currentView: 'workWeek', + currentDate: new Date(2021, 3, 26), + width: 600, + appointmentDragging: { + group: 'testDragGroup', + onRemove: new Function('e', 'e.component.deleteAppointment(e.itemData);') as any, + onAdd: new Function('e', 'e.component.addAppointment(e.itemData);') as any, + }, +}); + +test.describe('Drag-n-drop appointments between two schedulers with equal cell indexes (T1094035)', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + test('Should not lose drag-n-dropped appointment in the second scheduler', async ({ page }) => { + await setStyleAttribute(page, '#container', 'display: flex;'); + await appendElementTo(page, '#container', 'div', { id: FIRST_SCHEDULER_SELECTOR }); + await appendElementTo(page, '#container', 'div', { id: SECOND_SCHEDULER_SELECTOR }); + + await createWidget(page, 'dxScheduler', getSchedulerOptions([TEST_APPOINTMENT]), `#${FIRST_SCHEDULER_SELECTOR}`); + await createWidget(page, 'dxScheduler', getSchedulerOptions([]), `#${SECOND_SCHEDULER_SELECTOR}`); + + const appointmentToMove = page.locator(`#${FIRST_SCHEDULER_SELECTOR} .dx-scheduler-appointment`).filter({ hasText: TEST_APPOINTMENT.text }); + const cellToMove = page.locator(`#${SECOND_SCHEDULER_SELECTOR} .dx-scheduler-date-table-row`).nth(2).locator('.dx-scheduler-date-table-cell').nth(0); + + await appointmentToMove.dragTo(cellToMove); + + const movedAppointment = page.locator(`#${SECOND_SCHEDULER_SELECTOR} .dx-scheduler-appointment`).filter({ hasText: TEST_APPOINTMENT.text }); + const timeText = await movedAppointment.locator('.dx-scheduler-appointment-content-date').textContent(); + expect(timeText).toContain(EXPECTED_APPOINTMENT_TIME); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/betweenSchedulers/dragAppointmentWithDataSource.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/betweenSchedulers/dragAppointmentWithDataSource.spec.ts new file mode 100644 index 000000000000..61e389f8c184 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/betweenSchedulers/dragAppointmentWithDataSource.spec.ts @@ -0,0 +1,75 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, getContainerUrl, setupTestPage, setStyleAttribute, appendElementTo } from '../../../../../playwright-helpers'; + +const containerUrl = getContainerUrl(__dirname, '../../../../../tests/container.html'); + +const FIRST_SCHEDULER_SELECTOR = 'scheduler-first'; +const SECOND_SCHEDULER_SELECTOR = 'scheduler-second'; +const EXPECTED_APPOINTMENT_TIME = '12:00 AM - 1:00 AM'; + +const TEST_APPOINTMENT = { + id: 10, + text: 'My appointment', + startDate: new Date(2021, 3, 28, 1), + endDate: new Date(2021, 3, 28, 2), +}; + +const getBaseSchedulerOptions = (currentDate: Date) => ({ + currentDate, + currentView: 'workWeek', + width: 600, + appointmentDragging: { + group: 'testDragGroup', + onRemove: new Function('e', 'e.component.deleteAppointment(e.itemData);') as any, + onAdd: new Function('e', 'e.component.addAppointment(e.itemData);') as any, + }, +}); + +test.describe('Drag-n-drop appointments between two schedulers with async DataSource (T1094033)', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + test('Should set correct start and end dates in drag&dropped appointment', async ({ page }) => { + await setStyleAttribute(page, '#container', 'display: flex;'); + await appendElementTo(page, '#container', 'div', { id: FIRST_SCHEDULER_SELECTOR }); + await appendElementTo(page, '#container', 'div', { id: SECOND_SCHEDULER_SELECTOR }); + + // TODO: Original test uses ClientFunction-based DataSourceMock for async data source + await page.evaluate(({ options, selector, appointments }: any) => { + class DataSourceMock { + key = 'id'; + private data: any[]; + constructor(initialData: any[] = []) { this.data = initialData; } + load = () => Promise.resolve(this.data); + insert = (value: any) => { this.data = [...this.data, value]; return Promise.resolve(); }; + update = (key: any, value: any) => { + this.data = this.data.map((item: any) => item.id === key ? value : item); + return Promise.resolve(); + }; + remove = (id: any) => { this.data = this.data.filter((item: any) => item.id !== id); return Promise.resolve(); }; + } + (window as any).DevExpress.fx.off = true; + ($(selector) as any).dxScheduler({ ...options, dataSource: new DataSourceMock(appointments) }); + }, { + options: getBaseSchedulerOptions(new Date(2021, 3, 26)), + selector: `#${FIRST_SCHEDULER_SELECTOR}`, + appointments: [TEST_APPOINTMENT], + }); + + await createWidget(page, 'dxScheduler', { + ...getBaseSchedulerOptions(new Date(2021, 4, 26)), + dataSource: [], + }, `#${SECOND_SCHEDULER_SELECTOR}`); + + const appointmentToMove = page.locator(`#${FIRST_SCHEDULER_SELECTOR} .dx-scheduler-appointment`).filter({ hasText: TEST_APPOINTMENT.text }); + const cellToMove = page.locator(`#${SECOND_SCHEDULER_SELECTOR} .dx-scheduler-date-table-row`).nth(0).locator('.dx-scheduler-date-table-cell').nth(0); + + await appointmentToMove.dragTo(cellToMove); + await page.waitForTimeout(500); + + const movedAppointment = page.locator(`#${SECOND_SCHEDULER_SELECTOR} .dx-scheduler-appointment`).filter({ hasText: TEST_APPOINTMENT.text }); + const timeText = await movedAppointment.locator('.dx-scheduler-appointment-content-date').textContent(); + expect(timeText).toContain(EXPECTED_APPOINTMENT_TIME); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/betweenSchedulers/removeDroppableCellClass.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/betweenSchedulers/removeDroppableCellClass.spec.ts new file mode 100644 index 000000000000..2421bfe6532d --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/betweenSchedulers/removeDroppableCellClass.spec.ts @@ -0,0 +1,56 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, getContainerUrl, setupTestPage, setStyleAttribute, appendElementTo } from '../../../../../playwright-helpers'; + +const containerUrl = getContainerUrl(__dirname, '../../../../../tests/container.html'); + +const FIRST_SCHEDULER_SELECTOR = 'scheduler-first'; +const SECOND_SCHEDULER_SELECTOR = 'scheduler-second'; +const METHODS_TO_CANCEL = ['onDragStart', 'onDragMove', 'onDragEnd', 'onRemove', 'onAdd']; + +const TEST_APPOINTMENT = { + id: 10, + text: 'My appointment', + startDate: new Date(2021, 3, 28, 1), + endDate: new Date(2021, 3, 28, 2), +}; + +const getSchedulerOptions = (dataSource: any[], currentDate: Date, cancelMethodName: string) => ({ + dataSource, + currentDate, + currentView: 'workWeek', + width: 600, + appointmentDragging: { + group: 'testDragGroup', + onRemove: new Function('e', 'e.component.deleteAppointment(e.itemData);') as any, + onAdd: new Function('e', 'e.component.addAppointment(e.itemData);') as any, + [cancelMethodName]: new Function('e', 'e.cancel = true;') as any, + }, +}); + +test.describe('Cancel drag-n-drop when dragging an appointment from one scheduler to another', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + METHODS_TO_CANCEL.forEach((methodName) => { + test(`Should remove drag-n-drop classes if event was canceled in method ${methodName}`, async ({ page }) => { + await setStyleAttribute(page, '#container', 'display: flex;'); + await appendElementTo(page, '#container', 'div', { id: FIRST_SCHEDULER_SELECTOR }); + await appendElementTo(page, '#container', 'div', { id: SECOND_SCHEDULER_SELECTOR }); + + await createWidget(page, 'dxScheduler', getSchedulerOptions([TEST_APPOINTMENT], new Date(2021, 3, 26), methodName), `#${FIRST_SCHEDULER_SELECTOR}`); + await createWidget(page, 'dxScheduler', getSchedulerOptions([], new Date(2021, 4, 26), methodName), `#${SECOND_SCHEDULER_SELECTOR}`); + + const appointmentToMove = page.locator(`#${FIRST_SCHEDULER_SELECTOR} .dx-scheduler-appointment`).filter({ hasText: TEST_APPOINTMENT.text }); + const cellToMove = page.locator(`#${SECOND_SCHEDULER_SELECTOR} .dx-scheduler-date-table-row`).nth(0).locator('.dx-scheduler-date-table-cell').nth(0); + + await appointmentToMove.dragTo(cellToMove); + + const droppableCellFirst = await page.locator(`#${FIRST_SCHEDULER_SELECTOR} .dx-scheduler-date-table-droppable-cell`).count(); + const droppableCellSecond = await page.locator(`#${SECOND_SCHEDULER_SELECTOR} .dx-scheduler-date-table-droppable-cell`).count(); + + expect(droppableCellFirst).toBe(0); + expect(droppableCellSecond).toBe(0); + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/cancelAppointmentDrag.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/cancelAppointmentDrag.spec.ts new file mode 100644 index 000000000000..5572d2d33daf --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/cancelAppointmentDrag.spec.ts @@ -0,0 +1,49 @@ +import { test, expect } from '@playwright/test'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('Cancel appointment Drag-and-Drop', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + +); + +const APPOINTMENT_DRAG_SOURCE_CLASS = '.dx-scheduler-appointment-drag-source'; + +test('on escape - date should not changed when it\'s pressed during dragging (T832754)', async ({ page }) => { + // Scheduler on '#container' + const draggableAppointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'Appointment' }); + await MouseUpEvents.disable(MouseAction.dragToElement); + + await t + .dragToElement(draggableAppointment.element, page.locator('.dx-scheduler-date-table-row').nth(4).locator('.dx-scheduler-date-table-cell').nth(0)) + .pressKey('esc'); + + await MouseUpEvents.enable(MouseAction.dragToElement); + + expect(page.locator('.dx-scheduler').find(APPOINTMENT_DRAG_SOURCE_CLASS).exists) + .notOk() + .expect(draggableAppointment.date.time) + .eql('10:00 AM - 10:30 AM'); +}).before(async () => createScheduler({ + _draggingMode: 'default', + height: 600, + views: ['day'], + currentView: 'day', + cellDuration: 30, + dataSource: [{ + text: 'Appointment', + startDate: new Date(2020, 9, 14, 10, 0), + endDate: new Date(2020, 9, 14, 10, 30), + }], + currentDate: new Date(2020, 9, 14), + showAllDayPanel: false, +})); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/dragAppointmentAfterResize.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/dragAppointmentAfterResize.spec.ts new file mode 100644 index 000000000000..f224fc66bb98 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/dragAppointmentAfterResize.spec.ts @@ -0,0 +1,79 @@ +import { test, expect } from '@playwright/test'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('Drag-n-drop appointment after resize (T835545)', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + +); + +['day', 'week', 'month', 'timelineDay', 'timelineWeek', 'timelineMonth'].forEach((view) => test(`After drag-n-drop appointment, size of appointment shouldn't change in the '${view}' view`, async (t) => { + // Scheduler on '#container' + const { element, resizableHandle } = page.locator('.dx-scheduler-appointment').filter({ hasText: 'app' }); + + const initSize = { + width: await element.clientWidth, + height: await element.clientHeight, + }; + + const isVertical = await resizableHandle.bottom.count !== 0; + + await t + .drag(isVertical ? resizableHandle.bottom : resizableHandle.right, 50, 50); + + const size = isVertical ? await element.clientHeight : await element.clientWidth; + expect(size) + .gt(isVertical ? initSize.height : initSize.width); + + const sizeBeforeDrag = { + width: await element.clientWidth, + height: await element.clientHeight, + }; + const positionBeforeDrag = { + left: await element.clientLeft, + top: await element.clientTop, + }; + + await t + .drag(element, 10, 10, { + offsetX: 0, + offsetY: 0, + }); + + const elementClientWidth = await element.clientWidth; + const elementClientHeight = await element.clientHeight; + + const elementClientLeft = await element.clientLeft; + const elementClientTop = await element.clientTop; + + expect(sizeBeforeDrag.width) + .eql(elementClientWidth) + + .expect(sizeBeforeDrag.height) + .eql(elementClientHeight) + + .expect(positionBeforeDrag.left) + .eql(elementClientLeft) + + .expect(positionBeforeDrag.top) + .eql(elementClientTop); +}).before(async () => createScheduler({ + views: [view], + currentView: view, + startDayHour: 9, + currentDate: new Date(2017, 4, 1), + dataSource: [{ + text: 'app', + startDate: new Date(2017, 4, 1, 9, 0), + endDate: new Date(2017, 4, 1, 10, 0), + }], +}))); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/dragEvents.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/dragEvents.spec.ts new file mode 100644 index 000000000000..426fd1bc40f4 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/dragEvents.spec.ts @@ -0,0 +1,187 @@ +import { test, expect } from '@playwright/test'; +import { createWidget } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('Scheduler dragging - drag events', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + +); + +const SCHEDULER_SELECTOR = '#container'; + +const initCallbackTesting = async () => { + await CallbackTestHelper.initClientTesting([ + 'onDragStartItemData', + 'onDragMoveItemData', + 'onDragEndItemData', + 'onDragEndToItemData', + ]); +}; + +const clearCallbackTesting = async () => { + await CallbackTestHelper.clearClientData([ + 'onDragStartItemData', + 'onDragMoveItemData', + 'onDragEndItemData', + 'onDragEndToItemData', + ]); +}; + +const collectEventsCallbackResults = async () => [ + await CallbackTestHelper.getClientResults('onDragStartItemData'), + await CallbackTestHelper.getClientResults('onDragMoveItemData'), + await CallbackTestHelper.getClientResults('onDragEndItemData'), + await CallbackTestHelper.getClientResults('onDragEndToItemData'), +]; + +const INITIAL_APPOINTMENT = { + text: 'Test', + startDate: '2023-01-01T01:00:00', + endDate: '2023-01-01T02:00:00', +}; +const TEST_CASES = [ + { + view: 'month', + expectedToItemData: { + text: 'Test', + startDate: '2023-01-05T01:00:00', + endDate: '2023-01-05T02:00:00', + }, + }, + { + view: 'week', + expectedToItemData: { + text: 'Test', + startDate: '2023-01-05T00:00:00', + endDate: '2023-01-05T01:00:00', + allDay: true, + }, + }, + { + view: 'timelineDay', + expectedToItemData: { + text: 'Test', + startDate: '2023-01-01T01:30:00', + endDate: '2023-01-01T02:30:00', + allDay: false, + }, + }, +]; + +TEST_CASES.forEach(({ view, expectedToItemData }) => { + test(`Should fire correct events with correct itemData inside during drag-n-drop in ${view} view.`, async ({ page }) => { + // --- setup --- +await initCallbackTesting(); + await createWidget(page, 'dxScheduler', { + dataSource: [INITIAL_APPOINTMENT], + currentView: view, + currentDate: '2023-01-01', + appointmentDragging: { + onDragStart: ({ itemData }) => { + (window as WindowCallbackExtended) + .clientTesting! + .addCallbackResult('onDragStartItemData', { ...itemData + // --- test --- +const scheduler = new Scheduler(SCHEDULER_SELECTOR); + const appointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'Test' }); + const targetCell = page.locator('.dx-scheduler-date-table-row').nth(0).locator('.dx-scheduler-date-table-cell').nth(4); + + await t + .dragToElement(appointment.element, targetCell, { speed: 0.5 }); + + const [ + onDragStartItemData, + onDragMoveItemData, + onDragEndItemData, + onDragEndToItemData, + ] = await collectEventsCallbackResults(); + + expect(onDragStartItemData.length).toBe(1) + .expect(onDragStartItemData[0]).toBe(INITIAL_APPOINTMENT); + + // eslint-disable-next-line no-restricted-syntax + for (const itemData of onDragMoveItemData) { + expect(itemData).toBe(INITIAL_APPOINTMENT); + } + + expect(onDragEndItemData.length).toBe(1) + .expect(onDragEndToItemData.length).toBe(1) + .expect(onDragEndItemData[0]) + .eql(INITIAL_APPOINTMENT) + .expect(onDragEndToItemData[0]) + .eql(expectedToItemData); +}); + }, + onDragMove: ({ itemData }) => { + (window as WindowCallbackExtended) + .clientTesting! + .addCallbackResult('onDragMoveItemData', { ...itemData }); + }, + onDragEnd: ({ itemData, toItemData }) => { + const clientTesting = (window as WindowCallbackExtended).clientTesting!; + clientTesting.addCallbackResult('onDragEndItemData', { ...itemData }); + clientTesting.addCallbackResult('onDragEndToItemData', { ...toItemData }); + }, + }, + }); + }).after(async () => { + await clearCallbackTesting(); + }); +}); + +test('Should block appointment dragging while onAppointmentUpdating Promise is pending (T1308596)', async ({ page }) => { + const scheduler = new Scheduler(SCHEDULER_SELECTOR); + const appointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'Test Appointment' }); + + const targetCell1 = page.locator('.dx-scheduler-date-table-row').nth(18).locator('.dx-scheduler-date-table-cell').nth(2); + const targetCell2 = page.locator('.dx-scheduler-date-table-row').nth(18).locator('.dx-scheduler-date-table-cell').nth(5); + + const initialPosition = await appointment.element.boundingClientRect; + + await /* TODO: dragToElement(appointment.element, targetCell1, { speed: 1 }) */; + await /* TODO: dragToElement(appointment.element, targetCell2, { speed: 1 }) */; + await /* TODO: dragToElement(appointment.element, targetCell2, { speed: 1 }) */; + await /* TODO: dragToElement(appointment.element, targetCell2, { speed: 1 }) */; + + await await page.waitForTimeout(6000); + + const positionAfterPromiseResolved = await appointment.element.boundingClientRect; + const cell1Position = await targetCell1.boundingClientRect; + + expect(positionAfterPromiseResolved.left) + .notEql(initialPosition.left) + .expect(positionAfterPromiseResolved.left) + .eql(cell1Position.left); +}); + +// TODO: .before() block not converted - move to test setup +// { + await createWidget(page, 'dxScheduler', { + dataSource: [{ + text: 'Test Appointment', + startDate: new Date(2023, 0, 2, 10, 0), + endDate: new Date(2023, 0, 2, 11, 0), + }], + views: ['week'], + currentView: 'week', + currentDate: new Date(2023, 0, 2), + height: 600, + onAppointmentUpdating: (e) => { + e.cancel = new Promise((resolve) => { + setTimeout(() => { + resolve(false); + }, 5000); + }); + }, + }); +}); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/externalDragging.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/externalDragging.spec.ts new file mode 100644 index 000000000000..ed233cff4ddf --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/externalDragging.spec.ts @@ -0,0 +1,65 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, appendElementTo } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('Drag-n-drop from another draggable area', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + +); + +test('Drag-n-drop an appointment when "cellDuration" changes dynamically', async ({ page }) => { + // --- setup --- +await appendElementTo('#container', 'div', 'drag-area'); + + await ClientFunction(() => { + $('
') + .text('New Brochures') + .addClass('item') + .appendTo('#drag-area'); + })(); + + await appendElementTo('#container', 'div', 'scheduler'); + + await createWidget(page, 'dxDraggable', { + group: 'draggableGroup', + data: { text: 'New Brochures' }, + onDragStart(e) { + e.itemData = e.fromData; + }, + }, '#group'); + + await createWidget(page, 'dxDraggable', { + group: 'draggableGroup', + }, '#drag-area'); + + return createScheduler({ + views: ['week'], + currentView: 'week', + appointmentDragging: { + group: 'draggableGroup', + onAdd(e) { + e.component.addAppointment(e.itemData); + e.itemElement.remove(); + }, + }, + }, '#scheduler'); + // --- test --- +// Scheduler on '#scheduler' + + await scheduler.option('cellDuration', 10); + + await t + .dragToElement(Selector('.item'), page.locator('.dx-scheduler-date-table-row').nth(0).locator('.dx-scheduler-date-table-cell').nth(0)) + .expect(page.locator('.dx-scheduler-appointment').nth(0).date.time) + .eql('9:00 AM - 9:10 AM'); +}); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/insideScheduler/removeDroppableCellClass.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/insideScheduler/removeDroppableCellClass.spec.ts new file mode 100644 index 000000000000..6173419cedf7 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/insideScheduler/removeDroppableCellClass.spec.ts @@ -0,0 +1,46 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, getContainerUrl, setupTestPage } from '../../../../../playwright-helpers'; + +const containerUrl = getContainerUrl(__dirname, '../../../../../tests/container.html'); + +const METHODS_TO_CANCEL = ['onDragStart', 'onDragMove', 'onDragEnd']; + +const TEST_APPOINTMENT = { + id: 10, + text: 'My appointment', + startDate: new Date(2021, 3, 28, 1), + endDate: new Date(2021, 3, 28, 2), +}; + +test.describe('Cancel drag-n-drop when dragging an appointment inside the scheduler', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + METHODS_TO_CANCEL.forEach((methodName) => { + test(`Should remove drag-n-drop classes if event was canceled in method ${methodName}`, async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource: [TEST_APPOINTMENT], + currentDate: new Date(2021, 3, 28), + currentView: 'workWeek', + width: 600, + appointmentDragging: { + [methodName]: new Function('e', 'e.cancel = true;') as any, + }, + }); + + const appointmentToMove = page.locator('.dx-scheduler-appointment').filter({ hasText: TEST_APPOINTMENT.text }); + const cellToMove = page.locator('.dx-scheduler-date-table-row').nth(1).locator('.dx-scheduler-date-table-cell').nth(0); + + await appointmentToMove.dragTo(cellToMove); + + const droppableCellCount = await page.locator('.dx-scheduler-date-table-droppable-cell').count(); + expect(droppableCellCount).toBe(0); + + const isDraggableSource = await appointmentToMove.evaluate( + (el) => el.classList.contains('dx-scheduler-appointment-drag-source'), + ); + expect(isDraggableSource).toBe(false); + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/outlookDragging/base.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/outlookDragging/base.spec.ts new file mode 100644 index 000000000000..d86fe968bb1d --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/outlookDragging/base.spec.ts @@ -0,0 +1,432 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../../tests/container.html')}`; + +test.describe('Outlook dragging base tests', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + +); + +test('Basic drag-n-drop movements in groups', async ({ page }) => { + // --- setup --- +await createWidget(page, 'dxScheduler', { + dataSource: [{ + text: 'Test', + startDate: new Date(2021, 1, 2), + endDate: new Date(2021, 1, 2, 1), + }], + views: ['timelineWeek'], + currentView: 'timelineWeek', + currentDate: new Date(2021, 1, 2), + cellDuration: 1440, + height: 300, + with: 500, + // --- test --- +// Scheduler on '#container' + + const draggableAppointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'Website Re-Design Plan' }); + + await /* TODO: drag */ await (draggableAppointment.element).click() /* drag(330, 70) */; + + await testScreenshot(page, 'drag-n-drop-to-orange-group.png', { element: page.locator('.dx-scheduler-work-space') }); + + await /* TODO: drag */ await (draggableAppointment.element).click() /* drag(-330, 70) */; + await testScreenshot(page, 'drag-n-drop-blue-group.png', { element: page.locator('.dx-scheduler-work-space') }); + + expect(compareResults.isValid()) + .ok(compareResults.errorMessages()); +}).before(async () => createWidget(page, 'dxScheduler', { + dataSource: [{ + text: 'Website Re-Design Plan', + startDate: new Date(2021, 2, 26, 8, 30), + endDate: new Date(2021, 2, 26, 11, 0), + priorityId: 1, + }], + groups: ['priorityId'], + resources: [{ + fieldExpr: 'priorityId', + allowMultiple: false, + dataSource: [{ + text: 'Low Priority', + id: 1, + color: '#1e90ff', + }, { + text: 'High Priority', + id: 2, + color: '#ff9747', + }], + label: 'Priority', + }], + views: ['day'], + currentView: 'day', + currentDate: new Date(2021, 2, 26), + startDayHour: 8, + height: 600, + width: 1000, +})); + +test('Basic drag-n-drop movements from tooltip in week view', async ({ page }) => { + // Scheduler on '#container' + + await (scheduler.collectors.find('2').click().element) + .expect(scheduler.appointmentTooltip.isVisible()).toBeTruthy() + .drag(scheduler.appointmentTooltip.getListItem('Appointment 3').element, 200, 50); + + await testScreenshot(page, 'drag-n-drop-\'Appointment 3\'-from-tooltip-in-week.png', { + element: page.locator('.dx-scheduler-work-space'), + }); + + await (scheduler.collectors.find('1').click().element) + .expect(scheduler.appointmentTooltip.isVisible()).toBeTruthy() + .drag(scheduler.appointmentTooltip.getListItem('Appointment 2').element, 350, 150); + + await testScreenshot(page, 'drag-n-drop-\'Appointment 2\'-from-tooltip-in-week.png', { + element: page.locator('.dx-scheduler-work-space'), + }); + + expect(compareResults.isValid()) + .ok(compareResults.errorMessages()); +}).before(async () => createWidget(page, 'dxScheduler', { + dataSource: [{ + text: 'Appointment 1', + startDate: new Date(2021, 2, 21, 9, 30), + endDate: new Date(2021, 2, 21, 12, 0), + }, { + text: 'Appointment 2', + startDate: new Date(2021, 2, 21, 9, 30), + endDate: new Date(2021, 2, 21, 12, 0), + }, { + text: 'Appointment 3', + startDate: new Date(2021, 2, 21, 9, 30), + endDate: new Date(2021, 2, 21, 11, 0), + }, { + text: 'Appointment 4', + startDate: new Date(2021, 2, 21, 9, 30), + endDate: new Date(2021, 2, 21, 12, 30), + }], + views: ['week'], + currentView: 'week', + currentDate: new Date(2021, 2, 21), + startDayHour: 8, + height: 600, + width: 1000, +})); + +test.meta({ runInTheme: Themes.genericLight })('Basic drag-n-drop movements from tooltip in month view', async (t) => { + // Scheduler on '#container' + + await (scheduler.collectors.find('2').click().element) + .expect(scheduler.appointmentTooltip.isVisible()).toBeTruthy() + .drag(scheduler.appointmentTooltip.getListItem('Appointment 3').element, -180, -30); + + await testScreenshot(page, 'drag-n-drop-\'Appointment 3\'-from-tooltip-in-month.png', { + element: page.locator('.dx-scheduler-work-space'), + }); + + await (scheduler.collectors.find('1', 1).click().element) + .expect(scheduler.appointmentTooltip.isVisible()).toBeTruthy() + .drag(scheduler.appointmentTooltip.getListItem('Appointment 2').element, 320, 150); + + await testScreenshot(page, 'drag-n-drop-\'Appointment 2\'-from-tooltip-in-month.png', { + element: page.locator('.dx-scheduler-work-space'), + }); + + expect(compareResults.isValid()) + .ok(compareResults.errorMessages()); +}).before(async () => createWidget(page, 'dxScheduler', { + dataSource: [{ + text: 'Appointment 1', + startDate: new Date(2021, 2, 31, 9, 30), + endDate: new Date(2021, 3, 1, 12, 0), + }, { + text: 'Appointment 2', + startDate: new Date(2021, 2, 31, 9, 30), + endDate: new Date(2021, 3, 1, 12, 0), + }, { + text: 'Appointment 3', + startDate: new Date(2021, 2, 31, 9, 30), + endDate: new Date(2021, 3, 1, 11, 0), + }, { + text: 'Appointment 4', + startDate: new Date(2021, 2, 31, 9, 30), + endDate: new Date(2021, 3, 1, 12, 30), + }], + views: ['month'], + currentView: 'month', + currentDate: new Date(2021, 2, 27), + startDayHour: 8, + height: 600, + width: 1000, +})); + +[{ + currentView: 'timelineWeek', + dataSource: [{ + text: 'Website Re-Design Plan', + startDate: new Date(2021, 2, 21, 9, 30), + endDate: new Date(2021, 2, 21, 10, 45), + }], +}, { + currentView: 'timelineMonth', + dataSource: [{ + text: 'Website Re-Design Plan', + startDate: new Date(2021, 2, 2, 9, 30), + endDate: new Date(2021, 2, 3, 11, 0), + }], +}].forEach(({ currentView, dataSource }) => { + test(`Basic drag-n-drop movements in ${currentView} view`, async ({ page }) => { + // Scheduler on '#container' + const draggableAppointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'Website Re-Design Plan' }); + + await /* TODO: drag */ await (draggableAppointment.element).click() /* drag(250, 0) */; + + await testScreenshot(page, `drag-n-drop-${currentView}-to-right.png`, { element: page.locator('.dx-scheduler-work-space') }); + + await /* TODO: drag */ await (draggableAppointment.element).click() /* drag(-250, 0) */; + + await testScreenshot(page, `drag-n-drop-${currentView}-to-left.png`, { element: page.locator('.dx-scheduler-work-space') }); + + expect(compareResults.isValid()) + .ok(compareResults.errorMessages()); + }).before(async () => createWidget(page, 'dxScheduler', { + dataSource, + views: ['timelineWeek', 'timelineMonth'], + currentView, + currentDate: new Date(2021, 2, 21), + startDayHour: 9, + height: 600, + width: 1000, + })); +}); + +test('Basic drag-n-drop movements', async ({ page }) => { + // Scheduler on '#container' + const draggableAppointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'Website Re-Design Plan' }); + + await t.drag(draggableAppointment.element, 100, 0, { speed: 0.5 }); + + await testScreenshot(page, 'drag-n-drop-to-right.png', { element: page.locator('.dx-scheduler-work-space') }); + + await t.drag(draggableAppointment.element, -100, 0, { speed: 0.5 }); + + await testScreenshot(page, 'drag-n-drop-to-left.png', { element: page.locator('.dx-scheduler-work-space') }); + + await t.drag(draggableAppointment.element, 0, 100, { speed: 0.5 }); + + await testScreenshot(page, 'drag-n-drop-to-bottom.png', { element: page.locator('.dx-scheduler-work-space') }); + + await t.drag(draggableAppointment.element, 0, -100, { speed: 0.5 }); + + await testScreenshot(page, 'drag-n-drop-to-top.png', { element: page.locator('.dx-scheduler-work-space') }); + + expect(compareResults.isValid()) + .ok(compareResults.errorMessages()); +}).before(async () => createWidget(page, 'dxScheduler', { + dataSource: [{ + text: 'Website Re-Design Plan', + startDate: new Date(2021, 2, 22, 10), + endDate: new Date(2021, 2, 22, 12, 30), + }], + views: ['week'], + currentView: 'week', + currentDate: new Date(2021, 2, 22), + startDayHour: 9, + height: 600, + width: 1000, +})); + +test('Basic drag-n-drop movements with mouse offset', async ({ page }) => { + // Scheduler on '#container' + const draggableAppointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'Website Re-Design Plan' }); + + await t.drag(draggableAppointment.element, 100, 0, { offsetX: 10, offsetY: 200, speed: 0.5 }); + await testScreenshot(page, 'drag-n-drop-mouse-offset-to-right.png', { element: page.locator('.dx-scheduler-work-space') }); + + await t.drag(draggableAppointment.element, -100, 0, { offsetX: 10, offsetY: 200, speed: 0.5 }); + await testScreenshot(page, 'drag-n-drop-mouse-offset-to-left.png', { element: page.locator('.dx-scheduler-work-space') }); + + await t.drag(draggableAppointment.element, 0, 100, { offsetX: 10, offsetY: 200, speed: 0.5 }); + await testScreenshot(page, 'drag-n-drop-mouse-offset-to-bottom.png', { element: page.locator('.dx-scheduler-work-space') }); + + await t.drag(draggableAppointment.element, 0, -100, { offsetX: 10, offsetY: 200, speed: 0.5 }); + await testScreenshot(page, 'drag-n-drop-mouse-offset-to-top.png', { element: page.locator('.dx-scheduler-work-space') }); + + expect(compareResults.isValid()) + .ok(compareResults.errorMessages()); +}).before(async () => createWidget(page, 'dxScheduler', { + dataSource: [{ + text: 'Website Re-Design Plan', + startDate: new Date(2021, 2, 22, 10), + endDate: new Date(2021, 2, 22, 12, 30), + }], + views: ['week'], + currentView: 'week', + currentDate: new Date(2021, 2, 22), + startDayHour: 9, + height: 600, + width: 1000, +})); + +test('Basic drag-n-drop all day appointment movements', async ({ page }) => { + // Scheduler on '#container' + const draggableAppointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'Website Re-Design Plan' }); + + await t.drag(draggableAppointment.element, 200, 0, { speed: 0.1 }); + await testScreenshot(page, 'drag-n-drop-all-day-to-right.png', { element: page.locator('.dx-scheduler-work-space') }); + + await t.drag(draggableAppointment.element, -200, 0, { speed: 0.1 }); + await testScreenshot(page, 'drag-n-drop-all-day-to-left.png', { element: page.locator('.dx-scheduler-work-space') }); + + await t.drag(draggableAppointment.element, 260, 270, { speed: 0.1 }); + await testScreenshot(page, 'drag-n-drop-all-day-to-bottom.png', { element: page.locator('.dx-scheduler-work-space') }); + + await t.drag(draggableAppointment.element, 0, -260, { speed: 0.1 }); + await testScreenshot(page, 'drag-n-drop-all-day-to-top.png', { element: page.locator('.dx-scheduler-work-space') }); + + expect(compareResults.isValid()) + .ok(compareResults.errorMessages()); +}).before(async () => createWidget(page, 'dxScheduler', { + dataSource: [{ + text: 'Website Re-Design Plan', + startDate: new Date(2021, 2, 23, 10), + endDate: new Date(2021, 2, 25, 12, 30), + }], + views: ['week'], + currentView: 'week', + currentDate: new Date(2021, 2, 23), + startDayHour: 9, + height: 600, + width: 1000, +})); + +test('Basic drag-n-drop movements within the cell', async ({ page }) => { + // Scheduler on '#container' + const draggableAppointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'Website Re-Design Plan' }); + + async function blurActiveElement(page: Page) { + await page.evaluate(() => { + const el = document.activeElement as HTMLElement | null; + el?.blur(); + }, ); +} + + await /* TODO: drag */ await (draggableAppointment.element).click() /* drag(55, 0) */; + await blurActiveElement(); + await testScreenshot(page, 'drag-n-drop-within-cell-to-right.png', { element: page.locator('.dx-scheduler-work-space') }); + + await /* TODO: drag */ await (draggableAppointment.element).click() /* drag(-50, 0) */; + await blurActiveElement(); + await testScreenshot(page, 'drag-n-drop-within-cell-to-left.png', { element: page.locator('.dx-scheduler-work-space') }); + + await /* TODO: drag */ await (draggableAppointment.element).click() /* drag(0, 30) */; + await blurActiveElement(); + await testScreenshot(page, 'drag-n-drop-within-cell-to-bottom.png', { element: page.locator('.dx-scheduler-work-space') }); + + expect(compareResults.isValid()) + .ok(compareResults.errorMessages()); +}).before(async () => createWidget(page, 'dxScheduler', { + dataSource: [{ + text: 'Website Re-Design Plan', + startDate: new Date(2021, 2, 22, 10), + endDate: new Date(2021, 2, 22, 12, 30), + }], + views: ['week'], + currentView: 'week', + currentDate: new Date(2021, 2, 22), + startDayHour: 9, + height: 600, + width: 1000, +})); + +test('Basic drag-n-drop small appointments', async ({ page }) => { + // Scheduler on '#container' + const draggableAppointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'Website Re-Design Plan' }); + + await /* TODO: drag */ await (draggableAppointment.element).click() /* drag(250, 0) */; + await testScreenshot(page, 'drag-n-drop-small-appoint-to-right.png', { element: page.locator('.dx-scheduler-work-space') }); + + await /* TODO: drag */ await (draggableAppointment.element).click() /* drag(-250, 0) */; + await testScreenshot(page, 'drag-n-drop-small-appoint-to-left.png', { element: page.locator('.dx-scheduler-work-space') }); + + await /* TODO: drag */ await (draggableAppointment.element).click() /* drag(0, 170) */; + await testScreenshot(page, 'drag-n-drop-small-appoint-to-bottom.png', { element: page.locator('.dx-scheduler-work-space') }); + + await /* TODO: drag */ await (draggableAppointment.element).click() /* drag(0, -170) */; + await testScreenshot(page, 'drag-n-drop-small-appoint-to-top.png', { element: page.locator('.dx-scheduler-work-space') }); + + expect(compareResults.isValid()) + .ok(compareResults.errorMessages()); +}).before(async () => createWidget(page, 'dxScheduler', { + dataSource: [{ + text: 'Website Re-Design Plan', + startDate: new Date(2021, 2, 17, 10), + endDate: new Date(2021, 2, 17, 12, 30), + }], + views: ['month'], + currentView: 'month', + currentDate: new Date(2021, 2, 17), + startDayHour: 9, + height: 600, + width: 1000, +})); + +test('Basic drag-n-drop long appointments', async ({ page }) => { + // Scheduler on '#container' + const draggableAppointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'Website Re-Design Plan' }); + + await /* TODO: drag */ await (draggableAppointment.element).click() /* drag(150, 0) */; + await testScreenshot(page, 'drag-n-drop-long-appoint-to-right.png', { element: page.locator('.dx-scheduler-work-space') }); + + await /* TODO: drag */ await (draggableAppointment.element).click() /* drag(-30, 0) */; + await testScreenshot(page, 'drag-n-drop-long-appoint-to-left.png', { element: page.locator('.dx-scheduler-work-space') }); + + await /* TODO: drag */ await (draggableAppointment.element).click() /* drag(0, 70) */; + await testScreenshot(page, 'drag-n-drop-long-appoint-to-bottom.png', { element: page.locator('.dx-scheduler-work-space') }); + + await /* TODO: drag */ await (draggableAppointment.element).click() /* drag(0, -70) */; + await testScreenshot(page, 'drag-n-drop-long-appoint-to-top.png', { element: page.locator('.dx-scheduler-work-space') }); + + expect(compareResults.isValid()) + .ok(compareResults.errorMessages()); +}).before(async () => createWidget(page, 'dxScheduler', { + dataSource: [{ + text: 'Website Re-Design Plan', + startDate: new Date(2021, 2, 16, 10), + endDate: new Date(2021, 2, 18, 12, 30), + }], + views: ['month'], + currentView: 'month', + currentDate: new Date(2021, 2, 16), + startDayHour: 9, + height: 600, + width: 1000, +})); + +test('Narrow appointment dragging on minimal distance should be expected(1171520)', async ({ page }) => { + // Scheduler on '#container' + await t.drag(page.locator('.dx-scheduler-appointment').filter({ hasText: 'Test' }).element, -10, 0, { offsetX: 10 }); + + await testScreenshot(page, 'drag-short-app-min-dist-to-left.png', { element: page.locator('.dx-scheduler-work-space') }); + + await t.drag(page.locator('.dx-scheduler-appointment').filter({ hasText: 'Test' }).element, 195, 0, { offsetX: 10 }); + + await testScreenshot(page, 'drag-short-app-to-right.png', { element: page.locator('.dx-scheduler-work-space') }); + + await t.drag(page.locator('.dx-scheduler-appointment').filter({ hasText: 'Test' }).element, 200, 0, { offsetX: 10 }); + + await testScreenshot(page, 'drag-short-app-to-right-on-next-cell.png', { element: page.locator('.dx-scheduler-work-space') }); + + expect(compareResults.isValid()) + .ok(compareResults.errorMessages()); +}); +}); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/outlookDragging/schedulerInContainer/schedulerInContainer.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/outlookDragging/schedulerInContainer/schedulerInContainer.spec.ts new file mode 100644 index 000000000000..99069cba84c3 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/outlookDragging/schedulerInContainer/schedulerInContainer.spec.ts @@ -0,0 +1,63 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../../../tests/container.html')}`; + +test.describe('Outlook dragging, for case scheduler in container', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + +); + +test('Dragging should be work right in case dxScheduler placed in dxTabPanel', async ({ page }) => { + // --- setup --- +await createWidget(page, 'dxTabPanel', { + items: [{ + title: 'Info', + text: 'This is Info Tab', + }, { + title: 'Contacts', + text: 'This is Contacts Tab', + disabled: true, + }], + itemTemplate: ClientFunction(() => ($('
') as any).dxScheduler({ + dataSource: [{ + text: 'Website Re-Design Plan', + startDate: new Date(2021, 2, 30, 11), + endDate: new Date(2021, 2, 30, 12), + }], + views: ['week', 'month'], + currentView: 'week', + currentDate: new Date(2021, 2, 28), + startDayHour: 9, + height: 600, + })), + // --- test --- +// Scheduler on '.dx-scheduler' + + const draggableAppointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'Website Re-Design Plan' }); + + await /* TODO: drag */ await (draggableAppointment.element).click() /* drag(0, 120) */; + + await testScreenshot(page, 'dxScheduler-placed-in-dxTabPanel-drag-to-bottom.png'); + + await /* TODO: drag */ await (draggableAppointment.element).click() /* drag(0, -170) */; + + await testScreenshot(page, 'dxScheduler-placed-in-dxTabPanel-drag-to-top.png'); + + await /* TODO: drag */ await (draggableAppointment.element).click() /* drag(100, 0) */; + + await testScreenshot(page, 'dxScheduler-placed-in-dxTabPanel-drag-to-right.png'); + + expect(compareResults.isValid()) + .ok(compareResults.errorMessages()); +}); +}); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/outlookDragging/schedulerInContainer/schedulerInTransformContainer.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/outlookDragging/schedulerInContainer/schedulerInTransformContainer.spec.ts new file mode 100644 index 000000000000..4d3a911ba3b9 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/outlookDragging/schedulerInContainer/schedulerInTransformContainer.spec.ts @@ -0,0 +1,66 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, setStyleAttribute, appendElementTo } from '../../../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../../../tests/container.html')}`; + +test.describe('Outlook dragging, for case scheduler in container with transform style', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + +appendElementTo, + setStyleAttribute, +} from '../../../../../../helpers/domUtils'; + +); + +test('Dragging should be work right in case dxScheduler placed in container with transform style', async ({ page }) => { + // --- setup --- +await setStyleAttribute(Selector('#container'), 'margin-top: 100px; margin-left: 100px; transform: translate(0px, 0px);'); + await appendElementTo('#container', 'div', 'scheduler'); + + return createWidget(page, 'dxScheduler', { + dataSource: [{ + text: 'Website Re-Design Plan', + startDate: new Date(2021, 2, 24, 11), + endDate: new Date(2021, 2, 24, 12), + }], + views: ['workWeek'], + currentView: 'workWeek', + currentDate: new Date(2021, 2, 22), + startDayHour: 9, + height: 600, + width: 800, + }, '#scheduler'); + // --- test --- +// Scheduler on '#scheduler' + + const draggableAppointment = page.locator('.dx-scheduler-appointment').nth(0); + + await t + .drag(draggableAppointment.element, 0, 120); + + await testScreenshot(page, 'dxScheduler-placed-in-transform-container-drag-to-bottom.png'); + + await /* TODO: drag */ await (draggableAppointment.element).click() /* drag(0, -170) */; + + await testScreenshot(page, 'dxScheduler-placed-in-transform-container-drag-to-top.png'); + + await /* TODO: drag */ await (draggableAppointment.element).click() /* drag(100, 0) */; + + await testScreenshot(page, 'dxScheduler-placed-in-transform-container-drag-to-right.png'); + + await /* TODO: drag */ await (draggableAppointment.element).click() /* drag(-230, 0) */; + + await testScreenshot(page, 'dxScheduler-placed-in-transform-container-drag-to-left.png'); + + expect(compareResults.isValid()) + .ok(compareResults.errorMessages()); +}); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/outlookDragging/shiftedContainer.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/outlookDragging/shiftedContainer.spec.ts new file mode 100644 index 000000000000..e2a7cf9f54d8 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/outlookDragging/shiftedContainer.spec.ts @@ -0,0 +1,59 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, setStyleAttribute } from '../../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../../tests/container.html')}`; + +test.describe('Outlook dragging base tests in shifted container', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + +); + +test('Basic drag-n-drop movements in shifted container', async ({ page }) => { + // --- setup --- +await setStyleAttribute(Selector('#container'), 'margin-left: 50px; margin-top: 70px;'); + + return createWidget(page, 'dxScheduler', { + dataSource: [{ + text: 'Website Re-Design Plan', + startDate: new Date(2021, 2, 22, 10), + endDate: new Date(2021, 2, 22, 12, 30), + }], + views: ['week'], + currentView: 'week', + currentDate: new Date(2021, 2, 22), + startDayHour: 9, + height: 600, + width: 950, + // --- test --- +// Scheduler on '#container' + const draggableAppointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'Website Re-Design Plan' }); + + await t.drag(draggableAppointment.element, 100, 0, { speed: 0.5 }); + + await testScreenshot(page, 'drag-n-drop-to-right-in-shifted-container.png', { element: page.locator('.dx-scheduler-work-space') }); + + await t.drag(draggableAppointment.element, -100, 0, { speed: 0.5 }); + + await testScreenshot(page, 'drag-n-drop-to-left-in-shifted-container.png', { element: page.locator('.dx-scheduler-work-space') }); + + await t.drag(draggableAppointment.element, 0, 100, { speed: 0.5 }); + + await testScreenshot(page, 'drag-n-drop-to-bottom-in-shifted-container.png', { element: page.locator('.dx-scheduler-work-space') }); + + await t.drag(draggableAppointment.element, 0, -100, { speed: 0.5 }); + + await testScreenshot(page, 'drag-n-drop-to-top-in-shifted-container.png', { element: page.locator('.dx-scheduler-work-space') }); + + expect(compareResults.isValid()) + .ok(compareResults.errorMessages()); +}); +}); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/timeline.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/timeline.spec.ts new file mode 100644 index 000000000000..9004ecc3c04b --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/timeline.spec.ts @@ -0,0 +1,50 @@ +import { test, expect } from '@playwright/test'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('Drag-and-drop appointments in the Scheduler timeline views', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + +); + +['timelineDay', 'timelineWeek', 'timelineWorkWeek'].forEach((view) => test(`Drag-n-drop in the "${view}" view`, async ({ page }) => { + // Scheduler on '#container' + const draggableAppointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'Brochure Design Review' }); + + await t + .dragToElement(draggableAppointment.element, page.locator('.dx-scheduler-date-table-row').nth(0).locator('.dx-scheduler-date-table-cell').nth(4)) + .expect(draggableAppointment.size.width).toBe('200px') + .expect(draggableAppointment.date.time) + .eql('11:00 AM - 11:30 AM'); +}).before(async () => createScheduler({ + views: [view], + currentView: view, + dataSource, +}))); + +test('Drag-n-drop in the "timelineMonth" view', async ({ page }) => { + // Scheduler on '#container' + const draggableAppointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'Brochure Design Review' }); + + await t + .dragToElement(draggableAppointment.element, page.locator('.dx-scheduler-date-table-row').nth(0).locator('.dx-scheduler-date-table-cell').nth(4)) + .expect(parseInt(await draggableAppointment.size.height, 10)) + .within(139, 140) + .expect(draggableAppointment.size.width) + .eql('200px') + .expect(draggableAppointment.date.time) + .eql('9:00 AM - 9:30 AM'); +}).before(async () => createScheduler({ + views: ['timelineMonth'], + currentView: 'timelineMonth', + dataSource, +})); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/verticalGrouping.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/verticalGrouping.spec.ts new file mode 100644 index 000000000000..86ab1cce9ae0 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/verticalGrouping.spec.ts @@ -0,0 +1,67 @@ +import { test, expect } from '@playwright/test'; +import { testScreenshot } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('Drag-and-drop appointments in the Scheduler with vertical grouping', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + +); + +test('Should drag appoinment to the previous day`s cell (T1025952)', async ({ page }) => { + // Scheduler on '#container' + const appointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'appointment' }); + + await /* TODO: dragToElement(appointment.element, page.locator('.dx-scheduler-date-table-row').nth(1).locator('.dx-scheduler-date-table-cell').nth(1) */); + + await testScreenshot(page, 'drag-n-drop-previous-day-cell.png', { element: page.locator('.dx-scheduler-work-space') }); + + expect(compareResults.isValid()) + .ok(compareResults.errorMessages()); +}).before(async () => createScheduler({ + dataSource: [ + { + text: 'appointment', + startDate: new Date(2021, 3, 21, 9, 30), + endDate: new Date(2021, 3, 21, 10), + priorityId: 1, + }, + ], + views: [ + { + type: 'week', + groupOrientation: 'vertical', + }, + ], + currentView: 'week', + currentDate: new Date(2021, 3, 21), + groups: ['priorityId'], + resources: [ + { + dataSource: [ + { + text: 'Low Priority', + id: 1, + }, { + text: 'High Priority', + id: 2, + }, + ], + fieldExpr: 'priorityId', + displayExpr: 'name', + allowMultiple: false, + }, + ], + startDayHour: 9, + endDayHour: 12, + height: 600, +})); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/grouping/groupHeaderLongNamesCss.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/grouping/groupHeaderLongNamesCss.spec.ts new file mode 100644 index 000000000000..6f10abe859ff --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/grouping/groupHeaderLongNamesCss.spec.ts @@ -0,0 +1,146 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, insertStylesheetRulesToPage, setupTestPage } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +const resources = [ + { + text: 'Very Long Priority Name for High Priority Tasks and Urgent Matters', + id: 1, + color: '#ff9747', + }, + { + text: 'Extremely Long Priority Name for Medium Priority Tasks and Regular Work', + id: 2, + color: '#24ff50', + }, + { + text: 'Super Long Priority Name for Low Priority Tasks and Background Activities', + id: 3, + color: '#3366ff', + }, +]; + +const dataSource = [ + { + text: 'Team Meeting', + startDate: new Date(2021, 3, 21, 10, 0), + endDate: new Date(2021, 3, 21, 11, 30), + priorityId: 1, + }, + { + text: 'Code Review', + startDate: new Date(2021, 3, 21, 14, 0), + endDate: new Date(2021, 3, 21, 15, 0), + priorityId: 2, + }, + { + text: 'Planning Session', + startDate: new Date(2021, 3, 22, 9, 0), + endDate: new Date(2021, 3, 22, 12, 0), + priorityId: 3, + }, +]; + +const DEFAULT_OPTIONS = { + currentDate: new Date(2021, 3, 21), + height: 600, + width: 1000, + startDayHour: 9, + endDayHour: 16, + crossScrollingEnabled: true, + showCurrentTimeIndicator: false, + showAllDayPanel: false, + groups: ['priorityId'], + views: [{ + type: 'workWeek', + name: 'Vertical Grouping', + groupOrientation: 'vertical', + cellDuration: 60, + intervalCount: 2, + }, + { + type: 'workWeek', + name: 'Horizontal Grouping', + groupOrientation: 'horizontal', + cellDuration: 30, + intervalCount: 2, + }, { + type: 'month', + name: 'Group By Date', + groupOrientation: 'horizontal', + }, 'agenda'], + resources: [{ + fieldExpr: 'priorityId', + allowMultiple: false, + dataSource: resources, + label: 'Priority', + }], + dataSource, +}; + +const CELL_SIZE_CSS = ` + #container .dx-scheduler-group-header { + width: auto; + } + #container .dx-scheduler-group-flex-container, + #container .dx-scheduler-work-space-vertical-group-table, + #container .dx-scheduler-sidebar-scrollable { + flex: 0 0 auto; + } +`; + +test.describe('Scheduler: Group Header CSS for Long Resource Names', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + test('Group header CSS should work with vertical grouping and long resource names', async ({ page }) => { + await insertStylesheetRulesToPage(page, CELL_SIZE_CSS); + await createWidget(page, 'dxScheduler', { ...DEFAULT_OPTIONS, currentView: 'Vertical Grouping' }); + + const groupHeaders = page.locator('.dx-scheduler-group-header'); + await expect(groupHeaders.first()).toBeVisible(); + + await testScreenshot(page, 'group-header-css-vertical-grouping-long-names.png', { + element: page.locator('.dx-scheduler'), + }); + }); + + test('Group header CSS should work with horizontal grouping and long resource names', async ({ page }) => { + await insertStylesheetRulesToPage(page, CELL_SIZE_CSS); + await createWidget(page, 'dxScheduler', { ...DEFAULT_OPTIONS, currentView: 'Horizontal Grouping' }); + + const groupHeaders = page.locator('.dx-scheduler-group-header'); + await expect(groupHeaders.first()).toBeVisible(); + + await testScreenshot(page, 'group-header-css-horizontal-grouping-long-names.png', { + element: page.locator('.dx-scheduler'), + }); + }); + + test('Group header CSS should work with group by date and long resource names', async ({ page }) => { + await insertStylesheetRulesToPage(page, CELL_SIZE_CSS); + await createWidget(page, 'dxScheduler', { ...DEFAULT_OPTIONS, currentView: 'Group By Date', groupByDate: true }); + + const groupHeaders = page.locator('.dx-scheduler-group-header'); + await expect(groupHeaders.first()).toBeVisible(); + + await testScreenshot(page, 'group-header-css-group-by-date-long-names.png', { + element: page.locator('.dx-scheduler'), + }); + }); + + test('Group header CSS should work with agenda view and long resource names', async ({ page }) => { + await insertStylesheetRulesToPage(page, CELL_SIZE_CSS); + await createWidget(page, 'dxScheduler', { ...DEFAULT_OPTIONS, currentView: 'agenda' }); + + const groupHeaders = page.locator('.dx-scheduler-group-header'); + await expect(groupHeaders.first()).toBeVisible(); + + await testScreenshot(page, 'group-header-css-agenda-view-long-names.png', { + element: page.locator('.dx-scheduler'), + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/grouping/groupingByDate.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/grouping/groupingByDate.spec.ts new file mode 100644 index 000000000000..14101b106a00 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/grouping/groupingByDate.spec.ts @@ -0,0 +1,90 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, setupTestPage } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +const priorityData = [ + { + text: 'Low Priority', + id: 1, + color: '#1e90ff', + }, { + text: 'High Priority', + id: 2, + color: '#ff9747', + }, +]; + +const dataSource = [ + { + text: 'Website Re-Design Plan', + priorityId: 2, + startDate: new Date(2018, 4, 21, 9, 30), + endDate: new Date(2018, 4, 21, 11, 30), + }, { + text: 'Book Flights to San Fran for Sales Trip', + priorityId: 1, + startDate: new Date(2018, 4, 24, 10, 0), + endDate: new Date(2018, 4, 24, 12, 0), + }, { + text: 'Install New Router in Dev Room', + priorityId: 1, + startDate: new Date(2018, 4, 20, 13), + endDate: new Date(2018, 4, 20, 15, 30), + }, +]; + +const createScheduler = async (page: any, options = {}) => { + await createWidget(page, 'dxScheduler', { + views: ['week'], + dataSource: [], + resources: [ + { + fieldExpr: 'priorityId', + allowMultiple: false, + dataSource: priorityData, + label: 'Priority', + }, + ], + groups: ['priorityId'], + crossScrollingEnabled: true, + groupByDate: false, + width: 1666, + height: 833, + startDayHour: 9, + firstDayOfWeek: 1, + maxAppointmentsPerCell: 5, + currentView: 'week', + currentDate: new Date(2018, 4, 21), + ...options, + }); +}; + +test.describe('Drag-and-drop appointments into allDay panel in the grouped Scheduler', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + test('Drag-n-drop between dateTable and allDay panel, groupByDate=true', async ({ page }) => { + await createScheduler(page, { + dataSource, + groupByDate: true, + }); + + const draggableAppointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'Website Re-Design Plan' }); + const allDayCell = page.locator('.dx-scheduler-all-day-table-cell').nth(1); + + await draggableAppointment.dragTo(allDayCell); + + await expect(draggableAppointment).toBeVisible(); + + const isAllDay = await page.evaluate(() => { + const instance = ($('#container') as any).dxScheduler('instance'); + const appointments = instance.option('dataSource'); + const appt = appointments.find((a: any) => a.text === 'Website Re-Design Plan'); + return appt?.allDay === true; + }); + expect(isAllDay).toBe(true); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/grouping/monthViewVerticalGrouping.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/grouping/monthViewVerticalGrouping.spec.ts new file mode 100644 index 000000000000..9de66edd0c58 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/grouping/monthViewVerticalGrouping.spec.ts @@ -0,0 +1,59 @@ +import { test } from '@playwright/test'; +import { createWidget, testScreenshot, setupTestPage } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('Month view vertical grouping', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + test('Scrolling: usual. Shouldn\'t overlap the next group with long all-day appointment in the month view (T1122185)', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource: [ + { + text: 'Appointment group 1', + groupId: 1, + startDate: '2021-04-29T14:00:00Z', + endDate: '2021-06-20T14:00:00Z', + allDay: true, + }, + ], + views: [{ + type: 'month', + groupOrientation: 'vertical', + }], + currentView: 'month', + currentDate: '2021-04-29T00:00:00Z', + groups: ['groupId'], + resources: [ + { + fieldExpr: 'groupId', + allowMultiple: false, + dataSource: [{ + text: 'Group 1', + id: 1, + color: '#ff0000', + }, { + text: 'Group 2', + id: 2, + color: '#0000ff', + }], + label: 'Group', + }, + ], + }); + + const workSpace = page.locator('.dx-scheduler-work-space'); + const nextButton = page.locator('.dx-scheduler-navigator-next'); + + await testScreenshot(page, 'month-view_vertical-grouping_fist-app-part_T1122185.png', { element: workSpace }); + + await nextButton.click(); + await testScreenshot(page, 'month-view_vertical-grouping_middle-app-part_T1122185.png', { element: workSpace }); + + await nextButton.click(); + await testScreenshot(page, 'month-view_vertical-grouping_last-app-part_T1122185.png', { element: workSpace }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/grouping/overflow.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/grouping/overflow.spec.ts new file mode 100644 index 000000000000..933fc45e36b2 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/grouping/overflow.spec.ts @@ -0,0 +1,82 @@ +import { test } from '@playwright/test'; +import { createWidget, testScreenshot, setupTestPage } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('Scheduler: Grouping overflow', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + ['week', 'month'].forEach((viewType) => { + ['vertical', 'horizontal'].forEach((groupOrientation) => { + ['hidden', 'allDay'].forEach((allDayPanelMode) => { + [[9, 14, 60], [0, 24, 360]].forEach(([startDayHour, endDayHour, cellDuration]) => { + const allParams = `${viewType}-${groupOrientation}-${allDayPanelMode}-${startDayHour}-${endDayHour}`; + + test(`Long appointments should not overflow group view (${allParams})`, async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource: [ + { + text: '1', + priorityId: 1, + startDate: '2021-04-19T16:30:00', + endDate: '2021-04-25T18:30:00', + }, { + text: '2', + priorityId: 2, + startDate: '2021-04-19T16:30:00', + endDate: '2021-04-25T18:30:00', + }, { + text: '3', + priorityId: 3, + startDate: '2021-04-19T16:30:00', + endDate: '2021-04-25T18:30:00', + }, + ], + views: [{ + type: viewType, + name: 'myView', + groupOrientation, + }], + cellDuration, + currentView: 'myView', + currentDate: new Date(2021, 3, 21), + allDayPanelMode, + startDayHour, + endDayHour, + groups: ['priorityId'], + resources: [ + { + fieldExpr: 'priorityId', + dataSource: [ + { + text: 'Low Priority', + id: 1, + color: '#1e90ff', + }, { + text: 'High Priority', + id: 2, + color: '#ff9747', + }, + { + text: 'Custom', + id: 3, + color: 'red', + }, + ], + }, + ], + showAllDayPanel: false, + }); + + await testScreenshot(page, `group-overflow-(${allParams}).png`, { + element: page.locator('.dx-scheduler'), + }); + }); + }); + }); + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/grouping/resourceCellTemplate.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/grouping/resourceCellTemplate.spec.ts new file mode 100644 index 000000000000..dfc2322fc07b --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/grouping/resourceCellTemplate.spec.ts @@ -0,0 +1,47 @@ +import { test, expect } from '@playwright/test'; +import { setupTestPage } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('ResourceCellTemplate', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + test('resourceCellTemplate layout should be rendered right in the agenda view', async ({ page }) => { + const currentDate = new Date(2017, 4, 25); + + await page.evaluate(({ date }) => { + const currentDateValue = new Date(date); + (window as any).DevExpress.fx.off = true; + ($('#container') as any).dxScheduler({ + dataSource: [{ + text: 'appointment', + startDate: currentDateValue, + endDate: currentDateValue, + resource: 1, + }], + views: ['agenda'], + currentView: 'agenda', + currentDate: currentDateValue, + resourceCellTemplate() { + return 'Custom resource text'; + }, + groups: ['resource'], + resources: [{ + fieldExpr: 'resource', + dataSource: [{ + text: 'Resource text', + id: 1, + }], + label: 'Resource', + }], + height: 600, + }); + }, { date: currentDate.toISOString() }); + + const groupHeader = page.locator('.dx-scheduler-group-header').first(); + await expect(groupHeader).toHaveText('Custom resource text'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/grouping/smoothCellLines.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/grouping/smoothCellLines.spec.ts new file mode 100644 index 000000000000..02adcc847f8a --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/grouping/smoothCellLines.spec.ts @@ -0,0 +1,37 @@ +import { test } from '@playwright/test'; +import { createWidget, testScreenshot, setupTestPage } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +const resourcesData = [...Array(20).keys()].map((num: number) => ({ + text: `Name ${num}`, + id: num, +})); + +test.describe('Scheduler: SmoothCellLines', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + test('The group panel and date table stay in sync during scrolling on material themes (T1146448)', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource: [], + views: ['timelineWeek'], + currentView: 'timelineWeek', + groups: ['ownerId'], + currentDate: new Date(2021, 1, 2), + resources: [{ fieldExpr: 'ownerId', dataSource: resourcesData, label: 'Owner' }], + height: 600, + }); + + const scrollable = page.locator('.dx-scheduler-date-table-scrollable .dx-scrollable-container'); + await scrollable.evaluate((el) => { el.scrollTop = 1100; }); + + await page.waitForTimeout(300); + + await testScreenshot(page, 'scrolling-vertical', { + element: page.locator('.dx-scheduler-work-space'), + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/header/customization.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/header/customization.spec.ts new file mode 100644 index 000000000000..eaec342ac2dc --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/header/customization.spec.ts @@ -0,0 +1,79 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, setupTestPage } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +const customToolbarItems = [ + { + location: 'before', + name: 'dateNavigator', + options: { + items: [ + { key: 'today', text: 'Today' }, + 'prev', + 'next', + 'dateInterval', + ], + }, + }, + { + location: 'before', + locateInMenu: 'auto', + widget: 'dxButton', + options: { icon: 'plus' }, + }, + 'viewSwitcher', +]; + +test.describe('Scheduler header customization', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + test('Scheduler default toolbar should works', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + currentDate: new Date(2021, 3, 27), + }); + + await testScreenshot(page, 'scheduler-default toolbar.png', { + element: page.locator('.dx-scheduler-header'), + }); + }); + + test('Scheduler toolbar should be hided', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + currentDate: new Date(2021, 3, 27), + toolbar: { + visible: false, + items: [ + { location: 'before', name: 'viewSwitcher' }, + { location: 'after', name: 'dateNavigator' }, + ], + }, + }); + + await expect(page.locator('.dx-scheduler-header')).not.toBeVisible(); + + await testScreenshot(page, 'scheduler-hidden-toolbar.png', { + element: page.locator('.dx-scheduler'), + }); + }); + + [ + { toolbar: { items: customToolbarItems }, description: 'custom toolbar' }, + { toolbar: { items: ['today', 'dateNavigator', 'viewSwitcher'] }, description: 'toolbar with today' }, + { toolbar: { disabled: true, items: customToolbarItems }, description: 'disabled toolbar' }, + ].forEach(({ toolbar, description }) => { + test(`Scheduler ${description} should works`, async ({ page }) => { + await createWidget(page, 'dxScheduler', { + currentDate: new Date(2021, 3, 27), + toolbar, + }); + + await testScreenshot(page, `scheduler-${description}.png`, { + element: page.locator('.dx-scheduler-header'), + }); + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/header/dateNavigator.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/header/dateNavigator.spec.ts new file mode 100644 index 000000000000..e9fc6ac5d8d2 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/header/dateNavigator.spec.ts @@ -0,0 +1,147 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, setupTestPage } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('Date navigator', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + [{ + agendaDuration: 20, + result: '11-30 May 2021', + }, { + agendaDuration: 40, + result: '11 May-19 Jun 2021', + }].forEach(({ agendaDuration, result }) => { + test(`Caption of date navigator should be valid after change view to Agenda with agendaDuration=${agendaDuration}`, async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource: [], + views: [{ + type: 'agenda', + agendaDuration, + }, 'month'], + currentView: 'month', + currentDate: new Date(2021, 4, 11), + height: 600, + }); + + const viewSwitcherMonthButton = page.locator('.dx-scheduler-view-switcher .dx-buttongroup .dx-button').filter({ hasText: 'Month' }); + await viewSwitcherMonthButton.click(); + await page.keyboard.press('ArrowDown'); + await page.keyboard.press('ArrowDown'); + await page.keyboard.press('Enter'); + + const caption = page.locator('.dx-scheduler-navigator-caption'); + await expect(caption).toHaveText(result); + }); + }); + + test('Current date in Calendar should be respond on prev and next buttons of Navigator', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource: [], + views: ['week'], + currentView: 'week', + currentDate: new Date(2021, 2, 28), + width: 600, + height: 400, + }); + + const caption = page.locator('.dx-scheduler-navigator-caption'); + const nextButton = page.locator('.dx-scheduler-navigator-next'); + const prevButton = page.locator('.dx-scheduler-navigator-previous'); + + await caption.click(); + await testScreenshot(page, 'initial-calendar-state.png'); + + await nextButton.click(); + await nextButton.click(); + await nextButton.click(); + await caption.click(); + await testScreenshot(page, 'calendar-state-after-next-button-click.png'); + + await prevButton.click(); + await prevButton.click(); + await prevButton.click(); + await prevButton.click(); + await prevButton.click(); + await prevButton.click(); + await caption.click(); + await testScreenshot(page, 'calendar-state-after-prev-button-click.png'); + }); + + test('Current date in Navigator should be respond on Current date of Calendar', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource: [], + views: ['week'], + currentView: 'week', + currentDate: new Date(2021, 2, 28), + width: 600, + height: 400, + }); + + const caption = page.locator('.dx-scheduler-navigator-caption'); + + await caption.click(); + + const calendarNextButton = page.locator('.dx-calendar .dx-calendar-navigator-next-view'); + const calendarPrevButton = page.locator('.dx-calendar .dx-calendar-navigator-previous-view'); + const calendarCells = page.locator('.dx-calendar-body td.dx-calendar-cell'); + + await calendarNextButton.click(); + await calendarCells.nth(20).click(); + + await testScreenshot(page, 'navigator-state-after-calendar-next-button-click.png'); + + await caption.click(); + await calendarPrevButton.click(); + await calendarPrevButton.click(); + await calendarCells.nth(15).click(); + + await testScreenshot(page, 'navigator-state-after-calendar-prev-button-click.png'); + }); + + test('Current date in navigator should be updated if scheduler currentDate is changed', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource: [], + views: ['week'], + currentView: 'week', + currentDate: new Date(2021, 2, 28), + width: 600, + height: 400, + }); + + await page.evaluate(() => { + ($('#container') as any).dxScheduler('instance').option('currentDate', new Date(2022, 2, 28)); + }); + + const caption = page.locator('.dx-scheduler-navigator-caption'); + await caption.click(); + + await testScreenshot( + page, + 'navigator-state-after-change-currentDate-option.png', + { element: page.locator('.dx-calendar') }, + ); + }); + + test('Calendar should be have right appearance', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource: [], + views: ['week'], + currentView: 'week', + currentDate: new Date(2021, 2, 28), + }); + + const caption = page.locator('.dx-scheduler-navigator-caption'); + await caption.click(); + + await testScreenshot( + page, + 'right-calendar-appearance.png', + { element: page.locator('.dx-calendar') }, + ); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/header/header.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/header/header.spec.ts new file mode 100644 index 000000000000..5b546c3cfbef --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/header/header.spec.ts @@ -0,0 +1,178 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, insertStylesheetRulesToPage, setupTestPage } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('Scheduler header', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + test('dateNavigator buttons should not be selected after clicking', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + currentView: 'day', + views: ['day'], + height: 580, + }); + + const nextButton = page.locator('.dx-scheduler-navigator-next'); + const prevButton = page.locator('.dx-scheduler-navigator-previous'); + const caption = page.locator('.dx-scheduler-navigator-caption'); + + await nextButton.click(); + + await expect(prevButton).not.toHaveClass(/dx-item-selected/); + await expect(caption).not.toHaveClass(/dx-item-selected/); + await expect(nextButton).not.toHaveClass(/dx-item-selected/); + }); + + test('dateNavigator buttons should have "contained" styling mode with generic theme', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + currentView: 'day', + views: ['day'], + height: 580, + }); + + const nextButton = page.locator('.dx-scheduler-navigator-next'); + const prevButton = page.locator('.dx-scheduler-navigator-previous'); + const caption = page.locator('.dx-scheduler-navigator-caption'); + + await expect(prevButton).toHaveClass(/dx-button-mode-contained|dx-button-mode-text/); + await expect(caption).toHaveClass(/dx-button-mode-contained|dx-button-mode-text/); + await expect(nextButton).toHaveClass(/dx-button-mode-contained|dx-button-mode-text/); + }); + + const testData = [ + { + text: 'Website Re-Design Plan', + startDate: new Date('2021-03-29T16:30:00.000Z'), + endDate: new Date('2021-03-29T18:30:00.000Z'), + }, { + text: 'Book Flights to San Fran for Sales Trip', + startDate: new Date('2021-03-29T19:00:00.000Z'), + endDate: new Date('2021-03-29T20:00:00.000Z'), + allDay: true, + }, { + text: 'Install New Router in Dev Room', + startDate: new Date('2021-03-29T21:30:00.000Z'), + endDate: new Date('2021-03-29T22:30:00.000Z'), + }, { + text: 'Approve Personal Computer Upgrade Plan', + startDate: new Date('2021-03-30T17:00:00.000Z'), + endDate: new Date('2021-03-30T18:00:00.000Z'), + }, { + text: 'Final Budget Review', + startDate: new Date('2021-03-30T19:00:00.000Z'), + endDate: new Date('2021-03-30T20:35:00.000Z'), + }, { + text: 'New Brochures', + startDate: new Date('2021-03-30T21:30:00.000Z'), + endDate: new Date('2021-03-30T22:45:00.000Z'), + }, { + text: 'Install New Database', + startDate: new Date('2021-03-31T16:45:00.000Z'), + endDate: new Date('2021-03-31T18:15:00.000Z'), + }, { + text: 'Approve New Online Marketing Strategy', + startDate: new Date('2021-03-31T19:00:00.000Z'), + endDate: new Date('2021-03-31T21:00:00.000Z'), + }, { + text: 'Upgrade Personal Computers', + startDate: new Date('2021-03-31T22:15:00.000Z'), + endDate: new Date('2021-03-31T23:30:00.000Z'), + }, { + text: 'Customer Workshop', + startDate: new Date('2021-04-01T18:00:00.000Z'), + endDate: new Date('2021-04-01T19:00:00.000Z'), + allDay: true, + }, { + text: 'Prepare 2021 Marketing Plan', + startDate: new Date('2021-04-01T18:00:00.000Z'), + endDate: new Date('2021-04-01T20:30:00.000Z'), + }, { + text: 'Brochure Design Review', + startDate: new Date('2021-04-01T21:00:00.000Z'), + endDate: new Date('2021-04-01T22:30:00.000Z'), + }, { + text: 'Create Icons for Website', + startDate: new Date('2021-04-02T17:00:00.000Z'), + endDate: new Date('2021-04-02T18:30:00.000Z'), + }, { + text: 'Upgrade Server Hardware', + startDate: new Date('2021-04-02T21:30:00.000Z'), + endDate: new Date('2021-04-02T23:00:00.000Z'), + }, { + text: 'Submit New Website Design', + startDate: new Date('2021-04-02T23:30:00.000Z'), + endDate: new Date('2021-04-03T01:00:00.000Z'), + }, { + text: 'Launch New Website', + startDate: new Date('2021-04-02T19:20:00.000Z'), + endDate: new Date('2021-04-02T21:00:00.000Z'), + }, + ]; + + const SCROLLBAR_STYLES = ` + ::-webkit-scrollbar { + -webkit-appearance: none; + width: 7px; + } + ::-webkit-scrollbar-thumb { + border-radius: 4px; + background-color: rgba(0, 0, 0, .5); + -webkit-box-shadow: 0 0 1px rgba(255, 255, 255, .5); + } + .dx-scheduler-date-table-scrollable .dx-scrollable-container { + overflow: scroll !important; + } + `; + + test('Scheduler: maintain layout after horizontal scroll (T1306971)', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + timeZone: 'America/Los_Angeles', + dataSource: testData, + views: ['week'], + currentView: 'week', + currentDate: new Date(2021, 2, 28), + startDayHour: 9, + height: 730, + crossScrollingEnabled: true, + width: 500, + }); + + await insertStylesheetRulesToPage(page, SCROLLBAR_STYLES); + + await page.waitForTimeout(100); + + await page.evaluate(() => { + ($('#container') as any).dxScheduler('instance').repaint(); + }); + + await page.waitForTimeout(100); + + await testScreenshot(page, 'T1306971__scheduler__horizontal-scrolling__before.png', { + element: page.locator('.dx-scheduler'), + }); + + const maxScrollLeft = await page.evaluate(() => { + const container = document.querySelector('.dx-scheduler-date-table-scrollable .dx-scrollable-container'); + if (!container) return 0; + return container.scrollWidth - container.clientWidth; + }); + + const scrollableContainer = page.locator('.dx-scheduler-date-table-scrollable .dx-scrollable-container'); + await scrollableContainer.evaluate((el, scrollLeft) => { el.scrollLeft = scrollLeft; }, maxScrollLeft); + + const finalScrollLeft = await scrollableContainer.evaluate((el) => el.scrollLeft); + + expect(maxScrollLeft).toBeGreaterThan(0); + expect(finalScrollLeft).toBeGreaterThan(0); + + await page.waitForTimeout(500); + + await testScreenshot(page, 'T1306971__scheduler__horizontal-scrolling__after.png', { + element: page.locator('.dx-scheduler'), + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/header/header_material.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/header/header_material.spec.ts new file mode 100644 index 000000000000..3abf8272f503 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/header/header_material.spec.ts @@ -0,0 +1,63 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, setupTestPage, isMaterialBased, isMaterial } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('Scheduler header: material theme', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + test('dateNavigator buttons should have "text" styling mode', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + currentView: 'day', + views: ['day'], + height: 580, + }); + + const expectedClass = isMaterialBased() ? 'dx-button-mode-text' : 'dx-button-mode-contained'; + + const nextButton = page.locator('.dx-scheduler-navigator-next'); + const prevButton = page.locator('.dx-scheduler-navigator-previous'); + const caption = page.locator('.dx-scheduler-navigator-caption'); + + await expect(prevButton).toHaveClass(new RegExp(expectedClass)); + await expect(caption).toHaveClass(new RegExp(expectedClass)); + await expect(nextButton).toHaveClass(new RegExp(expectedClass)); + }); + + test('viewSwitcher dropdown button popup should have a specified class', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + currentView: 'day', + views: ['day', 'week'], + height: 580, + }); + + const dropDownButton = page.locator('.dx-scheduler-view-switcher .dx-dropdownbutton'); + await dropDownButton.click(); + + const viewSwitcherDropDownButtonContent = page.locator('.dx-scheduler-view-switcher-dropdown-button-content'); + const count = await viewSwitcherDropDownButtonContent.count(); + + expect(count).toBe(isMaterial() ? 1 : 0); + }); + + test('The toolbar should not display if the config is empty', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + currentDate: new Date(2020, 2, 2), + currentView: 'day', + views: ['day', 'week'], + height: 580, + toolbar: { items: [] }, + }); + + await testScreenshot(page, 'scheduler-with-empty-toolbar-config.png'); + + await page.evaluate(() => { + ($('#container') as any).dxScheduler('instance').option('toolbar', { items: ['viewSwitcher'] }); + }); + + await testScreenshot(page, 'scheduler-with-non-empty-toolbar-config.png'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/header/multiline_header.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/header/multiline_header.spec.ts new file mode 100644 index 000000000000..202c7c9631e4 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/header/multiline_header.spec.ts @@ -0,0 +1,43 @@ +import { test } from '@playwright/test'; +import { createWidget, testScreenshot, setupTestPage } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +const buttons = Array.from({ length: 12 }).map((_, index) => ({ + location: 'before', + locateInMenu: 'auto', + widget: 'dxButton', + options: { text: `Button ${index}` }, +})); + +test.describe('Scheduler multiline header', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + [true, false].forEach((multiline) => { + test(`Scheduler [multiline=${multiline}] toolbar`, async ({ page }) => { + await createWidget(page, 'dxScheduler', { + views: ['day', 'week', 'workWeek', 'month'], + currentView: 'workWeek', + currentDate: new Date(2021, 3, 27), + height: 200, + toolbar: { + multiline, + items: [ + 'dateNavigator', + ...buttons, + 'viewSwitcher', + ], + }, + }); + + await testScreenshot( + page, + `scheduler-multiline-${multiline}-toolbar.png`, + { element: page.locator('.dx-scheduler-header') }, + ); + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/header/sizes.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/header/sizes.spec.ts new file mode 100644 index 000000000000..73d0bdb0b9d9 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/header/sizes.spec.ts @@ -0,0 +1,48 @@ +import { test } from '@playwright/test'; +import { createWidget, testScreenshot, setupTestPage } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +const buttons = Array.from({ length: 4 }).map((_, index) => ({ + location: 'before', + locateInMenu: 'auto', + widget: 'dxButton', + options: { text: `Button ${index}` }, +})); + +test.describe('Scheduler header sizes', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + test('items inside toolbar menu should stretch', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + width: 320, + currentDate: new Date('2025-05-02T07:59:01.167Z'), + toolbar: { + items: ['today', 'dateNavigator', ...buttons, { + location: 'after', + locateInMenu: 'auto', + name: 'viewSwitcher', + }], + }, + }); + + const menuButton = page.locator('.dx-scheduler-header .dx-toolbar-menu-container .dx-dropdownmenu, .dx-scheduler-header .dx-toolbar-menu-container .dx-button'); + await menuButton.click(); + + await testScreenshot(page, 'scheduler-toolbar-menu.png'); + }); + + test('Scheduler header should have correct sizes', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + currentDate: new Date('2025-05-02T07:59:01.167Z'), + toolbar: { items: ['today', 'dateNavigator', ...buttons, 'viewSwitcher'] }, + }); + + await testScreenshot(page, 'scheduler-toolbar.png', { + element: page.locator('.dx-scheduler-header'), + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/header/todayButton.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/header/todayButton.spec.ts new file mode 100644 index 000000000000..a6fe8ed12f41 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/header/todayButton.spec.ts @@ -0,0 +1,56 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, setupTestPage } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('Scheduler header today button', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + test('Scheduler today button should works', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + currentDate: new Date(2021, 3, 27), + toolbar: { items: ['today', 'dateNavigator', 'viewSwitcher'] }, + }); + + const todayButton = page.locator('.dx-scheduler-header .dx-button').filter({ hasText: /today/i }).first(); + await todayButton.click(); + + const currentDate = await page.evaluate(() => { + const instance = ($('#container') as any).dxScheduler('instance'); + return instance.option('currentDate'); + }); + + const today = new Date(); + const currentDateObj = new Date(currentDate); + currentDateObj.setHours(0, 0, 0, 0); + today.setHours(0, 0, 0, 0); + + expect(currentDateObj.getTime()).toBe(today.getTime()); + }); + + test('Scheduler today button should use indicatorTime', async ({ page }) => { + const indicatorTime = new Date(2023, 3, 27); + + await createWidget(page, 'dxScheduler', { + currentDate: new Date(2021, 3, 27), + indicatorTime, + toolbar: { items: ['today', 'dateNavigator', 'viewSwitcher'] }, + }); + + const todayButton = page.locator('.dx-scheduler-header .dx-button').filter({ hasText: /today/i }).first(); + await todayButton.click(); + + const currentDate = await page.evaluate(() => { + const instance = ($('#container') as any).dxScheduler('instance'); + return instance.option('currentDate'); + }); + + const currentDateObj = new Date(currentDate); + expect(currentDateObj.getFullYear()).toBe(indicatorTime.getFullYear()); + expect(currentDateObj.getMonth()).toBe(indicatorTime.getMonth()); + expect(currentDateObj.getDate()).toBe(indicatorTime.getDate()); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/header/toolbar_option_change.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/header/toolbar_option_change.spec.ts new file mode 100644 index 000000000000..2fa87d46fe0f --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/header/toolbar_option_change.spec.ts @@ -0,0 +1,103 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, setupTestPage } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +const SCHEDULER_SELECTOR = '#container'; + +const createSchedulerWidget = async (page: any) => { + await createWidget(page, 'dxScheduler', { + views: ['day', 'week'], + currentView: 'day', + currentDate: new Date(2021, 3, 27), + height: 200, + width: 500, + }); +}; + +const buttons = Array.from({ length: 7 }).map((_, index) => ({ + location: 'before', + locateInMenu: 'auto', + widget: 'dxButton', + options: { text: `Button ${index}` }, +})); + +const setSchedulerOption = async (page: any, optionPath: string, value: any) => { + await page.evaluate(({ sel, opt, val }) => { + ($(sel) as any).dxScheduler('instance').option(opt, val); + }, { sel: SCHEDULER_SELECTOR, opt: optionPath, val: value }); +}; + +test.describe('Scheduler: Toolbar options change', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + test('Scheduler should change toolbar item location', async ({ page }) => { + await createSchedulerWidget(page); + + await setSchedulerOption(page, 'toolbar.items[0].location', 'after'); + + await testScreenshot(page, 'scheduler-toolbar-location-changed.png', { + element: page.locator('.dx-scheduler-header'), + }); + }); + + test('Scheduler should change toolbar', async ({ page }) => { + await createSchedulerWidget(page); + + await setSchedulerOption(page, 'toolbar', { items: [{ template: 'Custom text' }] }); + + await testScreenshot(page, 'scheduler-toolbar-changed.png', { + element: page.locator('.dx-scheduler-header'), + }); + }); + + test('Scheduler should hide and show toolbar', async ({ page }) => { + await createSchedulerWidget(page); + + await setSchedulerOption(page, 'toolbar.visible', false); + await expect(page.locator('.dx-scheduler-header')).not.toBeVisible(); + + await setSchedulerOption(page, 'toolbar.visible', true); + await expect(page.locator('.dx-scheduler-header')).toBeVisible(); + }); + + test('Scheduler should change toolbar items', async ({ page }) => { + await createSchedulerWidget(page); + + await setSchedulerOption(page, 'toolbar.items', buttons); + + await testScreenshot(page, 'scheduler-toolbar-items-changed.png', { + element: page.locator('.dx-scheduler-header'), + }); + }); + + test('Scheduler should change toolbar item option', async ({ page }) => { + await createSchedulerWidget(page); + + await setSchedulerOption(page, 'toolbar.items[0].options.text', 'Changed text'); + + await testScreenshot(page, 'scheduler-toolbar-item-option-changed.png', { + element: page.locator('.dx-scheduler-header'), + }); + }); + + test('Scheduler should change toolbar options / integration', async ({ page }) => { + await createSchedulerWidget(page); + + await setSchedulerOption(page, 'toolbar.items', buttons); + await setSchedulerOption(page, 'toolbar.multiline', true); + + await testScreenshot(page, 'scheduler-toolbar-property-changed.png', { + element: page.locator('.dx-scheduler-header'), + }); + + await setSchedulerOption(page, 'toolbar', { multiline: false }); + + await testScreenshot(page, 'scheduler-toolbar-changed-2.png', { + element: page.locator('.dx-scheduler-header'), + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/header/viewSwitcher.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/header/viewSwitcher.spec.ts new file mode 100644 index 000000000000..bc91aaa662d3 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/header/viewSwitcher.spec.ts @@ -0,0 +1,114 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, setupTestPage } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('Scheduler header - View switcher', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + test('it should correctly switch a differently typed views (T1080992)', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + width: 800, + height: 600, + views: [ + 'day', + { + type: 'week', + name: 'Some week', + }, + ], + }); + + const dayButton = page.locator('.dx-scheduler-view-switcher .dx-buttongroup .dx-button').filter({ hasText: 'Day' }); + await dayButton.click(); + await page.keyboard.press('ArrowDown'); + await page.keyboard.press('ArrowDown'); + await page.keyboard.press('Enter'); + + const someWeekButton = page.locator('.dx-scheduler-view-switcher .dx-buttongroup .dx-button').filter({ hasText: 'Some week' }); + await someWeekButton.click(); + + const isWeekView = await page.evaluate(() => { + const instance = ($('#container') as any).dxScheduler('instance'); + return instance.option('currentView') === 'Some week'; + }); + expect(isWeekView).toBe(true); + + await page.keyboard.press('ArrowDown'); + await page.keyboard.press('ArrowDown'); + await page.keyboard.press('Enter'); + + const isDayView = await page.evaluate(() => { + const instance = ($('#container') as any).dxScheduler('instance'); + return instance.option('currentView') === 'day'; + }); + expect(isDayView).toBe(true); + }); + + const defaultSelectBoxValue = 'Samantha Bright'; + + test('Changing view does not reset toolbar items state', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + views: ['week', 'month'], + currentView: 'week', + currentDate: new Date(2021, 3, 27), + toolbar: { + items: [ + { + location: 'before', + widget: 'dxSelectBox', + options: { items: [defaultSelectBoxValue] }, + }, + 'viewSwitcher', + ], + }, + }); + + const selectBox = page.locator('.dx-selectbox'); + await selectBox.click(); + const listItem = page.locator('.dx-list-item').first(); + await listItem.click(); + + const selectBoxValue = await selectBox.locator('input').inputValue(); + expect(selectBoxValue).toBe(defaultSelectBoxValue); + + await page.keyboard.press('Tab'); + await page.keyboard.press('Enter'); + await page.keyboard.press('ArrowDown'); + await page.keyboard.press('ArrowDown'); + await page.keyboard.press('Enter'); + + const monthButton = page.locator('.dx-scheduler-view-switcher .dx-buttongroup .dx-button').filter({ hasText: 'Month' }); + await monthButton.click(); + + const isMonthView = await page.evaluate(() => { + const instance = ($('#container') as any).dxScheduler('instance'); + return instance.option('currentView') === 'month'; + }); + expect(isMonthView).toBe(true); + + const selectBoxValueAfter = await selectBox.locator('input').inputValue(); + expect(selectBoxValueAfter).toBe(defaultSelectBoxValue); + }); + + [true, false].forEach((useDropDownViewSwitcher) => { + test(`view switcher should not be displayed if views has only one element when useDropDownViewSwitcher: ${useDropDownViewSwitcher}`, async ({ page }) => { + await createWidget(page, 'dxScheduler', { + currentDate: new Date(2020, 2, 2), + currentView: 'day', + views: ['day'], + useDropDownViewSwitcher, + height: 580, + }); + + await testScreenshot( + page, + `toolbar-without-view-switcher-(useDropDownViewSwitcher=${useDropDownViewSwitcher}).png`, + { element: page.locator('.dx-scheduler-header') }, + ); + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/hotkeysBehaviour/hotkeysBehaviour.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/hotkeysBehaviour/hotkeysBehaviour.spec.ts new file mode 100644 index 000000000000..f256f37f3e03 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/hotkeysBehaviour/hotkeysBehaviour.spec.ts @@ -0,0 +1,127 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, getContainerUrl, setupTestPage } from '../../../../playwright-helpers'; + +const containerUrl = getContainerUrl(__dirname, '../../../../tests/container.html'); + +const hotkeyDataSource = [ + { text: 'Website Re-Design Plan', startDate: new Date(2019, 3, 1, 9, 0), endDate: new Date(2019, 3, 1, 11, 30) }, + { text: 'Book Flights to San Fran for Sales Trip', startDate: new Date(2019, 3, 1, 9, 0), endDate: new Date(2019, 3, 1, 11, 30) }, + { text: 'Install New Router in Dev Room', startDate: new Date(2019, 3, 1, 9, 0), endDate: new Date(2019, 3, 1, 11, 30) }, + { text: 'Approve Personal Computer Upgrade Plan', startDate: new Date(2019, 3, 1, 9, 0), endDate: new Date(2019, 3, 1, 11, 30) }, + { text: 'Final Budget Review', startDate: new Date(2019, 3, 1, 9, 0), endDate: new Date(2019, 3, 1, 11, 30) }, + { text: 'New Brochures', startDate: new Date(2019, 3, 1, 9, 0), endDate: new Date(2019, 3, 1, 11, 30) }, + { text: 'Install New Database', startDate: new Date(2019, 3, 1, 9, 0), endDate: new Date(2019, 3, 1, 11, 30) }, + { text: 'Approve New Online Marketing Strategy', startDate: new Date(2019, 3, 1, 9, 0), endDate: new Date(2019, 3, 1, 11, 30) }, +]; + +const defaultSchedulerOptions = { + views: ['month'], + dataSource: [], + width: 1402, + height: 833, + startDayHour: 9, + firstDayOfWeek: 1, + maxAppointmentsPerCell: 5, + currentView: 'month', + currentDate: new Date(2019, 3, 1), +}; + +test.describe('Hotkeys for appointments update and navigation', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + ['week', 'month'].forEach((view) => { + test(`Navigate between appointments in the "${view}" view (Tab/Shift+Tab)`, async ({ page }) => { + await createWidget(page, 'dxScheduler', { + ...defaultSchedulerOptions, + views: [view], + currentView: view, + dataSource: hotkeyDataSource, + }); + + const firstAppointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'Website Re-Design Plan' }); + const secondAppointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'Book Flights to San Fran for Sales Trip' }); + + await firstAppointment.click(); + let isFocused = await firstAppointment.evaluate((el) => el.classList.contains('dx-state-focused')); + expect(isFocused).toBe(true); + + await page.keyboard.press('Tab'); + isFocused = await firstAppointment.evaluate((el) => el.classList.contains('dx-state-focused')); + expect(isFocused).toBe(false); + isFocused = await secondAppointment.evaluate((el) => el.classList.contains('dx-state-focused')); + expect(isFocused).toBe(true); + + await page.keyboard.press('Shift+Tab'); + isFocused = await secondAppointment.evaluate((el) => el.classList.contains('dx-state-focused')); + expect(isFocused).toBe(false); + isFocused = await firstAppointment.evaluate((el) => el.classList.contains('dx-state-focused')); + expect(isFocused).toBe(true); + }); + + test(`Remove appointment in the "${view}" view (Del)`, async ({ page }) => { + await createWidget(page, 'dxScheduler', { + ...defaultSchedulerOptions, + views: [view], + currentView: view, + dataSource: hotkeyDataSource, + }); + + const appointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'Website Re-Design Plan' }); + await appointment.click(); + await page.keyboard.press('Delete'); + await expect(appointment).not.toBeVisible(); + }); + + test(`Show appointment popup in the "${view}" view (Enter)`, async ({ page }) => { + await createWidget(page, 'dxScheduler', { + ...defaultSchedulerOptions, + views: [view], + currentView: view, + dataSource: hotkeyDataSource, + }); + + const appointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'Website Re-Design Plan' }); + await appointment.click(); + await page.keyboard.press('Enter'); + await expect(page.locator('.dx-scheduler-appointment-popup')).toBeVisible(); + }); + + test(`Navigate between tooltip appointments in the "${view}" view (Up/Down)`, async ({ page }) => { + await createWidget(page, 'dxScheduler', { + ...defaultSchedulerOptions, + views: [view], + currentView: view, + dataSource: hotkeyDataSource, + }); + + const collector = page.locator('.dx-scheduler-appointment-collector').filter({ hasText: '3' }); + await collector.click(); + await expect(page.locator('.dx-scheduler-appointment-tooltip-wrapper')).toBeVisible(); + + await page.keyboard.press('ArrowDown'); + await page.keyboard.press('ArrowUp'); + await page.keyboard.press('Enter'); + + await expect(page.locator('.dx-scheduler-appointment-tooltip-wrapper')).not.toBeVisible(); + await expect(page.locator('.dx-scheduler-appointment-popup')).toBeVisible(); + }); + }); + + test('Navigate between toolbar items', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + ...defaultSchedulerOptions, + views: ['day', 'week'], + currentView: 'day', + }); + + const toolbar = page.locator('.dx-scheduler-header'); + await toolbar.click(); + await page.keyboard.press('Tab'); + + const prevButton = page.locator('.dx-scheduler-navigator-previous'); + const isFocused = await prevButton.evaluate((el) => el.classList.contains('dx-state-focused')); + expect(isFocused).toBe(true); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/keyboardNavigation/appointments.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/keyboardNavigation/appointments.spec.ts new file mode 100644 index 000000000000..34623ea91f5a --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/keyboardNavigation/appointments.spec.ts @@ -0,0 +1,145 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, getContainerUrl, setupTestPage, insertStylesheetRulesToPage } from '../../../../playwright-helpers'; + +const containerUrl = getContainerUrl(__dirname, '../../../../tests/container.html'); + +const SCHEDULER_SELECTOR = '#container'; + +const colors = [ + '#74d57b', '#1db2f5', '#f5564a', '#97c95c', '#ffc720', '#eb3573', + '#a63db8', '#ffaa66', '#2dcdc4', '#c34cb9', '#3d44ec', '#4ddcca', + '#2ec98d', '#ef9e44', '#45a5cc', '#a067bd', '#3d44ec', '#4ddcca', + '#3ff6ca', '#f665aa', '#d1c974', '#ff6741', '#ee53dc', '#795ac3', + '#ff7d8a', '#4cd482', '#9d67cc', '#5ab1ef', '#68e18f', '#4dd155', +]; + +const resources = colors.map((color, index) => ({ text: `Resource ${index + 1}`, id: index + 1, color })); +const resourceCount = 30; + +const getPseudoRandomDuration = (durationState: number): number => { + const durationMin = Math.floor((durationState % 23) / 3 + 5) * 15; + return durationMin * 60 * 1000; +}; + +const generateAppointments = () => { + const startDay = new Date(2021, 1, 1); + const endDay = new Date(2021, 1, 6); + let appointments: any[] = []; + let durationState = 1; + const durationIncrement = 19; + + resources.slice(0, resourceCount).forEach((resource) => { + let startDate = startDay; + while (startDate.getTime() < endDay.getTime()) { + durationState += durationIncrement; + const endDate = new Date(startDate.getTime() + getPseudoRandomDuration(durationState)); + appointments.push({ startDate, endDate, resourceId: resource.id }); + durationState += durationIncrement; + startDate = new Date(endDate.getTime() + getPseudoRandomDuration(durationState)); + } + }); + + appointments = appointments.filter(({ startDate, endDate }) => ( + startDate.getDay() === endDate.getDay() + && startDate.getHours() >= 7 + && endDate.getHours() <= 19)); + + return appointments.map((a, i) => ({ ...a, text: `[Appointment ${i + 1}]` })); +}; + +const dataSource = generateAppointments(); +const appointmentCount = dataSource.length; + +const getConfig = () => ({ + views: [{ type: 'timelineWorkWeek', name: 'Timeline', groupOrientation: 'vertical' }, 'week'], + dataSource, + resources: [{ fieldExpr: 'resourceId', label: 'Resource', dataSource: resources }], + groups: ['resourceId'], + scrolling: { mode: 'virtual' }, + height: 600, + cellDuration: 60, + startDayHour: 8, + endDayHour: 20, + showAllDayPanel: false, + currentView: 'Timeline', + currentDate: new Date(2021, 1, 2), +}); + +const cellStyles = '#container .dx-scheduler-cell-sizes-vertical { height: 100px; } #container .dx-scheduler-cell-sizes-horizontal { width: 150px; }'; + +test.describe('KeyboardNavigation.Appointments', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + ['virtual', 'standard'].forEach((scrollingMode) => { + test(`focus next appointment on single tab (${scrollingMode} scrolling)`, async ({ page }) => { + await insertStylesheetRulesToPage(page, cellStyles); + await createWidget(page, 'dxScheduler', { ...getConfig(), scrolling: { mode: scrollingMode } }); + + await page.locator('.dx-scheduler-appointment').filter({ hasText: '[Appointment 1]' }).click(); + await page.keyboard.press('Tab'); + + const isFocused = await page.locator('.dx-scheduler-appointment').filter({ hasText: '[Appointment 2]' }).evaluate( + (el) => el.classList.contains('dx-state-focused'), + ); + expect(isFocused).toBe(true); + }); + + test(`focus next appointment on 5 tab (${scrollingMode} scrolling)`, async ({ page }) => { + await insertStylesheetRulesToPage(page, cellStyles); + await createWidget(page, 'dxScheduler', { ...getConfig(), scrolling: { mode: scrollingMode } }); + + await page.locator('.dx-scheduler-appointment').filter({ hasText: '[Appointment 1]' }).click(); + for (let i = 0; i < 5; i++) { + await page.keyboard.press('Tab'); + } + + const isFocused = await page.locator('.dx-scheduler-appointment').filter({ hasText: '[Appointment 6]' }).evaluate( + (el) => el.classList.contains('dx-state-focused'), + ); + expect(isFocused).toBe(true); + }); + + test(`focus last appointment on End (${scrollingMode} scrolling)`, async ({ page }) => { + await insertStylesheetRulesToPage(page, cellStyles); + await createWidget(page, 'dxScheduler', { ...getConfig(), scrolling: { mode: scrollingMode } }); + + await page.locator('.dx-scheduler-appointment').filter({ hasText: '[Appointment 1]' }).click(); + await page.keyboard.press('End'); + + const isFocused = await page.locator('.dx-scheduler-appointment').filter({ hasText: `[Appointment ${appointmentCount}]` }).evaluate( + (el) => el.classList.contains('dx-state-focused'), + ); + expect(isFocused).toBe(true); + }); + + test(`should focus appointment after close edit popup (${scrollingMode} scrolling)`, async ({ page }) => { + await insertStylesheetRulesToPage(page, cellStyles); + await createWidget(page, 'dxScheduler', { ...getConfig(), scrolling: { mode: scrollingMode } }); + + await page.locator('.dx-scheduler-appointment').filter({ hasText: '[Appointment 1]' }).click(); + await page.keyboard.press('Tab'); + await page.keyboard.press('Enter'); + await page.keyboard.press('Escape'); + + const isFocused = await page.locator('.dx-scheduler-appointment').filter({ hasText: '[Appointment 2]' }).evaluate( + (el) => el.classList.contains('dx-state-focused'), + ); + expect(isFocused).toBe(true); + }); + + test(`should focus next appointment on tab after any appointment was clicked (${scrollingMode} scrolling)`, async ({ page }) => { + await insertStylesheetRulesToPage(page, cellStyles); + await createWidget(page, 'dxScheduler', { ...getConfig(), scrolling: { mode: scrollingMode } }); + + await page.locator('.dx-scheduler-appointment').filter({ hasText: '[Appointment 15]' }).click(); + await page.keyboard.press('Tab'); + + const isFocused = await page.locator('.dx-scheduler-appointment').filter({ hasText: '[Appointment 16]' }).evaluate( + (el) => el.classList.contains('dx-state-focused'), + ); + expect(isFocused).toBe(true); + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/keyboardNavigation/dateTable.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/keyboardNavigation/dateTable.spec.ts new file mode 100644 index 000000000000..d0479f15436c --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/keyboardNavigation/dateTable.spec.ts @@ -0,0 +1,56 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, getContainerUrl, setupTestPage, appendElementTo } from '../../../../playwright-helpers'; + +const containerUrl = getContainerUrl(__dirname, '../../../../tests/container.html'); + +const PARENT_SELECTOR = '#parentContainer'; +const SCHEDULER_SELECTOR = '#container'; +const BOTTOM_BTN_ID = 'bottom-btn'; +const BOTTOM_BTN_SELECTOR = `#${BOTTOM_BTN_ID}`; + +test.describe('KeyboardNavigation.DateTable', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + ['day', 'week'].forEach((currentView) => { + test(`Should pass focus to the next elements after date table on Mac devices (view: ${currentView})`, async ({ page }) => { + await appendElementTo(page, PARENT_SELECTOR, 'button', { id: BOTTOM_BTN_ID }); + + await createWidget(page, 'dxScheduler', { + dataSource: [ + { + startDate: '2024-01-01T01:00:00', + endDate: '2024-01-01T02:00:00', + text: 'Usual', + }, + { + startDate: '2024-01-01T01:00:00', + endDate: '2024-01-01T02:00:00', + text: 'All-day', + allDay: true, + }, + ], + height: 300, + currentDate: '2024-01-01', + currentView, + }); + + await page.evaluate((sel) => { + ($(sel) as any) + .dxScheduler('instance') + .getWorkSpaceScrollable() + .option('useNative', true); + }, SCHEDULER_SELECTOR); + + const allDayAppointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'All-day' }); + await allDayAppointment.click(); + await page.keyboard.press('Tab'); + await page.keyboard.press('Tab'); + + const bottomBtn = page.locator(BOTTOM_BTN_SELECTOR); + const isFocused = await bottomBtn.evaluate((el) => document.activeElement === el); + expect(isFocused).toBe(true); + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/keyboardNavigation/documentScrollPrevented.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/keyboardNavigation/documentScrollPrevented.spec.ts new file mode 100644 index 000000000000..71a66bd7d3d6 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/keyboardNavigation/documentScrollPrevented.spec.ts @@ -0,0 +1,61 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, getContainerUrl, setupTestPage } from '../../../../playwright-helpers'; + +const containerUrl = getContainerUrl(__dirname, '../../../../tests/container.html'); + +test.describe('KeyboardNavigation.DocumentScrollPrevented', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + test('Document should not scroll on \'End\' press when appointment is focused', async ({ page }) => { + await page.evaluate(() => { + document.body.style.height = '2000px'; + }); + + await createWidget(page, 'dxScheduler', { + dataSource: [ + { text: 'Appointment 1', startDate: new Date(2015, 1, 9, 8), endDate: new Date(2015, 1, 9, 9) }, + { text: 'Appointment 2', startDate: new Date(2015, 1, 9, 10), endDate: new Date(2015, 1, 9, 11) }, + { text: 'Appointment 3', startDate: new Date(2015, 1, 9, 12), endDate: new Date(2015, 1, 9, 13) }, + ], + height: 300, + currentView: 'day', + currentDate: new Date(2015, 1, 9), + }); + + await page.locator('.dx-scheduler-appointment').filter({ hasText: 'Appointment 1' }).click(); + const expectedScrollTop = await page.evaluate(() => document.documentElement.scrollTop); + await page.keyboard.press('End'); + const actualScrollTop = await page.evaluate(() => document.documentElement.scrollTop); + expect(actualScrollTop).toBe(expectedScrollTop); + + await page.evaluate(() => { document.body.style.height = ''; }); + }); + + test('Document should not scroll on \'Home\' press when appointment is focused', async ({ page }) => { + await page.evaluate(() => { + document.body.style.height = '2000px'; + }); + + await createWidget(page, 'dxScheduler', { + dataSource: [ + { text: 'Appointment 1', startDate: new Date(2015, 1, 9, 8), endDate: new Date(2015, 1, 9, 9) }, + { text: 'Appointment 2', startDate: new Date(2015, 1, 9, 10), endDate: new Date(2015, 1, 9, 11) }, + { text: 'Appointment 3', startDate: new Date(2015, 1, 9, 12), endDate: new Date(2015, 1, 9, 13) }, + ], + height: 300, + currentView: 'day', + currentDate: new Date(2015, 1, 9), + }); + + await page.evaluate(() => window.scrollTo(0, 100)); + await page.locator('.dx-scheduler-appointment').filter({ hasText: 'Appointment 1' }).click(); + const expectedScrollTop = await page.evaluate(() => document.documentElement.scrollTop); + await page.keyboard.press('Home'); + const actualScrollTop = await page.evaluate(() => document.documentElement.scrollTop); + expect(actualScrollTop).toBe(expectedScrollTop); + + await page.evaluate(() => { document.body.style.height = ''; }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/adaptive/adaptive.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/adaptive/adaptive.spec.ts new file mode 100644 index 000000000000..4c64199a1a57 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/adaptive/adaptive.spec.ts @@ -0,0 +1,152 @@ +import { test } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../../tests/container.html')}`; + +const resourceDataSource = [{ + fieldExpr: 'priorityId', + dataSource: [ + { text: 'Low Priority', id: 0, color: '#24ff50' }, + { text: 'High Priority', id: 1, color: '#ff9747' }, + ], + label: 'Priority', +}]; + +const views = [ + 'day', 'week', 'month', + 'timelineDay', 'timelineWeek', 'timelineMonth', +]; + +const verticalViews = views.map((viewType) => ({ + type: viewType, + groupOrientation: 'vertical', +})); + +const horizontalViews = views.map((viewType) => ({ + type: viewType, + groupOrientation: 'horizontal', +})); + +const createDataSetForScreenShotTests = (): Record[] => { + const result: any[] = []; + for (let day = 1; day < 25; day++) { + result.push({ + text: '1 appointment', + startDate: new Date(2020, 6, day, 0), + endDate: new Date(2020, 6, day, 1), + priorityId: 0, + }); + result.push({ + text: '2 appointment', + startDate: new Date(2020, 6, day, 1), + endDate: new Date(2020, 6, day, 2), + priorityId: 1, + }); + result.push({ + text: '3 appointment', + startDate: new Date(2020, 6, day, 3), + endDate: new Date(2020, 6, day, 5), + allDay: true, + priorityId: 0, + }); + } + return result; +}; + +test.describe('Scheduler: Adaptive layout in themes', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + [false, true].forEach((rtlEnabled) => { + [false, true].forEach((crossScrollingEnabled) => { + test(`Adaptive views layout test, crossScrollingEnabled=${crossScrollingEnabled}${rtlEnabled ? ' in RTL' : ''}`, async ({ page }) => { + await page.setViewportSize({ width: 400, height: 600 }); + + await createWidget(page, 'dxScheduler', { + dataSource: createDataSetForScreenShotTests(), + currentDate: new Date(2020, 6, 15), + height: 600, + views, + currentView: 'day', + crossScrollingEnabled, + rtlEnabled, + }); + + for (const view of views) { + await page.evaluate((v: string) => { + ($('#container') as any).dxScheduler('instance').option('currentView', v); + }, view); + + await testScreenshot( + page, + `view=${view}-crossScrolling=${crossScrollingEnabled}${rtlEnabled ? '-rtl' : ''}.png`, + { element: page.locator('.dx-scheduler-work-space') }, + ); + } + }); + + test(`Adaptive views layout test crossScrollingEnabled=${crossScrollingEnabled} when horizontal grouping${rtlEnabled ? ' and RTL are' : ' is'} used`, async ({ page }) => { + await page.setViewportSize({ width: 400, height: 600 }); + + await createWidget(page, 'dxScheduler', { + dataSource: createDataSetForScreenShotTests(), + currentDate: new Date(2020, 6, 15), + height: 600, + views: horizontalViews, + currentView: 'day', + crossScrollingEnabled, + rtlEnabled, + groups: ['priorityId'], + resources: resourceDataSource, + }); + + for (const view of views) { + await page.evaluate((v: string) => { + ($('#container') as any).dxScheduler('instance').option('currentView', v); + }, view); + + await testScreenshot( + page, + `view=${view}-crossScrolling=${crossScrollingEnabled}-horizontal${rtlEnabled ? '-rtl' : ''}.png`, + { element: page.locator('.dx-scheduler-work-space') }, + ); + } + }); + + test(`Adaptive views layout test, crossScrollingEnabled=${crossScrollingEnabled} when vertical grouping${rtlEnabled ? ' and RTL are' : ' is'} used`, async ({ page }) => { + await page.setViewportSize({ width: 400, height: 600 }); + + await createWidget(page, 'dxScheduler', { + dataSource: createDataSetForScreenShotTests(), + currentDate: new Date(2020, 6, 15), + height: 600, + views: verticalViews, + currentView: 'day', + crossScrollingEnabled, + rtlEnabled, + groups: ['priorityId'], + resources: resourceDataSource, + }); + + for (const view of views) { + await page.evaluate((v: string) => { + ($('#container') as any).dxScheduler('instance').option('currentView', v); + }, view); + + await testScreenshot( + page, + `view=${view}-crossScrolling=${crossScrollingEnabled}-vertical${rtlEnabled ? '-rtl' : ''}.png`, + { element: page.locator('.dx-scheduler-work-space') }, + ); + } + }); + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/adaptive/resize/browserResize.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/adaptive/resize/browserResize.spec.ts new file mode 100644 index 000000000000..384d715d0486 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/adaptive/resize/browserResize.spec.ts @@ -0,0 +1,87 @@ +import { test } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../../../tests/container.html')}`; + +test.describe('Layout:BrowserResize', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + const data = [ + { text: 'Website Re-Design Plan', startDate: new Date(2017, 4, 22, 9, 30), endDate: new Date(2017, 4, 22, 11, 30), roomId: 1 }, + { text: 'Book Flights to San Fran for Sales Trip', startDate: new Date(2017, 4, 22, 12, 0), endDate: new Date(2017, 4, 22, 13, 0), allDay: true, roomId: 2 }, + { text: 'Install New Router in Dev Room', startDate: new Date(2017, 4, 22, 14, 30), endDate: new Date(2017, 4, 22, 15, 30), roomId: 3 }, + { text: 'Approve Personal Computer Upgrade Plan', startDate: new Date(2017, 4, 23, 10, 0), endDate: new Date(2017, 4, 23, 11, 0) }, + { text: 'Final Budget Review', startDate: new Date(2017, 4, 23, 12, 0), endDate: new Date(2017, 4, 23, 13, 35), roomId: 1 }, + { text: 'New Brochures', startDate: new Date(2017, 4, 23, 14, 30), endDate: new Date(2017, 4, 23, 15, 45), roomId: 2 }, + { text: 'Install New Database', startDate: new Date(2017, 4, 24, 9, 45), endDate: new Date(2017, 4, 24, 11, 15), roomId: 1 }, + { text: 'Approve New Online Marketing Strategy', startDate: new Date(2017, 4, 24, 12, 0), endDate: new Date(2017, 4, 24, 14, 0) }, + { text: 'Upgrade Personal Computers', startDate: new Date(2017, 4, 24, 15, 15), endDate: new Date(2017, 4, 24, 16, 30), roomId: 1 }, + { text: 'Customer Workshop', startDate: new Date(2017, 4, 25, 11, 0), endDate: new Date(2017, 4, 25, 12, 0), allDay: true }, + { text: 'Prepare 2015 Marketing Plan', startDate: new Date(2017, 4, 25, 11, 0), endDate: new Date(2017, 4, 25, 13, 30) }, + { text: 'Brochure Design Review', startDate: new Date(2017, 4, 25, 14, 0), endDate: new Date(2017, 4, 25, 15, 30), roomId: 3 }, + { text: 'Create Icons for Website', startDate: new Date(2017, 4, 26, 10, 0), endDate: new Date(2017, 4, 26, 11, 30), roomId: 2 }, + { text: 'Upgrade Server Hardware', startDate: new Date(2017, 4, 26, 14, 30), endDate: new Date(2017, 4, 26, 16, 0) }, + { text: 'Submit New Website Design', startDate: new Date(2017, 4, 26, 16, 30), endDate: new Date(2017, 4, 26, 18, 0) }, + { text: 'Launch New Website', startDate: new Date(2017, 4, 26, 12, 20), endDate: new Date(2017, 4, 26, 14, 0) }, + ]; + + const resourceDataSource = [ + { text: 'Room 1', id: 1, color: '#00af2c' }, + { text: 'Room 2', id: 2, color: '#56ca85' }, + { text: 'Room 3', id: 3, color: '#8ecd3c' }, + ]; + + [{ + currentView: 'agenda', + currentDate: new Date(2017, 4, 25), + }, { + currentView: 'day', + currentDate: new Date(2017, 4, 25), + }, { + currentView: 'week', + currentDate: new Date(2017, 4, 25), + }, { + currentView: 'month', + currentDate: new Date(2017, 4, 25), + }, { + currentView: 'timelineDay', + currentDate: new Date(2017, 4, 26), + }].forEach(({ currentView, currentDate }) => { + test(`Appointment layout after resize should be rendered right in '${currentView}'`, async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource: data, + views: [currentView], + currentView, + currentDate, + resources: [{ + fieldExpr: 'roomId', + dataSource: resourceDataSource, + }], + startDayHour: 9, + height: 600, + }); + + await testScreenshot( + page, + `browser-resize-currentView=${currentView}-before-resize.png`, + { element: page.locator('.dx-scheduler-work-space') }, + ); + + await page.setViewportSize({ width: 600, height: 600 }); + + await testScreenshot( + page, + `browser-resize-currentView=${currentView}-after-resize.png`, + { element: page.locator('.dx-scheduler-work-space') }, + ); + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/allDayPanel/allDayPanelMode.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/allDayPanel/allDayPanelMode.spec.ts new file mode 100644 index 000000000000..0cb72e54e806 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/allDayPanel/allDayPanelMode.spec.ts @@ -0,0 +1,100 @@ +import { test, expect } from '@playwright/test'; +import { createWidget } from '../../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../../tests/container.html')}`; + +test.describe('Layout:AllDayPanelMode', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + [ + { + testCaseName: 'Usual appointment', + dataSource: [{ startDate: '2023-12-01T00:00:00', endDate: '2023-12-01T10:00:00', text: 'Usual appt' }], + modesOrder: ['all', 'allDay', 'hidden'], + expectedCollapsed: [true, true, false], + expectedVisible: [true, true, false], + }, + { + testCaseName: 'Usual appointment reverse', + dataSource: [{ startDate: '2023-12-01T00:00:00', endDate: '2023-12-01T10:00:00', text: 'Usual appt' }], + modesOrder: ['hidden', 'allDay', 'all'], + expectedCollapsed: [false, true, true], + expectedVisible: [false, true, true], + }, + { + testCaseName: 'Long appointment', + dataSource: [{ startDate: '2023-12-01T00:00:00', endDate: '2024-01-01T00:00:00', text: 'Long appt' }], + modesOrder: ['all', 'allDay', 'hidden'], + expectedCollapsed: [false, true, false], + expectedVisible: [true, true, false], + }, + { + testCaseName: 'Long appointment reverse', + dataSource: [{ startDate: '2023-12-01T00:00:00', endDate: '2024-01-01T00:00:00', text: 'Long appt' }], + modesOrder: ['hidden', 'allDay', 'all'], + expectedCollapsed: [false, true, false], + expectedVisible: [false, true, true], + }, + { + testCaseName: 'All-day appointment', + dataSource: [{ + startDate: '2023-12-01T00:00:00', endDate: '2023-12-01T00:00:00', text: 'All-day appt', allDay: true, + }], + modesOrder: ['all', 'allDay', 'hidden'], + expectedCollapsed: [false, false, false], + expectedVisible: [true, true, false], + }, + { + testCaseName: 'All-day appointment reverse', + dataSource: [{ + startDate: '2023-12-01T00:00:00', endDate: '2023-12-01T00:00:00', text: 'All-day appt', allDay: true, + }], + modesOrder: ['hidden', 'allDay', 'all'], + expectedCollapsed: [false, false, false], + expectedVisible: [false, true, true], + }, + ].forEach(({ + testCaseName, dataSource, modesOrder, expectedCollapsed, expectedVisible, + }) => { + test(`${testCaseName}: AllDayPanel visibility and collapsed state should be correct in runtime`, async ({ page }) => { + await createWidget(page, 'dxScheduler', { + currentView: 'week', + currentDate: '2023-12-01', + dataSource, + }); + + for (let idx = 0; idx < modesOrder.length; idx++) { + const mode = modesOrder[idx]; + + await page.evaluate((m: string) => { + ($('#container') as any).dxScheduler('instance').option('allDayPanelMode', m); + }, mode); + + const isCollapsed = await page.evaluate(() => { + const allDayTable = document.querySelector('.dx-scheduler-all-day-table'); + if (!allDayTable) return false; + const row = allDayTable.querySelector('tr'); + if (!row) return false; + return row.getBoundingClientRect().height === 0; + }); + + const isVisible = await page.locator('.dx-scheduler-all-day-table-row').count() > 0; + + expect(isCollapsed).toBe( + expectedCollapsed[idx], + ); + expect(isVisible).toBe( + expectedVisible[idx], + ); + } + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/appointments/allDay/allDayExpr.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/appointments/allDay/allDayExpr.spec.ts new file mode 100644 index 000000000000..d9aa27e5a8da --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/appointments/allDay/allDayExpr.spec.ts @@ -0,0 +1,57 @@ +import { test } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../../../tests/container.html')}`; + +test.describe('Layout:Appointments:allDayExpr', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + [{ + config: { + allDayExpr: 'AllDay', + }, + data: { + AllDay: true, + }, + }, { + config: {}, + data: { + allDay: true, + }, + }].forEach(({ config, data }) => { + test(`All day appointment should be render valid in case without endDate property with allDayExpr=${(config as any).allDayExpr}(T1155630)`, async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource: [{ + text: 'MY EVENT', + startDate: new Date(2023, 2, 19, 23, 45), + ...data, + }], + views: ['week', 'timelineWeek'], + currentView: 'week', + cellDuration: 360, + startDayHour: 18, + currentDate: new Date(2023, 2, 21), + height: 600, + ...config, + }); + + await testScreenshot(page, `week-all-day-expr-${(config as any).allDayExpr}.png`, { + element: page.locator('.dx-scheduler-work-space'), + }); + + await page.locator('.dx-scheduler-view-switcher .dx-buttongroup .dx-button').filter({ hasText: 'Timeline Week' }).click(); + + await testScreenshot(page, `timelineWeek-all-day-expr-${(config as any).allDayExpr}.png`, { + element: page.locator('.dx-scheduler-work-space'), + }); + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/appointments/allDay/longAppointment.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/appointments/allDay/longAppointment.spec.ts new file mode 100644 index 000000000000..478be07363e6 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/appointments/allDay/longAppointment.spec.ts @@ -0,0 +1,56 @@ +import { test } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../../../tests/container.html')}`; + +test.describe('Layout:Appointments:AllDay', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('Long all day appointment should be render, if him ended on next view day in currentView: day(T1021963)', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource: [{ allDay: true, startDate: new Date(2021, 2, 28), endDate: new Date(2021, 2, 29) }], + views: ['day'], currentView: 'day', currentDate: new Date(2021, 2, 28), + startDayHour: 9, width: 400, height: 600, + }); + + const prevButton = page.locator('.dx-scheduler-navigator-previous'); + const nextButton = page.locator('.dx-scheduler-navigator-next'); + const workSpace = page.locator('.dx-scheduler-work-space'); + + await prevButton.click(); + await testScreenshot(page, '27-march-day-view.png', { element: workSpace }); + await nextButton.click(); + await testScreenshot(page, '28-march-day-view.png', { element: workSpace }); + await nextButton.click(); + await testScreenshot(page, '29-march-day-view.png', { element: workSpace }); + await nextButton.click(); + await testScreenshot(page, '30-march-day-view.png', { element: workSpace }); + }); + + test('Long all day appointment should be render, if him ended on next view day in currentView: week', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource: [{ allDay: true, startDate: new Date(2021, 2, 27), endDate: new Date(2021, 3, 4) }], + views: ['week'], currentView: 'week', currentDate: new Date(2021, 2, 28), + startDayHour: 9, width: 600, height: 600, + }); + + const prevButton = page.locator('.dx-scheduler-navigator-previous'); + const nextButton = page.locator('.dx-scheduler-navigator-next'); + const workSpace = page.locator('.dx-scheduler-work-space'); + + await prevButton.click(); + await testScreenshot(page, '21-27-march-week-view.png', { element: workSpace }); + await nextButton.click(); + await testScreenshot(page, '28-march-3-apr-week-view.png', { element: workSpace }); + await nextButton.click(); + await testScreenshot(page, '4-10-apr-week-view.png', { element: workSpace }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/appointments/collector.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/appointments/collector.spec.ts new file mode 100644 index 000000000000..4d5b49c55aeb --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/appointments/collector.spec.ts @@ -0,0 +1,63 @@ +import { test } from '@playwright/test'; +import { createWidget, testScreenshot, generateOptionMatrix } from '../../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../../tests/container.html')}`; + +test.describe('Appointments collector', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('Appointment collector has correct offset when adaptivityEnabled=true (T1024299)', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + adaptivityEnabled: true, currentDate: new Date(2021, 7, 1), + views: ['timelineMonth'], currentView: 'timelineMonth', + dataSource: [{ text: 'text', startDate: new Date(2021, 7, 1), endDate: new Date(2021, 7, 2) }], + height: 300, + }); + await testScreenshot(page, 'appointment-collector-adaptability-timelineMonth.png', { + element: page.locator('.dx-scheduler-work-space'), + }); + }); + + const getSchedulerBaseOptions = (view: string) => { + const count = 20; + const day = 1; + const allDayAppointments = Array(Math.round(count / 4)).fill({ + allDay: true, text: 'text', startDate: new Date(2021, 7, day, 0), endDate: new Date(2021, 7, day, 2), + }); + const regularAppointments = Array(Math.round((count * 3) / 4)).fill({ + text: 'text', startDate: new Date(2021, 7, day, 0), endDate: new Date(2021, 7, day, 2), + }); + const width = ['month', 'week'].includes(view) ? 800 : 500; + const height = ['month'].includes(view) ? 500 : 300; + return { currentDate: new Date(2021, 7, day), views: [view], currentView: view, dataSource: [...allDayAppointments, ...regularAppointments], height, width }; + }; + + generateOptionMatrix({ view: ['week', 'month', 'timelineWeek'], adaptivityEnabled: [true, false] }) + .forEach(({ view, adaptivityEnabled }) => { + test(`Appointment collector has correct offset when view=${view} adaptivityEnabled=${adaptivityEnabled}`, async ({ page }) => { + await createWidget(page, 'dxScheduler', { adaptivityEnabled, ...getSchedulerBaseOptions(view) }); + await testScreenshot(page, `appointment-collector-${view}-adapt(${adaptivityEnabled}).png`, { element: page.locator('.dx-scheduler-work-space') }); + }); + }); + + test('Appointment collector has correct offset when month view with double interval', async ({ page }) => { + await createWidget(page, 'dxScheduler', { ...getSchedulerBaseOptions('month'), views: [{ type: 'month', intervalCount: 2 }] }); + await testScreenshot(page, 'appointment-collector-month-double-interval.png', { element: page.locator('.dx-scheduler-work-space') }); + }); + + generateOptionMatrix({ view: ['week', 'month', 'timelineWeek'], rtlEnabled: [false, true] }) + .forEach(({ view, rtlEnabled }) => { + test(`Appointment collector has correct offset when view=${view} rtlEnabled=${rtlEnabled}`, async ({ page }) => { + await createWidget(page, 'dxScheduler', { ...getSchedulerBaseOptions(view), rtlEnabled }); + await testScreenshot(page, `appointment-collector-${view}-rtl(${rtlEnabled}).png`, { element: page.locator('.dx-scheduler-work-space') }); + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/appointments/dataSource.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/appointments/dataSource.spec.ts new file mode 100644 index 000000000000..36279705c996 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/appointments/dataSource.spec.ts @@ -0,0 +1,38 @@ +import { test } from '@playwright/test'; +import { testScreenshot } from '../../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../../tests/container.html')}`; + +test.describe('DataSource', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('Appointment key should be deleted when removing an appointment from series (T1024213)', async ({ page }) => { + await page.evaluate(() => { + const devExpress = (window as any).DevExpress; + (window as any).DevExpress.fx.off = true; + ($('#container') as any).dxScheduler({ + dataSource: new devExpress.data.DataSource({ + store: { type: 'array', key: 'appointmentId', data: [{ + startDate: new Date(2021, 6, 12, 10), endDate: new Date(2021, 6, 12, 11), + text: 'Test Appointment', recurrenceRule: 'FREQ=DAILY;COUNT=3', appointmentId: 0, + }] }, + }), + recurrenceEditMode: 'occurrence', views: ['week'], currentView: 'week', + startDayHour: 9, currentDate: new Date(2021, 6, 12, 10), height: 600, + }); + }); + await page.locator('.dx-scheduler-appointment').nth(1).dblclick(); + await page.locator('.dx-popup-bottom .dx-button').filter({ hasText: /done|save/i }).click(); + await testScreenshot(page, 'exclude-appointment-from-series-via-form-editing.png', { + element: page.locator('.dx-scheduler-work-space'), + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/appointments/disable.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/appointments/disable.spec.ts new file mode 100644 index 000000000000..3f73eeff05e4 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/appointments/disable.spec.ts @@ -0,0 +1,53 @@ +import { test } from '@playwright/test'; +import { testScreenshot } from '../../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../../tests/container.html')}`; + +test.describe('Layout:Appointments:disable', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('Appointment popup should be readOnly if appointment is disabled', async ({ page }) => { + await page.evaluate(() => { + (window as any).DevExpress.fx.off = true; + ($('#container') as any).dxScheduler({ + dataSource: [ + { disabled: true, text: 'A', startDate: new Date(2021, 4, 27, 0, 30), endDate: new Date(2021, 4, 27, 1), recurrenceRule: 'FREQ=DAILY;UNTIL=20210615T205959Z' }, + { disabled: false, text: 'B', startDate: new Date(2021, 4, 27, 1), endDate: new Date(2021, 4, 27, 1, 30), recurrenceRule: 'FREQ=DAILY;UNTIL=20210615T205959Z' }, + { disabled: () => true, text: 'C', startDate: new Date(2021, 4, 27, 1, 30), endDate: new Date(2021, 4, 27, 2), recurrenceRule: 'FREQ=WEEKLY;UNTIL=20210615T205959Z' }, + { disabled: () => false, text: 'D', startDate: new Date(2021, 4, 27, 2), endDate: new Date(2021, 4, 27, 2, 30), recurrenceRule: 'FREQ=WEEKLY;UNTIL=20210615T205959Z' }, + ], + recurrenceEditMode: 'series', views: ['week'], currentView: 'week', currentDate: new Date(2021, 4, 27), + }); + }); + + await testScreenshot(page, 'disabled-appointments-in-grid.png'); + + await page.locator('.dx-scheduler-appointment').filter({ hasText: 'A' }).first().click(); + await page.locator('.dx-tooltip-appointment-item').filter({ hasText: 'A' }).click(); + await testScreenshot(page, 'disabled-appointment.png', { element: page.locator('.dx-popup-content') }); + await page.locator('.dx-popup-bottom .dx-button').filter({ hasText: /cancel/i }).click(); + + await page.locator('.dx-scheduler-appointment').filter({ hasText: 'B' }).first().click(); + await page.locator('.dx-tooltip-appointment-item').filter({ hasText: 'B' }).click(); + await testScreenshot(page, 'enabled-appointment.png', { element: page.locator('.dx-popup-content') }); + await page.locator('.dx-popup-bottom .dx-button').filter({ hasText: /cancel/i }).click(); + + await page.locator('.dx-scheduler-appointment').filter({ hasText: 'C' }).first().click(); + await page.locator('.dx-tooltip-appointment-item').filter({ hasText: 'C' }).click(); + await testScreenshot(page, 'disabled-by-function-appointment.png', { element: page.locator('.dx-popup-content') }); + await page.locator('.dx-popup-bottom .dx-button').filter({ hasText: /cancel/i }).click(); + + await page.locator('.dx-scheduler-appointment').filter({ hasText: 'D' }).first().click(); + await page.locator('.dx-tooltip-appointment-item').filter({ hasText: 'D' }).click(); + await testScreenshot(page, 'enabled-by-function-appointment.png', { element: page.locator('.dx-popup-content') }); + await page.locator('.dx-popup-bottom .dx-button').filter({ hasText: /cancel/i }).click(); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/appointments/longAppointments.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/appointments/longAppointments.spec.ts new file mode 100644 index 000000000000..96f5d62f4c2f --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/appointments/longAppointments.spec.ts @@ -0,0 +1,55 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../../tests/container.html')}`; + +test.describe('Layout:Appointments:longAppointments(T1086079)', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + const dataSource = [{ text: 'Website Re-Design Plan', startDate: new Date('2021-02-29T01:30:00.000Z'), endDate: new Date('2021-02-29T14:30:00.000Z'), recurrenceRule: 'FREQ=DAILY' }]; + const appointmentName = 'Website Re-Design Plan'; + + test('Control should be render top part of recurrent long appointment in day view(T1086079)', async ({ page }) => { + await createWidget(page, 'dxScheduler', { timeZone: 'America/Los_Angeles', dataSource, cellDuration: 120, views: ['day'], currentView: 'day', currentDate: new Date(2021, 2, 30), startDayHour: 2, endDayHour: 22, height: 600 }); + await testScreenshot(page, 'long-appointment-day-view-T1086079.png', { element: page.locator('.dx-scheduler-work-space') }); + + await page.locator('.dx-scheduler-appointment').filter({ hasText: appointmentName }).nth(0).click(); + expect(await page.locator('.dx-tooltip-appointment-item-content-date').textContent()).toBe('March 29 5:30 PM - March 30 6:30 AM'); + + await page.locator('.dx-scheduler-appointment').filter({ hasText: appointmentName }).nth(1).click(); + expect(await page.locator('.dx-tooltip-appointment-item-content-date').textContent()).toBe('March 30 5:30 PM - March 31 6:30 AM'); + + await page.locator('.dx-scheduler-navigator-next').click(); + + await page.locator('.dx-scheduler-appointment').filter({ hasText: appointmentName }).nth(0).click(); + expect(await page.locator('.dx-tooltip-appointment-item-content-date').textContent()).toBe('March 30 5:30 PM - March 31 6:30 AM'); + + await page.locator('.dx-scheduler-appointment').filter({ hasText: appointmentName }).nth(1).click(); + expect(await page.locator('.dx-tooltip-appointment-item-content-date').textContent()).toBe('March 31 5:30 PM - April 1 6:30 AM'); + }); + + test('Control should be render top part of recurrent long appointment in week view(T1086079)', async ({ page }) => { + await createWidget(page, 'dxScheduler', { timeZone: 'America/Los_Angeles', dataSource, cellDuration: 120, views: ['week'], currentView: 'week', currentDate: new Date(2021, 2, 30), startDayHour: 2, endDayHour: 22, height: 600 }); + await testScreenshot(page, 'long-appointment-week-view-T1086079.png', { element: page.locator('.dx-scheduler-work-space') }); + + await page.locator('.dx-scheduler-appointment').filter({ hasText: appointmentName }).nth(0).click(); + expect(await page.locator('.dx-tooltip-appointment-item-content-date').textContent()).toBe('March 27 5:30 PM - March 28 6:30 AM'); + + await page.locator('.dx-scheduler-appointment').filter({ hasText: appointmentName }).nth(1).click(); + expect(await page.locator('.dx-tooltip-appointment-item-content-date').textContent()).toBe('March 28 5:30 PM - March 29 6:30 AM'); + + await page.locator('.dx-scheduler-appointment').filter({ hasText: appointmentName }).nth(2).click(); + expect(await page.locator('.dx-tooltip-appointment-item-content-date').textContent()).toBe('March 28 5:30 PM - March 29 6:30 AM'); + + await page.locator('.dx-scheduler-appointment').filter({ hasText: appointmentName }).nth(3).click(); + expect(await page.locator('.dx-tooltip-appointment-item-content-date').textContent()).toBe('March 29 5:30 PM - March 30 6:30 AM'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/appointments/noSubject.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/appointments/noSubject.spec.ts new file mode 100644 index 000000000000..325c47d57b3b --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/appointments/noSubject.spec.ts @@ -0,0 +1,38 @@ +import { test } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../../tests/container.html')}`; + +test.describe('Layout:Appointments:noSubject', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + const viewsList = ['day', 'week', 'workWeek', 'month', 'timelineDay', 'timelineWeek', 'timelineWorkWeek', 'timelineMonth', 'agenda']; + const timelineViews = ['timelineDay', 'timelineWeek', 'timelineWorkWeek', 'timelineMonth']; + + viewsList.forEach((currentView) => { + test(`Appointment without text should display "(No subject)" in ${currentView} view`, async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource: [{ startDate: new Date(2021, 0, 1, 10, 30), endDate: new Date(2021, 0, 1, 12, 0), text: '' }], + views: viewsList, currentView, currentDate: new Date(2021, 0, 1), + startDayHour: 9, endDayHour: 18, height: 600, width: 600, + }); + + if (timelineViews.includes(currentView)) { + await page.evaluate(() => { + ($('#container') as any).dxScheduler('instance').scrollTo(new Date(2021, 0, 1, 10, 30)); + }); + await page.waitForTimeout(300); + } + + await testScreenshot(page, `appointment-no-subject-${currentView}.png`, { element: page.locator('.dx-scheduler-work-space') }); + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/appointments/recurrence.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/appointments/recurrence.spec.ts new file mode 100644 index 000000000000..22ca3d78557e --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/appointments/recurrence.spec.ts @@ -0,0 +1,28 @@ +import { test } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../../tests/container.html')}`; + +test.describe('AppointmentForm screenshot tests', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + ['day', 'week', 'workWeek', 'month', 'timelineDay', 'timelineWeek', 'timelineWorkWeek', 'timelineMonth', 'agenda'].forEach((currentView) => { + [true, false].forEach((rtlEnabled) => { + test(`Recurrent appointment in ${currentView} view and ${rtlEnabled ? 'rtl' : 'non-rtl'} mode`, async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource: [{ text: 'Long Long Long Long Long Long Long Long Long Description', startDate: new Date(2021, 0, 1, 1, 30), endDate: new Date(2021, 0, 1, 3, 0), recurrenceRule: 'FREQ=DAILY;COUNT=30' }], + currentDate: new Date(2021, 0, 4), height: 600, currentView, rtlEnabled, + }); + await testScreenshot(page, `recurrent-appointment-in-${currentView}_view-and-${rtlEnabled ? 'rtl' : 'non-rtl'}_mode.png`, { element: page.locator('.dx-scheduler') }); + }); + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/appointments/two-schedulers.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/appointments/two-schedulers.spec.ts new file mode 100644 index 000000000000..7e4234974f62 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/appointments/two-schedulers.spec.ts @@ -0,0 +1,51 @@ +import { test } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../../tests/container.html')}`; + +test.describe('Layout:Appointments:two-schedulers', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('Appointment dragging should work properly with two dxSchedulers(T1020820)', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + maxAppointmentsPerCell: 'unlimited', + dataSource: [ + { text: 'Website Re-Design Plan', startDate: new Date('2021-03-29T16:30:00.000Z'), endDate: new Date('2021-03-29T18:30:00.000Z') }, + { text: 'Book Flights to San Fran for Sales Trip', startDate: new Date('2021-03-29T19:00:00.000Z'), endDate: new Date('2021-03-29T20:00:00.000Z'), allDay: true }, + { text: 'Approve Personal Computer Upgrade Plan', startDate: new Date('2021-03-30T17:00:00.000Z'), endDate: new Date('2021-03-30T18:00:00.000Z') }, + { text: 'Final Budget Review', startDate: new Date('2021-03-30T19:00:00.000Z'), endDate: new Date('2021-03-30T20:35:00.000Z') }, + { text: 'Install New Database', startDate: new Date('2021-03-31T16:45:00.000Z'), endDate: new Date('2021-03-31T18:15:00.000Z') }, + ], + views: ['month'], currentView: 'month', currentDate: new Date(2021, 2, 29), startDayHour: 9, height: 400, + }); + await createWidget(page, 'dxScheduler', { + maxAppointmentsPerCell: 'unlimited', + dataSource: [ + { text: 'Helen', startDate: new Date('2021-03-29T16:30:00.000Z'), endDate: new Date('2021-04-29T18:30:00.000Z') }, + { text: 'Alex', startDate: new Date('2021-03-29T19:00:00.000Z'), endDate: new Date('2021-04-29T20:00:00.000Z') }, + ], + views: ['day'], currentView: 'day', currentDate: new Date(2021, 2, 29), startDayHour: 9, height: 400, + }, '#otherContainer'); + + await testScreenshot(page, 'before-dragging(T1020820).png'); + + const appointment = page.locator('#container .dx-scheduler-appointment').filter({ hasText: 'Install New Database' }); + const box = await appointment.boundingBox(); + if (box) { + await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2); + await page.mouse.down(); + await page.mouse.move(box.x + box.width / 2 - 100, box.y + box.height / 2 - 100, { steps: 20 }); + await page.mouse.up(); + } + + await testScreenshot(page, 'after-dragging(T1020820).png'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/appointments/visible.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/appointments/visible.spec.ts new file mode 100644 index 000000000000..30d70fc7b490 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/appointments/visible.spec.ts @@ -0,0 +1,32 @@ +import { test } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../../tests/container.html')}`; + +test.describe('Layout:Appointments:visible', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + [1, 0].forEach((maxAppointmentsPerCell) => { + [true, false, undefined].forEach((visible) => { + test(`Appointments should be filtered by visible property(visible='${visible}', maxAppointmentsPerCell='${maxAppointmentsPerCell}'`, async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource: [ + { text: 'Recurrence app', roomId: [1], startDate: new Date(2021, 3, 13, 1, 30), endDate: new Date(2021, 3, 13, 2, 30), recurrenceRule: 'FREQ=DAILY', visible }, + { text: 'Simple app', roomId: [1], startDate: new Date(2021, 3, 12, 3), endDate: new Date(2021, 3, 12, 4), visible }, + ], + views: [{ type: 'week', name: 'Numeric Mode', maxAppointmentsPerCell }], + currentView: 'Numeric Mode', currentDate: new Date(2021, 3, 15), height: 600, + }); + await testScreenshot(page, `filtering-visible=${visible}-maxAppointmentsPerCell=${maxAppointmentsPerCell}.png`, { element: page.locator('.dx-scheduler-work-space') }); + }); + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/customization/cellSizes.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/customization/cellSizes.spec.ts new file mode 100644 index 000000000000..6b470bed2d5a --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/customization/cellSizes.spec.ts @@ -0,0 +1,87 @@ +import { test } from '@playwright/test'; +import { createWidget, testScreenshot, insertStylesheetRulesToPage } from '../../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../../tests/container.html')}`; + +test.describe('Scheduler: Layout Customization: Cell Sizes', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + const views = [{ + type: 'week', + groupOrientation: 'horizontal', + }, { + type: 'month', + groupOrientation: 'horizontal', + }, { + type: 'timelineWeek', + groupOrientation: 'vertical', + }, { + type: 'timelineMonth', + groupOrientation: 'vertical', + }]; + + const createSchedulerOnPage = async ( + page: any, + additionalProps: Record, + ): Promise => { + await createWidget(page, 'dxScheduler', { + currentDate: new Date(2021, 4, 11), + height: 500, + width: 700, + startDayHour: 9, + showAllDayPanel: false, + dataSource: [], + crossScrollingEnabled: true, + groups: ['priorityId'], + resources: [{ + fieldExpr: 'priorityId', + dataSource: [ + { text: 'Low Priority 1', id: 0, color: '#24ff50' }, + { text: 'Low Priority 2', id: 1, color: '#ff9747' }, + { text: 'Low Priority 3', id: 2, color: '#24ff50' }, + { text: 'High Priority 1', id: 3, color: '#ff9747' }, + { text: 'High Priority 2', id: 4, color: '#24ff50' }, + { text: 'High Priority 3', id: 5, color: '#ff9747' }, + ], + label: 'Priority', + }], + ...additionalProps, + }); + }; + + test('Cell sizes customization should work', async ({ page }) => { + await insertStylesheetRulesToPage(page, '#container .dx-scheduler-cell-sizes-vertical { height: 150px; } #container .dx-scheduler-cell-sizes-horizontal { width: 150px; }'); + await createSchedulerOnPage(page, { views }); + + for (const { type } of views) { + await page.evaluate((viewType: string) => { + ($('#container') as any).dxScheduler('instance').option('currentView', viewType); + }, type); + + await testScreenshot(page, `custom-cell-sizes-in-${type}.png`, { + element: page.locator('.dx-scheduler-work-space'), + }); + } + }); + + test('Cell sizes customization should work when all-day panel is enabled', async ({ page }) => { + await insertStylesheetRulesToPage(page, '#container .dx-scheduler-cell-sizes-vertical { height: 150px; } #container .dx-scheduler-cell-sizes-horizontal { width: 150px; }'); + await createSchedulerOnPage(page, { + views, + showAllDayPanel: true, + currentView: 'week', + }); + + await testScreenshot(page, 'custom-cell-sizes-with-all-day-panel-in-week.png', { + element: page.locator('.dx-scheduler-work-space'), + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/customization/cellSizesCss.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/customization/cellSizesCss.spec.ts new file mode 100644 index 000000000000..338153fc6349 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/customization/cellSizesCss.spec.ts @@ -0,0 +1,64 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, insertStylesheetRulesToPage } from '../../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../../tests/container.html')}`; + +test.describe('Scheduler: Layout Customization: Cell Sizes CSS classes', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + const HORIZONTAL_SIZE_CLASSNAME = 'dx-scheduler-cell-sizes-horizontal'; + const VERTICAL_SIZE_CLASSNAME = 'dx-scheduler-cell-sizes-vertical'; + const CELL_SIZE_CSS = ` +#container .${HORIZONTAL_SIZE_CLASSNAME} { width: 300px; } +#container .${VERTICAL_SIZE_CLASSNAME} { height: 300px; } +`; + + const cases = [ + { views: ['day'], crossScrollingEnabled: false, expected: { width: 'skipCheck', height: 300, hasHorizontalClass: false, hasVerticalClass: true } }, + { views: ['day'], crossScrollingEnabled: true, expected: { width: 'skipCheck', height: 300, hasHorizontalClass: true, hasVerticalClass: true } }, + { views: ['week', 'workWeek', 'month'], crossScrollingEnabled: false, expected: { width: 'skipCheck', height: 300, hasHorizontalClass: false, hasVerticalClass: true } }, + { views: ['week', 'workWeek', 'month'], crossScrollingEnabled: true, expected: { width: 300, height: 300, hasHorizontalClass: true, hasVerticalClass: true } }, + { views: ['timelineDay', 'timelineWeek', 'timelineMonth'], crossScrollingEnabled: false, expected: { width: 300, height: 300, hasHorizontalClass: true, hasVerticalClass: true } }, + { views: ['timelineDay', 'timelineWeek', 'timelineMonth'], crossScrollingEnabled: true, expected: { width: 300, height: 300, hasHorizontalClass: true, hasVerticalClass: true } }, + ]; + + cases.forEach(({ views, expected, crossScrollingEnabled }) => { + views.forEach((view) => { + test.skip(`Cells should have correct sizes and css classes (view:${view}, crossScrolling:${crossScrollingEnabled})`, async ({ page }) => { + await insertStylesheetRulesToPage(page, CELL_SIZE_CSS); + await createWidget(page, 'dxScheduler', { + dataSource: [], + currentView: view, + currentDate: '2024-01-01', + crossScrollingEnabled, + }); + + const cell = page.locator('.dx-scheduler-date-table-cell').first(); + const box = await cell.boundingBox(); + const hasHorizontalClass = await cell.evaluate( + (el, cls) => el.classList.contains(cls), HORIZONTAL_SIZE_CLASSNAME, + ); + const hasVerticalClass = await cell.evaluate( + (el, cls) => el.classList.contains(cls), VERTICAL_SIZE_CLASSNAME, + ); + + if (typeof expected.width === 'number' && box) { + expect(box.width).toBe(expected.width); + } + if (box) { + expect(box.height).toBe(expected.height); + } + expect(hasHorizontalClass).toBe(expected.hasHorizontalClass); + expect(hasVerticalClass).toBe(expected.hasVerticalClass); + }); + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/customization/groupPanel.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/customization/groupPanel.spec.ts new file mode 100644 index 000000000000..56b11b0dbd3f --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/customization/groupPanel.spec.ts @@ -0,0 +1,71 @@ +import { test } from '@playwright/test'; +import { createWidget, testScreenshot, insertStylesheetRulesToPage } from '../../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../../tests/container.html')}`; + +test.describe('Scheduler: Layout Customization: Group Panel', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + const views = [ + { type: 'week', groupOrientation: 'vertical' }, + { type: 'month', groupOrientation: 'vertical' }, + { type: 'timelineWeek', groupOrientation: 'vertical' }, + { type: 'timelineMonth', groupOrientation: 'vertical' }, + ]; + + [false, true].forEach((crossScrollingEnabled) => { + test(`Group panel customization should work (crossScrollingEnabled=${crossScrollingEnabled})`, async ({ page }) => { + await insertStylesheetRulesToPage(page, '#container .dx-scheduler-group-header { width: 200px;}'); + + await createWidget(page, 'dxScheduler', { + currentDate: new Date(2021, 4, 11), + height: 500, + width: 700, + startDayHour: 9, + showAllDayPanel: false, + dataSource: [{ + text: 'Create Report on Customer Feedback', + startDate: new Date(2021, 4, 1, 14), + endDate: new Date(2021, 4, 1, 15), + priorityId: 0, + }, { + text: 'Review Customer Feedback Report', + startDate: new Date(2021, 4, 9, 9, 30), + endDate: new Date(2021, 4, 9, 11), + priorityId: 0, + }], + groups: ['priorityId'], + resources: [{ + fieldExpr: 'priorityId', + dataSource: [ + { text: 'Low Priority', id: 0, color: '#24ff50' }, + { text: 'High Priority', id: 1, color: '#ff9747' }, + ], + label: 'Priority', + }], + views, + crossScrollingEnabled, + }); + + for (const view of views) { + await page.evaluate((viewType: string) => { + ($('#container') as any).dxScheduler('instance').option('currentView', viewType); + }, view.type); + + await testScreenshot( + page, + `custom-group-panel-in-${view.type}-cross-scrolling=${crossScrollingEnabled}.png`, + { element: page.locator('.dx-scheduler') }, + ); + } + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/customization/headerPanel.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/customization/headerPanel.spec.ts new file mode 100644 index 000000000000..bbeb4a667e9f --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/customization/headerPanel.spec.ts @@ -0,0 +1,71 @@ +import { test } from '@playwright/test'; +import { createWidget, testScreenshot, insertStylesheetRulesToPage } from '../../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../../tests/container.html')}`; + +test.describe('Scheduler: Layout Customization: Header Panel', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + const views = [ + { type: 'week', groupOrientation: 'horizontal' }, + { type: 'month', groupOrientation: 'horizontal' }, + { type: 'timelineWeek', groupOrientation: 'horizontal' }, + { type: 'timelineMonth', groupOrientation: 'horizontal' }, + ]; + + [false, true].forEach((crossScrollingEnabled) => { + test(`Header panel customization should work (crossScrollingEnabled=${crossScrollingEnabled})`, async ({ page }) => { + await insertStylesheetRulesToPage(page, '#container .dx-scheduler-group-header, #container .dx-scheduler-header-panel-cell { height: 100px; }'); + + await createWidget(page, 'dxScheduler', { + currentDate: new Date(2021, 4, 11), + height: 500, + width: 700, + startDayHour: 9, + showAllDayPanel: false, + dataSource: [{ + text: 'Create Report on Customer Feedback', + startDate: new Date(2021, 4, 11, 14), + endDate: new Date(2021, 4, 11, 15), + priorityId: 0, + }, { + text: 'Review Customer Feedback Report', + startDate: new Date(2021, 4, 9, 9, 30), + endDate: new Date(2021, 4, 9, 11), + priorityId: 0, + }], + groups: ['priorityId'], + resources: [{ + fieldExpr: 'priorityId', + dataSource: [ + { text: 'Low Priority', id: 0, color: '#24ff50' }, + { text: 'High Priority', id: 1, color: '#ff9747' }, + ], + label: 'Priority', + }], + views, + crossScrollingEnabled, + }); + + for (const view of views) { + await page.evaluate((viewType: string) => { + ($('#container') as any).dxScheduler('instance').option('currentView', viewType); + }, view.type); + + await testScreenshot( + page, + `custom-header-panel-in-${view.type}-cross-scrolling=${crossScrollingEnabled}.png`, + { element: page.locator('.dx-scheduler') }, + ); + } + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/customization/timePanel.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/customization/timePanel.spec.ts new file mode 100644 index 000000000000..276ea4c0797e --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/customization/timePanel.spec.ts @@ -0,0 +1,81 @@ +import { test } from '@playwright/test'; +import { createWidget, testScreenshot, insertStylesheetRulesToPage } from '../../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../../tests/container.html')}`; + +test.describe('Scheduler: Layout Customization: Time Panel', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + [false, true].forEach((crossScrollingEnabled) => { + ['week', 'agenda'].forEach((view) => { + test(`Time panel customization should work in ${view} view (crossScrollingEnabled=${crossScrollingEnabled})`, async ({ page }) => { + await insertStylesheetRulesToPage(page, '#container .dx-scheduler-time-panel { width: 150px;}'); + + await createWidget(page, 'dxScheduler', { + timeZone: 'America/Los_Angeles', + currentDate: new Date(2021, 4, 11), + height: 500, + width: 700, + startDayHour: 9, + showAllDayPanel: false, + dataSource: [{ + text: 'Create Report on Customer Feedback', + startDate: new Date('2021-05-11T22:15:00.000Z'), + endDate: new Date('2021-05-12T00:30:00.000Z'), + }, { + text: 'Review Customer Feedback Report', + startDate: new Date('2021-05-11T23:15:00.000Z'), + endDate: new Date('2021-05-12T01:30:00.000Z'), + }, { + text: 'Customer Feedback Report Analysis', + startDate: new Date('2021-05-12T16:30:00.000Z'), + endDate: new Date('2021-05-12T17:30:00.000Z'), + recurrenceRule: 'FREQ=WEEKLY', + }, { + text: 'Prepare Shipping Cost Analysis Report', + startDate: new Date('2021-05-12T19:30:00.000Z'), + endDate: new Date('2021-05-12T20:30:00.000Z'), + }, { + text: 'Provide Feedback on Shippers', + startDate: new Date('2021-05-12T21:15:00.000Z'), + endDate: new Date('2021-05-12T23:00:00.000Z'), + }, { + text: 'Select Preferred Shipper', + startDate: new Date('2021-05-13T00:30:00.000Z'), + endDate: new Date('2021-05-13T03:00:00.000Z'), + }, { + text: 'Complete Shipper Selection Form', + startDate: new Date('2021-05-13T15:30:00.000Z'), + endDate: new Date('2021-05-13T17:00:00.000Z'), + }, { + text: 'Upgrade Server Hardware', + startDate: new Date('2021-05-13T19:00:00.000Z'), + endDate: new Date('2021-05-13T21:15:00.000Z'), + recurrenceRule: 'FREQ=WEEKLY', + }, { + text: 'Upgrade Personal Computers', + startDate: new Date('2021-05-13T21:45:00.000Z'), + endDate: new Date('2021-05-13T23:30:00.000Z'), + }], + views: [view], + currentView: view, + crossScrollingEnabled, + }); + + await testScreenshot( + page, + `custom-time-panel-in-${view}-cross-scrolling=${crossScrollingEnabled}.png`, + { element: '#container' }, + ); + }); + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/legacyAppointmentForm/allDay.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/legacyAppointmentForm/allDay.spec.ts new file mode 100644 index 000000000000..b2dc26bd5029 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/legacyAppointmentForm/allDay.spec.ts @@ -0,0 +1,133 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../../tests/container.html')}`; + +test.describe('Layout:AppointmentForm:AllDay', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + +); + +test('Start and end dates should be reflect the current day(appointment is already available case)', async ({ page }) => { + // Scheduler on '#container' + const { legacyAppointmentPopup: appointmentPopup } = scheduler; + + await (page.locator('.dx-scheduler-appointment').filter({ hasText: 'Text' }).click().element) + .click(scheduler.appointmentTooltip.getListItem('Text').element); + + await testScreenshot(page, 'appointment-form-before-click-all-day.png'); + + await (appointmentPopup.allDayElement).click(); + + await testScreenshot(page, 'appointment-form-after-click-all-day.png'); + + await (appointmentPopup.doneButton).click(); + + await testScreenshot(page, 'all-day-appointment-on-tables.png'); + + await (page.locator('.dx-scheduler-appointment').filter({ hasText: 'Text' }).click().element) + .click(scheduler.appointmentTooltip.getListItem('Text').element); + + await testScreenshot(page, 'appointment-form-after-render-on-table.png'); + + await (appointmentPopup.allDayElement).click(); + + await testScreenshot(page, 'appointment-form-after-switch-off-all-day.png'); + + expect(compareResults.isValid()) + .ok(compareResults.errorMessages()); +}); + +// TODO: .before() block not converted - move to test setup +// { + await createWidget(page, 'dxScheduler', { + dataSource: [{ + text: 'Text', + startDate: new Date(2021, 3, 28, 10), + endDate: new Date(2021, 3, 28, 12), + }], + editing: { legacyForm: true }, + views: ['week'], + currentView: 'week', + currentDate: new Date(2021, 3, 29), + startDayHour: 9, + height: 600, + }); +}); + +test('Start and end dates should be reflect the current day(create new appointment case)', async ({ page }) => { + // Scheduler on '#container' + const { legacyAppointmentPopup: appointmentPopup } = scheduler; + + await (page.locator('.dx-scheduler-date-table-row').nth(2).locator('.dx-scheduler-date-table-cell').nth(3).dblclick()); + + await testScreenshot(page, 'new-appointment-form-before-click-all-day.png'); + + await (appointmentPopup.allDayElement).click(); + + await testScreenshot(page, 'new-appointment-form-after-click-all-day.png'); + + await (appointmentPopup.doneButton).click(); + + await testScreenshot(page, 'new-all-day-appointment-on-tables.png'); + + await (page.locator('.dx-scheduler-appointment').filter({ hasText: '' }).click().element) + .click(scheduler.appointmentTooltip.getListItem('').element); + + await testScreenshot(page, 'new-appointment-form-after-render-on-table.png'); + + await (appointmentPopup.allDayElement).click(); + + await testScreenshot(page, 'new-appointment-form-after-switch-off-all-day.png'); + + expect(compareResults.isValid()) + .ok(compareResults.errorMessages()); +}); + +// TODO: .before() block not converted - move to test setup +// { + await createWidget(page, 'dxScheduler', { + dataSource: [], + views: ['week'], + editing: { legacyForm: true }, + currentView: 'week', + currentDate: new Date(2021, 3, 29), + startDayHour: 9, + height: 600, + }); +}); + +test('StartDate and endDate should have correct type after "allDay" and "repeat" option are changed (T1002864)', async ({ page }) => { + // Scheduler on '#container' + const { legacyAppointmentPopup: appointmentPopup } = scheduler; + + await (page.locator('.dx-scheduler-date-table-row').nth(0).locator('.dx-scheduler-date-table-cell').nth(0).dblclick()); + + await testScreenshot(page, + 'form-before-change-allday-and-reccurence-options.png', + { element: appointmentPopup.content }, + ); + + await (appointmentPopup.allDayElement).click() + .click(appointmentPopup.recurrenceElement); + + await testScreenshot(page, + 'form-after-change-allday-and-reccurence-options.png', + { element: appointmentPopup.content }, + ); + + expect(compareResults.isValid()) + .ok(compareResults.errorMessages()); +}).before(async () => createWidget(page, 'dxScheduler', { + currentDate: new Date(2021, 1, 1), + editing: { legacyForm: true }, +})); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/legacyAppointmentForm/common.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/legacyAppointmentForm/common.spec.ts new file mode 100644 index 000000000000..315727121a78 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/legacyAppointmentForm/common.spec.ts @@ -0,0 +1,48 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../../tests/container.html')}`; + +test.describe('AppointmentForm screenshot tests', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + +); + +// visual: generic.light +// visual: fluent.blue.light +// visual: material.blue.light +test('Appointemt form tests', async ({ page }) => { + // --- setup --- +await createWidget(page, 'dxScheduler', { + currentDate: new Date(2021, 1, 1), + editing: { legacyForm: true }, + // --- test --- +// Scheduler on '#container' + const { legacyAppointmentPopup: appointmentPopup } = scheduler; + + await (page.locator('.dx-scheduler-date-table-row').nth(0).locator('.dx-scheduler-date-table-cell').nth(0).dblclick()); + + await testScreenshot(page, 'initial-form.png', { + element: appointmentPopup.content, + }); + + await (appointmentPopup.allDayElement).click() + .click(appointmentPopup.recurrenceElement); + + await testScreenshot(page, 'allday-and-reccurence-form.png', { + element: appointmentPopup.content, + }); + + expect(compareResults.isValid()) + .ok(compareResults.errorMessages()); +}); +}); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/legacyAppointmentForm/integerFormatNumberBox.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/legacyAppointmentForm/integerFormatNumberBox.spec.ts new file mode 100644 index 000000000000..5221847aa8bc --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/legacyAppointmentForm/integerFormatNumberBox.spec.ts @@ -0,0 +1,49 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../../tests/container.html')}`; + +test.describe('Layout:AppointmentForm:IntegerFormatNumberBox', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + +); + +test('dxNumberBox should not allow to enter not integer chars(T1002864)', async ({ page }) => { + // Scheduler on '#container' + const { legacyAppointmentPopup: appointmentPopup } = scheduler; + + await (page.locator('.dx-scheduler-appointment').filter({ hasText: 'Website Re-Design Plan' }).dblclick().element); + + await t + .typeText(appointmentPopup.repeatEveryElement, '.,2', { speed: 0.5 }); + + await testScreenshot(page, 'dx-number-boxes-not-integer-chars.png', { + element: appointmentPopup.content, + }); + + expect(compareResults.isValid()) + .ok(compareResults.errorMessages()); +}).before(async () => createWidget(page, 'dxScheduler', { + dataSource: [{ + text: 'Website Re-Design Plan', + startDate: new Date(2021, 3, 26, 10), + endDate: new Date(2021, 3, 26, 11), + recurrenceRule: 'FREQ=WEEKLY;BYDAY=MO,TH;UNTIL=20220114T205959Z', + }], + editing: { legacyForm: true }, + views: ['day', 'week', 'workWeek', 'month'], + currentView: 'week', + currentDate: new Date(2021, 3, 29), + startDayHour: 9, + height: 600, + recurrenceEditMode: 'series', +})); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/resources/base/resources.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/resources/base/resources.spec.ts new file mode 100644 index 000000000000..586fd3e78c75 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/resources/base/resources.spec.ts @@ -0,0 +1,106 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../../../tests/container.html')}`; + +const resourceDataSource = [{ + fieldExpr: 'priorityId', + dataSource: [ + { text: 'Low Priority', id: 0, color: '#24ff50' }, + { text: 'High Priority', id: 1, color: '#ff9747' }, + ], + label: 'Priority', +}]; + +const createDataSetForScreenShotTests = (): Record[] => { + const result: any[] = []; + for (let day = 1; day < 25; day++) { + result.push({ + text: '1 appointment', startDate: new Date(2020, 6, day, 0), + endDate: new Date(2020, 6, day, 1), priorityId: 0, + }); + result.push({ + text: '2 appointment', startDate: new Date(2020, 6, day, 1), + endDate: new Date(2020, 6, day, 2), priorityId: 1, + }); + result.push({ + text: '3 appointment', startDate: new Date(2020, 6, day, 3), + endDate: new Date(2020, 6, day, 5), allDay: true, priorityId: 0, + }); + } + return result; +}; + +test.describe('Scheduler: Resources layout', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + [undefined, resourceDataSource].forEach((resourcesValue) => { + ['agenda', 'day', 'week', 'month', 'workWeek'].forEach((view) => { + test(`Base views layout test with resources(view='${view}'), resource=${!!resourcesValue}`, async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource: createDataSetForScreenShotTests(), + currentDate: new Date(2020, 6, 15), + views: [view], + currentView: view, + resources: resourcesValue, + height: 600, + }); + + await page.locator('.dx-scheduler-header').click(); + await page.locator('.dx-scheduler-appointment').filter({ hasText: '1 appointment' }).first().click(); + await expect(page.locator('.dx-tooltip-appointment-item')).toBeVisible(); + + await testScreenshot(page, `resource(view=${view}-resource=${!!resourcesValue}).png`); + }); + }); + }); + + [undefined, resourceDataSource].forEach((resourcesValue) => { + ['timelineDay', 'timelineWeek', 'timelineMonth', 'timelineWorkWeek'].forEach((view) => { + test(`Timeline views layout test with resources(view='${view}'), resource=${!!resourcesValue}`, async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource: createDataSetForScreenShotTests(), + currentDate: new Date(2020, 6, 15), + views: [view], + currentView: view, + resources: resourcesValue, + height: 600, + }); + + await page.locator('.dx-scheduler-header').click(); + await page.locator('.dx-scheduler-appointment').filter({ hasText: '1 appointment' }).first().click(); + await expect(page.locator('.dx-tooltip-appointment-item')).toBeVisible(); + + await testScreenshot(page, `resource(view=${view}-resource=${!!resourcesValue}).png`); + }); + }); + }); + + test('Scheduler should have correct height in month view (T927862)', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource: [], + views: ['month'], + currentView: 'month', + height: 800, + }); + + const result = await page.evaluate(() => { + const dateTable = document.querySelector('.dx-scheduler-date-table'); + const scrollable = document.querySelector('.dx-scheduler-date-table-scrollable'); + if (!dateTable || !scrollable) return { match: false }; + const dtRect = dateTable.getBoundingClientRect(); + const scRect = scrollable.getBoundingClientRect(); + return { match: Math.abs(dtRect.bottom - scRect.bottom) < 1 }; + }); + + expect(result.match).toBeTruthy(); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/resources/groups/groups.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/resources/groups/groups.spec.ts new file mode 100644 index 000000000000..6be648015097 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/resources/groups/groups.spec.ts @@ -0,0 +1,94 @@ +import { test } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../../../tests/container.html')}`; + +const resourceDataSource = [{ + fieldExpr: 'priorityId', + dataSource: [ + { text: 'Low Priority', id: 0, color: '#24ff50' }, + { text: 'High Priority', id: 1, color: '#ff9747' }, + ], + label: 'Priority', +}]; + +const createDataSetForScreenShotTests = (): Record[] => { + const result: any[] = []; + for (let day = 1; day < 25; day++) { + result.push({ + text: '1 appointment', startDate: new Date(2020, 6, day, 0), + endDate: new Date(2020, 6, day, 1), priorityId: 0, + }); + result.push({ + text: '2 appointment', startDate: new Date(2020, 6, day, 1), + endDate: new Date(2020, 6, day, 2), priorityId: 1, + }); + result.push({ + text: '3 appointment', startDate: new Date(2020, 6, day, 3), + endDate: new Date(2020, 6, day, 5), allDay: true, priorityId: 0, + }); + } + return result; +}; + +test.describe('Scheduler: Groups layout', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + ['vertical', 'horizontal'].forEach((groupOrientation) => { + ['agenda', 'day', 'week', 'workWeek', 'month'].forEach((view) => { + test(`Base views layout test with groups(view='${view}', groupOrientation=${groupOrientation})`, async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource: createDataSetForScreenShotTests(), + currentDate: new Date(2020, 6, 15), + startDayHour: 0, + endDayHour: 4, + views: [{ + type: view, + name: view, + groupOrientation, + }], + currentView: view, + crossScrollingEnabled: true, + resources: resourceDataSource, + groups: ['priorityId'], + height: 700, + }); + + await testScreenshot(page, `groups(view=${view}-orientation=${groupOrientation}).png`); + }); + }); + }); + + ['vertical', 'horizontal'].forEach((groupOrientation) => { + ['timelineDay', 'timelineWeek', 'timelineWorkWeek', 'timelineMonth'].forEach((view) => { + test(`Timeline views layout test with groups(view='${view}', groupOrientation=${groupOrientation})`, async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource: createDataSetForScreenShotTests(), + currentDate: new Date(2020, 6, 15), + startDayHour: 0, + endDayHour: 4, + views: [{ + type: view, + name: view, + groupOrientation, + }], + currentView: view, + crossScrollingEnabled: true, + resources: resourceDataSource, + groups: ['priorityId'], + height: 700, + }); + + await testScreenshot(page, `groups(view=${view}-orientation=${groupOrientation}).png`); + }); + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/templates/appointmentTemplate.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/templates/appointmentTemplate.spec.ts new file mode 100644 index 000000000000..bafab00de374 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/templates/appointmentTemplate.spec.ts @@ -0,0 +1,78 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../../tests/container.html')}`; + +test.describe('Layout:Templates:appointmentTemplate', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + +); + +['day', 'workWeek', 'month', 'timelineDay', 'timelineWorkWeek', 'agenda'].forEach((currentView) => { + test(`appointmentTemplate layout should be rendered right in '${currentView}'`, async ({ page }) => { + // --- setup --- +await createWidget(page, 'dxScheduler', { + dataSource: [{ + startDate: new Date(2017, 4, 21, 0, 30), + endDate: new Date(2017, 4, 21, 2, 30), + }, { + startDate: new Date(2017, 4, 22, 0, 30), + endDate: new Date(2017, 4, 22, 2, 30), + }, { + startDate: new Date(2017, 4, 23, 0, 30), + endDate: new Date(2017, 4, 23, 2, 30), + }, { + startDate: new Date(2017, 4, 24, 0, 30), + endDate: new Date(2017, 4, 24, 2, 30), + }, { + startDate: new Date(2017, 4, 25, 0, 30), + endDate: new Date(2017, 4, 25, 2, 30), + }, { + startDate: new Date(2017, 4, 26, 0, 30), + endDate: new Date(2017, 4, 26, 2, 30), + }, { + startDate: new Date(2017, 4, 27, 0, 30), + endDate: new Date(2017, 4, 27, 2, 30), + }], + views: [currentView], + currentView, + currentDate: new Date(2017, 4, 25), + appointmentTemplate: ClientFunction((appointment) => { + const result = $('
'); + + const startDateBox = ($('
') as any).dxDateBox({ + type: 'datetime', + value: appointment.appointmentData.startDate, + // --- test --- +// Scheduler on '#container' + await testScreenshot(page, + `appointment-template-currentView=${currentView}.png`, + { element: page.locator('.dx-scheduler-work-space') }, + ); + + expect(compareResults.isValid()) + .ok(compareResults.errorMessages()); +}); + + const endDateBox = ($('
') as any).dxDateBox({ + type: 'datetime', + value: appointment.appointmentData.endDate, + }); + + result.append(startDateBox, endDateBox); + + return result; + }), + height: 600, + }); + }); +}); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/templates/appointmentTemplateTargetedData.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/templates/appointmentTemplateTargetedData.spec.ts new file mode 100644 index 000000000000..7ff867618a20 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/templates/appointmentTemplateTargetedData.spec.ts @@ -0,0 +1,280 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, generateOptionMatrix } from '../../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../../tests/container.html')}`; + +test.describe('Layout:Templates:appointmentTemplate:targetedData', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + +Appointment, + Orientation, + ScrollMode, + ViewType, +} from 'devextreme/ui/scheduler'; + +); + +const getResourceCount = ( + viewType: ViewType, + scrollMode: ScrollMode, + groupOrientation: Orientation, +): number => { + if ( + (viewType === 'workWeek' + || viewType === 'timelineWorkWeek' + || viewType === 'week' + || viewType === 'timelineWeek') + && (scrollMode === 'standard' && groupOrientation === 'horizontal') + ) { + return 2; + } + + if (scrollMode === 'standard') { + return 10; + } + + return 30; +}; + +const getGroupAppointmentDates = (viewType: ViewType): Date[] => { + const isWorkWeek = viewType === 'workWeek' || viewType === 'timelineWorkWeek'; + + if (isWorkWeek || viewType === 'week' || viewType === 'timelineWeek') { + const [ + dayCount, + startDate, + ] = isWorkWeek + ? [5, new Date(2024, 0, 1, 8)] + : [7, new Date(2023, 11, 31, 8)]; + + return Array.from( + { length: 12 * dayCount }, + (_, index) => { + const result = new Date(startDate); + result.setDate(result.getDate() + Math.floor(index / 12)); + result.setHours(result.getHours() + (index % 12)); + return result; + }, + ); + } + + if (viewType === 'month' || viewType === 'timelineMonth') { + return Array.from( + { length: 31 }, + (_, index) => { + const result = new Date(2024, 0, 1, 8); + result.setDate(result.getDate() + index); + return result; + }, + ); + } + + const startDate = viewType === 'agenda' + ? new Date(2024, 0, 1, 8) + : new Date(2024, 0, 2, 8); + + return Array.from( + { length: 12 }, + (_, index) => { + const result = new Date(startDate); + result.setHours(result.getHours() + index); + return result; + }, + ); +}; + +const viewTypes: ViewType[] = [ + 'agenda', + 'day', + 'week', + 'workWeek', + 'month', + 'timelineDay', + 'timelineWeek', + 'timelineWorkWeek', + 'timelineMonth', +]; + +const groupOrientations: Orientation[] = ['horizontal', 'vertical']; +const scrollModes: ScrollMode[] = ['standard', 'virtual']; +const rtlEnabledOptions: boolean[] = [false, true]; + +const testOptions = generateOptionMatrix({ + viewType: viewTypes, + groupOrientation: groupOrientations, + scrollMode: scrollModes, + rtlEnabled: rtlEnabledOptions, +}, [ + // Not supported + { + viewType: 'agenda', + scrollMode: 'virtual', + }, + // Not supported + { + viewType: 'agenda', + groupOrientation: 'horizontal', + }, + { + viewType: 'day', + groupOrientation: 'vertical', + scrollMode: 'standard', + }, + { + viewType: 'week', + groupOrientation: 'vertical', + scrollMode: 'standard', + }, + { + viewType: 'workWeek', + groupOrientation: 'vertical', + scrollMode: 'standard', + }, + { + viewType: 'day', + groupOrientation: 'horizontal', + rtlEnabled: true, + }, + { + viewType: 'week', + groupOrientation: 'horizontal', + rtlEnabled: true, + }, + { + viewType: 'workWeek', + groupOrientation: 'horizontal', + rtlEnabled: true, + }, + { + viewType: 'month', + groupOrientation: 'horizontal', + rtlEnabled: true, + }, + { + viewType: 'timelineDay', + groupOrientation: 'vertical', + rtlEnabled: true, + }, + { + viewType: 'timelineWeek', + groupOrientation: 'vertical', + rtlEnabled: true, + }, + { + viewType: 'timelineWorkWeek', + groupOrientation: 'vertical', + rtlEnabled: true, + }, + { + viewType: 'timelineMonth', + groupOrientation: 'vertical', + rtlEnabled: true, + }, +]); + +// NOTE: no assertions are present, checking but not throwing an error in a template function +testOptions.forEach(({ + viewType, + groupOrientation, + scrollMode, + rtlEnabled, +}) => { + test(`targetedAppointmentData should be correct with groups (viewType="${viewType}", groupOrientation="${groupOrientation}", scrollMode="${scrollMode}", rtlEnabled="${rtlEnabled}") (T1205120)`, async (t) => { + const currentDate = new Date(2024, 0, 2); + const HOUR = 1000 * 60 * 60; + const resourceCount = getResourceCount(viewType, scrollMode, groupOrientation); + const appointmentDates = getGroupAppointmentDates(viewType); + + const datesToCheck = [ + appointmentDates[0], + appointmentDates[Math.floor(appointmentDates.length / 3)], + appointmentDates[appointmentDates.length - 1], + ]; + + const groupsToCheck = [ + { groupId: 0 }, + { groupId: Math.floor(resourceCount / 3) }, + { groupId: resourceCount - 1 }, + ]; + + const resourceDataSource = Array.from({ length: resourceCount }, (_, index) => ({ + id: index, + text: `Resource ${index}`, + })); + + const appointments = resourceDataSource.reduce((acc, resource) => acc.concat( + appointmentDates + .map((date) => ({ + text: resource.text, + startDate: date, + endDate: new Date(date.getTime() + HOUR / 2), + groupId: resource.id, + })), + ), []); + + await createWidget(page, 'dxScheduler', { + rtlEnabled, + height: 600, + width: 800, + currentDate, + startDayHour: 8, + endDayHour: 20, + scrolling: { + mode: scrollMode, + }, + groups: ['groupId'], + views: [ + { + type: viewType, + groupOrientation, + }, + ], + currentView: viewType, + dataSource: appointments, + resources: [ + { + fieldExpr: 'groupId', + allowMultiple: true, + dataSource: resourceDataSource, + label: 'Employees', + displayExpr: 'id', + }, + ], + appointmentTemplate(model, _, element) { + const { groupId: targetedId } = model.targetedAppointmentData; + const { groupId } = model.appointmentData; + + if (groupId !== targetedId[0]) { + throw new Error('Group ID and targeted ID are mismatched'); + } + + element.append(`tid[${targetedId}] gid[${groupId}]`); + return element; + }, + }); + + if (scrollMode !== 'virtual') { + return; + } + + const scrollOptions = generateOptionMatrix({ + date: datesToCheck, + group: groupsToCheck, + }); + + // eslint-disable-next-line no-restricted-syntax + for (const { date, group } of scrollOptions) { + await scrollToDate(date, group); + await await page.waitForTimeout(50); + } + }); +}); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/templates/cellTemplate.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/templates/cellTemplate.spec.ts new file mode 100644 index 000000000000..a67287a5bfff --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/templates/cellTemplate.spec.ts @@ -0,0 +1,118 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../../tests/container.html')}`; + +test.describe('Layout:Templates:CellTemplate', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + +); + +const SCHEDULER_SELECTOR = '#container'; + +['day', 'workWeek', 'month', 'timelineDay', 'timelineWorkWeek', 'timelineMonth'].forEach((currentView) => { + test(`dataCellTemplate and dateCellTemplate layout should be rendered right in '${currentView}'`, async ({ page }) => { + // --- setup --- +await createWidget(page, 'dxScheduler', { + dataSource: [], + views: [currentView], + currentView, + currentDate: new Date(2017, 4, 25), + showAllDayPanel: false, + dataCellTemplate: ClientFunction((itemData) => ($('
') as any).dxDateBox({ + type: 'time', + value: itemData.startDate, + })), + dateCellTemplate: ClientFunction((itemData) => ($('
') as any).dxTextBox({ + value: new Intl.DateTimeFormat('en-US').format(itemData.date), + })), + height: 600, + // --- test --- +const scheduler = new Scheduler(SCHEDULER_SELECTOR); + await testScreenshot(page, + `data-cell-template-currentView=${currentView}.png`, + { element: page.locator('.dx-scheduler-work-space') }, + ); + + expect(compareResults.isValid()) + .ok(compareResults.errorMessages()); +}); + }); +}); + +test('[T1251590] Async dateCellTemplate should be rendered only once', async ({ page }) => { + // --- setup --- +await createWidget(page, 'dxScheduler', { + dataSource: [ + { + startDate: '2024-01-01T01:00:00', + endDate: '2024-01-01T02:00:00', + allDay: true, + }, + ], + dateCellTemplate: ClientFunction((_, __, itemElement) => { + setTimeout(() => { + itemElement.append('TEST'); + }, 0); + }), + currentDate: '2024-01-01', + currentView: 'week', + // --- test --- +const scheduler = new Scheduler(SCHEDULER_SELECTOR); + + const firstTableCell = scheduler.headerPanel.headerCells.nth(0); + + expect(firstTableCell.textContent).toBe('TEST'); +}); +}); + +test('[T1251590] Async dateCellTemplate should be rendered only once if has reference props (grouping)', async ({ page }) => { + const scheduler = new Scheduler(SCHEDULER_SELECTOR); + + const firstTableCell = scheduler.headerPanel.headerCells.nth(0); + + expect(firstTableCell.textContent).toBe('TEST'); +}); + +// TODO: .before() block not converted - move to test setup +// { + await createWidget(page, 'dxScheduler', { + dataSource: [ + { + startDate: '2024-01-01T01:00:00', + endDate: '2024-01-01T02:00:00', + allDay: true, + }, + ], + groups: ['groupId'], + resources: [ + { + label: 'group', + fieldExpr: 'groupId', + dataSource: [ + { + text: 'A', + id: 0, + color: '#00af2c', + }, + ], + }, + ], + dateCellTemplate: ClientFunction((_, __, itemElement) => { + setTimeout(() => { + itemElement.append('TEST'); + }, 0); + }), + currentDate: '2024-01-01', + currentView: 'week', + }); +}); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/templates/tooltipTemplate.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/templates/tooltipTemplate.spec.ts new file mode 100644 index 000000000000..a6f1a2211092 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/templates/tooltipTemplate.spec.ts @@ -0,0 +1,60 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../../tests/container.html')}`; + +test.describe('Layout:Templates:appointmentTooltipTemplate', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + +); + +test('appointmentTooltipTemplate layout should be rendered right', async ({ page }) => { + // --- setup --- +await createWidget(page, 'dxScheduler', { + dataSource: [{ + startDate: new Date(2017, 4, 25, 0, 30), + endDate: new Date(2017, 4, 25, 2, 30), + }], + views: ['workWeek'], + currentView: 'workWeek', + currentDate: new Date(2017, 4, 25), + appointmentTooltipTemplate: ClientFunction((appointment) => { + const result = $('
'); + + const startDateBox = ($('
') as any).dxDateBox({ + type: 'datetime', + value: appointment.appointmentData.startDate, + // --- test --- +// Scheduler on '#container' + await (scheduler.getAppointmentByIndex().click().element); + + await testScreenshot(page, + 'appointment-tooltip-template.png', + { element: page.locator('.dx-scheduler') }, + ); + + expect(compareResults.isValid()) + .ok(compareResults.errorMessages()); +}); + + const endDateBox = ($('
') as any).dxDateBox({ + type: 'datetime', + value: appointment.appointmentData.endDate, + }); + + result.append(startDateBox, endDateBox); + + return result; + }), + height: 600, + }); +}); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/timeIndication/currentTimeIndicator.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/timeIndication/currentTimeIndicator.spec.ts new file mode 100644 index 000000000000..ac623716506d --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/timeIndication/currentTimeIndicator.spec.ts @@ -0,0 +1,126 @@ +import { test } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../../tests/container.html')}`; + +test.describe('Scheduler: Current Time Indication', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('Current time indicator should be placed correctly when there are many groups and orientation is horizontal', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource: [], + currentDate: new Date(2021, 7, 1), + height: 400, + width: 700, + startDayHour: 5, + indicatorTime: new Date(2021, 7, 1, 6), + currentView: 'day', + views: ['day', 'week'], + groups: ['groupId'], + resources: [{ + fieldExpr: 'groupId', + label: 'group', + dataSource: [ + { text: 'Group 1', id: 1 }, + { text: 'Group 2', id: 2 }, + { text: 'Group 3', id: 3 }, + { text: 'Group 4', id: 4 }, + { text: 'Group 5', id: 5 }, + { text: 'Group 6', id: 6 }, + ], + }], + }); + + for (const view of ['day', 'week']) { + await page.evaluate((v: string) => { + ($('#container') as any).dxScheduler('instance').option('currentView', v); + }, view); + + await testScreenshot( + page, + `current-time-indicator-in-${view}-with-many-groups.png`, + { element: page.locator('.dx-scheduler-work-space') }, + ); + } + }); + + const TIMELINE_VIEWS = ['timelineDay', 'timelineWeek', 'timelineMonth']; + + [ + 'none', + 'vertical', + 'horizontal', + ].forEach((grouping) => { + [ + { view: 'day', cellDuration: 240 }, + { view: 'week', cellDuration: 240 }, + { view: 'timelineDay', cellDuration: 360 }, + { view: 'timelineWeek', cellDuration: 360 }, + { view: 'timelineMonth', cellDuration: 60 }, + ].forEach(({ view, cellDuration }) => { + [ + [0, 24], + [6, 18], + ].forEach(([startDayHour, endDayHour]) => { + [ + '2023-12-03T00:00:00', + '2023-12-03T06:30:00', + '2023-12-03T12:00:00', + '2023-12-03T17:30:00', + '2023-12-03T23:59:59', + ].forEach((indicatorTime) => { + if (grouping === 'horizontal' && TIMELINE_VIEWS.includes(view)) { + return; + } + if (view === 'timelineMonth' && startDayHour !== 0 && endDayHour !== 24) { + return; + } + + test(`Current time indicator should be rendered correctly (view: ${view}, now: ${indicatorTime}, grouping: ${grouping}, startDayHour: ${startDayHour}, endDayHour: ${endDayHour})`, async ({ page }) => { + const additionalOptions = grouping === 'none' + ? { + views: [{ type: view, name: 'TEST_VIEW' }], + } + : { + views: [{ type: view, name: 'TEST_VIEW', groupOrientation: grouping }], + groups: ['any'], + resources: [{ + fieldExpr: 'any', + dataSource: [ + { text: 'Group_0', id: 0 }, + { text: 'Group_1', id: 1 }, + ], + }], + }; + + await createWidget(page, 'dxScheduler', { + dataSource: [], + currentView: 'TEST_VIEW', + shadeUntilCurrentTime: true, + currentDate: indicatorTime, + startDayHour, + endDayHour, + indicatorTime, + cellDuration, + ...additionalOptions, + }); + + await testScreenshot( + page, + `current-time-indicator_${view}_${indicatorTime.replace(/:/g, '-')}_g-${grouping}_${startDayHour}_${endDayHour}.png`, + { element: page.locator('.dx-scheduler-work-space') }, + ); + }); + }); + }); + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/timeIndication/shader.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/timeIndication/shader.spec.ts new file mode 100644 index 000000000000..74e4209d7295 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/timeIndication/shader.spec.ts @@ -0,0 +1,123 @@ +import { test } from '@playwright/test'; +import { createWidget, testScreenshot, insertStylesheetRulesToPage } from '../../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../../tests/container.html')}`; + +test.describe('Scheduler: Current Time Indication: Shader', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + const views = ['day', 'week', 'timelineDay', 'timelineWeek', 'timelineMonth']; + const style = ` +.dx-scheduler-date-time-shader-top::before, +.dx-scheduler-date-time-shader-bottom::before, +.dx-scheduler-timeline .dx-scheduler-date-time-shader::before, +.dx-scheduler-date-time-shader-all-day { + background-color: red !important; +}`; + + const baseOptions = { + dataSource: [], + currentDate: new Date(2021, 7, 1), + height: 400, + width: 700, + startDayHour: 5, + indicatorTime: new Date(2021, 7, 1, 6), + currentView: 'day', + resources: [{ + fieldExpr: 'priorityId', + dataSource: [ + { text: 'Low Priority', id: 0, color: '#24ff50' }, + { text: 'High Priority', id: 1, color: '#ff9747' }, + ], + label: 'Priority', + }], + shadeUntilCurrentTime: true, + }; + + [false, true].forEach((crossScrollingEnabled) => { + test(`Shader should be displayed correctly when crossScrollingEnabled=${crossScrollingEnabled}`, async ({ page }) => { + await insertStylesheetRulesToPage(page, style); + await createWidget(page, 'dxScheduler', { + ...baseOptions, + views, + crossScrollingEnabled, + }); + + for (const view of views) { + await page.evaluate((v: string) => { + ($('#container') as any).dxScheduler('instance').option('currentView', v); + }, view); + + await testScreenshot( + page, + `shader-in-${view}-crossScrolling=${crossScrollingEnabled}.png`, + { element: page.locator('.dx-scheduler-work-space') }, + ); + } + }); + + test(`Shader should be displayed correctly when crossScrollingEnabled=${crossScrollingEnabled} and horizontal grouping is used`, async ({ page }) => { + await insertStylesheetRulesToPage(page, style); + await createWidget(page, 'dxScheduler', { + ...baseOptions, + views: [ + { type: 'day', groupOrientation: 'horizontal' }, + { type: 'week', groupOrientation: 'horizontal' }, + { type: 'tiemlineDay', groupOrientation: 'horizontal' }, + { type: 'timelineWeek', groupOrientation: 'horizontal' }, + { type: 'timelineMonth', groupOrientation: 'horizontal' }, + ], + crossScrollingEnabled, + groups: ['priorityId'], + }); + + for (const view of views) { + await page.evaluate((v: string) => { + ($('#container') as any).dxScheduler('instance').option('currentView', v); + }, view); + + await testScreenshot( + page, + `shader-in-${view}-crossScrolling=${crossScrollingEnabled}-horizontal-grouping.png`, + { element: page.locator('.dx-scheduler-work-space') }, + ); + } + }); + + test(`Shader should be displayed correctly when crossScrollingEnabled=${crossScrollingEnabled} and vertical grouping is used`, async ({ page }) => { + await insertStylesheetRulesToPage(page, style); + await createWidget(page, 'dxScheduler', { + ...baseOptions, + views: [ + { type: 'day', groupOrientation: 'vertical' }, + { type: 'week', groupOrientation: 'vertical' }, + { type: 'tiemlineDay', groupOrientation: 'vertical' }, + { type: 'timelineWeek', groupOrientation: 'vertical' }, + { type: 'timelineMonth', groupOrientation: 'vertical' }, + ], + crossScrollingEnabled, + groups: ['priorityId'], + }); + + for (const view of views) { + await page.evaluate((v: string) => { + ($('#container') as any).dxScheduler('instance').option('currentView', v); + }, view); + + await testScreenshot( + page, + `shader-in-${view}-crossScrolling=${crossScrollingEnabled}-vertical-grouping.png`, + { element: page.locator('.dx-scheduler-work-space') }, + ); + } + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/timeIndication/shaderVirtualScrolling.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/timeIndication/shaderVirtualScrolling.spec.ts new file mode 100644 index 000000000000..ae978f6f6431 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/timeIndication/shaderVirtualScrolling.spec.ts @@ -0,0 +1,91 @@ +import { test } from '@playwright/test'; +import { createWidget, testScreenshot, insertStylesheetRulesToPage } from '../../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../../tests/container.html')}`; + +test.describe('Scheduler: Current Time Indication: Shader with Virtual Scrolling', () => { + test.beforeEach(async ({ page }) => { + await page.setViewportSize({ width: 2560, height: 600 }); + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + const style = ` +.dx-scheduler-date-time-shader-top::before, +.dx-scheduler-date-time-shader-bottom::before, +.dx-scheduler-timeline .dx-scheduler-date-time-shader::before, +.dx-scheduler-date-time-shader-all-day { + background-color: red !important; +}`; + + const resources = [ + { text: 'Room 1', id: 1, color: '#cb6bb2' }, + { text: 'Room 2', id: 2, color: '#56ca85' }, + { text: 'Room 3', id: 3, color: '#1e90ff' }, + { text: 'Room 4', id: 4, color: '#ff9747' }, + { text: 'Room 5', id: 5, color: '#ff6a00' }, + { text: 'Room 6', id: 6, color: '#ffc0cb' }, + ]; + + test('Should render shader correct with virtual scrolling without current time indicator', async ({ page }) => { + await insertStylesheetRulesToPage(page, style); + + await createWidget(page, 'dxScheduler', { + dataSource: [], + currentView: 'week', + views: ['week'], + groups: ['roomId'], + resources: [{ fieldExpr: 'roomId', dataSource: resources, label: 'Room' }], + startDayHour: 8, + endDayHour: 18, + currentDate: new Date(2025, 9, 15), + height: 400, + shadeUntilCurrentTime: true, + scrolling: { mode: 'virtual' }, + }); + + await testScreenshot(page, 'shader-virtual-scrolling-week-start.png'); + + await page.evaluate(() => { + ($('#container') as any).dxScheduler('instance').scrollTo( + new Date(2025, 9, 15, 17, 30), { roomId: 6 }, + ); + }); + + await testScreenshot(page, 'shader-virtual-scrolling-week-end.png'); + }); + + test('Should render shader correctly with virtual scrolling and current time indicator', async ({ page }) => { + await insertStylesheetRulesToPage(page, style); + + await createWidget(page, 'dxScheduler', { + dataSource: [], + currentView: 'week', + views: ['week'], + groups: ['roomId'], + resources: [{ fieldExpr: 'roomId', dataSource: resources, label: 'Room' }], + startDayHour: 8, + endDayHour: 18, + currentDate: new Date(2025, 9, 15), + indicatorTime: new Date(2025, 9, 15, 17, 30), + height: 400, + shadeUntilCurrentTime: true, + scrolling: { mode: 'virtual' }, + }); + + await testScreenshot(page, 'shader-virtual-scrolling-week-start-with-current-time-indicator.png'); + + await page.evaluate(() => { + ($('#container') as any).dxScheduler('instance').scrollTo( + new Date(2025, 9, 15, 17, 30), { roomId: 6 }, + ); + }); + + await testScreenshot(page, 'shader-virtual-scrolling-week-end-with-current-time-indicator.png'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/views/crossScrolling.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/views/crossScrolling.spec.ts new file mode 100644 index 000000000000..59f89f37040c --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/views/crossScrolling.spec.ts @@ -0,0 +1,94 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../../tests/container.html')}`; + +test.describe('Scheduler: View with cross-scrolling', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + +); + +test('Scrollable synchronization should work after changing current date (T1027231)', async ({ page }) => { + // Scheduler on '#container' + await scheduler.option('currentDate', new Date(2021, 4, 5)); + await page.evaluate((d) => $('#container').dxScheduler('instance').scrollTo(new Date(d)), (new Date(2021, 4, 15).toISOString()), { priorityId: 2 }); + + await testScreenshot(page, 'cross-scrolling-sync.png', { + element: page.locator('.dx-scheduler-work-space'), + }); + + expect(compareResults.isValid()) + .ok(compareResults.errorMessages()); +}); + +// TODO: .before() block not converted - move to test setup +// { + await createWidget(page, 'dxScheduler', { + views: [{ + type: 'week', + name: 'Horizontal Grouping', + groupOrientation: 'horizontal', + cellDuration: 30, + intervalCount: 2, + }], + currentView: 'Horizontal Grouping', + crossScrollingEnabled: true, + currentDate: new Date(2021, 3, 21), + groups: ['priorityId'], + resources: [{ + fieldExpr: 'priorityId', + allowMultiple: false, + dataSource: [ + { + text: 'Low Priority', + id: 1, + color: '#1e90ff', + }, { + text: 'High Priority', + id: 2, + color: '#ff9747', + }, + ], + label: 'Priority', + }], + height: 600, + }); +}); + +test('Scrollable should be prepared correctly after change visibility (T1032171)', async ({ page }) => { + // Scheduler on '#container' + await scheduler.option('visible', true); + await page.evaluate((d) => $('#container').dxScheduler('instance').scrollTo(new Date(d)), (new Date(2021, 1, 12).toISOString())); + + await testScreenshot(page, 'cross-scrolling-sync-visibility.png', { + element: page.locator('.dx-scheduler-work-space'), + }); + + expect(compareResults.isValid()) + .ok(compareResults.errorMessages()); +}); + +// TODO: .before() block not converted - move to test setup +// { + await createWidget(page, 'dxScheduler', { + dataSource: [], + views: ['timelineMonth'], + currentView: 'timelineMonth', + currentDate: new Date(2021, 1, 2), + firstDayOfWeek: 0, + startDayHour: 8, + endDayHour: 20, + cellDuration: 60, + visible: false, + height: 400, + }); +}); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/views/day/allDay.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/views/day/allDay.spec.ts new file mode 100644 index 000000000000..87dd6fa0b24e --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/views/day/allDay.spec.ts @@ -0,0 +1,70 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../../../tests/container.html')}`; + +test.describe('Layout:Views:Day:AllDay', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + +); + +async function enableNativeScroll(page: Page) { + await page.evaluate(() => { + ($('#container') as any).dxScheduler('instance').getWorkSpaceScrollable().option('useNative', true); +}, ); +} + +[1, 2].forEach((intervalCount) => { + ['horizontal', 'vertical'].forEach((groupOrientation) => { + [true, false].forEach((showAllDayPanel) => { + const testName = `Day view with interval and crossScrollingEnabled(groupOrientation='${groupOrientation}', showAllDayPanel='${showAllDayPanel}', intervalCount='${intervalCount}') + layout test`; + + test(testName, async ({ page }) => { + // Scheduler on '#container' + await enableNativeScroll(); + + const pngName = `day-orientation=${groupOrientation}-allDay=${showAllDayPanel}-interval=${intervalCount}.png`; + + await testScreenshot(page, pngName, { element: page.locator('.dx-scheduler') }); + + expect(compareResults.isValid()) + .ok(compareResults.errorMessages()); + }).before(async () => createWidget(page, 'dxScheduler', { + resources: [{ + fieldExpr: 'roomId', + dataSource: [{ + text: 'Room 1', + id: 1, + }, { + text: 'Room 2', + id: 2, + }], + label: 'Room', + }], + dataSource: [], + views: [{ + name: 'dayView', + type: 'day', + intervalCount, + groupOrientation, + }], + currentView: 'dayView', + currentDate: new Date(2021, 2, 25), + height: 600, + groups: ['roomId'], + showAllDayPanel, + crossScrollingEnabled: true, + })); + }); + }); +}); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/views/firstDayOfWeek.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/views/firstDayOfWeek.spec.ts new file mode 100644 index 000000000000..46c0cadd6b51 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/views/firstDayOfWeek.spec.ts @@ -0,0 +1,37 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../../tests/container.html')}`; + +test.describe('Scheduler: View with first day of week', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + +); + +test('WorkWeek should generate correct start view date', async ({ page }) => { + // --- setup --- +await createWidget(page, 'dxScheduler', { + views: ['workWeek'], + currentView: 'workWeek', + firstDayOfWeek: 1, + currentDate: new Date(2021, 11, 12), + height: 600, + // --- test --- +// Scheduler on '#container' + await testScreenshot(page, 'work-week-first-day-of-week.png', { + element: page.locator('.dx-scheduler'), + }); + + expect(compareResults.isValid()) + .ok(compareResults.errorMessages()); +}); +}); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/views/intervalCount/viewsWithStartDate.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/views/intervalCount/viewsWithStartDate.spec.ts new file mode 100644 index 000000000000..1593d21ee045 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/views/intervalCount/viewsWithStartDate.spec.ts @@ -0,0 +1,121 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../../../tests/container.html')}`; + +test.describe('Layout: Views: IntervalCount with StartDate', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + +); + +[{ + view: 'timelineDay', + currentDate: new Date(2021, 4, 11), + startDate: new Date(2021, 4, 8), + intervalCount: 6, +}, { + view: 'week', + currentDate: new Date(2021, 4, 11), + startDate: new Date(2021, 3, 12), + intervalCount: 8, +}, { + view: 'timelineWeek', + currentDate: new Date(2021, 4, 11), + startDate: new Date(2021, 3, 12), + intervalCount: 8, +}, { + view: 'workWeek', + currentDate: new Date(2021, 4, 11), + startDate: new Date(2021, 3, 12), + intervalCount: 8, +}, { + view: 'timelineWorkWeek', + currentDate: new Date(2021, 4, 11), + startDate: new Date(2021, 3, 12), + intervalCount: 8, +}, { + view: 'month', + currentDate: new Date(2020, 5, 11), + startDate: new Date(2020, 3, 8), + intervalCount: 6, +}, { + view: 'timelineMonth', + currentDate: new Date(2020, 5, 11), + startDate: new Date(2020, 3, 8), + intervalCount: 6, +}].forEach(({ + view, currentDate, startDate, intervalCount, +}) => { + test(`startDate should work in ${view} view`, async ({ page }) => { + // Scheduler on '#container' + + await testScreenshot(page, `start-date-in-${view}.png`); + + await (page.locator('.dx-scheduler-date-table-row').nth(0).locator('.dx-scheduler-date-table-cell').nth(0).dblclick()); + + await testScreenshot(page, `start-date-in-${view}-with-form.png`); + + expect(compareResults.isValid()) + .ok(compareResults.errorMessages()); + }).before(async () => createWidget(page, 'dxScheduler', { + views: [{ + type: view, + intervalCount, + startDate, + }], + currentView: view, + currentDate, + dataSource: [], + crossScrollingEnabled: true, + })); +}); + +[{ + view: 'week', + currentDate: new Date(2020, 9, 6), + startDate: new Date(2020, 8, 16), + intervalCount: 3, +}, { + view: 'timelineWeek', + currentDate: new Date(2020, 9, 6), + startDate: new Date(2020, 8, 16), + intervalCount: 3, +}, { + view: 'workWeek', + currentDate: new Date(2020, 9, 6), + startDate: new Date(2020, 8, 16), + intervalCount: 3, +}, { + view: 'timelineWorkWeek', + currentDate: new Date(2020, 9, 6), + startDate: new Date(2020, 8, 16), + intervalCount: 3, +}].forEach(({ + view, currentDate, startDate, intervalCount, +}) => { + test(`startDate should work in ${view} view when it indicates the same week as the start as currentDate`, async ({ page }) => { + await testScreenshot(page, `complex-start-date-in-${view}.png`); + + expect(compareResults.isValid()) + .ok(compareResults.errorMessages()); + }).before(async () => createWidget(page, 'dxScheduler', { + views: [{ + type: view, + intervalCount, + startDate, + }], + currentView: view, + currentDate, + dataSource: [], + crossScrollingEnabled: true, + })); +}); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/views/material/withoutAllDay.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/views/material/withoutAllDay.spec.ts new file mode 100644 index 000000000000..7d7fe299a411 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/views/material/withoutAllDay.spec.ts @@ -0,0 +1,35 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../../../tests/container.html')}`; + +test.describe('Scheduler: Material theme without all-day panel', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + +); + +// visual: material.blue.light +test('Week view without all-day panel should be rendered correctly', async ({ page }) => { + // Scheduler on '#container' + await testScreenshot(page, 'week-without-all-day-panel.png', { + element: page.locator('.dx-scheduler-work-space'), + }); + + expect(compareResults.isValid()) + .ok(compareResults.errorMessages()); +}).before(async () => createWidget(page, 'dxScheduler', { + dataSource: [], + currentDate: new Date(2020, 6, 15), + views: ['week'], + currentView: 'week', + height: 500, +})); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/views/timeline/crossScrolling.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/views/timeline/crossScrolling.spec.ts new file mode 100644 index 000000000000..7f82b8fb1092 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/views/timeline/crossScrolling.spec.ts @@ -0,0 +1,81 @@ +import { test, expect } from '@playwright/test'; +import { createWidget } from '../../../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../../../tests/container.html')}`; + +test.describe('Scheduler Timeline: Cross-Scrolling', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + +); + +test('Timeline should have Cross-Scrolling enabled', async ({ page }) => { + // --- setup --- +await createWidget(page, 'dxScheduler', { + height: 400, + width: 800, + currentDate: new Date(2021, 1, 2), + dataSource: [], + views: ['timelineDay'], + currentView: 'timelineDay', + startDayHour: 8, + endDayHour: 20, + cellDuration: 60, + showAllDayPanel: false, + groups: ['humanId'], + resources: [{ + fieldExpr: 'humanId', + dataSource: [{ + id: 0, + text: 'David Carter', + color: '#74d57b', + }, { + id: 1, + text: 'Emma Lewis', + color: '#1db2f5', + }, { + id: 2, + text: 'Noah Hill', + color: '#f5564a', + }, { + id: 3, + text: 'William Bell', + color: '#97c95c', + }, { + id: 4, + text: 'Jane Jones', + color: '#ffc720', + }, { + id: 5, + text: 'Violet Young', + color: '#eb3573', + }, { + id: 6, + text: 'Samuel Perry', + color: '#a63db8', + }, { + id: 7, + text: 'Luther Murphy', + color: '#ffaa66', + }, { + id: 8, + text: 'Craig Morris', + color: '#2dcdc4', + }], + label: 'Employee', + }], + // --- test --- +// Scheduler on '#container' + + expect(await scheduler.workspaceHasBothScrollbar) + .ok(); +}); +}); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/views/timeline/grouping.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/views/timeline/grouping.spec.ts new file mode 100644 index 000000000000..0347976b1570 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/views/timeline/grouping.spec.ts @@ -0,0 +1,49 @@ +import { test, expect } from '@playwright/test'; +import { createWidget } from '../../../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../../../tests/container.html')}`; + +test.describe('Scheduler Timeline: Grouping', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + +); + +[ + 'timelineDay', + 'timelineWeek', + 'timelineWorkWeek', +].forEach((view) => { + test(`${view} view - header panel should contain group rows if horizontal grouping`, async ({ page }) => { + // --- setup --- +await createWidget(page, 'dxScheduler', { + groupOrientation: 'horizontal', + views: [{ + type: 'timelineDay', + groupOrientation: 'horizontal', + }], + currentView: 'timelineDay', + groups: ['one'], + resources: [{ + fieldExpr: 'one', + dataSource: [ + { id: 1, text: 'a' }, + { id: 2, text: 'b' }, + ], + }], + // --- test --- +// Scheduler on '#container' + + expect(await scheduler.headerPanel.groupCells.count) + .eql(2); +}); + }); +}); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/views/timeline/month.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/views/timeline/month.spec.ts new file mode 100644 index 000000000000..dedc9df542d5 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/views/timeline/month.spec.ts @@ -0,0 +1,37 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../../../tests/container.html')}`; + +test.describe('Scheduler: Layout Views: Timeline Month', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + +); + +test('Header cells should be aligned with date-table cells in timeline-month when current date changes', async ({ page }) => { + // --- setup --- +await createWidget(page, 'dxScheduler', { + currentDate: new Date(2020, 10, 1), + currentView: 'timelineMonth', + height: 600, + views: ['timelineMonth'], + crossScrollingEnabled: true, + // --- test --- +// Scheduler on '#container' + await scheduler.option('currentDate', new Date(2020, 11, 1)); + + await testScreenshot(page, 'timeline-month-change-current-date.png'); + + expect(compareResults.isValid()) + .ok(compareResults.errorMessages()); +}); +}); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/legacyAppointmentForm.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/legacyAppointmentForm.spec.ts new file mode 100644 index 000000000000..964ab92b684b --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/legacyAppointmentForm.spec.ts @@ -0,0 +1,210 @@ +import { test, expect } from '@playwright/test'; +import { createWidget } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('Legacy appointment popup form', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + +); + +test('Subject and description fields should be empty after showing popup on empty cell', async ({ page }) => { + const APPOINTMENT_TEXT = 'Website Re-Design Plan'; + + // Scheduler on '#container' + const { legacyAppointmentPopup: appointmentPopup } = scheduler; + + await (scheduler.getAppointment(APPOINTMENT_TEXT).dblclick().element) + .expect(appointmentPopup.subjectElement.value) + .eql(APPOINTMENT_TEXT) + + .typeText(appointmentPopup.descriptionElement, 'temp') + + .click(appointmentPopup.doneButton) + .doubleClick(page.locator('.dx-scheduler-date-table-row').nth(0).locator('.dx-scheduler-date-table-cell').nth(5)) + + .expect(appointmentPopup.subjectElement.value) + .eql('') + + .expect(appointmentPopup.descriptionElement.value) + .eql(''); +}).before(async () => createWidget(page, 'dxScheduler', { + views: ['month'], + currentView: 'month', + currentDate: new Date(2017, 4, 22), + height: 600, + width: 600, + editing: { legacyForm: true }, + dataSource: [ + { + text: 'Website Re-Design Plan', + startDate: new Date(2017, 4, 22, 9, 30), + endDate: new Date(2017, 4, 22, 11, 30), + }, + ], +})); + +test('Custom form shouldn\'t throw exception, after second show appointment form(T812654)', async (t) => { + const APPOINTMENT_TEXT = 'Website Re-Design Plan'; + const TEXT_EDITOR_CLASS = '.dx-texteditor-input'; + const CHECKBOX_CLASS = '.dx-checkbox.dx-widget'; + + // Scheduler on '#container' + + await (scheduler.getAppointment(APPOINTMENT_TEXT).dblclick().element, { + speed: 0.5, + }) + .click(CHECKBOX_CLASS) + + .expect(Selector(TEXT_EDITOR_CLASS).value) + .eql(APPOINTMENT_TEXT) + + .click(scheduler.legacyAppointmentPopup.cancelButton) + + .click(scheduler.getAppointment(APPOINTMENT_TEXT).element) + .click(scheduler.appointmentTooltip.getListItem(APPOINTMENT_TEXT).element) + + .expect(Selector(TEXT_EDITOR_CLASS).exists) + .eql(false); +}).before(async () => createWidget(page, 'dxScheduler', { + views: ['month'], + currentView: 'month', + currentDate: new Date(2017, 4, 22), + height: 600, + width: 600, + editing: { legacyForm: true }, + onAppointmentFormOpening: (e) => { + const items = [{ + name: 'show1', + dataField: 'show1', + editorType: 'dxCheckBox', + editorOptions: { + type: 'boolean', + onValueChanged: (args): boolean => e.form.itemOption('text1', 'visible', args.value), + }, + }, { + name: 'text1', + dataField: 'text', + editorType: 'dxTextArea', + colSpan: 6, + visible: false, + }]; + e.form.option('items', items); + }, + dataSource: [ + { + show1: false, + text: 'Website Re-Design Plan', + startDate: new Date(2017, 4, 22, 9, 30), + endDate: new Date(2017, 4, 22, 11, 30), + }, + ], +})); + +test.meta({ runInTheme: Themes.genericLight })('Appointment should have correct form data on consecutive shows (T832711)', async (t) => { + const APPOINTMENT_TEXT = 'Google AdWords Strategy'; + + // Scheduler on '#container' + const { legacyAppointmentPopup: appointmentPopup } = scheduler; + + await (scheduler.getAppointment(APPOINTMENT_TEXT).dblclick().element) + .expect(appointmentPopup.element.exists) + .ok() + .expect(appointmentPopup.isVisible()) + .ok() + .expect(appointmentPopup.subjectElement.value) + .eql(APPOINTMENT_TEXT) + + .click(appointmentPopup.allDayElement) + .click(appointmentPopup.cancelButton) + .expect(appointmentPopup.isVisible()) + .notOk(); + + await (scheduler.getAppointment(APPOINTMENT_TEXT).dblclick().element) + .expect(appointmentPopup.isVisible()) + .ok() + + .expect(appointmentPopup.endDateElement.value) + .eql('5/5/2017'); +}).before(async () => createWidget(page, 'dxScheduler', { + views: ['month'], + currentView: 'month', + currentDate: new Date(2017, 4, 25), + endDayHour: 20, + editing: { legacyForm: true }, + dataSource: [{ + text: 'Google AdWords Strategy', + startDate: new Date(2017, 4, 1), + endDate: new Date(2017, 4, 5), + allDay: true, + }], + height: 580, +})); + +test('From elements for disabled appointments should be read only (T835731)', async ({ page }) => { + const APPOINTMENT_TEXT = 'Install New Router in Dev Room'; + // Scheduler on '#container' + const { legacyAppointmentPopup: appointmentPopup } = scheduler; + + await (scheduler.getAppointment(APPOINTMENT_TEXT).dblclick().element) + .expect(appointmentPopup.freqElement.hasClass('dx-state-readonly')).toBeTruthy() + + .expect(appointmentPopup.subjectElement.value) + .eql(APPOINTMENT_TEXT) + + .typeText(appointmentPopup.subjectElement, 'New Title') + .expect(appointmentPopup.subjectElement.value) + .eql(APPOINTMENT_TEXT) + + .typeText(appointmentPopup.descriptionElement, 'description') + .expect(appointmentPopup.descriptionElement.value) + .eql('') + + .click(appointmentPopup.allDayElement) + .expect(appointmentPopup.startDateElement.value) + .eql('5/22/2017, 2:30 PM'); +}).before(async () => createWidget(page, 'dxScheduler', { + dataSource: [{ + text: 'Install New Router in Dev Room', + startDate: new Date(2017, 4, 22, 14, 30), + endDate: new Date(2017, 4, 25, 15, 30), + disabled: true, + recurrenceRule: 'FREQ=DAILY', + }], + editing: { legacyForm: true }, + currentView: 'week', + recurrenceEditMode: 'series', + currentDate: new Date(2017, 4, 25), + startDayHour: 9, + height: 600, +})); + +test('AppointmentForm should display correct dates in work-week when firstDayOfWeek is used', async ({ page }) => { + // Scheduler on '#container' + const { legacyAppointmentPopup: appointmentPopup } = scheduler; + + await (page.locator('.dx-scheduler-date-table-row').nth(2).locator('.dx-scheduler-date-table-cell').nth(4).dblclick()) + + .expect(appointmentPopup.startDateElement.value) + .eql('6/28/2021, 6:00 AM') + + .expect(appointmentPopup.endDateElement.value) + .eql('6/28/2021, 6:30 AM'); +}).before(async () => createWidget(page, 'dxScheduler', { + views: ['workWeek'], + currentView: 'workWeek', + editing: { legacyForm: true }, + currentDate: new Date(2021, 5, 28), + startDayHour: 5, + height: 600, + firstDayOfWeek: 2, +})); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/legacyAppointmentForm/appointmentPopupErrors.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/legacyAppointmentForm/appointmentPopupErrors.spec.ts new file mode 100644 index 000000000000..7158e73b305e --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/legacyAppointmentForm/appointmentPopupErrors.spec.ts @@ -0,0 +1,48 @@ +import { test, expect } from '@playwright/test'; +import { createWidget } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('Appointment Popup errors check', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + +); + +// NOTE: This test case requires page reloading, +// without page reloads the getBrowserConsoleMessages will return undefined. +test('Appointment popup shouldn\'t raise error if appointment is recursive', async ({ page }) => { + // --- setup --- +const data = [{ + text: 'Meeting of Instructors', + startDate: new Date('2020-11-01T17:00:00.000Z'), + endDate: new Date('2020-11-01T17:15:00.000Z'), + recurrenceRule: 'FREQ=DAILY;BYDAY=TU;UNTIL=20201203', + }]; + + return createWidget(page, 'dxScheduler', { + timeZone: 'America/Los_Angeles', + dataSource: data, + currentView: 'month', + currentDate: new Date(2020, 10, 25), + height: 600, + editing: { + legacyForm: true, + }, + // --- test --- +// Scheduler on '#container' + await (page.locator('.dx-scheduler-appointment').filter({ hasText: 'Meeting of Instructors' }).dblclick().element); + await (Scheduler.getEditRecurrenceDialog().click().series); + + const consoleMessages = await t.getBrowserConsoleMessages(); + expect(consoleMessages.error.length).toBe(0); +}); +}); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/legacyAppointmentForm/dataEditors.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/legacyAppointmentForm/dataEditors.spec.ts new file mode 100644 index 000000000000..ae78f4cb4bd3 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/legacyAppointmentForm/dataEditors.spec.ts @@ -0,0 +1,175 @@ +import { test, expect } from '@playwright/test'; +import { createWidget } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('Appointment popup form:date editors', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + +); + +test('Form date editors should be pass numeric chars according by date mask', async ({ page }) => { + // Scheduler on '#container' + const { legacyAppointmentPopup: appointmentPopup } = scheduler; + + await (page.locator('.dx-scheduler-appointment').filter({ hasText: 'Website Re-Design Plan' }).dblclick().element); + + await (appointmentPopup.subjectElement).click(); + + await t + .pressKey('tab') + .typeText(appointmentPopup.startDateElement, '111111111111') + .expect(appointmentPopup.startDateElement.value) + .eql('11/11/1111, 11:11 AM'); + + await t + .pressKey('tab') + .typeText(appointmentPopup.endDateElement, '111111111111') + .expect(appointmentPopup.endDateElement.value) + .eql('11/11/1111, 11:11 PM'); + + await t + .pressKey('tab') + .pressKey('tab') + .pressKey('tab') + .pressKey('tab') + .pressKey('tab') + .pressKey('tab') + .pressKey('tab') + .typeText(appointmentPopup.endRepeatDateElement, '11111111') + .expect(appointmentPopup.endRepeatDateElement.value) + .eql('11/11/1111'); +}).before(async () => createWidget(page, 'dxScheduler', { + dataSource: [{ + text: 'Website Re-Design Plan', + startDate: new Date(2021, 2, 30, 11), + endDate: new Date(2021, 2, 30, 12), + recurrenceRule: 'FREQ=DAILY;UNTIL=20211029T205959Z', + }], + recurrenceEditMode: 'series', + views: ['week'], + currentView: 'week', + currentDate: new Date(2021, 2, 28), + startDayHour: 9, + height: 600, + editing: { + legacyForm: true, + }, +})); + +test('Form date editors should not be pass chars according by date mask', async ({ page }) => { + // Scheduler on '#container' + const { legacyAppointmentPopup: appointmentPopup } = scheduler; + + await (page.locator('.dx-scheduler-appointment').filter({ hasText: 'Website Re-Design Plan' }).dblclick().element); + + await (appointmentPopup.subjectElement).click(); + + await t + .pressKey('tab') + .typeText(appointmentPopup.startDateElement, 'TEXT') + .expect(appointmentPopup.startDateElement.value) + .eql('3/30/2021, 11:00 AM'); + + await t + .pressKey('tab') + .typeText(appointmentPopup.endDateElement, 'TEXT') + .expect(appointmentPopup.endDateElement.value) + .eql('3/30/2021, 12:00 PM'); + + await t + .pressKey('tab') + .pressKey('tab') + .pressKey('tab') + .pressKey('tab') + .pressKey('tab') + .pressKey('tab') + .pressKey('tab') + .typeText(appointmentPopup.endRepeatDateElement, 'TEXT') + .expect(appointmentPopup.endRepeatDateElement.value) + .eql('10/29/2021'); +}).before(async () => createWidget(page, 'dxScheduler', { + dataSource: [{ + text: 'Website Re-Design Plan', + startDate: new Date(2021, 2, 30, 11), + endDate: new Date(2021, 2, 30, 12), + recurrenceRule: 'FREQ=DAILY;UNTIL=20211029T205959Z', + }], + recurrenceEditMode: 'series', + views: ['week'], + currentView: 'week', + currentDate: new Date(2021, 2, 28), + startDayHour: 9, + height: 600, + editing: { + legacyForm: true, + }, +})); + +test('Form date editors should not be pass chars after remove all characters according by date mask', async ({ page }) => { + // Scheduler on '#container' + const { legacyAppointmentPopup: appointmentPopup } = scheduler; + + await (page.locator('.dx-scheduler-appointment').filter({ hasText: 'Website Re-Design Plan' }).dblclick().element); + + await (appointmentPopup.startDateElement).click() + .selectText(appointmentPopup.startDateElement) + .pressKey('backspace') + + .typeText(appointmentPopup.startDateElement, 'TEXT') + .expect(appointmentPopup.startDateElement.value) + .eql('') + + .typeText(appointmentPopup.startDateElement, '1') + .expect(appointmentPopup.startDateElement.value) + .eql('1/30/2021, 11:00 AM'); + + await (appointmentPopup.endDateElement).click() + .selectText(appointmentPopup.endDateElement) + .pressKey('backspace') + + .typeText(appointmentPopup.endDateElement, 'TEXT') + .expect(appointmentPopup.endDateElement.value) + .eql('') + + .typeText(appointmentPopup.endDateElement, '1') + .expect(appointmentPopup.endDateElement.value) + .eql('1/30/2021, 12:00 PM'); + + await (appointmentPopup.endRepeatDateElement).click() + .selectText(appointmentPopup.endRepeatDateElement) + .pressKey('backspace') + + .typeText(appointmentPopup.endRepeatDateElement, 'TEXT') + .expect(appointmentPopup.endRepeatDateElement.value) + .eql('') + + .typeText(appointmentPopup.endRepeatDateElement, '1') + .expect(appointmentPopup.endRepeatDateElement.value) + .eql('1/29/2021'); +}).before(async () => createWidget(page, 'dxScheduler', { + dataSource: [{ + text: 'Website Re-Design Plan', + startDate: new Date(2021, 2, 30, 11), + endDate: new Date(2021, 2, 30, 12), + recurrenceRule: 'FREQ=DAILY;UNTIL=20211029T205959Z', + }], + recurrenceEditMode: 'series', + views: ['week'], + currentView: 'week', + currentDate: new Date(2021, 2, 28), + startDayHour: 9, + height: 600, + editing: { + legacyForm: true, + }, +})); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/legacyAppointmentForm/expressions.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/legacyAppointmentForm/expressions.spec.ts new file mode 100644 index 000000000000..8165534d8c8f --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/legacyAppointmentForm/expressions.spec.ts @@ -0,0 +1,622 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('Appointment form: expressions', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + +); + +const SCHEDULER_SELECTOR = '#container'; +const TEST_TITLE = 'Test'; +const TEST_DESCRIPTION = 'Test description...'; + +const getDataSourceValues = ClientFunction(() => ($(SCHEDULER_SELECTOR) as any) + .dxScheduler('instance') + .option('dataSource'), { dependencies: { SCHEDULER_SELECTOR } }); + +// tests config +// common +const TEXT_TEST_CASES = { + editor: 'text', + errorMessage: 'appointment\'s text incorrect', + getValue: async (scheduler: Scheduler) => scheduler.legacyAppointmentPopup.subjectElement().value, + setValue: async (t: TestController, scheduler: Scheduler, value: string) => t + .typeText(scheduler.legacyAppointmentPopup.subjectElement, value, { replace: true }), + setTestValue: '???', + 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', + }, + }, + ], +}; +const DESCRIPTION_TEST_CASES = { + editor: 'description', + errorMessage: 'appointment\'s description incorrect', + getValue: async (scheduler: Scheduler) => scheduler + .legacyAppointmentPopup.descriptionElement().value, + setValue: async (t: TestController, scheduler: Scheduler, value: string) => t + .typeText(scheduler.legacyAppointmentPopup.descriptionElement, value, { replace: true }), + setTestValue: '???', + 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', + }, + }, + ], +}; +const START_DATE_TEST_CASES = { + editor: 'startDate', + errorMessage: 'appointment\'s startDate incorrect', + getValue: async (scheduler: Scheduler) => scheduler + .legacyAppointmentPopup.startDateElement().value, + setValue: async (t: TestController, scheduler: Scheduler, value: string) => t + .typeText(scheduler.legacyAppointmentPopup.startDateElement, value, { replace: true }), + setTestValue: '10/10/2020, 01:00 AM', + 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', + }, + }, + ], +}; +const END_DATE_TEST_CASES = { + editor: 'endDate', + errorMessage: 'appointment\'s endDate incorrect', + getValue: async (scheduler: Scheduler) => scheduler.legacyAppointmentPopup.endDateElement().value, + setValue: async (t: TestController, scheduler: Scheduler, value: string) => t + .typeText(scheduler.legacyAppointmentPopup.endDateElement, value, { replace: true }), + setTestValue: '10/10/2020, 01:00 AM', + 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', + }, + }, + ], +}; +const ALL_DAY_TEST_CASES = { + editor: 'allDay', + errorMessage: 'appointment\'s allDay incorrect', + getValue: async (scheduler: Scheduler) => scheduler + .legacyAppointmentPopup.getAllDaySwitchValue(), + setValue: async (t: TestController, scheduler: Scheduler, value: string) => { + const currentValue = await scheduler.legacyAppointmentPopup.getAllDaySwitchValue(); + + if (currentValue !== value) { + await (scheduler.legacyAppointmentPopup.allDayElement).click(); + } + }, + setTestValue: 'false', + 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', + }, + }, + ], +}; + +// additional +const START_DATE_TIME_ZONE_TEST_CASES = { + editor: 'startDateTimeZone', + errorMessage: 'appointment\'s startDateTimeZone incorrect', + // eslint-disable-next-line @stylistic/max-len + getValue: async (scheduler: Scheduler) => scheduler.legacyAppointmentPopup.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', + }, + }, + ], +}; +const END_DATE_TIME_ZONE_TEST_CASES = { + editor: 'endDateTimeZone', + errorMessage: 'appointment\'s endDateTimeZone incorrect', + getValue: async (scheduler: Scheduler) => scheduler + .legacyAppointmentPopup.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', + }, + }, + ], +}; +const RECURRENCE_RULE_TEST_CASES = { + editor: 'recurrenceRule', + errorMessage: 'appointment\'s recurrenceRule incorrect', + getValue: async (scheduler: Scheduler) => scheduler + .legacyAppointmentPopup.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', + }, + }, + ], +}; + +[ + TEXT_TEST_CASES, + DESCRIPTION_TEST_CASES, + START_DATE_TEST_CASES, + END_DATE_TEST_CASES, + ALL_DAY_TEST_CASES, + START_DATE_TIME_ZONE_TEST_CASES, + END_DATE_TIME_ZONE_TEST_CASES, + RECURRENCE_RULE_TEST_CASES, +].forEach(({ + editor, + errorMessage, + getValue, + expectedValue, + cases, +}) => { + cases.forEach(({ + name, + options, + }) => { + test(`${editor}: ${name}`, async ({ page }) => { + // --- setup --- +await createWidget(page, 'dxScheduler', { + currentDate: '2023-12-10', + cellDuration: 240, + ...options, + editing: { + legacyForm: true, + ...options.editing, + }, + // --- test --- +const scheduler = new Scheduler(SCHEDULER_SELECTOR); + const appointment = scheduler.getAppointment(TEST_TITLE); + + expect(appointment).ok(`appointment with title: ${TEST_TITLE} not found.`); + + await (appointment.element).dblclick(); + + const value = await getValue(scheduler); + + expect(value).toBe(expectedValue, errorMessage); +}); + }); + }); +}); + +// test cases +[ + TEXT_TEST_CASES, + DESCRIPTION_TEST_CASES, + START_DATE_TEST_CASES, + END_DATE_TEST_CASES, + ALL_DAY_TEST_CASES, +].forEach(({ + editor, + setValue, + setTestValue, + cases, +}) => { + cases.forEach(({ + name, + options, + }) => { + test(`${editor}: ${name} should not mutate DataSource data directly`, async ({ page }) => { + // --- setup --- +await createWidget(page, 'dxScheduler', { + currentDate: '2023-12-10', + cellDuration: 240, + ...options, + editing: { + legacyForm: true, + ...options.editing, + }, + // --- test --- +const scheduler = new Scheduler(SCHEDULER_SELECTOR); + const appointment = scheduler.getAppointment(TEST_TITLE); + const expectedDataSource = options.dataSource; + + expect(appointment).ok(`appointment with title: ${TEST_TITLE} not found.`); + + await (appointment.element).dblclick(); + await setValue(t, scheduler, setTestValue); + await (scheduler.legacyAppointmentPopup.cancelButton).click(); + + const dataSource = await getDataSourceValues(); + + expect(dataSource).toBe(expectedDataSource); +}); + }); + }); +}); + +test( + 'Appointment popup should has correct width when the nested "recurrenceRuleExpr" option is set', async ({ page }) => { + const scheduler = new Scheduler(SCHEDULER_SELECTOR); + const appointment = scheduler.getAppointment(TEST_TITLE); + + await (appointment.element).dblclick(); + expect(scheduler.legacyAppointmentPopup.form.exists).toBeTruthy(); + + await testScreenshot(page, + 'form_recurrence-editor-first-opening_nested-expr.png', + { element: scheduler.legacyAppointmentPopup.content }, + ); + + expect(compareResults.isValid()) + .ok(compareResults.errorMessages()); + }, +}); + +// TODO: .before() block not converted - move to test setup +// { + await createWidget(page, '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', + editing: { + legacyForm: true, + }, + }); +}); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/legacyAppointmentForm/recurrenceEditor.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/legacyAppointmentForm/recurrenceEditor.spec.ts new file mode 100644 index 000000000000..721468965707 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/legacyAppointmentForm/recurrenceEditor.spec.ts @@ -0,0 +1,160 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('Appointment Form: recurrence editor', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + +); + +const SCHEDULER_SELECTOR = '#container'; + +const fillRecurrenceForm = async ( + t: TestController, + popup: LegacyAppointmentPopup, +): Promise => { + await (popup.recurrenceTypeElement).click(); + await (popup.getRecurrenceTypeSelectItem(2).click()); + await await (popup.repeatEveryElement).fill('10'); + await (popup.getEndRepeatRadioButton(1).click()); + await await (popup.endRepeatDateElement).fill('01/01/2024'); +}; + +test('Should not reset the recurrence editor value after the repeat toggling', async ({ page }) => { + // --- setup --- +await createWidget(page, 'dxScheduler', { + dataSource: [], + views: ['week'], + currentView: 'week', + currentDate: '2024-01-01T10:00:00', + editing: { + legacyForm: true, + }, + // --- test --- + const scheduler = new Scheduler(SCHEDULER_SELECTOR); + const popup = scheduler.legacyAppointmentPopup; + const cell = page.locator('.dx-scheduler-date-table-row').nth(0).locator('.dx-scheduler-date-table-cell').nth(0); + + await (cell).dblclick(); + await (popup.recurrenceElement).click(); + await fillRecurrenceForm(t, popup); + await (popup.recurrenceElement).click(); + await (popup.recurrenceElement).click(); + + await testScreenshot(page, 'recurrence-editor_after-hide.png', { element: popup.content }); + + expect(compareResults.isValid()) + .ok(compareResults.errorMessages()); +}); +}); + +test('Should reset the recurrence editor value after the popup reopening', async ({ page }) => { + // --- setup --- +await createWidget(page, 'dxScheduler', { + dataSource: [], + views: ['week'], + currentView: 'week', + currentDate: '2024-01-01T10:00:00', + editing: { + legacyForm: true, + }, + // --- test --- + const scheduler = new Scheduler(SCHEDULER_SELECTOR); + const popup = scheduler.legacyAppointmentPopup; + const cell = page.locator('.dx-scheduler-date-table-row').nth(0).locator('.dx-scheduler-date-table-cell').nth(0); + + await (cell).dblclick(); + await (popup.recurrenceElement).click(); + await fillRecurrenceForm(t, popup); + await (popup.cancelButton).click(); + await (cell).dblclick(); + await (popup.recurrenceElement).click(); + + await testScreenshot(page, 'recurrence-editor_after-popup-reopen.png', { element: popup.content }); + + expect(compareResults.isValid()) + .ok(compareResults.errorMessages()); +}); +}); + +test('Should correctly create usual appointment after repeat toggling', async ({ page }) => { + // --- setup --- +await createWidget(page, 'dxScheduler', { + dataSource: [], + views: ['week'], + currentView: 'week', + currentDate: '2024-01-01T10:00:00', + editing: { + legacyForm: true, + }, + // --- test --- +const scheduler = new Scheduler(SCHEDULER_SELECTOR); + const popup = scheduler.legacyAppointmentPopup; + const cell = page.locator('.dx-scheduler-date-table-row').nth(0).locator('.dx-scheduler-date-table-cell').nth(0); + + await (cell).dblclick(); + await (popup.recurrenceElement).click(); + await (popup.recurrenceElement).click(); + await (popup.doneButton).click(); + + expect(page.locator('.dx-scheduler-appointment').count()).toBe(1); +}); +}); + +test('Should correctly create recurrent appointment', async ({ page }) => { + // --- setup --- +await createWidget(page, 'dxScheduler', { + dataSource: [], + views: ['week'], + currentView: 'week', + currentDate: '2024-01-01T10:00:00', + editing: { + legacyForm: true, + }, + // --- test --- +const scheduler = new Scheduler(SCHEDULER_SELECTOR); + const popup = scheduler.legacyAppointmentPopup; + const cell = page.locator('.dx-scheduler-date-table-row').nth(0).locator('.dx-scheduler-date-table-cell').nth(0); + + await (cell).dblclick(); + await (popup.recurrenceElement).click(); + await (popup.doneButton).click(); + + expect(page.locator('.dx-scheduler-appointment').count()).toBe(7); +}); +}); + +test('Should correctly create recurrent appointment after repeat toggle', async ({ page }) => { + // --- setup --- +await createWidget(page, 'dxScheduler', { + dataSource: [], + views: ['week'], + currentView: 'week', + currentDate: '2024-01-01T10:00:00', + editing: { + legacyForm: true, + }, + // --- test --- +const scheduler = new Scheduler(SCHEDULER_SELECTOR); + const popup = scheduler.legacyAppointmentPopup; + const cell = page.locator('.dx-scheduler-date-table-row').nth(0).locator('.dx-scheduler-date-table-cell').nth(0); + + await (cell).dblclick(); + await (popup.recurrenceElement).click(); + await (popup.recurrenceElement).click(); + await (popup.recurrenceElement).click(); + await (popup.doneButton).click(); + + expect(page.locator('.dx-scheduler-appointment').count()).toBe(7); +}); +}); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/legacyAppointmentForm/showAppointmentPopup.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/legacyAppointmentForm/showAppointmentPopup.spec.ts new file mode 100644 index 000000000000..82fcb4690852 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/legacyAppointmentForm/showAppointmentPopup.spec.ts @@ -0,0 +1,81 @@ +import { test, expect } from '@playwright/test'; +import { createWidget } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('Appointment Form', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + +); + +async function showAppointmentPopup(page: Page) { + await page.evaluate(() => { + const instance = ($('#container') as any).dxScheduler('instance'); + instance.showAppointmentPopup(); +}, ); +} + +test('Invoke showAppointmentPopup method shouldn\'t raise error if value of currentDate property as a string', async ({ page }) => { + // --- setup --- +await ClientFunction(() => { + (window as any).DevExpress.ui.dxPopup.defaultOptions({ + options: { + deferRendering: false, + }, + // --- test --- +// Scheduler on '#container' + + await showAppointmentPopup(); + + expect(scheduler.legacyAppointmentPopup.startDateElement.value) + .eql('3/25/2021, 12:00 AM'); + + expect(scheduler.legacyAppointmentPopup.endDateElement.value) + .eql('3/25/2021, 12:30 AM'); +}).before(async () => createWidget(page, 'dxScheduler', { + dataSource: [], + views: ['week'], + currentView: 'week', + currentDate: new Date(2021, 2, 25).toISOString(), + height: 600, + editing: { + legacyForm: true, + }, +})); + +test('Show appointment popup if deffereRendering is false (T1069753)', async ({ page }) => { + // Scheduler on '#container' + const appointment = page.locator('.dx-scheduler-appointment').nth(0); + + await (appointment.element).dblclick() + .expect(scheduler.legacyAppointmentPopup.isVisible) + .ok(); +}); + })(); + + await createWidget(page, 'dxScheduler', { + dataSource: [{ + text: 'Test', + startDate: new Date(2021, 2, 29, 10), + endDate: new Date(2021, 2, 29, 11), + }], + views: ['day'], + currentView: 'day', + currentDate: new Date(2021, 2, 29), + startDayHour: 9, + endDayHour: 12, + width: 400, + editing: { + legacyForm: true, + }, + }); +}); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/legacyAppointmentForm/timezoneEditors.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/legacyAppointmentForm/timezoneEditors.spec.ts new file mode 100644 index 000000000000..8fa8255a2428 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/legacyAppointmentForm/timezoneEditors.spec.ts @@ -0,0 +1,97 @@ +import { test, expect } from '@playwright/test'; +import { createWidget } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('Layout:AppointmentForm:TimezoneEditors(T1080932)', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + +); + +const dataSource = [{ + text: 'Watercolor Landscape', + startDate: new Date('2020-06-01T17:30:00.000Z'), + endDate: new Date('2020-06-01T19:00:00.000Z'), + recurrenceRule: 'FREQ=WEEKLY', + startDateTimeZone: 'Etc/GMT+10', + endDateTimeZone: 'US/Alaska', +}]; + +const inputClassName = '.dx-texteditor-input'; +const startDateTimeZoneValue = '(GMT -10:00) Etc - GMT+10'; +const endDateTimeZoneValue = '(GMT -08:00) US - Alaska'; + +test.skip('TimeZone editors should be have data after hide forms data(T1080932)', async ({ page }) => { + // Scheduler on '#container' + const { legacyAppointmentPopup: appointmentPopup } = scheduler; + + await (page.locator('.dx-scheduler-appointment').nth(0).dblclick().element); + + const startDateTimeZone = appointmentPopup.wrapper.find(inputClassName).nth(1); + expect(startDateTimeZone.value).toBe(startDateTimeZoneValue); + + const endDateTimeZone = appointmentPopup.wrapper.find(inputClassName).nth(3); + expect(endDateTimeZone.value).toBe(endDateTimeZoneValue); +}); + +// TODO: .before() block not converted - move to test setup +// { + await createWidget(page, 'dxScheduler', { + dataSource, + onAppointmentFormOpening: (e) => { + e.form.itemOption('mainGroup.text', 'visible', false); + }, + editing: { + allowTimeZoneEditing: true, + legacyForm: true, + }, + recurrenceEditMode: 'series', + views: ['month'], + currentView: 'month', + currentDate: new Date(2020, 6, 25), + startDayHour: 9, + height: 600, + }); +}); + +test.skip('TimeZone editors should be have data in default case(T1080932)', async ({ page }) => { + // Scheduler on '#container' + + await (page.locator('.dx-scheduler-appointment').nth(0).dblclick().element); + + const { legacyAppointmentPopup: appointmentPopup } = scheduler; + + await (page.locator('.dx-scheduler-appointment').nth(0).dblclick().element); + + const startDateTimeZone = appointmentPopup.wrapper.find(inputClassName).nth(2); + expect(startDateTimeZone.value).toBe(startDateTimeZoneValue); + + const endDateTimeZone = appointmentPopup.wrapper.find(inputClassName).nth(4); + expect(endDateTimeZone.value).toBe(endDateTimeZoneValue); +}); + +// TODO: .before() block not converted - move to test setup +// { + await createWidget(page, 'dxScheduler', { + dataSource, + editing: { + allowTimeZoneEditing: true, + legacyForm: true, + }, + recurrenceEditMode: 'series', + views: ['month'], + currentView: 'month', + currentDate: new Date(2020, 6, 25), + startDayHour: 9, + height: 600, + }); +}); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/loadingPanel.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/loadingPanel.spec.ts new file mode 100644 index 000000000000..21e496d5d80e --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/loadingPanel.spec.ts @@ -0,0 +1,55 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('Scheduler loading panel', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + +const SCHEDULER_SELECTOR = '#container'; +const INITIAL_APPOINTMENT_TITLE = 'appointment'; +const ADDITIONAL_TITLE_TEXT = '-updated'; + +); + +test('Save appointment loading panel screenshot', async ({ page }) => { + const scheduler = new Scheduler(SCHEDULER_SELECTOR); + const appointment = scheduler.getAppointment(INITIAL_APPOINTMENT_TITLE); + const { appointmentPopup } = scheduler; + + await (appointment.element).dblclick() + .click(appointmentPopup.textEditor.element) + .typeText(appointmentPopup.textEditor.element, ADDITIONAL_TITLE_TEXT) + .click(appointmentPopup.saveButton.element); + + await testScreenshot(page, 'save-appointment-loading-panel-screenshot.png', { element: page.locator('.dx-scheduler') }); + + expect(compareResults.isValid()) + .ok(compareResults.errorMessages()); +}).before(async () => createWidget(page, 'dxScheduler', { + dataSource: [{ + id: 1, + text: INITIAL_APPOINTMENT_TITLE, + startDate: new Date(2021, 2, 29, 9, 30), + endDate: new Date(2021, 2, 29, 11, 30), + }], + views: ['day'], + currentView: 'day', + currentDate: new Date(2021, 2, 29), + startDayHour: 9, + endDayHour: 14, + height: 600, + onAppointmentUpdating: (e) => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + e.cancel = new Promise((resolve, reject) => {}); + }, +})); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/month/diferrentInvtervalCounts.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/month/diferrentInvtervalCounts.spec.ts new file mode 100644 index 000000000000..821205404694 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/month/diferrentInvtervalCounts.spec.ts @@ -0,0 +1,48 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('Scheduler: different intervalCount option values', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('Interval count: 1, February of 2021', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + views: [{ + type: 'month', + intervalCount: 1, + }], + currentView: 'month', + firstDayOfWeek: 1, + currentDate: new Date(2021, 1, 1), + }); + + await testScreenshot(page, 'month-february-2021.png', { element: page.locator('.dx-scheduler-work-space') }); + }); + + test('Interval count: 12', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + views: [{ + type: 'month', + intervalCount: 12, + }], + height: 600, + currentView: 'month', + currentDate: new Date(2023, 6, 1), + }); + + await page.evaluate(() => { + ($('#container') as any).dxScheduler('instance').scrollTo(new Date(2024, 6, 1)); + }); + + await testScreenshot(page, 'month-interval-count-12.png', { element: page.locator('.dx-scheduler-work-space') }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/month/etalons/month-february-2021.png b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/month/etalons/month-february-2021.png new file mode 100644 index 0000000000000000000000000000000000000000..14f09cd7ebc579760a28a3915dafcd773bd5ea94 GIT binary patch literal 20120 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 F`yYm*<>e| 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 W$FJ8!df_))F6tOwC^+}$gZ~G$CBilU literal 0 HcmV?d00001 diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/month/etalons/month-long-appointment-rtl-false-text-Appointment-spans-2-rows-.png b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/month/etalons/month-long-appointment-rtl-false-text-Appointment-spans-2-rows-.png new file mode 100644 index 0000000000000000000000000000000000000000..4d20fb1d1bde5c89fc60815d9a995d102745b72f GIT binary patch literal 26385 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 Dd`C24 literal 0 HcmV?d00001 diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/month/etalons/month-long-appointment-rtl-false-text-Appointment-spans-3-rows-.png b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/month/etalons/month-long-appointment-rtl-false-text-Appointment-spans-3-rows-.png new file mode 100644 index 0000000000000000000000000000000000000000..e505040b74a9f550a6732e843513f3a8ee0b07d8 GIT binary patch literal 29454 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 N*k@q0i>dDr_8(1>`+xud literal 0 HcmV?d00001 diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/month/etalons/month-long-appointment-rtl-false-text-Appointment-spans-all-rows-.png b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/month/etalons/month-long-appointment-rtl-false-text-Appointment-spans-all-rows-.png new file mode 100644 index 0000000000000000000000000000000000000000..781289d45321e87bd94cb5a7d10ccec3f93e1db1 GIT binary patch literal 37622 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 literal 0 HcmV?d00001 diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/month/etalons/month-long-appointment-rtl-true-text-Appointment-spans-2-rows-.png b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/month/etalons/month-long-appointment-rtl-true-text-Appointment-spans-2-rows-.png new file mode 100644 index 0000000000000000000000000000000000000000..dc71d1f5fcb69b4d7a6bb599d78ec572abe58bbd GIT binary patch literal 26058 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

14@6T!RzHR%G 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

gl27R0?*I6UfeQx*2d=i7q82_G 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%0A2}d;D9X{g^+mzd3nG3dFH7H3>M?k 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 literal 0 HcmV?d00001 diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/month/etalons/month-long-appointment-several-months-february-rtl-true-.png b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/month/etalons/month-long-appointment-several-months-february-rtl-true-.png new file mode 100644 index 0000000000000000000000000000000000000000..17c1ad6e0d3c74099101e1ec86a2de872ecb4e1e GIT binary patch literal 26939 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 G|M)l0$yD?J literal 0 HcmV?d00001 diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/month/etalons/month-long-appointment-several-months-january-rtl-false-.png b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/month/etalons/month-long-appointment-several-months-january-rtl-false-.png new file mode 100644 index 0000000000000000000000000000000000000000..23188b517c2cb462d3238831d20d7c819ad5f80b GIT binary patch literal 26004 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 ap_az&_vqn1$tIVOL+sBwoq1^MpYnJ1q?$MY literal 0 HcmV?d00001 diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/month/etalons/month-long-appointment-several-months-january-rtl-true-.png b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/month/etalons/month-long-appointment-several-months-january-rtl-true-.png new file mode 100644 index 0000000000000000000000000000000000000000..a2aa9157a9242a1cf946d014a7b2075887a978e8 GIT binary patch literal 25799 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 hs{)R(KP4`mnpMTx|e*iPxYgqsQ literal 0 HcmV?d00001 diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/month/etalons/month-long-appointment-several-months-march-rtl-true-.png b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/month/etalons/month-long-appointment-several-months-march-rtl-true-.png new file mode 100644 index 0000000000000000000000000000000000000000..09c96c157bad4b3c8ce6ad0311c2cbe2dc795753 GIT binary patch literal 21986 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 literal 0 HcmV?d00001 diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/month/etalons/month-long-recurrence-appointment-several-months-february.png b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/month/etalons/month-long-recurrence-appointment-several-months-february.png new file mode 100644 index 0000000000000000000000000000000000000000..1c81ed2ebb4572053c94d979fa075c3dbc058c29 GIT binary patch literal 35347 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 X%aei3*b}$N-=TOz?RxIj+X4Rv*0BF9 literal 0 HcmV?d00001 diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/month/etalons/month-long-recurrence-appointment-several-months-january.png b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/month/etalons/month-long-recurrence-appointment-several-months-january.png new file mode 100644 index 0000000000000000000000000000000000000000..8406434f8223c929e1ccf2fb07d151890ed77ca9 GIT binary patch literal 32742 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 literal 0 HcmV?d00001 diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/month/etalons/regression-month-february-2021.png b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/month/etalons/regression-month-february-2021.png new file mode 100644 index 0000000000000000000000000000000000000000..14f09cd7ebc579760a28a3915dafcd773bd5ea94 GIT binary patch literal 20120 zcmeHvXINBOw~7!N)!Q!N)XACkyN4- zh$KPDp_CjX$I7#|GdIlKd%wB&neUnV=lfy*prPuVv-e)>P3r_*($?6vdEaIZ4vuZ- zG|%dBaQyy~gX1^9KYoLsT;7dS<>1)KaqjFX1Mj$>{eFFhZUY$B^QR)I5&QLIbYBfe z9~P03X|9Xgt@xmtpg-gvS8FO2Hl%W`F;I_Cuv1#7>=MYSv?)^TwBx#>4S4vv!te^=q)xO4UoZ}`igopbQt-3PvN za&U;8eh}^y zRHHet7*sx6yPm@9iT(>gd8(2{x!>d6+s<(vzQC(+>$`LLbaA7G7v-1`Sn7q zxyOV;25XgspLsWin@c|G8zpWowVON5O(c&#Q|=P)(NVw3ptj(vhQgG!6@EOpzg_7l zMcVdr1dd4)xb>b>QjI}mjqqaSDE^Ct#FV4sS*8W>IlS8}d?!*#j1dX+&)D3folxjc zn@H0T3Rsd-=9hO?@n4&>^%OVF7ZK+a%`R+P(Em;A8^241YxR1b6DDu}#Fw*ZQ z&&4hmXHnVv6uqqatGc8Ft>_n#OciHQ0A{IhE z!{H+ui(RIS--0GOvU?{pa}!+Z+(|x*qpqW4E}dqM*#vi|Jn~-M`H}(OOy0TLFaiGD z+$!1)$?xPes=V~>{Mjn{D5uNhw{+bZhXJRG`C2~Ps}pUh^F2A{HC1TZ9!ze=5*K;bn#Nt8akwpJ|Je!q_4GoT$We__p@c|Xv1(alx zWz^}1%}ON8mDj%C=a;*=R>P%J!B}YEF?VkriLy-4r5U*sSZhN>4E<+;0yV_%xTvW0 zIeB4S)h!Ft(K9Y(6R*?aZfjxrK3Vt1x>R+Sd(C#4RCv#?%@0`*#d{3|safLci8@2= zr=6{6E7P$q6|joC<_0+em&*wGd0(}a1$@U}$@(9-eKlUmqqx_(oM2EBr)H5`Hksi+ zOtFnJkCAiQw?#R8sd&J5YOr&m&0b`<+deUL3$5RaA}}F7FjGaJIg;Q#=$Kn6hi5Uk zNf$rfoqtwznpEA8=HAekF+ZM6+>80F{(`-fhEeA_x*^bktupt19hLRYZA=h$%*RLD z%9q;C4{=PkXBwa2tZOE;Ej3EFoIRCxG1jP0`s({T=%XnK()P{m#+H>799{iYi|T6M z_;$O9h<2mP*@l@$WTA#iHj5rUWhHsU0t1iI-#+Vm(eJB3fS zV{vXtOH^Ong`rF%&!zEJpH^$SJc-%_&)1U@usX7_Hm^?5v2yH) z!^7V!hH690J*W3&mXPW0)`9F^3acagatStcA2-RfL9C?DZ?N>cMuSnkb_GV?e zcWki?4!`7KT5C_p6#eu1ydU=MhmO;VU2oJ4tB^V#kN09H7C-USo!PoTmKeLwfQJ#q z{b)7H!uMFDkKfYxt~zXr@&Wb{2X&WL!gn>#pIyclKDLhqJU?xAq=oDiQ-U41=vQ+3 zN`nw??MHLG@(w{21vaNNeU@92 zoOn8y;8Yc?_0nU=B5Wd_VLh-uFvqfS8%smpa{*r+%zE(vcAu2pb*jGM=?g3?BjaTr z1#0DGa_?$#Jx?YhZ`W6*zfR*G>*gqzx)C6oFl3)5Ys zRxCdmTVU4|ETpc`sHwZ3ciy+d)ZVaP5gH@msU8W(^qlVD#@i%rtne0)yJ>P_ikx)u z$_^{DCp*S>jU|;^F5`*k)mfQS%bk{_Qn&T=dyE=xjaid~CKhu^z3tY|ygH-RC)_&P zIwtPJC8D1;>zuEecpYo?1P}k`V2|ozqjaZ*pVL5Q6V>thu^`zJ)h$UiL=0;UwuLV& z@Xxm&xBKK4Z!k=orxrU_>ol>ypR4oxnWyo4g>Eu-ByCm7%Q35brp)vQw#lltp;v{9 zYtvtDc{N&M`?%Pt7n_0~fAu&+%`n}$zl<;Su=vU8icZ*lS0E$ z7hbnvYv7^s=y6L|GPh*25S}qFpyck*SL&Lm7I2m+Q?pZ`sXIDJ-l<1M^u+K?`D~Ts zBaQbV+5tF~J!}~(lhRw)u;)x$FUx$4am;x~-g}vFqU%Ox_JVcNQIbAs(&c6Xjy5rf z)7zblHyWvJFv&Sqyeiji^DOIDwFAwCy;g@=(3sRWTi~BdO6B3Fe!M*-i@oD8Z}U)S z!uFx$t5f$ksdFCxyHtU(kPoiAZ|0mY$YCE2l7XCY!A$hGfOZsYK`7 zE*0}w>^W*8GrMiwc7^YZ#TH`dV6uMj!I`=^38Te=4?Daq@XY0LX5Gu3`X|qvv|9Lr z>ooTwn@|8@p{}*o+N?aQ;y(Umi$ulTmoqy;i~68~{D3lbdDPiL>vUF+deY^5n{)F! z`W`!shw;mcukf&0%eoo@9)M9;qw=KgfYtu7#uskTU2WJF1DL@;Vbt9j$-%+FyM(be zA{mb)IoFOUkH5U11DoX|oDbMnmmRZ<4r|9nv!@q$sf%?f7$~ioS@~LpH-mLFo7G7Y zN_XV@Zk=gKP*wJrjL_9fDs??2ByD%$O0rk4Q^})k{46K|{)0hBlFVb-O@tcht`)1-1Je zYp#g1*L%cu-rr)SoHOdfg((4iS)0Oc)ewqT3n;r|9Bfr>_vy(2JiV6^Ow_+YsS1ct z!;I)CjzpmL6nalVB&n}8RV-|Sp~jtk*c~jP46gWP#4p%>k>ck23d$H^0=PjyitJ~T z_~mxv@NwK+7-M{e{Ml(~p=lj1hV`*cVs4~qoj|E&w z9&+5mBi?uQ{>eeo|Kn4prlT6~)WfIt zZyGt1{pxJd2@@Rt!lYGDRT*xm>95#wqM38f6C}K@7~l=06YaY~Ms2Si0a`M9y^ialgiMr2n)$T24DLC zBJ5wFTfZj~0YE+vKPb%*$-eZS{!xha7TCs5KIIPY6xa|CH1l7*z;@*{6ie=;Z3m&ON$M;`eov~(b zSN{37+b(t+?_XD8U-NAW%DD)oH>v8fOXc#pBk@acBu^|p+{AVGrokLwUt5usvNjBD zJei;AWnD|C*Zfq_qE^1xtXwA}AwDjdoT=1Fr#@%{EDRS(z z)utjGlAQ8Qa8A&FuA1Mu^xE9_YN&m+l6CvtfDn>^=H+1y`@5b*G^i;?_8DE@1Gp&E zs~Nz^^g0Zy6*>|i2BpL%(sIY;bsM)=3bT&Mz#7Dum6a zCkh)9d?(YR2Q6>cAL2`o?iQ#i|0gW~XvL-@ac@Us9BaD-i&(?Ybea{02qu{lxC47V z(0Oy1u}MjN)P@KAx{I2SeQ>}o<=|^KM0q$j?%!EZj-D)_r6C}n33wT+nn2#Uq1$t# zu`abmYGDs3td$bJ^f%6r*uXP*bXMNb0bf+k*Yew=qBej^&Np3+<)7Vdu#Ch|-j3us z(`#oHpkYI0-21RavlYs_AoRp7;-xPG=qpXJ@}#*BTarDZVG2t2M>)eRpn-hpf!sa3lfpy5rc*ALIH$p*1ZMdH z?+)dgoMApYP{|K%zLWN8UFQ|Ii`e5Cu|G;~v}btdRuBmA^JgvQAX2UMZoj`nZ+kFh5AntKw-;oXRP8#-2+*^c_x& zss+*#1`|9wOvhkR&^frgju*^>}^<=#^2Gd2u&ED$FFqT z2hf(%X?{1g`fGx?tZx|m$5JA>LrSPz#o|?KgGXtwt#{I`Cs-`{Y>VcH;KsQ`07B8< z$wg|=@fPfN8(nrP@{Y&gajrTt9#)6slefy;_;ys(!2h|SQT?hva1GdOxwQDlg1+M` zKsw;C!Scudy7r#yX#zl;KqWzYK@r#^>^6DfO_CKF*&!}GVrCJujhjiI0~INis{^s& zN&B=&{hqyK>Hrcb!XA(St&%?pUB?J-+BG`X9DmuLUl?yxy18cZq7g4+wsa`WcX^U5 zx%2$0cB|LK{!I&+SlM`U00B0XMLFE~yR51AQ<=BG@k}T))CKq%a$*$GnnL zPvPr}v7>&+j~@qmlQzg>;XhA~?zX&Dy}tZs{(0=_8)5-CN0wag&95&{-OtP^o9ra9 zJaFkRzp0yMvIfOn=CSm{k+b3?#sYv_H4t99NuAyr1m3)l+m+pC`V^Cw42@~ts?_0U zA@re$+3o^+mdJ*-FHn{;@Y|12oV)XFS%L8Yy_MR`S*>BpGrW%P3hY}vlkI-H5&r`- zU&G}(K>{5{TIr~0E+FG@DNQ?GMN6Jfmtqn@H$Qb@EeDnt9+etf(Wo60urlB` z3pE!Zx9 zLNnZ7P#Ilo4qBw4Q02~~rlFfUhHMt|$c5-zfMRPs8;6j3cGSygE%^;KS9Wjg_<1=D2ilvwTvB?w?)x+@9vl#I|8i{z<)BKG+okhS;s8B; zzrMa`oh^}^l67?g#J7%@r`C}Q&(&`3P|d7bavN<7Do|tX^12Zb$t@A!?*`Ap6lai# z!Ly*Xz>Xc66^e}HK2gbs^G#Y3Rk~1jr|RCi-zTmcJkiBvhz4BQ5NTR}L~`! zay{QfhI0(hyXKEPyX_t&iUcV9EzSEsh57yq813Jx>i_tL{|HL|rylFK^nY?X*hTj` zq52Z?4NOhbX(Q(*8Y0`Gj(A91{^fbxRG;Wi+Vp?N@BgVU%__V{xm@VbnsV+L(4I1e z#^P8@qMVZ%wrsRHo=zQ0lptZk9{+M&L}jqz}Mf=D>%(L zy92-W5xPME)Uau2Js{E_pJ79JrGpR&A~h75DnJ5w=9Hy$o0Q8y1%)vxfB9yfI&k|H zfF-LGJZVjL|0aM#{fM)+W0t<0F=ugcrIKiqnesgj$=+f!SGKw&i6S{uV$ z0@p!RG;5%y7gWs(sEwejjlyHX5BQ9Q@<|^SJ}{Qx=S-jz0kC0M^F-)pf&ojd(sqr3 zOgx)T8PY_>K50pJ2^P3kBQG*G{DI~|_@D)jIs!z6n`bJ6f3xPfD-Fr9T*l171n7%M zTm`;uO5os|FsoYHviF1nhzdyZ23<;2dj*=!@SEf?vkGrlteJS_QrnsBiZ&oNw`|rg z_Xfg$BSOs++iw}L+f5YdKmdxT|=rW-B?OjDcgtnTe3)&(ytpWeV zCRhKOpKJ*{_54plJ=~}obLp%1G!Q&ivFlK6_|m~oWY0?9<&3apAL|zLn0f`!X`;II zKVCYil|h0I}I^{L3o@587SyCP6#gp9|*RiJ71RRkMJ+R-lWTz6kIEW`Kh&l-j}y zD14F<^Z2(vomxln-|ud0hL2G4n6!y_j7|zD-V*=`1THd=6ql^h=RFkKTBo@1NmyFT zR7}(xt%8xawwOD4HNYPU8;I%^HvcCMmi$sz3*wqT z@R>R?4)|qW0f4gB&7Uy`&!NiFGk|9^IoEAinx&AuW(W9eli|REh{=n=F%ZN1a%yAf ze#P6V&yAK^ry|}UUD?QewrnZ~Ad-DVzS(aU7rw{HIE!S8`mI=@-CZrBxd5{ot^b05T_}kQdCaOj)ZXvgeLh zEPNJi{m~Cw8)(~fQ#3-s;6YM7mu8W|7An_sR8+-M)B>FchF#WTjg>a`_(E?kLVFCS z!ui+Q@iAjm5bmIe;L$P&52>f%e>~*L|G?}+jsGZzo-F7c$cSrZX69CvAz69k8tevO zxMEdKfJ2PD4{Kog0*lJ|*fV@ar-kZkD;_#L4;Ov;L-Hrz@;wf>bfF~JU+Qmf72gFl+xlo%?fE!yW2>3HYhG}-h)AT{ptE?KvRyp40?`# zIZ(WD`+ns8*Y(1>HT5f>3%<>hn0~4M(n(+3+^KyDIl9|3jHn>aHnKZLKxsDX<7B;{N0(OrbT}Wn}=?WqzGw{TW~7{gxh)_FL9jI z>M*em-Fozt=spy9SPSC%H@Sy@AhiD{g!UiBN&eoC^^Lx+ZaAb>F40(y>vMs4@Y$y4 z=j$yuJZp;*OO?=>w1DW#&0_)G!ENR0Tz^Z${ZC=Q|9M*`$Lr8Q>A2U=gCshO$;u4S+!TC$w$oskHC+cSt=g zxG?c}8-H=@Z3{@#5QhCBRDi54T9}}!5`}ByM}S%sD2fUrpm5y+#Z3%iEy9WoQ7lU~ z%@|ZPkVdLNPns$Gs;x~GAvdB%6YEsmE$H{N6<{cIwF-sKL~x*8zN53LXz7)el6@SXswn_lY+DG3Y*FDhr=V+v-@QT`mPpav(FtDtkF&Pe)2H z0dlc!awEa;=S?8+STh6_p~{@K%$;ohgNk_S+>ehGP|uKj1ky$MNaSUywtW8y`=*#8 zu!S}JR%h{Szs41$K1c63n}+#?Fj_u^3lDJwsvyZ?0-QdlCXN|#Mx!PC7(WflgWws5Gu$4pS5Gz3Bo^O+B z)l%p_+2aYblH(YF7?SOozE__-0y%>I8h{8WeHk+h_+=F2*v3-$TD}LAX+>c%SZzS9 zp`A!ER}a|>*j^3fprPq?J5-*8M*=R7gWd&6taGrdtn!frGe1;U#~S}p>&`}vtAUA% zCUW=ywMI+XYGHRD>`^xbt*G(M?D5R3+W?E~9;(y5<7KYvWsR!S)c_zbJb4ZhkCe97#_B#OC<&HPpzS@D#MuU$S#MCm;k#gFFgE-(1`%u zvow=Wq1LcnB_B6zj;CiR4|yv9lCvHyfqupY1CJ1yIGJWoZL4#~SRnb2)WlDH{`@)L zbwvRD!M))WD{l$Rx*+SRn$7lc=eTsKZ<^*{#K$Y^dZeVG%k^Qug8!F z1lITtP1UKWOGhpvk=&$Gaq>-aZ74sFXa%xIsy7P7>265MlecS(HX(&_Cj`=3=R>ty zAb|2jS1@Tz9z_2)EZ?c@C|yC25~C#}-uED*#p`55%>JCd`x zacNQEZ=xIG!8L5{rhuGTaQc0SW5E1ZhDCN)cctGdq42CTX0~E+JBBfb%p{Q?b-z~t zNlE}EC(oIy*HxxwkOgE>{QL7D0Csg#kZc$mZHleKX3g%{uW$+N zc(lo@(B(LjDt^V=!=Rh*_ZyU91GfmV{B&MO85k;5Hl&h0TD9UK{?3TL(+Pzo!v%iI z*NHM$av<^wd$hKMC`6@|&%x5Ei1rnVeSdx*$|egiX~_DxU-11gie2ne+BIplOJcwo zj{p2V7sP?brUiHJ6;%ZBxVXG#v$0hwhfJ$HbU2xv;iiw@l73sE%0yUK$Q z)mTvUl&hf_^u~n`54X6y8;LY|TdzqbQkA`<_Uxh5esG5DL`u0-E0o8hUh{mPsk4%V zHCs^A65hOd6KG_K?GD_6uP_RDqNlbg@cJoi+u$JUF^2I9a3e}3ic~_NLTg3P>vUA102DTe7kOn3p>m5t9-(lW44I<)QjY5}zgOQEqdboV z&*3KA7V?(i6zyra z0L-qAIF()5Q~WAp?$hy)FWk#lwBXLnu=Prs_pMV)$poPo0 z<1z_()jarJNj#>?PwH}>VIkD|`4quG1iP*zK%jZsIOO`l{CWQi( zEe9DY*Fv?#_OxkG!9YR8=Sxeo{m;!uA(8Qd^Uz4$Mo8Ln;Pn1>;P&s0kp3Y){>m}` zJs#_%fq{VyEMY_y+pO9VSR>XnBP+{*xsbACdw!zfSs8+yNO??Ja&G+{!qb0`FEui& zo^47{r9+KH)>>3Iay)?2-$GIA2^#oOuP?mFXRrW9f~zPSc9(G0(Ac-F7*u&kM4~7q zpY3b#lR+17eP{Xh>M3mSg|W{@9@31zAmZd!vD?t^eXOF}C3L7*vAuHhE1q8mf zxwOw~^4kPd2ak6;=RCf?gF`0n_F2faW*6vH&X0uLD&X(Zz)wIa#r?eCYhG|)L^1;B zEYu+o`sPt=jH}9$Icc1hi1SrfSN}=ufvoBR0MXDFltk828W^RHHG%MfLE27)QP(^? z67e9A18SkjhIbTGA5erVD&0d*e!<28?;&`g@*jzc7ICa!PS+MlPhB}N7=QcoF}_2o zk#~3=vLU{?Hyc^M`s`Mc1dS$mSBwK}`pLqf=a22-Hg1e0dLJ&u99SN;IAS?zVUeFdF z8#=%bjXT8UUcjTu?4uwr&Gs1Rf}KV%fW=o>1)8$Apwgr-WD1uxR0(PiGK)0TVL74m zbL*@ifvV$H80f$dCiv?kF(&}po+k)G(d!DG`+cFyQ9){sJYo7&R|(0IKmdjcC90To z3R^H&a&wJi`*T`!pHB2&2Rd#^@7`^{3}Ds7pAIXgzAhJFZ=QfLE$fzL9kf+>i01zI z_!#L*5R-Vd^drAPycaqWn1BKT6hh(8OHW+tHmL#3W7xE89`etcT$=8cAN7Il#k|A0 zZt%?~xb9Sh%ZG{pD4{SbC^2s#23RAx^I|1jzfydN$X}4Zf^K>fZlOt29c)ZX#vqUU zfVXwl_i5;xz#GXZDG4FRoWizXwc`7U_t>Y32HS9;1|grjwIRI>^Loub&J_!Yeg(Wa z!dX=ijnkZI{t|GhR6GFYhr^Vk1@aMU<8=-jCrB~n1~)o2+qYV zXLebDnu^v15GouVZ1O7fvrto(JjMT6cv41ci%WNKYOKSeMh5X=U-UTgiTUc&ot1b# z;`Z-w&2G{BCIv3+f(76RB-{Zx+%<+&&3(`p+jHob2ZUMM$&zLX*x_V)buI{dIFIRq%{yWiClfrHWnZMhK2Os zu0Zc!7QYwBvk~D>CSSV&>b;BEeg~QN!s-nzb3z~-cZE5Wz!gArf(eH(cs&E|wMB*W zAnVfmkn?n)c@O+7x)vnOTQ7(UmlsPQ z!vx9{gT0)KuY}WMn_@+61FZfL0E#()YW6}Dsiyby=g*KwSAoFDMgjLruydvL=AmZS z8b~OWh7T^Ny56t^&6_Myb4SRD1>|dscMxzrs<{s!N*|US7L+#%3rv+yn&;uXfXsN% z$Q2a{!XD=Ltr#;Ea1C?@zHZ{b`L-Im0~?;}&0k|GNh^0spat;GpeuqzN{VTLogpST zs2>^1net!*)obc5d4wrVz5!-r0oQA2KL-_(Mw`kN67{C`0@JX2NEzSaUB6b{rXlJD zIIIdF-(m^X?b(p*BOIp$->%*ySNtfr}_mmIHkbI!h3H*$3d6?^p)-<0yQhDEJX~qO&DhatI1e$V%l%c^gPTCkk+=Yhc3+a#`iwX6)#}9m%5st)QkZ6k#xR%> zl(d7`C)SI?>`Em&SQfL4N4+84xocWWI|8Skn6#-O6HZP;q?q)}n+hWhMqlr3J-TDm z`_RR|8-Du-@cwU*xzEN!oO>(8E!tKsMZF;`Re$t6C^Z?D3BQC{AH7vHlL+wHC}Kb? z2GI4tGlckG#?dueqHC3)$f8LrXi7~-F#s&-Ffs*3K;4=B+yGqaH4V0$DIgvDb-m97oHV9&nBr^5^Fk6*lnLiDek1)T4N02NWDJ=fhU=f@0%Yw{05BaR z?~)K6JgLP7oe>p!Lz2%@tA-bNwZhz}azuDTa2m}wqVyS9#HauYse+P*tmA>^5Kc^3 zFsNMiDT5pB*HGLDgjvgFN6-&C6vyJ80o>Z*jR59_5fn~%b>=Y|pz3;i%~jn6V&dxR zWWe$HlNp&EnZ`OZI))eK!S??OKty%J5{6A}L5?-Z&_V^&=v9~_NXLOGN`p&9k^r;- zjFbh$H$?+v+!J6t?3175pZ>@n1d5J|7Gz-wA~i8i-;{Djj=&HpgSu$_1csrmU5c9~ zH+OLv-xa#jlj`1ni3ZL;ia>*3PnJM)8^YX@@gcBWpbsxka`#HEX}qOCAT0m%5%~>_ zPk!NJLw~Ie39*r@l?d`1#MGQ0VQzhYZ_?2~;*w@SQeBO<255TavJ7g@Vq5mf-RuHY zi+99Y$B1_q4kieYpWh(kb*>itD&bE8-rw)G7KXeC*vi@33L3Om{s)7r@ae?oy@9Jp z5IG!sbN5B&uM!P`0P<6F5Ek~yIP6Sy=`Efn@JT~dHW$wnZJzmKPVzoc)K|t8NSzeG z;uo9b$>j^DW96Jc%-F-PJYA@d zZG))A-dyCxQ)<5mb>0Cdu-0?zoN>0rWozLJOtOMv)k{ z*yBk{;S^-7F`RG~btg!mV&_T1gkT_N-Ulv9z2NOM< z2RBj$ZRID4LfFNmJB&gKu)^zd5lymcOrM>>qIHsLW&e!91JW;A0>aq<&{rTt5e_Yy zG=(Ufp}6+^8Hj7-<2SNO+t4IQ>vxJna;Vf({WBB7-_eX`>kzR%pmFoy5*s`KSSBP7 zeal&sql9I?him2IhvOU#ne!A{=|L5cB2kPIyW7a-5gSM=`F9DDWfT|zJjH-1O@Fv< zA*<$PdtE+k1QW?wm*%00jQLr;z`dxuj#Rm3HcG3z%sqPqaozWv8*7RV>~*t%;2(8y z3`JY`&IPs^2PoWNNfQ{}-oTFsweX-c+orsE)K=m`!NJCx=_zu&x%#tv5!8Dh7+8^m zTs54c3ocLx$R$+I2sQjuqhGU@9#b9Dzy%S!1QG6^Qm?5(vNsHXxt*XqPeOSLYaVhA zjX!pLDXXZ%jL-JZmYY7rBRozO8K-7|!2}o4p*{t`ss5~~nVpKpx*-T%3Xe@ggAH>B zxk`#JQ`BuU)wZf)HdxtTnLw-rtZ_F8hf017Db_afE(P4k1u18MRkF=XD*X=~Xf^LF z!PE;6=IzbYUR;<4?|V^Il(AnF(yh9U%hrXf#xiY}rncgLcI81;FYq@`x`F?s71t|y zx1mHB(ZK2g_s?es3XU1EpXsOtv1XK}vh@8yv{?0=9S8sx(}1=j2{{R{L-vwh5I4AP z`=oi=Krz1$2ALO2Ku5kqkjjSPpIw}z^sD_2iG5HnmCPlneZFg^7lE!fsu+iDY{=5{%)IWSbSmsQ4@d?h-Bry7y}U)1xxO0n|+p z8Qhu`5iP+Ud-HC_bUBq+z=C8i4bkvSA4w^shyzXb+`n$Vh@f}LaUwZFGCU-yZupl6 zWk`dtm560UsH>XY9m!4v7QZn;aSzn5f)34>`m&>~6SzOCABM}^8YSzz4RFVT+d5f} zWsFg}0@d2c7@|iC(^1rQ3LcxK^a`PrL5Vyxx@!7ElHWC>PDMgAYkJsg4B0hgi5>f7 zG_580+MlZ(_RO>3iK0U6T}8BUE95XrVdVVk0CZ&F^s13J3O+0@p{Nu=>e87BBILzT z7LDAhw&CueK&;50G44Q~Y}}{7#ERh^gXd*IxV38`Igx(hR0T>q?ce%q+(KxYURqQ4 zIh11aJO>HRq6>HEfIl?yUApsAPeeU6xoa9eS@!@3q8)X5E#(L1L;!(>#Qp2PW)XDt zjNUHg8Sr=dRrcCsUF|QJJTH`dDsDON?Xy+iquinvG*pS7HZSaGy9@(}jT{g}{PDj9 zU}-9iWgb(bQ_B&r9#X!hZ`g2R1qFWV27`Gmk6)n8C`bn6GLSN@2Po!ItSYSlJV6A@{n|GQ}V==LC(6ZMDsm)8$a1YQv$ZUHmesgPn^1FsOI zGhdi;8CK3jInl;Z^hSt_2`W_h_CAoQ^G}P8LFwHGwkFhD9Dsg%ksH)Dofw%3s0T#F)#qwBL-=%8#2zpAs+49*7I%hfz$efj1hAXo+TzS7IU_pk@mF-wcGtp&JRa z(4g-zL3-M6j*4DgI~xFA6->#2vmYBC98=olT39UbCJe0 zCu$q0`<<G~TAnfv2tTw?Ny-wa6tOW5BBrxH%=7*^vEE^Q;|) zdy*WlbDZkPUM!L9B+BXFG@J|Pfo2*o?A+I+=G6`W0{<%IcY_x;E3~@&4=v6iey|3r zQPkUS{Uh zPHF@!e(MB#+@ywI)z@9*E|MI<=lJ|DnON6+jrUW)DIqp3_4$m<%rAu%rSyL3_zQo5 z$zxJTf$X>x#9=ulZp3j5mR#!N>V+rUAb+r*oDo1+LthQ`g9WpS8uyQ|ZRIwZfUH$; z`c<-H>}=49s1g_JbQBp8P`qtJU_rSE`W{936dwL&-y=xnLYqa76QnLmz)B|M@8Wj( z`nu_Vc{M27fPfmAJ`87Gu}l$O{ZlyQm!6r5+r}?P*auQva_Jac0L;)8Sq(uZsuRg! zqV(3Dpc3>p3;$`*<95wGZOylB$a?F}#i>S-2H5~6Vo%6fMa*{QT5kOv^kLnsN%zTb z$HE>!)>HYP)2}3WTS8Bn&!4>G96H7d1vj zS*mYubkTDZFaNna(}y}_h>kA7-B5lHM|zC1*9@cvhWdp5x_alaeIyY@k70+NsA?)N z0R-|{)4XtY=mLI{V$}i(g*BKhMI-7}zm)^A?1P6ZtPw_7L+=EVFwM6C&yL7MfuRbl z{usp2n%NgV0D#X_dl;lw0wo=&hZ~F=0E2Yy$U<}PLb*t4-i+ryvVx`tbm!mDK`(B5 z=Vt$vS%@z1TyWR`dXEc4lqq3lVUNjrLt}% z8(}B4K^w?$(SZX4J*9yqaIAGygS%V-0mFV-2|M-4!@rpEaea;tIDU|^xq4) z&h))&ZFd6o1-t2XM^yGvX0Vz_8ZhH_)zj9`?-AT((Qx~IZs&#*@({x2ND=9nk$U;N z&*tO*Kze^4Rq?+C-u^432k#7Ev$a0=nt)2W0TIP@97HSDpHwroV)zNiId$!`Ij67Q F`yYm*iYP^lAP@*8e4-#Aq9UM3QF;#u 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 literal 0 HcmV?d00001 diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/month/longAppointments.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/month/longAppointments.spec.ts new file mode 100644 index 000000000000..e212e04b880b --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/month/longAppointments.spec.ts @@ -0,0 +1,121 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('Scheduler: long appointments in month view', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + const appointments = [ + { + text: 'Appointment spans 3 rows', + startDate: new Date(2020, 0, 6), + endDate: new Date(2020, 0, 24), + }, + { + text: 'Appointment spans all rows', + startDate: new Date(2019, 11, 29), + endDate: new Date(2020, 1, 8, 15), + }, + { + text: 'Appointment spans 2 rows', + startDate: new Date(2020, 0, 17), + endDate: new Date(2020, 0, 20), + }, + ]; + + for (const rtlEnabled of [false, true]) { + for (const appointment of appointments) { + test(`Long appointment (rtl=${rtlEnabled}, text=${appointment.text})`, async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource: [appointment], + views: ['month'], + currentView: 'month', + rtlEnabled, + currentDate: new Date(2020, 0, 1), + }); + + await testScreenshot(page, + `month-long-appointment(rtl=${rtlEnabled}, text=${appointment.text}).png`, + { element: page.locator('.dx-scheduler-work-space') }, + ); + }); + } + } + + for (const rtlEnabled of [false, true]) { + test(`Long appointment several months (rtl=${rtlEnabled})`, async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource: [{ + text: 'Text', + startDate: new Date(2020, 0, 6), + endDate: new Date(2020, 2, 10), + }], + views: ['month'], + currentView: 'month', + rtlEnabled, + currentDate: new Date(2020, 0, 1), + }); + + const workSpace = page.locator('.dx-scheduler-work-space'); + + await testScreenshot(page, + `month-long-appointment-several-months-january(rtl=${rtlEnabled}).png`, + { element: workSpace }, + ); + + await page.locator('.dx-scheduler-navigator-next').click(); + await page.waitForTimeout(300); + + await testScreenshot(page, + `month-long-appointment-several-months-february(rtl=${rtlEnabled}).png`, + { element: workSpace }, + ); + + await page.locator('.dx-scheduler-navigator-next').click(); + await page.waitForTimeout(300); + + await testScreenshot(page, + `month-long-appointment-several-months-march(rtl=${rtlEnabled}).png`, + { element: workSpace }, + ); + }); + } + + test('Long recurrence appointment several months', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource: [{ + text: 'Text', + startDate: new Date(2020, 0, 6), + endDate: new Date(2020, 0, 10), + recurrenceRule: 'FREQ=DAILY;INTERVAL=5', + }], + views: ['month'], + currentView: 'month', + currentDate: new Date(2020, 0, 1), + }); + + const workSpace = page.locator('.dx-scheduler-work-space'); + + await testScreenshot(page, + 'month-long-recurrence-appointment-several-months-january.png', + { element: workSpace }, + ); + + await page.locator('.dx-scheduler-navigator-next').click(); + await page.waitForTimeout(300); + + await testScreenshot(page, + 'month-long-recurrence-appointment-several-months-february.png', + { element: workSpace }, + ); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/month/outOfDayHours.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/month/outOfDayHours.spec.ts new file mode 100644 index 000000000000..79815af3fef2 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/month/outOfDayHours.spec.ts @@ -0,0 +1,50 @@ +import { test, expect } from '@playwright/test'; +import { createWidget } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('Scheduler: take into account start and end day hour', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('Should show appointment in month view', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource: [{ + startDate: '2024-01-01T11:00:00', + endDate: '2024-01-01T12:00:00', + text: 'test', + }], + startDayHour: 11, + endDayHour: 22, + currentDate: '2024-01-01', + views: ['month', 'timelineMonth'], + currentView: 'month', + }); + + await expect(page.locator('.dx-scheduler-appointment').filter({ hasText: 'test' })).toBeVisible(); + }); + + test('Should not show appointment in month view', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource: [{ + startDate: '2024-01-01T11:00:00', + endDate: '2024-01-01T12:00:00', + text: 'test', + }], + startDayHour: 13, + endDayHour: 22, + currentDate: '2024-01-01', + views: ['month', 'timelineMonth'], + currentView: 'month', + }); + + await expect(page.locator('.dx-scheduler-appointment').filter({ hasText: 'test' })).toHaveCount(0); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/month/regression-detection.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/month/regression-detection.spec.ts new file mode 100644 index 000000000000..7f1cd3bc0b61 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/month/regression-detection.spec.ts @@ -0,0 +1,49 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('Regression detection: verify Playwright catches visual changes', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('Month view with intervalCount=1 should match baseline', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + views: [{ + type: 'month', + intervalCount: 1, + }], + currentView: 'month', + firstDayOfWeek: 1, + currentDate: new Date(2021, 1, 1), + }); + + await testScreenshot(page, 'regression-month-february-2021.png', { + element: page.locator('.dx-scheduler-work-space'), + }); + }); + + test('Month view with appointments should match baseline', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource: [{ + text: 'Test Appointment', + startDate: new Date(2020, 0, 6), + endDate: new Date(2020, 0, 10), + }], + views: ['month'], + currentView: 'month', + currentDate: new Date(2020, 0, 1), + }); + + await testScreenshot(page, 'regression-month-with-appointment.png', { + element: page.locator('.dx-scheduler-work-space'), + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/navigator.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/navigator.spec.ts new file mode 100644 index 000000000000..de00da595837 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/navigator.spec.ts @@ -0,0 +1,92 @@ +import { test, expect } from '@playwright/test'; +import { createWidget } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('Scheduler: Navigator', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + +); + +const createScheduler = async (options = {}): Promise => { + await createWidget(page, 'dxScheduler', extend(options, { + dataSource: [], + currentDate: new Date(2017, 4, 18), + firstDayOfWeek: 1, + height: 600, + views: ['week', 'month'], + })); +}; + +test('Navigator can change week when current date interval is more than diff between current date and `max` (T830754)', async ({ page }) => { + // Scheduler on '#container' (destructured: toolbar ) + + // Navigation `next` must be enabled at default + + expect(page.locator('.dx-scheduler-navigator-next').hasClass('dx-state-disabled')).toBeFalsy(); + + // Navigation `next` must be disabled after change 1 week earlier + + await (page.locator('.dx-scheduler-navigator-next')).click() + .expect(page.locator('.dx-scheduler-navigator-next').hasClass('dx-state-disabled')).toBeTruthy(); +}).before(async () => createScheduler({ + max: new Date(2017, 4, 24), + currentView: 'week', +})); + +test('Navigator can change week when current date interval is more than diff between current date and `min` (T830754)', async ({ page }) => { + // Scheduler on '#container' (destructured: toolbar ) + + // Navigation `prev` must be enabled at default + + expect(page.locator('.dx-scheduler-navigator-previous').hasClass('dx-state-disabled')).toBeFalsy(); + + // Navigation `prev` must be disabled after change 1 week later + + await (page.locator('.dx-scheduler-navigator-previous')).click() + .expect(page.locator('.dx-scheduler-navigator-previous').hasClass('dx-state-disabled')).toBeTruthy(); +}).before(async () => createScheduler({ + min: new Date(2017, 4, 13), + currentView: 'week', +})); + +test('Navigator can change month when current date interval is more than diff between current date and `max` (T830754)', async ({ page }) => { + // Scheduler on '#container' (destructured: toolbar ) + + // Navigation `next` must be enabled at default + + expect(page.locator('.dx-scheduler-navigator-next').hasClass('dx-state-disabled')).toBeFalsy(); + + // Navigation `next` must be disabled after change 1 week earlier + + await (page.locator('.dx-scheduler-navigator-next')).click() + .expect(page.locator('.dx-scheduler-navigator-next').hasClass('dx-state-disabled')).toBeTruthy(); +}).before(async () => createScheduler({ + max: new Date(2017, 5, 15), + currentView: 'month', +})); + +test('Navigator can change month when current date interval is more than diff between current date and `min` (T830754)', async ({ page }) => { + // Scheduler on '#container' (destructured: toolbar ) + + // Navigation `prev` must be enabled at default + + expect(page.locator('.dx-scheduler-navigator-previous').hasClass('dx-state-disabled')).toBeFalsy(); + + // Navigation `prev` must be disabled after change 1 week later + + await (page.locator('.dx-scheduler-navigator-previous')).click() + .expect(page.locator('.dx-scheduler-navigator-previous').hasClass('dx-state-disabled')).toBeTruthy(); +}).before(async () => createScheduler({ + min: new Date(2017, 3, 28), + currentView: 'month', +})); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/recurrences/appointmentTooltip.timeZone.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/recurrences/appointmentTooltip.timeZone.spec.ts new file mode 100644 index 000000000000..b2f50683aaf2 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/recurrences/appointmentTooltip.timeZone.spec.ts @@ -0,0 +1,63 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, getContainerUrl, setupTestPage } from '../../../../playwright-helpers'; + +const containerUrl = getContainerUrl(__dirname, '../../../../tests/container.html'); + +test.describe('Appointment tooltip with recurrence appointment and custom time zone', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + test('Time in appointment tooltip should has valid value (T848058)', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource: [{ + text: 'Stand-up meeting', + startDate: '2017-05-22T15:30:00.000Z', + endDate: '2017-05-22T15:45:00.000Z', + recurrenceRule: 'FREQ=DAILY', + startDateTimeZone: 'America/Los_Angeles', + endDateTimeZone: 'America/Los_Angeles', + }], + views: ['week'], + currentView: 'week', + currentDate: new Date(2017, 4, 25), + startDayHour: 8, + timeZone: 'America/Los_Angeles', + height: 600, + }); + + const appointments = page.locator('.dx-scheduler-appointment').filter({ hasText: 'Stand-up meeting' }); + const appointmentCount = await appointments.count(); + + for (let i = 0; i < appointmentCount; i += 1) { + await appointments.nth(i).click(); + const tooltipDate = await page.locator('.dx-tooltip-appointment-item-content-date').first().textContent(); + expect(tooltipDate).toBe('8:30 AM - 8:45 AM'); + } + }); + + test('The only one displayed part of recurrence appointment must have correct offset after DST(T1034216)', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + timeZone: 'Europe/Moscow', + startDateTimeZoneExpr: 'TimeZone', + endDateTimeZoneExpr: 'TimeZone', + views: ['month', 'week'], + currentView: 'month', + currentDate: '2021-12-01', + dataSource: [{ + text: 'apt', + startDate: '2021-09-01T01:00:00-07:00', + endDate: '2021-09-01T02:00:00-07:00', + recurrenceException: '', + recurrenceRule: 'FREQ=MONTHLY;BYDAY=WE,FR;BYSETPOS=1;UNTIL=20211231T235959Z', + TimeZone: 'America/Los_Angeles', + }], + height: 600, + }); + + const appointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'apt' }); + await appointment.click(); + const tooltipDate = await page.locator('.dx-tooltip-appointment-item-content-date').first().textContent(); + expect(tooltipDate).toBe('December 2 12:00 PM - 1:00 PM'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/recurrences/basic.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/recurrences/basic.spec.ts new file mode 100644 index 000000000000..245db0d2d347 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/recurrences/basic.spec.ts @@ -0,0 +1,129 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, getContainerUrl, setupTestPage } from '../../../../playwright-helpers'; + +const containerUrl = getContainerUrl(__dirname, '../../../../tests/container.html'); + +test.describe('Rendering of the recurrence appointments in Scheduler', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + test('Drag-n-drop recurrence appointment between dateTable and allDay panel', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource: [{ + text: 'Simple recurrence appointment', + startDate: new Date(2019, 3, 1, 10, 0), + endDate: new Date(2019, 3, 1, 11, 0), + recurrenceRule: 'FREQ=DAILY;COUNT=7', + }], + startDayHour: 1, + recurrenceEditMode: 'series', + views: ['week'], + currentView: 'week', + currentDate: new Date(2019, 3, 1), + height: 600, + width: 800, + }); + + await testScreenshot(page, 'basic-recurrence-appointment-init.png'); + + const appointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'Simple recurrence appointment' }).first(); + const allDayCell = page.locator('.dx-scheduler-all-day-table-cell').nth(0); + + await appointment.dragTo(allDayCell); + await page.waitForTimeout(300); + + const appointmentCount = await page.locator('.dx-scheduler-appointment').count(); + expect(appointmentCount).toBe(7); + + await page.waitForTimeout(500); + await testScreenshot(page, 'basic-recurrence-appointment-after-drag.png'); + }); + + test('Appointments in DST should not have offset when recurring appointment timezone not equal to scheduler timezone', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + timeZone: 'America/New_York', + dataSource: [{ + text: 'Recurrence', + startDate: new Date('2021-03-13T19:00:00.000Z'), + endDate: new Date('2021-03-13T19:30:00.000Z'), + recurrenceRule: 'FREQ=DAILY;COUNT=1000', + startDateTimeZone: 'America/New_York', + endDateTimeZone: 'America/New_York', + }], + views: ['week'], + currentView: 'week', + currentDate: new Date(2021, 2, 13), + firstDayOfWeek: 1, + height: 600, + width: 800, + }); + + const appt0 = page.locator('.dx-scheduler-appointment').nth(0); + const time0 = await appt0.locator('.dx-scheduler-appointment-content-date').textContent(); + expect(time0).toContain('2:00 PM - 2:30 PM'); + + const appt1 = page.locator('.dx-scheduler-appointment').nth(1); + const time1 = await appt1.locator('.dx-scheduler-appointment-content-date').textContent(); + expect(time1).toContain('2:00 PM - 2:30 PM'); + + await page.evaluate(() => { + ($('#container') as any).dxScheduler('instance').option('currentDate', new Date(2021, 10, 1)); + }); + + const appt0b = page.locator('.dx-scheduler-appointment').nth(0); + const time0b = await appt0b.locator('.dx-scheduler-appointment-content-date').textContent(); + expect(time0b).toContain('2:00 PM - 2:30 PM'); + + const appt1b = page.locator('.dx-scheduler-appointment').nth(1); + const time1b = await appt1b.locator('.dx-scheduler-appointment-content-date').textContent(); + expect(time1b).toContain('2:00 PM - 2:30 PM'); + }); + + test('Appointments in end of DST should have correct offset', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + timeZone: 'America/Phoenix', + dataSource: [{ + text: 'Recurrence', + startDate: new Date('2021-03-13T19:00:00.000Z'), + endDate: new Date('2021-03-13T19:30:00.000Z'), + recurrenceRule: 'FREQ=DAILY;COUNT=1000', + startDateTimeZone: 'America/New_York', + endDateTimeZone: 'America/New_York', + }], + views: ['week'], + currentView: 'week', + currentDate: new Date(2021, 10, 1), + firstDayOfWeek: 1, + height: 600, + width: 800, + }); + + const appt5 = page.locator('.dx-scheduler-appointment').nth(5); + const time5 = await appt5.locator('.dx-scheduler-appointment-content-date').textContent(); + expect(time5).toContain('11:00 AM - 11:30 AM'); + + const appt6 = page.locator('.dx-scheduler-appointment').nth(6); + const time6 = await appt6.locator('.dx-scheduler-appointment-content-date').textContent(); + expect(time6).toContain('12:00 PM - 12:30 PM'); + }); + + test('Appointment displayed without errors if it was only one DST in year(T1037853)', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + timeZone: 'America/Los_Angeles', + dataSource: [{ + text: 'Recurrence', + startDate: new Date(1942, 3, 29, 0), + endDate: new Date(1942, 3, 29, 1), + recurrenceRule: 'FREQ=DAILY;COUNT=2', + }], + views: ['day'], + currentView: 'day', + currentDate: new Date(1942, 3, 29), + height: 600, + }); + + const appt = page.locator('.dx-scheduler-appointment').nth(0); + await expect(appt).toBeVisible(); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/recurrences/dialog.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/recurrences/dialog.spec.ts new file mode 100644 index 000000000000..97947fd3e75d --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/recurrences/dialog.spec.ts @@ -0,0 +1,72 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, getContainerUrl, setupTestPage } from '../../../../playwright-helpers'; + +const containerUrl = getContainerUrl(__dirname, '../../../../tests/container.html'); + +const INITIAL_APPOINTMENT_TITLE = 'appointment'; + +test.describe('Recurrence dialog', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + test('Recurrence edit dialog screenshot', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource: [{ + id: 1, + text: INITIAL_APPOINTMENT_TITLE, + startDate: new Date(2021, 2, 29, 9, 30), + endDate: new Date(2021, 2, 29, 11, 30), + recurrenceRule: 'FREQ=WEEKLY;BYDAY=MO,TH;COUNT=10', + }], + views: ['day'], + currentView: 'day', + currentDate: new Date(2021, 2, 29), + startDayHour: 9, + endDayHour: 14, + height: 600, + }); + + const appointment = page.locator('.dx-scheduler-appointment').filter({ hasText: INITIAL_APPOINTMENT_TITLE }); + await appointment.dblclick(); + + const dialog = page.locator('.dx-dialog'); + await expect(dialog).toBeVisible(); + + const scheduler = page.locator('.dx-scheduler'); + await testScreenshot(page, 'recurrence-edit-dialog-screenshot.png', { element: scheduler }); + }); + + test('Recurrence delete dialog screenshot', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource: [{ + id: 1, + text: INITIAL_APPOINTMENT_TITLE, + startDate: new Date(2021, 2, 29, 9, 30), + endDate: new Date(2021, 2, 29, 11, 30), + recurrenceRule: 'FREQ=WEEKLY;BYDAY=MO,TH;COUNT=10', + }], + views: ['day'], + currentView: 'day', + currentDate: new Date(2021, 2, 29), + startDayHour: 9, + endDayHour: 14, + height: 600, + }); + + const appointment = page.locator('.dx-scheduler-appointment').filter({ hasText: INITIAL_APPOINTMENT_TITLE }); + await appointment.click(); + + const tooltip = page.locator('.dx-scheduler-appointment-tooltip'); + await expect(tooltip).toBeVisible(); + + const deleteButton = page.locator('.dx-tooltip-appointment-item-delete-button').first(); + await deleteButton.click(); + + const dialog = page.locator('.dx-dialog'); + await expect(dialog).toBeVisible(); + + const scheduler = page.locator('.dx-scheduler'); + await testScreenshot(page, 'recurrence-delete-dialog-screenshot.png', { element: scheduler }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/rerenderOnResize.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/rerenderOnResize.spec.ts new file mode 100644 index 000000000000..adc044ebcf33 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/rerenderOnResize.spec.ts @@ -0,0 +1,96 @@ +import { test, expect } from '@playwright/test'; +import { createWidget } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +const createScheduler = async (page, container: string, options?: Record): Promise => { + await createWidget(page, 'dxScheduler', { + currentDate: new Date(2020, 8, 7), + startDayHour: 8, + endDayHour: 20, + cellDuration: 60, + scrolling: { + mode: 'virtual', + }, + currentView: 'Timeline', + views: [{ + type: 'timelineWorkWeek', + name: 'Timeline', + groupOrientation: 'vertical', + }], + dataSource: [{ + startDate: new Date(2020, 8, 7, 8), + endDate: new Date(2020, 8, 7, 9), + text: 'test', + }], + ...options, + }, container); +}; + +test.describe('Re-render on resize', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + test('Appointment should re-rendered on window resize-up (T1139566)', async ({ page }) => { + await page.setViewportSize({ width: 800, height: 400 }); + + await createScheduler(page, '#container', { currentView: 'workWeek' }); + + const appointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'test' }); + await appointment.evaluate((el) => { + (el as HTMLElement).style.backgroundColor = 'red'; + }); + + const styleAttr = await appointment.evaluate((el) => (el as HTMLElement).style.cssText); + expect(styleAttr).toMatch(/transform: translate\(0px, 0px\); width: \d+\.\d+px; height: \d+px; background-color: red;/); + }); + + test('Appointment should not re-rendered on window resize when width and height not set (T1139566)', async ({ page }) => { + await page.setViewportSize({ width: 300, height: 300 }); + + await createScheduler(page, '#container'); + + const appointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'test' }); + await appointment.evaluate((el) => { + (el as HTMLElement).style.backgroundColor = 'red'; + }); + + const styleAttr = await appointment.evaluate((el) => (el as HTMLElement).style.cssText); + expect(styleAttr).toBe('transform: translate(0px, 30px); width: 200px; height: 70px; background-color: red;'); + }); + + test('Appointment should not re-rendered on window resize when width and height have percent value (T1139566)', async ({ page }) => { + await page.setViewportSize({ width: 300, height: 400 }); + + await createScheduler(page, '#container', { width: '100%', height: '100%' }); + + const appointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'test' }); + await appointment.evaluate((el) => { + (el as HTMLElement).style.backgroundColor = 'red'; + }); + + const styleAttr = await appointment.evaluate((el) => (el as HTMLElement).style.cssText); + expect(styleAttr).toBe('transform: translate(0px, 30px); width: 200px; height: 70px; background-color: red;'); + }); + + test('Appointment should not re-rendered on window resize when width and height have static value (T1139566)', async ({ page }) => { + await page.setViewportSize({ width: 300, height: 300 }); + + await createScheduler(page, '#container', { width: 600, height: 400 }); + + const appointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'test' }); + await appointment.evaluate((el) => { + (el as HTMLElement).style.backgroundColor = 'red'; + }); + + const styleAttr = await appointment.evaluate((el) => (el as HTMLElement).style.cssText); + expect(styleAttr).toBe('transform: translate(0px, 30px); width: 200px; height: 61.7539px; background-color: red;'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/resizeAppointments/T1255474.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/resizeAppointments/T1255474.spec.ts new file mode 100644 index 000000000000..782c58eba4bf --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/resizeAppointments/T1255474.spec.ts @@ -0,0 +1,48 @@ +import { test } from '@playwright/test'; +import { createWidget, testScreenshot, getContainerUrl, setupTestPage } from '../../../../playwright-helpers'; + +const containerUrl = getContainerUrl(__dirname, '../../../../tests/container.html'); + +const appointmentText = 'Book Flights to San Fran for Sales Trip'; + +test.describe('Resize appointment that cross DTC time', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + test('Resize appointment that cross DTC time', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + timeZone: 'America/Los_Angeles', + views: ['week'], + currentView: 'week', + currentDate: new Date(2021, 2, 28), + allDayPanelMode: 'allDay', + height: 600, + width: 800, + firstDayOfWeek: 7, + dataSource: [{ + text: appointmentText, + startDate: new Date('2021-03-28T17:00:00.000Z'), + endDate: new Date('2021-03-28T18:00:00.000Z'), + TimeZone: 'Europe/Belgrade', + allDay: true, + }], + }); + + const appointment = page.locator('.dx-scheduler-appointment').filter({ hasText: appointmentText }); + const rightHandle = appointment.locator('.dx-resizable-handle-right'); + + await rightHandle.hover(); + await page.mouse.down(); + await page.mouse.move(100, 0, { steps: 5 }); + await page.mouse.up(); + + await rightHandle.hover(); + await page.mouse.down(); + await page.mouse.move(-100, 0, { steps: 5 }); + await page.mouse.up(); + + const scheduler = page.locator('.dx-scheduler'); + await testScreenshot(page, 'T1255474-resize-all-day-appointment.png', { element: scheduler }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/resizeAppointments/T1294528.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/resizeAppointments/T1294528.spec.ts new file mode 100644 index 000000000000..3bc5a21ffc89 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/resizeAppointments/T1294528.spec.ts @@ -0,0 +1,174 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, getContainerUrl, setupTestPage } from '../../../../playwright-helpers'; + +const containerUrl = getContainerUrl(__dirname, '../../../../tests/container.html'); + +test.describe('Resize all day panel appointments', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + [true, false].forEach((rtlEnabled) => { + test(`Resize all day appointment rtlEnabled=${rtlEnabled}`, async ({ page }) => { + await createWidget(page, 'dxScheduler', { + currentDate: new Date(2015, 1, 9), + currentView: 'week', + firstDayOfWeek: 0, + rtlEnabled, + height: 400, + dataSource: [{ + text: 'Appointment', + startDate: new Date(2015, 1, 9, 8), + endDate: new Date(2015, 1, 9, 10), + allDay: true, + }], + width: 800, + }); + + const appointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'Appointment' }); + const { right, left } = { + right: appointment.locator('.dx-resizable-handle-right'), + left: appointment.locator('.dx-resizable-handle-left'), + }; + const text = 'Appointment: February 9, 2015, All day'; + const startDateExtendedText = 'Appointment: February 8, 2015 - February 9, 2015, All day'; + const endDateExtendedText = 'Appointment: February 9, 2015 - February 10, 2015, All day'; + + await right.hover(); + await page.mouse.down(); + await page.mouse.move(100, 0, { steps: 5 }); + await page.mouse.up(); + + const ariaLabel1 = await appointment.getAttribute('aria-label'); + expect(ariaLabel1).toBe(rtlEnabled ? startDateExtendedText : endDateExtendedText); + + await right.hover(); + await page.mouse.down(); + await page.mouse.move(-100, 0, { steps: 5 }); + await page.mouse.up(); + + const ariaLabel2 = await appointment.getAttribute('aria-label'); + expect(ariaLabel2).toBe(text); + + await left.hover(); + await page.mouse.down(); + await page.mouse.move(-100, 0, { steps: 5 }); + await page.mouse.up(); + + const ariaLabel3 = await appointment.getAttribute('aria-label'); + expect(ariaLabel3).toBe(rtlEnabled ? endDateExtendedText : startDateExtendedText); + + await left.hover(); + await page.mouse.down(); + await page.mouse.move(100, 0, { steps: 5 }); + await page.mouse.up(); + + const ariaLabel4 = await appointment.getAttribute('aria-label'); + expect(ariaLabel4).toBe(text); + }); + }); + + test('Resize long appointment rtlEnabled=true', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + currentDate: new Date(2015, 1, 9), + currentView: 'week', + firstDayOfWeek: 0, + rtlEnabled: true, + height: 400, + dataSource: [{ + text: 'Appointment', + startDate: new Date(2015, 1, 9, 8), + endDate: new Date(2015, 1, 10, 10), + }], + width: 800, + }); + + const appointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'Appointment' }); + const right = appointment.locator('.dx-resizable-handle-right'); + const left = appointment.locator('.dx-resizable-handle-left'); + + await right.hover(); + await page.mouse.down(); + await page.mouse.move(100, 0, { steps: 5 }); + await page.mouse.up(); + + const ariaLabel1 = await appointment.getAttribute('aria-label'); + expect(ariaLabel1).toBe('Appointment: February 8, 2015, 12:00 AM - February 10, 2015, 10:00 AM'); + + await right.hover(); + await page.mouse.down(); + await page.mouse.move(-100, 0, { steps: 5 }); + await page.mouse.up(); + + const ariaLabel2 = await appointment.getAttribute('aria-label'); + expect(ariaLabel2).toBe('Appointment: February 9, 2015, 12:00 AM - February 10, 2015, 10:00 AM'); + + await left.hover(); + await page.mouse.down(); + await page.mouse.move(-100, 0, { steps: 5 }); + await page.mouse.up(); + + const ariaLabel3 = await appointment.getAttribute('aria-label'); + expect(ariaLabel3).toBe('Appointment: February 9, 2015, 12:00 AM - February 12, 2015, 12:00 AM'); + + await left.hover(); + await page.mouse.down(); + await page.mouse.move(100, 0, { steps: 5 }); + await page.mouse.up(); + + const ariaLabel4 = await appointment.getAttribute('aria-label'); + expect(ariaLabel4).toBe('Appointment: February 9, 2015, 12:00 AM - February 11, 2015, 12:00 AM'); + }); + + test('Resize long appointment rtlEnabled=false', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + currentDate: new Date(2015, 1, 9), + currentView: 'week', + firstDayOfWeek: 0, + rtlEnabled: false, + height: 400, + dataSource: [{ + text: 'Appointment', + startDate: new Date(2015, 1, 9, 8), + endDate: new Date(2015, 1, 10, 10), + }], + width: 800, + }); + + const appointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'Appointment' }); + const right = appointment.locator('.dx-resizable-handle-right'); + const left = appointment.locator('.dx-resizable-handle-left'); + + await right.hover(); + await page.mouse.down(); + await page.mouse.move(100, 0, { steps: 5 }); + await page.mouse.up(); + + const ariaLabel1 = await appointment.getAttribute('aria-label'); + expect(ariaLabel1).toBe('Appointment: February 9, 2015, 8:00 AM - February 12, 2015, 12:00 AM'); + + await right.hover(); + await page.mouse.down(); + await page.mouse.move(-100, 0, { steps: 5 }); + await page.mouse.up(); + + const ariaLabel2 = await appointment.getAttribute('aria-label'); + expect(ariaLabel2).toBe('Appointment: February 9, 2015, 8:00 AM - February 11, 2015, 12:00 AM'); + + await left.hover(); + await page.mouse.down(); + await page.mouse.move(-100, 0, { steps: 5 }); + await page.mouse.up(); + + const ariaLabel3 = await appointment.getAttribute('aria-label'); + expect(ariaLabel3).toBe('Appointment: February 8, 2015, 12:00 AM - February 11, 2015, 12:00 AM'); + + await left.hover(); + await page.mouse.down(); + await page.mouse.move(100, 0, { steps: 5 }); + await page.mouse.up(); + + const ariaLabel4 = await appointment.getAttribute('aria-label'); + expect(ariaLabel4).toBe('Appointment: February 9, 2015, 12:00 AM - February 11, 2015, 12:00 AM'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/resizeAppointments/allDay.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/resizeAppointments/allDay.spec.ts new file mode 100644 index 000000000000..e3913db30d3a --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/resizeAppointments/allDay.spec.ts @@ -0,0 +1,48 @@ +import { test } from '@playwright/test'; +import { createWidget, testScreenshot, getContainerUrl, setupTestPage } from '../../../../playwright-helpers'; + +const containerUrl = getContainerUrl(__dirname, '../../../../tests/container.html'); + +test.describe('Resize appointments in All Day Panel', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + test('Resize in the workWeek view between weeks', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + width: 800, + height: 600, + views: [{ + type: 'workWeek', + intervalCount: 2, + startDate: new Date(2021, 5, 29), + }], + currentDate: new Date(2021, 5, 29), + currentView: 'workWeek', + maxAppointmentsPerCell: 'unlimited', + startDayHour: 9, + endDayHour: 13, + dataSource: [ + { text: '1st', startDate: new Date(2021, 5, 29), allDay: true }, + { text: '2nd', startDate: new Date(2021, 6, 7), allDay: true }, + { text: '3rd', startDate: new Date(2021, 6, 1), endDate: new Date(2021, 6, 5), allDay: true }, + ], + }); + + const appointment1 = page.locator('.dx-scheduler-appointment').filter({ hasText: '1st' }); + const appointment2 = page.locator('.dx-scheduler-appointment').filter({ hasText: '2nd' }); + const appointment3 = page.locator('.dx-scheduler-appointment').filter({ hasText: '3rd' }); + + await appointment1.locator('.dx-resizable-handle-right').dragTo(appointment1.locator('.dx-resizable-handle-right'), { targetPosition: { x: 400, y: 0 } }); + await appointment2.locator('.dx-resizable-handle-left').dragTo(appointment2.locator('.dx-resizable-handle-left'), { targetPosition: { x: -400, y: 0 } }); + await appointment3.locator('.dx-resizable-handle-right').dragTo(appointment3.locator('.dx-resizable-handle-right'), { targetPosition: { x: -140, y: 0 } }); + + await testScreenshot(page, 'resize-all-day-workweek-weekend-0.png'); + + await appointment1.locator('.dx-resizable-handle-right').dragTo(appointment1.locator('.dx-resizable-handle-right'), { targetPosition: { x: -400, y: 0 } }); + await appointment2.locator('.dx-resizable-handle-left').dragTo(appointment2.locator('.dx-resizable-handle-left'), { targetPosition: { x: 400, y: 0 } }); + await appointment3.locator('.dx-resizable-handle-right').dragTo(appointment3.locator('.dx-resizable-handle-right'), { targetPosition: { x: 140, y: 0 } }); + + await testScreenshot(page, 'resize-all-day-workweek-weekend-1.png'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/resizeAppointments/basic.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/resizeAppointments/basic.spec.ts new file mode 100644 index 000000000000..576e07d27f45 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/resizeAppointments/basic.spec.ts @@ -0,0 +1,109 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, getContainerUrl, setupTestPage } from '../../../../playwright-helpers'; + +const containerUrl = getContainerUrl(__dirname, '../../../../tests/container.html'); + +const dataSource = [ + { + text: 'Brochure Design Review', + startDate: new Date(2019, 3, 1, 10, 0), + endDate: new Date(2019, 3, 1, 11, 0), + resourceId: 0, + }, +]; + +test.describe('Resize appointments in the Scheduler basic views', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + ['day', 'week', 'workWeek'].forEach((view) => { + test(`Resize in the "${view}" view`, async ({ page }) => { + await createWidget(page, 'dxScheduler', { + views: [view], + currentView: view, + dataSource, + width: 800, + height: 600, + startDayHour: 9, + currentDate: new Date(2019, 3, 1), + }); + + const appointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'Brochure Design Review' }); + const bottomHandle = appointment.locator('.dx-resizable-handle-bottom'); + + await bottomHandle.hover(); + await page.mouse.down(); + await page.mouse.move(0, 100, { steps: 5 }); + await page.mouse.up(); + + const height1 = await appointment.evaluate((el) => getComputedStyle(el).height); + expect(height1).toBe('190px'); + + const timeText1 = await appointment.locator('.dx-scheduler-appointment-content-date').textContent(); + expect(timeText1).toContain('10:00 AM - 12:30 PM'); + + const topHandle = appointment.locator('.dx-resizable-handle-top'); + await topHandle.hover(); + await page.mouse.down(); + await page.mouse.move(0, 100, { steps: 5 }); + await page.mouse.up(); + + const height2 = await appointment.evaluate((el) => getComputedStyle(el).height); + expect(height2).toBe('76px'); + + const timeText2 = await appointment.locator('.dx-scheduler-appointment-content-date').textContent(); + expect(timeText2).toContain('11:30 AM - 12:30 PM'); + }); + }); + + test('Resize in the "month" view', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + views: ['month'], + currentView: 'month', + dataSource, + width: 800, + height: 600, + startDayHour: 9, + currentDate: new Date(2019, 3, 1), + }); + + const appointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'Brochure Design Review' }); + const rightHandle = appointment.locator('.dx-resizable-handle-right'); + + await rightHandle.hover(); + await page.mouse.down(); + await page.mouse.move(100, 0, { steps: 5 }); + await page.mouse.up(); + + const width = await appointment.evaluate((el) => getComputedStyle(el).width); + expect(width).toBe('400px'); + }); + + test('Resize should work correctly with startDateExpr (T944693)', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + views: ['week'], + currentView: 'week', + startDateExpr: 'start', + dataSource: dataSource.map(({ startDate, ...restProps }) => ({ + ...restProps, + start: startDate, + })), + width: 800, + height: 600, + startDayHour: 9, + currentDate: new Date(2019, 3, 1), + }); + + const appointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'Brochure Design Review' }); + const bottomHandle = appointment.locator('.dx-resizable-handle-bottom'); + + await bottomHandle.hover(); + await page.mouse.down(); + await page.mouse.move(0, 100, { steps: 5 }); + await page.mouse.up(); + + const height = await appointment.evaluate((el) => getComputedStyle(el).height); + expect(height).toBe('190px'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/resizeAppointments/cancelAppointmentResize.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/resizeAppointments/cancelAppointmentResize.spec.ts new file mode 100644 index 000000000000..f62716e5df50 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/resizeAppointments/cancelAppointmentResize.spec.ts @@ -0,0 +1,106 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, getContainerUrl, setupTestPage } from '../../../../playwright-helpers'; + +const containerUrl = getContainerUrl(__dirname, '../../../../tests/container.html'); + +const defaultSetupOptions = { + timeZone: 'Etc/GMT', + width: 400, + currentDate: '2021-06-01T00:00:00Z', + dataSource: [{ + text: 'Test Resize', + startDate: '2021-06-01T01:00:00Z', + endDate: '2021-06-01T20:00:00Z', + }], + views: [{ + type: 'timelineDay', + intervalCount: 2, + }], + currentView: 'timelineDay', + startDayHour: 0, + cellDuration: 1440, +}; + +test.describe('Cancel appointment Resizing', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + test('onAppointmentUpdating - newDate should be correct after cancel appointment resize and cellDuration=24h (T1070565)', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + ...defaultSetupOptions, + onAppointmentUpdating: ((e: any) => { + (window as any).newEndDate = e.newData.endDate; + e.cancel = true; + }) as any, + }); + + const appointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'Test Resize' }); + const rightHandle = appointment.locator('.dx-resizable-handle-right'); + const etalonEndDateIso = '2021-06-03T00:00:00Z'; + + await rightHandle.hover(); + await page.mouse.down(); + await page.mouse.move(200, 0, { steps: 5 }); + await page.mouse.up(); + + const timeText1 = await appointment.locator('.dx-scheduler-appointment-content-date').textContent(); + expect(timeText1).toContain('1:00 AM - 8:00 PM'); + + const newEndDate1 = await page.evaluate(() => (window as any).newEndDate); + expect(newEndDate1).toBe(etalonEndDateIso); + + await rightHandle.hover(); + await page.mouse.down(); + await page.mouse.move(200, 0, { steps: 5 }); + await page.mouse.up(); + + const timeText2 = await appointment.locator('.dx-scheduler-appointment-content-date').textContent(); + expect(timeText2).toContain('1:00 AM - 8:00 PM'); + + const newEndDate2 = await page.evaluate(() => (window as any).newEndDate); + expect(newEndDate2).toBe(etalonEndDateIso); + }); + + test('on escape - date should not changed when it is pressed after resize (T1125615)', async ({ page }) => { + await createWidget(page, 'dxScheduler', defaultSetupOptions); + + const appointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'Test Resize' }); + const rightHandle = appointment.locator('.dx-resizable-handle-right'); + + await rightHandle.hover(); + await page.mouse.down(); + await page.mouse.move(50, 0, { steps: 5 }); + await page.mouse.up(); + + const timeText1 = await appointment.locator('.dx-scheduler-appointment-content-date').textContent(); + expect(timeText1).toContain('1:00 AM - 12:00 AM'); + + await appointment.click(); + await page.keyboard.press('Escape'); + + await rightHandle.hover(); + await page.mouse.down(); + await page.mouse.move(150, 0, { steps: 5 }); + await page.mouse.up(); + + const timeText2 = await appointment.locator('.dx-scheduler-appointment-content-date').textContent(); + expect(timeText2).toContain('1:00 AM - 12:00 AM'); + }); + + test('on escape - date should not changed when it is pressed during resize (T1125615)', async ({ page }) => { + await createWidget(page, 'dxScheduler', defaultSetupOptions); + + const appointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'Test Resize' }); + const rightHandle = appointment.locator('.dx-resizable-handle-right'); + + await rightHandle.hover(); + await page.mouse.down(); + await page.mouse.move(150, 0, { steps: 5 }); + await page.keyboard.press('Escape'); + await page.mouse.up(); + + const timeText = await appointment.locator('.dx-scheduler-appointment-content-date').textContent(); + expect(timeText).toContain('1:00 AM - 8:00 PM'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/resizeAppointments/timeline.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/resizeAppointments/timeline.spec.ts new file mode 100644 index 000000000000..1b7ad364b4e8 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/resizeAppointments/timeline.spec.ts @@ -0,0 +1,181 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, getContainerUrl, setupTestPage } from '../../../../playwright-helpers'; + +const containerUrl = getContainerUrl(__dirname, '../../../../tests/container.html'); + +const dataSource = [ + { + text: 'Brochure Design Review', + startDate: new Date(2019, 3, 1, 10, 0), + endDate: new Date(2019, 3, 1, 11, 0), + resourceId: 0, + }, +]; + +const defaultOptions = { + width: 800, + height: 600, + startDayHour: 9, + currentDate: new Date(2019, 3, 1), +}; + +test.describe('Resize appointments in the Scheduler timeline views', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + ['timelineDay', 'timelineWeek', 'timelineWorkWeek'].forEach((view) => { + test(`Resize in the "${view}" view`, async ({ page }) => { + await createWidget(page, 'dxScheduler', { + ...defaultOptions, + views: [view], + currentView: view, + dataSource, + }); + + const appointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'Brochure Design Review' }); + const rightHandle = appointment.locator('.dx-resizable-handle-right'); + + await rightHandle.hover(); + await page.mouse.down(); + await page.mouse.move(400, 0, { steps: 10 }); + await page.mouse.up(); + + const width1 = await appointment.evaluate((el) => getComputedStyle(el).width); + expect(width1).toBe('800px'); + }); + }); + + test('Resize in the "timelineMonth" view', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + ...defaultOptions, + views: ['timelineMonth'], + currentView: 'timelineMonth', + dataSource, + }); + + const appointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'Brochure Design Review' }); + const rightHandle = appointment.locator('.dx-resizable-handle-right'); + + await rightHandle.hover(); + await page.mouse.down(); + await page.mouse.move(400, 0, { steps: 10 }); + await page.mouse.up(); + + const width = await appointment.evaluate((el) => getComputedStyle(el).width); + expect(width).toBe('600px'); + }); + + test('Resize appointment on timelineWeek view with custom startDayHour & endDayHour (T804779)', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + ...defaultOptions, + views: [{ + type: 'timelineWeek', startDayHour: 10, endDayHour: 16, cellDuration: 60, + }], + currentView: 'timelineWeek', + currentDate: new Date(2019, 8, 1), + firstDayOfWeek: 0, + dataSource: [{ + text: 'Appointment', + startDate: new Date(2019, 8, 1, 14), + endDate: new Date(2019, 8, 2, 11), + }], + }); + + const appointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'Appointment' }); + const rightHandle = appointment.locator('.dx-resizable-handle-right'); + + await rightHandle.hover(); + await page.mouse.down(); + await page.mouse.move(-400, 0, { steps: 10 }); + await page.mouse.up(); + + const width = await appointment.evaluate((el) => getComputedStyle(el).width); + expect(width).toBe('200px'); + }); + + test('Resize should work correctly when cell width is not an integer', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + views: [{ type: 'timelineDay', cellDuration: 120 }], + currentView: 'timelineDay', + currentDate: new Date(2020, 10, 13), + dataSource: [{ + text: 'Appointment', + startDate: new Date(2020, 10, 13, 0, 0), + endDate: new Date(2020, 10, 13, 2, 0), + }], + width: 2999, + startDayHour: 0, + endDayHour: 24, + height: 600, + }); + + const appointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'Appointment' }); + const rightHandle = appointment.locator('.dx-resizable-handle-right'); + + await rightHandle.hover(); + await page.mouse.down(); + await page.mouse.move(100, 0, { steps: 5 }); + await page.mouse.up(); + + const timeText = await appointment.locator('.dx-scheduler-appointment-content-date').textContent(); + expect(timeText).toContain('12:00 AM - 4:00 AM'); + }); + + test('Resize in the "timelineDay" view with start and end day hour (T1134583)', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource: [{ + text: 'Appointment', + startDate: new Date(2024, 0, 3, 9, 30), + endDate: new Date(2024, 0, 3, 12, 30), + }], + views: [{ type: 'timelineDay', intervalCount: 3 }], + currentView: 'timelineDay', + currentDate: new Date(2024, 0, 2), + cellDuration: 60, + startDayHour: 10, + endDayHour: 12, + width: 1200, + height: 600, + }); + + const appointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'Appointment' }); + const rightHandle = appointment.locator('.dx-resizable-handle-right'); + + await rightHandle.hover(); + await page.mouse.down(); + await page.mouse.move(200, 0, { steps: 5 }); + await page.mouse.up(); + + const width1 = await appointment.evaluate((el) => getComputedStyle(el).width); + expect(width1).toBe('600px'); + }); + + test('Resize in the "timelineMonth" view with start and end day hour (T1134583)', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource: [{ + text: 'Appointment', + startDate: new Date(2024, 0, 3, 9, 30), + endDate: new Date(2024, 0, 3, 12, 30), + }], + views: ['timelineMonth'], + currentView: 'timelineMonth', + currentDate: new Date(2024, 0, 2), + cellDuration: 60, + startDayHour: 10, + endDayHour: 12, + height: 600, + }); + + const appointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'Appointment' }); + const rightHandle = appointment.locator('.dx-resizable-handle-right'); + + await rightHandle.hover(); + await page.mouse.down(); + await page.mouse.move(200, 0, { steps: 5 }); + await page.mouse.up(); + + const width = await appointment.evaluate((el) => getComputedStyle(el).width); + expect(width).toBe('400px'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/resizeAppointments/verticalGrouping.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/resizeAppointments/verticalGrouping.spec.ts new file mode 100644 index 000000000000..54ea30ef076b --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/resizeAppointments/verticalGrouping.spec.ts @@ -0,0 +1,90 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, getContainerUrl, setupTestPage } from '../../../../playwright-helpers'; + +const containerUrl = getContainerUrl(__dirname, '../../../../tests/container.html'); + +const resourcesData = [{ + fieldExpr: 'priorityId', + allowMultiple: false, + dataSource: [ + { text: 'Low Priority', id: 1, color: '#1e90ff' }, + { text: 'High Priority', id: 2, color: '#ff9747' }, + ], +}]; + +test.describe('Resize appointments in the Scheduler with vertical grouping', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + test('Should correctly calculate group resizing area (T1025952)', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource: [ + { text: 'first', startDate: new Date(2021, 3, 21, 9, 30), endDate: new Date(2021, 3, 21, 10), priorityId: 1 }, + { text: 'second', startDate: new Date(2021, 3, 21, 9, 30), endDate: new Date(2021, 3, 21, 10), priorityId: 2 }, + ], + views: [{ type: 'workWeek', groupOrientation: 'vertical' }], + currentView: 'workWeek', + currentDate: new Date(2021, 3, 21), + startDayHour: 9, + endDayHour: 12, + groups: ['priorityId'], + resources: resourcesData, + width: 800, + height: 600, + }); + + const firstAppointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'first' }); + const bottomHandle1 = firstAppointment.locator('.dx-resizable-handle-bottom'); + + await bottomHandle1.hover(); + await page.mouse.down(); + await page.mouse.move(0, 100, { steps: 5 }); + await page.mouse.up(); + + const height1 = await firstAppointment.evaluate((el) => getComputedStyle(el).height); + expect(height1).toBe('140.594px'); + + const secondAppointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'second' }); + const bottomHandle2 = secondAppointment.locator('.dx-resizable-handle-bottom'); + + await bottomHandle2.hover(); + await page.mouse.down(); + await page.mouse.move(0, 100, { steps: 5 }); + await page.mouse.up(); + + const height2 = await secondAppointment.evaluate((el) => getComputedStyle(el).height); + expect(height2).toBe('165.922px'); + }); + + test('Should correctly calculate group resizing area after scroll (T1041672)', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource: [ + { text: 'app', startDate: new Date(2021, 3, 21, 9, 30), endDate: new Date(2021, 3, 21, 10), priorityId: 2 }, + ], + views: [{ type: 'week', groupOrientation: 'vertical' }], + currentView: 'week', + currentDate: new Date(2021, 3, 21), + height: 400, + groups: ['priorityId'], + resources: resourcesData, + width: 800, + }); + + await page.evaluate(() => { + const instance = ($('#container') as any).dxScheduler('instance'); + instance.scrollTo(new Date(2021, 3, 21, 9, 30), { priorityId: 2 }); + }); + + const appointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'app' }); + const bottomHandle = appointment.locator('.dx-resizable-handle-bottom'); + + await bottomHandle.hover(); + await page.mouse.down(); + await page.mouse.move(0, 100, { steps: 5 }); + await page.mouse.up(); + + const height = await appointment.evaluate((el) => getComputedStyle(el).height); + expect(height).toBe('165.922px'); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/resizeAppointments/zooming.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/resizeAppointments/zooming.spec.ts new file mode 100644 index 000000000000..27d7c148a087 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/resizeAppointments/zooming.spec.ts @@ -0,0 +1,46 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, getContainerUrl, setupTestPage, insertStylesheetRulesToPage } from '../../../../playwright-helpers'; + +const containerUrl = getContainerUrl(__dirname, '../../../../tests/container.html'); + +async function setZoomLevel(page, zoomLevel: number): Promise { + await page.evaluate((z) => { + $('body').css('zoom', `${z}%`); + }, zoomLevel); +} + +test.describe('Resize appointments - Zooming', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + test('Vertical resize with zooming', async ({ page }) => { + await setZoomLevel(page, 110); + await insertStylesheetRulesToPage(page, '.dx-scheduler-cell-sizes-vertical { height: 43px;}'); + + await createWidget(page, 'dxScheduler', { + dataSource: [{ + text: 'Appt-01', + startDate: new Date(2021, 2, 28, 0), + endDate: new Date(2021, 2, 28, 0, 30), + }], + views: ['day'], + currentView: 'day', + cellDuration: 15, + currentDate: new Date(2021, 2, 28), + }); + + const appointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'Appt-01' }); + const bottomHandle = appointment.locator('.dx-resizable-handle-bottom'); + + await bottomHandle.hover(); + await page.mouse.down(); + await page.mouse.move(0, 430, { steps: 10 }); + await page.mouse.up(); + + const height = await appointment.evaluate((el) => parseInt(getComputedStyle(el).height, 10)); + expect(height).toBe(94); + + await setZoomLevel(page, 0); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/scrollTo.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/scrollTo.spec.ts new file mode 100644 index 000000000000..149cd423f060 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/scrollTo.spec.ts @@ -0,0 +1,337 @@ +import { test, expect } from '@playwright/test'; +import { createWidget } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('Scheduler: ScrollTo', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + +); + +const createScheduler = async (options): Promise => createWidget(page, 'dxScheduler', options); + +async function scrollToDate(page: Page) { + await page.evaluate(() => { + const instance = ($('#container') as any).dxScheduler('instance'); + const currentDate = instance.option('currentDate'); + const date = new Date(currentDate.getTime()); + date.setHours(date.getHours() + 6, 30, 0, 0); + instance.scrollTo(date); +}, ); +} + +async function scrollToDateWithGroups(page: Page) { + await page.evaluate(() => { + const instance = ($('#container') as any).dxScheduler('instance'); + const currentDate = instance.option('currentDate'); + const date = new Date(currentDate.getTime()); + date.setHours(date.getHours() + 6, 30, 0, 0); + instance.scrollTo(date, { priority: 1 }, ); +} +}); + +async function scrollToAllDay(page: Page) { + await page.evaluate(() => { + const instance = ($('#container') as any).dxScheduler('instance'); + const currentDate = instance.option('currentDate'); + const date = new Date(currentDate.getTime()); + date.setHours(date.getHours() + 6, 30, 0, 0); + instance.scrollTo(date, undefined, true); +}, ); +} + +test('ScrollTo works correctly with week and day views', async ({ page }) => { + // Scheduler on '#container' + + const views = [{ name: 'week', initValue: 0 }, { name: 'day', initValue: 0 }]; + + // eslint-disable-next-line no-restricted-syntax + for (const view of views) { + const { name, initValue } = view; + + await scheduler.option('currentView', name); + await scheduler.option('useNative', true); + await await page.waitForTimeout(1000); + + await scrollToDate(); + await await page.waitForTimeout(1000); + + expect(page.locator('.dx-scheduler-work-space')Scroll.top).toBeGreaterThan(initValue, `Work space is scrolled in ${name} view`); + } +}).before(async () => createScheduler({ + dataSource: [], + views: ['week', 'day'], + currentView: 'week', + currentDate: new Date(2019, 5, 1, 9, 40), + firstDayOfWeek: 0, + startDayHour: 0, + endDayHour: 20, + height: 580, +})); + +test('ScrollTo works correctly with grouping in week view', async ({ page }) => { + // Scheduler on '#container' + + await scheduler.option('currentView', 'week'); + await scheduler.option('useNative', true); + await await page.waitForTimeout(1000); + + const initialTop = await page.locator('.dx-scheduler-work-space')Scroll.top; + + await scrollToDateWithGroups(); + await await page.waitForTimeout(1000); + + expect(page.locator('.dx-scheduler-work-space')Scroll.top).toBeGreaterThan(initialTop, 'Work space is scrolled with groups'); +}).before(async () => createScheduler({ + dataSource: [], + views: ['week'], + currentView: 'week', + currentDate: new Date(2019, 5, 1, 9, 40), + firstDayOfWeek: 0, + startDayHour: 0, + endDayHour: 20, + groups: ['priority'], + resources: [{ + fieldExpr: 'priority', + dataSource: [ + { id: 1, text: 'High Priority' }, + { id: 2, text: 'Low Priority' }, + ], + }], + height: 580, +})); + +test('ScrollTo works correctly with all-day panel', async ({ page }) => { + // Scheduler on '#container' + + await scheduler.option('currentView', 'week'); + await scheduler.option('useNative', true); + + const initValue = 0; + const expectedTopValue = 0; + + expect(page.locator('.dx-scheduler-work-space')Scroll.top).toBe(initValue, 'Work space has init scroll position'); + + await scrollToAllDay(); + await await page.waitForTimeout(3000); + + expect(page.locator('.dx-scheduler-work-space')Scroll.top).toBe(expectedTopValue, 'Work space is scrolled to all-day panel'); +}).before(async () => createScheduler({ + dataSource: [], + views: ['week'], + currentView: 'week', + currentDate: new Date(2019, 5, 1, 9, 40), + firstDayOfWeek: 0, + startDayHour: 0, + endDayHour: 20, + showAllDayPanel: true, + height: 580, +})); + +test('ScrollTo works correctly with RTL mode', async ({ page }) => { + // Scheduler on '#container' + + await scheduler.option('currentView', 'week'); + await scheduler.option('useNative', true); + await scheduler.option('rtlEnabled', true); + await await page.waitForTimeout(1000); + + const initialBrowserTop = await page.locator('.dx-scheduler-work-space')Scroll.top; + + await scrollToDate(); + await await page.waitForTimeout(1000); + + const browserTop = await ClientFunction(() => ($('#container') as any).dxScheduler('instance').getWorkSpaceScrollable().scrollTop())(); + + expect(browserTop).toBeGreaterThan(initialBrowserTop, 'Work space is scrolled in RTL'); +}).before(async () => createScheduler({ + dataSource: [], + views: ['week'], + currentView: 'week', + currentDate: new Date(2019, 5, 1, 9, 40), + firstDayOfWeek: 0, + startDayHour: 0, + endDayHour: 20, + height: 580, +})); + +test('ScrollTo works correctly with timeline views (native, sync header/workspace) (T749957)', async (t) => { + // Scheduler on '#container' + + const views = [{ name: 'timelineDay' }, { name: 'timelineWeek' }]; + + // eslint-disable-next-line no-restricted-syntax + for (const view of views) { + const { name } = view; + + await scheduler.option('currentView', name); + await scheduler.option('useNative', true); + await await page.waitForTimeout(200); + + async function getWSLeft(page: Page) { + return page.evaluate(() => ($('#container') as any).dxScheduler('instance').getWorkSpaceScrollable().scrollLeft(), ); +} + async function getHeaderLeft(page: Page) { + return page.evaluate(() => $('.dx-scheduler-header-scrollable .dx-scrollable-container').scrollLeft(), ); +} + + const initialLeft = await getWSLeft(); + const initialHeaderLeft = await getHeaderLeft(); + + expect(initialLeft).toBe(initialHeaderLeft, `${name}: header/workspace initial sync`); + + await scrollToDate(); + await await page.waitForTimeout(300); + + const left = await getWSLeft(); + const headerLeft = await getHeaderLeft(); + + expect(left).notEql(initialLeft, `${name}: workspace left changed`) + .expect(headerLeft).toBe(left, `${name}: header synchronized with workspace`); + } +}).before(async () => createScheduler({ + dataSource: [], + views: ['timelineDay', 'timelineWeek'], + currentView: 'timelineDay', + currentDate: new Date(2019, 5, 1, 9, 40), + firstDayOfWeek: 0, + startDayHour: 0, + endDayHour: 20, + height: 580, +})); + +test('ScrollTo works correctly in timeline RTL (native, sync header/workspace)', async (t) => { + // Scheduler on '#container' + + await scheduler.option('currentView', 'timelineWeek'); + await scheduler.option('useNative', true); + await scheduler.option('rtlEnabled', true); + await await page.waitForTimeout(200); + + async function getWSLeft(page: Page) { + return page.evaluate(() => ($('#container') as any).dxScheduler('instance').getWorkSpaceScrollable().scrollLeft(), ); +} + async function getHeaderLeft(page: Page) { + return page.evaluate(() => $('.dx-scheduler-header-scrollable .dx-scrollable-container').scrollLeft(), ); +} + + const initialLeft = await getWSLeft(); + const initialHeaderLeft = await getHeaderLeft(); + + expect(initialLeft).toBe(initialHeaderLeft, 'timeline RTL: initial sync'); + + await scrollToDate(); + await await page.waitForTimeout(300); + + const left = await getWSLeft(); + const headerLeft = await getHeaderLeft(); + + expect(left).notEql(initialLeft, 'timeline RTL: workspace left changed') + .expect(headerLeft).toBe(left, 'timeline RTL: header synchronized'); +}).before(async () => createScheduler({ + dataSource: [], + views: ['timelineWeek'], + currentView: 'timelineWeek', + currentDate: new Date(2019, 5, 1, 9, 40), + firstDayOfWeek: 0, + startDayHour: 0, + endDayHour: 20, + height: 580, + rtlEnabled: true, +})); + +[ + // startDayHour: 6:00, endDayHour: 18:00 + { + offset: 0, + targetDate: new Date(2021, 1, 3, 4, 0), + expectedDate: new Date(2021, 1, 3, 6, 0), + }, + { + offset: 0, + targetDate: new Date(2021, 1, 3, 12, 0), + expectedDate: new Date(2021, 1, 3, 12, 0), + }, + { + offset: 0, + targetDate: new Date(2021, 1, 3, 20, 0), + expectedDate: new Date(2021, 1, 3, 18, 0), + }, + + // startDayHour: 18:00, endDayHour: next day 6:00 + { + offset: 720, + targetDate: new Date(2021, 1, 3, 10, 0), + expectedDate: new Date(2021, 1, 3, 6, 0), + }, + { + offset: 720, + targetDate: new Date(2021, 1, 3, 20, 0), + expectedDate: new Date(2021, 1, 3, 20, 0), + }, + { + offset: 720, + targetDate: new Date(2021, 1, 4, 1, 0), + expectedDate: new Date(2021, 1, 4, 1, 0), + }, + { + offset: 720, + targetDate: new Date(2021, 1, 4, 7, 0), + expectedDate: new Date(2021, 1, 4, 6, 0), + }, + + // startDayHour: prev day 18:00, endDayHour: 6:00 + { + offset: -720, + targetDate: new Date(2021, 1, 3, 16, 0), + expectedDate: new Date(2021, 1, 3, 18, 0), + }, + { + offset: -720, + targetDate: new Date(2021, 1, 3, 21, 0), + expectedDate: new Date(2021, 1, 3, 21, 0), + }, + { + offset: -720, + targetDate: new Date(2021, 1, 4, 3, 0), + expectedDate: new Date(2021, 1, 4, 3, 0), + }, + { + offset: -720, + targetDate: new Date(2021, 1, 3, 7, 0), + expectedDate: new Date(2021, 1, 3, 6, 0), + }, +].forEach(({ offset, targetDate, expectedDate }) => { + test(`scrollTo should scroll to date with offset=${offset}, targetDate=${targetDate.toString()} (T1310544)`, async (t) => { + // Scheduler on '#container' + + await page.evaluate((d) => $('#container').dxScheduler('instance').scrollTo(new Date(d)), (targetDate).toISOString()); + + const cellData = await scheduler.getCellDataAtViewportCenter(); + + expect(expectedDate.getTime()).toBeGreaterThanOrEqual(cellData.startDate.getTime()) + // eslint-disable-next-line spellcheck/spell-checker + .expect(expectedDate.getTime()).toBeLessThanOrEqual(cellData.endDate.getTime()); + }).before(async () => createScheduler({ + dataSource: [], + views: [{ + type: 'timelineWeek', + offset, + cellDuration: 60, + }], + currentView: 'timelineWeek', + currentDate: new Date(2021, 1, 2), + startDayHour: 6, + endDayHour: 18, + height: 580, + })); +}); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/timezone/T1102713/recurrenceAppointmentInDstTimeEditing.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/timezone/T1102713/recurrenceAppointmentInDstTimeEditing.spec.ts new file mode 100644 index 000000000000..619dcefc7216 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/timezone/T1102713/recurrenceAppointmentInDstTimeEditing.spec.ts @@ -0,0 +1,219 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../../tests/container.html')}`; + +test.describe('Editing recurrent appointment in DST time', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + +); + +interface ITestResizeOptions { + direction: keyof Appointment['resizableHandle']; + value: number; +} +interface ITestDragNDropOptions { + rowIdx: number; + cellIdx: number; +} + +const SCREENSHOT_BASE_NAME = 'recurrent-appointment-timezone-dst__editing'; +const SCHEDULER_SELECTOR = '#container'; +const TEST_APPOINTMENT_TEXT = 'Watercolor Landscape'; +const TEST_CURSOR_OPTIONS = { speed: 0.5 }; +const APPOINTMENT_DATETIME = { + winter: { + start: new Date('2020-11-01T17:30:00.000Z'), + end: new Date('2020-11-01T19:00:00.000Z'), + }, + summer: { + start: new Date('2020-03-08T16:30:00.000Z'), + end: new Date('2020-03-08T18:00:00.000Z'), + }, +}; + +async function editingPopupTestFunction(t: TestController, screenshotName: string): Promise { + const scheduler = new Scheduler(SCHEDULER_SELECTOR); + const screenshotZone = page.locator('.dx-scheduler-work-space'); + const appointmentToEdit = scheduler.getAppointment(TEST_APPOINTMENT_TEXT); + await (appointmentToEdit.element, TEST_CURSOR_OPTIONS).dblclick(); + + const appointmentDialog = new AppointmentDialog(); + await (appointmentDialog.series).click(); + + const { appointmentPopup } = scheduler; + await (appointmentPopup.saveButton.element).click(); + + await testScreenshot(page, `${SCREENSHOT_BASE_NAME}__${screenshotName}.png`, { element: screenshotZone }); + + expect(compareResults.isValid()) + .ok(compareResults.errorMessages()); +} + +async function dragAndDropTestFunction( + t: TestController, + screenshotName: string, + { rowIdx, cellIdx }: ITestDragNDropOptions, +): Promise { + const scheduler = new Scheduler(SCHEDULER_SELECTOR); + const screenshotZone = page.locator('.dx-scheduler-work-space'); + const appointmentToEdit = scheduler.getAppointment(TEST_APPOINTMENT_TEXT); + const cellToMoveElement = page.locator('.dx-scheduler-date-table-row').nth(rowIdx).locator('.dx-scheduler-date-table-cell').nth(cellIdx); + + await /* TODO: dragToElement(appointmentToEdit.element, cellToMoveElement, TEST_CURSOR_OPTIONS) */; + + const appointmentDialog = new AppointmentDialog(); + await (appointmentDialog.series).click(); + + await testScreenshot(page, `${SCREENSHOT_BASE_NAME}__${screenshotName}.png`, { element: screenshotZone }); + expect(compareResults.isValid()) + .ok(compareResults.errorMessages()); +} + +async function resizeTestFunction( + t: TestController, + screenshotName: string, + resizeOptions: ITestResizeOptions, +): Promise { + const scheduler = new Scheduler(SCHEDULER_SELECTOR); + const screenshotZone = page.locator('.dx-scheduler-work-space'); + const appointmentToEdit = scheduler.getAppointment(TEST_APPOINTMENT_TEXT); + + await t.drag( + appointmentToEdit.resizableHandle[resizeOptions.direction], + 0, + resizeOptions.value, + TEST_CURSOR_OPTIONS, + ); + + const appointmentDialog = new AppointmentDialog(); + await (appointmentDialog.series).click(); + + await testScreenshot(page, `${SCREENSHOT_BASE_NAME}__${screenshotName}.png`, { element: screenshotZone }); + expect(compareResults.isValid()) + .ok(compareResults.errorMessages()); +} + +async function configureScheduler({ start, end }: { start: Date; end: Date }) { + await createWidget(page, 'dxScheduler', { + dataSource: [{ + startDate: start, + endDate: end, + recurrenceRule: 'FREQ=WEEKLY;BYDAY=MO', + text: TEST_APPOINTMENT_TEXT, + }], + timeZone: 'America/Los_Angeles', + currentView: 'week', + currentDate: start, + startDayHour: 9, + cellDuration: 30, + width: 1000, + height: 585, + }); +} + +// === EDITING POPUP === +test('Editing popup: should have correctly been edited from editing popup. DST - winter time', async ({ page }) => { + // --- setup --- +await configureScheduler(APPOINTMENT_DATETIME.winter); + // --- test --- +await editingPopupTestFunction(t, 'popup__winter-time'); +}); + +test('Editing popup: should have correctly been edited from editing popup. DST - summer time', async ({ page }) => { + // --- setup --- +await configureScheduler(APPOINTMENT_DATETIME.summer); + // --- test --- +await editingPopupTestFunction(t, 'popup__summer-time'); +}); + +// === DRAG_N_DROP === +test('Drag-n-drop up: should have correctly been edited. DST - winter time', async ({ page }) => { + // --- setup --- +await configureScheduler(APPOINTMENT_DATETIME.winter); + // --- test --- +await dragAndDropTestFunction(t, 'drag-n-drop-up__winter-time', { + rowIdx: 1, + cellIdx: 1, + }); +}); + +test('Drag-n-drop down: should have correctly been edited. DST - winter time', async ({ page }) => { + // --- setup --- +await configureScheduler(APPOINTMENT_DATETIME.winter); + // --- test --- +await dragAndDropTestFunction(t, 'drag-n-drop-down__winter-time', { + rowIdx: 4, + cellIdx: 1, + }); +}); + +test('Drag-n-drop up: should have correctly been edited. DST - summer time', async ({ page }) => { + // --- setup --- +await configureScheduler(APPOINTMENT_DATETIME.summer); + // --- test --- +await dragAndDropTestFunction(t, 'drag-n-drop-up__summer-time', { + rowIdx: 1, + cellIdx: 1, + }); +}); + +test('Drag-n-drop down: should have correctly been edited. DST - summer time', async ({ page }) => { + // --- setup --- +await configureScheduler(APPOINTMENT_DATETIME.summer); + // --- test --- +await dragAndDropTestFunction(t, 'drag-n-drop-down__summer-time', { + rowIdx: 4, + cellIdx: 1, + }); +}); + +// === RESIZE === +test('Resize top: should have correctly been edited. DST - winter time', async ({ page }) => { + // --- setup --- +await configureScheduler(APPOINTMENT_DATETIME.winter); + // --- test --- +await resizeTestFunction(t, 'resize-top__winter-time', { + direction: 'top', + value: 100, + }); +}); + +test('Resize bottom: should have correctly been edited. DST - winter time', async ({ page }) => { + // --- setup --- +await configureScheduler(APPOINTMENT_DATETIME.winter); + // --- test --- +await resizeTestFunction(t, 'resize-bottom__winter-time', { + direction: 'bottom', + value: 100, + }); +}); + +test('Resize top: should have correctly been edited. DST - summer time', async ({ page }) => { + // --- setup --- +await configureScheduler(APPOINTMENT_DATETIME.summer); + // --- test --- +await resizeTestFunction(t, 'resize-top__summer-time', { + direction: 'top', + value: 100, + }); +}); + +test('Resize bottom: should have correctly been edited. DST - summer time', async ({ page }) => { + // --- setup --- +await configureScheduler(APPOINTMENT_DATETIME.summer); + // --- test --- +await resizeTestFunction(t, 'resize-bottom__summer-time', { + direction: 'bottom', + value: 100, + }); +}); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/timezone/recurrence/appointmentWithoutTimezone.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/timezone/recurrence/appointmentWithoutTimezone.spec.ts new file mode 100644 index 000000000000..92d3c1ce6dd3 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/timezone/recurrence/appointmentWithoutTimezone.spec.ts @@ -0,0 +1,262 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../../tests/container.html')}`; + +test.describe('Recurrent appointments without timezone in scheduler with timezone', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + +); + +const SELECT_SELECTOR = '#container'; +const SCHEDULER_SELECTOR = '#otherContainer'; +const SCREENSHOT_BASE_NAME = 'without-timezone-recurrent'; +const TEST_TIMEZONES = ['Etc/GMT-10', 'Etc/GMT+1', 'Etc/GMT+10']; +const TEST_CURSOR_OPTIONS = { speed: 0.5 }; + +const createTimezoneSelect = async ( + selector: string, + items: string[], + schedulerSelector: string, +): Promise => { + await ClientFunction(() => { + ($(selector) as any).dxSelectBox({ + items, + width: 240, + value: items[1], + onValueChanged(data) { + const scheduler = ($(schedulerSelector) as any).dxScheduler('instance'); + scheduler.option('timeZone', data.value); + }, + }); + }, { + dependencies: { selector, schedulerSelector, items }, + })(); +}; + +const selectTimezoneInUI = async (t: TestController, selectBox: SelectBox, timezoneIdx: number) => { + await (selectBox.element, TEST_CURSOR_OPTIONS).click(); + const timezonesList = await selectBox.getList(); + + await (timezonesList.getItem(timezoneIdx).click().element, TEST_CURSOR_OPTIONS); +}; + +test('Should correctly display the recurrent weekly appointment without timezone', async ({ page }) => { + // --- setup --- +const schedulerTimezone = TEST_TIMEZONES[1]; + + await createTimezoneSelect(SELECT_SELECTOR, TEST_TIMEZONES, SCHEDULER_SELECTOR); + await createWidget(page, 'dxScheduler', { + dataSource: [{ + allDay: false, + startDate: new Date('2021-04-28T11:00:00.000Z'), + endDate: new Date('2021-04-28T13:00:00.000Z'), + recurrenceRule: 'FREQ=WEEKLY;BYDAY=WE', + text: 'Test', + }], + timeZone: schedulerTimezone, + currentView: 'week', + currentDate: new Date(2021, 3, 28), + startDayHour: 0, + cellDuration: 180, + width: 1000, + height: 585, + }, SCHEDULER_SELECTOR); + // --- test --- +const selectBox = new SelectBox(SELECT_SELECTOR); + const schedulerWorkspace = new Scheduler(SCHEDULER_SELECTOR).page.locator('.dx-scheduler-work-space'); + // expected date: 4/28/2021 10:00 AM - 12:00 PM + await testScreenshot(page, + getScreenshotName(SCREENSHOT_BASE_NAME, 'weekly-appointment__same-timezone'), + { element: schedulerWorkspace }, + ); + + await selectTimezoneInUI(t, selectBox, 0); + // expected date: 4/28/2021 9:00 PM - 11:00 PM + await testScreenshot(page, + getScreenshotName(SCREENSHOT_BASE_NAME, 'weekly-appointment__greater-timezone'), + { element: schedulerWorkspace }, + ); + + await selectTimezoneInUI(t, selectBox, 2); + // expected date: 4/28/2021 1:00 AM - 3:00 AM + await testScreenshot(page, + getScreenshotName(SCREENSHOT_BASE_NAME, 'weekly-appointment__lower-timezone'), + { element: schedulerWorkspace }, + ); + + expect(compareResults.isValid()) + .ok(compareResults.errorMessages()); +}); + +test('Should correctly display the recurrent monthly appointment without timezone', async ({ page }) => { + // --- setup --- +const schedulerTimezone = TEST_TIMEZONES[1]; + + await createTimezoneSelect(SELECT_SELECTOR, TEST_TIMEZONES, SCHEDULER_SELECTOR); + await createWidget(page, 'dxScheduler', { + dataSource: [{ + allDay: false, + startDate: new Date('2021-04-28T11:00:00.000Z'), + endDate: new Date('2021-04-28T13:00:00.000Z'), + recurrenceRule: 'FREQ=WEEKLY;BYDAY=WE', + text: 'Test', + }], + timeZone: schedulerTimezone, + currentView: 'week', + currentDate: new Date(2021, 3, 28), + startDayHour: 0, + cellDuration: 180, + width: 1000, + height: 585, + }, SCHEDULER_SELECTOR); + // --- test --- +const selectBox = new SelectBox(SELECT_SELECTOR); + const schedulerWorkspace = new Scheduler(SCHEDULER_SELECTOR).page.locator('.dx-scheduler-work-space'); + // expected date: 4/28/2021 10:00 AM - 12:00 PM + await testScreenshot(page, + getScreenshotName(SCREENSHOT_BASE_NAME, 'monthly-appointment__same-timezone'), + { element: schedulerWorkspace }, + ); + + await selectTimezoneInUI(t, selectBox, 0); + // expected date: 4/28/2021 9:00 PM - 11:00 PM + await testScreenshot(page, + getScreenshotName(SCREENSHOT_BASE_NAME, 'monthly-appointment__greater-timezone'), + { element: schedulerWorkspace }, + ); + + await selectTimezoneInUI(t, selectBox, 2); + // expected date: 4/28/2021 1:00 AM - 3:00 AM + await testScreenshot(page, + getScreenshotName(SCREENSHOT_BASE_NAME, 'monthly-appointment__lower-timezone'), + { element: schedulerWorkspace }, + ); + + expect(compareResults.isValid()) + .ok(compareResults.errorMessages()); +}); + +test('Should correctly display the recurrent yearly appointment without timezone', async ({ page }) => { + // --- setup --- +const schedulerTimezone = TEST_TIMEZONES[1]; + + await createTimezoneSelect(SELECT_SELECTOR, TEST_TIMEZONES, SCHEDULER_SELECTOR); + await createWidget(page, 'dxScheduler', { + dataSource: [{ + allDay: false, + startDate: new Date('2021-04-28T11:00:00.000Z'), + endDate: new Date('2021-04-28T13:00:00.000Z'), + recurrenceRule: 'FREQ=WEEKLY;BYDAY=WE', + text: 'Test', + }], + timeZone: schedulerTimezone, + currentView: 'week', + currentDate: new Date(2021, 3, 28), + startDayHour: 0, + cellDuration: 180, + width: 1000, + height: 585, + }, SCHEDULER_SELECTOR); + // --- test --- +const selectBox = new SelectBox(SELECT_SELECTOR); + const schedulerWorkspace = new Scheduler(SCHEDULER_SELECTOR).page.locator('.dx-scheduler-work-space'); + // expected date: 4/28/2021 10:00 AM - 12:00 PM + await testScreenshot(page, + getScreenshotName(SCREENSHOT_BASE_NAME, 'yearly-appointment__same-timezone'), + { element: schedulerWorkspace }, + ); + + await selectTimezoneInUI(t, selectBox, 0); + // expected date: 4/28/2021 9:00 PM - 11:00 PM + await testScreenshot(page, + getScreenshotName(SCREENSHOT_BASE_NAME, 'yearly-appointment__greater-timezone'), + { element: schedulerWorkspace }, + ); + + await selectTimezoneInUI(t, selectBox, 2); + // expected date: 4/28/2021 1:00 AM - 3:00 AM + await testScreenshot(page, + getScreenshotName(SCREENSHOT_BASE_NAME, 'yearly-appointment__lower-timezone'), + { element: schedulerWorkspace }, + ); + + expect(compareResults.isValid()) + .ok(compareResults.errorMessages()); +}); + +test('Should correctly display morning weekly recurrent appointment in a greater timezone.', async ({ page }) => { + // --- setup --- +const schedulerTimezone = TEST_TIMEZONES[0]; + + await createTimezoneSelect(SELECT_SELECTOR, TEST_TIMEZONES, SCHEDULER_SELECTOR); + await createWidget(page, 'dxScheduler', { + dataSource: [{ + text: 'test', + startDate: new Date('2021-04-29T15:00:00.000Z'), + endDate: new Date('2021-04-29T17:00:00.000Z'), + recurrenceRule: 'FREQ=WEEKLY;BYDAY=FR', + }], + timeZone: schedulerTimezone, + currentView: 'week', + currentDate: new Date(2021, 3, 28), + startDayHour: 0, + cellDuration: 180, + width: 1000, + height: 585, + }, SCHEDULER_SELECTOR); + // --- test --- +const schedulerWorkspace = new Scheduler(SCHEDULER_SELECTOR).page.locator('.dx-scheduler-work-space'); + await testScreenshot(page, + getScreenshotName(SCREENSHOT_BASE_NAME, 'weekly-morning-appointment__greater-timezone'), + { element: schedulerWorkspace }, + ); + + expect(compareResults.isValid()) + .ok(compareResults.errorMessages()); +}); + +test('Should correctly display \'corner\' weekly recurrent appointments in a greater timezone.', async ({ page }) => { + // --- setup --- +const schedulerTimezone = TEST_TIMEZONES[0]; + + await createTimezoneSelect(SELECT_SELECTOR, TEST_TIMEZONES, SCHEDULER_SELECTOR); + await createWidget(page, 'dxScheduler', { + dataSource: [{ + text: 'test 1', + startDate: new Date('2021-04-24T14:00:00.000Z'), + endDate: new Date('2021-04-24T16:00:00.000Z'), + recurrenceRule: 'FREQ=WEEKLY;BYDAY=SU', + }, { + text: 'test 2', + startDate: new Date('2021-05-01T12:00:00.000Z'), + endDate: new Date('2021-05-01T14:00:00.000Z'), + recurrenceRule: 'FREQ=WEEKLY;BYDAY=SA', + }], + timeZone: schedulerTimezone, + currentView: 'week', + currentDate: new Date(2021, 3, 28), + startDayHour: 0, + cellDuration: 180, + width: 1000, + height: 585, + }, SCHEDULER_SELECTOR); + // --- test --- +const schedulerWorkspace = new Scheduler(SCHEDULER_SELECTOR).page.locator('.dx-scheduler-work-space'); + await testScreenshot(page, + getScreenshotName(SCREENSHOT_BASE_NAME, 'weekly-corner-appointments__greater-timezone'), + { element: schedulerWorkspace }, + ); + + expect(compareResults.isValid()) + .ok(compareResults.errorMessages()); +}); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/timezone/recurrence/monthlyRecurrentAppointment.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/timezone/recurrence/monthlyRecurrentAppointment.spec.ts new file mode 100644 index 000000000000..7739d027bfc2 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/timezone/recurrence/monthlyRecurrentAppointment.spec.ts @@ -0,0 +1,365 @@ +import { test, expect } from '@playwright/test'; +import { createWidget } from '../../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../../tests/container.html')}`; + +test.describe('Monthly recurrent appointments with timezones', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + +const SCREENSHOT_BASE_NAME = 'timezone-monthly-recurrent'; + +); + +test('Should correctly display the recurrent monthly appointment with the same timezone', async ({ page }) => { + // --- setup --- +const appointmentTimezone = 'Etc/GMT+1'; + const schedulerTimezone = 'Etc/GMT+1'; + + await createWidget(page, 'dxScheduler', { + dataSource: [{ + allDay: false, + startDate: getAppointmentTime(new Date(2021, 3, 28, 10, 0, 0), appointmentTimezone), + startDateTimeZone: appointmentTimezone, + endDate: getAppointmentTime(new Date(2021, 3, 28, 12, 0, 0), appointmentTimezone), + endDateTimeZone: appointmentTimezone, + recurrenceRule: 'FREQ=MONTHLY;BYMONTHDAY=28', + text: 'Test', + }], + timeZone: schedulerTimezone, + currentView: 'week', + currentDate: new Date(2021, 3, 28), + startDayHour: 0, + cellDuration: 180, + width: 1000, + height: 585, + // --- test --- +// expected date: 4/28/2021 10:00 AM - 12:00 PM + await screenshotTestFunc(t, SCREENSHOT_BASE_NAME, 'same-date__same-timezone'); +}); +}); + +test('Should correctly display the recurrent monthly appointment with a greater time timezone', async ({ page }) => { + // --- setup --- +const appointmentTimezone = 'Etc/GMT+10'; + const schedulerTimezone = 'Etc/GMT-2'; + + await createWidget(page, 'dxScheduler', { + dataSource: [{ + allDay: false, + startDate: getAppointmentTime(new Date(2021, 3, 28, 22, 0, 0), appointmentTimezone), + startDateTimeZone: appointmentTimezone, + endDate: getAppointmentTime(new Date(2021, 3, 29, 0, 0, 0), appointmentTimezone), + endDateTimeZone: appointmentTimezone, + recurrenceRule: 'FREQ=MONTHLY;BYMONTHDAY=28', + text: 'Test', + }], + timeZone: schedulerTimezone, + currentView: 'week', + currentDate: new Date(2021, 3, 28), + startDayHour: 0, + cellDuration: 180, + width: 1000, + height: 585, + // --- test --- +// expected date: 4/29/2021 10:00 AM - 12:00 PM + await screenshotTestFunc(t, SCREENSHOT_BASE_NAME, 'same-date__greater-timezone'); +}); +}); + +test('Should correctly display the recurrent monthly appointment with a lower time timezone', async ({ page }) => { + // --- setup --- +const appointmentTimezone = 'Etc/GMT-2'; + const schedulerTimezone = 'Etc/GMT+10'; + + await createWidget(page, 'dxScheduler', { + dataSource: [{ + allDay: false, + startDate: getAppointmentTime(new Date(2021, 3, 28, 0, 0, 0), appointmentTimezone), + startDateTimeZone: appointmentTimezone, + endDate: getAppointmentTime(new Date(2021, 3, 28, 2, 0, 0), appointmentTimezone), + endDateTimeZone: appointmentTimezone, + recurrenceRule: 'FREQ=MONTHLY;BYMONTHDAY=28', + text: 'Test', + }], + timeZone: schedulerTimezone, + currentView: 'week', + currentDate: new Date(2021, 3, 28), + startDayHour: 0, + cellDuration: 180, + width: 1000, + height: 585, + // --- test --- +// expected date: 4/27/2021 12:00 PM - 2:00 PM + await screenshotTestFunc(t, SCREENSHOT_BASE_NAME, 'same-date__lower-timezone'); +}); +}); + +test(`Should correctly display the recurrent monthly appointment +if start date lower that recurrent date with the same time timezone`, async ({ page }) => { + // --- setup --- +const appointmentTimezone = 'Etc/GMT-2'; + const schedulerTimezone = 'Etc/GMT-2'; + + await createWidget(page, 'dxScheduler', { + dataSource: [{ + allDay: false, + startDate: getAppointmentTime(new Date(2021, 3, 26, 10, 0, 0), appointmentTimezone), + startDateTimeZone: appointmentTimezone, + endDate: getAppointmentTime(new Date(2021, 3, 26, 12, 0, 0), appointmentTimezone), + endDateTimeZone: appointmentTimezone, + recurrenceRule: 'FREQ=MONTHLY;BYMONTHDAY=28', + text: 'Test', + }], + timeZone: schedulerTimezone, + currentView: 'week', + currentDate: new Date(2021, 3, 28), + startDayHour: 0, + cellDuration: 180, + width: 1000, + height: 585, + // --- test --- +// expected date: 4/28/2021 10:00 AM - 12:00 PM + await screenshotTestFunc(t, SCREENSHOT_BASE_NAME, 'lower-date__same-timezone'); +}); +}); + +test(`Should correctly display the recurrent monthly appointment +if start date lower that recurrent date with a greater time timezone`, async ({ page }) => { + // --- setup --- +const appointmentTimezone = 'Etc/GMT+10'; + const schedulerTimezone = 'Etc/GMT-2'; + + await createWidget(page, 'dxScheduler', { + dataSource: [{ + allDay: false, + startDate: getAppointmentTime(new Date(2021, 3, 26, 14, 0, 0), appointmentTimezone), + startDateTimeZone: appointmentTimezone, + endDate: getAppointmentTime(new Date(2021, 3, 26, 16, 0, 0), appointmentTimezone), + endDateTimeZone: appointmentTimezone, + recurrenceRule: 'FREQ=MONTHLY;BYMONTHDAY=28', + text: 'Test', + }], + timeZone: schedulerTimezone, + currentView: 'week', + currentDate: new Date(2021, 3, 28), + startDayHour: 0, + cellDuration: 180, + width: 1000, + height: 585, + // --- test --- +// expected date: 4/29/2021 2:00 AM - 4:00 AM + await screenshotTestFunc(t, SCREENSHOT_BASE_NAME, 'lower-date__greater-timezone'); +}); +}); + +test(`Should correctly display the recurrent monthly appointment +if start date lower that recurrent date with a lower time timezone`, async ({ page }) => { + // --- setup --- +const appointmentTimezone = 'Etc/GMT-2'; + const schedulerTimezone = 'Etc/GMT+10'; + + await createWidget(page, 'dxScheduler', { + dataSource: [{ + allDay: false, + startDate: getAppointmentTime(new Date(2021, 3, 26, 4, 0, 0), appointmentTimezone), + startDateTimeZone: appointmentTimezone, + endDate: getAppointmentTime(new Date(2021, 3, 26, 6, 0, 0), appointmentTimezone), + endDateTimeZone: appointmentTimezone, + recurrenceRule: 'FREQ=MONTHLY;BYMONTHDAY=28', + text: 'Test', + }], + timeZone: schedulerTimezone, + currentView: 'week', + currentDate: new Date(2021, 3, 28), + startDayHour: 0, + cellDuration: 180, + width: 1000, + height: 585, + // --- test --- +// expected date: 4/27/2021 4:00 PM - 6:00 PM + await screenshotTestFunc(t, SCREENSHOT_BASE_NAME, 'lower-date__lower-timezone'); +}); +}); + +test(`Should correctly display the recurrent monthly appointment at first date +if start date greater that recurrent date with a same time timezone`, async ({ page }) => { + // --- setup --- +const appointmentTimezone = 'Etc/GMT-2'; + const schedulerTimezone = 'Etc/GMT-2'; + + await createWidget(page, 'dxScheduler', { + dataSource: [{ + allDay: false, + startDate: getAppointmentTime(new Date(2021, 3, 28, 10, 0, 0), appointmentTimezone), + startDateTimeZone: appointmentTimezone, + endDate: getAppointmentTime(new Date(2021, 3, 28, 12, 0, 0), appointmentTimezone), + endDateTimeZone: appointmentTimezone, + recurrenceRule: 'FREQ=MONTHLY;BYMONTHDAY=26', + text: 'Test', + }], + timeZone: schedulerTimezone, + currentView: 'week', + currentDate: new Date(2021, 3, 28), + startDayHour: 0, + cellDuration: 180, + width: 1000, + height: 585, + // --- test --- +// expected no visible date + await screenshotTestFunc(t, SCREENSHOT_BASE_NAME, 'greater-date__same-timezone__same-view-date'); +}); +}); + +test(`Should correctly display the recurrent monthly appointment at next date +if start date greater that recurrent date with a same time timezone`, async ({ page }) => { + // --- setup --- +const appointmentTimezone = 'Etc/GMT-2'; + const schedulerTimezone = 'Etc/GMT-2'; + + await createWidget(page, 'dxScheduler', { + dataSource: [{ + allDay: false, + startDate: getAppointmentTime(new Date(2021, 3, 28, 10, 0, 0), appointmentTimezone), + startDateTimeZone: appointmentTimezone, + endDate: getAppointmentTime(new Date(2021, 3, 28, 12, 0, 0), appointmentTimezone), + endDateTimeZone: appointmentTimezone, + recurrenceRule: 'FREQ=MONTHLY;BYMONTHDAY=26', + text: 'Test', + }], + timeZone: schedulerTimezone, + currentView: 'week', + currentDate: new Date(2021, 4, 28), + startDayHour: 0, + cellDuration: 180, + width: 1000, + height: 585, + // --- test --- +// expected date: 5/26/2021 10:00 AM - 12:00 PM + await screenshotTestFunc(t, SCREENSHOT_BASE_NAME, 'greater-date__same-timezone__next-view-date'); +}); +}); + +test(`Should correctly display the recurrent monthly appointment at first date +if start date greater that recurrent date with a greater time timezone`, async ({ page }) => { + // --- setup --- +const appointmentTimezone = 'Etc/GMT+10'; + const schedulerTimezone = 'Etc/GMT-2'; + + await createWidget(page, 'dxScheduler', { + dataSource: [{ + allDay: false, + startDate: getAppointmentTime(new Date(2021, 3, 28, 14, 0, 0), appointmentTimezone), + startDateTimeZone: appointmentTimezone, + endDate: getAppointmentTime(new Date(2021, 3, 28, 16, 0, 0), appointmentTimezone), + endDateTimeZone: appointmentTimezone, + recurrenceRule: 'FREQ=MONTHLY;BYMONTHDAY=26', + text: 'Test', + }], + timeZone: schedulerTimezone, + currentView: 'week', + currentDate: new Date(2021, 3, 28), + startDayHour: 0, + cellDuration: 180, + width: 1000, + height: 585, + // --- test --- +// expected no visible date + await screenshotTestFunc(t, SCREENSHOT_BASE_NAME, 'greater-date__greater-timezone__same-view-date'); +}); +}); + +test(`Should correctly display the recurrent monthly appointment at next date +if start date greater that recurrent date with a greater time timezone`, async ({ page }) => { + // --- setup --- +const appointmentTimezone = 'Etc/GMT+10'; + const schedulerTimezone = 'Etc/GMT-2'; + + await createWidget(page, 'dxScheduler', { + dataSource: [{ + allDay: false, + startDate: getAppointmentTime(new Date(2021, 3, 28, 14, 0, 0), appointmentTimezone), + startDateTimeZone: appointmentTimezone, + endDate: getAppointmentTime(new Date(2021, 3, 28, 16, 0, 0), appointmentTimezone), + endDateTimeZone: appointmentTimezone, + recurrenceRule: 'FREQ=MONTHLY;BYMONTHDAY=26', + text: 'Test', + }], + timeZone: schedulerTimezone, + currentView: 'week', + currentDate: new Date(2021, 4, 28), + startDayHour: 0, + cellDuration: 180, + width: 1000, + height: 585, + // --- test --- +// expected date: 5/27/2021 2:00 AM - 4:00 AM + await screenshotTestFunc(t, SCREENSHOT_BASE_NAME, 'greater-date__greater-timezone__next-view-date'); +}); +}); + +test(`Should correctly display the recurrent monthly appointment at first date +if start date greater that recurrent date with a lower time timezone`, async ({ page }) => { + // --- setup --- +const appointmentTimezone = 'Etc/GMT-2'; + const schedulerTimezone = 'Etc/GMT+10'; + + await createWidget(page, 'dxScheduler', { + dataSource: [{ + allDay: false, + startDate: getAppointmentTime(new Date(2021, 3, 28, 4, 0, 0), appointmentTimezone), + startDateTimeZone: appointmentTimezone, + endDate: getAppointmentTime(new Date(2021, 3, 28, 6, 0, 0), appointmentTimezone), + endDateTimeZone: appointmentTimezone, + recurrenceRule: 'FREQ=MONTHLY;BYMONTHDAY=26', + text: 'Test', + }], + timeZone: schedulerTimezone, + currentView: 'week', + currentDate: new Date(2021, 3, 28), + startDayHour: 0, + cellDuration: 180, + width: 1000, + height: 585, + // --- test --- +// expected no visible date + await screenshotTestFunc(t, SCREENSHOT_BASE_NAME, 'greater-date__lower-timezone__same-view-date'); +}); +}); + +test(`Should correctly display the recurrent monthly appointment at next date +if start date greater that recurrent date with a lower time timezone`, async ({ page }) => { + // --- setup --- +const appointmentTimezone = 'Etc/GMT-2'; + const schedulerTimezone = 'Etc/GMT+10'; + + await createWidget(page, 'dxScheduler', { + dataSource: [{ + allDay: false, + startDate: getAppointmentTime(new Date(2021, 3, 28, 4, 0, 0), appointmentTimezone), + startDateTimeZone: appointmentTimezone, + endDate: getAppointmentTime(new Date(2021, 3, 28, 6, 0, 0), appointmentTimezone), + endDateTimeZone: appointmentTimezone, + recurrenceRule: 'FREQ=MONTHLY;BYMONTHDAY=26', + text: 'Test', + }], + timeZone: schedulerTimezone, + currentView: 'week', + currentDate: new Date(2021, 4, 28), + startDayHour: 0, + cellDuration: 180, + width: 1000, + height: 585, + // --- test --- +// expected date: 5/25/2021 4:00 PM - 6:00 PM + await screenshotTestFunc(t, SCREENSHOT_BASE_NAME, 'greater-date__lower-timezone__next-view-date'); +}); +}); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/timezone/recurrence/weeklyRecurrentAppointment.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/timezone/recurrence/weeklyRecurrentAppointment.spec.ts new file mode 100644 index 000000000000..667677675a84 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/timezone/recurrence/weeklyRecurrentAppointment.spec.ts @@ -0,0 +1,668 @@ +import { test, expect } from '@playwright/test'; +import { createWidget } from '../../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../../tests/container.html')}`; + +test.describe('Weekly recurrent appointments with timezones', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + +const SCREENSHOT_BASE_NAME = 'timezone-weekly-recurrent'; + +); + +// === One day in week tests section === + +test('Should correctly display the recurrent (one day at week) appointment with the same timezone', async ({ page }) => { + // expected date: 4/28/2021 10:00 AM - 12:00 PM + await screenshotTestFunc(t, SCREENSHOT_BASE_NAME, 'one-appointment__same-timezone'); +}); + +// TODO: .before() block not converted - move to test setup +// { + const appointmentTimezone = 'Etc/GMT+1'; + const schedulerTimezone = 'Etc/GMT+1'; + + await createWidget(page, 'dxScheduler', { + dataSource: [{ + allDay: false, + startDate: getAppointmentTime(new Date(2021, 3, 28, 10, 0, 0), appointmentTimezone), + startDateTimeZone: appointmentTimezone, + endDate: getAppointmentTime(new Date(2021, 3, 28, 12, 0, 0), appointmentTimezone), + endDateTimeZone: appointmentTimezone, + recurrenceRule: 'FREQ=WEEKLY;BYDAY=WE', + text: 'Test', + }], + timeZone: schedulerTimezone, + currentView: 'week', + currentDate: new Date(2021, 3, 28), + startDayHour: 0, + cellDuration: 180, + width: 1000, + height: 585, + }); +}); + +test('Should correctly display the recurrent (one day at week) morning appointment with the same timezone', async ({ page }) => { + // expected date: 4/28/2021 12:00 AM - 2:00 AM + await screenshotTestFunc(t, SCREENSHOT_BASE_NAME, 'one-morning-appointment__same-timezone'); +}); + +// TODO: .before() block not converted - move to test setup +// { + const appointmentTimezone = 'Etc/GMT+1'; + const schedulerTimezone = 'Etc/GMT+1'; + + await createWidget(page, 'dxScheduler', { + dataSource: [{ + allDay: false, + startDate: getAppointmentTime(new Date(2021, 3, 28, 0, 0, 0), appointmentTimezone), + startDateTimeZone: appointmentTimezone, + endDate: getAppointmentTime(new Date(2021, 3, 28, 2, 0, 0), appointmentTimezone), + endDateTimeZone: appointmentTimezone, + recurrenceRule: 'FREQ=WEEKLY;BYDAY=WE', + text: 'Test', + }], + timeZone: schedulerTimezone, + currentView: 'week', + currentDate: new Date(2021, 3, 28), + startDayHour: 0, + cellDuration: 180, + width: 1000, + height: 585, + }); +}); + +test('Should correctly display the recurrent (one day at week) evening appointment with the same timezone', async ({ page }) => { + // expected date: 4/28/2021 10:00 PM - 12:00 AM + await screenshotTestFunc(t, SCREENSHOT_BASE_NAME, 'one-evening-appointment__same-timezone'); +}); + +// TODO: .before() block not converted - move to test setup +// { + const appointmentTimezone = 'Etc/GMT+1'; + const schedulerTimezone = 'Etc/GMT+1'; + + await createWidget(page, 'dxScheduler', { + dataSource: [{ + allDay: false, + startDate: getAppointmentTime(new Date(2021, 3, 28, 22, 0, 0), appointmentTimezone), + startDateTimeZone: appointmentTimezone, + endDate: getAppointmentTime(new Date(2021, 3, 29, 0, 0, 0), appointmentTimezone), + endDateTimeZone: appointmentTimezone, + recurrenceRule: 'FREQ=WEEKLY;BYDAY=WE', + text: 'Test', + }], + timeZone: schedulerTimezone, + currentView: 'week', + currentDate: new Date(2021, 3, 28), + startDayHour: 0, + cellDuration: 180, + width: 1000, + height: 585, + }); +}); + +test(`Should correctly display the recurrent (one day at week) appointment +with a greater time timezone and day shift to the next day`, async ({ page }) => { + // expected date: 4/29/2021 10:00 AM - 12:00 PM + await screenshotTestFunc(t, SCREENSHOT_BASE_NAME, 'one-appointment__day-shift__greater-timezone'); +}); + +// TODO: .before() block not converted - move to test setup +// { + const appointmentTimezone = 'Etc/GMT+10'; + const schedulerTimezone = 'Etc/GMT-2'; + + await createWidget(page, 'dxScheduler', { + dataSource: [{ + allDay: false, + startDate: getAppointmentTime(new Date(2021, 3, 28, 22, 0, 0), appointmentTimezone), + startDateTimeZone: appointmentTimezone, + endDate: getAppointmentTime(new Date(2021, 3, 29, 0, 0, 0), appointmentTimezone), + endDateTimeZone: appointmentTimezone, + recurrenceRule: 'FREQ=WEEKLY;BYDAY=WE', + text: 'Test', + }], + timeZone: schedulerTimezone, + currentView: 'week', + currentDate: new Date(2021, 3, 28), + startDayHour: 0, + cellDuration: 180, + width: 1000, + height: 585, + }); +}); + +test('Should correctly display the recurrent (one day at week) appointment with a lower timezone and day shift to the previous day', async ({ page }) => { + // expected date: 4/27/2021 6:00 PM - 8:00 PM + await screenshotTestFunc(t, SCREENSHOT_BASE_NAME, 'one-appointment__day-shift__lower-timezone'); +}); + +// TODO: .before() block not converted - move to test setup +// { + const appointmentTimezone = 'Etc/GMT-10'; + const schedulerTimezone = 'Etc/GMT+2'; + + await createWidget(page, 'dxScheduler', { + dataSource: [{ + allDay: false, + startDate: getAppointmentTime(new Date(2021, 3, 28, 6, 0, 0), appointmentTimezone), + startDateTimeZone: appointmentTimezone, + endDate: getAppointmentTime(new Date(2021, 3, 28, 8, 0, 0), appointmentTimezone), + endDateTimeZone: appointmentTimezone, + recurrenceRule: 'FREQ=WEEKLY;BYDAY=WE', + text: 'Test', + }], + timeZone: schedulerTimezone, + currentView: 'week', + currentDate: new Date(2021, 3, 28), + startDayHour: 0, + cellDuration: 180, + width: 1000, + height: 585, + }); +}); + +test('Should correctly display the recurrent (one day at week) appointment with timezone week shift to the previous week', async ({ page }) => { + // expected date: 4/25/2021 6:00 AM - 8:00 AM + await screenshotTestFunc(t, SCREENSHOT_BASE_NAME, 'one-appointment__week-shift__lower-timezone'); +}); + +// TODO: .before() block not converted - move to test setup +// { + const appointmentTimezone = 'Etc/GMT-10'; + const schedulerTimezone = 'Etc/GMT+10'; + + await createWidget(page, 'dxScheduler', { + dataSource: [{ + allDay: false, + startDate: getAppointmentTime(new Date(2021, 3, 26, 2, 0, 0), appointmentTimezone), + startDateTimeZone: appointmentTimezone, + endDate: getAppointmentTime(new Date(2021, 3, 26, 4, 0, 0), appointmentTimezone), + endDateTimeZone: appointmentTimezone, + recurrenceRule: 'FREQ=WEEKLY;BYDAY=MO', + text: 'Test', + }], + timeZone: schedulerTimezone, + currentView: 'week', + currentDate: new Date(2021, 3, 28), + startDayHour: 0, + cellDuration: 180, + width: 1000, + height: 585, + }); +}); + +test('Should correctly display the recurrent (one day at week) appointment with timezone week shift to the next week', async ({ page }) => { + // expected date: 4/25/2021 4:00 PM - 6:00 PM + await screenshotTestFunc(t, SCREENSHOT_BASE_NAME, 'one-appointment__week-shift__greater-timezone'); +}); + +// TODO: .before() block not converted - move to test setup +// { + const appointmentTimezone = 'Etc/GMT+10'; + const schedulerTimezone = 'Etc/GMT-10'; + + await createWidget(page, 'dxScheduler', { + dataSource: [{ + allDay: false, + startDate: getAppointmentTime(new Date(2021, 3, 25, 20, 0, 0), appointmentTimezone), + startDateTimeZone: appointmentTimezone, + endDate: getAppointmentTime(new Date(2021, 3, 25, 22, 0, 0), appointmentTimezone), + endDateTimeZone: appointmentTimezone, + recurrenceRule: 'FREQ=WEEKLY;BYDAY=SU', + text: 'Test', + }], + timeZone: schedulerTimezone, + currentView: 'week', + currentDate: new Date(2021, 3, 28), + startDayHour: 0, + cellDuration: 180, + width: 1000, + height: 585, + }); +}); + +test(`Should correctly display the recurrent (one day at week) appointment +with timezone view period shift to the next view period at the first week`, async ({ page }) => { + // expected no visible date + await screenshotTestFunc(t, SCREENSHOT_BASE_NAME, 'one-appointment__next-view-shift__first-week'); +}); + +// TODO: .before() block not converted - move to test setup +// { + const appointmentTimezone = 'Etc/GMT+10'; + const schedulerTimezone = 'Etc/GMT-10'; + + await createWidget(page, 'dxScheduler', { + dataSource: [{ + allDay: false, + startDate: getAppointmentTime(new Date(2021, 4, 1, 20, 0, 0), appointmentTimezone), + startDateTimeZone: appointmentTimezone, + endDate: getAppointmentTime(new Date(2021, 4, 1, 22, 0, 0), appointmentTimezone), + endDateTimeZone: appointmentTimezone, + recurrenceRule: 'FREQ=WEEKLY;BYDAY=SA', + text: 'Test', + }], + timeZone: schedulerTimezone, + currentView: 'week', + currentDate: new Date(2021, 3, 28), + startDayHour: 0, + cellDuration: 180, + width: 1000, + height: 585, + }); +}); + +test(`Should correctly display the recurrent (one day at week) appointment +with timezone view period shift to the next view period at the second week`, async ({ page }) => { + // expected date: 5/2/2021 4:00 PM - 6:00 PM + await screenshotTestFunc(t, SCREENSHOT_BASE_NAME, 'one-appointment__next-view-shift__second-week'); +}); + +// TODO: .before() block not converted - move to test setup +// { + const appointmentTimezone = 'Etc/GMT+10'; + const schedulerTimezone = 'Etc/GMT-10'; + + await createWidget(page, 'dxScheduler', { + dataSource: [{ + allDay: false, + startDate: getAppointmentTime(new Date(2021, 4, 1, 20, 0, 0), appointmentTimezone), + startDateTimeZone: appointmentTimezone, + endDate: getAppointmentTime(new Date(2021, 4, 1, 22, 0, 0), appointmentTimezone), + endDateTimeZone: appointmentTimezone, + recurrenceRule: 'FREQ=WEEKLY;BYDAY=SA', + text: 'Test', + }], + timeZone: schedulerTimezone, + currentView: 'week', + currentDate: new Date(2021, 4, 5), + startDayHour: 0, + cellDuration: 180, + width: 1000, + height: 585, + }); +}); + +test(`Should correctly display the recurrent (one day at week) appointment +with timezone view period shift to the previous view period at the first week`, async ({ page }) => { + // expected date: 5/1/2021 6:00 AM - 8:00 AM + await screenshotTestFunc(t, SCREENSHOT_BASE_NAME, 'one-appointment__previous-view-shift__first-week'); +}); + +// TODO: .before() block not converted - move to test setup +// { + const appointmentTimezone = 'Etc/GMT-10'; + const schedulerTimezone = 'Etc/GMT+10'; + + await createWidget(page, 'dxScheduler', { + dataSource: [{ + allDay: false, + startDate: getAppointmentTime(new Date(2021, 3, 25, 2, 0, 0), appointmentTimezone), + startDateTimeZone: appointmentTimezone, + endDate: getAppointmentTime(new Date(2021, 3, 25, 4, 0, 0), appointmentTimezone), + endDateTimeZone: appointmentTimezone, + recurrenceRule: 'FREQ=WEEKLY;BYDAY=SU', + text: 'Test', + }], + timeZone: schedulerTimezone, + currentView: 'week', + currentDate: new Date(2021, 3, 28), + startDayHour: 0, + cellDuration: 180, + width: 1000, + height: 585, + }); +}); + +test(`Should correctly display the recurrent (one day at week) appointment +with timezone view period shift to the previous view period at the second week`, async ({ page }) => { + // expected date: 4/24/2021 6:00 AM - 8:00 AM + await screenshotTestFunc(t, SCREENSHOT_BASE_NAME, 'one-appointment__previous-view-shift__before-week'); +}); + +// TODO: .before() block not converted - move to test setup +// { + const appointmentTimezone = 'Etc/GMT-10'; + const schedulerTimezone = 'Etc/GMT+10'; + + await createWidget(page, 'dxScheduler', { + dataSource: [{ + allDay: false, + startDate: getAppointmentTime(new Date(2021, 3, 25, 2, 0, 0), appointmentTimezone), + startDateTimeZone: appointmentTimezone, + endDate: getAppointmentTime(new Date(2021, 3, 25, 4, 0, 0), appointmentTimezone), + endDateTimeZone: appointmentTimezone, + recurrenceRule: 'FREQ=WEEKLY;BYDAY=SU', + text: 'Test', + }], + timeZone: schedulerTimezone, + currentView: 'week', + currentDate: new Date(2021, 3, 21), + startDayHour: 0, + cellDuration: 180, + width: 1000, + height: 585, + }); +}); + +// === multiple day in week tests section === + +test('Should correctly display recurrent appointment with multiple day in week on the first week in same timezone', async ({ page }) => { + // --- setup --- +const appointmentTimezone = 'Etc/GMT+1'; + const schedulerTimezone = 'Etc/GMT+1'; + + await createWidget(page, 'dxScheduler', { + dataSource: [{ + allDay: false, + startDate: getAppointmentTime(new Date(2021, 3, 28, 10, 0, 0), appointmentTimezone), + startDateTimeZone: appointmentTimezone, + endDate: getAppointmentTime(new Date(2021, 3, 28, 14, 0, 0), appointmentTimezone), + endDateTimeZone: appointmentTimezone, + recurrenceRule: 'FREQ=WEEKLY;BYDAY=TU,WE,TH', + text: 'Test', + }], + timeZone: schedulerTimezone, + currentView: 'week', + currentDate: new Date(2021, 3, 28), + startDayHour: 0, + cellDuration: 180, + width: 1000, + height: 585, + // --- test --- +// expected dates: + // 4/28/2021 10:00 AM - 2:00 AM + // 4/29/2021 10:00 AM - 2:00 AM + await screenshotTestFunc(t, SCREENSHOT_BASE_NAME, 'multiple-appointment__first-week__same-timezone'); +}); +}); + +test('Should correctly display recurrent appointment with multiple day in week on the second week in same timezone', async ({ page }) => { + // --- setup --- +const appointmentTimezone = 'Etc/GMT+1'; + const schedulerTimezone = 'Etc/GMT+1'; + + await createWidget(page, 'dxScheduler', { + dataSource: [{ + allDay: false, + startDate: getAppointmentTime(new Date(2021, 3, 28, 10, 0, 0), appointmentTimezone), + startDateTimeZone: appointmentTimezone, + endDate: getAppointmentTime(new Date(2021, 3, 28, 14, 0, 0), appointmentTimezone), + endDateTimeZone: appointmentTimezone, + recurrenceRule: 'FREQ=WEEKLY;BYDAY=TU,WE,TH', + text: 'Test', + }], + timeZone: schedulerTimezone, + currentView: 'week', + currentDate: new Date(2021, 4, 5), + startDayHour: 0, + cellDuration: 180, + width: 1000, + height: 585, + // --- test --- +// expected dates: + // 5/4/2021 10:00 AM - 2:00 AM + // 5/5/2021 10:00 AM - 2:00 AM + // 5/6/2021 10:00 AM - 2:00 AM + await screenshotTestFunc(t, SCREENSHOT_BASE_NAME, 'multiple-appointment__second-week__same-timezone'); +}); +}); + +test('Should correctly display recurrent appointment with multiple day in week on the first week in a greater time timezone', async ({ page }) => { + // --- setup --- +const appointmentTimezone = 'Etc/GMT+10'; + const timezone = 'Etc/GMT-5'; + + await createWidget(page, 'dxScheduler', { + dataSource: [{ + allDay: false, + startDate: getAppointmentTime(new Date(2021, 3, 28, 10, 0, 0), appointmentTimezone), + startDateTimeZone: appointmentTimezone, + endDate: getAppointmentTime(new Date(2021, 3, 28, 14, 0, 0), appointmentTimezone), + endDateTimeZone: appointmentTimezone, + recurrenceRule: 'FREQ=WEEKLY;BYDAY=TU,WE,TH', + text: 'Test', + }], + timeZone: timezone, + currentView: 'week', + currentDate: new Date(2021, 3, 28), + startDayHour: 0, + cellDuration: 180, + width: 1000, + height: 585, + // --- test --- +// expected dates: + // 4/29/2021 1:00 AM - 5:00 AM + // 4/30/2021 1:00 AM - 5:00 AM + await screenshotTestFunc(t, SCREENSHOT_BASE_NAME, 'multiple-appointment__first-week__greater-timezone'); +}); +}); + +test('Should correctly display recurrent appointment with multiple day in week on the second week in a greater time timezone', async ({ page }) => { + // --- setup --- +const appointmentTimezone = 'Etc/GMT+10'; + const timezone = 'Etc/GMT-5'; + + await createWidget(page, 'dxScheduler', { + dataSource: [{ + allDay: false, + startDate: getAppointmentTime(new Date(2021, 3, 28, 10, 0, 0), appointmentTimezone), + startDateTimeZone: appointmentTimezone, + endDate: getAppointmentTime(new Date(2021, 3, 28, 14, 0, 0), appointmentTimezone), + endDateTimeZone: appointmentTimezone, + recurrenceRule: 'FREQ=WEEKLY;BYDAY=TU,WE,TH', + text: 'Test', + }], + timeZone: timezone, + currentView: 'week', + currentDate: new Date(2021, 4, 5), + startDayHour: 0, + cellDuration: 180, + width: 1000, + height: 585, + // --- test --- +// expected dates: + // 5/5/2021 1:00 AM - 5:00 AM + // 5/6/2021 1:00 AM - 5:00 AM + // 5/7/2021 1:00 AM - 5:00 AM + await screenshotTestFunc(t, SCREENSHOT_BASE_NAME, 'multiple-appointment__second-week__greater-timezone'); +}); +}); + +test('Should correctly display recurrent appointment with multiple day in week on the first week in a lower time timezone', async ({ page }) => { + // --- setup --- +const appointmentTimezone = 'Etc/GMT-10'; + const timezone = 'Etc/GMT+5'; + + await createWidget(page, 'dxScheduler', { + dataSource: [{ + allDay: false, + startDate: getAppointmentTime(new Date(2021, 3, 28, 10, 0, 0), appointmentTimezone), + startDateTimeZone: appointmentTimezone, + endDate: getAppointmentTime(new Date(2021, 3, 28, 14, 0, 0), appointmentTimezone), + endDateTimeZone: appointmentTimezone, + recurrenceRule: 'FREQ=WEEKLY;BYDAY=TU,WE,TH', + text: 'Test', + }], + timeZone: timezone, + currentView: 'week', + currentDate: new Date(2021, 3, 28), + startDayHour: 0, + cellDuration: 180, + width: 1000, + height: 585, + // --- test --- +// expected dates: + // 4/27/2021 7:00 PM - 11:00 PM + // 4/28/2021 7:00 PM - 11:00 PM + await screenshotTestFunc(t, SCREENSHOT_BASE_NAME, 'multiple-appointment__first-week__lower-timezone'); +}); +}); + +test('Should correctly display recurrent appointment with multiple day in week on the second week in a lower time timezone', async ({ page }) => { + // --- setup --- +const appointmentTimezone = 'Etc/GMT-10'; + const timezone = 'Etc/GMT+5'; + + await createWidget(page, 'dxScheduler', { + dataSource: [{ + allDay: false, + startDate: getAppointmentTime(new Date(2021, 3, 28, 10, 0, 0), appointmentTimezone), + startDateTimeZone: appointmentTimezone, + endDate: getAppointmentTime(new Date(2021, 3, 28, 14, 0, 0), appointmentTimezone), + endDateTimeZone: appointmentTimezone, + recurrenceRule: 'FREQ=WEEKLY;BYDAY=TU,WE,TH', + text: 'Test', + }], + timeZone: timezone, + currentView: 'week', + currentDate: new Date(2021, 4, 5), + startDayHour: 0, + cellDuration: 180, + width: 1000, + height: 585, + // --- test --- +// expected dates: + // 5/3/2021 7:00 PM - 11:00 PM + // 5/4/2021 7:00 PM - 11:00 PM + // 5/5/2021 7:00 PM - 11:00 PM + await screenshotTestFunc(t, SCREENSHOT_BASE_NAME, 'multiple-appointment__second-week__lower-timezone'); +}); +}); + +// === maximum timezone offset tests section === + +test(`Should correctly display recurrent appointment with multiple day in week + on the first week with maximum positive timezone offset`, async ({ page }) => { + // --- setup --- +const appointmentTimezone = 'Etc/GMT+12'; + const timezone = 'Etc/GMT-14'; + + await createWidget(page, 'dxScheduler', { + dataSource: [{ + allDay: false, + startDate: getAppointmentTime(new Date(2021, 3, 29, 22, 0, 0), appointmentTimezone), + startDateTimeZone: appointmentTimezone, + endDate: getAppointmentTime(new Date(2021, 3, 30, 0, 0, 0), appointmentTimezone), + endDateTimeZone: appointmentTimezone, + recurrenceRule: 'FREQ=WEEKLY;BYDAY=WE,TH, FT', + text: 'Test', + }], + timeZone: timezone, + currentView: 'week', + currentDate: new Date(2021, 3, 28), + startDayHour: 0, + cellDuration: 180, + width: 1000, + height: 585, + // --- test --- +// expected date: 5/1/2021 12:00 AM - 2:00 AM + await screenshotTestFunc(t, SCREENSHOT_BASE_NAME, 'one-appointment__first-week__max-positive-timezone-offset'); +}); +}); + +test(`Should correctly display recurrent appointment with multiple day in week + on the first week with maximum positive timezone offset`, async ({ page }) => { + // --- setup --- +const appointmentTimezone = 'Etc/GMT+12'; + const timezone = 'Etc/GMT-14'; + + await createWidget(page, 'dxScheduler', { + dataSource: [{ + allDay: false, + startDate: getAppointmentTime(new Date(2021, 3, 29, 22, 0, 0), appointmentTimezone), + startDateTimeZone: appointmentTimezone, + endDate: getAppointmentTime(new Date(2021, 3, 30, 0, 0, 0), appointmentTimezone), + endDateTimeZone: appointmentTimezone, + recurrenceRule: 'FREQ=WEEKLY;BYDAY=TU,WE,TH', + text: 'Test', + }], + timeZone: timezone, + currentView: 'week', + currentDate: new Date(2021, 4, 5), + startDayHour: 0, + cellDuration: 180, + width: 1000, + height: 585, + // --- test --- +// expected dates: + // 5/6/2021 12:00 AM - 2:00 AM + // 5/7/2021 12:00 AM - 2:00 AM + // 5/8/2021 12:00 AM - 2:00 AM + await screenshotTestFunc(t, SCREENSHOT_BASE_NAME, 'one-appointment__second-week__max-positive-timezone-offset'); +}); +}); + +test(`Should correctly display recurrent appointment with multiple day in week + on the first week with maximum negative timezone offset`, async ({ page }) => { + // --- setup --- +const appointmentTimezone = 'Etc/GMT-14'; + const timezone = 'Etc/GMT+12'; + + await createWidget(page, 'dxScheduler', { + dataSource: [{ + allDay: false, + startDate: getAppointmentTime(new Date(2021, 3, 28, 0, 0, 0), appointmentTimezone), + startDateTimeZone: appointmentTimezone, + endDate: getAppointmentTime(new Date(2021, 3, 28, 2, 0, 0), appointmentTimezone), + endDateTimeZone: appointmentTimezone, + recurrenceRule: 'FREQ=WEEKLY;BYDAY=MO,TU,WE', + text: 'Test', + }], + timeZone: timezone, + currentView: 'week', + currentDate: new Date(2021, 3, 28), + startDayHour: 0, + cellDuration: 180, + width: 1000, + height: 585, + // --- test --- +// expected dates: + // 4/26/2021 10:00 PM - 12:00 AM + // 5/1/2021 10:00 PM - 12:00 AM + await screenshotTestFunc(t, SCREENSHOT_BASE_NAME, 'one-appointment__first-week__max-negative-timezone-offset'); +}); +}); + +test(`Should correctly display recurrent appointment with multiple day in week + on the first week with maximum negative timezone offset`, async ({ page }) => { + // --- setup --- +const appointmentTimezone = 'Etc/GMT-14'; + const timezone = 'Etc/GMT+12'; + + await createWidget(page, 'dxScheduler', { + dataSource: [{ + allDay: false, + startDate: getAppointmentTime(new Date(2021, 3, 28, 0, 0, 0), appointmentTimezone), + startDateTimeZone: appointmentTimezone, + endDate: getAppointmentTime(new Date(2021, 3, 28, 2, 0, 0), appointmentTimezone), + endDateTimeZone: appointmentTimezone, + recurrenceRule: 'FREQ=WEEKLY;BYDAY=MO,TU,WE', + text: 'Test', + }], + timeZone: timezone, + currentView: 'week', + currentDate: new Date(2021, 4, 5), + startDayHour: 0, + cellDuration: 180, + width: 1000, + height: 585, + // --- test --- +// expected dates: + // 5/2/2021 10:00 PM - 12:00 AM + // 5/3/2021 10:00 PM - 12:00 AM + // 5/8/2021 10:00 PM - 12:00 AM + await screenshotTestFunc(t, SCREENSHOT_BASE_NAME, 'one-appointment__second-week__max-negative-timezone-offset'); +}); +}); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/timezone/recurrence/yearlyRecurrentAppointment.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/timezone/recurrence/yearlyRecurrentAppointment.spec.ts new file mode 100644 index 000000000000..b1aabd014ff1 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/timezone/recurrence/yearlyRecurrentAppointment.spec.ts @@ -0,0 +1,365 @@ +import { test, expect } from '@playwright/test'; +import { createWidget } from '../../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../../tests/container.html')}`; + +test.describe('Yearly recurrent appointments with timezones', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + +const SCREENSHOT_BASE_NAME = 'timezone-yearly-recurrent'; + +); + +test('Should correctly display the recurrent yearly appointment with the same timezone', async ({ page }) => { + // --- setup --- +const appointmentTimezone = 'Etc/GMT+1'; + const schedulerTimezone = 'Etc/GMT+1'; + + await createWidget(page, 'dxScheduler', { + dataSource: [{ + allDay: false, + startDate: getAppointmentTime(new Date(2021, 3, 28, 10, 0, 0), appointmentTimezone), + startDateTimeZone: appointmentTimezone, + endDate: getAppointmentTime(new Date(2021, 3, 28, 12, 0, 0), appointmentTimezone), + endDateTimeZone: appointmentTimezone, + recurrenceRule: 'FREQ=YEARLY;BYMONTHDAY=28;BYMONTH=4', + text: 'Test', + }], + timeZone: schedulerTimezone, + currentView: 'week', + currentDate: new Date(2021, 3, 28), + startDayHour: 0, + cellDuration: 180, + width: 1000, + height: 585, + // --- test --- +// expected date: 4/28/2021 10:00 AM - 12:00 PM + await screenshotTestFunc(t, SCREENSHOT_BASE_NAME, 'same-date__same-timezone'); +}); +}); + +test('Should correctly display the recurrent yearly appointment with a greater time timezone', async ({ page }) => { + // --- setup --- +const appointmentTimezone = 'Etc/GMT+10'; + const schedulerTimezone = 'Etc/GMT-2'; + + await createWidget(page, 'dxScheduler', { + dataSource: [{ + allDay: false, + startDate: getAppointmentTime(new Date(2021, 3, 28, 14, 0, 0), appointmentTimezone), + startDateTimeZone: appointmentTimezone, + endDate: getAppointmentTime(new Date(2021, 3, 28, 16, 0, 0), appointmentTimezone), + endDateTimeZone: appointmentTimezone, + recurrenceRule: 'FREQ=YEARLY;BYMONTHDAY=28;BYMONTH=4', + text: 'Test', + }], + timeZone: schedulerTimezone, + currentView: 'week', + currentDate: new Date(2021, 3, 28), + startDayHour: 0, + cellDuration: 180, + width: 1000, + height: 585, + // --- test --- +// expected date: 4/29/2021 2:00 AM - 4:00 AM + await screenshotTestFunc(t, SCREENSHOT_BASE_NAME, 'same-date__greater-timezone'); +}); +}); + +test('Should correctly display the recurrent yearly appointment with a lower time timezone', async ({ page }) => { + // --- setup --- +const appointmentTimezone = 'Etc/GMT-2'; + const schedulerTimezone = 'Etc/GMT+10'; + + await createWidget(page, 'dxScheduler', { + dataSource: [{ + allDay: false, + startDate: getAppointmentTime(new Date(2021, 3, 28, 4, 0, 0), appointmentTimezone), + startDateTimeZone: appointmentTimezone, + endDate: getAppointmentTime(new Date(2021, 3, 28, 6, 0, 0), appointmentTimezone), + endDateTimeZone: appointmentTimezone, + recurrenceRule: 'FREQ=YEARLY;BYMONTHDAY=28;BYMONTH=4', + text: 'Test', + }], + timeZone: schedulerTimezone, + currentView: 'week', + currentDate: new Date(2021, 3, 28), + startDayHour: 0, + cellDuration: 180, + width: 1000, + height: 585, + // --- test --- +// expected date: 4/27/2021 2:00 PM - 4:00 PM + await screenshotTestFunc(t, SCREENSHOT_BASE_NAME, 'same-date__lower-timezone'); +}); +}); + +test(`Should correctly display the recurrent yearly appointment if start date +lower than recurrent date with the same timezone`, async ({ page }) => { + // --- setup --- +const appointmentTimezone = 'Etc/GMT+1'; + const schedulerTimezone = 'Etc/GMT+1'; + + await createWidget(page, 'dxScheduler', { + dataSource: [{ + allDay: false, + startDate: getAppointmentTime(new Date(2021, 3, 26, 10, 0, 0), appointmentTimezone), + startDateTimeZone: appointmentTimezone, + endDate: getAppointmentTime(new Date(2021, 3, 26, 12, 0, 0), appointmentTimezone), + endDateTimeZone: appointmentTimezone, + recurrenceRule: 'FREQ=YEARLY;BYMONTHDAY=28;BYMONTH=4', + text: 'Test', + }], + timeZone: schedulerTimezone, + currentView: 'week', + currentDate: new Date(2021, 3, 28), + startDayHour: 0, + cellDuration: 180, + width: 1000, + height: 585, + // --- test --- +// expected date: 4/28/2021 10:00 AM - 12:00 PM + await screenshotTestFunc(t, SCREENSHOT_BASE_NAME, 'lower-date__same-timezone'); +}); +}); + +test(`Should correctly display the recurrent yearly appointment if start date +lower than recurrent date with a greater time timezone`, async ({ page }) => { + // --- setup --- +const appointmentTimezone = 'Etc/GMT+10'; + const schedulerTimezone = 'Etc/GMT-2'; + + await createWidget(page, 'dxScheduler', { + dataSource: [{ + allDay: false, + startDate: getAppointmentTime(new Date(2021, 3, 26, 14, 0, 0), appointmentTimezone), + startDateTimeZone: appointmentTimezone, + endDate: getAppointmentTime(new Date(2021, 3, 26, 16, 0, 0), appointmentTimezone), + endDateTimeZone: appointmentTimezone, + recurrenceRule: 'FREQ=YEARLY;BYMONTHDAY=28;BYMONTH=4', + text: 'Test', + }], + timeZone: schedulerTimezone, + currentView: 'week', + currentDate: new Date(2021, 3, 28), + startDayHour: 0, + cellDuration: 180, + width: 1000, + height: 585, + // --- test --- +// expected date: 4/29/2021 2:00 AM - 4:00 PM + await screenshotTestFunc(t, SCREENSHOT_BASE_NAME, 'lower-date__greater-timezone'); +}); +}); + +test(`Should correctly display the recurrent yearly appointment if start date +lower than recurrent date with a lower time timezone`, async ({ page }) => { + // --- setup --- +const appointmentTimezone = 'Etc/GMT-2'; + const schedulerTimezone = 'Etc/GMT+10'; + + await createWidget(page, 'dxScheduler', { + dataSource: [{ + allDay: false, + startDate: getAppointmentTime(new Date(2021, 3, 26, 4, 0, 0), appointmentTimezone), + startDateTimeZone: appointmentTimezone, + endDate: getAppointmentTime(new Date(2021, 3, 26, 6, 0, 0), appointmentTimezone), + endDateTimeZone: appointmentTimezone, + recurrenceRule: 'FREQ=YEARLY;BYMONTHDAY=28;BYMONTH=4', + text: 'Test', + }], + timeZone: schedulerTimezone, + currentView: 'week', + currentDate: new Date(2021, 3, 28), + startDayHour: 0, + cellDuration: 180, + width: 1000, + height: 585, + // --- test --- +// expected date: 4/27/2021 4:00 PM - 6:00 PM + await screenshotTestFunc(t, SCREENSHOT_BASE_NAME, 'lower-date__lower-timezone'); +}); +}); + +test(`Should correctly display the recurrent yearly appointment at first date if start date +greater than recurrent date with the same timezone`, async ({ page }) => { + // --- setup --- +const appointmentTimezone = 'Etc/GMT+1'; + const schedulerTimezone = 'Etc/GMT+1'; + + await createWidget(page, 'dxScheduler', { + dataSource: [{ + allDay: false, + startDate: getAppointmentTime(new Date(2021, 3, 29, 10, 0, 0), appointmentTimezone), + startDateTimeZone: appointmentTimezone, + endDate: getAppointmentTime(new Date(2021, 3, 29, 12, 0, 0), appointmentTimezone), + endDateTimeZone: appointmentTimezone, + recurrenceRule: 'FREQ=YEARLY;BYMONTHDAY=28;BYMONTH=4', + text: 'Test', + }], + timeZone: schedulerTimezone, + currentView: 'week', + currentDate: new Date(2021, 3, 28), + startDayHour: 0, + cellDuration: 180, + width: 1000, + height: 585, + // --- test --- +// expected no visible date + await screenshotTestFunc(t, SCREENSHOT_BASE_NAME, 'greater-date__same-timezone__same-view-date'); +}); +}); + +test(`Should correctly display the recurrent yearly appointment at next date if start date +greater than recurrent date with the same timezone`, async ({ page }) => { + // --- setup --- +const appointmentTimezone = 'Etc/GMT+1'; + const schedulerTimezone = 'Etc/GMT+1'; + + await createWidget(page, 'dxScheduler', { + dataSource: [{ + allDay: false, + startDate: getAppointmentTime(new Date(2021, 3, 29, 10, 0, 0), appointmentTimezone), + startDateTimeZone: appointmentTimezone, + endDate: getAppointmentTime(new Date(2021, 3, 29, 12, 0, 0), appointmentTimezone), + endDateTimeZone: appointmentTimezone, + recurrenceRule: 'FREQ=YEARLY;BYMONTHDAY=28;BYMONTH=4', + text: 'Test', + }], + timeZone: schedulerTimezone, + currentView: 'week', + currentDate: new Date(2022, 3, 28), + startDayHour: 0, + cellDuration: 180, + width: 1000, + height: 585, + // --- test --- +// expected date: 4/28/2022 10:00 AM - 12:00 PM + await screenshotTestFunc(t, SCREENSHOT_BASE_NAME, 'greater-date__same-timezone__next-view-date'); +}); +}); + +test(`Should correctly display the recurrent yearly appointment at first date if start date +greater than recurrent date with a greater time timezone`, async ({ page }) => { + // --- setup --- +const appointmentTimezone = 'Etc/GMT+10'; + const schedulerTimezone = 'Etc/GMT-2'; + + await createWidget(page, 'dxScheduler', { + dataSource: [{ + allDay: false, + startDate: getAppointmentTime(new Date(2021, 3, 29, 14, 0, 0), appointmentTimezone), + startDateTimeZone: appointmentTimezone, + endDate: getAppointmentTime(new Date(2021, 3, 29, 16, 0, 0), appointmentTimezone), + endDateTimeZone: appointmentTimezone, + recurrenceRule: 'FREQ=YEARLY;BYMONTHDAY=28;BYMONTH=4', + text: 'Test', + }], + timeZone: schedulerTimezone, + currentView: 'week', + currentDate: new Date(2021, 3, 28), + startDayHour: 0, + cellDuration: 180, + width: 1000, + height: 585, + // --- test --- +// expected no visible date + await screenshotTestFunc(t, SCREENSHOT_BASE_NAME, 'greater-date__greater-timezone__same-view-date'); +}); +}); + +test(`Should correctly display the recurrent yearly appointment at next date if start date +greater than recurrent date with a greater time timezone`, async ({ page }) => { + // --- setup --- +const appointmentTimezone = 'Etc/GMT+10'; + const schedulerTimezone = 'Etc/GMT-2'; + + await createWidget(page, 'dxScheduler', { + dataSource: [{ + allDay: false, + startDate: getAppointmentTime(new Date(2021, 3, 29, 14, 0, 0), appointmentTimezone), + startDateTimeZone: appointmentTimezone, + endDate: getAppointmentTime(new Date(2021, 3, 29, 16, 0, 0), appointmentTimezone), + endDateTimeZone: appointmentTimezone, + recurrenceRule: 'FREQ=YEARLY;BYMONTHDAY=28;BYMONTH=4', + text: 'Test', + }], + timeZone: schedulerTimezone, + currentView: 'week', + currentDate: new Date(2022, 3, 28), + startDayHour: 0, + cellDuration: 180, + width: 1000, + height: 585, + // --- test --- +// expected date: 4/29/2022 2:00 AM - 4:00 PM + await screenshotTestFunc(t, SCREENSHOT_BASE_NAME, 'greater-date__greater-timezone__next-view-date'); +}); +}); + +test(`Should correctly display the recurrent yearly appointment at first date if start date +greater than recurrent date with a lower time timezone`, async ({ page }) => { + // --- setup --- +const appointmentTimezone = 'Etc/GMT-2'; + const schedulerTimezone = 'Etc/GMT+10'; + + await createWidget(page, 'dxScheduler', { + dataSource: [{ + allDay: false, + startDate: getAppointmentTime(new Date(2021, 3, 29, 4, 0, 0), appointmentTimezone), + startDateTimeZone: appointmentTimezone, + endDate: getAppointmentTime(new Date(2021, 3, 29, 6, 0, 0), appointmentTimezone), + endDateTimeZone: appointmentTimezone, + recurrenceRule: 'FREQ=YEARLY;BYMONTHDAY=28;BYMONTH=4', + text: 'Test', + }], + timeZone: schedulerTimezone, + currentView: 'week', + currentDate: new Date(2021, 3, 28), + startDayHour: 0, + cellDuration: 180, + width: 1000, + height: 585, + // --- test --- +// expected no visible date + await screenshotTestFunc(t, SCREENSHOT_BASE_NAME, 'greater-date__lower-timezone__same-view-date'); +}); +}); + +test(`Should correctly display the recurrent yearly appointment at next date if start date +greater than recurrent date with a lower time timezone`, async ({ page }) => { + // --- setup --- +const appointmentTimezone = 'Etc/GMT-2'; + const schedulerTimezone = 'Etc/GMT+10'; + + await createWidget(page, 'dxScheduler', { + dataSource: [{ + allDay: false, + startDate: getAppointmentTime(new Date(2021, 3, 29, 4, 0, 0), appointmentTimezone), + startDateTimeZone: appointmentTimezone, + endDate: getAppointmentTime(new Date(2021, 3, 29, 6, 0, 0), appointmentTimezone), + endDateTimeZone: appointmentTimezone, + recurrenceRule: 'FREQ=YEARLY;BYMONTHDAY=28;BYMONTH=4', + text: 'Test', + }], + timeZone: schedulerTimezone, + currentView: 'week', + currentDate: new Date(2022, 3, 28), + startDayHour: 0, + cellDuration: 180, + width: 1000, + height: 585, + // --- test --- +// expected date: 4/27/2022 4:00 PM - 6:00 PM + await screenshotTestFunc(t, SCREENSHOT_BASE_NAME, 'greater-date__lower-timezone__next-view-date'); +}); +}); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/tooltipBehaviour/hideTooltip.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/tooltipBehaviour/hideTooltip.spec.ts new file mode 100644 index 000000000000..bd3e230a69ea --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/tooltipBehaviour/hideTooltip.spec.ts @@ -0,0 +1,35 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, getContainerUrl, setupTestPage } from '../../../../playwright-helpers'; + +const containerUrl = getContainerUrl(__dirname, '../../../../tests/container.html'); + +test.describe('Hide tooltip', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + test('Appointment tooltip should be hidden when drag is started', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + views: ['day'], + currentDate: new Date(2021, 3, 26), + startDayHour: 9, + height: 600, + dataSource: [{ + text: 'Test', + startDate: new Date(2021, 3, 26, 9), + endDate: new Date(2021, 3, 26, 9, 30), + }], + }); + + const appointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'Test' }); + await appointment.click(); + + const tooltip = page.locator('.dx-scheduler-appointment-tooltip'); + await expect(tooltip).toBeVisible(); + + const targetCell = page.locator('.dx-scheduler-date-table-row').nth(4).locator('.dx-scheduler-date-table-cell').nth(0); + await appointment.dragTo(targetCell); + + await expect(tooltip).not.toBeVisible(); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/tooltipBehaviour/tooltipBehavior.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/tooltipBehaviour/tooltipBehavior.spec.ts new file mode 100644 index 000000000000..961864026c36 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/tooltipBehaviour/tooltipBehavior.spec.ts @@ -0,0 +1,139 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, getContainerUrl, setupTestPage } from '../../../../playwright-helpers'; + +const containerUrl = getContainerUrl(__dirname, '../../../../tests/container.html'); + +const tooltipDataSource = [{ + text: 'Brochure Design Review', + startDate: new Date(2019, 3, 1, 10, 0), + endDate: new Date(2019, 3, 1, 12, 0), +}]; + +const defaultSchedulerOptions = { + views: ['day'], + dataSource: [], + width: 600, + height: 600, + startDayHour: 9, + firstDayOfWeek: 1, + maxAppointmentsPerCell: 5, + currentView: 'day', + currentDate: new Date(2019, 3, 1), +}; + +test.describe('Appointment tooltip behavior during scrolling in the Scheduler (T755449)', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + test('The tooltip of collector should not scroll page and immediately hide', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + ...defaultSchedulerOptions, + views: [{ + type: 'week', + name: 'week', + maxAppointmentsPerCell: '0', + }], + currentDate: new Date(2017, 4, 25), + startDayHour: 9, + currentView: 'week', + dataSource: [ + { text: 'A', startDate: new Date(2017, 4, 22, 9, 30), endDate: new Date(2017, 4, 22, 11, 30) }, + { text: 'B', startDate: new Date(2017, 4, 22, 9, 30), endDate: new Date(2017, 4, 22, 11, 30) }, + { text: 'C', startDate: new Date(2017, 4, 22, 9, 30), endDate: new Date(2017, 4, 22, 11, 30) }, + { text: 'D', startDate: new Date(2017, 4, 22, 9, 30), endDate: new Date(2017, 4, 22, 11, 30) }, + { text: 'E', startDate: new Date(2017, 4, 22, 9, 30), endDate: new Date(2017, 4, 22, 11, 30) }, + { text: 'F', startDate: new Date(2017, 4, 22, 9, 30), endDate: new Date(2017, 4, 22, 11, 30) }, + { text: 'G', startDate: new Date(2017, 4, 22, 9, 30), endDate: new Date(2017, 4, 22, 11, 30) }, + ], + }); + + const collector = page.locator('.dx-scheduler-appointment-collector').filter({ hasText: '7' }); + await collector.click(); + + const tooltip = page.locator('.dx-scheduler-appointment-tooltip'); + await expect(tooltip).toBeVisible(); + }); + + test('The tooltip should not hide after automatic scrolling during an appointment click', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + ...defaultSchedulerOptions, + views: ['week'], + currentView: 'week', + dataSource: tooltipDataSource, + }); + + const appointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'Brochure Design Review' }); + await appointment.click(); + + const tooltip = page.locator('.dx-scheduler-appointment-tooltip'); + await expect(tooltip).toBeVisible(); + }); + + test('The tooltip should hide after manually scrolling in the browser', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + ...defaultSchedulerOptions, + views: ['week'], + currentView: 'week', + dataSource: tooltipDataSource, + }); + + const appointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'Brochure Design Review' }); + await appointment.click(); + + const tooltip = page.locator('.dx-scheduler-appointment-tooltip'); + await expect(tooltip).toBeVisible(); + + await page.evaluate(() => { window.scroll(0, 100); }); + await page.waitForTimeout(500); + + await expect(tooltip).not.toBeVisible(); + }); + + [false, true].forEach((adaptivityEnabled) => { + test(`The tooltip screenshot (adaptivityEnabled=${adaptivityEnabled})`, async ({ page }) => { + await createWidget(page, 'dxScheduler', { + ...defaultSchedulerOptions, + views: ['week'], + currentView: 'week', + dataSource: tooltipDataSource, + adaptivityEnabled, + }); + + const appointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'Brochure Design Review' }); + await appointment.click(); + + const tooltipNamePrefix = adaptivityEnabled ? 'mobile' : 'desktop'; + const scheduler = page.locator('.dx-scheduler'); + await testScreenshot(page, `appointment-${tooltipNamePrefix}-tooltip-screenshot.png`, { element: scheduler }); + }); + }); + + test('Collector tooltip focused list item screenshot', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource: [ + { text: 'Text', startDate: new Date(2017, 4, 22, 9, 30, 0, 0), endDate: new Date(2017, 4, 22, 10, 30, 0, 0) }, + { text: 'Text2', startDate: new Date(2017, 4, 22, 9, 30, 0, 0), endDate: new Date(2017, 4, 22, 10, 30, 0, 0) }, + { text: 'Text3', startDate: new Date(2017, 4, 22, 9, 30, 0, 0), endDate: new Date(2017, 4, 22, 10, 30, 0, 0) }, + ], + views: [{ + type: 'month', + maxAppointmentsPerCell: 1, + }], + currentView: 'month', + currentDate: new Date(2017, 4, 22), + }); + + const collector = page.locator('.dx-scheduler-appointment-collector').filter({ hasText: '2 more' }); + await expect(collector).toBeVisible(); + await collector.click(); + + const tooltip = page.locator('.dx-scheduler-appointment-tooltip'); + await expect(tooltip).toBeVisible(); + + await page.keyboard.press('Tab'); + + const scheduler = page.locator('.dx-scheduler'); + await testScreenshot(page, 'collector-tooltip-focused-list-item.png', { element: scheduler }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/twoSchedulers.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/twoSchedulers.spec.ts new file mode 100644 index 000000000000..b7b682550e3e --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/twoSchedulers.spec.ts @@ -0,0 +1,42 @@ +import { test, expect } from '@playwright/test'; +import { createWidget } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('Interaction of two schedulers', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + +); + +const createScheduler = async (container): Promise => { + await createWidget(page, 'dxScheduler', { + dataSource: [], + currentDate: new Date(2022, 3, 5), + height: 600, + views: ['day'], + currentView: 'day', + }, container); +}; + +test('First scheduler should work after removing second (T1063130)', async ({ page }) => { + // Scheduler on '#container' + const { navigator } = scheduler.toolbar; + + await (navigator.nextButton).click() + .expect(navigator.caption.textContent).toBe('6 April 2022'); +}); + +// TODO: .before() block not converted - move to test setup +// { + await createScheduler('#container'); + await createScheduler('#otherContainer'); +}); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/virtualScrolling/T1091980.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/virtualScrolling/T1091980.spec.ts new file mode 100644 index 000000000000..738854dcf46e --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/virtualScrolling/T1091980.spec.ts @@ -0,0 +1,47 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, getContainerUrl, setupTestPage } from '../../../../playwright-helpers'; + +const containerUrl = getContainerUrl(__dirname, '../../../../tests/container.html'); + +test.describe('Scheduler: Virtual scrolling', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + test('it should correctly render virtual table if scheduler sizes are set in % (T1091980)', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + width: '100%', + height: '100%', + dataSource: [], + views: [{ + type: 'week', + intervalCount: 10, + }], + currentView: 'week', + currentDate: new Date(2021, 3, 5), + startDayHour: 8, + endDayHour: 20, + crossScrollingEnabled: true, + scrolling: { + mode: 'virtual', + }, + }); + + const allDayCellCount = await page.locator('.dx-scheduler-all-day-table-cell').count(); + expect(allDayCellCount).toBe(24); + + const dateTableCellCount = await page.locator('.dx-scheduler-date-table-cell').count(); + expect(dateTableCellCount).toBe(576); + + await page.evaluate(() => { + const instance = ($('#container') as any).dxScheduler('instance'); + instance.scrollTo(new Date(2021, 5, 12, 19)); + }); + + const allDayCellCountAfter = await page.locator('.dx-scheduler-all-day-table-cell').count(); + expect(allDayCellCountAfter).toBe(24); + + const dateTableCellCountAfter = await page.locator('.dx-scheduler-date-table-cell').count(); + expect(dateTableCellCountAfter).toBe(576); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/virtualScrolling/T1258030.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/virtualScrolling/T1258030.spec.ts new file mode 100644 index 000000000000..8d5afe7ccff5 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/virtualScrolling/T1258030.spec.ts @@ -0,0 +1,39 @@ +import { test } from '@playwright/test'; +import { createWidget, testScreenshot, getContainerUrl, setupTestPage } from '../../../../playwright-helpers'; + +const containerUrl = getContainerUrl(__dirname, '../../../../tests/container.html'); + +async function scrollTo(page, x: number, y: number): Promise { + await page.evaluate(({ sx, sy }) => { + const instance = ($('#container') as any).dxScheduler('instance'); + const scrollable = instance.getWorkSpaceScrollable(); + scrollable.scrollTo({ y: sy, x: sx }); + }, { sx: x, sy: y }); +} + +test.describe('Scheduler: Virtual scrolling', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + test('it should render recurrence appointment with correct width in month timeline view for virtual scrolling', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + height: 300, + currentView: 'timelineMonth', + views: ['timelineMonth'], + currentDate: new Date(2024, 9, 1), + dataSource: [{ + text: 'appointment', + startDate: new Date(2024, 9, 1), + endDate: new Date(2024, 9, 2), + recurrenceRule: 'FREQ=DAILY', + }], + scrolling: { mode: 'virtual' }, + }); + + await scrollTo(page, 3000, 0); + + const workSpace = page.locator('.dx-scheduler-work-space'); + await testScreenshot(page, 'virtual_scroll_timeline_3000.png', { element: workSpace }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/virtualScrolling/T1287345.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/virtualScrolling/T1287345.spec.ts new file mode 100644 index 000000000000..0a68e5bf4211 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/virtualScrolling/T1287345.spec.ts @@ -0,0 +1,43 @@ +import { test } from '@playwright/test'; +import { createWidget, testScreenshot, getContainerUrl, setupTestPage, insertStylesheetRulesToPage } from '../../../../playwright-helpers'; + +const containerUrl = getContainerUrl(__dirname, '../../../../tests/container.html'); + +async function scrollTo(page, x: number, y: number): Promise { + await page.evaluate(({ sx, sy }) => { + const instance = ($('#container') as any).dxScheduler('instance'); + const scrollable = instance.getWorkSpaceScrollable(); + scrollable.scrollTo({ y: sy, x: sx }); + }, { sx: x, sy: y }); +} + +test.describe('Scheduler: Virtual scrolling', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + test('Cell width set in css should be correct for virtual scrolling after scroll down (T1287345)', async ({ page }) => { + await insertStylesheetRulesToPage(page, ` + #container .dx-scheduler-cell-sizes-horizontal { + width: 200px !important; + }`); + + await createWidget(page, 'dxScheduler', { + dataSource: [], + currentView: 'week', + scrolling: { + mode: 'virtual', + }, + currentDate: new Date(2021, 2, 28), + height: 300, + }); + + await scrollTo(page, 0, 3000); + + const nextButton = page.locator('.dx-scheduler-navigator-next'); + await nextButton.click(); + + const workSpace = page.locator('.dx-scheduler-work-space'); + await testScreenshot(page, 'virtual_scroll_cell_width.png', { element: workSpace }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/virtualScrolling/appointments.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/virtualScrolling/appointments.spec.ts new file mode 100644 index 000000000000..63008b0046c9 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/virtualScrolling/appointments.spec.ts @@ -0,0 +1,92 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, getContainerUrl, setupTestPage, setStyleAttribute, getStyleAttribute } from '../../../../playwright-helpers'; + +const containerUrl = getContainerUrl(__dirname, '../../../../tests/container.html'); + +async function scrollToDate(page, date: Date, groups?: Record): Promise { + await page.evaluate(({ d, g }) => { + const instance = ($('#container') as any).dxScheduler('instance'); + instance.scrollTo(new Date(d), g); + }, { d: date.toISOString(), g: groups }); +} + +test.describe('Scheduler: Virtual Scrolling', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + test.skip('Appointment should not repaint after scrolling if present on viewport', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + height: 600, + width: 800, + currentDate: new Date(2020, 8, 7), + scrolling: { + mode: 'virtual', + orientation: 'both', + outlineCount: 0, + }, + currentView: 'week', + views: [{ + type: 'week', + intervalCount: 10, + }], + dataSource: [{ + startDate: new Date(2020, 8, 13, 2), + endDate: new Date(2020, 8, 13, 3), + text: 'test', + }], + }); + + const element = page.locator('.dx-scheduler-appointment').nth(0); + + await setStyleAttribute(page, element, 'background-color: red;'); + const style1 = await getStyleAttribute(page, element); + expect(style1).toBe('transform: translate(525px, 200px); width: 49px; height: 100px; background-color: red;'); + + await scrollToDate(page, new Date(2020, 8, 17, 4)); + + const style2 = await getStyleAttribute(page, element); + expect(style2).toBe('transform: translate(525px, 200px); width: 49px; height: 100px; background-color: red;'); + }); + + test('The appointment should render correctly when scrolling vertically (T1263428)', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + height: 500, + width: 900, + timeZone: 'Europe/Vienna', + dateSerializationFormat: 'yyyy-MM-ddTHH:mm:ssxx', + currentDate: new Date(2024, 10, 11, 20, 54, 23, 361), + cellDuration: 20, + firstDayOfWeek: 1, + startDayHour: 12.0, + endDayHour: 18.0, + allDayPanelMode: 'hidden', + scrolling: { + mode: 'virtual', + }, + crossScrollingEnabled: true, + currentView: 'week', + textExpr: 'Subject', + startDateExpr: 'StartDate', + endDateExpr: 'EndDate', + views: [{ + type: 'week', + groupByDate: true, + startDayHour: 6.0, + endDayHour: 22.0, + }], + dataSource: [{ + Subject: 'Website Re-Design Plan', + StartDate: new Date('2024-11-11T12:10:00+0100'), + EndDate: new Date('2024-11-12T21:00:00+0100'), + }], + }); + + await scrollToDate(page, new Date('2024-11-12T09:00:00+0100')); + + const scheduler = page.locator('.dx-scheduler'); + await testScreenshot(page, 'T1263428-virtual-scrolling-render-appointment.png', { + element: scheduler, + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/virtualScrolling/layout.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/virtualScrolling/layout.spec.ts new file mode 100644 index 000000000000..e6f84f71b1f2 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/virtualScrolling/layout.spec.ts @@ -0,0 +1,143 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('Scheduler: Virtual Scrolling', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + +resources, + createDataSetForScreenShotTests, + views, + horizontalViews, + scrollConfig, + groupedByDateViews, +} from './utils'; + +); + +const createScheduler = async ( + additionalProps: Record, +): Promise => { + await createWidget(page, 'dxScheduler', { + dataSource: createDataSetForScreenShotTests(), + currentDate: new Date(2021, 0, 1), + height: 600, + resources, + views, + currentView: 'day', + scrolling: { mode: 'virtual' }, + startDayHour: 0, + endDayHour: 3, + ...additionalProps, + }); +}; + +test('Virtual scrolling layout in scheduler views', async ({ page }) => { + // --- setup --- +await createScheduler({ + // --- test --- +// Scheduler on '#container' + + // TODO: views[0] is day view and we have a bug in its CSS + // It is not advisable to create screenshots for incorrect layout + for (let i = 1; i < views.length; i += 1) { + const view = views[i]; + + await scheduler.option('currentView', view.type); + await scrollToDate(scrollConfig[i].firstDate); + + await testScreenshot(page, `virtual-scrolling-${view.type}-after-scroll.png`); + + await scrollToDate(scrollConfig[i].lastDate); + + await testScreenshot(page, `virtual-scrolling-${view.type}-before-scroll.png`); + } + + expect(compareResults.isValid()) + .ok(compareResults.errorMessages()); +}); +}); + +test('Virtual scrolling layout in scheduler views when horizontal grouping is enabled', async ({ page }) => { + // --- setup --- +await createScheduler({ + views: horizontalViews, + groups: ['resourceId'], + // --- test --- +// Scheduler on '#container' + + // TODO: views[0] is day view and we have a bug in its CSS + // It is not advisable to create screenshots for incorrect layout + for (let i = 1; i < views.length; i += 1) { + const view = views[i]; + + await scheduler.option('currentView', view.type); + await scrollToDate(scrollConfig[i].firstDate, { resourceId: 6 }); + + await testScreenshot(page, `virtual-scrolling-${view.type}-after-scroll-horizontal-grouping.png`); + + await scrollToDate(scrollConfig[i].lastDate, { resourceId: 0 }); + + await testScreenshot(page, `virtual-scrolling-${view.type}-before-scroll-horizontal-grouping.png`); + } + + expect(compareResults.isValid()) + .ok(compareResults.errorMessages()); +}); +}); + +test('Virtual scrolling layout in scheduler views when grouping by date is enabled', async ({ page }) => { + // --- setup --- +await createScheduler({ + views: groupedByDateViews, + groups: ['resourceId'], + // --- test --- +// Scheduler on '#container' + + // TODO: views[0] is day view and we have a bug in its CSS + // It is not advisable to create screenshots for incorrect layout + for (let i = 1; i < views.length; i += 1) { + const view = views[i]; + + await scheduler.option('currentView', view.type); + + await scrollToDate(scrollConfig[i].firstDate, { resourceId: 3 }); + + await testScreenshot(page, `virtual-scrolling-${view.type}-after-scroll-grouping-by-date.png`); + + await scrollToDate(scrollConfig[i].lastDate, { resourceId: 0 }); + + await testScreenshot(page, `virtual-scrolling-${view.type}-before-scroll-grouping-by-date.png`); + } + + expect(compareResults.isValid()) + .ok(compareResults.errorMessages()); +}); +}); + +test('Header cells should be aligned with date-table cells in timeline-month when current date changes and virtual scrolling is used', async ({ page }) => { + // --- setup --- +await createScheduler({ + currentDate: new Date(2020, 10, 1), + currentView: 'timelineMonth', + // --- test --- + // Scheduler on '#container' + + await scheduler.option('currentDate', new Date(2020, 11, 1)); + + await testScreenshot(page, 'virtual-scrolling-timeline-month-change-current-date-virtual.png'); + + expect(compareResults.isValid()) + .ok(compareResults.errorMessages()); +}); +}); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/virtualScrolling/many-cells.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/virtualScrolling/many-cells.spec.ts new file mode 100644 index 000000000000..0244515d0af2 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/virtualScrolling/many-cells.spec.ts @@ -0,0 +1,79 @@ +import { test } from '@playwright/test'; +import { createWidget, testScreenshot, getContainerUrl, setupTestPage, generateOptionMatrix } from '../../../../playwright-helpers'; + +const containerUrl = getContainerUrl(__dirname, '../../../../tests/container.html'); + +const buildScreenshotName = (viewType: string, orientation: string, step: string) => `virtual-scrolling-many-cells-${viewType}-${orientation}-${step}.png`; + +async function scrollTo(page, date: Date, groups?: Record): Promise { + await page.evaluate(({ d, g }) => { + const instance = ($('#container') as any).dxScheduler('instance'); + instance.scrollTo(new Date(d), g); + }, { d: date.toISOString(), g: groups }); +} + +const testCases = generateOptionMatrix({ + viewType: ['month', 'week', 'workWeek'], + groupOrientation: ['horizontal', 'vertical'], +}); + +test.describe('Scheduler: Virtual scrolling (many cells)', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + testCases.forEach(({ viewType, groupOrientation }) => { + const resourceCount = 400; + + test(`it should correctly render virtual table if a lot of resources are presented for ${viewType} view and ${groupOrientation} orientation (T1205597, T1137490)`, async ({ page }) => { + const resources = Array.from({ length: resourceCount }, (_, i) => ({ + id: i, + text: `Resource ${i}`, + })); + + const appointmentDateInfo = Array.from({ length: 29 }) + .map((_, i) => ({ + startDate: new Date(2024, 1, i + 1, 1), + endDate: new Date(2024, 1, i + 1, 4), + })); + + const appointments = Array.from({ length: resourceCount }) + .map((_, resourceIndex) => appointmentDateInfo.map(({ startDate, endDate }) => ({ + text: `Appointment for Resource ${resourceIndex}`, + startDate, + endDate, + groupId: resourceIndex, + }))) + .flat(); + + await createWidget(page, 'dxScheduler', { + height: 600, + currentDate: new Date(2024, 1, 1), + dataSource: appointments, + views: [{ + type: viewType, + groupOrientation, + }], + currentView: viewType, + scrolling: { + mode: 'virtual', + }, + groups: ['groupId'], + resources: [{ + fieldExpr: 'groupId', + dataSource: resources, + label: 'Group', + }], + }); + + const scheduler = page.locator('.dx-scheduler'); + await testScreenshot(page, buildScreenshotName(viewType, groupOrientation, 'start'), { element: scheduler }); + + await scrollTo(page, new Date(2024, 1, 1, 1), { groupId: resourceCount / 2 }); + await testScreenshot(page, buildScreenshotName(viewType, groupOrientation, 'middle'), { element: scheduler }); + + await scrollTo(page, new Date(2024, 1, 1, 1), { groupId: resourceCount - 1 }); + await testScreenshot(page, buildScreenshotName(viewType, groupOrientation, 'end'), { element: scheduler }); + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/virtualScrolling/resources.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/virtualScrolling/resources.spec.ts new file mode 100644 index 000000000000..7aa1b6a1aab4 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/virtualScrolling/resources.spec.ts @@ -0,0 +1,45 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, getContainerUrl, setupTestPage } from '../../../../playwright-helpers'; + +const containerUrl = getContainerUrl(__dirname, '../../../../tests/container.html'); + +test.describe('Scheduler: Generic theme layout', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + test('Should correctly render view if virtual scrolling and groupByDate', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + height: 600, + width: 200, + dataSource: [{ + userId: 1, + startDate: new Date(2022, 0, 16, 14, 30), + endDate: new Date(2022, 0, 16, 15), + }], + currentDate: new Date(2022, 0, 15), + views: ['month'], + currentView: 'month', + groupByDate: true, + groups: ['userId'], + resources: [{ + fieldExpr: 'userId', + allowMultiple: false, + dataSource: [ + { id: 1, text: 'User 1' }, + { id: 2, text: 'User 2' }, + { id: 3, text: 'User 3' }, + { id: 4, text: 'User 4' }, + { id: 5, text: 'User 5' }, + ], + label: 'User', + }], + scrolling: { + mode: 'virtual', + }, + }); + + const appointment = page.locator('.dx-scheduler-appointment').nth(0); + await expect(appointment).toBeVisible(); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/virtualScrolling/zooming.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/virtualScrolling/zooming.spec.ts new file mode 100644 index 000000000000..79b4488b4dc8 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/virtualScrolling/zooming.spec.ts @@ -0,0 +1,94 @@ +import { test } from '@playwright/test'; +import { createWidget, testScreenshot, getContainerUrl, setupTestPage } from '../../../../playwright-helpers'; + +const containerUrl = getContainerUrl(__dirname, '../../../../tests/container.html'); + +const resources = [{ + fieldExpr: 'resourceId', + allowMultiple: true, + dataSource: [ + { text: 'Resource 0', id: 0, color: '#20B2AA' }, + { text: 'Resource 1', id: 1, color: '#87CEEB' }, + { text: 'Resource 2', id: 2, color: '#228B22' }, + { text: 'Resource 3', id: 3, color: '#98FB98' }, + { text: 'Resource 4', id: 4, color: '#2E8B57' }, + { text: 'Resource 5', id: 5, color: '#66CDAA' }, + { text: 'Resource 6', id: 6, color: '#008080' }, + { text: 'Resource 7', id: 7, color: '#00FFFF' }, + ], + label: 'Priority', +}]; + +const views = [ + { type: 'day', intervalCount: 7, endDayHour: 8 }, + { type: 'week', intervalCount: 10, endDayHour: 8 }, + { type: 'month' }, + { type: 'timelineDay', intervalCount: 7 }, + { type: 'timelineWeek', intervalCount: 3 }, + { type: 'timelineMonth' }, +]; + +const horizontalViews = views.map((view) => ({ ...view, groupOrientation: 'horizontal' })); + +const scrollConfig = [ + { firstDate: new Date(2021, 0, 7), lastDate: new Date(2021, 0, 1) }, + { firstDate: new Date(2021, 0, 15), lastDate: new Date(2020, 11, 27) }, + { firstDate: new Date(2021, 0, 1), lastDate: new Date(2020, 11, 27) }, + { firstDate: new Date(2021, 0, 7), lastDate: new Date(2021, 0, 1) }, + { firstDate: new Date(2021, 0, 15), lastDate: new Date(2020, 11, 27) }, + { firstDate: new Date(2021, 0, 30), lastDate: new Date(2021, 0, 1) }, +]; + +async function scrollToDate(page, date: Date, groups?: Record): Promise { + await page.evaluate(({ d, g }) => { + const instance = ($('#container') as any).dxScheduler('instance'); + instance.scrollTo(new Date(d), g); + }, { d: date.toISOString(), g: groups }); +} + +async function setZoomLevel(page, zoomLevel: number): Promise { + await page.evaluate((z) => { + $('body').css('zoom', `${z}%`); + }, zoomLevel); +} + +async function setOption(page, optionName: string, value: unknown): Promise { + await page.evaluate(({ opt, val }) => { + ($('#container') as any).dxScheduler('instance').option(opt, val); + }, { opt: optionName, val: value }); +} + +test.describe('Scheduler: Virtual Scrolling with Zooming', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + test('Virtual scrolling layout in scheduler views when horizontal grouping is enabled and zooming is used', async ({ page }) => { + await setZoomLevel(page, 125); + + await createWidget(page, 'dxScheduler', { + currentDate: new Date(2021, 0, 1), + height: 600, + resources, + views: horizontalViews, + currentView: 'day', + scrolling: { mode: 'virtual' }, + startDayHour: 0, + endDayHour: 3, + groups: ['resourceId'], + }); + + for (let i = 1; i < views.length; i += 1) { + const view = views[i]; + await setOption(page, 'currentView', view.type); + + await testScreenshot(page, `virtual-scrolling-${view.type}-before-scroll-horizontal-grouping-scaling.png`); + + await scrollToDate(page, scrollConfig[i].firstDate, { resourceId: 7 }); + + await testScreenshot(page, `virtual-scrolling-${view.type}-after-scroll-horizontal-grouping-scaling.png`); + } + + await setZoomLevel(page, 0); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/workSpace.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/workSpace.spec.ts new file mode 100644 index 000000000000..5942b1526f3a --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/workSpace.spec.ts @@ -0,0 +1,373 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, testScreenshot, insertStylesheetRulesToPage } from '../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; + +test.describe('Scheduler: Workspace', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + +); + +const FIXED_PARENT_CONTAINER_SIZE = ` +#parentContainer { + width: 400px; + height: 500px; +} + +#container { + height: 100%; +} +`; + +const createScheduler = async (options = {}): Promise => { + await createWidget(page, 'dxScheduler', extend(options, { + dataSource: [], + startDayHour: 9, + height: 600, + })); +}; + +const getResourcesDataSource = (count: number) => new Array(count) + .fill(null) + .map((_, idx) => ({ + id: idx, + name: idx.toString(), + })); + +test('Vertical selection between two workspace cells should focus cells between them (T804954)', async ({ page }) => { + // Scheduler on '#container' + + await t + .dragToElement(page.locator('.dx-scheduler-date-table-row').nth(0).locator('.dx-scheduler-date-table-cell').nth(0), page.locator('.dx-scheduler-date-table-row').nth(3).locator('.dx-scheduler-date-table-cell').nth(0)) + .expect(page.locator('.dx-scheduler-date-table-cell').filter('.dx-state-focused').count).toBe(4); +}).before(async () => createScheduler({ + views: [{ name: '2 Days', type: 'day', intervalCount: 2 }], + currentDate: new Date(2015, 1, 9), + currentView: 'day', +})); + +test('Horizontal selection between two workspace cells should focus cells between them', async ({ page }) => { + // --- setup --- +await createWidget(page, 'dxScheduler', { + views: ['month'], + currentView: 'month', + currentDate: new Date(2019, 4, 1), + height: 250, + // --- test --- +// Scheduler on '#container' + + await t + .dragToElement(page.locator('.dx-scheduler-date-table-row').nth(0).locator('.dx-scheduler-date-table-cell').nth(0), page.locator('.dx-scheduler-date-table-row').nth(0).locator('.dx-scheduler-date-table-cell').nth(3)) + .expect(page.locator('.dx-scheduler-date-table-cell').filter('.dx-state-focused').count) + .eql(4); +}).before(async () => createScheduler({ + views: ['timelineWeek'], + currentDate: new Date(2015, 1, 9), + currentView: 'timelineWeek', + groups: ['roomId'], + resources: [{ + fieldExpr: 'roomId', + label: 'Room', + dataSource: [{ + text: '1', id: 1, + }, { + text: '2', id: 2, + }], + }], +})); + +test('Vertical grouping should work correctly when there is one group', async ({ page }) => { + // --- setup --- + await { + await createWidget(page, 'dxScheduler', { + dataSource: [], + currentView: viewName, + currentDate: '2024-01-01T00:00:00', + crossScrollingEnabled: true, + height: 300, + }; + // --- test --- +// Scheduler on '#container' + + expect(page.locator('.dx-scheduler-date-table-cell').count) + .eql(336); +}).before(async () => createWidget(page, 'dxScheduler', { + views: [{ + type: 'week', + groupOrientation: 'vertical', + }], + currentView: 'week', + dataSource: [], + groups: ['priorityId'], + resources: [{ + field: 'priorityId', + dataSource: [{ id: 1, color: 'black' }], + }], + height: 600, +})); + +async function hideShow(page: Page, container) { + await page.evaluate((container) => { + const instance = ($(container) as any).dxScheduler('instance'); + instance.option('visible', !instance.option('visible')); +}, container); +} + +async function resize(page: Page, container) { + await page.evaluate((container) => { + const instance = ($(container) as any).dxScheduler('instance'); + // eslint-disable-next-line no-underscore-dangle + instance._dimensionChanged(); + // eslint-disable-next-line no-underscore-dangle + instance._workSpace._dimensionChanged(); +}, container); +} + +test('Hidden scheduler should not resize', async ({ page }) => { + await hideShow('#container'); + await resize('#container'); + await hideShow('#container'); + + await testScreenshot(page, 'scheduler-after-hiding-and-resizing.png'); + + expect(compareResults.isValid()) + .ok(compareResults.errorMessages()); +}).before(async () => createWidget(page, 'dxScheduler', { + dataSource: [ + { + text: 'Google AdWords Strategy', + ownerId: [2], + startDate: new Date('2021-02-01T16:00:00.000Z'), + endDate: new Date('2021-02-01T17:30:00.000Z'), + priority: 1, + }, + ], + resources: [ + { + fieldExpr: 'priority', + dataSource: [ + { + text: 'Priority 1', + id: 1, + color: '#1e90ff', + }, + ], + label: 'Priority', + }, + ], + groups: ['priority'], + views: [ + { + type: 'timelineMonth', + groupOrientation: 'vertical', + }, + ], + crossScrollingEnabled: true, + currentView: 'timelineMonth', + currentDate: new Date(2021, 1, 1), + height: 400, +})); + +test('All day panel should be hidden when allDayPanelMode=hidden by initializing scheduler', async ({ page }) => { + // Scheduler on '#container' + + expect(page.locator('.dx-scheduler-all-day-title').exists) + .eql(false); + + expect(page.locator('.dx-scheduler-all-day-table-row').exists) + .eql(false); +}).before(async () => createWidget(page, 'dxScheduler', { + currentDate: new Date(2021, 2, 28), + currentView: 'day', + allDayPanelMode: 'hidden', + dataSource: [{ + text: 'Book Flights to San Fran for Sales Trip', + startDate: new Date('2021-03-28T17:00:00.000Z'), + endDate: new Date('2021-03-28T18:00:00.000Z'), + allDay: true, + }, { + text: 'Customer Workshop', + startDate: new Date('2021-03-29T17:30:00.000Z'), + endDate: new Date('2021-04-03T19:00:00.000Z'), + }], +})); + +// visual: generic.light +// visual: fluent.blue.light +// visual: material.blue.light +test('Month workspace should be scrollable to the last row (T1203250)', async ({ page }) => { + // Scheduler on '#container' + await page.evaluate((d) => $('#container').dxScheduler('instance').scrollTo(new Date(d)), (new Date(2019, 5, 8, 0, 0).toISOString())); + + await testScreenshot(page, 'scrollable-month-workspace.png', { element: page.locator('.dx-scheduler-work-space') }); + + expect(compareResults.isValid()) + .ok(compareResults.errorMessages()); +}); +}); + +// visual: generic.light +// visual: generic.dark +// visual: fluent.blue.light +// visual: fluent.blue.dark +// visual: fluent.saas.light +// visual: fluent.saas.dark +// visual: material.blue.light +// visual: material.blue.dark +test('Check cell hover state', async ({ page }) => { + // --- setup --- +await createWidget(page, 'dxScheduler', { + views: ['week'], + currentView: 'week', + currentDate: new Date(2019, 4, 1), + height: 500, + // --- test --- +// arrange + // Scheduler on '#container' + const firstDateTableCell = page.locator('.dx-scheduler-date-table-row').nth(0).locator('.dx-scheduler-date-table-cell').nth(0); + + // act + await (firstDateTableCell).hover() + .expect(firstDateTableCell.hasClass(CLASS.hoverCell)) + .ok(); + + // assert + await testScreenshot(page, 'scheduler-week-cell-hover-state.png', { element: page.locator('.dx-scheduler-work-space') }); + + await (page.locator('.dx-scheduler-date-table-row').nth(0).locator('.dx-scheduler-date-table-cell').nth(1).hover()) + .expect(page.locator('.dx-scheduler-date-table-row').nth(0).locator('.dx-scheduler-date-table-cell').nth(1).hasClass(CLASS.hoverCell)) + .ok() + ; +}); +}); + +test('Check cell active state', async ({ page }) => { + // --- setup --- +await createWidget(page, 'dxScheduler', { + views: ['week'], + currentView: 'week', + currentDate: new Date(2019, 4, 1), + height: 500, + // --- test --- +// arrange + // Scheduler on '#container' + const firstDateTableCell = page.locator('.dx-scheduler-date-table-row').nth(0).locator('.dx-scheduler-date-table-cell').nth(0); + + // act + await (firstDateTableCell).hover() + .expect(firstDateTableCell.hasClass(CLASS.hoverCell)) + .ok() + .dispatchEvent(firstDateTableCell, 'mousedown') + .expect(firstDateTableCell.hasClass(CLASS.activeCell)) + .ok(); + + // assert + await testScreenshot(page, 'scheduler-week-cell-active-state.png', { element: page.locator('.dx-scheduler-work-space') }); + + await t + .dispatchEvent(firstDateTableCell, 'mouseup') + .expect(firstDateTableCell.hasClass(CLASS.activeCell)) + .notOk() + .hover(page.locator('.dx-scheduler-date-table-row').nth(0).locator('.dx-scheduler-date-table-cell').nth(1)) + .expect(page.locator('.dx-scheduler-date-table-row').nth(0).locator('.dx-scheduler-date-table-cell').nth(1).hasClass(CLASS.hoverCell)) + .ok() + ; +}); +}); + +[ + 'day', + 'week', + 'workWeek', + 'month', +].forEach((viewName) => { + test(`[T1225772]: should not have the horizontal scroll in horizontal views when the crossScrollingEnabled: true (view:${viewName})`, async ({ page }) => { + // Scheduler on '#container' + + const scrollableContainer = page.locator('.dx-scheduler-date-table')ScrollableContainer; + const scrollWidth = await scrollableContainer.scrollWidth; + const clientWidth = await scrollableContainer.clientWidth; + const hasHorizontalScroll = scrollWidth > clientWidth; + + expect(hasHorizontalScroll).toBeFalsy(/* workspace has the horizontal scrollbar */); +}); + }); +}); + +// NOTE: Moved "as is" from the QUnit integration.resources.tests (see history) +test('[T716993]: should has horizontal scrollbar with multiple resources and fixed height container', async ({ page }) => { + // --- setup --- +const resourcesDataSource = getResourcesDataSource(10); + + await insertStylesheetRulesToPage(FIXED_PARENT_CONTAINER_SIZE); + return createWidget(page, 'dxScheduler', { + dataSource: [], + groups: ['id'], + resources: [{ + dataSource: resourcesDataSource, + displayExpr: 'name', + valueExpr: 'id', + fieldExpr: 'id', + allowMultiple: false, + }], + crossScrollingEnabled: true, + // --- test --- +// Scheduler on '#container' + + const scrollableContainer = page.locator('.dx-scheduler-date-table')ScrollableContainer; + const scrollWidth = await scrollableContainer.scrollWidth; + const clientWidth = await scrollableContainer.clientWidth; + const hasHorizontalScroll = scrollWidth > clientWidth; + + expect(hasHorizontalScroll).ok('workspace hasn\'t the horizontal scrollbar'); +}); +}); + +test('Scheduler appointments should change color on update resources', async ({ page }) => { + // --- setup --- +await createWidget(page, 'dxScheduler', { + timeZone: 'America/Los_Angeles', + dataSource: [{ + text: 'Website Re-Design Plan', + startDate: new Date('2021-03-29T16:30:00.000Z'), + endDate: new Date('2021-03-29T18:30:00.000Z'), + resource: 1, + }], + views: ['week', 'month'], + currentView: 'week', + currentDate: new Date(2021, 2, 28), + startDayHour: 9, + height: 730, + resources: [{ + fieldExpr: 'resource', + dataSource: [{ id: 1, text: 'res 1', color: 'red' }], + }], + }, '#otherContainer'); + await createWidget(page, 'dxButton', { + text: 'Change resources', + onClick() { + const schedulerWidget = ($('#otherContainer') as any).dxScheduler('instance'); + schedulerWidget.option('resources', [{ + fieldExpr: 'resource', + dataSource: [{ id: 1, text: 'new res 1', color: 'pink' }], + }]); + schedulerWidget.getDataSource().reload(); + }, + }, '#container'); + // --- test --- +// Button on '#container' + // Scheduler on '#otherContainer' + await (button.element).click(); + + await testScreenshot(page, 'scheduler-appointments-should-update-color.png', { element: page.locator('.dx-scheduler-work-space') }); + expect(compareResults.isValid()).ok(compareResults.errorMessages()); +}); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/timezones/appointmentCollectorTimezone.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/timezones/appointmentCollectorTimezone.spec.ts new file mode 100644 index 000000000000..fe53a11775e9 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/timezones/appointmentCollectorTimezone.spec.ts @@ -0,0 +1,64 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, setupTestPage, getContainerUrl } from '../../../playwright-helpers'; + +const containerUrl = getContainerUrl(__dirname, '../../../tests/container.html'); + +const MACHINE_TIMEZONES = { + EuropeBerlin: 'Europe/Berlin', + AmericaLosAngeles: 'America/Los_Angeles', +} as const; + +test.describe('Scheduler - Appointment Collector Timezone', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + [ + MACHINE_TIMEZONES.EuropeBerlin, + ].forEach((machineTimezone) => { + test(`Appointment collector button should have correct date (${machineTimezone})`, async ({ page }) => { + const browserTimezone = await page.evaluate( + () => Intl.DateTimeFormat().resolvedOptions().timeZone, + ); + test.skip(browserTimezone !== machineTimezone, `Skipping: machine timezone is ${browserTimezone}, expected ${machineTimezone}`); + + await createWidget(page, 'dxScheduler', { + timeZone: 'America/Los_Angeles', + dataSource: [ + { + text: 'Website Re-Design Plan', + startDate: new Date('2021-03-05T15:30:00.000Z'), + endDate: new Date('2021-03-05T17:00:00.000Z'), + }, + { + text: 'Complete Shipper Selection Form', + startDate: new Date('2021-03-05T15:30:00.000Z'), + endDate: new Date('2021-03-05T17:00:00.000Z'), + }, + { + text: 'Upgrade Server Hardware', + startDate: new Date('2021-03-05T19:00:00.000Z'), + endDate: new Date('2021-03-05T21:15:00.000Z'), + }, + { + text: 'Upgrade Personal Computers', + startDate: new Date('2021-03-05T23:45:00.000Z'), + endDate: new Date('2021-03-06T01:30:00.000Z'), + }, + ], + currentView: 'month', + currentDate: new Date(2021, 2, 1), + maxAppointmentsPerCell: 3, + }); + + const scheduler = page.locator('#container'); + await expect(scheduler).toBeVisible(); + + const collector = page.locator('.dx-scheduler-appointment-collector').first(); + const expectedDate = 'March 5, 2021'; + + const ariaRoleDescription = await collector.getAttribute('aria-roledescription'); + expect(ariaRoleDescription).toContain(expectedDate); + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/timezones/check.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/timezones/check.spec.ts new file mode 100644 index 000000000000..7e60b0b97499 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/timezones/check.spec.ts @@ -0,0 +1,36 @@ +import { test, expect } from '@playwright/test'; +import { setupTestPage, getContainerUrl } from '../../../playwright-helpers'; + +const containerUrl = getContainerUrl(__dirname, '../../../tests/container.html'); + +const MACHINE_TIMEZONES = { + EuropeBerlin: 'Europe/Berlin', + AmericaLosAngeles: 'America/Los_Angeles', +} as const; +type MachineTimezonesType = typeof MACHINE_TIMEZONES[keyof typeof MACHINE_TIMEZONES]; + +type CheckType = [MachineTimezonesType, string]; +const checks: CheckType[] = [ + [MACHINE_TIMEZONES.AmericaLosAngeles, 'Mon Jan 01 2024 10:00:00 GMT-0800 (Pacific Standard Time)'], + [MACHINE_TIMEZONES.EuropeBerlin, 'Mon Jan 01 2024 10:00:00 GMT+0100 (Central European Standard Time)'], +]; + +test.describe('Runner machine timezone checks', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + checks.forEach(([timezone, expectedResult]) => { + test(`${timezone} check`, async ({ page }) => { + const browserTimezone = await page.evaluate( + () => Intl.DateTimeFormat().resolvedOptions().timeZone, + ); + test.skip(browserTimezone !== timezone, `Skipping: machine timezone is ${browserTimezone}, expected ${timezone}`); + + const dateFromBrowser = await page.evaluate( + () => new Date(2024, 0, 1, 10).toString(), + ); + expect(dateFromBrowser).toBe(expectedResult); + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/timezones/dragAndDropDst.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/timezones/dragAndDropDst.spec.ts new file mode 100644 index 000000000000..b3aead60902c --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/timezones/dragAndDropDst.spec.ts @@ -0,0 +1,186 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, setupTestPage, getContainerUrl, insertStylesheetRulesToPage, generateOptionMatrix } from '../../../playwright-helpers'; + +const containerUrl = getContainerUrl(__dirname, '../../../tests/container.html'); + +const MACHINE_TIMEZONES = { + EuropeBerlin: 'Europe/Berlin', + AmericaLosAngeles: 'America/Los_Angeles', +} as const; +type MachineTimezonesType = typeof MACHINE_TIMEZONES[keyof typeof MACHINE_TIMEZONES]; + +interface TestCase { + timezone: MachineTimezonesType; + season: string; + currentDate: string; + startDate: Date; + cellIdxArray: [rowIdx: number, colIdx: number][]; + expectedTopPosition: number[]; +} + +const SCHEDULER_SELECTOR = '#container'; +const APPOINTMENT_TEXT = 'Appointment'; +const CUSTOM_CSS = ` +#container .dx-scheduler-header-panel-cell { + color: rgba(0,0,0,.54); +} + +#container .dx-scheduler-header-panel-cell::before { + display: none; +} + +.dx-scheduler-cell-sizes-vertical { + height: 25px; +}`; +const DRAG_Y_OFFSET_PX = 12; + +const getAppointmentFromStartDate = (startDate: Date, offset: number) => { + const minuteMs = 60000; + const appointmentDurationMs = 60 * minuteMs; + return { + startDate: new Date(startDate.getTime() + offset * minuteMs), + endDate: new Date(startDate.getTime() + offset * minuteMs + appointmentDurationMs), + text: APPOINTMENT_TEXT, + }; +}; + +const BERLIN_SUMMER_CASE: TestCase = { + timezone: MACHINE_TIMEZONES.EuropeBerlin, + season: 'summer', + currentDate: '2024-03-31', + startDate: new Date('2024-03-30T23:00:00Z'), + cellIdxArray: Array.from({ length: 8 }, (_, idx) => [idx, 3]) as [number, number][], + expectedTopPosition: [0, 25, 25, 75, 100, 125, 150, 175], +}; + +const BERLIN_SUMMER_CASE_OFFSET: TestCase = { + timezone: MACHINE_TIMEZONES.EuropeBerlin, + season: 'summer', + currentDate: '2024-03-31', + startDate: new Date('2024-03-30T23:00:00Z'), + cellIdxArray: Array.from({ length: 8 }, (_, idx) => [idx, 3]) as [number, number][], + expectedTopPosition: [0, 25, 50, 75, 100, 125, 150, 175], +}; + +const BERLIN_WINTER_CASE: TestCase = { + timezone: MACHINE_TIMEZONES.EuropeBerlin, + season: 'winter', + currentDate: '2024-10-27', + startDate: new Date('2024-10-26T22:00:00Z'), + cellIdxArray: Array.from({ length: 8 }, (_, idx) => [idx, 3]) as [number, number][], + expectedTopPosition: [0, 25, 50, 75, 100, 125, 150, 175], +}; + +const LOS_ANGELES_SUMMER_CASE: TestCase = { + timezone: MACHINE_TIMEZONES.AmericaLosAngeles, + season: 'summer', + currentDate: '2024-03-10', + startDate: new Date('2024-03-10T08:00:00Z'), + cellIdxArray: Array.from({ length: 8 }, (_, idx) => [idx, 3]) as [number, number][], + expectedTopPosition: [0, 25, 25, 75, 100, 125, 150, 175], +}; + +const LOS_ANGELES_SUMMER_CASE_OFFSET: TestCase = { + timezone: MACHINE_TIMEZONES.AmericaLosAngeles, + season: 'summer', + currentDate: '2024-03-10', + startDate: new Date('2024-03-10T08:00:00Z'), + cellIdxArray: Array.from({ length: 8 }, (_, idx) => [idx, 3]) as [number, number][], + expectedTopPosition: [0, 25, 50, 75, 100, 125, 150, 175], +}; + +const LOS_ANGELES_WINTER_CASE: TestCase = { + timezone: MACHINE_TIMEZONES.AmericaLosAngeles, + season: 'summer', + currentDate: '2024-11-03', + startDate: new Date('2024-11-03T07:00:00Z'), + cellIdxArray: Array.from({ length: 8 }, (_, idx) => [idx, 3]) as [number, number][], + expectedTopPosition: [0, 25, 50, 75, 100, 125, 150, 175], +}; + +const ZERO_OFFSET_TEST_CASES = generateOptionMatrix({ + offset: [0], + testCase: [ + BERLIN_SUMMER_CASE, + BERLIN_WINTER_CASE, + LOS_ANGELES_SUMMER_CASE, + LOS_ANGELES_WINTER_CASE, + ], +}); + +const OFFSET_TEST_CASES = generateOptionMatrix({ + offset: [-360, 360], + testCase: [ + BERLIN_SUMMER_CASE_OFFSET, + BERLIN_WINTER_CASE, + LOS_ANGELES_SUMMER_CASE_OFFSET, + LOS_ANGELES_WINTER_CASE, + ], +}); + +test.describe('Scheduler render during DST - drag and drop', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + [ + ...ZERO_OFFSET_TEST_CASES, + ...OFFSET_TEST_CASES, + ].forEach(({ + offset, + testCase: { + timezone, + season, + currentDate, + startDate, + cellIdxArray, + expectedTopPosition, + }, + }) => { + test(`Should drag-n-drop appointment correctly during around DST (${timezone}, ${season}, ${offset})`, async ({ page }) => { + const browserTimezone = await page.evaluate( + () => Intl.DateTimeFormat().resolvedOptions().timeZone, + ); + test.skip(browserTimezone !== timezone, `Skipping: machine timezone is ${browserTimezone}, expected ${timezone}`); + + await insertStylesheetRulesToPage(page, CUSTOM_CSS); + + const dataSource = [getAppointmentFromStartDate(startDate, offset)]; + await createWidget(page, 'dxScheduler', { + timeZone: timezone, + dataSource, + currentView: 'week', + currentDate, + offset, + showCurrentTimeIndicator: false, + showAllDayPanel: false, + firstDayOfWeek: 4, + cellDuration: 60, + height: 800, + }); + + const appointment = page.locator('.dx-scheduler-appointment').filter({ hasText: APPOINTMENT_TEXT }); + const initialHeight = await appointment.evaluate((el) => el.getBoundingClientRect().height); + const [[firstCellRowIdx, firstCellColIdx]] = cellIdxArray; + const firstCell = page.locator('.dx-scheduler-date-table-row').nth(firstCellRowIdx) + .locator('.dx-scheduler-date-table-cell').nth(firstCellColIdx); + const firstCellTop = await firstCell.evaluate((el) => el.getBoundingClientRect().top); + + for (let idx = 0; idx < cellIdxArray.length; idx += 1) { + const [rowIdx, colIdx] = cellIdxArray[idx]; + const cell = page.locator('.dx-scheduler-date-table-row').nth(rowIdx) + .locator('.dx-scheduler-date-table-cell').nth(colIdx); + + // TODO: dragToElement with offsetY - Playwright dragTo doesn't support offset on target the same way + await appointment.dragTo(cell); + + const currentHeight = await appointment.evaluate((el) => el.getBoundingClientRect().height); + const currentTop = await appointment.evaluate((el) => el.getBoundingClientRect().top); + const relativeTop = currentTop - firstCellTop; + + expect(currentHeight).toBe(initialHeight); + expect(relativeTop).toBe(expectedTopPosition[idx]); + } + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/timezones/recurrence/excludeFromRecurrence_T1225416.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/timezones/recurrence/excludeFromRecurrence_T1225416.spec.ts new file mode 100644 index 000000000000..48071ee9b232 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/timezones/recurrence/excludeFromRecurrence_T1225416.spec.ts @@ -0,0 +1,86 @@ +import { test, expect } from '@playwright/test'; +import { createWidget, setupTestPage, getContainerUrl, generateOptionMatrix } from '../../../../playwright-helpers'; + +const containerUrl = getContainerUrl(__dirname, '../../../../tests/container.html'); + +const MACHINE_TIMEZONES = { + EuropeBerlin: 'Europe/Berlin', + AmericaLosAngeles: 'America/Los_Angeles', +} as const; +type MachineTimezonesType = typeof MACHINE_TIMEZONES[keyof typeof MACHINE_TIMEZONES]; + +const SCHEDULER_SELECTOR = '#container'; +const MS_IN_MINUTE = 60000; +const MS_IN_HOUR = MS_IN_MINUTE * 60; +const APPOINTMENT_TEXT = 'TEST_APPT'; + +const getAppointments = ( + startDate: Date, + currentView: string, +) => [ + { + startDate, + endDate: new Date(startDate.getTime() + MS_IN_HOUR), + text: APPOINTMENT_TEXT, + recurrenceRule: currentView === 'week' ? 'FREQ=DAILY' : 'FREQ=WEEKLY;BYDAY=FR', + }, +]; + +const getFirstDayOfWeek = (currentView: string) => (currentView === 'week' ? 4 : 0); +const getAppointmentsCount = (currentView: string) => (currentView === 'week' ? 7 : 6); + +test.describe('Scheduler exclude from recurrence', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + generateOptionMatrix({ + timeZone: [undefined, 'America/New_York'] as (string | undefined)[], + currentView: ['week', 'month'], + location: [ + [MACHINE_TIMEZONES.EuropeBerlin, 'summer', '2024-03-31', new Date('2024-01-01T12:00:00Z')], + [MACHINE_TIMEZONES.EuropeBerlin, 'winter', '2024-10-27', new Date('2024-01-01T12:00:00Z')], + [MACHINE_TIMEZONES.AmericaLosAngeles, 'summer', '2024-03-10', new Date('2024-01-01T12:00:00Z')], + [MACHINE_TIMEZONES.AmericaLosAngeles, 'winter', '2024-11-03', new Date('2024-01-01T12:00:00Z')], + ] as [MachineTimezonesType, string, string, Date][], + }).forEach(({ + timeZone, + currentView, + location: [machineTimezone, caseName, currentDate, startDate], + }) => { + const dataSource = getAppointments(startDate, currentView); + const firstDayOfWeek = getFirstDayOfWeek(currentView); + const appointmentsCount = getAppointmentsCount(currentView); + + test(`Should correctly exclude appointment from recurrence (${currentView}, ${timeZone}, ${machineTimezone}, ${caseName})`, async ({ page }) => { + const browserTimezone = await page.evaluate( + () => Intl.DateTimeFormat().resolvedOptions().timeZone, + ); + test.skip(browserTimezone !== machineTimezone, `Skipping: machine timezone is ${browserTimezone}, expected ${machineTimezone}`); + + await createWidget(page, 'dxScheduler', { + timeZone, + dataSource, + currentDate, + currentView, + firstDayOfWeek, + recurrenceEditMode: 'occurrence', + }); + + const appointments = page.locator('.dx-scheduler-appointment'); + await expect(appointments).toHaveCount(appointmentsCount); + + for (let idx = 0; idx < appointmentsCount; idx += 1) { + const firstAppointment = page.locator('.dx-scheduler-appointment').filter({ hasText: APPOINTMENT_TEXT }).first(); + await firstAppointment.click(); + + const deleteButton = page.locator('.dx-tooltip-appointment-item-delete-button'); + await deleteButton.click(); + + await expect(appointments).toHaveCount(appointmentsCount - (idx + 1)); + } + + await expect(appointments).toHaveCount(0); + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/timezones/renderCrossDst.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/timezones/renderCrossDst.spec.ts new file mode 100644 index 000000000000..8428bad2c058 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/timezones/renderCrossDst.spec.ts @@ -0,0 +1,175 @@ +import { test } from '@playwright/test'; +import { createWidget, setupTestPage, getContainerUrl, insertStylesheetRulesToPage, testScreenshot, generateOptionMatrix } from '../../../playwright-helpers'; + +const containerUrl = getContainerUrl(__dirname, '../../../tests/container.html'); + +const MACHINE_TIMEZONES = { + EuropeBerlin: 'Europe/Berlin', + AmericaLosAngeles: 'America/Los_Angeles', +} as const; +type MachineTimezonesType = typeof MACHINE_TIMEZONES[keyof typeof MACHINE_TIMEZONES]; + +const normalizeTimezoneName = (timezone: string): string => timezone.replace(/\//g, '-'); + +const SCHEDULER_SELECTOR = '#container'; +const CUSTOM_CSS = ` +#container .dx-scheduler-header-panel-cell { + color: rgba(0,0,0,.54); +} + +#container .dx-scheduler-header-panel-cell::before { + display: none; +} + +.dx-scheduler-cell-sizes-vertical { + height: 25px; +}`; + +const SUMMER_BERLIN_LOCAL_DATE_CASE = { + timezone: MACHINE_TIMEZONES.EuropeBerlin, + caseName: 'summer-local', + currentDate: '2024-03-31', + dataSource: [ + { text: '#0', startDate: '2024-03-30T00:00:00', endDate: '2024-03-30T05:00:00' }, + { text: '#1', startDate: '2024-03-31T00:00:00', endDate: '2024-03-31T05:00:00' }, + { text: '#2', startDate: '2024-04-01T00:00:00', endDate: '2024-04-01T05:00:00' }, + { text: 'Recurrent', startDate: '2020-01-01T00:00', endDate: '2020-01-01T05:00', recurrenceRule: 'FREQ=DAILY' }, + ], +}; + +const SUMMER_BERLIN_UTC_DATE_CASE = { + timezone: MACHINE_TIMEZONES.EuropeBerlin, + caseName: 'summer-utc', + currentDate: '2024-03-31', + dataSource: [ + { text: '#0', startDate: '2024-03-29T23:00:00Z', endDate: '2024-03-30T04:00:00Z' }, + { text: '#1', startDate: '2024-03-30T23:00:00Z', endDate: '2024-03-31T04:00:00Z' }, + { text: '#2', startDate: '2024-03-31T23:00:00Z', endDate: '2024-04-01T04:00:00Z' }, + ], +}; + +const WINTER_BERLIN_LOCAL_DATE_CASE = { + timezone: MACHINE_TIMEZONES.EuropeBerlin, + caseName: 'winter-local', + currentDate: '2024-10-27', + dataSource: [ + { text: '#0', startDate: '2024-10-26T01:00:00', endDate: '2024-10-26T04:00:00' }, + { text: '#1', startDate: '2024-10-27T01:00:00', endDate: '2024-10-27T04:00:00' }, + { text: '#2', startDate: '2024-10-28T01:00:00', endDate: '2024-10-28T04:00:00' }, + { text: 'Recurrent', startDate: '2020-01-01T01:00', endDate: '2020-01-01T04:00', recurrenceRule: 'FREQ=DAILY' }, + ], +}; + +const WINTER_BERLIN_UTC_DATE_CASE = { + timezone: MACHINE_TIMEZONES.EuropeBerlin, + caseName: 'winter-utc', + currentDate: '2024-10-27', + dataSource: [ + { text: '#0', startDate: '2024-10-25T23:00:00Z', endDate: '2024-10-26T04:00:00Z' }, + { text: '#1', startDate: '2024-10-26T23:00:00Z', endDate: '2024-10-27T04:00:00Z' }, + { text: '#2', startDate: '2024-10-27T23:00:00Z', endDate: '2024-10-28T04:00:00Z' }, + ], +}; + +const SUMMER_LOS_ANGELES_LOCAL_DATE_CASE = { + timezone: MACHINE_TIMEZONES.AmericaLosAngeles, + caseName: 'summer-local', + currentDate: '2024-03-10', + dataSource: [ + { text: '#0', startDate: '2024-03-09T00:00:00', endDate: '2024-03-09T05:00:00' }, + { text: '#1', startDate: '2024-03-10T00:00:00', endDate: '2024-03-10T05:00:00' }, + { text: '#2', startDate: '2024-03-11T00:00:00', endDate: '2024-03-11T05:00:00' }, + { text: 'Recurrent', startDate: '2020-01-01T00:00', endDate: '2020-01-01T05:00', recurrenceRule: 'FREQ=DAILY' }, + ], +}; + +const SUMMER_LOS_ANGELES_UTC_DATE_CASE = { + timezone: MACHINE_TIMEZONES.AmericaLosAngeles, + caseName: 'summer-utc', + currentDate: '2024-03-10', + dataSource: [ + { text: '#0', startDate: '2024-03-09T08:00:00Z', endDate: '2024-03-09T13:00:00Z' }, + { text: '#1', startDate: '2024-03-10T08:00:00Z', endDate: '2024-03-10T13:00:00Z' }, + { text: '#2', startDate: '2024-03-11T08:00:00Z', endDate: '2024-03-11T13:00:00Z' }, + ], +}; + +const WINTER_LOS_ANGELES_LOCAL_DATE_CASE = { + timezone: MACHINE_TIMEZONES.AmericaLosAngeles, + caseName: 'winter-local', + currentDate: '2024-11-03', + dataSource: [ + { text: '#0', startDate: '2024-11-02T00:00:00', endDate: '2024-11-02T05:00:00' }, + { text: '#1', startDate: '2024-11-03T00:00:00', endDate: '2024-11-03T05:00:00' }, + { text: '#2', startDate: '2024-11-04T00:00:00', endDate: '2024-11-04T05:00:00' }, + { text: 'Recurrent', startDate: '2020-01-01T00:00', endDate: '2020-01-01T05:00', recurrenceRule: 'FREQ=DAILY' }, + ], +}; + +const WINTER_LOS_ANGELES_UTC_DATE_CASE = { + timezone: MACHINE_TIMEZONES.AmericaLosAngeles, + caseName: 'winter-utc', + currentDate: '2024-11-03', + dataSource: [ + { text: '#0', startDate: '2024-11-02T08:00:00Z', endDate: '2024-11-02T13:00:00Z' }, + { text: '#1', startDate: '2024-11-03T08:00:00Z', endDate: '2024-11-03T13:00:00Z' }, + { text: '#2', startDate: '2024-11-04T08:00:00Z', endDate: '2024-11-04T13:00:00Z' }, + ], +}; + +test.describe('Scheduler render during DST - cross DST rendering', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + generateOptionMatrix({ + currentView: ['week'] as string[], + offset: [-360, 0, 360], + location: [ + SUMMER_BERLIN_LOCAL_DATE_CASE, + SUMMER_BERLIN_UTC_DATE_CASE, + WINTER_BERLIN_LOCAL_DATE_CASE, + WINTER_BERLIN_UTC_DATE_CASE, + SUMMER_LOS_ANGELES_LOCAL_DATE_CASE, + SUMMER_LOS_ANGELES_UTC_DATE_CASE, + WINTER_LOS_ANGELES_LOCAL_DATE_CASE, + WINTER_LOS_ANGELES_UTC_DATE_CASE, + ], + }).forEach(({ + currentView, + offset, + location: { + timezone, + caseName, + currentDate, + dataSource, + }, + }) => { + test(`Should correctly render appointments with local machine date crossing DST (${timezone}, ${caseName}, offset: ${offset})`, async ({ page }) => { + const browserTimezone = await page.evaluate( + () => Intl.DateTimeFormat().resolvedOptions().timeZone, + ); + test.skip(browserTimezone !== timezone, `Skipping: machine timezone is ${browserTimezone}, expected ${timezone}`); + + await insertStylesheetRulesToPage(page, CUSTOM_CSS); + await createWidget(page, 'dxScheduler', { + dataSource, + currentView, + currentDate, + offset, + showCurrentTimeIndicator: false, + firstDayOfWeek: 4, + cellDuration: 60, + height: 800, + }); + + const workSpace = page.locator('.dx-scheduler-work-space'); + const timezoneName = normalizeTimezoneName(timezone); + await testScreenshot( + page, + `${currentView}_appts-render-cross-dts_t-${timezoneName}-${caseName}_offset-${offset}.png`, + { element: workSpace }, + ); + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/timezones/renderDst.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/timezones/renderDst.spec.ts new file mode 100644 index 000000000000..106e2afa3391 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/timezones/renderDst.spec.ts @@ -0,0 +1,142 @@ +import { test } from '@playwright/test'; +import { createWidget, setupTestPage, getContainerUrl, insertStylesheetRulesToPage, testScreenshot, generateOptionMatrix } from '../../../playwright-helpers'; + +const containerUrl = getContainerUrl(__dirname, '../../../tests/container.html'); + +const MACHINE_TIMEZONES = { + EuropeBerlin: 'Europe/Berlin', + AmericaLosAngeles: 'America/Los_Angeles', +} as const; +type MachineTimezonesType = typeof MACHINE_TIMEZONES[keyof typeof MACHINE_TIMEZONES]; + +const normalizeTimezoneName = (timezone: string): string => timezone.replace(/\//g, '-'); + +const SCHEDULER_SELECTOR = '#container'; +const CUSTOM_CSS = ` +#container .dx-scheduler-header-panel-cell { + color: rgba(0,0,0,.54); +} + +#container .dx-scheduler-header-panel-cell::before { + display: none; +} + +.dx-scheduler-cell-sizes-vertical { + height: 25px; +}`; +const MS_IN_MINUTE = 60000; + +const generateAppointments = ( + startDate: Date, + durationMin: number, + count: number, + textPrefix = '', +) => new Array(count).fill(null).map((_, idx) => { + const currentStartDate = new Date(startDate.getTime() + durationMin * MS_IN_MINUTE * idx); + const currentEndDate = new Date(currentStartDate.getTime() + durationMin * MS_IN_MINUTE); + return { + text: `${textPrefix}${idx}`, + startDate: currentStartDate, + endDate: currentEndDate, + }; +}); + +test.describe('Scheduler render during DST', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + generateOptionMatrix({ + currentView: ['week'] as string[], + offset: [-360, 0, 360], + location: [ + [MACHINE_TIMEZONES.EuropeBerlin, 'summer', '2024-03-31', new Date('2024-03-28T23:00:00Z')], + [MACHINE_TIMEZONES.EuropeBerlin, 'winter', '2024-10-27', new Date('2024-10-24T22:00:00Z')], + [MACHINE_TIMEZONES.AmericaLosAngeles, 'summer', '2024-03-10', new Date('2024-03-08T08:00:00Z')], + [MACHINE_TIMEZONES.AmericaLosAngeles, 'winter', '2024-11-03', new Date('2024-11-01T08:00:00Z')], + ] as [MachineTimezonesType, string, string, Date][], + }).forEach(({ + currentView, + offset, + location: [timezone, caseName, currentDate, startDate], + }) => { + const dataSource = generateAppointments(startDate, 60, 120); + + test(`Should correctly render hourly appointments at DST (${timezone}, ${caseName}, offset: ${offset})`, async ({ page }) => { + const browserTimezone = await page.evaluate( + () => Intl.DateTimeFormat().resolvedOptions().timeZone, + ); + test.skip(browserTimezone !== timezone, `Skipping: machine timezone is ${browserTimezone}, expected ${timezone}`); + + await insertStylesheetRulesToPage(page, CUSTOM_CSS); + await createWidget(page, 'dxScheduler', { + timeZone: timezone, + dataSource, + currentView, + currentDate, + offset, + showCurrentTimeIndicator: false, + firstDayOfWeek: 4, + cellDuration: 60, + height: 800, + }); + + const workSpace = page.locator('.dx-scheduler-work-space'); + const timezoneName = normalizeTimezoneName(timezone); + await testScreenshot( + page, + `${currentView}_usual-appts-render-dts_t-${timezoneName}-${caseName}_offset-${offset}.png`, + { element: workSpace }, + ); + }); + }); + + generateOptionMatrix({ + currentView: ['day'] as string[], + offset: [-60, 0, 60], + location: [ + [MACHINE_TIMEZONES.EuropeBerlin, 'summer', '2024-03-31', new Date('2024-03-30T23:00:00Z')], + [MACHINE_TIMEZONES.EuropeBerlin, 'winter', '2024-10-27', new Date('2024-10-26T22:00:00Z')], + [MACHINE_TIMEZONES.AmericaLosAngeles, 'summer', '2024-03-10', new Date('2024-03-10T08:00:00Z')], + [MACHINE_TIMEZONES.AmericaLosAngeles, 'winter', '2024-11-03', new Date('2024-11-03T07:00:00Z')], + ] as [MachineTimezonesType, string, string, Date][], + }).forEach(({ + currentView, + offset, + location: [timezone, caseName, currentDate, startDate], + }) => { + const dataSource = [ + ...generateAppointments(startDate, 60, 5, 'A_'), + ...generateAppointments(startDate, 30, 10, 'B_'), + ]; + + test(`Should resolve appointment start cell correctly during DST (${timezone}, ${caseName}, offset: ${offset})`, async ({ page }) => { + const browserTimezone = await page.evaluate( + () => Intl.DateTimeFormat().resolvedOptions().timeZone, + ); + test.skip(browserTimezone !== timezone, `Skipping: machine timezone is ${browserTimezone}, expected ${timezone}`); + + await insertStylesheetRulesToPage(page, CUSTOM_CSS); + await createWidget(page, 'dxScheduler', { + timeZone: timezone, + dataSource, + currentView, + currentDate, + offset, + showCurrentTimeIndicator: false, + maxAppointmentsPerCell: 'unlimited', + firstDayOfWeek: 4, + cellDuration: 30, + height: 800, + }); + + const workSpace = page.locator('.dx-scheduler-work-space'); + const timezoneName = normalizeTimezoneName(timezone); + await testScreenshot( + page, + `${currentView}_usual-appts-start-cell-dts_t-${timezoneName}-${caseName}_offset-${offset}.png`, + { element: workSpace }, + ); + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/viewOffset/common/agenda.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/viewOffset/common/agenda.spec.ts new file mode 100644 index 000000000000..c5ed0320f432 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/viewOffset/common/agenda.spec.ts @@ -0,0 +1,51 @@ +import { test } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('Offset: Agenda', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + [ + 0, + -240, + 240, + ].forEach((offset) => { + test(`Agenda view should not be affected by root offset option (offset: ${offset})`, async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource: [ + { + startDate: '2023-09-04T00:00:00', + endDate: '2023-09-04T02:00:00', + text: '#0 04: 00 -> 02', + }, + { + startDate: '2023-09-04T10:00:00', + endDate: '2023-09-04T12:00:00', + text: '#1 04: 10 -> 12', + }, + { + startDate: '2023-09-04T23:00:00', + endDate: '2023-09-05T01:00:00', + text: '#2 04: 22 -> 01', + }, + ], + currentView: 'agenda', + currentDate: '2023-09-03', + height: 800, + offset, + }); + + const workSpace = page.locator('.dx-scheduler-work-space'); + await testScreenshot(page, `offset_agenda-not-affected_offset-${offset}.png`, { element: workSpace }); + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/viewOffset/common/apiCallbacks.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/viewOffset/common/apiCallbacks.spec.ts new file mode 100644 index 000000000000..18cdca838e0b --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/viewOffset/common/apiCallbacks.spec.ts @@ -0,0 +1,380 @@ +// @ts-nocheck +import { test, expect } from '@playwright/test'; +import { createWidget, insertStylesheetRulesToPage } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +const SCHEDULER_SELECTOR = '#container'; +const REDUCE_CELLS_CSS = ` +.dx-scheduler-cell-sizes-vertical { + height: 25px; +}`; +const MINUTE_MS = 60000; +const APPOINTMENT_TITLE = 'Test'; + +const getCellDateWithOffset = (initialDateString: string, offset: number): string => { + const initialDate = new Date(initialDateString); + const cellDate = new Date(initialDate.getTime() + (offset * MINUTE_MS)); + const [result] = cellDate.toISOString().split('.'); + return result; +}; + +const getAppointmentAfterUpdate = (offset: number) => { + switch (offset) { + case 700: + return { + startDate: '2023-09-05T12:40:00', + endDate: '2023-09-05T13:10:00', + text: APPOINTMENT_TITLE, + allDay: false, + }; + case -700: + return { + startDate: '2023-09-05T12:20:00', + endDate: '2023-09-05T12:50:00', + text: APPOINTMENT_TITLE, + allDay: false, + }; + default: + return { + startDate: '2023-09-05T12:00:00', + endDate: '2023-09-05T12:30:00', + text: APPOINTMENT_TITLE, + allDay: false, + }; + } +}; + +const EXPECTED = { + appointmentData: { + startDate: '2023-09-06T12:30:00', + endDate: '2023-09-06T13:00:00', + text: APPOINTMENT_TITLE, + }, + targetedAppointmentData: { + startDate: '2023-09-06T12:30:00', + endDate: '2023-09-06T13:00:00', + displayStartDate: new Date('2023-09-06T12:30:00'), + displayEndDate: new Date('2023-09-06T13:00:00'), + text: APPOINTMENT_TITLE, + }, +}; + +const STANDARD_DATA_SOURCE = [ + { + startDate: '2023-09-06T12:30:00', + endDate: '2023-09-06T13:00:00', + text: APPOINTMENT_TITLE, + }, +]; + +const initClientTesting = async (page, callbacks: string[]) => { + await page.evaluate((cbs) => { + (window as any).clientTesting = (window as any).clientTesting || {}; + cbs.forEach((cb) => { + (window as any).clientTesting[cb] = []; + }); + }, callbacks); +}; + +const getClientResults = async (page, callbackName: string) => { + return page.evaluate((name) => (window as any).clientTesting[name], callbackName); +}; + +const clearClientData = async (page, callbacks: string[]) => { + await page.evaluate((cbs) => { + cbs.forEach((cb) => { + if ((window as any).clientTesting) { + (window as any).clientTesting[cb] = []; + } + }); + }, callbacks); +}; + +test.describe('Offset: Api callbacks', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + [ + 0, + -700, + 700, + ].forEach((offset) => { + test(`onAppointmentRendered (offset: ${offset})`, async ({ page }) => { + await initClientTesting(page, ['onAppointmentRendered']); + await insertStylesheetRulesToPage(page, REDUCE_CELLS_CSS); + await page.evaluate(({ ds, off }) => { + const win = window as any; + win.DevExpress.fx.off = true; + ($('#container') as any).dxScheduler({ + currentDate: '2023-09-05', + height: 800, + dataSource: ds, + currentView: 'week', + cellDuration: 60, + offset: off, + onAppointmentRendered: ({ appointmentData, targetedAppointmentData }) => { + win.clientTesting.onAppointmentRendered.push({ appointmentData, targetedAppointmentData }); + }, + }); + }, { ds: STANDARD_DATA_SOURCE, off: offset }); + + const appointment = page.locator('.dx-scheduler-appointment').filter({ hasText: APPOINTMENT_TITLE }); + await expect(appointment).toBeVisible(); + + const results = await getClientResults(page, 'onAppointmentRendered'); + expect(results[0].appointmentData).toEqual(EXPECTED.appointmentData); + + await clearClientData(page, ['onAppointmentRendered']); + }); + + test(`onAppointmentAdding and onAppointmentAdded (offset: ${offset})`, async ({ page }) => { + await initClientTesting(page, ['onAppointmentAdding', 'onAppointmentAdded']); + await insertStylesheetRulesToPage(page, REDUCE_CELLS_CSS); + await page.evaluate(({ ds, off }) => { + const win = window as any; + win.DevExpress.fx.off = true; + ($('#container') as any).dxScheduler({ + currentDate: '2023-09-05', + height: 800, + dataSource: ds, + currentView: 'week', + cellDuration: 60, + offset: off, + onAppointmentAdding: ({ appointmentData }) => { + win.clientTesting.onAppointmentAdding.push(appointmentData); + }, + onAppointmentAdded: ({ appointmentData }) => { + win.clientTesting.onAppointmentAdded.push(appointmentData); + }, + }); + }, { ds: STANDARD_DATA_SOURCE, off: offset }); + + const expectedAppointmentData = { + allDay: false, + startDate: getCellDateWithOffset('2023-09-05T01:00:00Z', offset), + endDate: getCellDateWithOffset('2023-09-05T02:00:00Z', offset), + text: '', + recurrenceRule: '', + }; + + const cell = page.locator('.dx-scheduler-date-table-row').nth(1) + .locator('.dx-scheduler-date-table-cell').nth(2); + await cell.dblclick(); + + const saveButton = page.locator('.dx-scheduler-appointment-popup .dx-popup-done'); + await saveButton.click(); + + const addingResults = await getClientResults(page, 'onAppointmentAdding'); + const addedResults = await getClientResults(page, 'onAppointmentAdded'); + + expect(addingResults[0]).toEqual(expectedAppointmentData); + expect(addedResults[0]).toEqual(expectedAppointmentData); + + await clearClientData(page, ['onAppointmentAdding', 'onAppointmentAdded']); + }); + + test(`onAppointmentClick and onAppointmentDbClick (offset: ${offset})`, async ({ page }) => { + await initClientTesting(page, ['onAppointmentClick', 'onAppointmentDblClick']); + await insertStylesheetRulesToPage(page, REDUCE_CELLS_CSS); + await page.evaluate(({ ds, off }) => { + const win = window as any; + win.DevExpress.fx.off = true; + ($('#container') as any).dxScheduler({ + currentDate: '2023-09-05', + height: 800, + dataSource: ds, + currentView: 'week', + cellDuration: 60, + offset: off, + onAppointmentClick: ({ appointmentData, targetedAppointmentData }) => { + win.clientTesting.onAppointmentClick.push({ appointmentData, targetedAppointmentData }); + }, + onAppointmentDblClick: ({ appointmentData, targetedAppointmentData }) => { + win.clientTesting.onAppointmentDblClick.push({ appointmentData, targetedAppointmentData }); + }, + }); + }, { ds: STANDARD_DATA_SOURCE, off: offset }); + + const appointment = page.locator('.dx-scheduler-appointment').filter({ hasText: APPOINTMENT_TITLE }); + await appointment.click(); + await appointment.dblclick(); + + const clickResults = await getClientResults(page, 'onAppointmentClick'); + const dblClickResults = await getClientResults(page, 'onAppointmentDblClick'); + + expect(clickResults[0].appointmentData).toEqual(EXPECTED.appointmentData); + expect(dblClickResults[0].appointmentData).toEqual(EXPECTED.appointmentData); + + await clearClientData(page, ['onAppointmentClick', 'onAppointmentDblClick']); + }); + + test(`onAppointmentTooltipShowing and onAppointmentFormOpening (offset: ${offset})`, async ({ page }) => { + await initClientTesting(page, ['onAppointmentTooltipShowing', 'onAppointmentFormOpening']); + await insertStylesheetRulesToPage(page, REDUCE_CELLS_CSS); + await page.evaluate(({ ds, off }) => { + const win = window as any; + win.DevExpress.fx.off = true; + ($('#container') as any).dxScheduler({ + currentDate: '2023-09-05', + height: 800, + dataSource: ds, + currentView: 'week', + cellDuration: 60, + offset: off, + onAppointmentTooltipShowing: ({ appointments }) => { + const tooltipAppointmentData = appointments?.map(({ appointmentData, currentAppointmentData }) => ({ + appointmentData, + currentAppointmentData, + })); + win.clientTesting.onAppointmentTooltipShowing.push(tooltipAppointmentData); + }, + onAppointmentFormOpening: ({ appointmentData }) => { + win.clientTesting.onAppointmentFormOpening.push(appointmentData); + }, + }); + }, { ds: STANDARD_DATA_SOURCE, off: offset }); + + const appointment = page.locator('.dx-scheduler-appointment').filter({ hasText: APPOINTMENT_TITLE }); + await appointment.click(); + + const tooltip = page.locator('.dx-scheduler-appointment-tooltip'); + await expect(tooltip).toBeVisible(); + + await appointment.dblclick(); + + const tooltipResults = await getClientResults(page, 'onAppointmentTooltipShowing'); + const formResults = await getClientResults(page, 'onAppointmentFormOpening'); + + expect(tooltipResults[0][0].appointmentData).toEqual(EXPECTED.appointmentData); + expect(formResults[0]).toEqual(EXPECTED.appointmentData); + + await clearClientData(page, ['onAppointmentTooltipShowing', 'onAppointmentFormOpening']); + }); + + test(`onAppointmentDeleting and onAppointmentDeleted (offset: ${offset})`, async ({ page }) => { + await initClientTesting(page, ['onAppointmentDeleting', 'onAppointmentDeleted']); + await insertStylesheetRulesToPage(page, REDUCE_CELLS_CSS); + await page.evaluate(({ ds, off }) => { + const win = window as any; + win.DevExpress.fx.off = true; + ($('#container') as any).dxScheduler({ + currentDate: '2023-09-05', + height: 800, + dataSource: ds, + currentView: 'week', + cellDuration: 60, + offset: off, + onAppointmentDeleting: ({ appointmentData }) => { + win.clientTesting.onAppointmentDeleting.push(appointmentData); + }, + onAppointmentDeleted: ({ appointmentData }) => { + win.clientTesting.onAppointmentDeleted.push(appointmentData); + }, + }); + }, { ds: STANDARD_DATA_SOURCE, off: offset }); + + const appointment = page.locator('.dx-scheduler-appointment').filter({ hasText: APPOINTMENT_TITLE }); + await appointment.click(); + + const tooltip = page.locator('.dx-scheduler-appointment-tooltip'); + await expect(tooltip).toBeVisible(); + + const deleteButton = page.locator('.dx-tooltip-appointment-item-delete-button'); + await deleteButton.click(); + + const deletingResults = await getClientResults(page, 'onAppointmentDeleting'); + const deletedResults = await getClientResults(page, 'onAppointmentDeleted'); + + expect(deletingResults[0]).toEqual(EXPECTED.appointmentData); + expect(deletedResults[0]).toEqual(EXPECTED.appointmentData); + + await clearClientData(page, ['onAppointmentDeleting', 'onAppointmentDeleted']); + }); + + test(`onAppointmentUpdating and onAppointmentUpdated (offset: ${offset})`, async ({ page }) => { + await initClientTesting(page, ['onAppointmentUpdating', 'onAppointmentUpdated']); + await insertStylesheetRulesToPage(page, REDUCE_CELLS_CSS); + await page.evaluate(({ ds, off }) => { + const win = window as any; + win.DevExpress.fx.off = true; + ($('#container') as any).dxScheduler({ + currentDate: '2023-09-05', + height: 800, + dataSource: ds, + currentView: 'week', + cellDuration: 60, + offset: off, + onAppointmentUpdating: ({ newData, oldData }) => { + win.clientTesting.onAppointmentUpdating.push({ newData, oldData }); + }, + onAppointmentUpdated: ({ appointmentData }) => { + win.clientTesting.onAppointmentUpdated.push(appointmentData); + }, + }); + }, { ds: STANDARD_DATA_SOURCE, off: offset }); + + const expectedOldData = { + startDate: '2023-09-06T12:30:00', + endDate: '2023-09-06T13:00:00', + text: APPOINTMENT_TITLE, + }; + const expectedNewData = getAppointmentAfterUpdate(offset); + + const appointment = page.locator('.dx-scheduler-appointment').filter({ hasText: APPOINTMENT_TITLE }); + // TODO: t.drag(element, -100, 0) - drag by pixel offset + const box = await appointment.boundingBox(); + if (box) { + await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2); + await page.mouse.down(); + await page.mouse.move(box.x + box.width / 2 - 100, box.y + box.height / 2, { steps: 5 }); + await page.mouse.up(); + } + + const updatingResults = await getClientResults(page, 'onAppointmentUpdating'); + const updatedResults = await getClientResults(page, 'onAppointmentUpdated'); + + expect(updatingResults[0].newData).toEqual(expectedNewData); + expect(updatingResults[0].oldData).toEqual(expectedOldData); + expect(updatedResults[0]).toEqual(expectedNewData); + + await clearClientData(page, ['onAppointmentUpdating', 'onAppointmentUpdated']); + }); + + test(`onAppointmentContextMenu (offset: ${offset})`, async ({ page }) => { + await initClientTesting(page, ['onAppointmentContextMenu']); + await insertStylesheetRulesToPage(page, REDUCE_CELLS_CSS); + await page.evaluate(({ ds, off }) => { + const win = window as any; + win.DevExpress.fx.off = true; + ($('#container') as any).dxScheduler({ + currentDate: '2023-09-05', + height: 800, + dataSource: ds, + currentView: 'week', + cellDuration: 60, + offset: off, + onAppointmentContextMenu: ({ appointmentData, targetedAppointmentData }) => { + win.clientTesting.onAppointmentContextMenu.push({ appointmentData, targetedAppointmentData }); + }, + }); + }, { ds: STANDARD_DATA_SOURCE, off: offset }); + + const appointment = page.locator('.dx-scheduler-appointment').filter({ hasText: APPOINTMENT_TITLE }); + await appointment.click({ button: 'right' }); + + const contextMenuResults = await getClientResults(page, 'onAppointmentContextMenu'); + + expect(contextMenuResults[0].appointmentData).toEqual(EXPECTED.appointmentData); + + await clearClientData(page, ['onAppointmentContextMenu']); + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/viewOffset/common/currentTimeIndicator.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/viewOffset/common/currentTimeIndicator.spec.ts new file mode 100644 index 000000000000..5ca7f12f2ce9 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/viewOffset/common/currentTimeIndicator.spec.ts @@ -0,0 +1,87 @@ +import { test } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +const getScreenshotName = ( + view: string, + indicatorTime: string, + offset: number, + startDayHour: number, + endDayHour: number, +): string => `offset_time-indicator_view-${view}_now-${indicatorTime.replace(/:/g, '-')}_offset-${offset}_start-${startDayHour}_end-${endDayHour}.png`; + +const TEST_CASES: [string, string, number, number, number, number][] = [ + ['day', '2023-12-04T00:00:00', 120, 720, 0, 24], + ['day', '2023-12-04T00:00:00', 120, 720, 6, 18], + ['day', '2023-12-04T12:00:00', 120, 1440, 0, 24], + ['day', '2023-12-04T12:00:00', 120, 1440, 6, 18], + ['day', '2023-12-03T00:00:00', 120, -720, 0, 24], + ['day', '2023-12-03T00:00:00', 120, -720, 6, 18], + ['day', '2023-12-02T12:00:00', 120, -1440, 0, 24], + ['day', '2023-12-02T12:00:00', 120, -1440, 6, 18], + ['week', '2023-12-06T00:00:00', 120, 720, 0, 24], + ['week', '2023-12-06T00:00:00', 120, 720, 6, 18], + ['week', '2023-12-06T12:00:00', 120, 1440, 0, 24], + ['week', '2023-12-06T12:00:00', 120, 1440, 6, 18], + ['week', '2023-12-05T00:00:00', 120, -720, 0, 24], + ['week', '2023-12-05T00:00:00', 120, -720, 6, 18], + ['week', '2023-12-04T12:00:00', 120, -1440, 0, 24], + ['week', '2023-12-04T12:00:00', 120, -1440, 6, 18], + ['timelineDay', '2023-12-04T00:00:00', 360, 720, 0, 24], + ['timelineDay', '2023-12-04T00:00:00', 360, 720, 6, 18], + ['timelineDay', '2023-12-04T12:00:00', 360, 1440, 0, 24], + ['timelineDay', '2023-12-04T12:00:00', 360, 1440, 6, 18], + ['timelineDay', '2023-12-03T00:00:00', 360, -720, 0, 24], + ['timelineDay', '2023-12-03T00:00:00', 360, -720, 6, 18], + ['timelineDay', '2023-12-02T12:00:00', 360, -1440, 0, 24], + ['timelineDay', '2023-12-02T12:00:00', 360, -1440, 6, 18], + ['timelineWeek', '2023-12-04T00:00:00', 360, 720, 0, 24], + ['timelineWeek', '2023-12-04T00:00:00', 360, 720, 6, 18], + ['timelineWeek', '2023-12-04T12:00:00', 360, 1440, 0, 24], + ['timelineWeek', '2023-12-04T12:00:00', 360, 1440, 6, 18], + ['timelineWeek', '2023-12-03T00:00:00', 360, -720, 0, 24], + ['timelineWeek', '2023-12-03T00:00:00', 360, -720, 6, 18], + ['timelineWeek', '2023-12-02T12:00:00', 360, -1440, 0, 24], + ['timelineWeek', '2023-12-02T12:00:00', 360, -1440, 6, 18], + ['timelineMonth', '2023-12-04T00:00:00', 120, 720, 0, 24], + ['timelineMonth', '2023-12-04T00:00:00', 120, 720, 6, 18], + ['timelineMonth', '2023-12-04T12:00:00', 120, 1440, 0, 24], + ['timelineMonth', '2023-12-04T12:00:00', 120, 1440, 6, 18], + ['timelineMonth', '2023-12-03T00:00:00', 120, -720, 0, 24], + ['timelineMonth', '2023-12-03T00:00:00', 120, -720, 6, 18], + ['timelineMonth', '2023-12-02T12:00:00', 120, -1440, 0, 24], + ['timelineMonth', '2023-12-02T12:00:00', 120, -1440, 6, 18], +]; + +test.describe('Offset: Current time indicator', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + TEST_CASES.forEach(([view, indicatorTime, cellDuration, offset, startDayHour, endDayHour]) => { + test(`Should correctly render current time indicator (${view}, now: ${indicatorTime}, offset: ${offset}, start: ${startDayHour}, end: ${endDayHour})`, async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource: [], + currentView: view, + shadeUntilCurrentTime: true, + currentDate: '2023-12-03', + indicatorTime, + cellDuration, + offset, + startDayHour, + endDayHour, + }); + + const workSpace = page.locator('.dx-scheduler-work-space'); + const screenshotName = getScreenshotName(view, indicatorTime, offset, startDayHour, endDayHour); + await testScreenshot(page, screenshotName, { element: workSpace }); + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/viewOffset/common/dragAndDrop.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/viewOffset/common/dragAndDrop.spec.ts new file mode 100644 index 000000000000..ad005b760c5b --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/viewOffset/common/dragAndDrop.spec.ts @@ -0,0 +1,79 @@ +import { test } from '@playwright/test'; +import { createWidget, insertStylesheetRulesToPage, testScreenshot } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +const APPOINTMENT_TITLE = 'Test'; +const REDUCE_CELLS_CSS = ` +.dx-scheduler-cell-sizes-vertical { + height: 25px; +}`; +const APPOINTMENTS: Record[]> = { + week: [{ startDate: '2023-09-05T05:00:00', endDate: '2023-09-05T09:00:00', text: APPOINTMENT_TITLE }], + month: [{ startDate: '2023-09-05T10:00:00', endDate: '2023-09-06T15:00:00', text: APPOINTMENT_TITLE }], + timelineMonth: [{ startDate: '2023-09-02T10:00:00', endDate: '2023-09-03T15:00:00', text: APPOINTMENT_TITLE }], + allDayWeek: [{ startDate: '2023-09-05T05:00:00', endDate: '2023-09-05T09:00:00', text: APPOINTMENT_TITLE, allDay: true }], + allDayMonth: [{ startDate: '2023-09-05T10:00:00', endDate: '2023-09-06T15:00:00', text: APPOINTMENT_TITLE, allDay: true }], + allDayTimelineMonth: [{ startDate: '2023-09-02T10:00:00', endDate: '2023-09-03T15:00:00', text: APPOINTMENT_TITLE, allDay: true }], +}; + +const getDragCoordinatesByView = (viewType: string): { x: number; y: number } => { + switch (viewType) { + case 'week': return { x: 150, y: 0 }; + case 'month': return { x: 300, y: 300 }; + default: return { x: 300, y: 0 }; + } +}; + +const getScreenshotName = (viewType: string, offset: number, isAllDay: boolean) => + `offset_drag-n-drop_${isAllDay ? 'all-day' : 'usual'}-appts_${viewType}_offset-${offset}.png`; + +test.describe('Offset: Drag-n-drop appointments', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + [ + { views: [{ type: 'week', cellDuration: 60 }], dataSource: APPOINTMENTS.week, isAllDay: false }, + { views: [{ type: 'week', cellDuration: 60 }], dataSource: APPOINTMENTS.allDayWeek, isAllDay: true }, + { views: [{ type: 'month' }], dataSource: APPOINTMENTS.month, isAllDay: false }, + { views: [{ type: 'month' }], dataSource: APPOINTMENTS.allDayMonth, isAllDay: true }, + { views: [{ type: 'timelineMonth' }], dataSource: APPOINTMENTS.timelineMonth, isAllDay: false }, + { views: [{ type: 'timelineMonth' }], dataSource: APPOINTMENTS.allDayTimelineMonth, isAllDay: true }, + ].forEach(({ views, dataSource, isAllDay }) => { + [0, 735, -735].forEach((offset) => { + test(`Drag-n-drop (view: ${views[0].type}, allDay: ${isAllDay}, offset: ${offset})`, async ({ page }) => { + await insertStylesheetRulesToPage(page, REDUCE_CELLS_CSS); + await createWidget(page, 'dxScheduler', { + currentDate: '2023-09-07', + height: 800, + dataSource, + views, + currentView: views[0].type, + offset, + }); + + const appointment = page.locator('.dx-scheduler-appointment').filter({ hasText: APPOINTMENT_TITLE }); + const viewType = views[0].type; + const { x, y } = getDragCoordinatesByView(viewType); + + const box = await appointment.boundingBox(); + if (box) { + await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2); + await page.mouse.down(); + await page.mouse.move(box.x + box.width / 2 + x, box.y + box.height / 2 + y, { steps: 5 }); + await page.mouse.up(); + } + + const workSpace = page.locator('.dx-scheduler-work-space'); + await testScreenshot(page, getScreenshotName(viewType, offset, isAllDay), { element: workSpace }); + }); + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/viewOffset/common/expressions.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/viewOffset/common/expressions.spec.ts new file mode 100644 index 000000000000..5fccc9a2c8c6 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/viewOffset/common/expressions.spec.ts @@ -0,0 +1,90 @@ +import { test } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +const APPOINTMENT_TITLES = { usual: 'Usual', allDay: 'All-day' }; +const APPOINTMENTS = { + week: [ + { StartDate2: '2023-09-06T04:00:00', EndDate2: '2023-09-06T06:00:00', Text2: APPOINTMENT_TITLES.usual }, + { StartDate2: '2023-09-06T00:00:00', EndDate2: '2023-09-06T00:00:00', Text2: APPOINTMENT_TITLES.allDay, AllDay2: true }, + ], +}; + +test.describe('Offset: Appointment expressions', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + [ + { views: [{ type: 'week', cellDuration: 60 }], dataSource: APPOINTMENTS.week }, + ].forEach(({ views, dataSource }) => { + [0, 180, -180].forEach((offset) => { + test(`Appointment with expr common test (view: ${views[0].type}, offset: ${offset})`, async ({ page }) => { + await createWidget(page, 'dxScheduler', { + currentDate: '2023-09-05', + height: 800, + dataSource, + views, + currentView: views[0].type, + offset, + startDateExpr: 'StartDate2', + endDateExpr: 'EndDate2', + textExpr: 'Text2', + allDayExpr: 'AllDay2', + }); + + const workSpace = page.locator('.dx-scheduler-work-space'); + const usualAppointment = page.locator('.dx-scheduler-appointment').filter({ hasText: APPOINTMENT_TITLES.usual }); + const allDayAppointment = page.locator('.dx-scheduler-appointment').filter({ hasText: APPOINTMENT_TITLES.allDay }); + const viewType = views[0].type; + + await testScreenshot(page, `offset_appt-expr_${viewType}_offset-${offset}.png`, { element: workSpace }); + + let box = await usualAppointment.boundingBox(); + if (box) { + await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2); + await page.mouse.down(); + await page.mouse.move(box.x + box.width / 2 + 100, box.y + box.height / 2 + 100, { steps: 5 }); + await page.mouse.up(); + } + + box = await allDayAppointment.boundingBox(); + if (box) { + await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2); + await page.mouse.down(); + await page.mouse.move(box.x + box.width / 2 - 100, box.y + box.height / 2, { steps: 5 }); + await page.mouse.up(); + } + + await testScreenshot(page, `offset_appt-expr_drag-n-drop_${viewType}_offset-${offset}.png`, { element: workSpace }); + + const usualResizeBottom = usualAppointment.locator('.dx-resizable-handle-bottom'); + box = await usualResizeBottom.boundingBox(); + if (box) { + await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2); + await page.mouse.down(); + await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2 + 100, { steps: 5 }); + await page.mouse.up(); + } + + const allDayResizeLeft = allDayAppointment.locator('.dx-resizable-handle-left'); + box = await allDayResizeLeft.boundingBox(); + if (box) { + await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2); + await page.mouse.down(); + await page.mouse.move(box.x + box.width / 2 - 100, box.y + box.height / 2, { steps: 5 }); + await page.mouse.up(); + } + + await testScreenshot(page, `offset_appt-expr_resize_${viewType}_offset-${offset}.png`, { element: workSpace }); + }); + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/viewOffset/common/keyboardNavigation.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/viewOffset/common/keyboardNavigation.spec.ts new file mode 100644 index 000000000000..806f1896af20 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/viewOffset/common/keyboardNavigation.spec.ts @@ -0,0 +1,75 @@ +import { test } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +const KEYBOARD_ACTIONS: Record = { + day: ['ArrowDown', 'ArrowDown', 'ArrowDown', 'ArrowUp'], + week: ['ArrowUp', 'ArrowRight', 'ArrowDown', 'ArrowDown', 'ArrowLeft', 'ArrowLeft', 'ArrowUp', 'ArrowUp'], + month: ['ArrowUp', 'ArrowRight', 'ArrowDown', 'ArrowDown', 'ArrowLeft', 'ArrowLeft', 'ArrowUp', 'ArrowUp'], + timelineDay: ['ArrowRight', 'ArrowRight', 'ArrowRight', 'ArrowLeft'], + timelineMonth: ['ArrowRight', 'ArrowRight', 'ArrowRight', 'ArrowLeft'], +}; + +test.describe('Offset: Keyboard navigation', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + [0, -120, 120].forEach((offset) => { + [ + { view: 'day', startCell: [1, 0], keyboardKeys: KEYBOARD_ACTIONS.day }, + { view: 'week', startCell: [3, 3], keyboardKeys: KEYBOARD_ACTIONS.week }, + { view: 'month', startCell: [3, 3], keyboardKeys: KEYBOARD_ACTIONS.month }, + { view: 'timelineDay', startCell: [0, 1], keyboardKeys: KEYBOARD_ACTIONS.timelineDay }, + { view: 'timelineMonth', startCell: [0, 1], keyboardKeys: KEYBOARD_ACTIONS.timelineMonth }, + ].forEach(({ view, startCell, keyboardKeys }) => { + test(`Keyboard navigation should work (view: ${view}, offset: ${offset})`, async ({ page }) => { + await createWidget(page, 'dxScheduler', { + currentDate: '2023-09-07', + height: 800, + dataSource: [], + currentView: view, + offset, + }); + + const [rowIdx, cellIdx] = startCell; + const startCellLocator = page.locator('.dx-scheduler-date-table-row').nth(rowIdx) + .locator('.dx-scheduler-date-table-cell').nth(cellIdx); + + await startCellLocator.click(); + for (const key of keyboardKeys) { + await page.keyboard.press(key); + } + + const workSpace = page.locator('.dx-scheduler-work-space'); + await testScreenshot(page, `offset_keyboard_${view}_offset-${offset}.png`, { element: workSpace }); + }); + }); + + test(`Keyboard navigation in the all-day panel should work (view: week, offset: ${offset})`, async ({ page }) => { + await createWidget(page, 'dxScheduler', { + currentDate: '2023-09-07', + height: 800, + dataSource: [], + currentView: 'week', + offset, + }); + + const startCellLocator = page.locator('.dx-scheduler-all-day-table-cell').nth(1); + await startCellLocator.click(); + await page.keyboard.press('ArrowRight'); + await page.keyboard.press('ArrowRight'); + await page.keyboard.press('ArrowLeft'); + + const workSpace = page.locator('.dx-scheduler-work-space'); + await testScreenshot(page, `offset_keyboard_week-all-day_offset-${offset}.png`, { element: workSpace }); + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/viewOffset/common/multiCellSelection.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/viewOffset/common/multiCellSelection.spec.ts new file mode 100644 index 000000000000..5159b0bf7ef1 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/viewOffset/common/multiCellSelection.spec.ts @@ -0,0 +1,87 @@ +import { test } from '@playwright/test'; +import { createWidget, testScreenshot } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +test.describe('Offset: Multi cell selection', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + [0, -120, 120].forEach((offset) => { + [true, false].forEach((rtlEnabled) => { + [ + { view: 'day', dragOptions: { direction: 'increase', from: [0, 0], to: [7, 0] } }, + { view: 'day', dragOptions: { direction: 'decrease', from: [7, 0], to: [0, 0] } }, + { view: 'week', dragOptions: { direction: 'increase_0', from: [0, 2], to: [7, 2] } }, + { view: 'week', dragOptions: { direction: 'decrease_0', from: [7, 2], to: [0, 2] } }, + { view: 'week', dragOptions: { direction: 'increase_1', from: [1, 3], to: [8, 4] } }, + { view: 'week', dragOptions: { direction: 'decrease_1', from: [8, 4], to: [1, 3] } }, + { view: 'week', dragOptions: { direction: 'increase_2', from: [6, 3], to: [6, 5] } }, + { view: 'week', dragOptions: { direction: 'decrease_2', from: [6, 5], to: [6, 3] } }, + { view: 'week', dragOptions: { direction: 'increase_3', from: [0, 0], to: [11, 6] } }, + { view: 'week', dragOptions: { direction: 'decrease_3', from: [11, 6], to: [0, 0] } }, + { view: 'month', dragOptions: { direction: 'increase', from: [2, 2], to: [3, 1] } }, + { view: 'month', dragOptions: { direction: 'decrease', from: [3, 1], to: [2, 2] } }, + { view: 'timelineDay', dragOptions: { direction: 'increase', from: [0, 0], to: [0, 5] } }, + { view: 'timelineDay', dragOptions: { direction: 'decrease', from: [0, 5], to: [0, 0] } }, + { view: 'timelineMonth', dragOptions: { direction: 'increase', from: [0, 0], to: [0, 3] } }, + { view: 'timelineMonth', dragOptions: { direction: 'decrease', from: [0, 3], to: [0, 0] } }, + ].forEach(({ view, dragOptions }) => { + test(`Multi cell selection (view: ${view}, offset: ${offset}, dir: ${dragOptions.direction}, rtl: ${rtlEnabled})`, async ({ page }) => { + await createWidget(page, 'dxScheduler', { + currentDate: '2023-09-07', + height: 800, + dataSource: [], + currentView: view, + offset, + rtlEnabled, + }); + + const { direction, from: [fromRow, fromCell], to: [toRow, toCell] } = dragOptions; + const firstCellLocator = page.locator('.dx-scheduler-date-table-row').nth(fromRow) + .locator('.dx-scheduler-date-table-cell').nth(fromCell); + const secondCellLocator = page.locator('.dx-scheduler-date-table-row').nth(toRow) + .locator('.dx-scheduler-date-table-cell').nth(toCell); + + await firstCellLocator.dragTo(secondCellLocator); + + const workSpace = page.locator('.dx-scheduler-work-space'); + await testScreenshot( + page, + `offset_multi-cell-select_${view}_offset-${offset}_${direction}${rtlEnabled ? '_rtl' : ''}.png`, + { element: workSpace }, + ); + }); + }); + + test(`Multi cell selection in all-day panel (view: week, offset: ${offset}, rtl: ${rtlEnabled})`, async ({ page }) => { + await createWidget(page, 'dxScheduler', { + currentDate: '2023-09-07', + height: 800, + dataSource: [], + currentView: 'week', + offset, + }); + + const firstCellLocator = page.locator('.dx-scheduler-all-day-table-cell').nth(0); + const secondCellLocator = page.locator('.dx-scheduler-all-day-table-cell').nth(3); + + await firstCellLocator.dragTo(secondCellLocator); + + const workSpace = page.locator('.dx-scheduler-work-space'); + await testScreenshot( + page, + `offset_multi-cell-select_week-all-day_offset-${offset}${rtlEnabled ? '_rtl' : ''}.png`, + { element: workSpace }, + ); + }); + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/viewOffset/common/resize.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/viewOffset/common/resize.spec.ts new file mode 100644 index 000000000000..9a734d3980ff --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/viewOffset/common/resize.spec.ts @@ -0,0 +1,170 @@ +import { test } from '@playwright/test'; +import type { Page, Locator } from '@playwright/test'; +import { createWidget, insertStylesheetRulesToPage, testScreenshot } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +const APPOINTMENT_TITLES = { usual: 'Usual', allDay: 'All-day' }; +const REDUCE_CELLS_CSS = ` +.dx-scheduler-cell-sizes-vertical { + height: 25px; +}`; +const APPOINTMENTS: Record[]> = { + week: [ + { startDate: '2023-09-05T05:00:00', endDate: '2023-09-05T09:00:00', text: APPOINTMENT_TITLES.usual }, + { startDate: '2023-09-05T00:00:00', endDate: '2023-09-06T00:00:00', text: APPOINTMENT_TITLES.allDay, allDay: true }, + ], + month: [ + { startDate: '2023-09-05T10:00:00', endDate: '2023-09-06T15:00:00', text: APPOINTMENT_TITLES.usual }, + { startDate: '2023-09-05T00:00:00', endDate: '2023-09-06T00:00:00', text: APPOINTMENT_TITLES.allDay, allDay: true }, + ], + timelineMonth: [ + { startDate: '2023-09-02T10:00:00', endDate: '2023-09-03T15:00:00', text: APPOINTMENT_TITLES.usual }, + { startDate: '2023-09-02T00:00:00', endDate: '2023-09-03T00:00:00', text: APPOINTMENT_TITLES.allDay, allDay: true }, + ], +}; + +enum ResizeType { + startPlus = 'start-plus', + startMinus = 'start-minus', + endPlus = 'end-plus', + endMinus = 'end-minus', +} + +const isVerticalView = (viewType: string, isAllDay: boolean): boolean => !isAllDay && viewType === 'week'; +const isStartResize = (resizeType: ResizeType): boolean => + resizeType === ResizeType.startPlus || resizeType === ResizeType.startMinus; + +const getResizableHandle = (appointment: Locator, viewType: string, resizeType: ResizeType, isAllDay: boolean): Locator => { + if (isVerticalView(viewType, isAllDay) && isStartResize(resizeType)) return appointment.locator('.dx-resizable-handle-top'); + if (isVerticalView(viewType, isAllDay) && !isStartResize(resizeType)) return appointment.locator('.dx-resizable-handle-bottom'); + if (isStartResize(resizeType)) return appointment.locator('.dx-resizable-handle-left'); + return appointment.locator('.dx-resizable-handle-right'); +}; + +const getResizableValues = (viewType: string, resizeType: ResizeType, isAllDay: boolean): { x: number; y: number } => { + if (isVerticalView(viewType, isAllDay) && resizeType === ResizeType.startPlus) return { x: 0, y: -100 }; + if (isVerticalView(viewType, isAllDay) && resizeType === ResizeType.startMinus) return { x: 0, y: 50 }; + if (isVerticalView(viewType, isAllDay) && resizeType === ResizeType.endPlus) return { x: 0, y: 100 }; + if (isVerticalView(viewType, isAllDay) && resizeType === ResizeType.endMinus) return { x: 0, y: -50 }; + if (resizeType === ResizeType.startPlus) return { x: -100, y: 0 }; + if (resizeType === ResizeType.startMinus) return { x: 50, y: 0 }; + if (resizeType === ResizeType.endPlus) return { x: 100, y: 0 }; + return { x: -50, y: 0 }; +}; + +const doResize = async (page: Page, appointment: Locator, viewType: string, resizeType: ResizeType, isAllDay: boolean): Promise => { + const handle = getResizableHandle(appointment, viewType, resizeType, isAllDay); + const { x, y } = getResizableValues(viewType, resizeType, isAllDay); + const box = await handle.boundingBox(); + if (box) { + await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2); + await page.mouse.down(); + await page.mouse.move(box.x + box.width / 2 + x, box.y + box.height / 2 + y, { steps: 5 }); + await page.mouse.up(); + } +}; + +const getScreenshotName = (viewType: string, resizeType: string, offset: number) => + `offset_resize-appts_${viewType}_${resizeType}_offset-${offset}.png`; + +test.describe('Offset: Resize appointments', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + [ + { views: [{ type: 'week', cellDuration: 60 }], dataSource: APPOINTMENTS.week }, + { views: [{ type: 'month', firstDayOfWeek: 0 }], dataSource: APPOINTMENTS.month }, + { views: [{ type: 'timelineMonth' }], dataSource: APPOINTMENTS.timelineMonth }, + ].forEach(({ views, dataSource }) => { + [0, 735, -735].forEach((offset) => { + test(`Appointments resize common (view: ${views[0].type}, offset: ${offset})`, async ({ page }) => { + await insertStylesheetRulesToPage(page, REDUCE_CELLS_CSS); + await createWidget(page, 'dxScheduler', { + currentDate: '2023-09-07', height: 800, dataSource, views, currentView: views[0].type, offset, + }); + + const usualAppt = page.locator('.dx-scheduler-appointment').filter({ hasText: APPOINTMENT_TITLES.usual }); + const allDayAppt = page.locator('.dx-scheduler-appointment').filter({ hasText: APPOINTMENT_TITLES.allDay }); + const viewType = views[0].type; + const workSpace = page.locator('.dx-scheduler-work-space'); + + await doResize(page, usualAppt, viewType, ResizeType.startMinus, false); + await doResize(page, allDayAppt, viewType, ResizeType.startMinus, true); + await testScreenshot(page, getScreenshotName(viewType, ResizeType.startMinus, offset), { element: workSpace }); + + await doResize(page, usualAppt, viewType, ResizeType.startPlus, false); + await doResize(page, allDayAppt, viewType, ResizeType.startPlus, true); + await testScreenshot(page, getScreenshotName(viewType, ResizeType.startPlus, offset), { element: workSpace }); + + await doResize(page, usualAppt, viewType, ResizeType.endMinus, false); + await doResize(page, allDayAppt, viewType, ResizeType.endMinus, true); + await testScreenshot(page, getScreenshotName(viewType, ResizeType.endMinus, offset), { element: workSpace }); + + await doResize(page, usualAppt, viewType, ResizeType.endPlus, false); + await doResize(page, allDayAppt, viewType, ResizeType.endPlus, true); + await testScreenshot(page, getScreenshotName(viewType, ResizeType.endPlus, offset), { element: workSpace }); + }); + }); + }); + + [-720, 720].forEach((offset) => { + test(`Resize with startDayHour/endDayHour (view: week, offset: ${offset})`, async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource: [ + { startDate: '2023-09-06T22:00:00', endDate: '2023-09-07T00:00:00', text: APPOINTMENT_TITLES.usual }, + { startDate: '2023-09-06T00:00:00', endDate: '2023-09-06T00:00:00', allDay: true, text: APPOINTMENT_TITLES.allDay }, + ], + currentView: 'week', startDayHour: 10, endDayHour: 12, currentDate: '2023-09-07', height: 800, offset, + }); + + const usualAppt = page.locator('.dx-scheduler-appointment').filter({ hasText: APPOINTMENT_TITLES.usual }); + const allDayAppt = page.locator('.dx-scheduler-appointment').filter({ hasText: APPOINTMENT_TITLES.allDay }); + + let box = await usualAppt.locator('.dx-resizable-handle-bottom').boundingBox(); + if (box) { await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2); await page.mouse.down(); await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2 - 50, { steps: 5 }); await page.mouse.up(); } + + box = await usualAppt.locator('.dx-resizable-handle-top').boundingBox(); + if (box) { await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2); await page.mouse.down(); await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2 + 50, { steps: 5 }); await page.mouse.up(); } + + box = await allDayAppt.locator('.dx-resizable-handle-left').boundingBox(); + if (box) { await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2); await page.mouse.down(); await page.mouse.move(box.x + box.width / 2 - 100, box.y + box.height / 2, { steps: 5 }); await page.mouse.up(); } + + box = await allDayAppt.locator('.dx-resizable-handle-right').boundingBox(); + if (box) { await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2); await page.mouse.down(); await page.mouse.move(box.x + box.width / 2 + 100, box.y + box.height / 2, { steps: 5 }); await page.mouse.up(); } + + const workSpace = page.locator('.dx-scheduler-work-space'); + await testScreenshot(page, `offset_resize-appts_week_offset-${offset}_startDayHour-10_endDayHour-12.png`, { element: workSpace }); + }); + }); + + [ + { offset: -720, currentDate: '2023-09-07' }, + { offset: 720, currentDate: '2023-09-06' }, + ].forEach(({ offset, currentDate }) => { + test(`Resize with startDayHour/endDayHour (view: timelineDay, offset: ${offset})`, async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource: [{ startDate: '2023-09-06T22:00:00', endDate: '2023-09-07T00:00:00', text: APPOINTMENT_TITLES.usual }], + currentView: 'timelineDay', startDayHour: 10, endDayHour: 12, height: 800, currentDate, offset, + }); + + const usualAppt = page.locator('.dx-scheduler-appointment').filter({ hasText: APPOINTMENT_TITLES.usual }); + + let box = await usualAppt.locator('.dx-resizable-handle-left').boundingBox(); + if (box) { await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2); await page.mouse.down(); await page.mouse.move(box.x + box.width / 2 + 200, box.y + box.height / 2, { steps: 5 }); await page.mouse.up(); } + + box = await usualAppt.locator('.dx-resizable-handle-right').boundingBox(); + if (box) { await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2); await page.mouse.down(); await page.mouse.move(box.x + box.width / 2 - 200, box.y + box.height / 2, { steps: 5 }); await page.mouse.up(); } + + const workSpace = page.locator('.dx-scheduler-work-space'); + await testScreenshot(page, `offset_resize-appts_timelineDay_offset-${offset}_startDayHour-10_endDayHour-12.png`, { element: workSpace }); + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/viewOffset/markup/allDayAppointments.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/viewOffset/markup/allDayAppointments.spec.ts new file mode 100644 index 000000000000..910e41f9de1c --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/viewOffset/markup/allDayAppointments.spec.ts @@ -0,0 +1,139 @@ +import { test } from '@playwright/test'; +import { createWidget, insertStylesheetRulesToPage, testScreenshot } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +const REDUCE_CELLS_CSS = ` +.dx-scheduler-cell-sizes-vertical { + height: 25px; +}`; + +const MS_IN_DAY = 24 * 60 * 60 * 1000; + +interface AppointmentData { + startTime: string; + endTime: string; + endDateShiftDays?: number; + allDay?: boolean; +} + +const getIsoDate = (date: Date, additionalDays = 0): string => { + const dateCopy = new Date(date.getTime() + additionalDays * MS_IN_DAY); + const [dateISO] = dateCopy.toISOString().split('T'); + return dateISO; +}; + +const timeToText = (time: string): string => { + const [hours, minutes] = time.split(':'); + return `${hours}:${minutes}`; +}; + +const generateAppointments = ( + startDateISO: string, + endDateISO: string, + appointments: AppointmentData[], +) => { + const startDate = new Date(startDateISO); + const endDate = new Date(endDateISO); + const diffTime = Math.abs(endDate.getTime() - startDate.getTime()); + const daysCount = Math.ceil((diffTime / MS_IN_DAY) + 1); + + return new Array(daysCount).fill(null).map((_, dayIdx) => { + const date = new Date(startDate.getTime() + MS_IN_DAY * dayIdx); + return new Array(appointments.length).fill(null).map((__, timeIdx) => { + const { startTime, endTime, endDateShiftDays, allDay } = appointments[timeIdx]; + const appointmentIdx = dayIdx * appointments.length + timeIdx; + const appointmentStartISO = getIsoDate(date); + const appointmentEndISO = getIsoDate(date, endDateShiftDays ?? 0); + const [, , dayISO] = appointmentStartISO.split('-'); + const titleText = `#${appointmentIdx}: ${dayISO.padStart(2, '0')} ${allDay ? 'All' : ''} ${timeToText(startTime)}-${timeToText(endTime)}`; + return { + startDate: `${appointmentStartISO}T${startTime}`, + endDate: `${appointmentEndISO}T${endTime}`, + text: titleText, + allDay, + }; + }); + }).flat(); +}; + +const ALL_DAY_APPOINTMENTS_DATA: AppointmentData[] = [ + { startTime: '02:00:00', endTime: '02:00:00', allDay: true, endDateShiftDays: 1 }, + { startTime: '20:30:00', endTime: '23:30:00', allDay: true }, +]; + +const APPOINTMENTS: Record[]> = { + day: [ + ...generateAppointments('2023-09-06', '2023-09-08', ALL_DAY_APPOINTMENTS_DATA), + { startDate: '2023-09-05T14:00:00', endDate: '2023-09-09T16:00:00', text: 'LONG APPT', allDay: true }, + ], + week: [ + ...generateAppointments('2023-09-02', '2023-09-10', ALL_DAY_APPOINTMENTS_DATA), + { startDate: '2023-09-01T14:00:00', endDate: '2023-09-12T16:00:00', text: 'LONG APPT', allDay: true }, + ], + workWeekWithFirstDay: [ + ...generateAppointments('2023-09-05', '2023-09-13', ALL_DAY_APPOINTMENTS_DATA), + { startDate: '2023-09-03T14:00:00', endDate: '2023-09-15T16:00:00', text: 'LONG APPT', allDay: true }, + ], + month: [ + ...generateAppointments('2023-08-26', '2023-10-08', ALL_DAY_APPOINTMENTS_DATA), + { startDate: '2023-08-24T14:00:00', endDate: '2023-10-10T16:00:00', text: 'LONG APPT', allDay: true }, + ], +}; + +const getScreenshotName = (viewType: string, offset: number, startDayHour: number, endDayHour: number, firstDay?: number) => + `view_markup_all-day_${viewType}_offset-${offset}_start-${startDayHour}_end-${endDayHour}_first-day-${firstDay}.png`; + +test.describe('Offset: Markup all-day appointments', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + [ + { views: [{ type: 'day', cellDuration: 60, firstDayOfWeek: 0 }], dataSource: APPOINTMENTS.day }, + { views: [{ type: 'week', cellDuration: 60, firstDayOfWeek: 0 }], dataSource: APPOINTMENTS.week }, + { views: [{ type: 'workWeek', cellDuration: 60, firstDayOfWeek: 0 }], dataSource: APPOINTMENTS.week }, + { views: [{ type: 'workWeek', cellDuration: 60, firstDayOfWeek: 3 }], dataSource: APPOINTMENTS.workWeekWithFirstDay }, + { views: [{ type: 'month', firstDayOfWeek: 0 }], dataSource: APPOINTMENTS.month }, + { views: [{ type: 'timelineDay', cellDuration: 240, firstDayOfWeek: 0 }], dataSource: APPOINTMENTS.day }, + { views: [{ type: 'timelineWeek', cellDuration: 480, firstDayOfWeek: 0 }], dataSource: APPOINTMENTS.week }, + { views: [{ type: 'timelineWorkWeek', cellDuration: 480, firstDayOfWeek: 0 }], dataSource: APPOINTMENTS.week }, + { views: [{ type: 'timelineWorkWeek', cellDuration: 480, firstDayOfWeek: 3 }], dataSource: APPOINTMENTS.workWeekWithFirstDay }, + { views: [{ type: 'timelineMonth', firstDayOfWeek: 0 }], dataSource: APPOINTMENTS.month }, + ].forEach(({ views, dataSource }) => { + [0, 735, 1440, -735, -1440].forEach((offset) => { + [ + { startDayHour: 0, endDayHour: 24 }, + { startDayHour: 9, endDayHour: 17 }, + ].forEach(({ startDayHour, endDayHour }) => { + test(`All-day appointments render (view: ${views[0].type}, offset: ${offset}, start: ${startDayHour}, end: ${endDayHour}, firstDay: ${(views[0] as any).firstDayOfWeek})`, async ({ page }) => { + await insertStylesheetRulesToPage(page, REDUCE_CELLS_CSS); + await createWidget(page, 'dxScheduler', { + currentDate: '2023-09-07', + height: 800, + maxAppointmentsPerCell: 'unlimited', + dataSource, + views, + currentView: views[0].type, + offset, + startDayHour, + endDayHour, + }); + + const workSpace = page.locator('.dx-scheduler-work-space'); + await testScreenshot( + page, + getScreenshotName(views[0].type, offset, startDayHour, endDayHour, (views[0] as any).firstDayOfWeek), + { element: workSpace }, + ); + }); + }); + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/viewOffset/markup/appointmentsOrdering.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/viewOffset/markup/appointmentsOrdering.spec.ts new file mode 100644 index 000000000000..43c19355ed72 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/viewOffset/markup/appointmentsOrdering.spec.ts @@ -0,0 +1,151 @@ +import { test } from '@playwright/test'; +import { createWidget, insertStylesheetRulesToPage, testScreenshot } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +const REDUCE_CELLS_CSS = ` +.dx-scheduler-cell-sizes-vertical { + height: 25px; +}`; + +const MS_IN_DAY = 24 * 60 * 60 * 1000; + +const getIsoDate = (date: Date, additionalDays = 0): string => { + const dateCopy = new Date(date.getTime() + additionalDays * MS_IN_DAY); + const [dateISO] = dateCopy.toISOString().split('T'); + return dateISO; +}; + +const timeToText = (time: string): string => { + const [hours, minutes] = time.split(':'); + return `${hours}:${minutes}`; +}; + +interface AppointmentData { + startTime: string; + endTime: string; + endDateShiftDays?: number; + text?: string; + allDay?: boolean; + recurrenceRule?: string; +} + +const generateAppointments = ( + startDateISO: string, + endDateISO: string, + appointments: AppointmentData[], +) => { + const startDate = new Date(startDateISO); + const endDate = new Date(endDateISO); + const diffTime = Math.abs(endDate.getTime() - startDate.getTime()); + const daysCount = Math.ceil((diffTime / MS_IN_DAY) + 1); + return new Array(daysCount).fill(null).map((_, dayIdx) => { + const date = new Date(startDate.getTime() + MS_IN_DAY * dayIdx); + return new Array(appointments.length).fill(null).map((__, timeIdx) => { + const { startTime, endTime, endDateShiftDays, text, allDay, recurrenceRule } = appointments[timeIdx]; + const appointmentIdx = dayIdx * appointments.length + timeIdx; + const appointmentStartISO = getIsoDate(date); + const appointmentEndISO = getIsoDate(date, endDateShiftDays ?? 0); + const [, , dayISO] = appointmentStartISO.split('-'); + const titleText = `#${appointmentIdx}: ${dayISO.padStart(2, '0')} ${allDay ? 'All' : ''} ${!text ? `${timeToText(startTime)}-${timeToText(endTime)}` : text}`; + return { startDate: `${appointmentStartISO}T${startTime}`, endDate: `${appointmentEndISO}T${endTime}`, text: titleText, allDay, recurrenceRule }; + }); + }).flat(); +}; + +const APPOINTMENTS_TIME: AppointmentData[] = [ + { startTime: '10:15:00', endTime: '16:15:00' }, + { startTime: '17:05:00', endTime: '22:05:00' }, +]; +const APPOINTMENTS_TIMELINE_TIME: AppointmentData[] = [ + { startTime: '04:00:00', endTime: '08:00:00', endDateShiftDays: 1 }, + { startTime: '10:15:00', endTime: '16:15:00', endDateShiftDays: 1 }, + { startTime: '17:05:00', endTime: '22:05:00', endDateShiftDays: 1 }, +]; + +const RECURRENT_APPOINTMENTS_MONTH = [ + { startDate: '2023-08-01T15:00:00', endDate: '2023-08-01T19:00:00', recurrenceRule: 'FREQ=WEEKLY;BYDAY=MO,WE,TH,FR', text: 'Daily 15-19' }, +]; +const RECURRENT_APPOINTMENTS_MONTH_TIMELINE = [ + { startDate: '2023-08-01T09:00:00', endDate: '2023-08-01T13:00:00', recurrenceRule: 'FREQ=HOURLY;INTERVAL=24', text: 'Hourly 09-13' }, + { startDate: '2023-08-01T15:00:00', endDate: '2023-08-01T19:00:00', recurrenceRule: 'FREQ=WEEKLY;BYDAY=MO,WE,TH,FR', text: 'Daily 15-19' }, +]; + +const APPOINTMENTS: Record[]> = { + month: [...generateAppointments('2023-08-26', '2023-10-08', APPOINTMENTS_TIME), ...RECURRENT_APPOINTMENTS_MONTH], + timelineMonth: [...generateAppointments('2023-08-31', '2023-09-08', APPOINTMENTS_TIMELINE_TIME), ...RECURRENT_APPOINTMENTS_MONTH_TIMELINE], +}; + +const getScreenshotName = (viewType: string, offset: number, startDayHour: number, endDayHour: number) => + `view_markup_ordering-appts_${viewType}_offset-${offset}_start-${startDayHour}_end-${endDayHour}.png`; + +test.describe('Offset: Markup appointments ordering', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + [ + { views: [{ type: 'month' }], dataSource: APPOINTMENTS.month }, + { views: [{ type: 'timelineMonth' }], dataSource: APPOINTMENTS.timelineMonth }, + ].forEach(({ views, dataSource }) => { + [0, 735, -735, 1440, -1440].forEach((offset) => { + [ + { startDayHour: 0, endDayHour: 24 }, + { startDayHour: 9, endDayHour: 17 }, + ].forEach(({ startDayHour, endDayHour }) => { + test(`Appointments ordering render (view: ${views[0].type}, offset: ${offset}, start: ${startDayHour}, end: ${endDayHour})`, async ({ page }) => { + await insertStylesheetRulesToPage(page, REDUCE_CELLS_CSS); + await createWidget(page, 'dxScheduler', { + currentDate: '2023-09-07', + height: 800, + maxAppointmentsPerCell: 'unlimited', + dataSource, + views: [views[0]], + currentView: views[0].type, + offset, + startDayHour, + endDayHour, + }); + + const workSpace = page.locator('.dx-scheduler-work-space'); + await testScreenshot(page, getScreenshotName(views[0].type, offset, startDayHour, endDayHour), { element: workSpace }); + }); + }); + }); + }); + + test('Appointments are ordered correctly with both recurrent and usual appointments (T1212573)', async ({ page }) => { + const data = [ + { text: 'Recurr 1', startDate: new Date('2020-11-01T17:30:00.000Z'), endDate: new Date('2020-11-01T19:00:00.000Z'), recurrenceRule: 'FREQ=WEEKLY;BYDAY=MO,TH;COUNT=10' }, + { text: 'Recurr 2', startDate: new Date('2020-11-01T17:30:00.000Z'), endDate: new Date('2020-11-01T19:00:00.000Z'), recurrenceRule: 'FREQ=WEEKLY;BYDAY=SU,WE;COUNT=10' }, + { text: 'Recurr 3', startDate: new Date('2020-11-01T20:00:00.000Z'), endDate: new Date('2020-11-01T21:00:00.000Z'), recurrenceRule: 'FREQ=WEEKLY;BYDAY=SU;WKST=TU;INTERVAL=2;COUNT=2' }, + { text: 'Recurr 4', startDate: new Date('2020-11-01T17:00:00.000Z'), endDate: new Date('2020-11-01T17:15:00.000Z'), recurrenceRule: 'FREQ=DAILY;BYDAY=TU;UNTIL=20201203' }, + { text: 'Test 1', startDate: new Date('2020-11-01T15:00:00.000Z'), endDate: new Date('2020-11-01T15:30:00.000Z') }, + { text: 'Test 2', startDate: new Date('2020-11-01T18:00:00.000Z'), endDate: new Date('2020-11-01T18:30:00.000Z') }, + { text: 'Test 3', startDate: new Date('2020-11-02T15:00:00.000Z'), endDate: new Date('2020-11-02T15:30:00.000Z') }, + { text: 'Test 4', startDate: new Date('2020-11-02T18:00:00.000Z'), endDate: new Date('2020-11-02T18:30:00.000Z') }, + { text: 'Test 5', startDate: new Date('2020-11-03T15:00:00.000Z'), endDate: new Date('2020-11-03T15:30:00.000Z') }, + { text: 'Test 6', startDate: new Date('2020-11-03T18:00:00.000Z'), endDate: new Date('2020-11-03T18:30:00.000Z') }, + { text: 'Test 7', startDate: new Date('2020-11-04T15:00:00.000Z'), endDate: new Date('2020-11-04T15:30:00.000Z') }, + { text: 'Test 8', startDate: new Date('2020-11-04T18:00:00.000Z'), endDate: new Date('2020-11-04T18:30:00.000Z') }, + ]; + + await insertStylesheetRulesToPage(page, REDUCE_CELLS_CSS); + await createWidget(page, 'dxScheduler', { + currentDate: '2020-11-07', + height: 800, + dataSource: data, + views: ['timelineMonth'], + currentView: 'timelineMonth', + }); + + const workSpace = page.locator('.dx-scheduler-work-space'); + await testScreenshot(page, 'view_markup_ordering-appts_T1212573.png', { element: workSpace }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/viewOffset/markup/recurrentAppointments.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/viewOffset/markup/recurrentAppointments.spec.ts new file mode 100644 index 000000000000..7474eed26ce3 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/viewOffset/markup/recurrentAppointments.spec.ts @@ -0,0 +1,161 @@ +import { test } from '@playwright/test'; +import { createWidget, insertStylesheetRulesToPage, testScreenshot } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +const REDUCE_CELLS_CSS = ` +.dx-scheduler-cell-sizes-vertical { + height: 25px; +}`; + +const APPOINTMENTS = [ + { startDate: '2023-08-01T10:00:00', endDate: '2023-08-01T14:00:00', recurrenceRule: 'FREQ=HOURLY;INTERVAL=24', text: 'Hourly 10-14' }, + { startDate: '2023-08-01T16:00:00', endDate: '2023-08-01T20:00:00', recurrenceRule: 'FREQ=WEEKLY;BYDAY=MO,WE,TH,FR', text: 'Daily 16-20' }, + { startDate: '2023-08-01T23:00:00', endDate: '2023-08-02T05:00:00', recurrenceRule: 'FREQ=MONTHLY;BYMONTHDAY=7', allDay: true, text: 'All day 01 -> 02' }, +]; +const APPOINTMENTS_TIMELINE = [ + { startDate: '2023-08-01T04:00:00', endDate: '2023-08-01T18:00:00', recurrenceRule: 'FREQ=HOURLY;INTERVAL=24', text: 'Hourly 04-18' }, + { startDate: '2023-08-01T20:00:00', endDate: '2023-08-02T10:00:00', recurrenceRule: 'FREQ=WEEKLY;BYDAY=MO,WE,TH,FR', text: 'Daily 20-10' }, +]; + +const getScreenshotName = (viewType: string, offset: number, startDayHour: number, endDayHour: number, firstDay?: number) => + `view_markup_recurrent-appts_${viewType}_offset-${offset}_start-${startDayHour}_end-${endDayHour}_first-day-${firstDay}.png`; + +const getScreenshotNameForEdgeCase = (edgeCaseName: string, viewType: string, offset: number, startDayHour: number, endDayHour: number) => + `view_markup_recurrent-appts_${edgeCaseName}_${viewType}_offset-${offset}_start-${startDayHour}_end-${endDayHour}.png`; + +const getViewWithCorrectCellDuration = ( + view: { type: string; cellDuration?: number }, + startDayHour: number, + endDayHour: number, +): { type: string; cellDuration?: number } => { + switch (view.type) { + case 'timelineWeek': + case 'timelineWorkWeek': + return { ...view, cellDuration: (endDayHour - startDayHour) * 60 }; + default: + return view; + } +}; + +test.describe('Offset: Markup recurrent appointments', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + [ + { views: [{ type: 'day', cellDuration: 60, firstDayOfWeek: 0 }], dataSource: APPOINTMENTS }, + { views: [{ type: 'week', cellDuration: 60, firstDayOfWeek: 0 }], dataSource: APPOINTMENTS }, + { views: [{ type: 'workWeek', cellDuration: 60, firstDayOfWeek: 0 }], dataSource: APPOINTMENTS }, + { views: [{ type: 'workWeek', cellDuration: 60, firstDayOfWeek: 3 }], dataSource: APPOINTMENTS }, + { views: [{ type: 'month', firstDayOfWeek: 0 }], dataSource: APPOINTMENTS }, + { views: [{ type: 'timelineDay', cellDuration: 240, firstDayOfWeek: 0 }], dataSource: APPOINTMENTS_TIMELINE }, + { views: [{ type: 'timelineWeek', firstDayOfWeek: 0 }], dataSource: APPOINTMENTS_TIMELINE }, + { views: [{ type: 'timelineWorkWeek', firstDayOfWeek: 0 }], dataSource: APPOINTMENTS_TIMELINE }, + { views: [{ type: 'timelineWorkWeek', firstDayOfWeek: 3 }], dataSource: APPOINTMENTS_TIMELINE }, + { views: [{ type: 'timelineMonth', firstDayOfWeek: 0 }], dataSource: APPOINTMENTS }, + ].forEach(({ views, dataSource }) => { + [0, 735, -735].forEach((offset) => { + [ + { startDayHour: 0, endDayHour: 24 }, + { startDayHour: 9, endDayHour: 17 }, + ].forEach(({ startDayHour, endDayHour }) => { + test(`Recurrence appointments render (view: ${views[0].type}, offset: ${offset}, start: ${startDayHour}, end: ${endDayHour}, firstDay: ${(views[0] as any).firstDayOfWeek})`, async ({ page }) => { + const view = getViewWithCorrectCellDuration(views[0], startDayHour, endDayHour); + + await insertStylesheetRulesToPage(page, REDUCE_CELLS_CSS); + await createWidget(page, 'dxScheduler', { + currentDate: '2023-09-07', + height: 800, + maxAppointmentsPerCell: 'unlimited', + dataSource, + views: [view], + currentView: view.type, + offset, + startDayHour, + endDayHour, + }); + + const workSpace = page.locator('.dx-scheduler-work-space'); + await testScreenshot( + page, + getScreenshotName(views[0].type, offset, startDayHour, endDayHour, (views[0] as any).firstDayOfWeek), + { element: workSpace }, + ); + }); + }); + }); + }); + + [ + { views: [{ type: 'day', cellDuration: 60 }] }, + { views: [{ type: 'timelineDay', cellDuration: 240 }] }, + ].forEach(({ views }) => { + [ + { + dataSource: [ + { startDate: '2023-09-01T10:00:00', endDate: '2023-09-01T14:00:00', text: '#0 WE 10:00->14:00', recurrenceRule: 'FREQ=WEEKLY;BYDAY=WE' }, + { startDate: '2023-09-01T20:00:00', endDate: '2023-09-02T04:00:00', text: '#1 WE 20:00->04:00', recurrenceRule: 'FREQ=WEEKLY;BYDAY=WE' }, + { startDate: '2023-09-01T10:00:00', endDate: '2023-09-01T14:00:00', text: '#2 TH 10:00->14:00', recurrenceRule: 'FREQ=WEEKLY;BYDAY=TH' }, + { startDate: '2023-09-01T00:00:00', endDate: '2023-09-01T00:00:00', text: '#3 All-day TH', allDay: true, recurrenceRule: 'FREQ=WEEKLY;BYDAY=TH' }, + ], + offset: 720, startDayHour: 0, endDayHour: 24, + }, + { + dataSource: [ + { startDate: '2023-09-01T20:00:00', endDate: '2023-09-01T22:00:00', text: '#0 WE 15:00->19:00', recurrenceRule: 'FREQ=WEEKLY;BYDAY=WE' }, + { startDate: '2023-09-01T23:00:00', endDate: '2023-09-02T01:00:00', text: '#1 WE 23:00->01:00', recurrenceRule: 'FREQ=WEEKLY;BYDAY=WE' }, + { startDate: '2023-09-01T04:00:00', endDate: '2023-09-01T06:00:00', text: '#2 TH 04:00->06:00', recurrenceRule: 'FREQ=WEEKLY;BYDAY=TH' }, + { startDate: '2023-09-01T00:00:00', endDate: '2023-09-01T00:00:00', text: '#3 All-day TH', allDay: true, recurrenceRule: 'FREQ=WEEKLY;BYDAY=TH' }, + ], + offset: 720, startDayHour: 9, endDayHour: 17, + }, + { + dataSource: [ + { startDate: '2023-09-01T10:00:00', endDate: '2023-09-01T14:00:00', text: '#0 TU 10:00->14:00', recurrenceRule: 'FREQ=WEEKLY;BYDAY=TU' }, + { startDate: '2023-09-01T20:00:00', endDate: '2023-09-02T04:00:00', text: '#1 TU 20:00->04:00', recurrenceRule: 'FREQ=WEEKLY;BYDAY=TU' }, + { startDate: '2023-09-01T10:00:00', endDate: '2023-09-01T14:00:00', text: '#2 WE 10:00->14:00', recurrenceRule: 'FREQ=WEEKLY;BYDAY=WE' }, + { startDate: '2023-09-01T00:00:00', endDate: '2023-09-01T00:00:00', text: '#3 All-day WE', allDay: true, recurrenceRule: 'FREQ=WEEKLY;BYDAY=WE' }, + ], + offset: -720, startDayHour: 0, endDayHour: 24, + }, + { + dataSource: [ + { startDate: '2023-09-01T20:00:00', endDate: '2023-09-01T22:00:00', text: '#0 TU 15:00->19:00', recurrenceRule: 'FREQ=WEEKLY;BYDAY=TU' }, + { startDate: '2023-09-01T23:00:00', endDate: '2023-09-02T01:00:00', text: '#1 TU 23:00->01:00', recurrenceRule: 'FREQ=WEEKLY;BYDAY=TU' }, + { startDate: '2023-09-01T04:00:00', endDate: '2023-09-01T06:00:00', text: '#2 WE 04:00->06:00', recurrenceRule: 'FREQ=WEEKLY;BYDAY=WE' }, + { startDate: '2023-09-01T00:00:00', endDate: '2023-09-01T00:00:00', text: '#3 All-day WE', allDay: true, recurrenceRule: 'FREQ=WEEKLY;BYDAY=WE' }, + ], + offset: -720, startDayHour: 9, endDayHour: 17, + }, + ].forEach(({ dataSource, offset, startDayHour, endDayHour }) => { + test(`Recurrence appointments in short day views (view: ${views[0].type}, offset: ${offset}, start: ${startDayHour}, end: ${endDayHour})`, async ({ page }) => { + await insertStylesheetRulesToPage(page, REDUCE_CELLS_CSS); + await createWidget(page, 'dxScheduler', { + currentDate: '2023-09-06', + height: 800, + maxAppointmentsPerCell: 'unlimited', + currentView: views[0].type, + dataSource, + views, + offset, + startDayHour, + endDayHour, + }); + + const workSpace = page.locator('.dx-scheduler-work-space'); + await testScreenshot( + page, + getScreenshotNameForEdgeCase('short-day-views', views[0].type, offset, startDayHour, endDayHour), + { element: workSpace }, + ); + }); + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/viewOffset/markup/usualAppointments.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/viewOffset/markup/usualAppointments.spec.ts new file mode 100644 index 000000000000..b45d791ab249 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/viewOffset/markup/usualAppointments.spec.ts @@ -0,0 +1,151 @@ +import { test } from '@playwright/test'; +import { createWidget, insertStylesheetRulesToPage, testScreenshot } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +const REDUCE_CELLS_CSS = ` +.dx-scheduler-cell-sizes-vertical { + height: 25px; +}`; + +const MS_IN_DAY = 24 * 60 * 60 * 1000; + +interface AppointmentData { + startTime: string; + endTime: string; + endDateShiftDays?: number; + text?: string; + allDay?: boolean; + recurrenceRule?: string; +} + +const getIsoDate = (date: Date, additionalDays = 0): string => { + const dateCopy = new Date(date.getTime() + additionalDays * MS_IN_DAY); + const [dateISO] = dateCopy.toISOString().split('T'); + return dateISO; +}; + +const timeToText = (time: string): string => { + const [hours, minutes] = time.split(':'); + return `${hours}:${minutes}`; +}; + +const generateAppointments = ( + startDateISO: string, + endDateISO: string, + appointments: AppointmentData[], +) => { + const startDate = new Date(startDateISO); + const endDate = new Date(endDateISO); + const diffTime = Math.abs(endDate.getTime() - startDate.getTime()); + const daysCount = Math.ceil((diffTime / MS_IN_DAY) + 1); + return new Array(daysCount).fill(null).map((_, dayIdx) => { + const date = new Date(startDate.getTime() + MS_IN_DAY * dayIdx); + return new Array(appointments.length).fill(null).map((__, timeIdx) => { + const { startTime, endTime, endDateShiftDays, text, allDay, recurrenceRule } = appointments[timeIdx]; + const appointmentIdx = dayIdx * appointments.length + timeIdx; + const appointmentStartISO = getIsoDate(date); + const appointmentEndISO = getIsoDate(date, endDateShiftDays ?? 0); + const [, , dayISO] = appointmentStartISO.split('-'); + const titleText = `#${appointmentIdx}: ${dayISO.padStart(2, '0')} ${allDay ? 'All' : ''} ${!text ? `${timeToText(startTime)}-${timeToText(endTime)}` : text}`; + return { startDate: `${appointmentStartISO}T${startTime}`, endDate: `${appointmentEndISO}T${endTime}`, text: titleText, allDay, recurrenceRule }; + }); + }).flat(); +}; + +const APPOINTMENTS_TIME: AppointmentData[] = [ + { startTime: '04:00:00', endTime: '08:00:00' }, + { startTime: '10:15:00', endTime: '16:15:00' }, + { startTime: '17:05:00', endTime: '22:05:00' }, + { startTime: '23:00:00', endTime: '03:30:00', endDateShiftDays: 1 }, +]; +const APPOINTMENTS_TIMELINE_TIME: AppointmentData[] = [ + { startTime: '04:00:00', endTime: '08:00:00', endDateShiftDays: 1 }, + { startTime: '10:15:00', endTime: '16:15:00', endDateShiftDays: 1 }, + { startTime: '17:05:00', endTime: '22:05:00', endDateShiftDays: 1 }, + { startTime: '23:00:00', endTime: '03:30:00', endDateShiftDays: 1 }, +]; + +const APPOINTMENTS: Record[]> = { + day: generateAppointments('2023-09-06', '2023-09-08', APPOINTMENTS_TIME), + week: generateAppointments('2023-09-02', '2023-09-10', APPOINTMENTS_TIME), + workWeekWithFirstDay: generateAppointments('2023-09-05', '2023-09-13', APPOINTMENTS_TIME), + month: generateAppointments('2023-08-26', '2023-10-08', APPOINTMENTS_TIME), + timelineDay: generateAppointments('2023-09-06', '2023-09-08', APPOINTMENTS_TIMELINE_TIME), + timelineWeek: generateAppointments('2023-09-02', '2023-09-10', APPOINTMENTS_TIMELINE_TIME), + timelineWeekWithFirstDay: generateAppointments('2023-09-05', '2023-09-13', APPOINTMENTS_TIMELINE_TIME), + timelineMonth: generateAppointments('2023-08-31', '2023-09-08', APPOINTMENTS_TIMELINE_TIME), +}; + +const getScreenshotName = (viewType: string, offset: number, startDayHour: number, endDayHour: number, firstDay?: number) => + `view_markup_usual-appts_${viewType}_offset-${offset}_start-${startDayHour}_end-${endDayHour}_first-day-${firstDay}.png`; + +const getViewWithCorrectCellDuration = ( + view: { type: string; cellDuration?: number }, + startDayHour: number, + endDayHour: number, +): { type: string; cellDuration?: number } => { + switch (view.type) { + case 'timelineWeek': + case 'timelineWorkWeek': + return { ...view, cellDuration: (endDayHour - startDayHour) * 60 }; + default: + return view; + } +}; + +test.describe('Offset: Markup usual appointments', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + [ + { views: [{ type: 'day', cellDuration: 60, firstDayOfWeek: 0 }], dataSource: APPOINTMENTS.day }, + { views: [{ type: 'week', cellDuration: 60, firstDayOfWeek: 0 }], dataSource: APPOINTMENTS.week }, + { views: [{ type: 'workWeek', cellDuration: 60, firstDayOfWeek: 0 }], dataSource: APPOINTMENTS.week }, + { views: [{ type: 'workWeek', cellDuration: 60, firstDayOfWeek: 3 }], dataSource: APPOINTMENTS.workWeekWithFirstDay }, + { views: [{ type: 'month', firstDayOfWeek: 0 }], dataSource: APPOINTMENTS.month }, + { views: [{ type: 'timelineDay', cellDuration: 240, firstDayOfWeek: 0 }], dataSource: APPOINTMENTS.timelineDay }, + { views: [{ type: 'timelineWeek', firstDayOfWeek: 0 }], dataSource: APPOINTMENTS.timelineWeek }, + { views: [{ type: 'timelineWorkWeek', firstDayOfWeek: 0 }], dataSource: APPOINTMENTS.timelineWeek }, + { views: [{ type: 'timelineWorkWeek', firstDayOfWeek: 3 }], dataSource: APPOINTMENTS.timelineWeekWithFirstDay }, + { views: [{ type: 'timelineMonth', firstDayOfWeek: 0 }], dataSource: APPOINTMENTS.month }, + ].forEach(({ views, dataSource }) => { + [0, 735, 1440, -735, -1440].forEach((offset) => { + [ + { startDayHour: 0, endDayHour: 24 }, + { startDayHour: 9, endDayHour: 17 }, + ].forEach(({ startDayHour, endDayHour }) => { + test(`Usual appointments render (view: ${views[0].type}, offset: ${offset}, start: ${startDayHour}, end: ${endDayHour}, firstDay: ${(views[0] as any).firstDayOfWeek})`, async ({ page }) => { + const view = getViewWithCorrectCellDuration(views[0], startDayHour, endDayHour); + + await insertStylesheetRulesToPage(page, REDUCE_CELLS_CSS); + await createWidget(page, 'dxScheduler', { + currentDate: '2023-09-07', + height: 800, + maxAppointmentsPerCell: 'unlimited', + dataSource, + views: [view], + currentView: view.type, + offset, + startDayHour, + endDayHour, + }); + + const workSpace = page.locator('.dx-scheduler-work-space'); + await testScreenshot( + page, + getScreenshotName(views[0].type, offset, startDayHour, endDayHour, (views[0] as any).firstDayOfWeek), + { element: workSpace }, + ); + }); + }); + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/viewOffset/markup/virtualScrolling.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/viewOffset/markup/virtualScrolling.spec.ts new file mode 100644 index 000000000000..140422eb0968 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/viewOffset/markup/virtualScrolling.spec.ts @@ -0,0 +1,62 @@ +import { test } from '@playwright/test'; +import { createWidget, insertStylesheetRulesToPage, testScreenshot } from '../../../../playwright-helpers'; +import path from 'path'; + +const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; + +const REDUCE_CELLS_CSS = ` +.dx-scheduler-cell-sizes-vertical { + height: 25px; +}`; +const APPOINTMENTS = [ + { startDate: '2023-09-05T00:00:00', endDate: '2023-09-05T03:00:00', text: '#0 Usual 05 00:00->03:00' }, + { startDate: '2023-09-05T04:00:00', endDate: '2023-09-05T09:00:00', text: '#1 Usual 05 05:00->09:00' }, + { startDate: '2023-09-05T10:30:00', endDate: '2023-09-05T16:30:00', text: '#2 Usual 05 12:30->16:30' }, + { startDate: '2023-09-05T17:00:00', endDate: '2023-09-05T23:30:00', text: '#3 Usual 05 18:00->22:00' }, + { startDate: '2023-09-05T00:00:00', endDate: '2023-09-05T00:00:00', text: '#4 All-day 05', allDay: true }, +]; + +const getScreenshotName = (viewType: string, offset: number, startDayHour: number, endDayHour: number) => + `view_markup_virtual-scrolling_${viewType}_offset-${offset}_start-${startDayHour}_end-${endDayHour}.png`; + +test.describe('Offset: Markup virtual scrolling', () => { + test.beforeEach(async ({ page }) => { + await page.goto(containerUrl); + await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); + await page.evaluate((theme) => new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(theme); + }), process.env.THEME || 'fluent.blue.light'); + }); + + [ + { views: [{ type: 'week', cellDuration: 60 }], dataSource: APPOINTMENTS }, + { views: [{ type: 'month' }], dataSource: APPOINTMENTS }, + { views: [{ type: 'timelineMonth' }], dataSource: APPOINTMENTS }, + ].forEach(({ views, dataSource }) => { + [0, 735, 1440, -735, -1440].forEach((offset) => { + [ + { startDayHour: 0, endDayHour: 24 }, + { startDayHour: 9, endDayHour: 17 }, + ].forEach(({ startDayHour, endDayHour }) => { + test(`Virtual scrolling render (view: ${views[0].type}, offset: ${offset}, start: ${startDayHour}, end: ${endDayHour})`, async ({ page }) => { + await insertStylesheetRulesToPage(page, REDUCE_CELLS_CSS); + await createWidget(page, 'dxScheduler', { + currentDate: '2023-09-07', + height: 800, + maxAppointmentsPerCell: 'unlimited', + dataSource, + views, + currentView: views[0].type, + offset, + startDayHour, + endDayHour, + }); + + const workSpace = page.locator('.dx-scheduler-work-space'); + await testScreenshot(page, getScreenshotName(views[0].type, offset, startDayHour, endDayHour), { element: workSpace }); + }); + }); + }); + }); +}); diff --git a/e2e/testcafe-devextreme/playwright.config.ts b/e2e/testcafe-devextreme/playwright.config.ts new file mode 100644 index 000000000000..99604db0796a --- /dev/null +++ b/e2e/testcafe-devextreme/playwright.config.ts @@ -0,0 +1,56 @@ +import { defineConfig } from '@playwright/test'; +import path from 'path'; + +const CHROME_FLAGS = [ + '--no-sandbox', + '--disable-dev-shm-usage', + '--disable-gpu', + '--disable-partial-raster', + '--disable-skia-runtime-opts', + '--run-all-compositor-stages-before-draw', + '--disable-new-content-rendering-timeout', + '--disable-threaded-animation', + '--disable-threaded-scrolling', + '--disable-checker-imaging', + '--disable-image-animation-resync', + '--use-gl=swiftshader', + '--disable-features=PaintHolding', + '--js-flags=--random-seed=2147483647', + '--font-render-hinting=none', + '--disable-font-subpixel-positioning', +]; + +export default defineConfig({ + testDir: './playwright-tests', + outputDir: './playwright-results', + snapshotDir: './playwright-tests', + snapshotPathTemplate: '{snapshotDir}/{testFileDir}/etalons/{arg}{ext}', + + expect: { + toHaveScreenshot: { + maxDiffPixelRatio: 0.01, + threshold: 0.2, + animations: 'disabled', + }, + }, + + use: { + viewport: { width: 1200, height: 800 }, + screenshot: 'off', + trace: 'off', + launchOptions: { + args: CHROME_FLAGS, + }, + }, + + projects: [ + { + name: 'chromium', + use: { + browserName: 'chromium', + }, + }, + ], + + reporter: [['html', { open: 'never' }]], +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ab9499cad295..e1fa4d4ceaab 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -355,13 +355,13 @@ importers: dependencies: '@angular-devkit/build-angular': specifier: ~21.1.0 - version: 21.1.5(6ufluysnpyscwwzwonpw7avw2i) + version: 21.1.5(o5gxij6rgpqqzvumztenwt44ru) '@angular/animations': specifier: ~21.1.0 version: 21.1.6(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.1)) '@angular/cli': specifier: ~21.1.5 - version: 21.1.5(@types/node@20.12.8)(chokidar@5.0.0) + version: 21.1.5(@types/node@25.5.0)(chokidar@5.0.0) '@angular/common': specifier: ~21.1.0 version: 21.1.6(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2) @@ -725,7 +725,7 @@ importers: version: 1.1.4 jest: specifier: 29.7.0 - version: 29.7.0(@types/node@20.12.8)(babel-plugin-macros@3.1.0)(node-notifier@9.0.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@20.12.8)(typescript@5.9.3)) + version: 29.7.0(@types/node@25.5.0)(babel-plugin-macros@3.1.0)(node-notifier@9.0.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@25.5.0)(typescript@5.9.3)) jest-environment-node: specifier: 29.7.0 version: 29.7.0 @@ -776,7 +776,7 @@ importers: version: 4.0.0 ts-node: specifier: 10.9.2 - version: 10.9.2(@swc/core@1.15.3)(@types/node@20.12.8)(typescript@5.9.3) + version: 10.9.2(@swc/core@1.15.3)(@types/node@25.5.0)(typescript@5.9.3) vue-eslint-parser: specifier: 'catalog:' version: 10.0.0(eslint@9.39.4(jiti@2.6.1)) @@ -1095,6 +1095,9 @@ importers: '@eslint/eslintrc': specifier: 'catalog:' version: 3.3.5 + '@playwright/test': + specifier: ^1.58.2 + version: 1.58.2 '@stylistic/eslint-plugin': specifier: 'catalog:' version: 5.10.0(eslint@9.39.4(jiti@2.6.1)) @@ -1152,6 +1155,12 @@ importers: nconf: specifier: 0.12.1 version: 0.12.1 + pixelmatch: + specifier: ^7.1.0 + version: 7.1.0 + pngjs: + specifier: ^7.0.0 + version: 7.0.0 testcafe: specifier: 3.7.4 version: 3.7.4 @@ -1384,25 +1393,25 @@ importers: version: 7.29.0(@babel/core@7.29.0) '@devextreme-generator/angular': specifier: 3.0.12 - version: 3.0.12(yq7mldp4wyckzhqs3zqk5sdezm) + version: 3.0.12(6ypj2vslczvlh7srkoyq3dooyq) '@devextreme-generator/build-helpers': specifier: 3.0.12 - version: 3.0.12(h5cwyqq6zwtbaf5nld75dd5oii) + version: 3.0.12(fybkaxaxz3xev2nltvn2o5rw5y) '@devextreme-generator/core': specifier: 3.0.12 - version: 3.0.12(yq7mldp4wyckzhqs3zqk5sdezm) + version: 3.0.12(6ypj2vslczvlh7srkoyq3dooyq) '@devextreme-generator/declarations': specifier: 3.0.12 version: 3.0.12 '@devextreme-generator/inferno': specifier: 3.0.12 - version: 3.0.12(yq7mldp4wyckzhqs3zqk5sdezm) + version: 3.0.12(6ypj2vslczvlh7srkoyq3dooyq) '@devextreme-generator/react': specifier: 3.0.12 - version: 3.0.12(yq7mldp4wyckzhqs3zqk5sdezm) + version: 3.0.12(6ypj2vslczvlh7srkoyq3dooyq) '@devextreme-generator/vue': specifier: 3.0.12 - version: 3.0.12(yq7mldp4wyckzhqs3zqk5sdezm) + version: 3.0.12(6ypj2vslczvlh7srkoyq3dooyq) '@eslint-stylistic/metadata': specifier: 'catalog:' version: 2.13.0 @@ -1450,7 +1459,7 @@ importers: version: 0.14.2 autoprefixer: specifier: 10.4.27 - version: 10.4.27(postcss@8.4.38) + version: 10.4.27(postcss@8.5.8) axe-core: specifier: 'catalog:' version: 4.11.1 @@ -1513,7 +1522,7 @@ importers: version: 18.0.0(@typescript-eslint/eslint-plugin@8.52.0(@typescript-eslint/parser@8.52.0(eslint@9.39.4(jiti@2.6.1))(typescript@4.9.5))(eslint@9.39.4(jiti@2.6.1))(typescript@4.9.5))(@typescript-eslint/parser@8.52.0(eslint@9.39.4(jiti@2.6.1))(typescript@4.9.5))(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.52.0(eslint@9.39.4(jiti@2.6.1))(typescript@4.9.5))(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1)) eslint-config-devextreme: specifier: 'catalog:' - version: 1.1.9(satltipdsoawfxnov7ffi4z7ju) + version: 1.1.9(z5gvwpd2vz7hyhtqnrp7ixzxle) eslint-migration-utils: specifier: workspace:* version: link:../eslint-migration-utils @@ -1525,7 +1534,7 @@ importers: version: 2.32.0(@typescript-eslint/parser@8.52.0(eslint@9.39.4(jiti@2.6.1))(typescript@4.9.5))(eslint@9.39.4(jiti@2.6.1)) eslint-plugin-jest: specifier: 29.15.0 - version: 29.15.0(@typescript-eslint/eslint-plugin@8.52.0(@typescript-eslint/parser@8.52.0(eslint@9.39.4(jiti@2.6.1))(typescript@4.9.5))(eslint@9.39.4(jiti@2.6.1))(typescript@4.9.5))(eslint@9.39.4(jiti@2.6.1))(jest@30.2.0(@types/node@20.12.8)(babel-plugin-macros@3.1.0)(node-notifier@9.0.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@20.12.8)(typescript@4.9.5)))(typescript@4.9.5) + version: 29.15.0(@typescript-eslint/eslint-plugin@8.52.0(@typescript-eslint/parser@8.52.0(eslint@9.39.4(jiti@2.6.1))(typescript@4.9.5))(eslint@9.39.4(jiti@2.6.1))(typescript@4.9.5))(eslint@9.39.4(jiti@2.6.1))(jest@30.2.0(@types/node@25.5.0)(babel-plugin-macros@3.1.0)(node-notifier@9.0.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@25.5.0)(typescript@4.9.5)))(typescript@4.9.5) eslint-plugin-jest-formatting: specifier: 3.1.0 version: 3.1.0(eslint@9.39.4(jiti@2.6.1)) @@ -1780,7 +1789,7 @@ importers: version: 2.0.5 ts-jest: specifier: 29.1.2 - version: 29.1.2(@babel/core@7.29.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.29.0))(jest@30.2.0(@types/node@20.12.8)(babel-plugin-macros@3.1.0)(node-notifier@9.0.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@20.12.8)(typescript@4.9.5)))(typescript@4.9.5) + version: 29.1.2(@babel/core@7.29.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.29.0))(jest@30.2.0(@types/node@25.5.0)(babel-plugin-macros@3.1.0)(node-notifier@9.0.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@25.5.0)(typescript@4.9.5)))(typescript@4.9.5) tsc-alias: specifier: 1.8.16 version: 1.8.16 @@ -1860,7 +1869,7 @@ importers: version: 19.2.19(@angular/common@19.2.19(@angular/core@19.2.20(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/compiler@19.2.20)(@angular/core@19.2.20(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@19.2.19(@angular/common@19.2.19(@angular/core@19.2.20(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@19.2.20(rxjs@7.8.2)(zone.js@0.15.1)))(rxjs@7.8.2) '@babel/eslint-parser': specifier: 'catalog:' - version: 7.28.6(@babel/core@7.29.0)(eslint@9.39.4(jiti@2.6.1)) + version: 7.28.6(@babel/core@7.26.9)(eslint@9.39.4(jiti@2.6.1)) '@eslint-stylistic/metadata': specifier: 'catalog:' version: 2.13.0 @@ -1984,7 +1993,7 @@ importers: version: 29.5.14 ts-jest: specifier: 29.1.3 - version: 29.1.3(@babel/core@7.29.0)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.29.0))(jest@30.2.0(@types/node@25.5.0)(babel-plugin-macros@3.1.0)(node-notifier@9.0.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@25.5.0)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.1.3(@babel/core@7.29.0)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.29.0))(jest@30.2.0(@types/node@20.12.8)(babel-plugin-macros@3.1.0)(node-notifier@9.0.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@20.12.8)(typescript@5.9.3)))(typescript@5.9.3) packages/devextreme-react: dependencies: @@ -2091,7 +2100,7 @@ importers: version: 15.11.0(typescript@5.9.3) stylelint-config-standard-scss: specifier: 9.0.0 - version: 9.0.0(postcss@8.5.8)(stylelint@15.11.0(typescript@5.9.3)) + version: 9.0.0(postcss@8.5.6)(stylelint@15.11.0(typescript@5.9.3)) stylelint-scss: specifier: 6.10.0 version: 6.10.0(stylelint@15.11.0(typescript@5.9.3)) @@ -5869,6 +5878,11 @@ packages: resolution: {integrity: sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + '@playwright/test@1.58.2': + resolution: {integrity: sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==} + engines: {node: '>=18'} + hasBin: true + '@popperjs/core@2.11.8': resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==} @@ -11225,6 +11239,11 @@ packages: os: [darwin] deprecated: Upgrade to fsevents v2 to mitigate potential security issues + fsevents@2.3.2: + resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -15082,6 +15101,10 @@ packages: resolution: {integrity: sha512-7uU4ZnKeQq22t9AsmHGD2w4OYQGonwFnTypDypaWi7Qr2EvQIFVtG8J5D/3bE7W123Wdc9+v4CZDu5hJXVCtBg==} engines: {node: '>=20.x'} + pixelmatch@7.1.0: + resolution: {integrity: sha512-1wrVzJ2STrpmONHKBy228LM1b84msXDUoAzVEl0R8Mz4Ce6EPr+IVtxm8+yvrqLYMHswREkjYFaMxnyGnaY3Ng==} + hasBin: true + pkce-challenge@5.0.1: resolution: {integrity: sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==} engines: {node: '>=16.20.0'} @@ -15102,6 +15125,16 @@ packages: resolution: {integrity: sha512-+KD8hJtqQMYoTuL1bbGOqxb4z+nZkTAwVdNtWwe8Tc2xNbEmdJYIYoc6Qt0uF55e6YW6KuTHw1DjQ18gMhzepw==} engines: {node: '>=16.0.0'} + playwright-core@1.58.2: + resolution: {integrity: sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==} + engines: {node: '>=18'} + hasBin: true + + playwright@1.58.2: + resolution: {integrity: sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==} + engines: {node: '>=18'} + hasBin: true + plimit-lit@1.6.1: resolution: {integrity: sha512-B7+VDyb8Tl6oMJT9oSO2CW8XC/T4UcJGrwOVoNGwOQsQYhlpfajmrMj5xeejqaASq3V/EqThyOeATEOMuSEXiA==} engines: {node: '>=12'} @@ -15135,6 +15168,10 @@ packages: resolution: {integrity: sha512-TRzzuFRRmEoSW/p1KVAmiOgPco2Irlah+bGFCeNfJXxxYGwSw7YwAOAcd7X28K/m5bjBWKsC29KyoMfHbypayg==} engines: {node: '>=12.13.0'} + pngjs@7.0.0: + resolution: {integrity: sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==} + engines: {node: '>=14.19.0'} + portfinder@1.0.32: resolution: {integrity: sha512-on2ZJVVDXRADWE6jnQaX0ioEylzgBpQk8r55NE4wjXW1ZxO+BgDlY6DXwj20i0V8eB4SenDQ00WEaxfiIQPcxg==} engines: {node: '>= 0.12.0'} @@ -19183,13 +19220,13 @@ snapshots: - webpack-cli - yaml - '@angular-devkit/build-angular@21.1.5(6ufluysnpyscwwzwonpw7avw2i)': + '@angular-devkit/build-angular@21.1.5(o5gxij6rgpqqzvumztenwt44ru)': dependencies: '@ampproject/remapping': 2.3.0 '@angular-devkit/architect': 0.2101.5(chokidar@5.0.0) '@angular-devkit/build-webpack': 0.2101.5(chokidar@5.0.0)(webpack-dev-server@5.2.2(webpack@5.105.0(@swc/core@1.15.3)(esbuild@0.27.2)))(webpack@5.105.0(@swc/core@1.15.3)(esbuild@0.27.2)) '@angular-devkit/core': 21.1.5(chokidar@5.0.0) - '@angular/build': 21.1.5(yfszryznq3cudajtfbi3mafxu4) + '@angular/build': 21.1.5(jnvohkvmeumnxrrszxgkvmipwy) '@angular/compiler-cli': 21.1.6(@angular/compiler@21.2.4)(typescript@5.9.3) '@babel/core': 7.28.5 '@babel/generator': 7.28.5 @@ -19246,7 +19283,7 @@ snapshots: '@angular/platform-browser': 21.1.6(@angular/animations@21.1.6(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@21.1.6(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.1)) '@angular/platform-server': 21.1.6(@angular/common@21.1.6(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/compiler@21.2.4)(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@21.1.6(@angular/animations@21.1.6(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@21.1.6(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.1)))(rxjs@7.8.2) esbuild: 0.27.2 - jest: 29.7.0(@types/node@20.12.8)(babel-plugin-macros@3.1.0)(node-notifier@9.0.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@20.12.8)(typescript@5.9.3)) + jest: 29.7.0(@types/node@25.5.0)(babel-plugin-macros@3.1.0)(node-notifier@9.0.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@25.5.0)(typescript@5.9.3)) karma: 6.4.4 transitivePeerDependencies: - '@angular/compiler' @@ -19509,7 +19546,7 @@ snapshots: - yaml optional: true - '@angular/build@21.1.5(yfszryznq3cudajtfbi3mafxu4)': + '@angular/build@21.1.5(jnvohkvmeumnxrrszxgkvmipwy)': dependencies: '@ampproject/remapping': 2.3.0 '@angular-devkit/architect': 0.2101.5(chokidar@5.0.0) @@ -19518,8 +19555,8 @@ snapshots: '@babel/core': 7.28.5 '@babel/helper-annotate-as-pure': 7.27.3 '@babel/helper-split-export-declaration': 7.24.7 - '@inquirer/confirm': 5.1.21(@types/node@20.12.8) - '@vitejs/plugin-basic-ssl': 2.1.0(vite@7.3.0(@types/node@20.12.8)(jiti@2.6.1)(less@4.4.2)(lightningcss@1.30.2)(sass-embedded@1.97.1)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.1)) + '@inquirer/confirm': 5.1.21(@types/node@25.5.0) + '@vitejs/plugin-basic-ssl': 2.1.0(vite@7.3.0(@types/node@25.5.0)(jiti@2.6.1)(less@4.4.2)(lightningcss@1.30.2)(sass-embedded@1.97.1)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.1)) beasties: 0.3.5 browserslist: 4.28.1 esbuild: 0.27.2 @@ -19540,7 +19577,7 @@ snapshots: tslib: 2.8.1 typescript: 5.9.3 undici: 7.24.4 - vite: 7.3.0(@types/node@20.12.8)(jiti@2.6.1)(less@4.4.2)(lightningcss@1.30.2)(sass-embedded@1.97.1)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.1) + vite: 7.3.0(@types/node@25.5.0)(jiti@2.6.1)(less@4.4.2)(lightningcss@1.30.2)(sass-embedded@1.97.1)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.1) watchpack: 2.5.0 optionalDependencies: '@angular/core': 21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.1) @@ -19611,13 +19648,13 @@ snapshots: - chokidar - supports-color - '@angular/cli@21.1.5(@types/node@20.12.8)(chokidar@5.0.0)': + '@angular/cli@21.1.5(@types/node@25.5.0)(chokidar@5.0.0)': dependencies: '@angular-devkit/architect': 0.2101.5(chokidar@5.0.0) '@angular-devkit/core': 21.1.5(chokidar@5.0.0) '@angular-devkit/schematics': 21.1.5(chokidar@5.0.0) - '@inquirer/prompts': 7.10.1(@types/node@20.12.8) - '@listr2/prompt-adapter-inquirer': 3.0.5(@inquirer/prompts@7.10.1(@types/node@20.12.8))(@types/node@20.12.8)(listr2@9.0.5) + '@inquirer/prompts': 7.10.1(@types/node@25.5.0) + '@listr2/prompt-adapter-inquirer': 3.0.5(@inquirer/prompts@7.10.1(@types/node@25.5.0))(@types/node@25.5.0)(listr2@9.0.5) '@modelcontextprotocol/sdk': 1.26.0(zod@4.3.5) '@schematics/angular': 21.1.5(chokidar@5.0.0) '@yarnpkg/lockfile': 1.1.0 @@ -19990,6 +20027,14 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/eslint-parser@7.28.6(@babel/core@7.26.9)(eslint@9.39.4(jiti@2.6.1))': + dependencies: + '@babel/core': 7.26.9 + '@nicolo-ribaudo/eslint-scope-5-internals': 5.1.1-v1 + eslint: 9.39.4(jiti@2.6.1) + eslint-visitor-keys: 2.1.0 + semver: 6.3.1 + '@babel/eslint-parser@7.28.6(@babel/core@7.29.0)(eslint@9.39.4(jiti@2.6.1))': dependencies: '@babel/core': 7.29.0 @@ -22840,9 +22885,9 @@ snapshots: dependencies: tslib: 2.3.1 - '@devextreme-generator/angular@3.0.12(yq7mldp4wyckzhqs3zqk5sdezm)': + '@devextreme-generator/angular@3.0.12(6ypj2vslczvlh7srkoyq3dooyq)': dependencies: - '@devextreme-generator/core': 3.0.12(yq7mldp4wyckzhqs3zqk5sdezm) + '@devextreme-generator/core': 3.0.12(6ypj2vslczvlh7srkoyq3dooyq) transitivePeerDependencies: - '@typescript-eslint/eslint-plugin' - eslint @@ -22857,13 +22902,13 @@ snapshots: - eslint-plugin-spellcheck - supports-color - '@devextreme-generator/build-helpers@3.0.12(h5cwyqq6zwtbaf5nld75dd5oii)': + '@devextreme-generator/build-helpers@3.0.12(fybkaxaxz3xev2nltvn2o5rw5y)': dependencies: - '@devextreme-generator/angular': 3.0.12(yq7mldp4wyckzhqs3zqk5sdezm) - '@devextreme-generator/core': 3.0.12(yq7mldp4wyckzhqs3zqk5sdezm) - '@devextreme-generator/inferno': 3.0.12(yq7mldp4wyckzhqs3zqk5sdezm) - '@devextreme-generator/preact': 3.0.12(yq7mldp4wyckzhqs3zqk5sdezm) - '@devextreme-generator/react': 3.0.12(yq7mldp4wyckzhqs3zqk5sdezm) + '@devextreme-generator/angular': 3.0.12(6ypj2vslczvlh7srkoyq3dooyq) + '@devextreme-generator/core': 3.0.12(6ypj2vslczvlh7srkoyq3dooyq) + '@devextreme-generator/inferno': 3.0.12(6ypj2vslczvlh7srkoyq3dooyq) + '@devextreme-generator/preact': 3.0.12(6ypj2vslczvlh7srkoyq3dooyq) + '@devextreme-generator/react': 3.0.12(6ypj2vslczvlh7srkoyq3dooyq) loader-utils: 2.0.4 typescript: 4.3.5 vinyl: 2.2.1 @@ -22886,10 +22931,10 @@ snapshots: - uglify-js - webpack-cli - '@devextreme-generator/core@3.0.12(yq7mldp4wyckzhqs3zqk5sdezm)': + '@devextreme-generator/core@3.0.12(6ypj2vslczvlh7srkoyq3dooyq)': dependencies: code-block-writer: 10.1.1 - eslint-config-devextreme: 0.2.0(yq7mldp4wyckzhqs3zqk5sdezm) + eslint-config-devextreme: 0.2.0(6ypj2vslczvlh7srkoyq3dooyq) prettier: 2.8.8 prettier-eslint: 13.0.0 typescript: 4.3.5 @@ -22912,11 +22957,11 @@ snapshots: react: 17.0.2 react-dom: 17.0.2(react@17.0.2) - '@devextreme-generator/inferno@3.0.12(yq7mldp4wyckzhqs3zqk5sdezm)': + '@devextreme-generator/inferno@3.0.12(6ypj2vslczvlh7srkoyq3dooyq)': dependencies: - '@devextreme-generator/core': 3.0.12(yq7mldp4wyckzhqs3zqk5sdezm) - '@devextreme-generator/preact': 3.0.12(yq7mldp4wyckzhqs3zqk5sdezm) - '@devextreme-generator/react': 3.0.12(yq7mldp4wyckzhqs3zqk5sdezm) + '@devextreme-generator/core': 3.0.12(6ypj2vslczvlh7srkoyq3dooyq) + '@devextreme-generator/preact': 3.0.12(6ypj2vslczvlh7srkoyq3dooyq) + '@devextreme-generator/react': 3.0.12(6ypj2vslczvlh7srkoyq3dooyq) transitivePeerDependencies: - '@typescript-eslint/eslint-plugin' - eslint @@ -22931,10 +22976,10 @@ snapshots: - eslint-plugin-spellcheck - supports-color - '@devextreme-generator/preact@3.0.12(yq7mldp4wyckzhqs3zqk5sdezm)': + '@devextreme-generator/preact@3.0.12(6ypj2vslczvlh7srkoyq3dooyq)': dependencies: - '@devextreme-generator/core': 3.0.12(yq7mldp4wyckzhqs3zqk5sdezm) - '@devextreme-generator/react': 3.0.12(yq7mldp4wyckzhqs3zqk5sdezm) + '@devextreme-generator/core': 3.0.12(6ypj2vslczvlh7srkoyq3dooyq) + '@devextreme-generator/react': 3.0.12(6ypj2vslczvlh7srkoyq3dooyq) transitivePeerDependencies: - '@typescript-eslint/eslint-plugin' - eslint @@ -22949,9 +22994,9 @@ snapshots: - eslint-plugin-spellcheck - supports-color - '@devextreme-generator/react@3.0.12(yq7mldp4wyckzhqs3zqk5sdezm)': + '@devextreme-generator/react@3.0.12(6ypj2vslczvlh7srkoyq3dooyq)': dependencies: - '@devextreme-generator/core': 3.0.12(yq7mldp4wyckzhqs3zqk5sdezm) + '@devextreme-generator/core': 3.0.12(6ypj2vslczvlh7srkoyq3dooyq) transitivePeerDependencies: - '@typescript-eslint/eslint-plugin' - eslint @@ -22966,10 +23011,10 @@ snapshots: - eslint-plugin-spellcheck - supports-color - '@devextreme-generator/vue@3.0.12(yq7mldp4wyckzhqs3zqk5sdezm)': + '@devextreme-generator/vue@3.0.12(6ypj2vslczvlh7srkoyq3dooyq)': dependencies: - '@devextreme-generator/angular': 3.0.12(yq7mldp4wyckzhqs3zqk5sdezm) - '@devextreme-generator/core': 3.0.12(yq7mldp4wyckzhqs3zqk5sdezm) + '@devextreme-generator/angular': 3.0.12(6ypj2vslczvlh7srkoyq3dooyq) + '@devextreme-generator/core': 3.0.12(6ypj2vslczvlh7srkoyq3dooyq) prettier: 2.8.8 transitivePeerDependencies: - '@typescript-eslint/eslint-plugin' @@ -23446,16 +23491,6 @@ snapshots: optionalDependencies: '@types/node': 20.11.17 - '@inquirer/checkbox@4.3.2(@types/node@20.12.8)': - dependencies: - '@inquirer/ansi': 1.0.2 - '@inquirer/core': 10.3.2(@types/node@20.12.8) - '@inquirer/figures': 1.0.15 - '@inquirer/type': 3.0.10(@types/node@20.12.8) - yoctocolors-cjs: 2.1.3 - optionalDependencies: - '@types/node': 20.12.8 - '@inquirer/checkbox@4.3.2(@types/node@25.5.0)': dependencies: '@inquirer/ansi': 1.0.2 @@ -23473,13 +23508,6 @@ snapshots: optionalDependencies: '@types/node': 20.11.17 - '@inquirer/confirm@5.1.21(@types/node@20.12.8)': - dependencies: - '@inquirer/core': 10.3.2(@types/node@20.12.8) - '@inquirer/type': 3.0.10(@types/node@20.12.8) - optionalDependencies: - '@types/node': 20.12.8 - '@inquirer/confirm@5.1.21(@types/node@25.5.0)': dependencies: '@inquirer/core': 10.3.2(@types/node@25.5.0) @@ -23514,19 +23542,6 @@ snapshots: optionalDependencies: '@types/node': 20.11.17 - '@inquirer/core@10.3.2(@types/node@20.12.8)': - dependencies: - '@inquirer/ansi': 1.0.2 - '@inquirer/figures': 1.0.15 - '@inquirer/type': 3.0.10(@types/node@20.12.8) - cli-width: 4.1.0 - mute-stream: 2.0.0 - signal-exit: 4.1.0 - wrap-ansi: 6.2.0 - yoctocolors-cjs: 2.1.3 - optionalDependencies: - '@types/node': 20.12.8 - '@inquirer/core@10.3.2(@types/node@25.5.0)': dependencies: '@inquirer/ansi': 1.0.2 @@ -23548,14 +23563,6 @@ snapshots: optionalDependencies: '@types/node': 20.11.17 - '@inquirer/editor@4.2.23(@types/node@20.12.8)': - dependencies: - '@inquirer/core': 10.3.2(@types/node@20.12.8) - '@inquirer/external-editor': 1.0.3(@types/node@20.12.8) - '@inquirer/type': 3.0.10(@types/node@20.12.8) - optionalDependencies: - '@types/node': 20.12.8 - '@inquirer/editor@4.2.23(@types/node@25.5.0)': dependencies: '@inquirer/core': 10.3.2(@types/node@25.5.0) @@ -23572,14 +23579,6 @@ snapshots: optionalDependencies: '@types/node': 20.11.17 - '@inquirer/expand@4.0.23(@types/node@20.12.8)': - dependencies: - '@inquirer/core': 10.3.2(@types/node@20.12.8) - '@inquirer/type': 3.0.10(@types/node@20.12.8) - yoctocolors-cjs: 2.1.3 - optionalDependencies: - '@types/node': 20.12.8 - '@inquirer/expand@4.0.23(@types/node@25.5.0)': dependencies: '@inquirer/core': 10.3.2(@types/node@25.5.0) @@ -23595,13 +23594,6 @@ snapshots: optionalDependencies: '@types/node': 20.11.17 - '@inquirer/external-editor@1.0.3(@types/node@20.12.8)': - dependencies: - chardet: 2.1.1 - iconv-lite: 0.7.1 - optionalDependencies: - '@types/node': 20.12.8 - '@inquirer/external-editor@1.0.3(@types/node@25.5.0)': dependencies: chardet: 2.1.1 @@ -23618,13 +23610,6 @@ snapshots: optionalDependencies: '@types/node': 20.11.17 - '@inquirer/input@4.3.1(@types/node@20.12.8)': - dependencies: - '@inquirer/core': 10.3.2(@types/node@20.12.8) - '@inquirer/type': 3.0.10(@types/node@20.12.8) - optionalDependencies: - '@types/node': 20.12.8 - '@inquirer/input@4.3.1(@types/node@25.5.0)': dependencies: '@inquirer/core': 10.3.2(@types/node@25.5.0) @@ -23639,13 +23624,6 @@ snapshots: optionalDependencies: '@types/node': 20.11.17 - '@inquirer/number@3.0.23(@types/node@20.12.8)': - dependencies: - '@inquirer/core': 10.3.2(@types/node@20.12.8) - '@inquirer/type': 3.0.10(@types/node@20.12.8) - optionalDependencies: - '@types/node': 20.12.8 - '@inquirer/number@3.0.23(@types/node@25.5.0)': dependencies: '@inquirer/core': 10.3.2(@types/node@25.5.0) @@ -23661,14 +23639,6 @@ snapshots: optionalDependencies: '@types/node': 20.11.17 - '@inquirer/password@4.0.23(@types/node@20.12.8)': - dependencies: - '@inquirer/ansi': 1.0.2 - '@inquirer/core': 10.3.2(@types/node@20.12.8) - '@inquirer/type': 3.0.10(@types/node@20.12.8) - optionalDependencies: - '@types/node': 20.12.8 - '@inquirer/password@4.0.23(@types/node@25.5.0)': dependencies: '@inquirer/ansi': 1.0.2 @@ -23677,20 +23647,20 @@ snapshots: optionalDependencies: '@types/node': 25.5.0 - '@inquirer/prompts@7.10.1(@types/node@20.12.8)': + '@inquirer/prompts@7.10.1(@types/node@25.5.0)': dependencies: - '@inquirer/checkbox': 4.3.2(@types/node@20.12.8) - '@inquirer/confirm': 5.1.21(@types/node@20.12.8) - '@inquirer/editor': 4.2.23(@types/node@20.12.8) - '@inquirer/expand': 4.0.23(@types/node@20.12.8) - '@inquirer/input': 4.3.1(@types/node@20.12.8) - '@inquirer/number': 3.0.23(@types/node@20.12.8) - '@inquirer/password': 4.0.23(@types/node@20.12.8) - '@inquirer/rawlist': 4.1.11(@types/node@20.12.8) - '@inquirer/search': 3.2.2(@types/node@20.12.8) - '@inquirer/select': 4.4.2(@types/node@20.12.8) + '@inquirer/checkbox': 4.3.2(@types/node@25.5.0) + '@inquirer/confirm': 5.1.21(@types/node@25.5.0) + '@inquirer/editor': 4.2.23(@types/node@25.5.0) + '@inquirer/expand': 4.0.23(@types/node@25.5.0) + '@inquirer/input': 4.3.1(@types/node@25.5.0) + '@inquirer/number': 3.0.23(@types/node@25.5.0) + '@inquirer/password': 4.0.23(@types/node@25.5.0) + '@inquirer/rawlist': 4.1.11(@types/node@25.5.0) + '@inquirer/search': 3.2.2(@types/node@25.5.0) + '@inquirer/select': 4.4.2(@types/node@25.5.0) optionalDependencies: - '@types/node': 20.12.8 + '@types/node': 25.5.0 '@inquirer/prompts@7.3.2(@types/node@20.11.17)': dependencies: @@ -23730,14 +23700,6 @@ snapshots: optionalDependencies: '@types/node': 20.11.17 - '@inquirer/rawlist@4.1.11(@types/node@20.12.8)': - dependencies: - '@inquirer/core': 10.3.2(@types/node@20.12.8) - '@inquirer/type': 3.0.10(@types/node@20.12.8) - yoctocolors-cjs: 2.1.3 - optionalDependencies: - '@types/node': 20.12.8 - '@inquirer/rawlist@4.1.11(@types/node@25.5.0)': dependencies: '@inquirer/core': 10.3.2(@types/node@25.5.0) @@ -23755,15 +23717,6 @@ snapshots: optionalDependencies: '@types/node': 20.11.17 - '@inquirer/search@3.2.2(@types/node@20.12.8)': - dependencies: - '@inquirer/core': 10.3.2(@types/node@20.12.8) - '@inquirer/figures': 1.0.15 - '@inquirer/type': 3.0.10(@types/node@20.12.8) - yoctocolors-cjs: 2.1.3 - optionalDependencies: - '@types/node': 20.12.8 - '@inquirer/search@3.2.2(@types/node@25.5.0)': dependencies: '@inquirer/core': 10.3.2(@types/node@25.5.0) @@ -23783,16 +23736,6 @@ snapshots: optionalDependencies: '@types/node': 20.11.17 - '@inquirer/select@4.4.2(@types/node@20.12.8)': - dependencies: - '@inquirer/ansi': 1.0.2 - '@inquirer/core': 10.3.2(@types/node@20.12.8) - '@inquirer/figures': 1.0.15 - '@inquirer/type': 3.0.10(@types/node@20.12.8) - yoctocolors-cjs: 2.1.3 - optionalDependencies: - '@types/node': 20.12.8 - '@inquirer/select@4.4.2(@types/node@25.5.0)': dependencies: '@inquirer/ansi': 1.0.2 @@ -23811,10 +23754,6 @@ snapshots: optionalDependencies: '@types/node': 20.11.17 - '@inquirer/type@3.0.10(@types/node@20.12.8)': - optionalDependencies: - '@types/node': 20.12.8 - '@inquirer/type@3.0.10(@types/node@25.5.0)': optionalDependencies: '@types/node': 25.5.0 @@ -23935,6 +23874,43 @@ snapshots: - supports-color - ts-node + '@jest/core@29.7.0(babel-plugin-macros@3.1.0)(node-notifier@9.0.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@25.5.0)(typescript@5.9.3))': + dependencies: + '@jest/console': 29.7.0 + '@jest/reporters': 29.7.0(node-notifier@9.0.1) + '@jest/test-result': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 20.12.8 + ansi-escapes: 4.3.2 + chalk: 4.1.2 + ci-info: 3.9.0 + exit: 0.1.2 + graceful-fs: 4.2.11 + jest-changed-files: 29.7.0 + jest-config: 29.7.0(@types/node@20.12.8)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@25.5.0)(typescript@5.9.3)) + jest-haste-map: 29.7.0 + jest-message-util: 29.7.0 + jest-regex-util: 29.6.3 + jest-resolve: 29.7.0 + jest-resolve-dependencies: 29.7.0 + jest-runner: 29.7.0 + jest-runtime: 29.7.0 + jest-snapshot: 29.7.0 + jest-util: 29.7.0 + jest-validate: 29.7.0 + jest-watcher: 29.7.0 + micromatch: 4.0.8 + pretty-format: 29.7.0 + slash: 3.0.0 + strip-ansi: 6.0.1 + optionalDependencies: + node-notifier: 9.0.1 + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + - ts-node + '@jest/core@30.2.0(babel-plugin-macros@3.1.0)(node-notifier@9.0.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@18.19.130)(typescript@4.9.5))': dependencies: '@jest/console': 30.2.0 @@ -24050,7 +24026,7 @@ snapshots: - ts-node optional: true - '@jest/core@30.2.0(babel-plugin-macros@3.1.0)(node-notifier@9.0.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@20.12.8)(typescript@4.9.5))': + '@jest/core@30.2.0(babel-plugin-macros@3.1.0)(node-notifier@9.0.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@20.12.8)(typescript@5.9.3))': dependencies: '@jest/console': 30.2.0 '@jest/pattern': 30.0.1 @@ -24065,7 +24041,7 @@ snapshots: exit-x: 0.2.2 graceful-fs: 4.2.11 jest-changed-files: 30.2.0 - jest-config: 30.2.0(@types/node@20.12.8)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@20.12.8)(typescript@4.9.5)) + jest-config: 30.2.0(@types/node@20.12.8)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@20.12.8)(typescript@5.9.3)) jest-haste-map: 30.2.0 jest-message-util: 30.2.0 jest-regex-util: 30.0.1 @@ -24555,10 +24531,10 @@ snapshots: '@inquirer/prompts': 7.3.2(@types/node@25.5.0) '@inquirer/type': 1.5.5 - '@listr2/prompt-adapter-inquirer@3.0.5(@inquirer/prompts@7.10.1(@types/node@20.12.8))(@types/node@20.12.8)(listr2@9.0.5)': + '@listr2/prompt-adapter-inquirer@3.0.5(@inquirer/prompts@7.10.1(@types/node@25.5.0))(@types/node@25.5.0)(listr2@9.0.5)': dependencies: - '@inquirer/prompts': 7.10.1(@types/node@20.12.8) - '@inquirer/type': 3.0.10(@types/node@20.12.8) + '@inquirer/prompts': 7.10.1(@types/node@25.5.0) + '@inquirer/type': 3.0.10(@types/node@25.5.0) listr2: 9.0.5 transitivePeerDependencies: - '@types/node' @@ -25802,6 +25778,10 @@ snapshots: '@pkgr/core@0.2.9': {} + '@playwright/test@1.58.2': + dependencies: + playwright: 1.58.2 + '@popperjs/core@2.11.8': {} '@preact/signals-core@1.8.0': {} @@ -27431,9 +27411,9 @@ snapshots: dependencies: vite: 6.4.1(@types/node@25.5.0)(jiti@2.6.1)(less@4.4.2)(lightningcss@1.30.2)(sass-embedded@1.97.1)(sass@1.97.1)(terser@5.46.0)(yaml@2.8.1) - '@vitejs/plugin-basic-ssl@2.1.0(vite@7.3.0(@types/node@20.12.8)(jiti@2.6.1)(less@4.4.2)(lightningcss@1.30.2)(sass-embedded@1.97.1)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.1))': + '@vitejs/plugin-basic-ssl@2.1.0(vite@7.3.0(@types/node@25.5.0)(jiti@2.6.1)(less@4.4.2)(lightningcss@1.30.2)(sass-embedded@1.97.1)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.1))': dependencies: - vite: 7.3.0(@types/node@20.12.8)(jiti@2.6.1)(less@4.4.2)(lightningcss@1.30.2)(sass-embedded@1.97.1)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.1) + vite: 7.3.0(@types/node@25.5.0)(jiti@2.6.1)(less@4.4.2)(lightningcss@1.30.2)(sass-embedded@1.97.1)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.1) '@vitejs/plugin-react@4.7.0(vite@6.4.1(@types/node@25.5.0)(jiti@2.6.1)(less@4.4.2)(lightningcss@1.30.2)(sass-embedded@1.97.1)(sass@1.97.1)(terser@5.46.0)(yaml@2.8.1))': dependencies: @@ -28479,22 +28459,22 @@ snapshots: postcss: 8.5.6 postcss-value-parser: 4.2.0 - autoprefixer@10.4.27(postcss@8.4.38): + autoprefixer@10.4.27(postcss@8.5.6): dependencies: browserslist: 4.28.1 caniuse-lite: 1.0.30001776 fraction.js: 5.3.4 picocolors: 1.1.1 - postcss: 8.4.38 + postcss: 8.5.6 postcss-value-parser: 4.2.0 - autoprefixer@10.4.27(postcss@8.5.6): + autoprefixer@10.4.27(postcss@8.5.8): dependencies: browserslist: 4.28.1 caniuse-lite: 1.0.30001776 fraction.js: 5.3.4 picocolors: 1.1.1 - postcss: 8.5.6 + postcss: 8.5.8 postcss-value-parser: 4.2.0 available-typed-arrays@1.0.7: @@ -30259,6 +30239,21 @@ snapshots: - ts-node optional: true + create-jest@29.7.0(@types/node@25.5.0)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@25.5.0)(typescript@5.9.3)): + dependencies: + '@jest/types': 29.6.3 + chalk: 4.1.2 + exit: 0.1.2 + graceful-fs: 4.2.11 + jest-config: 29.7.0(@types/node@25.5.0)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@25.5.0)(typescript@5.9.3)) + jest-util: 29.7.0 + prompts: 2.4.2 + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + create-require@1.1.1: {} cross-env@7.0.3: @@ -31692,14 +31687,14 @@ snapshots: transitivePeerDependencies: - eslint-plugin-import - eslint-config-devextreme@0.2.0(yq7mldp4wyckzhqs3zqk5sdezm): + eslint-config-devextreme@0.2.0(6ypj2vslczvlh7srkoyq3dooyq): dependencies: '@typescript-eslint/eslint-plugin': 8.52.0(@typescript-eslint/parser@8.52.0(eslint@9.39.4(jiti@2.6.1))(typescript@4.9.5))(eslint@9.39.4(jiti@2.6.1))(typescript@4.9.5) eslint: 9.39.4(jiti@2.6.1) eslint-config-airbnb-base: 15.0.0(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.52.0(eslint@9.39.4(jiti@2.6.1))(typescript@4.9.5))(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1)) eslint-config-airbnb-typescript: 18.0.0(@typescript-eslint/eslint-plugin@8.52.0(@typescript-eslint/parser@8.52.0(eslint@9.39.4(jiti@2.6.1))(typescript@4.9.5))(eslint@9.39.4(jiti@2.6.1))(typescript@4.9.5))(@typescript-eslint/parser@8.52.0(eslint@9.39.4(jiti@2.6.1))(typescript@4.9.5))(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.52.0(eslint@9.39.4(jiti@2.6.1))(typescript@4.9.5))(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1)) eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.52.0(eslint@9.39.4(jiti@2.6.1))(typescript@4.9.5))(eslint@9.39.4(jiti@2.6.1)) - eslint-plugin-jest: 29.15.0(@typescript-eslint/eslint-plugin@8.52.0(@typescript-eslint/parser@8.52.0(eslint@9.39.4(jiti@2.6.1))(typescript@4.9.5))(eslint@9.39.4(jiti@2.6.1))(typescript@4.9.5))(eslint@9.39.4(jiti@2.6.1))(jest@30.2.0(@types/node@20.12.8)(babel-plugin-macros@3.1.0)(node-notifier@9.0.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@20.12.8)(typescript@4.9.5)))(typescript@4.9.5) + eslint-plugin-jest: 29.15.0(@typescript-eslint/eslint-plugin@8.52.0(@typescript-eslint/parser@8.52.0(eslint@9.39.4(jiti@2.6.1))(typescript@4.9.5))(eslint@9.39.4(jiti@2.6.1))(typescript@4.9.5))(eslint@9.39.4(jiti@2.6.1))(jest@30.2.0(@types/node@25.5.0)(babel-plugin-macros@3.1.0)(node-notifier@9.0.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@25.5.0)(typescript@4.9.5)))(typescript@4.9.5) eslint-plugin-jest-formatting: 3.1.0(eslint@9.39.4(jiti@2.6.1)) eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.4(jiti@2.6.1)) eslint-plugin-qunit: 8.2.5(eslint@9.39.4(jiti@2.6.1)) @@ -31782,25 +31777,6 @@ snapshots: stylelint: 16.22.0(typescript@5.9.3) stylelint-config-standard: 38.0.0(stylelint@16.22.0(typescript@5.9.3)) - eslint-config-devextreme@1.1.9(satltipdsoawfxnov7ffi4z7ju): - dependencies: - '@stylistic/eslint-plugin': 5.10.0(eslint@9.39.4(jiti@2.6.1)) - '@typescript-eslint/eslint-plugin': 8.52.0(@typescript-eslint/parser@8.52.0(eslint@9.39.4(jiti@2.6.1))(typescript@4.9.5))(eslint@9.39.4(jiti@2.6.1))(typescript@4.9.5) - '@typescript-eslint/parser': 8.52.0(eslint@9.39.4(jiti@2.6.1))(typescript@4.9.5) - eslint: 9.39.4(jiti@2.6.1) - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.52.0(eslint@9.39.4(jiti@2.6.1))(typescript@4.9.5))(eslint@9.39.4(jiti@2.6.1)) - eslint-plugin-jest: 29.15.0(@typescript-eslint/eslint-plugin@8.52.0(@typescript-eslint/parser@8.52.0(eslint@9.39.4(jiti@2.6.1))(typescript@4.9.5))(eslint@9.39.4(jiti@2.6.1))(typescript@4.9.5))(eslint@9.39.4(jiti@2.6.1))(jest@30.2.0(@types/node@20.12.8)(babel-plugin-macros@3.1.0)(node-notifier@9.0.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@20.12.8)(typescript@4.9.5)))(typescript@4.9.5) - eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.4(jiti@2.6.1)) - eslint-plugin-no-only-tests: 3.3.0 - eslint-plugin-qunit: 8.2.5(eslint@9.39.4(jiti@2.6.1)) - eslint-plugin-react: 7.37.5(eslint@9.39.4(jiti@2.6.1)) - eslint-plugin-react-perf: 3.3.3(eslint@9.39.4(jiti@2.6.1)) - eslint-plugin-rulesdir: 0.2.2 - eslint-plugin-spellcheck: 0.0.20(eslint@9.39.4(jiti@2.6.1)) - eslint-plugin-vue: 10.6.2(@stylistic/eslint-plugin@5.10.0(eslint@9.39.4(jiti@2.6.1)))(@typescript-eslint/parser@8.52.0(eslint@9.39.4(jiti@2.6.1))(typescript@4.9.5))(eslint@9.39.4(jiti@2.6.1))(vue-eslint-parser@10.2.0(eslint@9.39.4(jiti@2.6.1))) - stylelint: 15.11.0(typescript@4.9.5) - stylelint-config-standard: 38.0.0(stylelint@15.11.0(typescript@4.9.5)) - eslint-config-devextreme@1.1.9(sizsemxbssuejtezeqnearawue): dependencies: '@stylistic/eslint-plugin': 5.10.0(eslint@9.39.4(jiti@2.6.1)) @@ -31839,6 +31815,25 @@ snapshots: stylelint: 16.22.0(typescript@4.9.5) stylelint-config-standard: 38.0.0(stylelint@16.22.0(typescript@4.9.5)) + eslint-config-devextreme@1.1.9(z5gvwpd2vz7hyhtqnrp7ixzxle): + dependencies: + '@stylistic/eslint-plugin': 5.10.0(eslint@9.39.4(jiti@2.6.1)) + '@typescript-eslint/eslint-plugin': 8.52.0(@typescript-eslint/parser@8.52.0(eslint@9.39.4(jiti@2.6.1))(typescript@4.9.5))(eslint@9.39.4(jiti@2.6.1))(typescript@4.9.5) + '@typescript-eslint/parser': 8.52.0(eslint@9.39.4(jiti@2.6.1))(typescript@4.9.5) + eslint: 9.39.4(jiti@2.6.1) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.52.0(eslint@9.39.4(jiti@2.6.1))(typescript@4.9.5))(eslint@9.39.4(jiti@2.6.1)) + eslint-plugin-jest: 29.15.0(@typescript-eslint/eslint-plugin@8.52.0(@typescript-eslint/parser@8.52.0(eslint@9.39.4(jiti@2.6.1))(typescript@4.9.5))(eslint@9.39.4(jiti@2.6.1))(typescript@4.9.5))(eslint@9.39.4(jiti@2.6.1))(jest@30.2.0(@types/node@25.5.0)(babel-plugin-macros@3.1.0)(node-notifier@9.0.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@25.5.0)(typescript@4.9.5)))(typescript@4.9.5) + eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.4(jiti@2.6.1)) + eslint-plugin-no-only-tests: 3.3.0 + eslint-plugin-qunit: 8.2.5(eslint@9.39.4(jiti@2.6.1)) + eslint-plugin-react: 7.37.5(eslint@9.39.4(jiti@2.6.1)) + eslint-plugin-react-perf: 3.3.3(eslint@9.39.4(jiti@2.6.1)) + eslint-plugin-rulesdir: 0.2.2 + eslint-plugin-spellcheck: 0.0.20(eslint@9.39.4(jiti@2.6.1)) + eslint-plugin-vue: 10.6.2(@stylistic/eslint-plugin@5.10.0(eslint@9.39.4(jiti@2.6.1)))(@typescript-eslint/parser@8.52.0(eslint@9.39.4(jiti@2.6.1))(typescript@4.9.5))(eslint@9.39.4(jiti@2.6.1))(vue-eslint-parser@10.2.0(eslint@9.39.4(jiti@2.6.1))) + stylelint: 15.11.0(typescript@4.9.5) + stylelint-config-standard: 38.0.0(stylelint@15.11.0(typescript@4.9.5)) + eslint-import-resolver-node@0.3.9: dependencies: debug: 3.2.7 @@ -31998,17 +31993,6 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-plugin-jest@29.15.0(@typescript-eslint/eslint-plugin@8.52.0(@typescript-eslint/parser@8.52.0(eslint@9.39.4(jiti@2.6.1))(typescript@4.9.5))(eslint@9.39.4(jiti@2.6.1))(typescript@4.9.5))(eslint@9.39.4(jiti@2.6.1))(jest@30.2.0(@types/node@20.12.8)(babel-plugin-macros@3.1.0)(node-notifier@9.0.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@20.12.8)(typescript@4.9.5)))(typescript@4.9.5): - dependencies: - '@typescript-eslint/utils': 8.52.0(eslint@9.39.4(jiti@2.6.1))(typescript@4.9.5) - eslint: 9.39.4(jiti@2.6.1) - optionalDependencies: - '@typescript-eslint/eslint-plugin': 8.52.0(@typescript-eslint/parser@8.52.0(eslint@9.39.4(jiti@2.6.1))(typescript@4.9.5))(eslint@9.39.4(jiti@2.6.1))(typescript@4.9.5) - jest: 30.2.0(@types/node@20.12.8)(babel-plugin-macros@3.1.0)(node-notifier@9.0.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@20.12.8)(typescript@4.9.5)) - typescript: 4.9.5 - transitivePeerDependencies: - - supports-color - eslint-plugin-jest@29.15.0(@typescript-eslint/eslint-plugin@8.52.0(@typescript-eslint/parser@8.52.0(eslint@9.39.4(jiti@2.6.1))(typescript@4.9.5))(eslint@9.39.4(jiti@2.6.1))(typescript@4.9.5))(eslint@9.39.4(jiti@2.6.1))(jest@30.2.0(@types/node@25.5.0)(babel-plugin-macros@3.1.0)(node-notifier@9.0.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@25.5.0)(typescript@4.9.5)))(typescript@4.9.5): dependencies: '@typescript-eslint/utils': 8.52.0(eslint@9.39.4(jiti@2.6.1))(typescript@4.9.5) @@ -33111,6 +33095,9 @@ snapshots: nan: 2.22.0 optional: true + fsevents@2.3.2: + optional: true + fsevents@2.3.3: optional: true @@ -35091,6 +35078,27 @@ snapshots: - ts-node optional: true + jest-cli@29.7.0(@types/node@25.5.0)(babel-plugin-macros@3.1.0)(node-notifier@9.0.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@25.5.0)(typescript@5.9.3)): + dependencies: + '@jest/core': 29.7.0(babel-plugin-macros@3.1.0)(node-notifier@9.0.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@25.5.0)(typescript@5.9.3)) + '@jest/test-result': 29.7.0 + '@jest/types': 29.6.3 + chalk: 4.1.2 + create-jest: 29.7.0(@types/node@25.5.0)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@25.5.0)(typescript@5.9.3)) + exit: 0.1.2 + import-local: 3.2.0 + jest-config: 29.7.0(@types/node@25.5.0)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@25.5.0)(typescript@5.9.3)) + jest-util: 29.7.0 + jest-validate: 29.7.0 + yargs: 17.7.2 + optionalDependencies: + node-notifier: 9.0.1 + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + jest-cli@30.2.0(@types/node@18.19.130)(babel-plugin-macros@3.1.0)(node-notifier@9.0.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@18.19.130)(typescript@4.9.5)): dependencies: '@jest/core': 30.2.0(babel-plugin-macros@3.1.0)(node-notifier@9.0.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@18.19.130)(typescript@4.9.5)) @@ -35155,15 +35163,15 @@ snapshots: - ts-node optional: true - jest-cli@30.2.0(@types/node@20.12.8)(babel-plugin-macros@3.1.0)(node-notifier@9.0.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@20.12.8)(typescript@4.9.5)): + jest-cli@30.2.0(@types/node@20.12.8)(babel-plugin-macros@3.1.0)(node-notifier@9.0.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@20.12.8)(typescript@5.9.3)): dependencies: - '@jest/core': 30.2.0(babel-plugin-macros@3.1.0)(node-notifier@9.0.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@20.12.8)(typescript@4.9.5)) + '@jest/core': 30.2.0(babel-plugin-macros@3.1.0)(node-notifier@9.0.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@20.12.8)(typescript@5.9.3)) '@jest/test-result': 30.2.0 '@jest/types': 30.2.0 chalk: 4.1.2 exit-x: 0.2.2 import-local: 3.2.0 - jest-config: 30.2.0(@types/node@20.12.8)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@20.12.8)(typescript@4.9.5)) + jest-config: 30.2.0(@types/node@20.12.8)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@20.12.8)(typescript@5.9.3)) jest-util: 30.2.0 jest-validate: 30.2.0 yargs: 17.7.2 @@ -35335,6 +35343,37 @@ snapshots: - babel-plugin-macros - supports-color + jest-config@29.7.0(@types/node@20.12.8)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@25.5.0)(typescript@5.9.3)): + dependencies: + '@babel/core': 7.29.0 + '@jest/test-sequencer': 29.7.0 + '@jest/types': 29.6.3 + babel-jest: 29.7.0(@babel/core@7.29.0) + chalk: 4.1.2 + ci-info: 3.9.0 + deepmerge: 4.3.1 + glob: 7.2.3 + graceful-fs: 4.2.11 + jest-circus: 29.7.0(babel-plugin-macros@3.1.0) + jest-environment-node: 29.7.0 + jest-get-type: 29.6.3 + jest-regex-util: 29.6.3 + jest-resolve: 29.7.0 + jest-runner: 29.7.0 + jest-util: 29.7.0 + jest-validate: 29.7.0 + micromatch: 4.0.8 + parse-json: 5.2.0 + pretty-format: 29.7.0 + slash: 3.0.0 + strip-json-comments: 3.1.1 + optionalDependencies: + '@types/node': 20.12.8 + ts-node: 10.9.2(@swc/core@1.15.3)(@types/node@25.5.0)(typescript@5.9.3) + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + jest-config@29.7.0(@types/node@25.5.0)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@20.12.8)(typescript@5.9.3)): dependencies: '@babel/core': 7.29.0 @@ -35367,6 +35406,37 @@ snapshots: - supports-color optional: true + jest-config@29.7.0(@types/node@25.5.0)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@25.5.0)(typescript@5.9.3)): + dependencies: + '@babel/core': 7.29.0 + '@jest/test-sequencer': 29.7.0 + '@jest/types': 29.6.3 + babel-jest: 29.7.0(@babel/core@7.29.0) + chalk: 4.1.2 + ci-info: 3.9.0 + deepmerge: 4.3.1 + glob: 7.2.3 + graceful-fs: 4.2.11 + jest-circus: 29.7.0(babel-plugin-macros@3.1.0) + jest-environment-node: 29.7.0 + jest-get-type: 29.6.3 + jest-regex-util: 29.6.3 + jest-resolve: 29.7.0 + jest-runner: 29.7.0 + jest-util: 29.7.0 + jest-validate: 29.7.0 + micromatch: 4.0.8 + parse-json: 5.2.0 + pretty-format: 29.7.0 + slash: 3.0.0 + strip-json-comments: 3.1.1 + optionalDependencies: + '@types/node': 25.5.0 + ts-node: 10.9.2(@swc/core@1.15.3)(@types/node@25.5.0)(typescript@5.9.3) + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + jest-config@30.2.0(@types/node@18.19.130)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@18.19.130)(typescript@4.9.5)): dependencies: '@babel/core': 7.29.0 @@ -35567,39 +35637,6 @@ snapshots: - supports-color optional: true - jest-config@30.2.0(@types/node@20.12.8)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@20.12.8)(typescript@4.9.5)): - dependencies: - '@babel/core': 7.29.0 - '@jest/get-type': 30.1.0 - '@jest/pattern': 30.0.1 - '@jest/test-sequencer': 30.2.0 - '@jest/types': 30.2.0 - babel-jest: 30.2.0(@babel/core@7.29.0) - chalk: 4.1.2 - ci-info: 4.3.0 - deepmerge: 4.3.1 - glob: 10.5.0 - graceful-fs: 4.2.11 - jest-circus: 30.2.0(babel-plugin-macros@3.1.0) - jest-docblock: 30.2.0 - jest-environment-node: 30.2.0 - jest-regex-util: 30.0.1 - jest-resolve: 30.2.0 - jest-runner: 30.2.0 - jest-util: 30.2.0 - jest-validate: 30.2.0 - micromatch: 4.0.8 - parse-json: 5.2.0 - pretty-format: 30.2.0 - slash: 3.0.0 - strip-json-comments: 3.1.1 - optionalDependencies: - '@types/node': 20.12.8 - ts-node: 10.9.2(@swc/core@1.15.3)(@types/node@20.12.8)(typescript@4.9.5) - transitivePeerDependencies: - - babel-plugin-macros - - supports-color - jest-config@30.2.0(@types/node@20.12.8)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@20.12.8)(typescript@5.9.3)): dependencies: '@babel/core': 7.29.0 @@ -36328,6 +36365,20 @@ snapshots: - ts-node optional: true + jest@29.7.0(@types/node@25.5.0)(babel-plugin-macros@3.1.0)(node-notifier@9.0.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@25.5.0)(typescript@5.9.3)): + dependencies: + '@jest/core': 29.7.0(babel-plugin-macros@3.1.0)(node-notifier@9.0.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@25.5.0)(typescript@5.9.3)) + '@jest/types': 29.6.3 + import-local: 3.2.0 + jest-cli: 29.7.0(@types/node@25.5.0)(babel-plugin-macros@3.1.0)(node-notifier@9.0.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@25.5.0)(typescript@5.9.3)) + optionalDependencies: + node-notifier: 9.0.1 + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + jest@30.2.0(@types/node@18.19.130)(babel-plugin-macros@3.1.0)(node-notifier@9.0.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@18.19.130)(typescript@4.9.5)): dependencies: '@jest/core': 30.2.0(babel-plugin-macros@3.1.0)(node-notifier@9.0.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@18.19.130)(typescript@4.9.5)) @@ -36374,12 +36425,12 @@ snapshots: - ts-node optional: true - jest@30.2.0(@types/node@20.12.8)(babel-plugin-macros@3.1.0)(node-notifier@9.0.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@20.12.8)(typescript@4.9.5)): + jest@30.2.0(@types/node@20.12.8)(babel-plugin-macros@3.1.0)(node-notifier@9.0.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@20.12.8)(typescript@5.9.3)): dependencies: - '@jest/core': 30.2.0(babel-plugin-macros@3.1.0)(node-notifier@9.0.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@20.12.8)(typescript@4.9.5)) + '@jest/core': 30.2.0(babel-plugin-macros@3.1.0)(node-notifier@9.0.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@20.12.8)(typescript@5.9.3)) '@jest/types': 30.2.0 import-local: 3.2.0 - jest-cli: 30.2.0(@types/node@20.12.8)(babel-plugin-macros@3.1.0)(node-notifier@9.0.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@20.12.8)(typescript@4.9.5)) + jest-cli: 30.2.0(@types/node@20.12.8)(babel-plugin-macros@3.1.0)(node-notifier@9.0.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@20.12.8)(typescript@5.9.3)) optionalDependencies: node-notifier: 9.0.1 transitivePeerDependencies: @@ -38962,6 +39013,10 @@ snapshots: optionalDependencies: '@napi-rs/nice': 1.1.1 + pixelmatch@7.1.0: + dependencies: + pngjs: 7.0.0 + pkce-challenge@5.0.1: {} pkg-dir@4.2.0: @@ -38985,6 +39040,14 @@ snapshots: pvutils: 1.1.5 tslib: 2.8.1 + playwright-core@1.58.2: {} + + playwright@1.58.2: + dependencies: + playwright-core: 1.58.2 + optionalDependencies: + fsevents: 2.3.2 + plimit-lit@1.6.1: dependencies: queue-lit: 1.5.2 @@ -39018,6 +39081,8 @@ snapshots: pngjs@6.0.0: {} + pngjs@7.0.0: {} + portfinder@1.0.32: dependencies: async: 2.6.4 @@ -39103,9 +39168,9 @@ snapshots: dependencies: postcss: 8.5.6 - postcss-scss@4.0.9(postcss@8.5.8): + postcss-scss@4.0.9(postcss@8.5.6): dependencies: - postcss: 8.5.8 + postcss: 8.5.6 postcss-selector-parser@6.1.2: dependencies: @@ -41254,14 +41319,14 @@ snapshots: postcss-html: 1.7.0 stylelint: 16.22.0(typescript@5.9.3) - stylelint-config-recommended-scss@11.0.0(postcss@8.5.8)(stylelint@15.11.0(typescript@5.9.3)): + stylelint-config-recommended-scss@11.0.0(postcss@8.5.6)(stylelint@15.11.0(typescript@5.9.3)): dependencies: - postcss-scss: 4.0.9(postcss@8.5.8) + postcss-scss: 4.0.9(postcss@8.5.6) stylelint: 15.11.0(typescript@5.9.3) stylelint-config-recommended: 12.0.0(stylelint@15.11.0(typescript@5.9.3)) stylelint-scss: 4.7.0(stylelint@15.11.0(typescript@5.9.3)) optionalDependencies: - postcss: 8.5.8 + postcss: 8.5.6 stylelint-config-recommended-vue@1.6.1(postcss-html@1.7.0)(stylelint@16.22.0(typescript@5.9.3)): dependencies: @@ -41291,13 +41356,13 @@ snapshots: dependencies: stylelint: 16.22.0(typescript@5.9.3) - stylelint-config-standard-scss@9.0.0(postcss@8.5.8)(stylelint@15.11.0(typescript@5.9.3)): + stylelint-config-standard-scss@9.0.0(postcss@8.5.6)(stylelint@15.11.0(typescript@5.9.3)): dependencies: stylelint: 15.11.0(typescript@5.9.3) - stylelint-config-recommended-scss: 11.0.0(postcss@8.5.8)(stylelint@15.11.0(typescript@5.9.3)) + stylelint-config-recommended-scss: 11.0.0(postcss@8.5.6)(stylelint@15.11.0(typescript@5.9.3)) stylelint-config-standard: 33.0.0(stylelint@15.11.0(typescript@5.9.3)) optionalDependencies: - postcss: 8.5.8 + postcss: 8.5.6 stylelint-config-standard@33.0.0(stylelint@15.11.0(typescript@5.9.3)): dependencies: @@ -42408,11 +42473,11 @@ snapshots: '@jest/types': 29.6.3 babel-jest: 29.7.0(@babel/core@7.29.0) - ts-jest@29.1.2(@babel/core@7.29.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.29.0))(jest@30.2.0(@types/node@20.12.8)(babel-plugin-macros@3.1.0)(node-notifier@9.0.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@20.12.8)(typescript@4.9.5)))(typescript@4.9.5): + ts-jest@29.1.2(@babel/core@7.29.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.29.0))(jest@30.2.0(@types/node@25.5.0)(babel-plugin-macros@3.1.0)(node-notifier@9.0.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@25.5.0)(typescript@4.9.5)))(typescript@4.9.5): dependencies: bs-logger: 0.2.6 fast-json-stable-stringify: 2.1.0 - jest: 30.2.0(@types/node@20.12.8)(babel-plugin-macros@3.1.0)(node-notifier@9.0.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@20.12.8)(typescript@4.9.5)) + jest: 30.2.0(@types/node@25.5.0)(babel-plugin-macros@3.1.0)(node-notifier@9.0.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@25.5.0)(typescript@4.9.5)) jest-util: 29.7.0 json5: 2.2.3 lodash.memoize: 4.1.2 @@ -42460,17 +42525,17 @@ snapshots: '@jest/types': 29.6.3 babel-jest: 29.7.0(@babel/core@7.28.6) - ts-jest@29.1.3(@babel/core@7.29.0)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.29.0))(jest@30.2.0(@types/node@25.5.0)(babel-plugin-macros@3.1.0)(node-notifier@9.0.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@25.5.0)(typescript@4.9.5)))(typescript@4.9.5): + ts-jest@29.1.3(@babel/core@7.29.0)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.29.0))(jest@30.2.0(@types/node@20.12.8)(babel-plugin-macros@3.1.0)(node-notifier@9.0.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@20.12.8)(typescript@5.9.3)))(typescript@5.9.3): dependencies: bs-logger: 0.2.6 fast-json-stable-stringify: 2.1.0 - jest: 30.2.0(@types/node@25.5.0)(babel-plugin-macros@3.1.0)(node-notifier@9.0.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@25.5.0)(typescript@4.9.5)) + jest: 30.2.0(@types/node@20.12.8)(babel-plugin-macros@3.1.0)(node-notifier@9.0.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@20.12.8)(typescript@5.9.3)) jest-util: 29.7.0 json5: 2.2.3 lodash.memoize: 4.1.2 make-error: 1.3.6 semver: 7.7.2 - typescript: 4.9.5 + typescript: 5.9.3 yargs-parser: 21.1.1 optionalDependencies: '@babel/core': 7.29.0 @@ -42478,17 +42543,17 @@ snapshots: '@jest/types': 29.6.3 babel-jest: 29.7.0(@babel/core@7.29.0) - ts-jest@29.1.3(@babel/core@7.29.0)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.29.0))(jest@30.2.0(@types/node@25.5.0)(babel-plugin-macros@3.1.0)(node-notifier@9.0.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@25.5.0)(typescript@5.9.3)))(typescript@5.9.3): + ts-jest@29.1.3(@babel/core@7.29.0)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.29.0))(jest@30.2.0(@types/node@25.5.0)(babel-plugin-macros@3.1.0)(node-notifier@9.0.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@25.5.0)(typescript@4.9.5)))(typescript@4.9.5): dependencies: bs-logger: 0.2.6 fast-json-stable-stringify: 2.1.0 - jest: 30.2.0(@types/node@25.5.0)(babel-plugin-macros@3.1.0)(node-notifier@9.0.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@25.5.0)(typescript@5.9.3)) + jest: 30.2.0(@types/node@25.5.0)(babel-plugin-macros@3.1.0)(node-notifier@9.0.1)(ts-node@10.9.2(@swc/core@1.15.3)(@types/node@25.5.0)(typescript@4.9.5)) jest-util: 29.7.0 json5: 2.2.3 lodash.memoize: 4.1.2 make-error: 1.3.6 semver: 7.7.2 - typescript: 5.9.3 + typescript: 4.9.5 yargs-parser: 21.1.1 optionalDependencies: '@babel/core': 7.29.0 @@ -42582,27 +42647,6 @@ snapshots: optionalDependencies: '@swc/core': 1.15.3 - ts-node@10.9.2(@swc/core@1.15.3)(@types/node@20.12.8)(typescript@4.9.5): - dependencies: - '@cspotcode/source-map-support': 0.8.1 - '@tsconfig/node10': 1.0.11 - '@tsconfig/node12': 1.0.11 - '@tsconfig/node14': 1.0.3 - '@tsconfig/node16': 1.0.4 - '@types/node': 20.12.8 - acorn: 8.15.0 - acorn-walk: 8.3.4 - arg: 4.1.3 - create-require: 1.1.1 - diff: 4.0.2 - make-error: 1.3.6 - typescript: 4.9.5 - v8-compile-cache-lib: 3.0.1 - yn: 3.1.1 - optionalDependencies: - '@swc/core': 1.15.3 - optional: true - ts-node@10.9.2(@swc/core@1.15.3)(@types/node@20.12.8)(typescript@5.9.3): dependencies: '@cspotcode/source-map-support': 0.8.1 @@ -43477,7 +43521,7 @@ snapshots: terser: 5.46.0 yaml: 2.8.1 - vite@7.3.0(@types/node@20.12.8)(jiti@2.6.1)(less@4.4.2)(lightningcss@1.30.2)(sass-embedded@1.97.1)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.1): + vite@7.3.0(@types/node@25.5.0)(jiti@2.6.1)(less@4.4.2)(lightningcss@1.30.2)(sass-embedded@1.97.1)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.1): dependencies: esbuild: 0.27.2 fdir: 6.5.0(picomatch@4.0.3) @@ -43486,7 +43530,7 @@ snapshots: rollup: 4.59.0 tinyglobby: 0.2.15 optionalDependencies: - '@types/node': 20.12.8 + '@types/node': 25.5.0 fsevents: 2.3.3 jiti: 2.6.1 less: 4.4.2 From a74ebdf8511c0176b919f630439796cccca2e495 Mon Sep 17 00:00:00 2001 From: Aleksey Semikozov Date: Sat, 21 Mar 2026 23:13:45 -0300 Subject: [PATCH 02/13] Playwright POC - improved scheduler test conversions from AI agents --- e2e/testcafe-devextreme/playwright-helpers.ts | 65 ++ .../common/accessibility/contrast.spec.ts | 2 +- .../aiColumn/columnFixing.visual.spec.ts | 2 +- .../keyboardNavigation.visual.spec.ts | 2 +- .../editing/editing.functional_matrix.spec.ts | 6 +- .../common/editing/editingEvents.spec.ts | 4 +- .../editingNewRow.functional_matrix.spec.ts | 4 +- .../common/editing/functional.spec.ts | 2 +- .../common/editing/initNewRow.spec.ts | 6 +- .../dataGrid/common/editing/visual.spec.ts | 2 +- .../common/filterPanel/functional.spec.ts | 28 +- .../common/filterPanel/visual.spec.ts | 6 +- ...100_changeFIlterIcon.visual_matrix.spec.ts | 2 +- .../T1162057_oneGroupOnDifferentPages.spec.ts | 8 +- .../calculateGroupValueRuntimeChanges.spec.ts | 16 +- .../common/headerFilter/headerFilter.spec.ts | 10 +- .../dataGrid/common/headerPanel.spec.ts | 4 +- .../keyboardNavigation.visual.spec.ts | 2 +- .../masterDetail/index.spec.ts | 6 +- .../startEditing.functional.spec.ts | 2 +- .../virtualColumns.functional.spec.ts | 2 +- .../dataGrid/common/pager.spec.ts | 8 +- .../dataGrid/common/searchPanel.spec.ts | 2 +- .../dataGrid/common/security/xss.spec.ts | 2 +- .../dataGrid/common/summary.spec.ts | 2 +- .../dataGrid/common/tagBox.spec.ts | 2 +- .../common/validation/cellEditing.spec.ts | 2 +- .../common/validation/validationPopup.spec.ts | 2 +- .../common/virtualColumns/functional.spec.ts | 2 +- .../dataGrid/sticky/common/appearance.spec.ts | 6 +- .../sticky/common/columnFixingIcons.spec.ts | 2 +- .../sticky/common/focusOverlay.spec.ts | 6 +- .../sticky/common/withEditing.spec.ts | 2 +- .../sticky/common/withFilterRow.spec.ts | 2 +- .../common/withKeyboardNavigation.spec.ts | 2 +- .../scheduler/common/agenda/keyField.spec.ts | 193 ++---- .../scheduler/common/agenda/layout.spec.ts | 212 ++----- .../common/agenda/switchingToAgenda.spec.ts | 56 +- .../scheduler/common/agenda/tooltip.spec.ts | 75 +-- .../common/dragAndDrop/basic.spec.ts | 371 ++++++----- .../dragAndDrop/outlookDragging/base.spec.ts | 575 +++++------------- .../schedulerInContainer.spec.ts | 115 ++-- .../schedulerInTransformContainer.spec.ts | 111 ++-- .../outlookDragging/shiftedContainer.spec.ts | 104 ++-- .../templates/appointmentTemplate.spec.ts | 87 +-- .../layout/templates/cellTemplate.spec.ts | 138 ++--- .../layout/templates/tooltipTemplate.spec.ts | 62 +- .../layout/views/crossScrolling.spec.ts | 95 +-- .../common/layout/views/day/allDay.spec.ts | 67 +- .../layout/views/firstDayOfWeek.spec.ts | 24 +- .../intervalCount/viewsWithStartDate.spec.ts | 136 +---- .../views/material/withoutAllDay.spec.ts | 22 +- .../views/timeline/crossScrolling.spec.ts | 84 +-- .../layout/views/timeline/grouping.spec.ts | 40 +- .../layout/views/timeline/month.spec.ts | 27 +- .../appointmentWithoutTimezone.spec.ts | 339 +++-------- .../monthlyRecurrentAppointment.spec.ts | 424 +++---------- 57 files changed, 1187 insertions(+), 2393 deletions(-) create mode 100644 e2e/testcafe-devextreme/playwright-helpers.ts diff --git a/e2e/testcafe-devextreme/playwright-helpers.ts b/e2e/testcafe-devextreme/playwright-helpers.ts new file mode 100644 index 000000000000..9a35b8e71d18 --- /dev/null +++ b/e2e/testcafe-devextreme/playwright-helpers.ts @@ -0,0 +1,65 @@ +import { Page, expect, Locator } from '@playwright/test'; + +export async function createWidget( + page: Page, + widgetName: string, + options: Record, + selector = '#container', +): Promise { + await page.evaluate( + ({ widgetName: wn, options: opts, selector: sel }) => { + const $element = (window as any).$(sel); + $element[wn](opts); + }, + { widgetName, options: JSON.parse(JSON.stringify(options)), selector }, + ); +} + +export async function testScreenshot( + page: Page, + screenshotName: string, + options?: { element?: Locator }, +): Promise { + const target = options?.element ?? page; + await expect(target).toHaveScreenshot(screenshotName); +} + +export async function changeTheme(page: Page, theme: string): Promise { + await page.evaluate( + (t) => + new Promise((resolve) => { + (window as any).DevExpress.ui.themes.ready(resolve); + (window as any).DevExpress.ui.themes.current(t); + }), + theme, + ); +} + +export async function insertStylesheetRulesToPage( + page: Page, + cssRules: string, +): Promise { + await page.evaluate((rules) => { + const style = document.createElement('style'); + style.setAttribute('type', 'text/css'); + style.textContent = rules; + document.head.appendChild(style); + }, cssRules); +} + +export async function setStyleAttribute( + page: Page, + locator: Locator, + styleValue: string, +): Promise { + await locator.evaluate((el, sv) => { + (el as HTMLElement).style.cssText = sv; + }, styleValue); +} + +export async function getStyleAttribute( + page: Page, + locator: Locator, +): Promise { + return locator.evaluate((el) => (el as HTMLElement).style.cssText); +} diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/accessibility/contrast.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/accessibility/contrast.spec.ts index 2657cb7343c8..6a5128d4af24 100644 --- a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/accessibility/contrast.spec.ts +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/accessibility/contrast.spec.ts @@ -55,7 +55,7 @@ test.describe('DataGrid - contrast', () => { .getSearchIcon() .element; - await (page.locator('.dx-datagrid-filter-row td').click().nth(0).element); + await (page.locator('.dx-datagrid-filter-row td').nth(0).element).click(); await page.keyboard.press('tab'); expect(await searchIconContainer.focused); await t.ok(); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/aiColumn/columnFixing.visual.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/aiColumn/columnFixing.visual.spec.ts index 3f2d24798c65..f59c3f6e8a67 100644 --- a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/aiColumn/columnFixing.visual.spec.ts +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/aiColumn/columnFixing.visual.spec.ts @@ -43,7 +43,7 @@ test.describe('Ai Column - Sticky columns.Visual', () => { // act await t.rightClick(page.locator('.dx-header-row').nth(0).locator('td').nth(0)); - await (dataGrid.getContextMenu().click().getItemByText('Set Fixed Position')); + await (dataGrid.getContextMenu().getItemByText('Set Fixed Position')).click(); await testScreenshot(page, 'datagrid__ai-column-and-sticky-columns__context-menu.png', { element: page.locator('#container') }); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/aiColumn/keyboardNavigation.visual.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/aiColumn/keyboardNavigation.visual.spec.ts index 11e5f3d99297..9a850f8bb781 100644 --- a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/aiColumn/keyboardNavigation.visual.spec.ts +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/aiColumn/keyboardNavigation.visual.spec.ts @@ -40,7 +40,7 @@ test.describe('Ai Column.KeyboardNavigation.Visual', () => { expect(await page.locator('.dx-datagrid').first().isVisible()).toBeTruthy(); // act - await (headerRow.locator('td').nth(0).click().element); + await (headerRow.locator('td').nth(0).element).click(); await page.keyboard.press('tab'); // assert diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/editing/editing.functional_matrix.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/editing/editing.functional_matrix.spec.ts index 7535ea2b5554..fae24f20828e 100644 --- a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/editing/editing.functional_matrix.spec.ts +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/editing/editing.functional_matrix.spec.ts @@ -421,7 +421,7 @@ test.describe('Editing.FunctionalMatrix', () => { const saveButton = getSaveButton(mode, form); if (saveButton) { - await (saveButton).click({ offsetX: 5, offsetY: 5 }); + await (saveButton, { offsetX: 5, offsetY: 5 }).click(); } // eslint-disable-next-line no-restricted-syntax @@ -459,7 +459,7 @@ test.describe('Editing.FunctionalMatrix', () => { await ('body').click(); } - await (saveButton).click({ offsetX: 5, offsetY: 5 }); + await (saveButton, { offsetX: 5, offsetY: 5 }).click(); } const expectedColumnResult: ColumnInfo[] = [ @@ -525,7 +525,7 @@ test.describe('Editing.FunctionalMatrix', () => { const saveButton = getSaveButton(mode, form); if (saveButton) { - await (saveButton).click({ offsetX: 5, offsetY: 5 }); + await (saveButton, { offsetX: 5, offsetY: 5 }).click(); } } diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/editing/editingEvents.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/editing/editingEvents.spec.ts index 0988f4121ed0..c0bfc53ed07f 100644 --- a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/editing/editingEvents.spec.ts +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/editing/editingEvents.spec.ts @@ -108,10 +108,10 @@ test.describe('Editing events', () => { const dataRow = page.locator('.dx-data-row').nth(0); - await (dataRow.locator('td').nth(1).click().getLinkEdit()); + await (dataRow.locator('td').nth(1).getLinkEdit()).click(); await (dataRow.locator('td').nth(0).locator('.dx-editor-cell')).fill('test text'); - await (dataRow.locator('td').nth(1).click().getLinkSave()); + await (dataRow.locator('td').nth(1).getLinkSave()).click(); expect(await dataRow.locator('td').nth(1).getLinkSave().exists).toBe(expected); }); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/editing/editingNewRow.functional_matrix.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/editing/editingNewRow.functional_matrix.spec.ts index 78c68f420494..7d8f56e4215e 100644 --- a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/editing/editingNewRow.functional_matrix.spec.ts +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/editing/editingNewRow.functional_matrix.spec.ts @@ -193,7 +193,7 @@ test.describe('Editing.NewRow', () => { const saveButton = getSaveButton(mode, form); if (saveButton) { - await (saveButton).click({ offsetX: 5, offsetY: 5 }); + await (saveButton, { offsetX: 5, offsetY: 5 }).click(); } await checkSavedCell(t, columnInfo, page.locator('.dx-data-row').nth(2).locator('td').nth(columnInfo.columnIndex)); @@ -221,7 +221,7 @@ test.describe('Editing.NewRow', () => { const saveButton = getSaveButton(mode, form); if (saveButton) { - await (saveButton).click({ offsetX: 5, offsetY: 5 }); + await (saveButton, { offsetX: 5, offsetY: 5 }).click(); } // eslint-disable-next-line no-restricted-syntax diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/editing/functional.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/editing/functional.spec.ts index c026d271a5c8..56fb1b5afa4a 100644 --- a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/editing/functional.spec.ts +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/editing/functional.spec.ts @@ -52,7 +52,7 @@ test.describe('Editing.Functional', () => { const resolveOnSavingDeferred = ClientFunction(() => (window as any).deferred.resolve()); - await (page.locator('.dx-data-row').click().nth(0).locator('td').nth(0)); + await (page.locator('.dx-data-row').nth(0).locator('td').nth(0)).click(); await (page.locator('.dx-data-row').nth(0).locator('td').nth(0)).fill('new_value'); await page.keyboard.press('tab tab'); await resolveOnSavingDeferred(); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/editing/initNewRow.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/editing/initNewRow.spec.ts index a460760cdb3e..d522ef7cbdad 100644 --- a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/editing/initNewRow.spec.ts +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/editing/initNewRow.spec.ts @@ -33,14 +33,12 @@ test.describe('initNewRow', () => { }); await (; - page.locator('.dx-datagrid-header-panel').click().getAddRowButton(), - ) + page.locator('.dx-datagrid-header-panel').getAddRowButton(),).click() .click( dataGrid.getPopupEditForm().cancelButton, ); - await (page.locator('.dx-datagrid-header-panel').click().getAddRowButton(), - ); + await (page.locator('.dx-datagrid-header-panel').getAddRowButton(),).click(); expect(await dataGrid.getPopupEditForm().element.exists, diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/editing/visual.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/editing/visual.spec.ts index 51a1939e85c8..8d1528d05017 100644 --- a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/editing/visual.spec.ts +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/editing/visual.spec.ts @@ -39,7 +39,7 @@ test.describe('Editing.Visual', () => { // act await (dataGrid.getFormItemEditor(0)).fill('new'); - await (dataGrid.getEditForm().click().saveButton); + await (dataGrid.getEditForm().saveButton).click(); // assert await testScreenshot(page, 'grid-form-editing-T1193894.png', { element: page.locator('#container') }); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/filterPanel/functional.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/filterPanel/functional.spec.ts index a815d408268a..8bc03a1e208e 100644 --- a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/filterPanel/functional.spec.ts +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/filterPanel/functional.spec.ts @@ -111,17 +111,17 @@ test.describe('Filtering', () => { let filterBuilderPopup = await filterPanel.openFilterBuilderPopup(t); let filterBuilder = filterBuilderPopup.getFilterBuilder(); - await (filterBuilder.getAddButton().click()); + await (filterBuilder.getAddButton()).click(); expect(await FilterBuilder.getPopupTreeView().visible).toBeTruthy(); - await (FilterBuilder.getPopupTreeViewNodeByText('Add Condition').click()); - await (filterBuilder.getField(0, 'item').click().element); - await (FilterBuilder.getPopupTreeViewNodeByText('Order Date').click()); - await (filterBuilder.getField(0, 'itemOperation').click().element); - await (FilterBuilder.getPopupTreeViewNodeByText('Is any of').click()); - await (filterBuilder.getField(0, 'itemValue').click().element); - await (FilterBuilder.getPopupTreeViewNodeCheckboxByText('Weekends').click()); - await (new Popup(FilterBuilder.getPopupTreeView().click()).getOkButton().element); - await (filterBuilderPopup.asPopup().click().getOkButton().element); + await (FilterBuilder.getPopupTreeViewNodeByText('Add Condition')).click(); + await (filterBuilder.getField(0, 'item').element).click(); + await (FilterBuilder.getPopupTreeViewNodeByText('Order Date')).click(); + await (filterBuilder.getField(0, 'itemOperation').element).click(); + await (FilterBuilder.getPopupTreeViewNodeByText('Is any of')).click(); + await (filterBuilder.getField(0, 'itemValue').element).click(); + await (FilterBuilder.getPopupTreeViewNodeCheckboxByText('Weekends')).click(); + await (new Popup(FilterBuilder.getPopupTreeView()).getOkButton().element).click(); + await (filterBuilderPopup.asPopup().getOkButton().element).click(); expect(await dataGrid.getRows().count); await t.eql(3); @@ -131,9 +131,9 @@ test.describe('Filtering', () => { filterBuilderPopup = await filterPanel.openFilterBuilderPopup(t); filterBuilder = filterBuilderPopup.getFilterBuilder(); - await (filterBuilder.getField(0, 'itemOperation').click().element); - await (FilterBuilder.getPopupTreeViewNodeByText('Weekends').click()); - await (filterBuilderPopup.asPopup().click().getOkButton().element); + await (filterBuilder.getField(0, 'itemOperation').element).click(); + await (FilterBuilder.getPopupTreeViewNodeByText('Weekends')).click(); + await (filterBuilderPopup.asPopup().getOkButton().element).click(); expect(await dataGrid.getRows().count); await t.eql(3); @@ -143,7 +143,7 @@ test.describe('Filtering', () => { const dateFilterCell = page.locator('.dx-datagrid-filter-row td').nth(1); await (dateFilterCell.menuButton).click(); - await (dateFilterCell.menu.getItemByText('Between').click()); + await (dateFilterCell.menu.getItemByText('Between')).click(); expect(await dataGrid.getFilterRangeOverlay().exists).toBeTruthy(); await (dataGrid.getFilterRangeStartEditor().locator('input')).fill('2/1/2017'); await (dataGrid.getFilterRangeEndEditor().locator('input')).fill('2/28/2017'); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/filterPanel/visual.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/filterPanel/visual.spec.ts index d93a7219f33d..b29f81a32031 100644 --- a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/filterPanel/visual.spec.ts +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/filterPanel/visual.spec.ts @@ -30,11 +30,11 @@ test.describe('filterPanel', () => { ).getFilterBuilder(); await testScreenshot(page, 'dataGrid-filterPanel-popup-focused.png'); - await (filterBuilder.getField().click().getValueText()); + await (filterBuilder.getField().getValueText()).click(); await testScreenshot(page, 'dataGrid-filterPanel-popup.-with-editor-popup.png'); - await (filterBuilder.getField().click().getValueText()); + await (filterBuilder.getField().getValueText()).click(); await testScreenshot(page, 'dataGrid-filterPanel-popup.png'); - await (filterBuilder.getField().click().getValueText()); + await (filterBuilder.getField().getValueText()).click(); await testScreenshot(page, 'dataGrid-filterPanel-popup.-with-editor-popup.png'); }); }); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/filterRow/T1163100_changeFIlterIcon.visual_matrix.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/filterRow/T1163100_changeFIlterIcon.visual_matrix.spec.ts index 1da600b7bbbb..363bd5eef9a1 100644 --- a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/filterRow/T1163100_changeFIlterIcon.visual_matrix.spec.ts +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/filterRow/T1163100_changeFIlterIcon.visual_matrix.spec.ts @@ -57,7 +57,7 @@ test.describe('Header Filter T1163100 change filter icon', () => { for (let columnIdx = 0; columnIdx < 4; columnIdx += 1) { const filterCell = page.locator('.dx-datagrid-filter-row td').nth(columnIdx); await (filterCell.menuButton).click(); - await (filterCell.menu.getItemByText('Starts with').click()); + await (filterCell.menu.getItemByText('Starts with')).click(); } await testScreenshot(page, diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/grouping/T1162057_oneGroupOnDifferentPages.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/grouping/T1162057_oneGroupOnDifferentPages.spec.ts index 88b2f6a40390..c4e78379f67c 100644 --- a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/grouping/T1162057_oneGroupOnDifferentPages.spec.ts +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/grouping/T1162057_oneGroupOnDifferentPages.spec.ts @@ -114,16 +114,16 @@ test.describe('Grouping Panel - One group on different pages', () => { }, })); - await (page.locator('.dx-group-row').click().nth(0).getCell(0).element); + await (page.locator('.dx-group-row').nth(0).getCell(0).element).click(); await testScreenshot(page, 'group-panel_loaded_first-page.png'); - await (page.locator('.dx-pager').click().locator('.dx-page').filter({hasText: '2'}).element); + await (page.locator('.dx-pager').locator('.dx-page').filter({hasText: '2'}).element).click(); await testScreenshot(page, 'group-panel_loaded_second-page.png'); - await (page.locator('.dx-pager').click().locator('.dx-page').filter({hasText: '1'}).element); + await (page.locator('.dx-pager').locator('.dx-page').filter({hasText: '1'}).element).click(); await testScreenshot(page, 'group-panel_restored_first-page.png'); - await (page.locator('.dx-pager').click().locator('.dx-page').filter({hasText: '2'}).element); + await (page.locator('.dx-pager').locator('.dx-page').filter({hasText: '2'}).element).click(); await testScreenshot(page, 'group-panel_restored_second-page.png'); }); // TODO: .after() block removed diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/grouping/calculateGroupValueRuntimeChanges.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/grouping/calculateGroupValueRuntimeChanges.spec.ts index 3cdb35a31b80..1cb0ca9307c2 100644 --- a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/grouping/calculateGroupValueRuntimeChanges.spec.ts +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/grouping/calculateGroupValueRuntimeChanges.spec.ts @@ -42,8 +42,8 @@ test.describe('Grouping API - calculateGroupValue runtime changes', () => { await t.eql(0); await (dataGrid - .getGroupRow(0).click() - .getExpandCell()); + .getGroupRow(0) + .getExpandCell()).click(); expect(await page.locator('.dx-group-row').nth(0).isExpanded); await t.ok(); @@ -83,8 +83,8 @@ test.describe('Grouping API - calculateGroupValue runtime changes', () => { await t.eql(0); await (dataGrid - .getGroupRow(0).click() - .getExpandCell()); + .getGroupRow(0) + .getExpandCell()).click(); expect(await page.locator('.dx-group-row').nth(0).isExpanded); await t.ok(); @@ -122,8 +122,8 @@ test.describe('Grouping API - calculateGroupValue runtime changes', () => { await t.eql(0); await (dataGrid - .getGroupRow(0).click() - .getExpandCell()); + .getGroupRow(0) + .getExpandCell()).click(); expect(await page.locator('.dx-group-row').nth(0).isExpanded); await t.ok(); @@ -172,8 +172,8 @@ test.describe('Grouping API - calculateGroupValue runtime changes', () => { await t.eql(0); await (dataGrid - .getGroupRow(0).click() - .getExpandCell()); + .getGroupRow(0) + .getExpandCell()).click(); expect(await page.locator('.dx-group-row').nth(0).isExpanded); await t.ok(); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/headerFilter/headerFilter.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/headerFilter/headerFilter.spec.ts index 8a5e4126caa0..f94c0ea7fc6e 100644 --- a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/headerFilter/headerFilter.spec.ts +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/headerFilter/headerFilter.spec.ts @@ -39,15 +39,15 @@ test.describe('Header Filter', () => { const list = headerFilter.getList(); await (filterIconElement).click(); - await (list.getItem(1).click().element) // Select second item with value 'Item 1'; - await (buttons.nth(0).click()); // Click OK; + await (list.getItem(1).element).click() // Select second item with value 'Item 1'; + await (buttons.nth(0)).click(); // Click OK; result[0] = await dataCell.element().innerText; await (filterIconElement).click(); - await (list.getItem(1).click().element) // Deselect second item with value 'Item 1'; - await (list.getItem(0).click().element) // Select second item with value '(Blanks)'; - await (buttons.nth(0).click()); // Click OK; + await (list.getItem(1).element).click() // Deselect second item with value 'Item 1'; + await (list.getItem(0).element).click() // Select second item with value '(Blanks)'; + await (buttons.nth(0)).click(); // Click OK; result[1] = await dataCell.element().innerText; diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/headerPanel.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/headerPanel.spec.ts index 10f2d6c19b7a..474907656638 100644 --- a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/headerPanel.spec.ts +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/headerPanel.spec.ts @@ -45,7 +45,7 @@ test.describe('Header Panel', () => { const headerPanel = page.locator('.dx-datagrid-header-panel'); // act - await (headerPanel.locator('.dx-dropdownmenu-button').click()); + await (headerPanel.locator('.dx-dropdownmenu-button')).click(); // assert const selectPopup = headerPanel.getDropDownSelectPopup(); @@ -57,7 +57,7 @@ test.describe('Header Panel', () => { await t.ok(); // act - await (selectPopup.editButton().click()); + await (selectPopup.editButton()).click(); // assert const menuItem = selectPopup.getSelectItem(1); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/keyboardNavigation/keyboardNavigation.visual.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/keyboardNavigation/keyboardNavigation.visual.spec.ts index 4519ed52918e..55daf9913fde 100644 --- a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/keyboardNavigation/keyboardNavigation.visual.spec.ts +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/keyboardNavigation/keyboardNavigation.visual.spec.ts @@ -32,7 +32,7 @@ test.describe('Keyboard Navigation.Visual', () => { await t.ok(); // act - await (page.locator('.dx-data-row').click().nth(0).locator('td').nth(0)); + await (page.locator('.dx-data-row').nth(0).locator('td').nth(0)).click(); await page.keyboard.press('end'); await testScreenshot(page, 'focus_last_cell_in_row_that_contains_focus_when_pressing_End_key.png', { element: page.locator('#container') }); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/keyboardNavigation/masterDetail/index.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/keyboardNavigation/masterDetail/index.spec.ts index 581d15709330..2d5f3dffac02 100644 --- a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/keyboardNavigation/masterDetail/index.spec.ts +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/keyboardNavigation/masterDetail/index.spec.ts @@ -21,13 +21,11 @@ test.describe('DataGrid Tests', () => { test('Focus goes inside master detail on tab', async ({ page }) => { await createWidget(page, 'dxDataGrid', gridOptions); - await (page.locator('.dx-data-row').click().nth(0).locator('.dx-command-edit').nth(0), - ); + await (page.locator('.dx-data-row').nth(0).locator('.dx-command-edit').nth(0),).click(); const innerDataGrid = new DataGrid(page.locator('.dx-master-detail-row').nth(0).element.find('.dx-datagrid').parent()); - await (page.locator('.dx-data-row').click().nth(0).locator('td').nth(4), - ) + await (page.locator('.dx-data-row').nth(0).locator('td').nth(4),).click() .pressKey('tab') .pressKey('tab'); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/keyboardNavigation/startEditing.functional.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/keyboardNavigation/startEditing.functional.spec.ts index 2f2c31bb3738..bd2ab3407dfa 100644 --- a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/keyboardNavigation/startEditing.functional.spec.ts +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/keyboardNavigation/startEditing.functional.spec.ts @@ -36,7 +36,7 @@ test.describe('Keyboard Navigation - editOnKeyPress', () => { await page.evaluate((opts) => ($('#container') as any).dxDataGrid('instance').getScrollable().scrollBy(opts), { y: 10000 }); - await (page.locator('.dx-data-row').click().nth(49).locator('td').nth(1)); + await (page.locator('.dx-data-row').nth(49).locator('td').nth(1)).click(); await page.keyboard.press('enter'); expect(await page.locator('.dx-data-row').nth(49).locator('td').nth(1).locator('.dx-editor-cell').focused).toBeTruthy(); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/keyboardNavigation/virtualColumns.functional.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/keyboardNavigation/virtualColumns.functional.spec.ts index 4e304ad86736..5c855777c87e 100644 --- a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/keyboardNavigation/virtualColumns.functional.spec.ts +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/keyboardNavigation/virtualColumns.functional.spec.ts @@ -54,7 +54,7 @@ test.describe('Virtual Columns.Functional', () => { await t.ok(); // act - await (page.locator('.dx-data-row').click().nth(0).locator('td').nth(19)); + await (page.locator('.dx-data-row').nth(0).locator('td').nth(19)).click(); // assert expect(await page.locator('.dx-data-row').nth(0).locator('td').nth(19).focused); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/pager.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/pager.spec.ts index 119efe76c4ba..2a3f9a1ca77a 100644 --- a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/pager.spec.ts +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/pager.spec.ts @@ -47,21 +47,21 @@ test.describe('Pager', () => { expect(await page.locator('.dx-data-row').nth(29).locator('td').nth(2).textContent()); await t.eql('29'); // set page sige to 10 - await (pager.locator('.dx-page-size').nth(1).click().element); + await (pager.locator('.dx-page-size').nth(1).element).click(); expect(await page.locator('.dx-data-row').nth(10 * 6 - 1).locator('td').nth(2).textContent()); await t.eql('59'); // set page index 7 - await (pager.locator('.dx-page').filter({hasText: '7'}).click().element); + await (pager.locator('.dx-page').filter({hasText: '7'}).element).click(); expect(await page.locator('.dx-data-row').nth(10 * 7 - 1).locator('td').nth(2).textContent()); await t.eql('69'); expect(await pager.locator('.dx-info').textContent); await t.eql('Page 7 of 10 (100 items)'); // navigate to prev page (6) - await (pager.locator('.dx-navigate-button.dx-prev-button').click().element); + await (pager.locator('.dx-navigate-button.dx-prev-button').element).click(); expect(await pager.locator('.dx-info').textContent); await t.eql('Page 6 of 10 (100 items)'); // navigate to next page (7) - await (pager.locator('.dx-navigate-button.dx-next-button').click().element); + await (pager.locator('.dx-navigate-button.dx-next-button').element).click(); expect(await pager.locator('.dx-info').textContent); await t.eql('Page 7 of 10 (100 items)'); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/searchPanel.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/searchPanel.spec.ts index 33bc46d8c284..9dcc4e8117cf 100644 --- a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/searchPanel.spec.ts +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/searchPanel.spec.ts @@ -39,7 +39,7 @@ test.describe('Search Panel', () => { }); // act - await (page.locator('.dx-data-row').click().nth(0).locator('.dx-command-edit').nth(0)); + await (page.locator('.dx-data-row').nth(0).locator('.dx-command-edit').nth(0)).click(); const masterRow = page.locator('.dx-master-detail-row').nth(0); const masterGrid = masterRow.getDataGrid(); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/security/xss.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/security/xss.spec.ts index 6a2dbec0a657..61a7ee623995 100644 --- a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/security/xss.spec.ts +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/security/xss.spec.ts @@ -32,7 +32,7 @@ test.describe('XSS', () => { await (group.element).click(); expect(await FilterBuilder.getPopupTreeView().visible).toBeTruthy(); - await (FilterBuilder.getPopupTreeViewNode().click()); + await (FilterBuilder.getPopupTreeViewNode()).click(); expect(await true); await t.ok(); }); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/summary.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/summary.spec.ts index 475e67fcce70..9b12c43654e1 100644 --- a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/summary.spec.ts +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/summary.spec.ts @@ -39,7 +39,7 @@ test.describe('Summary', () => { }, }); - await (page.locator('.dx-data-row').click().nth(4).locator('td').nth(1)); + await (page.locator('.dx-data-row').nth(4).locator('td').nth(1)).click(); await page.keyboard.press('tab'); await testScreenshot(page, 'group-summary-focused.png', { element: page.locator('#container') }); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/tagBox.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/tagBox.spec.ts index a79875bd06d7..d4580b0a5470 100644 --- a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/tagBox.spec.ts +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/tagBox.spec.ts @@ -52,7 +52,7 @@ test.describe('Tagbox Columns', () => { editing: { mode: 'batch', allowUpdating: true }, }); - await (page.locator('.dx-data-row').click().nth(0).locator('td').nth(1)); + await (page.locator('.dx-data-row').nth(0).locator('td').nth(1)).click(); await testScreenshot(page, 'T1228720-grid-tagbox-on-edit.png', { element: page.locator('#container') }); }); }); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/validation/cellEditing.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/validation/cellEditing.spec.ts index adf2c0a014cf..bfc8f01ecb66 100644 --- a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/validation/cellEditing.spec.ts +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/validation/cellEditing.spec.ts @@ -48,7 +48,7 @@ test.describe('Validation', () => { }], }); - await (grid.locator('td').nth(0, 0).click().element); + await (grid.locator('td').nth(0, 0)).click(); const editor = grid.locator('td').nth(0, 0).locator('.dx-editor-cell'); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/validation/validationPopup.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/validation/validationPopup.spec.ts index b004dacb1db4..8571935cac4a 100644 --- a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/validation/validationPopup.spec.ts +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/validation/validationPopup.spec.ts @@ -38,7 +38,7 @@ test.describe('Validation', () => { }); await t.maximizeWindow(); - await (page.locator('.dx-data-row').click().nth(0).locator('td').nth(0)); + await (page.locator('.dx-data-row').nth(0).locator('td').nth(0)).click(); await page.keyboard.press('ctrl+a backspace enter'); // act diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/virtualColumns/functional.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/virtualColumns/functional.spec.ts index 7009e5a61802..99b0f8930825 100644 --- a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/virtualColumns/functional.spec.ts +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/virtualColumns/functional.spec.ts @@ -39,7 +39,7 @@ test.describe('Virtual Columns.Functional', () => { }, }); - await (page.locator('.dx-data-row').click().nth(0).locator('td').nth(0)); + await (page.locator('.dx-data-row').nth(0).locator('td').nth(0)).click(); expect(await page.locator('.dx-data-row').nth(0).locator('td').nth(0).focused); await t.ok(); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/appearance.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/appearance.spec.ts index 263503a11312..ccbf239064c4 100644 --- a/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/appearance.spec.ts +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/appearance.spec.ts @@ -62,9 +62,9 @@ test.describe('FixedColumns - appearance', () => { await testScreenshot(page, `datagrid_default_state_with_${showRowLinesState}.png`, { element: page.locator('#container') }); - await (page.locator('.dx-data-row').click().nth(2).locator('.dx-command-edit').nth(41).locator('.dx-link').nth(0)); - await (page.locator('.dx-data-row').click().nth(3).locator('.dx-command-edit').nth(0)); - await (page.locator('.dx-data-row').click().nth(4).locator('td').nth(4)); + await (page.locator('.dx-data-row').nth(2).locator('.dx-command-edit').nth(41).locator('.dx-link').nth(0)).click(); + await (page.locator('.dx-data-row').nth(3).locator('.dx-command-edit').nth(0)).click(); + await (page.locator('.dx-data-row').nth(4).locator('td').nth(4)).click(); await testScreenshot(page, `datagrid_selected_focused_edit_state_with_${showRowLinesState}.png`, { element: page.locator('#container') }); }); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/columnFixingIcons.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/columnFixingIcons.spec.ts index f4a3d740e6e0..ec8b74ea931e 100644 --- a/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/columnFixingIcons.spec.ts +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/columnFixingIcons.spec.ts @@ -29,7 +29,7 @@ test.describe('Column Fixing', () => { expect(await page.locator('.dx-datagrid').first().isVisible()).toBeTruthy(); await t.rightClick(page.locator('.dx-header-row').nth(0).element); - await (dataGrid.getContextMenu().click().getItemByText('Set Fixed Position')); + await (dataGrid.getContextMenu().getItemByText('Set Fixed Position')).click(); await testScreenshot(page, 'sticky_columns_context_menu.png'); }); }); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/focusOverlay.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/focusOverlay.spec.ts index 404258b67729..1ce7b3ee98c2 100644 --- a/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/focusOverlay.spec.ts +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/focusOverlay.spec.ts @@ -98,17 +98,17 @@ test.describe('FixedColumns - Focus Overlay', () => { expect(await page.locator('.dx-datagrid').first().isVisible()).toBeTruthy(); - await (page.locator('.dx-group-row').click().nth(0).getCell(1).element); + await (page.locator('.dx-group-row').nth(0).getCell(1).element).click(); await page.keyboard.press('tab'); await testScreenshot(page, 'datagrid_group_row_focused.png', { element: page.locator('#container') }); - await (page.locator('.dx-data-row').click().nth(2).locator('.dx-command-edit').nth(40).getAdaptiveButton()); + await (page.locator('.dx-data-row').nth(2).locator('.dx-command-edit').nth(40).getAdaptiveButton()).click(); await page.keyboard.press('tab'); await testScreenshot(page, 'datagrid_adaptive_item_focused.png', { element: page.locator('#container') }); - await (dataGrid.getGroupFooterRow().click().nth(0), { offsetX: 5, offsetY: 5 }); + await (dataGrid.getGroupFooterRow().nth(0), { offsetX: 5, offsetY: 5 }).click(); await page.keyboard.press('tab'); await testScreenshot(page, 'datagrid_group_footer_row_focused.png', { element: page.locator('#container') }); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/withEditing.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/withEditing.spec.ts index 7e715b1d373f..3539ee8188c3 100644 --- a/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/withEditing.spec.ts +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/withEditing.spec.ts @@ -28,7 +28,7 @@ test.describe('Sticky columns - Editing', () => { expect(await page.locator('.dx-datagrid').first().isVisible()).toBeTruthy(); await dataGrid.apiEditRow(1); - await (page.locator('.dx-data-row').click().nth(1).locator('td').nth(1)); + await (page.locator('.dx-data-row').nth(1).locator('td').nth(1)).click(); await testScreenshot(page, 'edit_row_with_sticky_columns_1.png', { element: page.locator('#container') }); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/withFilterRow.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/withFilterRow.spec.ts index 5f1e33ae699e..507cdaa02453 100644 --- a/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/withFilterRow.spec.ts +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/withFilterRow.spec.ts @@ -27,7 +27,7 @@ test.describe('Sticky columns - Filter row', () => { expect(await page.locator('.dx-datagrid').first().isVisible()).toBeTruthy(); - await (page.locator('.dx-header-row').click().getFilterRow().getFilterCell(1).element); + await (page.locator('.dx-header-row').getFilterRow().getFilterCell(1).element).click(); await testScreenshot(page, 'filter_row_with_sticky_columns_1.png', { element: page.locator('#container') }); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/withKeyboardNavigation.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/withKeyboardNavigation.spec.ts index f9ca72140b94..81b22b3db63d 100644 --- a/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/withKeyboardNavigation.spec.ts +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/withKeyboardNavigation.spec.ts @@ -56,7 +56,7 @@ test.describe('Fixed Columns - keyboard navigation', () => { expect(await page.locator('.dx-datagrid').first().isVisible()).toBeTruthy(); // act - await (headerRow.locator('td').nth(0).click().element); + await (headerRow.locator('td').nth(0).element).click(); // assert expect(await headerRow.locator('td').nth(0).isFocused); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/agenda/keyField.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/agenda/keyField.spec.ts index 2638f5bb4b32..4d1fad5ffcbf 100644 --- a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/agenda/keyField.spec.ts +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/agenda/keyField.spec.ts @@ -1,164 +1,63 @@ import { test, expect } from '@playwright/test'; -import { createWidget } from '../../../../playwright-helpers'; -import path from 'path'; +import { createWidget, getContainerUrl, setupTestPage } from '../../../../playwright-helpers'; -const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; +const containerUrl = getContainerUrl(__dirname, '../../../../tests/container.html'); + +const hasWarningCode = (message: string) => message.startsWith('W1023'); test.describe('Agenda:KeyField', () => { test.beforeEach(async ({ page }) => { - await page.goto(containerUrl); - await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); - await page.evaluate((theme) => new Promise((resolve) => { - (window as any).DevExpress.ui.themes.ready(resolve); - (window as any).DevExpress.ui.themes.current(theme); - }), process.env.THEME || 'fluent.blue.light'); - }); - -); - -const hasWarningCode = (message) => message.startsWith('W1023'); - -['week', 'agenda'].forEach((currentView) => { - test(`Warning should be thrown in console in case currentView='${currentView}'(T1100758)`, async ({ page }) => { - const messages = await t.getBrowserConsoleMessages(); - - const isWarningExist = !!messages.warn.find(hasWarningCode); - expect(isWarningExist).toBeTruthy(); - }); - -// TODO: .before() block not converted - move to test setup -// { - await createWidget(page, 'dxScheduler', { - dataSource: [], - views: ['week', 'agenda'], - currentView, - currentDate: new Date(2021, 2, 28), - height: 600, + await setupTestPage(page, containerUrl); + }); + + ['week', 'agenda'].forEach((currentView) => { + test(`Warning should be thrown in console in case currentView='${currentView}'(T1100758)`, async ({ page }) => { + const consoleMessages: string[] = []; + page.on('console', (msg) => { + if (msg.type() === 'warning') { + consoleMessages.push(msg.text()); + } + }); + + await createWidget(page, 'dxScheduler', { + dataSource: [], + views: ['week', 'agenda'], + currentView, + currentDate: new Date(2021, 2, 28), + height: 600, + }); + + const isWarningExist = consoleMessages.some(hasWarningCode); + expect(isWarningExist).toBeTruthy(); }); }); -}); - -test('Warning should be thrown in console after set new views(T1100758)', async ({ page }) => { - const messages = await t.getBrowserConsoleMessages(); - const isWarningExist = !!messages.warn.find(hasWarningCode); - expect(isWarningExist).toBeFalsy(); - // Scheduler on '#container' - await scheduler.option('views', ['week', 'agenda']); - - const messagesAfterChangeViews = await t.getBrowserConsoleMessages(); - const isWarningExistAfterChangeViews = !!messagesAfterChangeViews.warn.find(hasWarningCode); - expect(isWarningExistAfterChangeViews).toBeTruthy(); -}); - -// TODO: .before() block not converted - move to test setup -// { - await createWidget(page, 'dxScheduler', { - dataSource: [], - views: ['week'], - currentView: 'week', - currentDate: new Date(2021, 2, 28), - height: 600, - }); -}); - -test('Warning shouldn\'t be thrown in console in case currentView=\'week\' if keyField exists(T1100758)', async ({ page }) => { - const messages = await t.getBrowserConsoleMessages(); - - const isWarningExist = !!messages.warn.find(hasWarningCode); - expect(isWarningExist).toBeFalsy(); -}); - -// TODO: .before() block not converted - move to test setup -// { - await createWidget(page, 'dxScheduler', () => { - const store = new (window as any).DevExpress.data.CustomStore({ - key: 'id', - load: () => [], - }); - - return { - dataSource: store, - views: ['week', 'agenda'], - currentView: 'week', - currentDate: new Date(2021, 2, 28), - height: 600, - }; - }); -}); - -test('Warning shouldn\'t be thrown in console in case currentView=\'agenda\' if keyField exists(T1100758)', async ({ page }) => { - const messages = await t.getBrowserConsoleMessages(); - - const isWarningExist = !!messages.warn.find(hasWarningCode); - expect(isWarningExist).toBeFalsy(); -}); - -// TODO: .before() block not converted - move to test setup -// { - await createWidget(page, 'dxScheduler', () => { - const store = new (window as any).DevExpress.data.CustomStore({ - key: 'id', - load: () => [], - }); - - return { - dataSource: store, + test('Wrong behavior: editing recurrence appointment does not affect to appointment data source(T1100758)', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource: [{ + text: 'Test', + startDate: new Date('2021-03-29T16:30:00.000Z'), + endDate: new Date('2021-03-29T18:30:00.000Z'), + recurrenceRule: 'FREQ=WEEKLY', + }], views: ['agenda'], currentView: 'agenda', currentDate: new Date(2021, 2, 28), + recurrenceEditMode: 'series', height: 600, - }; - }); -}); - -['week', 'agenda'].forEach((currentView) => { - test(`Warning should be thrown in console in case currentView='${currentView}' if keyField not set in Store(T1100758)`, async ({ page }) => { - const messages = await t.getBrowserConsoleMessages(); - - const isWarningExist = !!messages.warn.find(hasWarningCode); - expect(isWarningExist).toBeTruthy(); - }); - -// TODO: .before() block not converted - move to test setup -// { - await createWidget(page, 'dxScheduler', ClientFunction(() => ({ - dataSource: new (window as any).DevExpress.data.CustomStore({ - load: () => [], - }), - views: ['week', 'agenda'], - currentView, - currentDate: new Date(2021, 2, 28), - height: 600, - }), { dependencies: { currentView } })); - }); -}); + }); -test('Wrong behavior: editing recurrence appointment does not affect to appointment\'s data source(T1100758)', async ({ page }) => { - // Scheduler on '#container' + const appointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'Test' }); + await appointment.dblclick(); - await (page.locator('.dx-scheduler-appointment').filter({ hasText: 'Test' }).dblclick().element); - await t - .typeText(scheduler.appointmentPopup.textEditor.element, 'Updated', { replace: true }) - .click(scheduler.appointmentPopup.saveButton.element); + const popup = page.locator('.dx-scheduler-appointment-popup'); + const subjectInput = popup.locator('.dx-texteditor-input').first(); + await subjectInput.fill('Updated'); - expect(page.locator('.dx-scheduler-appointment').filter({ hasText: 'Updated' }).element.exists).toBeTruthy(); -}); + const doneButton = popup.locator('.dx-popup-done.dx-button'); + await doneButton.click(); -// TODO: .before() block not converted - move to test setup -// { - await createWidget(page, 'dxScheduler', { - dataSource: [{ - text: 'Test', - startDate: new Date('2021-03-29T16:30:00.000Z'), - endDate: new Date('2021-03-29T18:30:00.000Z'), - recurrenceRule: 'FREQ=WEEKLY', - }], - views: ['agenda'], - currentView: 'agenda', - currentDate: new Date(2021, 2, 28), - recurrenceEditMode: 'series', - height: 600, - }, '#container'); -}); + const updatedAppointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'Updated' }); + await expect(updatedAppointment.first()).toBeVisible(); + }); }); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/agenda/layout.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/agenda/layout.spec.ts index 824265275e72..9870b190a1fe 100644 --- a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/agenda/layout.spec.ts +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/agenda/layout.spec.ts @@ -1,139 +1,25 @@ -import { test, expect } from '@playwright/test'; -import { createWidget, testScreenshot } from '../../../../playwright-helpers'; -import path from 'path'; - -const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; - -test.describe('Agenda:layout', () => { - test.beforeEach(async ({ page }) => { - await page.goto(containerUrl); - await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); - await page.evaluate((theme) => new Promise((resolve) => { - (window as any).DevExpress.ui.themes.ready(resolve); - (window as any).DevExpress.ui.themes.current(theme); - }), process.env.THEME || 'fluent.blue.light'); - }); - -); - -const data = [{ - text: 'Website Re-Design Plan', - ownerId: [4, 1, 2], - roomId: [1, 2, 3], - priorityId: 2, - startDate: new Date('2021-05-24T16:30:00.000Z'), - endDate: new Date('2021-05-24T18:30:00.000Z'), - recurrenceRule: 'FREQ=WEEKLY', - allDay: true, -}, { - text: 'Book Flights to San Fran for Sales Trip', - ownerId: 2, - roomId: 2, - priorityId: 1, - startDate: new Date('2021-05-24T19:00:00.000Z'), - endDate: new Date('2021-05-24T20:00:00.000Z'), - allDay: true, -}, { - text: 'Final Budget Review', - ownerId: 1, - roomId: 1, - priorityId: 1, - startDate: new Date('2021-05-25T19:00:00.000Z'), - endDate: new Date('2021-05-25T20:35:00.000Z'), -}, { - text: 'New Brochures', - ownerId: 4, - roomId: 3, - priorityId: 2, - startDate: new Date('2021-05-25T21:30:00.000Z'), - endDate: new Date('2021-05-25T22:45:00.000Z'), -}, { - text: 'Install New Database', - ownerId: 2, - roomId: 3, - priorityId: 1, - startDate: new Date('2021-05-26T16:45:00.000Z'), - endDate: new Date('2021-05-26T18:15:00.000Z'), -}, { - text: 'Approve New Online Marketing Strategy', - ownerId: 4, - roomId: 2, - priorityId: 1, - startDate: new Date('2021-05-26T19:00:00.000Z'), - endDate: new Date('2021-05-26T21:00:00.000Z'), -}, { - text: 'Upgrade Personal Computers', - ownerId: 2, - roomId: 2, - priorityId: 2, - startDate: new Date('2021-05-26T22:15:00.000Z'), - endDate: new Date('2021-05-26T23:30:00.000Z'), -}]; - -const owners = [{ - text: 'Samantha Bright', - id: 1, - color: '#727bd2', -}, { - text: 'John Heart', - id: 2, - color: '#32c9ed', -}, { - text: 'Todd Hoffman', - id: 3, - color: '#2a7ee4', -}, { - text: 'Sandra Johnson', - id: 4, - color: '#7b49d3', -}]; - -const rooms = [{ - text: 'Room 1', - id: 1, - color: '#00af2c', -}, { - text: 'Room 2', - id: 2, - color: '#56ca85', -}, { - text: 'Room 3', - id: 3, - color: '#8ecd3c', -}]; - -const priorities = [{ - text: 'High priority', - id: 1, - color: '#cc5c53', -}, { - text: 'Low priority', - id: 2, - color: '#ff9747', -}]; - -const resourcesData = [{ - fieldExpr: 'roomId', - allowMultiple: true, - dataSource: rooms, - label: 'Room', -}, { - fieldExpr: 'priorityId', - allowMultiple: true, - dataSource: priorities, - label: 'Priority', -}, { - fieldExpr: 'ownerId', - allowMultiple: true, - dataSource: owners, - label: 'Owner', -}]; - -const createScheduler = async ( - rtlEnabled: boolean, - resources: undefined | any[], - groups: undefined | string[], -): Promise => { +import { test } from '@playwright/test'; +import { createWidget, testScreenshot, getContainerUrl, setupTestPage } from '../../../../playwright-helpers'; + +const containerUrl = getContainerUrl(__dirname, '../../../../tests/container.html'); + +const data = [ + { text: 'Website Re-Design Plan', ownerId: [4, 1, 2], roomId: [1, 2, 3], priorityId: 2, startDate: new Date('2021-05-24T16:30:00.000Z'), endDate: new Date('2021-05-24T18:30:00.000Z'), recurrenceRule: 'FREQ=WEEKLY', allDay: true }, + { text: 'Book Flights to San Fran for Sales Trip', ownerId: 2, roomId: 2, priorityId: 1, startDate: new Date('2021-05-24T19:00:00.000Z'), endDate: new Date('2021-05-24T20:00:00.000Z'), allDay: true }, + { text: 'Final Budget Review', ownerId: 1, roomId: 1, priorityId: 1, startDate: new Date('2021-05-25T19:00:00.000Z'), endDate: new Date('2021-05-25T20:35:00.000Z') }, + { text: 'New Brochures', ownerId: 4, roomId: 3, priorityId: 2, startDate: new Date('2021-05-25T21:30:00.000Z'), endDate: new Date('2021-05-25T22:45:00.000Z') }, + { text: 'Install New Database', ownerId: 2, roomId: 3, priorityId: 1, startDate: new Date('2021-05-26T16:45:00.000Z'), endDate: new Date('2021-05-26T18:15:00.000Z') }, + { text: 'Approve New Online Marketing Strategy', ownerId: 4, roomId: 2, priorityId: 1, startDate: new Date('2021-05-26T19:00:00.000Z'), endDate: new Date('2021-05-26T21:00:00.000Z') }, + { text: 'Upgrade Personal Computers', ownerId: 2, roomId: 2, priorityId: 2, startDate: new Date('2021-05-26T22:15:00.000Z'), endDate: new Date('2021-05-26T23:30:00.000Z') }, +]; + +const resourcesData = [ + { fieldExpr: 'roomId', allowMultiple: true, dataSource: [{ text: 'Room 1', id: 1, color: '#00af2c' }, { text: 'Room 2', id: 2, color: '#56ca85' }, { text: 'Room 3', id: 3, color: '#8ecd3c' }], label: 'Room' }, + { fieldExpr: 'priorityId', allowMultiple: true, dataSource: [{ text: 'High priority', id: 1, color: '#cc5c53' }, { text: 'Low priority', id: 2, color: '#ff9747' }], label: 'Priority' }, + { fieldExpr: 'ownerId', allowMultiple: true, dataSource: [{ text: 'Samantha Bright', id: 1, color: '#727bd2' }, { text: 'John Heart', id: 2, color: '#32c9ed' }, { text: 'Todd Hoffman', id: 3, color: '#2a7ee4' }, { text: 'Sandra Johnson', id: 4, color: '#7b49d3' }], label: 'Owner' }, +]; + +const createScheduler = async (page, rtlEnabled: boolean, resources: any[] | undefined, groups: string[] | undefined): Promise => { await createWidget(page, 'dxScheduler', { dataSource: data, views: ['agenda'], @@ -146,38 +32,36 @@ const createScheduler = async ( }); }; -[false, true].forEach((rtlEnabled) => { - [undefined, resourcesData].forEach((resources) => { - test(`Agenda test layout(rtl=${rtlEnabled}, resources=${!!resources}`, async (t) => { - await testScreenshot(page, - `agenda-layout-rtl=${rtlEnabled}-resources=${!!resources}.png`, - ); - - expect(compareResults.isValid()) - .ok(compareResults.errorMessages()); - }) - .before(async () => createScheduler(rtlEnabled, resources, undefined)); +test.describe('Agenda:layout', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); }); -}); -[false, true].forEach((rtlEnabled) => { - test(`Agenda test layout with groups(rtl=${rtlEnabled}`, async ({ page }) => { - await testScreenshot(page, `agenda-layout-groups-rtl=${rtlEnabled}.png`); + [false, true].forEach((rtlEnabled) => { + [undefined, resourcesData].forEach((resources) => { + test(`Agenda test layout(rtl=${rtlEnabled}, resources=${!!resources})`, async ({ page }) => { + await createScheduler(page, rtlEnabled, resources, undefined); + await testScreenshot(page, `agenda-layout-rtl=${rtlEnabled}-resources=${!!resources}.png`); + }); + }); + }); - expect(compareResults.isValid()) - .ok(compareResults.errorMessages()); - }).before(async () => createScheduler(rtlEnabled, resourcesData, ['roomId'])); -}); + [false, true].forEach((rtlEnabled) => { + test(`Agenda test layout with groups(rtl=${rtlEnabled})`, async ({ page }) => { + await createScheduler(page, rtlEnabled, resourcesData, ['roomId']); + await testScreenshot(page, `agenda-layout-groups-rtl=${rtlEnabled}.png`); + }); + }); -test('Agenda test appointment state', async ({ page }) => { - // Scheduler on '#container' - await (page.locator('.dx-scheduler-appointment').filter({ hasText: 'Final Budget Review' }).hover().element); - await testScreenshot(page, 'agenda-layout-appointment-state-hover.png'); + test('Agenda test appointment state', async ({ page }) => { + await createScheduler(page, false, resourcesData, undefined); - await (page.locator('.dx-scheduler-appointment').filter({ hasText: 'New Brochures' }).click().element); - await testScreenshot(page, 'agenda-layout-appointment-state-click.png'); + const finalBudget = page.locator('.dx-scheduler-appointment').filter({ hasText: 'Final Budget Review' }).first(); + await finalBudget.hover(); + await testScreenshot(page, 'agenda-layout-appointment-state-hover.png'); - expect(compareResults.isValid()) - .ok(compareResults.errorMessages()); -}).before(async () => createScheduler(false, resourcesData, undefined)); + const newBrochures = page.locator('.dx-scheduler-appointment').filter({ hasText: 'New Brochures' }).first(); + await newBrochures.click(); + await testScreenshot(page, 'agenda-layout-appointment-state-click.png'); + }); }); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/agenda/switchingToAgenda.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/agenda/switchingToAgenda.spec.ts index 5a574de218ff..b533a545381e 100644 --- a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/agenda/switchingToAgenda.spec.ts +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/agenda/switchingToAgenda.spec.ts @@ -1,42 +1,32 @@ -import { test, expect } from '@playwright/test'; -import { createWidget, testScreenshot } from '../../../../playwright-helpers'; -import path from 'path'; +import { test } from '@playwright/test'; +import { createWidget, testScreenshot, getContainerUrl, setupTestPage } from '../../../../playwright-helpers'; -const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; +const containerUrl = getContainerUrl(__dirname, '../../../../tests/container.html'); test.describe('Agenda:view switching', () => { test.beforeEach(async ({ page }) => { - await page.goto(containerUrl); - await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); - await page.evaluate((theme) => new Promise((resolve) => { - (window as any).DevExpress.ui.themes.ready(resolve); - (window as any).DevExpress.ui.themes.current(theme); - }), process.env.THEME || 'fluent.blue.light'); + await setupTestPage(page, containerUrl); }); -); + test('View switching should work for empty agenda', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource: [{ + startDate: new Date(2021, 4, 25, 0), + endDate: new Date(2021, 4, 25, 1), + text: 'Test Appointment', + }], + views: ['day', 'agenda'], + currentView: 'day', + currentDate: new Date(2021, 4, 25), + height: 600, + }); -test('View switching should work for empty agenda', async ({ page }) => { - // --- setup --- -await createWidget(page, 'dxScheduler', { - dataSource: [{ - startDate: new Date(2021, 4, 25, 0), - endDate: new Date(2021, 4, 25, 1), - text: 'Test Appointment', - }], - views: ['day', 'agenda'], - currentView: 'day', - currentDate: new Date(2021, 4, 25), - height: 600, - // --- test --- -// Scheduler on '#container' - await scheduler.option('currentDate', new Date(2021, 4, 26)); - await scheduler.option('currentView', 'agenda'); + await page.evaluate(() => { + const instance = ($('#container') as any).dxScheduler('instance'); + instance.option('currentDate', new Date(2021, 4, 26)); + instance.option('currentView', 'agenda'); + }); - await testScreenshot(page, 'switch-to-agenda-without-appointments.png'); - - expect(compareResults.isValid()) - .ok(compareResults.errorMessages()); -}); -}); + await testScreenshot(page, 'switch-to-agenda-without-appointments.png'); + }); }); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/agenda/tooltip.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/agenda/tooltip.spec.ts index 1631f3ccf1c3..0ce426801ea9 100644 --- a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/agenda/tooltip.spec.ts +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/agenda/tooltip.spec.ts @@ -1,51 +1,42 @@ import { test, expect } from '@playwright/test'; -import { createWidget } from '../../../../playwright-helpers'; -import path from 'path'; +import { createWidget, getContainerUrl, setupTestPage } from '../../../../playwright-helpers'; -const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; +const containerUrl = getContainerUrl(__dirname, '../../../../tests/container.html'); test.describe('Agenda:Tooltip', () => { test.beforeEach(async ({ page }) => { - await page.goto(containerUrl); - await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); - await page.evaluate((theme) => new Promise((resolve) => { - (window as any).DevExpress.ui.themes.ready(resolve); - (window as any).DevExpress.ui.themes.current(theme); - }), process.env.THEME || 'fluent.blue.light'); + await setupTestPage(page, containerUrl); }); -); - -test('Tooltip\'s date should be equal to date of current appointment(T1037028)', async ({ page }) => { - // Scheduler on '#container' - const appointmentName = 'Text'; - - for (let index = 0; index < 5; index += 1) { - await scheduler.hideAppointmentTooltip(); - - await (scheduler.getAppointment(appointmentName, index).click().element); - - const tooltipDate = await scheduler.appointmentTooltip - .getListItem(appointmentName, 0).date.innerText; - const expectedDate = await scheduler.getAppointment(appointmentName, index).date.time; - - expect(tooltipDate).toBe(expectedDate); - } -}); - -// TODO: .before() block not converted - move to test setup -// { - await createWidget(page, 'dxScheduler', { - dataSource: [{ - text: 'Text', - startDate: new Date(2021, 1, 1, 12), - endDate: new Date(2021, 1, 1, 13), - recurrenceRule: 'FREQ=HOURLY;COUNT=5', - }], - views: ['agenda'], - currentView: 'agenda', - currentDate: new Date(2021, 1, 1), - height: 600, + test('Tooltip date should be equal to date of current appointment(T1037028)', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource: [{ + text: 'Text', + startDate: new Date(2021, 1, 1, 12), + endDate: new Date(2021, 1, 1, 13), + recurrenceRule: 'FREQ=HOURLY;COUNT=5', + }], + views: ['agenda'], + currentView: 'agenda', + currentDate: new Date(2021, 1, 1), + height: 600, + }); + + const appointmentName = 'Text'; + + for (let index = 0; index < 5; index += 1) { + await page.evaluate(() => { + const instance = ($('#container') as any).dxScheduler('instance'); + instance.hideAppointmentTooltip(); + }); + + const appointment = page.locator('.dx-scheduler-appointment').filter({ hasText: appointmentName }).nth(index); + await appointment.click(); + + const tooltipDate = await page.locator('.dx-tooltip-appointment-item-content-date').first().innerText(); + const appointmentTime = await appointment.locator('.dx-scheduler-appointment-content-date').textContent(); + + expect(tooltipDate).toBe(appointmentTime); + } }); }); -}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/basic.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/basic.spec.ts index ef196abb11dd..4a4bc637e4fa 100644 --- a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/basic.spec.ts +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/basic.spec.ts @@ -1,166 +1,225 @@ import { test, expect } from '@playwright/test'; -import { testScreenshot } from '../../../../playwright-helpers'; -import path from 'path'; - -const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; +import { createWidget, testScreenshot, getContainerUrl, setupTestPage } from '../../../../playwright-helpers'; + +const containerUrl = getContainerUrl(__dirname, '../../../../tests/container.html'); + +const dataSource = [ + { + text: 'Brochure Design Review', + startDate: new Date(2019, 3, 1, 9, 0), + endDate: new Date(2019, 3, 1, 9, 30), + resourceId: 0, + }, + { + text: 'Update NDA Agreement', + startDate: new Date(2019, 3, 1, 9, 0), + endDate: new Date(2019, 3, 1, 10, 0), + resourceId: 1, + }, + { + text: 'Staff Productivity Report', + startDate: new Date(2019, 3, 1, 9, 0), + endDate: new Date(2019, 3, 1, 10, 30), + resourceId: 2, + }, +]; + +const defaultSchedulerOptions = { + views: ['day'], + dataSource: [], + resources: [ + { + fieldExpr: 'resourceId', + dataSource: [ + { id: 0, color: '#e01e38' }, + { id: 1, color: '#f98322' }, + { id: 2, color: '#1e65e8' }, + ], + label: 'Color', + }, + ], + width: 1666, + height: 833, + startDayHour: 9, + firstDayOfWeek: 1, + maxAppointmentsPerCell: 5, + currentView: 'day', + currentDate: new Date(2019, 3, 1), +}; test.describe('Drag-and-drop appointments in the Scheduler basic views', () => { test.beforeEach(async ({ page }) => { - await page.goto(containerUrl); - await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); - await page.evaluate((theme) => new Promise((resolve) => { - (window as any).DevExpress.ui.themes.ready(resolve); - (window as any).DevExpress.ui.themes.current(theme); - }), process.env.THEME || 'fluent.blue.light'); + await setupTestPage(page, containerUrl); }); -); - -['day', 'week', 'workWeek'].forEach((view) => test(`Drag-n-drop in the "${view}" view`, async ({ page }) => { - // --- setup --- -await createScheduler({ - timeZone: 'Etc/GMT', - dataSource: [{ - text: 'Test appointment', - startDate: new Date('2022-09-08T10:00:00.000Z'), - endDate: new Date('2022-09-08T10:30:00.000Z'), - }], - views: ['week'], - currentView: 'week', - currentDate: new Date('2022-09-09T10:00:00.000Z'), - startDayHour: 9, - width: 600, - height: 600, - // --- test --- -// Scheduler on '#container' - const draggableAppointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'Brochure Design Review' }); - - await t - .dragToElement(draggableAppointment.element, page.locator('.dx-scheduler-date-table-row').nth(4).locator('.dx-scheduler-date-table-cell').nth(0)) - .expect(draggableAppointment.size.height).toBe('38px') - .expect(draggableAppointment.date.time) - .eql('11:00 AM - 11:30 AM'); -}).before(async () => createScheduler({ - views: [view], - currentView: view, - dataSource, -}))); - -test('Drag-n-drop in the "month" view', async ({ page }) => { - // Scheduler on '#container' - const draggableAppointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'Brochure Design Review' }); - - await t - .dragToElement(draggableAppointment.element, page.locator('.dx-scheduler-date-table-row').nth(0).locator('.dx-scheduler-date-table-cell').nth(4)) - .expect(draggableAppointment.size.height).toBe('23.8281px') - .expect(draggableAppointment.date.time) - .eql('9:00 AM - 9:30 AM'); -}).before(async () => createScheduler({ - views: ['month'], - currentView: 'month', - dataSource, - height: 834, -})); - -test('Drag-n-drop when browser has horizontal scroll', async ({ page }) => { - // Scheduler on '#container' - const draggableAppointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'Staff Productivity Report' }); - - await t - .drag(draggableAppointment.element, 250, -50, { speed: 0.2 }) - .expect(draggableAppointment.isAllDay).toBe(true); -}).before(async () => createScheduler({ - views: ['week'], - currentView: 'week', - dataSource: [{ - text: 'Staff Productivity Report', - startDate: new Date(2019, 3, 6, 9, 0), - endDate: new Date(2019, 3, 6, 10, 30), - resourceId: 2, - }], - width: 1800, -})); - -test('Drag-n-drop when browser has vertical scroll', async ({ page }) => { - // Scheduler on '#container' - const draggableAppointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'Staff Productivity Report' }); - - await t - .dragToElement(draggableAppointment.element, page.locator('.dx-scheduler-date-table-row').nth(25).locator('.dx-scheduler-date-table-cell').nth(0), { speed: 0.5 }) - .expect(draggableAppointment.date.time).toBe('9:30 PM - 10:00 PM'); -}).before(async () => createScheduler({ - views: ['week'], - currentView: 'week', - dataSource: [{ - text: 'Staff Productivity Report', - startDate: new Date(2019, 3, 1, 21, 0), - endDate: new Date(2019, 3, 1, 21, 30), - resourceId: 2, - }], - height: 1800, -})); - -test('Drag recurrent appointment occurrence from collector (T832887)', async ({ page }) => { - // Scheduler on '#container' - const appointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'Recurrence two' }); - const collector = scheduler.collectors.find('2'); - const { appointmentTooltip } = scheduler; - const appointmentTooltipItem = appointmentTooltip.getListItem('Recurrence two'); - const popup = Scheduler.getDeleteRecurrenceDialog(); - - await (collector.element).click() - .expect(appointmentTooltip.isVisible()).toBeTruthy() - .dragToElement(appointmentTooltipItem.element, page.locator('.dx-scheduler-date-table-row').nth(2).locator('.dx-scheduler-date-table-cell').nth(2)) - .expect(appointmentTooltipItem.element.exists) - .notOk() - .click(popup.appointment) - .expect(appointment.element.exists) - .ok() - .expect(appointment.date.time) - .eql('4:00 AM - 6:00 AM') - .expect(collector.element.exists) - .notOk(); -}).before(async () => createScheduler({ - views: ['week'], - currentView: 'week', - firstDayOfWeek: 2, - startDayHour: 4, - maxAppointmentsPerCell: 1, - dataSource: [{ - text: 'Recurrence one', - startDate: new Date(2019, 2, 26, 8, 0), - endDate: new Date(2019, 2, 26, 10, 0), - recurrenceException: '', - recurrenceRule: 'FREQ=DAILY', - }, { - text: 'Non-recurrent appointment', - startDate: new Date(2019, 2, 26, 7, 0), - endDate: new Date(2019, 2, 26, 11, 0), - }, { - text: 'Recurrence two', - startDate: new Date(2019, 2, 26, 8, 0), - endDate: new Date(2019, 2, 26, 10, 0), - recurrenceException: '', - recurrenceRule: 'FREQ=DAILY', - }], - currentDate: new Date(2019, 2, 26), -})); - -test('Drag-n-drop the appointment to the left column to the cell that has the same time', async ({ page }) => { - // Scheduler on '#container' - const draggableAppointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'Test appointment' }); + ['day', 'week', 'workWeek'].forEach((view) => { + test(`Drag-n-drop in the "${view}" view`, async ({ page }) => { + await createWidget(page, 'dxScheduler', { + ...defaultSchedulerOptions, + views: [view], + currentView: view, + dataSource, + }); - await t - .dragToElement( - draggableAppointment.element, - page.locator('.dx-scheduler-date-table-row').nth(2).locator('.dx-scheduler-date-table-cell').nth(2), - { speed: 0.5 }, - ); + const draggableAppointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'Brochure Design Review' }); + const targetCell = page.locator('.dx-scheduler-date-table-row').nth(4).locator('.dx-scheduler-date-table-cell').nth(0); - await testScreenshot(page, 'drag-n-drop-appointment-to-left-column.png', { element: page.locator('.dx-scheduler-work-space') }); + await draggableAppointment.dragTo(targetCell); - expect(compareResults.isValid()) - .ok(compareResults.errorMessages()); -}); -}); + const height = await draggableAppointment.evaluate((el) => getComputedStyle(el).height); + expect(height).toBe('38px'); + + const timeText = await draggableAppointment.locator('.dx-scheduler-appointment-content-date').textContent(); + expect(timeText).toContain('11:00 AM - 11:30 AM'); + }); + }); + + test('Drag-n-drop in the "month" view', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + ...defaultSchedulerOptions, + views: ['month'], + currentView: 'month', + dataSource, + height: 834, + }); + + const draggableAppointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'Brochure Design Review' }); + const targetCell = page.locator('.dx-scheduler-date-table-row').nth(0).locator('.dx-scheduler-date-table-cell').nth(4); + + await draggableAppointment.dragTo(targetCell); + + const height = await draggableAppointment.evaluate((el) => getComputedStyle(el).height); + expect(height).toBe('23.8281px'); + + const timeText = await draggableAppointment.locator('.dx-scheduler-appointment-content-date').textContent(); + expect(timeText).toContain('9:00 AM - 9:30 AM'); + }); + + test('Drag-n-drop when browser has horizontal scroll', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + ...defaultSchedulerOptions, + views: ['week'], + currentView: 'week', + dataSource: [{ + text: 'Staff Productivity Report', + startDate: new Date(2019, 3, 6, 9, 0), + endDate: new Date(2019, 3, 6, 10, 30), + resourceId: 2, + }], + width: 1800, + }); + + const draggableAppointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'Staff Productivity Report' }); + const box = await draggableAppointment.boundingBox(); + + await draggableAppointment.hover(); + await page.mouse.down(); + await page.mouse.move(box!.x + box!.width / 2 + 250, box!.y + box!.height / 2 - 50, { steps: 10 }); + await page.mouse.up(); + + const isAllDay = await draggableAppointment.evaluate((el) => { + return el.closest('.dx-scheduler-all-day-appointments') !== null; + }); + expect(isAllDay).toBe(true); + }); + + test('Drag-n-drop when browser has vertical scroll', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + ...defaultSchedulerOptions, + views: ['week'], + currentView: 'week', + dataSource: [{ + text: 'Staff Productivity Report', + startDate: new Date(2019, 3, 1, 21, 0), + endDate: new Date(2019, 3, 1, 21, 30), + resourceId: 2, + }], + height: 1800, + }); + + const draggableAppointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'Staff Productivity Report' }); + const targetCell = page.locator('.dx-scheduler-date-table-row').nth(25).locator('.dx-scheduler-date-table-cell').nth(0); + + await draggableAppointment.dragTo(targetCell); + + const timeText = await draggableAppointment.locator('.dx-scheduler-appointment-content-date').textContent(); + expect(timeText).toContain('9:30 PM - 10:00 PM'); + }); + + test('Drag recurrent appointment occurrence from collector (T832887)', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + ...defaultSchedulerOptions, + views: ['week'], + currentView: 'week', + firstDayOfWeek: 2, + startDayHour: 4, + maxAppointmentsPerCell: 1, + dataSource: [{ + text: 'Recurrence one', + startDate: new Date(2019, 2, 26, 8, 0), + endDate: new Date(2019, 2, 26, 10, 0), + recurrenceException: '', + recurrenceRule: 'FREQ=DAILY', + }, { + text: 'Non-recurrent appointment', + startDate: new Date(2019, 2, 26, 7, 0), + endDate: new Date(2019, 2, 26, 11, 0), + }, { + text: 'Recurrence two', + startDate: new Date(2019, 2, 26, 8, 0), + endDate: new Date(2019, 2, 26, 10, 0), + recurrenceException: '', + recurrenceRule: 'FREQ=DAILY', + }], + currentDate: new Date(2019, 2, 26), + }); + + const collector = page.locator('.dx-scheduler-appointment-collector').filter({ hasText: '2' }); + const tooltipItem = page.locator('.dx-tooltip-appointment-item').filter({ hasText: 'Recurrence two' }); + const targetCell = page.locator('.dx-scheduler-date-table-row').nth(2).locator('.dx-scheduler-date-table-cell').nth(2); + const popup = page.locator('.dx-dialog'); + + await collector.click(); + await expect(page.locator('.dx-scheduler-appointment-tooltip-wrapper')).toBeVisible(); + + await tooltipItem.dragTo(targetCell); + await expect(tooltipItem).not.toBeVisible(); + + await popup.locator('.dx-dialog-button').first().click(); + + const appointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'Recurrence two' }); + await expect(appointment).toBeVisible(); + + const timeText = await appointment.locator('.dx-scheduler-appointment-content-date').textContent(); + expect(timeText).toContain('4:00 AM - 6:00 AM'); + + await expect(collector).not.toBeVisible(); + }); + + test('Drag-n-drop the appointment to the left column to the cell that has the same time', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + ...defaultSchedulerOptions, + timeZone: 'Etc/GMT', + dataSource: [{ + text: 'Test appointment', + startDate: new Date('2022-09-08T10:00:00.000Z'), + endDate: new Date('2022-09-08T10:30:00.000Z'), + }], + views: ['week'], + currentView: 'week', + currentDate: new Date('2022-09-09T10:00:00.000Z'), + startDayHour: 9, + width: 600, + height: 600, + }); + + const draggableAppointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'Test appointment' }); + const targetCell = page.locator('.dx-scheduler-date-table-row').nth(2).locator('.dx-scheduler-date-table-cell').nth(2); + + await draggableAppointment.dragTo(targetCell); + + await testScreenshot(page, 'drag-n-drop-appointment-to-left-column.png', { + element: page.locator('.dx-scheduler-work-space'), + }); + }); }); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/outlookDragging/base.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/outlookDragging/base.spec.ts index d86fe968bb1d..a363c9093c63 100644 --- a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/outlookDragging/base.spec.ts +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/outlookDragging/base.spec.ts @@ -1,432 +1,175 @@ -import { test, expect } from '@playwright/test'; -import { createWidget, testScreenshot } from '../../../../../playwright-helpers'; -import path from 'path'; +import { test } from '@playwright/test'; +import { createWidget, testScreenshot, getContainerUrl, setupTestPage } from '../../../../../playwright-helpers'; -const containerUrl = `file://${path.resolve(__dirname, '../../../../../tests/container.html')}`; +const containerUrl = getContainerUrl(__dirname, '../../../../../tests/container.html'); test.describe('Outlook dragging base tests', () => { test.beforeEach(async ({ page }) => { - await page.goto(containerUrl); - await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); - await page.evaluate((theme) => new Promise((resolve) => { - (window as any).DevExpress.ui.themes.ready(resolve); - (window as any).DevExpress.ui.themes.current(theme); - }), process.env.THEME || 'fluent.blue.light'); + await setupTestPage(page, containerUrl); }); -); - -test('Basic drag-n-drop movements in groups', async ({ page }) => { - // --- setup --- -await createWidget(page, 'dxScheduler', { - dataSource: [{ - text: 'Test', - startDate: new Date(2021, 1, 2), - endDate: new Date(2021, 1, 2, 1), - }], - views: ['timelineWeek'], - currentView: 'timelineWeek', - currentDate: new Date(2021, 1, 2), - cellDuration: 1440, - height: 300, - with: 500, - // --- test --- -// Scheduler on '#container' + test('Basic drag-n-drop movements in groups', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource: [{ + text: 'Website Re-Design Plan', + startDate: new Date(2021, 2, 26, 8, 30), + endDate: new Date(2021, 2, 26, 11, 0), + priorityId: 1, + }], + groups: ['priorityId'], + resources: [{ + fieldExpr: 'priorityId', + allowMultiple: false, + dataSource: [ + { text: 'Low Priority', id: 1, color: '#1e90ff' }, + { text: 'High Priority', id: 2, color: '#ff9747' }, + ], + label: 'Priority', + }], + views: ['day'], + currentView: 'day', + currentDate: new Date(2021, 2, 26), + startDayHour: 8, + height: 600, + width: 1000, + }); const draggableAppointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'Website Re-Design Plan' }); - - await /* TODO: drag */ await (draggableAppointment.element).click() /* drag(330, 70) */; - - await testScreenshot(page, 'drag-n-drop-to-orange-group.png', { element: page.locator('.dx-scheduler-work-space') }); - - await /* TODO: drag */ await (draggableAppointment.element).click() /* drag(-330, 70) */; - await testScreenshot(page, 'drag-n-drop-blue-group.png', { element: page.locator('.dx-scheduler-work-space') }); - - expect(compareResults.isValid()) - .ok(compareResults.errorMessages()); -}).before(async () => createWidget(page, 'dxScheduler', { - dataSource: [{ - text: 'Website Re-Design Plan', - startDate: new Date(2021, 2, 26, 8, 30), - endDate: new Date(2021, 2, 26, 11, 0), - priorityId: 1, - }], - groups: ['priorityId'], - resources: [{ - fieldExpr: 'priorityId', - allowMultiple: false, - dataSource: [{ - text: 'Low Priority', - id: 1, - color: '#1e90ff', - }, { - text: 'High Priority', - id: 2, - color: '#ff9747', - }], - label: 'Priority', - }], - views: ['day'], - currentView: 'day', - currentDate: new Date(2021, 2, 26), - startDayHour: 8, - height: 600, - width: 1000, -})); - -test('Basic drag-n-drop movements from tooltip in week view', async ({ page }) => { - // Scheduler on '#container' - - await (scheduler.collectors.find('2').click().element) - .expect(scheduler.appointmentTooltip.isVisible()).toBeTruthy() - .drag(scheduler.appointmentTooltip.getListItem('Appointment 3').element, 200, 50); - - await testScreenshot(page, 'drag-n-drop-\'Appointment 3\'-from-tooltip-in-week.png', { - element: page.locator('.dx-scheduler-work-space'), + const workSpace = page.locator('.dx-scheduler-work-space'); + + let box = await draggableAppointment.boundingBox(); + await draggableAppointment.hover(); + await page.mouse.down(); + await page.mouse.move(box!.x + box!.width / 2 + 330, box!.y + box!.height / 2 + 70, { steps: 15 }); + await page.mouse.up(); + await testScreenshot(page, 'drag-n-drop-to-orange-group.png', { element: workSpace }); + + box = await draggableAppointment.boundingBox(); + await draggableAppointment.hover(); + await page.mouse.down(); + await page.mouse.move(box!.x + box!.width / 2 - 330, box!.y + box!.height / 2 + 70, { steps: 15 }); + await page.mouse.up(); + await testScreenshot(page, 'drag-n-drop-blue-group.png', { element: workSpace }); }); - await (scheduler.collectors.find('1').click().element) - .expect(scheduler.appointmentTooltip.isVisible()).toBeTruthy() - .drag(scheduler.appointmentTooltip.getListItem('Appointment 2').element, 350, 150); - - await testScreenshot(page, 'drag-n-drop-\'Appointment 2\'-from-tooltip-in-week.png', { - element: page.locator('.dx-scheduler-work-space'), + test('Basic drag-n-drop movements', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource: [{ + text: 'Website Re-Design Plan', + startDate: new Date(2021, 2, 22, 10), + endDate: new Date(2021, 2, 22, 12, 30), + }], + views: ['week'], + currentView: 'week', + currentDate: new Date(2021, 2, 22), + startDayHour: 9, + height: 600, + width: 1000, + }); + + const appt = page.locator('.dx-scheduler-appointment').filter({ hasText: 'Website Re-Design Plan' }); + const ws = page.locator('.dx-scheduler-work-space'); + + let box = await appt.boundingBox(); + await appt.hover(); + await page.mouse.down(); + await page.mouse.move(box!.x + box!.width / 2 + 100, box!.y + box!.height / 2, { steps: 10 }); + await page.mouse.up(); + await testScreenshot(page, 'drag-n-drop-to-right.png', { element: ws }); + + box = await appt.boundingBox(); + await appt.hover(); + await page.mouse.down(); + await page.mouse.move(box!.x + box!.width / 2 - 100, box!.y + box!.height / 2, { steps: 10 }); + await page.mouse.up(); + await testScreenshot(page, 'drag-n-drop-to-left.png', { element: ws }); + + box = await appt.boundingBox(); + await appt.hover(); + await page.mouse.down(); + await page.mouse.move(box!.x + box!.width / 2, box!.y + box!.height / 2 + 100, { steps: 10 }); + await page.mouse.up(); + await testScreenshot(page, 'drag-n-drop-to-bottom.png', { element: ws }); + + box = await appt.boundingBox(); + await appt.hover(); + await page.mouse.down(); + await page.mouse.move(box!.x + box!.width / 2, box!.y + box!.height / 2 - 100, { steps: 10 }); + await page.mouse.up(); + await testScreenshot(page, 'drag-n-drop-to-top.png', { element: ws }); }); - expect(compareResults.isValid()) - .ok(compareResults.errorMessages()); -}).before(async () => createWidget(page, 'dxScheduler', { - dataSource: [{ - text: 'Appointment 1', - startDate: new Date(2021, 2, 21, 9, 30), - endDate: new Date(2021, 2, 21, 12, 0), - }, { - text: 'Appointment 2', - startDate: new Date(2021, 2, 21, 9, 30), - endDate: new Date(2021, 2, 21, 12, 0), - }, { - text: 'Appointment 3', - startDate: new Date(2021, 2, 21, 9, 30), - endDate: new Date(2021, 2, 21, 11, 0), - }, { - text: 'Appointment 4', - startDate: new Date(2021, 2, 21, 9, 30), - endDate: new Date(2021, 2, 21, 12, 30), - }], - views: ['week'], - currentView: 'week', - currentDate: new Date(2021, 2, 21), - startDayHour: 8, - height: 600, - width: 1000, -})); - -test.meta({ runInTheme: Themes.genericLight })('Basic drag-n-drop movements from tooltip in month view', async (t) => { - // Scheduler on '#container' - - await (scheduler.collectors.find('2').click().element) - .expect(scheduler.appointmentTooltip.isVisible()).toBeTruthy() - .drag(scheduler.appointmentTooltip.getListItem('Appointment 3').element, -180, -30); - - await testScreenshot(page, 'drag-n-drop-\'Appointment 3\'-from-tooltip-in-month.png', { - element: page.locator('.dx-scheduler-work-space'), + ['timelineWeek', 'timelineMonth'].forEach((currentView) => { + const dataSource = currentView === 'timelineWeek' + ? [{ text: 'Website Re-Design Plan', startDate: new Date(2021, 2, 21, 9, 30), endDate: new Date(2021, 2, 21, 10, 45) }] + : [{ text: 'Website Re-Design Plan', startDate: new Date(2021, 2, 2, 9, 30), endDate: new Date(2021, 2, 3, 11, 0) }]; + + test(`Basic drag-n-drop movements in ${currentView} view`, async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource, + views: ['timelineWeek', 'timelineMonth'], + currentView, + currentDate: new Date(2021, 2, 21), + startDayHour: 9, + height: 600, + width: 1000, + }); + + const appt = page.locator('.dx-scheduler-appointment').filter({ hasText: 'Website Re-Design Plan' }); + const ws = page.locator('.dx-scheduler-work-space'); + + let box = await appt.boundingBox(); + await appt.hover(); + await page.mouse.down(); + await page.mouse.move(box!.x + box!.width / 2 + 250, box!.y + box!.height / 2, { steps: 10 }); + await page.mouse.up(); + await testScreenshot(page, `drag-n-drop-${currentView}-to-right.png`, { element: ws }); + + box = await appt.boundingBox(); + await appt.hover(); + await page.mouse.down(); + await page.mouse.move(box!.x + box!.width / 2 - 250, box!.y + box!.height / 2, { steps: 10 }); + await page.mouse.up(); + await testScreenshot(page, `drag-n-drop-${currentView}-to-left.png`, { element: ws }); + }); }); - await (scheduler.collectors.find('1', 1).click().element) - .expect(scheduler.appointmentTooltip.isVisible()).toBeTruthy() - .drag(scheduler.appointmentTooltip.getListItem('Appointment 2').element, 320, 150); - - await testScreenshot(page, 'drag-n-drop-\'Appointment 2\'-from-tooltip-in-month.png', { - element: page.locator('.dx-scheduler-work-space'), + test('Narrow appointment dragging on minimal distance should be expected(1171520)', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource: [{ + text: 'Test', + startDate: new Date(2021, 1, 2), + endDate: new Date(2021, 1, 2, 1), + }], + views: ['timelineWeek'], + currentView: 'timelineWeek', + currentDate: new Date(2021, 1, 2), + cellDuration: 1440, + height: 300, + }); + + const ws = page.locator('.dx-scheduler-work-space'); + const appt = page.locator('.dx-scheduler-appointment').filter({ hasText: 'Test' }); + + let box = await appt.boundingBox(); + await page.mouse.move(box!.x + 10, box!.y + box!.height / 2); + await page.mouse.down(); + await page.mouse.move(box!.x + 10 - 10, box!.y + box!.height / 2, { steps: 5 }); + await page.mouse.up(); + await testScreenshot(page, 'drag-short-app-min-dist-to-left.png', { element: ws }); + + box = await appt.boundingBox(); + await page.mouse.move(box!.x + 10, box!.y + box!.height / 2); + await page.mouse.down(); + await page.mouse.move(box!.x + 10 + 195, box!.y + box!.height / 2, { steps: 10 }); + await page.mouse.up(); + await testScreenshot(page, 'drag-short-app-to-right.png', { element: ws }); + + box = await appt.boundingBox(); + await page.mouse.move(box!.x + 10, box!.y + box!.height / 2); + await page.mouse.down(); + await page.mouse.move(box!.x + 10 + 200, box!.y + box!.height / 2, { steps: 10 }); + await page.mouse.up(); + await testScreenshot(page, 'drag-short-app-to-right-on-next-cell.png', { element: ws }); }); - - expect(compareResults.isValid()) - .ok(compareResults.errorMessages()); -}).before(async () => createWidget(page, 'dxScheduler', { - dataSource: [{ - text: 'Appointment 1', - startDate: new Date(2021, 2, 31, 9, 30), - endDate: new Date(2021, 3, 1, 12, 0), - }, { - text: 'Appointment 2', - startDate: new Date(2021, 2, 31, 9, 30), - endDate: new Date(2021, 3, 1, 12, 0), - }, { - text: 'Appointment 3', - startDate: new Date(2021, 2, 31, 9, 30), - endDate: new Date(2021, 3, 1, 11, 0), - }, { - text: 'Appointment 4', - startDate: new Date(2021, 2, 31, 9, 30), - endDate: new Date(2021, 3, 1, 12, 30), - }], - views: ['month'], - currentView: 'month', - currentDate: new Date(2021, 2, 27), - startDayHour: 8, - height: 600, - width: 1000, -})); - -[{ - currentView: 'timelineWeek', - dataSource: [{ - text: 'Website Re-Design Plan', - startDate: new Date(2021, 2, 21, 9, 30), - endDate: new Date(2021, 2, 21, 10, 45), - }], -}, { - currentView: 'timelineMonth', - dataSource: [{ - text: 'Website Re-Design Plan', - startDate: new Date(2021, 2, 2, 9, 30), - endDate: new Date(2021, 2, 3, 11, 0), - }], -}].forEach(({ currentView, dataSource }) => { - test(`Basic drag-n-drop movements in ${currentView} view`, async ({ page }) => { - // Scheduler on '#container' - const draggableAppointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'Website Re-Design Plan' }); - - await /* TODO: drag */ await (draggableAppointment.element).click() /* drag(250, 0) */; - - await testScreenshot(page, `drag-n-drop-${currentView}-to-right.png`, { element: page.locator('.dx-scheduler-work-space') }); - - await /* TODO: drag */ await (draggableAppointment.element).click() /* drag(-250, 0) */; - - await testScreenshot(page, `drag-n-drop-${currentView}-to-left.png`, { element: page.locator('.dx-scheduler-work-space') }); - - expect(compareResults.isValid()) - .ok(compareResults.errorMessages()); - }).before(async () => createWidget(page, 'dxScheduler', { - dataSource, - views: ['timelineWeek', 'timelineMonth'], - currentView, - currentDate: new Date(2021, 2, 21), - startDayHour: 9, - height: 600, - width: 1000, - })); -}); - -test('Basic drag-n-drop movements', async ({ page }) => { - // Scheduler on '#container' - const draggableAppointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'Website Re-Design Plan' }); - - await t.drag(draggableAppointment.element, 100, 0, { speed: 0.5 }); - - await testScreenshot(page, 'drag-n-drop-to-right.png', { element: page.locator('.dx-scheduler-work-space') }); - - await t.drag(draggableAppointment.element, -100, 0, { speed: 0.5 }); - - await testScreenshot(page, 'drag-n-drop-to-left.png', { element: page.locator('.dx-scheduler-work-space') }); - - await t.drag(draggableAppointment.element, 0, 100, { speed: 0.5 }); - - await testScreenshot(page, 'drag-n-drop-to-bottom.png', { element: page.locator('.dx-scheduler-work-space') }); - - await t.drag(draggableAppointment.element, 0, -100, { speed: 0.5 }); - - await testScreenshot(page, 'drag-n-drop-to-top.png', { element: page.locator('.dx-scheduler-work-space') }); - - expect(compareResults.isValid()) - .ok(compareResults.errorMessages()); -}).before(async () => createWidget(page, 'dxScheduler', { - dataSource: [{ - text: 'Website Re-Design Plan', - startDate: new Date(2021, 2, 22, 10), - endDate: new Date(2021, 2, 22, 12, 30), - }], - views: ['week'], - currentView: 'week', - currentDate: new Date(2021, 2, 22), - startDayHour: 9, - height: 600, - width: 1000, -})); - -test('Basic drag-n-drop movements with mouse offset', async ({ page }) => { - // Scheduler on '#container' - const draggableAppointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'Website Re-Design Plan' }); - - await t.drag(draggableAppointment.element, 100, 0, { offsetX: 10, offsetY: 200, speed: 0.5 }); - await testScreenshot(page, 'drag-n-drop-mouse-offset-to-right.png', { element: page.locator('.dx-scheduler-work-space') }); - - await t.drag(draggableAppointment.element, -100, 0, { offsetX: 10, offsetY: 200, speed: 0.5 }); - await testScreenshot(page, 'drag-n-drop-mouse-offset-to-left.png', { element: page.locator('.dx-scheduler-work-space') }); - - await t.drag(draggableAppointment.element, 0, 100, { offsetX: 10, offsetY: 200, speed: 0.5 }); - await testScreenshot(page, 'drag-n-drop-mouse-offset-to-bottom.png', { element: page.locator('.dx-scheduler-work-space') }); - - await t.drag(draggableAppointment.element, 0, -100, { offsetX: 10, offsetY: 200, speed: 0.5 }); - await testScreenshot(page, 'drag-n-drop-mouse-offset-to-top.png', { element: page.locator('.dx-scheduler-work-space') }); - - expect(compareResults.isValid()) - .ok(compareResults.errorMessages()); -}).before(async () => createWidget(page, 'dxScheduler', { - dataSource: [{ - text: 'Website Re-Design Plan', - startDate: new Date(2021, 2, 22, 10), - endDate: new Date(2021, 2, 22, 12, 30), - }], - views: ['week'], - currentView: 'week', - currentDate: new Date(2021, 2, 22), - startDayHour: 9, - height: 600, - width: 1000, -})); - -test('Basic drag-n-drop all day appointment movements', async ({ page }) => { - // Scheduler on '#container' - const draggableAppointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'Website Re-Design Plan' }); - - await t.drag(draggableAppointment.element, 200, 0, { speed: 0.1 }); - await testScreenshot(page, 'drag-n-drop-all-day-to-right.png', { element: page.locator('.dx-scheduler-work-space') }); - - await t.drag(draggableAppointment.element, -200, 0, { speed: 0.1 }); - await testScreenshot(page, 'drag-n-drop-all-day-to-left.png', { element: page.locator('.dx-scheduler-work-space') }); - - await t.drag(draggableAppointment.element, 260, 270, { speed: 0.1 }); - await testScreenshot(page, 'drag-n-drop-all-day-to-bottom.png', { element: page.locator('.dx-scheduler-work-space') }); - - await t.drag(draggableAppointment.element, 0, -260, { speed: 0.1 }); - await testScreenshot(page, 'drag-n-drop-all-day-to-top.png', { element: page.locator('.dx-scheduler-work-space') }); - - expect(compareResults.isValid()) - .ok(compareResults.errorMessages()); -}).before(async () => createWidget(page, 'dxScheduler', { - dataSource: [{ - text: 'Website Re-Design Plan', - startDate: new Date(2021, 2, 23, 10), - endDate: new Date(2021, 2, 25, 12, 30), - }], - views: ['week'], - currentView: 'week', - currentDate: new Date(2021, 2, 23), - startDayHour: 9, - height: 600, - width: 1000, -})); - -test('Basic drag-n-drop movements within the cell', async ({ page }) => { - // Scheduler on '#container' - const draggableAppointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'Website Re-Design Plan' }); - - async function blurActiveElement(page: Page) { - await page.evaluate(() => { - const el = document.activeElement as HTMLElement | null; - el?.blur(); - }, ); -} - - await /* TODO: drag */ await (draggableAppointment.element).click() /* drag(55, 0) */; - await blurActiveElement(); - await testScreenshot(page, 'drag-n-drop-within-cell-to-right.png', { element: page.locator('.dx-scheduler-work-space') }); - - await /* TODO: drag */ await (draggableAppointment.element).click() /* drag(-50, 0) */; - await blurActiveElement(); - await testScreenshot(page, 'drag-n-drop-within-cell-to-left.png', { element: page.locator('.dx-scheduler-work-space') }); - - await /* TODO: drag */ await (draggableAppointment.element).click() /* drag(0, 30) */; - await blurActiveElement(); - await testScreenshot(page, 'drag-n-drop-within-cell-to-bottom.png', { element: page.locator('.dx-scheduler-work-space') }); - - expect(compareResults.isValid()) - .ok(compareResults.errorMessages()); -}).before(async () => createWidget(page, 'dxScheduler', { - dataSource: [{ - text: 'Website Re-Design Plan', - startDate: new Date(2021, 2, 22, 10), - endDate: new Date(2021, 2, 22, 12, 30), - }], - views: ['week'], - currentView: 'week', - currentDate: new Date(2021, 2, 22), - startDayHour: 9, - height: 600, - width: 1000, -})); - -test('Basic drag-n-drop small appointments', async ({ page }) => { - // Scheduler on '#container' - const draggableAppointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'Website Re-Design Plan' }); - - await /* TODO: drag */ await (draggableAppointment.element).click() /* drag(250, 0) */; - await testScreenshot(page, 'drag-n-drop-small-appoint-to-right.png', { element: page.locator('.dx-scheduler-work-space') }); - - await /* TODO: drag */ await (draggableAppointment.element).click() /* drag(-250, 0) */; - await testScreenshot(page, 'drag-n-drop-small-appoint-to-left.png', { element: page.locator('.dx-scheduler-work-space') }); - - await /* TODO: drag */ await (draggableAppointment.element).click() /* drag(0, 170) */; - await testScreenshot(page, 'drag-n-drop-small-appoint-to-bottom.png', { element: page.locator('.dx-scheduler-work-space') }); - - await /* TODO: drag */ await (draggableAppointment.element).click() /* drag(0, -170) */; - await testScreenshot(page, 'drag-n-drop-small-appoint-to-top.png', { element: page.locator('.dx-scheduler-work-space') }); - - expect(compareResults.isValid()) - .ok(compareResults.errorMessages()); -}).before(async () => createWidget(page, 'dxScheduler', { - dataSource: [{ - text: 'Website Re-Design Plan', - startDate: new Date(2021, 2, 17, 10), - endDate: new Date(2021, 2, 17, 12, 30), - }], - views: ['month'], - currentView: 'month', - currentDate: new Date(2021, 2, 17), - startDayHour: 9, - height: 600, - width: 1000, -})); - -test('Basic drag-n-drop long appointments', async ({ page }) => { - // Scheduler on '#container' - const draggableAppointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'Website Re-Design Plan' }); - - await /* TODO: drag */ await (draggableAppointment.element).click() /* drag(150, 0) */; - await testScreenshot(page, 'drag-n-drop-long-appoint-to-right.png', { element: page.locator('.dx-scheduler-work-space') }); - - await /* TODO: drag */ await (draggableAppointment.element).click() /* drag(-30, 0) */; - await testScreenshot(page, 'drag-n-drop-long-appoint-to-left.png', { element: page.locator('.dx-scheduler-work-space') }); - - await /* TODO: drag */ await (draggableAppointment.element).click() /* drag(0, 70) */; - await testScreenshot(page, 'drag-n-drop-long-appoint-to-bottom.png', { element: page.locator('.dx-scheduler-work-space') }); - - await /* TODO: drag */ await (draggableAppointment.element).click() /* drag(0, -70) */; - await testScreenshot(page, 'drag-n-drop-long-appoint-to-top.png', { element: page.locator('.dx-scheduler-work-space') }); - - expect(compareResults.isValid()) - .ok(compareResults.errorMessages()); -}).before(async () => createWidget(page, 'dxScheduler', { - dataSource: [{ - text: 'Website Re-Design Plan', - startDate: new Date(2021, 2, 16, 10), - endDate: new Date(2021, 2, 18, 12, 30), - }], - views: ['month'], - currentView: 'month', - currentDate: new Date(2021, 2, 16), - startDayHour: 9, - height: 600, - width: 1000, -})); - -test('Narrow appointment dragging on minimal distance should be expected(1171520)', async ({ page }) => { - // Scheduler on '#container' - await t.drag(page.locator('.dx-scheduler-appointment').filter({ hasText: 'Test' }).element, -10, 0, { offsetX: 10 }); - - await testScreenshot(page, 'drag-short-app-min-dist-to-left.png', { element: page.locator('.dx-scheduler-work-space') }); - - await t.drag(page.locator('.dx-scheduler-appointment').filter({ hasText: 'Test' }).element, 195, 0, { offsetX: 10 }); - - await testScreenshot(page, 'drag-short-app-to-right.png', { element: page.locator('.dx-scheduler-work-space') }); - - await t.drag(page.locator('.dx-scheduler-appointment').filter({ hasText: 'Test' }).element, 200, 0, { offsetX: 10 }); - - await testScreenshot(page, 'drag-short-app-to-right-on-next-cell.png', { element: page.locator('.dx-scheduler-work-space') }); - - expect(compareResults.isValid()) - .ok(compareResults.errorMessages()); -}); -}); }); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/outlookDragging/schedulerInContainer/schedulerInContainer.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/outlookDragging/schedulerInContainer/schedulerInContainer.spec.ts index 99069cba84c3..6e914bceeb75 100644 --- a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/outlookDragging/schedulerInContainer/schedulerInContainer.spec.ts +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/outlookDragging/schedulerInContainer/schedulerInContainer.spec.ts @@ -1,63 +1,66 @@ -import { test, expect } from '@playwright/test'; -import { createWidget, testScreenshot } from '../../../../../../playwright-helpers'; -import path from 'path'; +import { test } from '@playwright/test'; +import { createWidget, testScreenshot, getContainerUrl, setupTestPage } from '../../../../../../playwright-helpers'; -const containerUrl = `file://${path.resolve(__dirname, '../../../../../../tests/container.html')}`; +const containerUrl = getContainerUrl(__dirname, '../../../../../../tests/container.html'); test.describe('Outlook dragging, for case scheduler in container', () => { test.beforeEach(async ({ page }) => { - await page.goto(containerUrl); - await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); - await page.evaluate((theme) => new Promise((resolve) => { - (window as any).DevExpress.ui.themes.ready(resolve); - (window as any).DevExpress.ui.themes.current(theme); - }), process.env.THEME || 'fluent.blue.light'); + await setupTestPage(page, containerUrl); }); -); - -test('Dragging should be work right in case dxScheduler placed in dxTabPanel', async ({ page }) => { - // --- setup --- -await createWidget(page, 'dxTabPanel', { - items: [{ - title: 'Info', - text: 'This is Info Tab', - }, { - title: 'Contacts', - text: 'This is Contacts Tab', - disabled: true, - }], - itemTemplate: ClientFunction(() => ($('
') as any).dxScheduler({ - dataSource: [{ - text: 'Website Re-Design Plan', - startDate: new Date(2021, 2, 30, 11), - endDate: new Date(2021, 2, 30, 12), - }], - views: ['week', 'month'], - currentView: 'week', - currentDate: new Date(2021, 2, 28), - startDayHour: 9, - height: 600, - })), - // --- test --- -// Scheduler on '.dx-scheduler' - - const draggableAppointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'Website Re-Design Plan' }); - - await /* TODO: drag */ await (draggableAppointment.element).click() /* drag(0, 120) */; - - await testScreenshot(page, 'dxScheduler-placed-in-dxTabPanel-drag-to-bottom.png'); - - await /* TODO: drag */ await (draggableAppointment.element).click() /* drag(0, -170) */; - - await testScreenshot(page, 'dxScheduler-placed-in-dxTabPanel-drag-to-top.png'); - - await /* TODO: drag */ await (draggableAppointment.element).click() /* drag(100, 0) */; - - await testScreenshot(page, 'dxScheduler-placed-in-dxTabPanel-drag-to-right.png'); - - expect(compareResults.isValid()) - .ok(compareResults.errorMessages()); -}); -}); + test('Dragging should be work right in case dxScheduler placed in dxTabPanel', async ({ page }) => { + await page.evaluate(() => { + (window as any).DevExpress.fx.off = true; + + ($('#container') as any).dxTabPanel({ + items: [{ + title: 'Info', + text: 'This is Info Tab', + }, { + title: 'Contacts', + text: 'This is Contacts Tab', + disabled: true, + }], + itemTemplate: () => { + const scheduler = $('
'); + (scheduler as any).dxScheduler({ + dataSource: [{ + text: 'Website Re-Design Plan', + startDate: new Date(2021, 2, 30, 11), + endDate: new Date(2021, 2, 30, 12), + }], + views: ['week', 'month'], + currentView: 'week', + currentDate: new Date(2021, 2, 28), + startDayHour: 9, + height: 600, + }); + return scheduler; + }, + }); + }); + + const appt = page.locator('.dx-scheduler-appointment').filter({ hasText: 'Website Re-Design Plan' }); + + let box = await appt.boundingBox(); + await appt.hover(); + await page.mouse.down(); + await page.mouse.move(box!.x + box!.width / 2, box!.y + box!.height / 2 + 120, { steps: 10 }); + await page.mouse.up(); + await testScreenshot(page, 'dxScheduler-placed-in-dxTabPanel-drag-to-bottom.png'); + + box = await appt.boundingBox(); + await appt.hover(); + await page.mouse.down(); + await page.mouse.move(box!.x + box!.width / 2, box!.y + box!.height / 2 - 170, { steps: 10 }); + await page.mouse.up(); + await testScreenshot(page, 'dxScheduler-placed-in-dxTabPanel-drag-to-top.png'); + + box = await appt.boundingBox(); + await appt.hover(); + await page.mouse.down(); + await page.mouse.move(box!.x + box!.width / 2 + 100, box!.y + box!.height / 2, { steps: 10 }); + await page.mouse.up(); + await testScreenshot(page, 'dxScheduler-placed-in-dxTabPanel-drag-to-right.png'); + }); }); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/outlookDragging/schedulerInContainer/schedulerInTransformContainer.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/outlookDragging/schedulerInContainer/schedulerInTransformContainer.spec.ts index 4d3a911ba3b9..7711a94932ea 100644 --- a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/outlookDragging/schedulerInContainer/schedulerInTransformContainer.spec.ts +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/outlookDragging/schedulerInContainer/schedulerInTransformContainer.spec.ts @@ -1,66 +1,59 @@ -import { test, expect } from '@playwright/test'; -import { createWidget, testScreenshot, setStyleAttribute, appendElementTo } from '../../../../../../playwright-helpers'; -import path from 'path'; +import { test } from '@playwright/test'; +import { createWidget, testScreenshot, getContainerUrl, setupTestPage, setStyleAttribute, appendElementTo } from '../../../../../../playwright-helpers'; -const containerUrl = `file://${path.resolve(__dirname, '../../../../../../tests/container.html')}`; +const containerUrl = getContainerUrl(__dirname, '../../../../../../tests/container.html'); test.describe('Outlook dragging, for case scheduler in container with transform style', () => { test.beforeEach(async ({ page }) => { - await page.goto(containerUrl); - await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); - await page.evaluate((theme) => new Promise((resolve) => { - (window as any).DevExpress.ui.themes.ready(resolve); - (window as any).DevExpress.ui.themes.current(theme); - }), process.env.THEME || 'fluent.blue.light'); + await setupTestPage(page, containerUrl); }); -appendElementTo, - setStyleAttribute, -} from '../../../../../../helpers/domUtils'; - -); - -test('Dragging should be work right in case dxScheduler placed in container with transform style', async ({ page }) => { - // --- setup --- -await setStyleAttribute(Selector('#container'), 'margin-top: 100px; margin-left: 100px; transform: translate(0px, 0px);'); - await appendElementTo('#container', 'div', 'scheduler'); - - return createWidget(page, 'dxScheduler', { - dataSource: [{ - text: 'Website Re-Design Plan', - startDate: new Date(2021, 2, 24, 11), - endDate: new Date(2021, 2, 24, 12), - }], - views: ['workWeek'], - currentView: 'workWeek', - currentDate: new Date(2021, 2, 22), - startDayHour: 9, - height: 600, - width: 800, - }, '#scheduler'); - // --- test --- -// Scheduler on '#scheduler' - - const draggableAppointment = page.locator('.dx-scheduler-appointment').nth(0); - - await t - .drag(draggableAppointment.element, 0, 120); - - await testScreenshot(page, 'dxScheduler-placed-in-transform-container-drag-to-bottom.png'); - - await /* TODO: drag */ await (draggableAppointment.element).click() /* drag(0, -170) */; - - await testScreenshot(page, 'dxScheduler-placed-in-transform-container-drag-to-top.png'); - - await /* TODO: drag */ await (draggableAppointment.element).click() /* drag(100, 0) */; - - await testScreenshot(page, 'dxScheduler-placed-in-transform-container-drag-to-right.png'); - - await /* TODO: drag */ await (draggableAppointment.element).click() /* drag(-230, 0) */; - - await testScreenshot(page, 'dxScheduler-placed-in-transform-container-drag-to-left.png'); - - expect(compareResults.isValid()) - .ok(compareResults.errorMessages()); -}); + test('Dragging should be work right in case dxScheduler placed in container with transform style', async ({ page }) => { + await setStyleAttribute(page, '#container', 'margin-top: 100px; margin-left: 100px; transform: translate(0px, 0px);'); + await appendElementTo(page, '#container', 'div', { id: 'scheduler' }); + + await createWidget(page, 'dxScheduler', { + dataSource: [{ + text: 'Website Re-Design Plan', + startDate: new Date(2021, 2, 24, 11), + endDate: new Date(2021, 2, 24, 12), + }], + views: ['workWeek'], + currentView: 'workWeek', + currentDate: new Date(2021, 2, 22), + startDayHour: 9, + height: 600, + width: 800, + }, '#scheduler'); + + const appt = page.locator('#scheduler .dx-scheduler-appointment').first(); + + let box = await appt.boundingBox(); + await appt.hover(); + await page.mouse.down(); + await page.mouse.move(box!.x + box!.width / 2, box!.y + box!.height / 2 + 120, { steps: 10 }); + await page.mouse.up(); + await testScreenshot(page, 'dxScheduler-placed-in-transform-container-drag-to-bottom.png'); + + box = await appt.boundingBox(); + await appt.hover(); + await page.mouse.down(); + await page.mouse.move(box!.x + box!.width / 2, box!.y + box!.height / 2 - 170, { steps: 10 }); + await page.mouse.up(); + await testScreenshot(page, 'dxScheduler-placed-in-transform-container-drag-to-top.png'); + + box = await appt.boundingBox(); + await appt.hover(); + await page.mouse.down(); + await page.mouse.move(box!.x + box!.width / 2 + 100, box!.y + box!.height / 2, { steps: 10 }); + await page.mouse.up(); + await testScreenshot(page, 'dxScheduler-placed-in-transform-container-drag-to-right.png'); + + box = await appt.boundingBox(); + await appt.hover(); + await page.mouse.down(); + await page.mouse.move(box!.x + box!.width / 2 - 230, box!.y + box!.height / 2, { steps: 10 }); + await page.mouse.up(); + await testScreenshot(page, 'dxScheduler-placed-in-transform-container-drag-to-left.png'); + }); }); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/outlookDragging/shiftedContainer.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/outlookDragging/shiftedContainer.spec.ts index e2a7cf9f54d8..0215e758ebee 100644 --- a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/outlookDragging/shiftedContainer.spec.ts +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/outlookDragging/shiftedContainer.spec.ts @@ -1,59 +1,59 @@ -import { test, expect } from '@playwright/test'; -import { createWidget, testScreenshot, setStyleAttribute } from '../../../../../playwright-helpers'; -import path from 'path'; +import { test } from '@playwright/test'; +import { createWidget, testScreenshot, getContainerUrl, setupTestPage, setStyleAttribute } from '../../../../../playwright-helpers'; -const containerUrl = `file://${path.resolve(__dirname, '../../../../../tests/container.html')}`; +const containerUrl = getContainerUrl(__dirname, '../../../../../tests/container.html'); test.describe('Outlook dragging base tests in shifted container', () => { test.beforeEach(async ({ page }) => { - await page.goto(containerUrl); - await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); - await page.evaluate((theme) => new Promise((resolve) => { - (window as any).DevExpress.ui.themes.ready(resolve); - (window as any).DevExpress.ui.themes.current(theme); - }), process.env.THEME || 'fluent.blue.light'); + await setupTestPage(page, containerUrl); }); -); - -test('Basic drag-n-drop movements in shifted container', async ({ page }) => { - // --- setup --- -await setStyleAttribute(Selector('#container'), 'margin-left: 50px; margin-top: 70px;'); - - return createWidget(page, 'dxScheduler', { - dataSource: [{ - text: 'Website Re-Design Plan', - startDate: new Date(2021, 2, 22, 10), - endDate: new Date(2021, 2, 22, 12, 30), - }], - views: ['week'], - currentView: 'week', - currentDate: new Date(2021, 2, 22), - startDayHour: 9, - height: 600, - width: 950, - // --- test --- -// Scheduler on '#container' - const draggableAppointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'Website Re-Design Plan' }); - - await t.drag(draggableAppointment.element, 100, 0, { speed: 0.5 }); - - await testScreenshot(page, 'drag-n-drop-to-right-in-shifted-container.png', { element: page.locator('.dx-scheduler-work-space') }); - - await t.drag(draggableAppointment.element, -100, 0, { speed: 0.5 }); - - await testScreenshot(page, 'drag-n-drop-to-left-in-shifted-container.png', { element: page.locator('.dx-scheduler-work-space') }); - - await t.drag(draggableAppointment.element, 0, 100, { speed: 0.5 }); - - await testScreenshot(page, 'drag-n-drop-to-bottom-in-shifted-container.png', { element: page.locator('.dx-scheduler-work-space') }); - - await t.drag(draggableAppointment.element, 0, -100, { speed: 0.5 }); - - await testScreenshot(page, 'drag-n-drop-to-top-in-shifted-container.png', { element: page.locator('.dx-scheduler-work-space') }); - - expect(compareResults.isValid()) - .ok(compareResults.errorMessages()); -}); -}); + test('Basic drag-n-drop movements in shifted container', async ({ page }) => { + await setStyleAttribute(page, '#container', 'margin-left: 50px; margin-top: 70px;'); + + await createWidget(page, 'dxScheduler', { + dataSource: [{ + text: 'Website Re-Design Plan', + startDate: new Date(2021, 2, 22, 10), + endDate: new Date(2021, 2, 22, 12, 30), + }], + views: ['week'], + currentView: 'week', + currentDate: new Date(2021, 2, 22), + startDayHour: 9, + height: 600, + width: 950, + }); + + const appt = page.locator('.dx-scheduler-appointment').filter({ hasText: 'Website Re-Design Plan' }); + const ws = page.locator('.dx-scheduler-work-space'); + + let box = await appt.boundingBox(); + await appt.hover(); + await page.mouse.down(); + await page.mouse.move(box!.x + box!.width / 2 + 100, box!.y + box!.height / 2, { steps: 10 }); + await page.mouse.up(); + await testScreenshot(page, 'drag-n-drop-to-right-in-shifted-container.png', { element: ws }); + + box = await appt.boundingBox(); + await appt.hover(); + await page.mouse.down(); + await page.mouse.move(box!.x + box!.width / 2 - 100, box!.y + box!.height / 2, { steps: 10 }); + await page.mouse.up(); + await testScreenshot(page, 'drag-n-drop-to-left-in-shifted-container.png', { element: ws }); + + box = await appt.boundingBox(); + await appt.hover(); + await page.mouse.down(); + await page.mouse.move(box!.x + box!.width / 2, box!.y + box!.height / 2 + 100, { steps: 10 }); + await page.mouse.up(); + await testScreenshot(page, 'drag-n-drop-to-bottom-in-shifted-container.png', { element: ws }); + + box = await appt.boundingBox(); + await appt.hover(); + await page.mouse.down(); + await page.mouse.move(box!.x + box!.width / 2, box!.y + box!.height / 2 - 100, { steps: 10 }); + await page.mouse.up(); + await testScreenshot(page, 'drag-n-drop-to-top-in-shifted-container.png', { element: ws }); + }); }); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/templates/appointmentTemplate.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/templates/appointmentTemplate.spec.ts index bafab00de374..7cd12100ca13 100644 --- a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/templates/appointmentTemplate.spec.ts +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/templates/appointmentTemplate.spec.ts @@ -1,5 +1,5 @@ -import { test, expect } from '@playwright/test'; -import { createWidget, testScreenshot } from '../../../../../playwright-helpers'; +import { test } from '@playwright/test'; +import { testScreenshot } from '../../../../../playwright-helpers'; import path from 'path'; const containerUrl = `file://${path.resolve(__dirname, '../../../../../tests/container.html')}`; @@ -14,65 +14,32 @@ test.describe('Layout:Templates:appointmentTemplate', () => { }), process.env.THEME || 'fluent.blue.light'); }); -); - -['day', 'workWeek', 'month', 'timelineDay', 'timelineWorkWeek', 'agenda'].forEach((currentView) => { - test(`appointmentTemplate layout should be rendered right in '${currentView}'`, async ({ page }) => { - // --- setup --- -await createWidget(page, 'dxScheduler', { - dataSource: [{ - startDate: new Date(2017, 4, 21, 0, 30), - endDate: new Date(2017, 4, 21, 2, 30), - }, { - startDate: new Date(2017, 4, 22, 0, 30), - endDate: new Date(2017, 4, 22, 2, 30), - }, { - startDate: new Date(2017, 4, 23, 0, 30), - endDate: new Date(2017, 4, 23, 2, 30), - }, { - startDate: new Date(2017, 4, 24, 0, 30), - endDate: new Date(2017, 4, 24, 2, 30), - }, { - startDate: new Date(2017, 4, 25, 0, 30), - endDate: new Date(2017, 4, 25, 2, 30), - }, { - startDate: new Date(2017, 4, 26, 0, 30), - endDate: new Date(2017, 4, 26, 2, 30), - }, { - startDate: new Date(2017, 4, 27, 0, 30), - endDate: new Date(2017, 4, 27, 2, 30), - }], - views: [currentView], - currentView, - currentDate: new Date(2017, 4, 25), - appointmentTemplate: ClientFunction((appointment) => { - const result = $('
'); - - const startDateBox = ($('
') as any).dxDateBox({ - type: 'datetime', - value: appointment.appointmentData.startDate, - // --- test --- -// Scheduler on '#container' - await testScreenshot(page, - `appointment-template-currentView=${currentView}.png`, - { element: page.locator('.dx-scheduler-work-space') }, - ); - - expect(compareResults.isValid()) - .ok(compareResults.errorMessages()); -}); - - const endDateBox = ($('
') as any).dxDateBox({ - type: 'datetime', - value: appointment.appointmentData.endDate, + ['day', 'workWeek', 'month', 'timelineDay', 'timelineWorkWeek', 'agenda'].forEach((currentView) => { + test(`appointmentTemplate layout should be rendered right in '${currentView}'`, async ({ page }) => { + await page.evaluate((view: string) => { + (window as any).DevExpress.fx.off = true; + ($('#container') as any).dxScheduler({ + dataSource: [ + { startDate: new Date(2017, 4, 21, 0, 30), endDate: new Date(2017, 4, 21, 2, 30) }, + { startDate: new Date(2017, 4, 22, 0, 30), endDate: new Date(2017, 4, 22, 2, 30) }, + { startDate: new Date(2017, 4, 23, 0, 30), endDate: new Date(2017, 4, 23, 2, 30) }, + { startDate: new Date(2017, 4, 24, 0, 30), endDate: new Date(2017, 4, 24, 2, 30) }, + { startDate: new Date(2017, 4, 25, 0, 30), endDate: new Date(2017, 4, 25, 2, 30) }, + { startDate: new Date(2017, 4, 26, 0, 30), endDate: new Date(2017, 4, 26, 2, 30) }, + { startDate: new Date(2017, 4, 27, 0, 30), endDate: new Date(2017, 4, 27, 2, 30) }, + ], + views: [view], currentView: view, currentDate: new Date(2017, 4, 25), + appointmentTemplate(appointment: any) { + const result = $('
'); + const startDateBox = ($('
') as any).dxDateBox({ type: 'datetime', value: appointment.appointmentData.startDate }); + const endDateBox = ($('
') as any).dxDateBox({ type: 'datetime', value: appointment.appointmentData.endDate }); + result.append(startDateBox, endDateBox); + return result; + }, + height: 600, }); - - result.append(startDateBox, endDateBox); - - return result; - }), - height: 600, + }, currentView); + await testScreenshot(page, `appointment-template-currentView=${currentView}.png`, { element: page.locator('.dx-scheduler-work-space') }); }); }); }); -}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/templates/cellTemplate.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/templates/cellTemplate.spec.ts index a67287a5bfff..202f29e9b62a 100644 --- a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/templates/cellTemplate.spec.ts +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/templates/cellTemplate.spec.ts @@ -1,5 +1,5 @@ import { test, expect } from '@playwright/test'; -import { createWidget, testScreenshot } from '../../../../../playwright-helpers'; +import { testScreenshot } from '../../../../../playwright-helpers'; import path from 'path'; const containerUrl = `file://${path.resolve(__dirname, '../../../../../tests/container.html')}`; @@ -14,105 +14,47 @@ test.describe('Layout:Templates:CellTemplate', () => { }), process.env.THEME || 'fluent.blue.light'); }); -); - -const SCHEDULER_SELECTOR = '#container'; - -['day', 'workWeek', 'month', 'timelineDay', 'timelineWorkWeek', 'timelineMonth'].forEach((currentView) => { - test(`dataCellTemplate and dateCellTemplate layout should be rendered right in '${currentView}'`, async ({ page }) => { - // --- setup --- -await createWidget(page, 'dxScheduler', { - dataSource: [], - views: [currentView], - currentView, - currentDate: new Date(2017, 4, 25), - showAllDayPanel: false, - dataCellTemplate: ClientFunction((itemData) => ($('
') as any).dxDateBox({ - type: 'time', - value: itemData.startDate, - })), - dateCellTemplate: ClientFunction((itemData) => ($('
') as any).dxTextBox({ - value: new Intl.DateTimeFormat('en-US').format(itemData.date), - })), - height: 600, - // --- test --- -const scheduler = new Scheduler(SCHEDULER_SELECTOR); - await testScreenshot(page, - `data-cell-template-currentView=${currentView}.png`, - { element: page.locator('.dx-scheduler-work-space') }, - ); - - expect(compareResults.isValid()) - .ok(compareResults.errorMessages()); -}); + ['day', 'workWeek', 'month', 'timelineDay', 'timelineWorkWeek', 'timelineMonth'].forEach((currentView) => { + test(`dataCellTemplate and dateCellTemplate layout should be rendered right in '${currentView}'`, async ({ page }) => { + await page.evaluate((view: string) => { + (window as any).DevExpress.fx.off = true; + ($('#container') as any).dxScheduler({ + dataSource: [], views: [view], currentView: view, currentDate: new Date(2017, 4, 25), + showAllDayPanel: false, + dataCellTemplate(itemData: any) { return ($('
') as any).dxDateBox({ type: 'time', value: itemData.startDate }); }, + dateCellTemplate(itemData: any) { return ($('
') as any).dxTextBox({ value: new Intl.DateTimeFormat('en-US').format(itemData.date) }); }, + height: 600, + }); + }, currentView); + await testScreenshot(page, `data-cell-template-currentView=${currentView}.png`, { element: page.locator('.dx-scheduler-work-space') }); + }); }); -}); - -test('[T1251590] Async dateCellTemplate should be rendered only once', async ({ page }) => { - // --- setup --- -await createWidget(page, 'dxScheduler', { - dataSource: [ - { - startDate: '2024-01-01T01:00:00', - endDate: '2024-01-01T02:00:00', - allDay: true, - }, - ], - dateCellTemplate: ClientFunction((_, __, itemElement) => { - setTimeout(() => { - itemElement.append('TEST'); - }, 0); - }), - currentDate: '2024-01-01', - currentView: 'week', - // --- test --- -const scheduler = new Scheduler(SCHEDULER_SELECTOR); - const firstTableCell = scheduler.headerPanel.headerCells.nth(0); - - expect(firstTableCell.textContent).toBe('TEST'); -}); -}); - -test('[T1251590] Async dateCellTemplate should be rendered only once if has reference props (grouping)', async ({ page }) => { - const scheduler = new Scheduler(SCHEDULER_SELECTOR); - - const firstTableCell = scheduler.headerPanel.headerCells.nth(0); - - expect(firstTableCell.textContent).toBe('TEST'); -}); + test('[T1251590] Async dateCellTemplate should be rendered only once', async ({ page }) => { + await page.evaluate(() => { + (window as any).DevExpress.fx.off = true; + ($('#container') as any).dxScheduler({ + dataSource: [{ startDate: '2024-01-01T01:00:00', endDate: '2024-01-01T02:00:00', allDay: true }], + dateCellTemplate(_: any, __: any, itemElement: any) { setTimeout(() => { itemElement.append('TEST'); }, 0); }, + currentDate: '2024-01-01', currentView: 'week', + }); + }); + await page.waitForTimeout(100); + expect(await page.locator('.dx-scheduler-header-panel-cell').nth(0).textContent()).toBe('TEST'); + }); -// TODO: .before() block not converted - move to test setup -// { - await createWidget(page, 'dxScheduler', { - dataSource: [ - { - startDate: '2024-01-01T01:00:00', - endDate: '2024-01-01T02:00:00', - allDay: true, - }, - ], - groups: ['groupId'], - resources: [ - { - label: 'group', - fieldExpr: 'groupId', - dataSource: [ - { - text: 'A', - id: 0, - color: '#00af2c', - }, - ], - }, - ], - dateCellTemplate: ClientFunction((_, __, itemElement) => { - setTimeout(() => { - itemElement.append('TEST'); - }, 0); - }), - currentDate: '2024-01-01', - currentView: 'week', + test('[T1251590] Async dateCellTemplate should be rendered only once if has reference props (grouping)', async ({ page }) => { + await page.evaluate(() => { + (window as any).DevExpress.fx.off = true; + ($('#container') as any).dxScheduler({ + dataSource: [{ startDate: '2024-01-01T01:00:00', endDate: '2024-01-01T02:00:00', allDay: true }], + groups: ['groupId'], + resources: [{ label: 'group', fieldExpr: 'groupId', dataSource: [{ text: 'A', id: 0, color: '#00af2c' }] }], + dateCellTemplate(_: any, __: any, itemElement: any) { setTimeout(() => { itemElement.append('TEST'); }, 0); }, + currentDate: '2024-01-01', currentView: 'week', + }); + }); + await page.waitForTimeout(100); + expect(await page.locator('.dx-scheduler-header-panel-cell').nth(0).textContent()).toBe('TEST'); }); }); -}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/templates/tooltipTemplate.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/templates/tooltipTemplate.spec.ts index a6f1a2211092..eb39d4f747cf 100644 --- a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/templates/tooltipTemplate.spec.ts +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/templates/tooltipTemplate.spec.ts @@ -1,5 +1,5 @@ -import { test, expect } from '@playwright/test'; -import { createWidget, testScreenshot } from '../../../../../playwright-helpers'; +import { test } from '@playwright/test'; +import { testScreenshot } from '../../../../../playwright-helpers'; import path from 'path'; const containerUrl = `file://${path.resolve(__dirname, '../../../../../tests/container.html')}`; @@ -14,47 +14,23 @@ test.describe('Layout:Templates:appointmentTooltipTemplate', () => { }), process.env.THEME || 'fluent.blue.light'); }); -); - -test('appointmentTooltipTemplate layout should be rendered right', async ({ page }) => { - // --- setup --- -await createWidget(page, 'dxScheduler', { - dataSource: [{ - startDate: new Date(2017, 4, 25, 0, 30), - endDate: new Date(2017, 4, 25, 2, 30), - }], - views: ['workWeek'], - currentView: 'workWeek', - currentDate: new Date(2017, 4, 25), - appointmentTooltipTemplate: ClientFunction((appointment) => { - const result = $('
'); - - const startDateBox = ($('
') as any).dxDateBox({ - type: 'datetime', - value: appointment.appointmentData.startDate, - // --- test --- -// Scheduler on '#container' - await (scheduler.getAppointmentByIndex().click().element); - - await testScreenshot(page, - 'appointment-tooltip-template.png', - { element: page.locator('.dx-scheduler') }, - ); - - expect(compareResults.isValid()) - .ok(compareResults.errorMessages()); -}); - - const endDateBox = ($('
') as any).dxDateBox({ - type: 'datetime', - value: appointment.appointmentData.endDate, + test('appointmentTooltipTemplate layout should be rendered right', async ({ page }) => { + await page.evaluate(() => { + (window as any).DevExpress.fx.off = true; + ($('#container') as any).dxScheduler({ + dataSource: [{ startDate: new Date(2017, 4, 25, 0, 30), endDate: new Date(2017, 4, 25, 2, 30) }], + views: ['workWeek'], currentView: 'workWeek', currentDate: new Date(2017, 4, 25), + appointmentTooltipTemplate(appointment: any) { + const result = $('
'); + const startDateBox = ($('
') as any).dxDateBox({ type: 'datetime', value: appointment.appointmentData.startDate }); + const endDateBox = ($('
') as any).dxDateBox({ type: 'datetime', value: appointment.appointmentData.endDate }); + result.append(startDateBox, endDateBox); + return result; + }, + height: 600, }); - - result.append(startDateBox, endDateBox); - - return result; - }), - height: 600, + }); + await page.locator('.dx-scheduler-appointment').first().click(); + await testScreenshot(page, 'appointment-tooltip-template.png', { element: page.locator('.dx-scheduler') }); }); }); -}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/views/crossScrolling.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/views/crossScrolling.spec.ts index 59f89f37040c..dcbd468c04e8 100644 --- a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/views/crossScrolling.spec.ts +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/views/crossScrolling.spec.ts @@ -1,4 +1,4 @@ -import { test, expect } from '@playwright/test'; +import { test } from '@playwright/test'; import { createWidget, testScreenshot } from '../../../../../playwright-helpers'; import path from 'path'; @@ -14,81 +14,26 @@ test.describe('Scheduler: View with cross-scrolling', () => { }), process.env.THEME || 'fluent.blue.light'); }); -); - -test('Scrollable synchronization should work after changing current date (T1027231)', async ({ page }) => { - // Scheduler on '#container' - await scheduler.option('currentDate', new Date(2021, 4, 5)); - await page.evaluate((d) => $('#container').dxScheduler('instance').scrollTo(new Date(d)), (new Date(2021, 4, 15).toISOString()), { priorityId: 2 }); - - await testScreenshot(page, 'cross-scrolling-sync.png', { - element: page.locator('.dx-scheduler-work-space'), + test('Scrollable synchronization should work after changing current date (T1027231)', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + views: [{ type: 'week', name: 'Horizontal Grouping', groupOrientation: 'horizontal', cellDuration: 30, intervalCount: 2 }], + currentView: 'Horizontal Grouping', crossScrollingEnabled: true, currentDate: new Date(2021, 3, 21), + groups: ['priorityId'], + resources: [{ fieldExpr: 'priorityId', allowMultiple: false, dataSource: [{ text: 'Low Priority', id: 1, color: '#1e90ff' }, { text: 'High Priority', id: 2, color: '#ff9747' }], label: 'Priority' }], + height: 600, + }); + await page.evaluate(() => { ($('#container') as any).dxScheduler('instance').option('currentDate', new Date(2021, 4, 5)); }); + await page.evaluate(() => { ($('#container') as any).dxScheduler('instance').scrollTo(new Date(2021, 4, 15), { priorityId: 2 }); }); + await testScreenshot(page, 'cross-scrolling-sync.png', { element: page.locator('.dx-scheduler-work-space') }); }); - expect(compareResults.isValid()) - .ok(compareResults.errorMessages()); -}); - -// TODO: .before() block not converted - move to test setup -// { - await createWidget(page, 'dxScheduler', { - views: [{ - type: 'week', - name: 'Horizontal Grouping', - groupOrientation: 'horizontal', - cellDuration: 30, - intervalCount: 2, - }], - currentView: 'Horizontal Grouping', - crossScrollingEnabled: true, - currentDate: new Date(2021, 3, 21), - groups: ['priorityId'], - resources: [{ - fieldExpr: 'priorityId', - allowMultiple: false, - dataSource: [ - { - text: 'Low Priority', - id: 1, - color: '#1e90ff', - }, { - text: 'High Priority', - id: 2, - color: '#ff9747', - }, - ], - label: 'Priority', - }], - height: 600, + test('Scrollable should be prepared correctly after change visibility (T1032171)', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource: [], views: ['timelineMonth'], currentView: 'timelineMonth', currentDate: new Date(2021, 1, 2), + firstDayOfWeek: 0, startDayHour: 8, endDayHour: 20, cellDuration: 60, visible: false, height: 400, + }); + await page.evaluate(() => { ($('#container') as any).dxScheduler('instance').option('visible', true); }); + await page.evaluate(() => { ($('#container') as any).dxScheduler('instance').scrollTo(new Date(2021, 1, 12)); }); + await testScreenshot(page, 'cross-scrolling-sync-visibility.png', { element: page.locator('.dx-scheduler-work-space') }); }); }); - -test('Scrollable should be prepared correctly after change visibility (T1032171)', async ({ page }) => { - // Scheduler on '#container' - await scheduler.option('visible', true); - await page.evaluate((d) => $('#container').dxScheduler('instance').scrollTo(new Date(d)), (new Date(2021, 1, 12).toISOString())); - - await testScreenshot(page, 'cross-scrolling-sync-visibility.png', { - element: page.locator('.dx-scheduler-work-space'), - }); - - expect(compareResults.isValid()) - .ok(compareResults.errorMessages()); -}); - -// TODO: .before() block not converted - move to test setup -// { - await createWidget(page, 'dxScheduler', { - dataSource: [], - views: ['timelineMonth'], - currentView: 'timelineMonth', - currentDate: new Date(2021, 1, 2), - firstDayOfWeek: 0, - startDayHour: 8, - endDayHour: 20, - cellDuration: 60, - visible: false, - height: 400, - }); -}); -}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/views/day/allDay.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/views/day/allDay.spec.ts index 87dd6fa0b24e..ee337653d49c 100644 --- a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/views/day/allDay.spec.ts +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/views/day/allDay.spec.ts @@ -1,4 +1,4 @@ -import { test, expect } from '@playwright/test'; +import { test } from '@playwright/test'; import { createWidget, testScreenshot } from '../../../../../../playwright-helpers'; import path from 'path'; @@ -14,57 +14,20 @@ test.describe('Layout:Views:Day:AllDay', () => { }), process.env.THEME || 'fluent.blue.light'); }); -); - -async function enableNativeScroll(page: Page) { - await page.evaluate(() => { - ($('#container') as any).dxScheduler('instance').getWorkSpaceScrollable().option('useNative', true); -}, ); -} - -[1, 2].forEach((intervalCount) => { - ['horizontal', 'vertical'].forEach((groupOrientation) => { - [true, false].forEach((showAllDayPanel) => { - const testName = `Day view with interval and crossScrollingEnabled(groupOrientation='${groupOrientation}', showAllDayPanel='${showAllDayPanel}', intervalCount='${intervalCount}') - layout test`; - - test(testName, async ({ page }) => { - // Scheduler on '#container' - await enableNativeScroll(); - - const pngName = `day-orientation=${groupOrientation}-allDay=${showAllDayPanel}-interval=${intervalCount}.png`; - - await testScreenshot(page, pngName, { element: page.locator('.dx-scheduler') }); - - expect(compareResults.isValid()) - .ok(compareResults.errorMessages()); - }).before(async () => createWidget(page, 'dxScheduler', { - resources: [{ - fieldExpr: 'roomId', - dataSource: [{ - text: 'Room 1', - id: 1, - }, { - text: 'Room 2', - id: 2, - }], - label: 'Room', - }], - dataSource: [], - views: [{ - name: 'dayView', - type: 'day', - intervalCount, - groupOrientation, - }], - currentView: 'dayView', - currentDate: new Date(2021, 2, 25), - height: 600, - groups: ['roomId'], - showAllDayPanel, - crossScrollingEnabled: true, - })); + [1, 2].forEach((intervalCount) => { + ['horizontal', 'vertical'].forEach((groupOrientation) => { + [true, false].forEach((showAllDayPanel) => { + test(`Day view with interval and crossScrollingEnabled(groupOrientation='${groupOrientation}', showAllDayPanel='${showAllDayPanel}', intervalCount='${intervalCount}') layout test`, async ({ page }) => { + await createWidget(page, 'dxScheduler', { + resources: [{ fieldExpr: 'roomId', dataSource: [{ text: 'Room 1', id: 1 }, { text: 'Room 2', id: 2 }], label: 'Room' }], + dataSource: [], views: [{ name: 'dayView', type: 'day', intervalCount, groupOrientation }], + currentView: 'dayView', currentDate: new Date(2021, 2, 25), height: 600, + groups: ['roomId'], showAllDayPanel, crossScrollingEnabled: true, + }); + await page.evaluate(() => { ($('#container') as any).dxScheduler('instance').getWorkSpaceScrollable().option('useNative', true); }); + await testScreenshot(page, `day-orientation=${groupOrientation}-allDay=${showAllDayPanel}-interval=${intervalCount}.png`, { element: page.locator('.dx-scheduler') }); + }); + }); }); }); }); -}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/views/firstDayOfWeek.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/views/firstDayOfWeek.spec.ts index 46c0cadd6b51..03d77d985a11 100644 --- a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/views/firstDayOfWeek.spec.ts +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/views/firstDayOfWeek.spec.ts @@ -1,4 +1,4 @@ -import { test, expect } from '@playwright/test'; +import { test } from '@playwright/test'; import { createWidget, testScreenshot } from '../../../../../playwright-helpers'; import path from 'path'; @@ -14,24 +14,8 @@ test.describe('Scheduler: View with first day of week', () => { }), process.env.THEME || 'fluent.blue.light'); }); -); - -test('WorkWeek should generate correct start view date', async ({ page }) => { - // --- setup --- -await createWidget(page, 'dxScheduler', { - views: ['workWeek'], - currentView: 'workWeek', - firstDayOfWeek: 1, - currentDate: new Date(2021, 11, 12), - height: 600, - // --- test --- -// Scheduler on '#container' - await testScreenshot(page, 'work-week-first-day-of-week.png', { - element: page.locator('.dx-scheduler'), + test('WorkWeek should generate correct start view date', async ({ page }) => { + await createWidget(page, 'dxScheduler', { views: ['workWeek'], currentView: 'workWeek', firstDayOfWeek: 1, currentDate: new Date(2021, 11, 12), height: 600 }); + await testScreenshot(page, 'work-week-first-day-of-week.png', { element: page.locator('.dx-scheduler') }); }); - - expect(compareResults.isValid()) - .ok(compareResults.errorMessages()); -}); -}); }); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/views/intervalCount/viewsWithStartDate.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/views/intervalCount/viewsWithStartDate.spec.ts index 1593d21ee045..836e6d43da4f 100644 --- a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/views/intervalCount/viewsWithStartDate.spec.ts +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/views/intervalCount/viewsWithStartDate.spec.ts @@ -1,4 +1,4 @@ -import { test, expect } from '@playwright/test'; +import { test } from '@playwright/test'; import { createWidget, testScreenshot } from '../../../../../../playwright-helpers'; import path from 'path'; @@ -14,108 +14,36 @@ test.describe('Layout: Views: IntervalCount with StartDate', () => { }), process.env.THEME || 'fluent.blue.light'); }); -); - -[{ - view: 'timelineDay', - currentDate: new Date(2021, 4, 11), - startDate: new Date(2021, 4, 8), - intervalCount: 6, -}, { - view: 'week', - currentDate: new Date(2021, 4, 11), - startDate: new Date(2021, 3, 12), - intervalCount: 8, -}, { - view: 'timelineWeek', - currentDate: new Date(2021, 4, 11), - startDate: new Date(2021, 3, 12), - intervalCount: 8, -}, { - view: 'workWeek', - currentDate: new Date(2021, 4, 11), - startDate: new Date(2021, 3, 12), - intervalCount: 8, -}, { - view: 'timelineWorkWeek', - currentDate: new Date(2021, 4, 11), - startDate: new Date(2021, 3, 12), - intervalCount: 8, -}, { - view: 'month', - currentDate: new Date(2020, 5, 11), - startDate: new Date(2020, 3, 8), - intervalCount: 6, -}, { - view: 'timelineMonth', - currentDate: new Date(2020, 5, 11), - startDate: new Date(2020, 3, 8), - intervalCount: 6, -}].forEach(({ - view, currentDate, startDate, intervalCount, -}) => { - test(`startDate should work in ${view} view`, async ({ page }) => { - // Scheduler on '#container' - - await testScreenshot(page, `start-date-in-${view}.png`); - - await (page.locator('.dx-scheduler-date-table-row').nth(0).locator('.dx-scheduler-date-table-cell').nth(0).dblclick()); - - await testScreenshot(page, `start-date-in-${view}-with-form.png`); - - expect(compareResults.isValid()) - .ok(compareResults.errorMessages()); - }).before(async () => createWidget(page, 'dxScheduler', { - views: [{ - type: view, - intervalCount, - startDate, - }], - currentView: view, - currentDate, - dataSource: [], - crossScrollingEnabled: true, - })); -}); - -[{ - view: 'week', - currentDate: new Date(2020, 9, 6), - startDate: new Date(2020, 8, 16), - intervalCount: 3, -}, { - view: 'timelineWeek', - currentDate: new Date(2020, 9, 6), - startDate: new Date(2020, 8, 16), - intervalCount: 3, -}, { - view: 'workWeek', - currentDate: new Date(2020, 9, 6), - startDate: new Date(2020, 8, 16), - intervalCount: 3, -}, { - view: 'timelineWorkWeek', - currentDate: new Date(2020, 9, 6), - startDate: new Date(2020, 8, 16), - intervalCount: 3, -}].forEach(({ - view, currentDate, startDate, intervalCount, -}) => { - test(`startDate should work in ${view} view when it indicates the same week as the start as currentDate`, async ({ page }) => { - await testScreenshot(page, `complex-start-date-in-${view}.png`); + [ + { view: 'timelineDay', currentDate: new Date(2021, 4, 11), startDate: new Date(2021, 4, 8), intervalCount: 6 }, + { view: 'week', currentDate: new Date(2021, 4, 11), startDate: new Date(2021, 3, 12), intervalCount: 8 }, + { view: 'timelineWeek', currentDate: new Date(2021, 4, 11), startDate: new Date(2021, 3, 12), intervalCount: 8 }, + { view: 'workWeek', currentDate: new Date(2021, 4, 11), startDate: new Date(2021, 3, 12), intervalCount: 8 }, + { view: 'timelineWorkWeek', currentDate: new Date(2021, 4, 11), startDate: new Date(2021, 3, 12), intervalCount: 8 }, + { view: 'month', currentDate: new Date(2020, 5, 11), startDate: new Date(2020, 3, 8), intervalCount: 6 }, + { view: 'timelineMonth', currentDate: new Date(2020, 5, 11), startDate: new Date(2020, 3, 8), intervalCount: 6 }, + ].forEach(({ view, currentDate, startDate, intervalCount }) => { + test(`startDate should work in ${view} view`, async ({ page }) => { + await createWidget(page, 'dxScheduler', { + views: [{ type: view, intervalCount, startDate }], currentView: view, currentDate, dataSource: [], crossScrollingEnabled: true, + }); + await testScreenshot(page, `start-date-in-${view}.png`); + await page.locator('.dx-scheduler-date-table-cell').first().dblclick(); + await testScreenshot(page, `start-date-in-${view}-with-form.png`); + }); + }); - expect(compareResults.isValid()) - .ok(compareResults.errorMessages()); - }).before(async () => createWidget(page, 'dxScheduler', { - views: [{ - type: view, - intervalCount, - startDate, - }], - currentView: view, - currentDate, - dataSource: [], - crossScrollingEnabled: true, - })); -}); + [ + { view: 'week', currentDate: new Date(2020, 9, 6), startDate: new Date(2020, 8, 16), intervalCount: 3 }, + { view: 'timelineWeek', currentDate: new Date(2020, 9, 6), startDate: new Date(2020, 8, 16), intervalCount: 3 }, + { view: 'workWeek', currentDate: new Date(2020, 9, 6), startDate: new Date(2020, 8, 16), intervalCount: 3 }, + { view: 'timelineWorkWeek', currentDate: new Date(2020, 9, 6), startDate: new Date(2020, 8, 16), intervalCount: 3 }, + ].forEach(({ view, currentDate, startDate, intervalCount }) => { + test(`startDate should work in ${view} view when it indicates the same week as the start as currentDate`, async ({ page }) => { + await createWidget(page, 'dxScheduler', { + views: [{ type: view, intervalCount, startDate }], currentView: view, currentDate, dataSource: [], crossScrollingEnabled: true, + }); + await testScreenshot(page, `complex-start-date-in-${view}.png`); + }); + }); }); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/views/material/withoutAllDay.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/views/material/withoutAllDay.spec.ts index 7d7fe299a411..7a0418208328 100644 --- a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/views/material/withoutAllDay.spec.ts +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/views/material/withoutAllDay.spec.ts @@ -1,4 +1,4 @@ -import { test, expect } from '@playwright/test'; +import { test } from '@playwright/test'; import { createWidget, testScreenshot } from '../../../../../../playwright-helpers'; import path from 'path'; @@ -14,22 +14,8 @@ test.describe('Scheduler: Material theme without all-day panel', () => { }), process.env.THEME || 'fluent.blue.light'); }); -); - -// visual: material.blue.light -test('Week view without all-day panel should be rendered correctly', async ({ page }) => { - // Scheduler on '#container' - await testScreenshot(page, 'week-without-all-day-panel.png', { - element: page.locator('.dx-scheduler-work-space'), + test('Week view without all-day panel should be rendered correctly', async ({ page }) => { + await createWidget(page, 'dxScheduler', { dataSource: [], currentDate: new Date(2020, 6, 15), views: ['week'], currentView: 'week', height: 500 }); + await testScreenshot(page, 'week-without-all-day-panel.png', { element: page.locator('.dx-scheduler-work-space') }); }); - - expect(compareResults.isValid()) - .ok(compareResults.errorMessages()); -}).before(async () => createWidget(page, 'dxScheduler', { - dataSource: [], - currentDate: new Date(2020, 6, 15), - views: ['week'], - currentView: 'week', - height: 500, -})); }); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/views/timeline/crossScrolling.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/views/timeline/crossScrolling.spec.ts index 7f82b8fb1092..3f25f1605f86 100644 --- a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/views/timeline/crossScrolling.spec.ts +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/views/timeline/crossScrolling.spec.ts @@ -14,68 +14,24 @@ test.describe('Scheduler Timeline: Cross-Scrolling', () => { }), process.env.THEME || 'fluent.blue.light'); }); -); - -test('Timeline should have Cross-Scrolling enabled', async ({ page }) => { - // --- setup --- -await createWidget(page, 'dxScheduler', { - height: 400, - width: 800, - currentDate: new Date(2021, 1, 2), - dataSource: [], - views: ['timelineDay'], - currentView: 'timelineDay', - startDayHour: 8, - endDayHour: 20, - cellDuration: 60, - showAllDayPanel: false, - groups: ['humanId'], - resources: [{ - fieldExpr: 'humanId', - dataSource: [{ - id: 0, - text: 'David Carter', - color: '#74d57b', - }, { - id: 1, - text: 'Emma Lewis', - color: '#1db2f5', - }, { - id: 2, - text: 'Noah Hill', - color: '#f5564a', - }, { - id: 3, - text: 'William Bell', - color: '#97c95c', - }, { - id: 4, - text: 'Jane Jones', - color: '#ffc720', - }, { - id: 5, - text: 'Violet Young', - color: '#eb3573', - }, { - id: 6, - text: 'Samuel Perry', - color: '#a63db8', - }, { - id: 7, - text: 'Luther Murphy', - color: '#ffaa66', - }, { - id: 8, - text: 'Craig Morris', - color: '#2dcdc4', - }], - label: 'Employee', - }], - // --- test --- -// Scheduler on '#container' - - expect(await scheduler.workspaceHasBothScrollbar) - .ok(); -}); -}); + test('Timeline should have Cross-Scrolling enabled', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + height: 400, width: 800, currentDate: new Date(2021, 1, 2), dataSource: [], + views: ['timelineDay'], currentView: 'timelineDay', startDayHour: 8, endDayHour: 20, cellDuration: 60, + showAllDayPanel: false, groups: ['humanId'], + resources: [{ fieldExpr: 'humanId', dataSource: [ + { id: 0, text: 'David Carter', color: '#74d57b' }, { id: 1, text: 'Emma Lewis', color: '#1db2f5' }, + { id: 2, text: 'Noah Hill', color: '#f5564a' }, { id: 3, text: 'William Bell', color: '#97c95c' }, + { id: 4, text: 'Jane Jones', color: '#ffc720' }, { id: 5, text: 'Violet Young', color: '#eb3573' }, + { id: 6, text: 'Samuel Perry', color: '#a63db8' }, { id: 7, text: 'Luther Murphy', color: '#ffaa66' }, + { id: 8, text: 'Craig Morris', color: '#2dcdc4' }, + ], label: 'Employee' }], + }); + const hasBothScrollbars = await page.evaluate(() => { + const scrollable = document.querySelector('.dx-scheduler-work-space .dx-scrollable'); + if (!scrollable) return false; + return scrollable.scrollHeight > scrollable.clientHeight && scrollable.scrollWidth > scrollable.clientWidth; + }); + expect(hasBothScrollbars).toBeTruthy(); + }); }); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/views/timeline/grouping.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/views/timeline/grouping.spec.ts index 0347976b1570..fffed2a6a417 100644 --- a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/views/timeline/grouping.spec.ts +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/views/timeline/grouping.spec.ts @@ -14,36 +14,16 @@ test.describe('Scheduler Timeline: Grouping', () => { }), process.env.THEME || 'fluent.blue.light'); }); -); - -[ - 'timelineDay', - 'timelineWeek', - 'timelineWorkWeek', -].forEach((view) => { - test(`${view} view - header panel should contain group rows if horizontal grouping`, async ({ page }) => { - // --- setup --- -await createWidget(page, 'dxScheduler', { - groupOrientation: 'horizontal', - views: [{ - type: 'timelineDay', + ['timelineDay', 'timelineWeek', 'timelineWorkWeek'].forEach((view) => { + test(`${view} view - header panel should contain group rows if horizontal grouping`, async ({ page }) => { + await createWidget(page, 'dxScheduler', { groupOrientation: 'horizontal', - }], - currentView: 'timelineDay', - groups: ['one'], - resources: [{ - fieldExpr: 'one', - dataSource: [ - { id: 1, text: 'a' }, - { id: 2, text: 'b' }, - ], - }], - // --- test --- -// Scheduler on '#container' - - expect(await scheduler.headerPanel.groupCells.count) - .eql(2); -}); + views: [{ type: 'timelineDay', groupOrientation: 'horizontal' }], + currentView: 'timelineDay', groups: ['one'], + resources: [{ fieldExpr: 'one', dataSource: [{ id: 1, text: 'a' }, { id: 2, text: 'b' }] }], + }); + const groupCellCount = await page.locator('.dx-scheduler-header-panel .dx-scheduler-group-header').count(); + expect(groupCellCount).toBe(2); + }); }); }); -}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/views/timeline/month.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/views/timeline/month.spec.ts index dedc9df542d5..dcf4eb54ab7b 100644 --- a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/views/timeline/month.spec.ts +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/layout/views/timeline/month.spec.ts @@ -1,4 +1,4 @@ -import { test, expect } from '@playwright/test'; +import { test } from '@playwright/test'; import { createWidget, testScreenshot } from '../../../../../../playwright-helpers'; import path from 'path'; @@ -14,24 +14,9 @@ test.describe('Scheduler: Layout Views: Timeline Month', () => { }), process.env.THEME || 'fluent.blue.light'); }); -); - -test('Header cells should be aligned with date-table cells in timeline-month when current date changes', async ({ page }) => { - // --- setup --- -await createWidget(page, 'dxScheduler', { - currentDate: new Date(2020, 10, 1), - currentView: 'timelineMonth', - height: 600, - views: ['timelineMonth'], - crossScrollingEnabled: true, - // --- test --- -// Scheduler on '#container' - await scheduler.option('currentDate', new Date(2020, 11, 1)); - - await testScreenshot(page, 'timeline-month-change-current-date.png'); - - expect(compareResults.isValid()) - .ok(compareResults.errorMessages()); -}); -}); + test('Header cells should be aligned with date-table cells in timeline-month when current date changes', async ({ page }) => { + await createWidget(page, 'dxScheduler', { currentDate: new Date(2020, 10, 1), currentView: 'timelineMonth', height: 600, views: ['timelineMonth'], crossScrollingEnabled: true }); + await page.evaluate(() => { ($('#container') as any).dxScheduler('instance').option('currentDate', new Date(2020, 11, 1)); }); + await testScreenshot(page, 'timeline-month-change-current-date.png'); + }); }); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/timezone/recurrence/appointmentWithoutTimezone.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/timezone/recurrence/appointmentWithoutTimezone.spec.ts index 92d3c1ce6dd3..ab71a0e3b9ed 100644 --- a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/timezone/recurrence/appointmentWithoutTimezone.spec.ts +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/timezone/recurrence/appointmentWithoutTimezone.spec.ts @@ -1,262 +1,107 @@ -import { test, expect } from '@playwright/test'; -import { createWidget, testScreenshot } from '../../../../../playwright-helpers'; -import path from 'path'; +import { test } from '@playwright/test'; +import { createWidget, testScreenshot, getContainerUrl, setupTestPage } from '../../../../../playwright-helpers'; -const containerUrl = `file://${path.resolve(__dirname, '../../../../../tests/container.html')}`; +const containerUrl = getContainerUrl(__dirname, '../../../../../tests/container.html'); -test.describe('Recurrent appointments without timezone in scheduler with timezone', () => { - test.beforeEach(async ({ page }) => { - await page.goto(containerUrl); - await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); - await page.evaluate((theme) => new Promise((resolve) => { - (window as any).DevExpress.ui.themes.ready(resolve); - (window as any).DevExpress.ui.themes.current(theme); - }), process.env.THEME || 'fluent.blue.light'); - }); - -); - -const SELECT_SELECTOR = '#container'; -const SCHEDULER_SELECTOR = '#otherContainer'; const SCREENSHOT_BASE_NAME = 'without-timezone-recurrent'; const TEST_TIMEZONES = ['Etc/GMT-10', 'Etc/GMT+1', 'Etc/GMT+10']; -const TEST_CURSOR_OPTIONS = { speed: 0.5 }; -const createTimezoneSelect = async ( - selector: string, - items: string[], - schedulerSelector: string, -): Promise => { - await ClientFunction(() => { - ($(selector) as any).dxSelectBox({ - items, +const getScreenshotName = (baseName: string, suffix: string) => `${baseName}__${suffix}.png`; + +async function createTimezoneSelect(page, items: string[]): Promise { + await page.evaluate(({ tzItems }) => { + ($('#container') as any).dxSelectBox({ + items: tzItems, width: 240, - value: items[1], - onValueChanged(data) { - const scheduler = ($(schedulerSelector) as any).dxScheduler('instance'); + value: tzItems[1], + onValueChanged(data: any) { + const scheduler = ($('#otherContainer') as any).dxScheduler('instance'); scheduler.option('timeZone', data.value); }, }); - }, { - dependencies: { selector, schedulerSelector, items }, - })(); -}; - -const selectTimezoneInUI = async (t: TestController, selectBox: SelectBox, timezoneIdx: number) => { - await (selectBox.element, TEST_CURSOR_OPTIONS).click(); - const timezonesList = await selectBox.getList(); - - await (timezonesList.getItem(timezoneIdx).click().element, TEST_CURSOR_OPTIONS); -}; - -test('Should correctly display the recurrent weekly appointment without timezone', async ({ page }) => { - // --- setup --- -const schedulerTimezone = TEST_TIMEZONES[1]; + }, { tzItems: items }); +} - await createTimezoneSelect(SELECT_SELECTOR, TEST_TIMEZONES, SCHEDULER_SELECTOR); - await createWidget(page, 'dxScheduler', { - dataSource: [{ - allDay: false, - startDate: new Date('2021-04-28T11:00:00.000Z'), - endDate: new Date('2021-04-28T13:00:00.000Z'), - recurrenceRule: 'FREQ=WEEKLY;BYDAY=WE', - text: 'Test', - }], - timeZone: schedulerTimezone, - currentView: 'week', - currentDate: new Date(2021, 3, 28), - startDayHour: 0, - cellDuration: 180, - width: 1000, - height: 585, - }, SCHEDULER_SELECTOR); - // --- test --- -const selectBox = new SelectBox(SELECT_SELECTOR); - const schedulerWorkspace = new Scheduler(SCHEDULER_SELECTOR).page.locator('.dx-scheduler-work-space'); - // expected date: 4/28/2021 10:00 AM - 12:00 PM - await testScreenshot(page, - getScreenshotName(SCREENSHOT_BASE_NAME, 'weekly-appointment__same-timezone'), - { element: schedulerWorkspace }, - ); +async function selectTimezoneInUI(page, timezoneIdx: number): Promise { + await page.locator('#container').click(); + const listItems = page.locator('.dx-list-item'); + await listItems.nth(timezoneIdx).click(); +} - await selectTimezoneInUI(t, selectBox, 0); - // expected date: 4/28/2021 9:00 PM - 11:00 PM - await testScreenshot(page, - getScreenshotName(SCREENSHOT_BASE_NAME, 'weekly-appointment__greater-timezone'), - { element: schedulerWorkspace }, - ); - - await selectTimezoneInUI(t, selectBox, 2); - // expected date: 4/28/2021 1:00 AM - 3:00 AM - await testScreenshot(page, - getScreenshotName(SCREENSHOT_BASE_NAME, 'weekly-appointment__lower-timezone'), - { element: schedulerWorkspace }, - ); - - expect(compareResults.isValid()) - .ok(compareResults.errorMessages()); -}); - -test('Should correctly display the recurrent monthly appointment without timezone', async ({ page }) => { - // --- setup --- -const schedulerTimezone = TEST_TIMEZONES[1]; - - await createTimezoneSelect(SELECT_SELECTOR, TEST_TIMEZONES, SCHEDULER_SELECTOR); - await createWidget(page, 'dxScheduler', { - dataSource: [{ - allDay: false, - startDate: new Date('2021-04-28T11:00:00.000Z'), - endDate: new Date('2021-04-28T13:00:00.000Z'), - recurrenceRule: 'FREQ=WEEKLY;BYDAY=WE', - text: 'Test', - }], - timeZone: schedulerTimezone, - currentView: 'week', - currentDate: new Date(2021, 3, 28), - startDayHour: 0, - cellDuration: 180, - width: 1000, - height: 585, - }, SCHEDULER_SELECTOR); - // --- test --- -const selectBox = new SelectBox(SELECT_SELECTOR); - const schedulerWorkspace = new Scheduler(SCHEDULER_SELECTOR).page.locator('.dx-scheduler-work-space'); - // expected date: 4/28/2021 10:00 AM - 12:00 PM - await testScreenshot(page, - getScreenshotName(SCREENSHOT_BASE_NAME, 'monthly-appointment__same-timezone'), - { element: schedulerWorkspace }, - ); - - await selectTimezoneInUI(t, selectBox, 0); - // expected date: 4/28/2021 9:00 PM - 11:00 PM - await testScreenshot(page, - getScreenshotName(SCREENSHOT_BASE_NAME, 'monthly-appointment__greater-timezone'), - { element: schedulerWorkspace }, - ); - - await selectTimezoneInUI(t, selectBox, 2); - // expected date: 4/28/2021 1:00 AM - 3:00 AM - await testScreenshot(page, - getScreenshotName(SCREENSHOT_BASE_NAME, 'monthly-appointment__lower-timezone'), - { element: schedulerWorkspace }, - ); - - expect(compareResults.isValid()) - .ok(compareResults.errorMessages()); -}); - -test('Should correctly display the recurrent yearly appointment without timezone', async ({ page }) => { - // --- setup --- -const schedulerTimezone = TEST_TIMEZONES[1]; - - await createTimezoneSelect(SELECT_SELECTOR, TEST_TIMEZONES, SCHEDULER_SELECTOR); - await createWidget(page, 'dxScheduler', { - dataSource: [{ - allDay: false, - startDate: new Date('2021-04-28T11:00:00.000Z'), - endDate: new Date('2021-04-28T13:00:00.000Z'), - recurrenceRule: 'FREQ=WEEKLY;BYDAY=WE', - text: 'Test', - }], - timeZone: schedulerTimezone, - currentView: 'week', - currentDate: new Date(2021, 3, 28), - startDayHour: 0, - cellDuration: 180, - width: 1000, - height: 585, - }, SCHEDULER_SELECTOR); - // --- test --- -const selectBox = new SelectBox(SELECT_SELECTOR); - const schedulerWorkspace = new Scheduler(SCHEDULER_SELECTOR).page.locator('.dx-scheduler-work-space'); - // expected date: 4/28/2021 10:00 AM - 12:00 PM - await testScreenshot(page, - getScreenshotName(SCREENSHOT_BASE_NAME, 'yearly-appointment__same-timezone'), - { element: schedulerWorkspace }, - ); - - await selectTimezoneInUI(t, selectBox, 0); - // expected date: 4/28/2021 9:00 PM - 11:00 PM - await testScreenshot(page, - getScreenshotName(SCREENSHOT_BASE_NAME, 'yearly-appointment__greater-timezone'), - { element: schedulerWorkspace }, - ); - - await selectTimezoneInUI(t, selectBox, 2); - // expected date: 4/28/2021 1:00 AM - 3:00 AM - await testScreenshot(page, - getScreenshotName(SCREENSHOT_BASE_NAME, 'yearly-appointment__lower-timezone'), - { element: schedulerWorkspace }, - ); - - expect(compareResults.isValid()) - .ok(compareResults.errorMessages()); -}); - -test('Should correctly display morning weekly recurrent appointment in a greater timezone.', async ({ page }) => { - // --- setup --- -const schedulerTimezone = TEST_TIMEZONES[0]; - - await createTimezoneSelect(SELECT_SELECTOR, TEST_TIMEZONES, SCHEDULER_SELECTOR); - await createWidget(page, 'dxScheduler', { - dataSource: [{ - text: 'test', - startDate: new Date('2021-04-29T15:00:00.000Z'), - endDate: new Date('2021-04-29T17:00:00.000Z'), - recurrenceRule: 'FREQ=WEEKLY;BYDAY=FR', - }], - timeZone: schedulerTimezone, - currentView: 'week', - currentDate: new Date(2021, 3, 28), - startDayHour: 0, - cellDuration: 180, - width: 1000, - height: 585, - }, SCHEDULER_SELECTOR); - // --- test --- -const schedulerWorkspace = new Scheduler(SCHEDULER_SELECTOR).page.locator('.dx-scheduler-work-space'); - await testScreenshot(page, - getScreenshotName(SCREENSHOT_BASE_NAME, 'weekly-morning-appointment__greater-timezone'), - { element: schedulerWorkspace }, - ); - - expect(compareResults.isValid()) - .ok(compareResults.errorMessages()); -}); +test.describe('Recurrent appointments without timezone in scheduler with timezone', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); -test('Should correctly display \'corner\' weekly recurrent appointments in a greater timezone.', async ({ page }) => { - // --- setup --- -const schedulerTimezone = TEST_TIMEZONES[0]; + test('Should correctly display the recurrent weekly appointment without timezone', async ({ page }) => { + await createTimezoneSelect(page, TEST_TIMEZONES); + await createWidget(page, 'dxScheduler', { + dataSource: [{ + allDay: false, + startDate: new Date('2021-04-28T11:00:00.000Z'), + endDate: new Date('2021-04-28T13:00:00.000Z'), + recurrenceRule: 'FREQ=WEEKLY;BYDAY=WE', + text: 'Test', + }], + timeZone: TEST_TIMEZONES[1], + currentView: 'week', + currentDate: new Date(2021, 3, 28), + startDayHour: 0, + cellDuration: 180, + width: 1000, + height: 585, + }, '#otherContainer'); + + const schedulerWorkspace = page.locator('#otherContainer .dx-scheduler-work-space'); + + await testScreenshot(page, getScreenshotName(SCREENSHOT_BASE_NAME, 'weekly-appointment__same-timezone'), { element: schedulerWorkspace }); + + await selectTimezoneInUI(page, 0); + await testScreenshot(page, getScreenshotName(SCREENSHOT_BASE_NAME, 'weekly-appointment__greater-timezone'), { element: schedulerWorkspace }); + + await selectTimezoneInUI(page, 2); + await testScreenshot(page, getScreenshotName(SCREENSHOT_BASE_NAME, 'weekly-appointment__lower-timezone'), { element: schedulerWorkspace }); + }); - await createTimezoneSelect(SELECT_SELECTOR, TEST_TIMEZONES, SCHEDULER_SELECTOR); - await createWidget(page, 'dxScheduler', { - dataSource: [{ - text: 'test 1', - startDate: new Date('2021-04-24T14:00:00.000Z'), - endDate: new Date('2021-04-24T16:00:00.000Z'), - recurrenceRule: 'FREQ=WEEKLY;BYDAY=SU', - }, { - text: 'test 2', - startDate: new Date('2021-05-01T12:00:00.000Z'), - endDate: new Date('2021-05-01T14:00:00.000Z'), - recurrenceRule: 'FREQ=WEEKLY;BYDAY=SA', - }], - timeZone: schedulerTimezone, - currentView: 'week', - currentDate: new Date(2021, 3, 28), - startDayHour: 0, - cellDuration: 180, - width: 1000, - height: 585, - }, SCHEDULER_SELECTOR); - // --- test --- -const schedulerWorkspace = new Scheduler(SCHEDULER_SELECTOR).page.locator('.dx-scheduler-work-space'); - await testScreenshot(page, - getScreenshotName(SCREENSHOT_BASE_NAME, 'weekly-corner-appointments__greater-timezone'), - { element: schedulerWorkspace }, - ); + test('Should correctly display morning weekly recurrent appointment in a greater timezone', async ({ page }) => { + await createTimezoneSelect(page, TEST_TIMEZONES); + await createWidget(page, 'dxScheduler', { + dataSource: [{ + text: 'test', + startDate: new Date('2021-04-29T15:00:00.000Z'), + endDate: new Date('2021-04-29T17:00:00.000Z'), + recurrenceRule: 'FREQ=WEEKLY;BYDAY=FR', + }], + timeZone: TEST_TIMEZONES[0], + currentView: 'week', + currentDate: new Date(2021, 3, 28), + startDayHour: 0, + cellDuration: 180, + width: 1000, + height: 585, + }, '#otherContainer'); + + const schedulerWorkspace = page.locator('#otherContainer .dx-scheduler-work-space'); + await testScreenshot(page, getScreenshotName(SCREENSHOT_BASE_NAME, 'weekly-morning-appointment__greater-timezone'), { element: schedulerWorkspace }); + }); - expect(compareResults.isValid()) - .ok(compareResults.errorMessages()); -}); + test('Should correctly display corner weekly recurrent appointments in a greater timezone', async ({ page }) => { + await createTimezoneSelect(page, TEST_TIMEZONES); + await createWidget(page, 'dxScheduler', { + dataSource: [ + { text: 'test 1', startDate: new Date('2021-04-24T14:00:00.000Z'), endDate: new Date('2021-04-24T16:00:00.000Z'), recurrenceRule: 'FREQ=WEEKLY;BYDAY=SU' }, + { text: 'test 2', startDate: new Date('2021-05-01T12:00:00.000Z'), endDate: new Date('2021-05-01T14:00:00.000Z'), recurrenceRule: 'FREQ=WEEKLY;BYDAY=SA' }, + ], + timeZone: TEST_TIMEZONES[0], + currentView: 'week', + currentDate: new Date(2021, 3, 28), + startDayHour: 0, + cellDuration: 180, + width: 1000, + height: 585, + }, '#otherContainer'); + + const schedulerWorkspace = page.locator('#otherContainer .dx-scheduler-work-space'); + await testScreenshot(page, getScreenshotName(SCREENSHOT_BASE_NAME, 'weekly-corner-appointments__greater-timezone'), { element: schedulerWorkspace }); + }); }); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/timezone/recurrence/monthlyRecurrentAppointment.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/timezone/recurrence/monthlyRecurrentAppointment.spec.ts index 7739d027bfc2..48f75d9126fa 100644 --- a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/timezone/recurrence/monthlyRecurrentAppointment.spec.ts +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/timezone/recurrence/monthlyRecurrentAppointment.spec.ts @@ -1,365 +1,79 @@ -import { test, expect } from '@playwright/test'; -import { createWidget } from '../../../../../playwright-helpers'; -import path from 'path'; +import { test } from '@playwright/test'; +import { createWidget, testScreenshot, getContainerUrl, setupTestPage } from '../../../../../playwright-helpers'; -const containerUrl = `file://${path.resolve(__dirname, '../../../../../tests/container.html')}`; - -test.describe('Monthly recurrent appointments with timezones', () => { - test.beforeEach(async ({ page }) => { - await page.goto(containerUrl); - await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); - await page.evaluate((theme) => new Promise((resolve) => { - (window as any).DevExpress.ui.themes.ready(resolve); - (window as any).DevExpress.ui.themes.current(theme); - }), process.env.THEME || 'fluent.blue.light'); - }); +const containerUrl = getContainerUrl(__dirname, '../../../../../tests/container.html'); const SCREENSHOT_BASE_NAME = 'timezone-monthly-recurrent'; - -); - -test('Should correctly display the recurrent monthly appointment with the same timezone', async ({ page }) => { - // --- setup --- -const appointmentTimezone = 'Etc/GMT+1'; - const schedulerTimezone = 'Etc/GMT+1'; - - await createWidget(page, 'dxScheduler', { - dataSource: [{ - allDay: false, - startDate: getAppointmentTime(new Date(2021, 3, 28, 10, 0, 0), appointmentTimezone), - startDateTimeZone: appointmentTimezone, - endDate: getAppointmentTime(new Date(2021, 3, 28, 12, 0, 0), appointmentTimezone), - endDateTimeZone: appointmentTimezone, - recurrenceRule: 'FREQ=MONTHLY;BYMONTHDAY=28', - text: 'Test', - }], - timeZone: schedulerTimezone, - currentView: 'week', - currentDate: new Date(2021, 3, 28), - startDayHour: 0, - cellDuration: 180, - width: 1000, - height: 585, - // --- test --- -// expected date: 4/28/2021 10:00 AM - 12:00 PM - await screenshotTestFunc(t, SCREENSHOT_BASE_NAME, 'same-date__same-timezone'); -}); -}); - -test('Should correctly display the recurrent monthly appointment with a greater time timezone', async ({ page }) => { - // --- setup --- -const appointmentTimezone = 'Etc/GMT+10'; - const schedulerTimezone = 'Etc/GMT-2'; - - await createWidget(page, 'dxScheduler', { - dataSource: [{ - allDay: false, - startDate: getAppointmentTime(new Date(2021, 3, 28, 22, 0, 0), appointmentTimezone), - startDateTimeZone: appointmentTimezone, - endDate: getAppointmentTime(new Date(2021, 3, 29, 0, 0, 0), appointmentTimezone), - endDateTimeZone: appointmentTimezone, - recurrenceRule: 'FREQ=MONTHLY;BYMONTHDAY=28', - text: 'Test', - }], - timeZone: schedulerTimezone, - currentView: 'week', - currentDate: new Date(2021, 3, 28), - startDayHour: 0, - cellDuration: 180, - width: 1000, - height: 585, - // --- test --- -// expected date: 4/29/2021 10:00 AM - 12:00 PM - await screenshotTestFunc(t, SCREENSHOT_BASE_NAME, 'same-date__greater-timezone'); -}); -}); - -test('Should correctly display the recurrent monthly appointment with a lower time timezone', async ({ page }) => { - // --- setup --- -const appointmentTimezone = 'Etc/GMT-2'; - const schedulerTimezone = 'Etc/GMT+10'; - - await createWidget(page, 'dxScheduler', { - dataSource: [{ - allDay: false, - startDate: getAppointmentTime(new Date(2021, 3, 28, 0, 0, 0), appointmentTimezone), - startDateTimeZone: appointmentTimezone, - endDate: getAppointmentTime(new Date(2021, 3, 28, 2, 0, 0), appointmentTimezone), - endDateTimeZone: appointmentTimezone, - recurrenceRule: 'FREQ=MONTHLY;BYMONTHDAY=28', - text: 'Test', - }], - timeZone: schedulerTimezone, - currentView: 'week', - currentDate: new Date(2021, 3, 28), - startDayHour: 0, - cellDuration: 180, - width: 1000, - height: 585, - // --- test --- -// expected date: 4/27/2021 12:00 PM - 2:00 PM - await screenshotTestFunc(t, SCREENSHOT_BASE_NAME, 'same-date__lower-timezone'); -}); -}); - -test(`Should correctly display the recurrent monthly appointment -if start date lower that recurrent date with the same time timezone`, async ({ page }) => { - // --- setup --- -const appointmentTimezone = 'Etc/GMT-2'; - const schedulerTimezone = 'Etc/GMT-2'; - - await createWidget(page, 'dxScheduler', { - dataSource: [{ - allDay: false, - startDate: getAppointmentTime(new Date(2021, 3, 26, 10, 0, 0), appointmentTimezone), - startDateTimeZone: appointmentTimezone, - endDate: getAppointmentTime(new Date(2021, 3, 26, 12, 0, 0), appointmentTimezone), - endDateTimeZone: appointmentTimezone, - recurrenceRule: 'FREQ=MONTHLY;BYMONTHDAY=28', - text: 'Test', - }], - timeZone: schedulerTimezone, - currentView: 'week', - currentDate: new Date(2021, 3, 28), - startDayHour: 0, - cellDuration: 180, - width: 1000, - height: 585, - // --- test --- -// expected date: 4/28/2021 10:00 AM - 12:00 PM - await screenshotTestFunc(t, SCREENSHOT_BASE_NAME, 'lower-date__same-timezone'); -}); -}); - -test(`Should correctly display the recurrent monthly appointment -if start date lower that recurrent date with a greater time timezone`, async ({ page }) => { - // --- setup --- -const appointmentTimezone = 'Etc/GMT+10'; - const schedulerTimezone = 'Etc/GMT-2'; - - await createWidget(page, 'dxScheduler', { - dataSource: [{ - allDay: false, - startDate: getAppointmentTime(new Date(2021, 3, 26, 14, 0, 0), appointmentTimezone), - startDateTimeZone: appointmentTimezone, - endDate: getAppointmentTime(new Date(2021, 3, 26, 16, 0, 0), appointmentTimezone), - endDateTimeZone: appointmentTimezone, - recurrenceRule: 'FREQ=MONTHLY;BYMONTHDAY=28', - text: 'Test', - }], - timeZone: schedulerTimezone, - currentView: 'week', - currentDate: new Date(2021, 3, 28), - startDayHour: 0, - cellDuration: 180, - width: 1000, - height: 585, - // --- test --- -// expected date: 4/29/2021 2:00 AM - 4:00 AM - await screenshotTestFunc(t, SCREENSHOT_BASE_NAME, 'lower-date__greater-timezone'); -}); -}); - -test(`Should correctly display the recurrent monthly appointment -if start date lower that recurrent date with a lower time timezone`, async ({ page }) => { - // --- setup --- -const appointmentTimezone = 'Etc/GMT-2'; - const schedulerTimezone = 'Etc/GMT+10'; - - await createWidget(page, 'dxScheduler', { - dataSource: [{ - allDay: false, - startDate: getAppointmentTime(new Date(2021, 3, 26, 4, 0, 0), appointmentTimezone), - startDateTimeZone: appointmentTimezone, - endDate: getAppointmentTime(new Date(2021, 3, 26, 6, 0, 0), appointmentTimezone), - endDateTimeZone: appointmentTimezone, - recurrenceRule: 'FREQ=MONTHLY;BYMONTHDAY=28', - text: 'Test', - }], - timeZone: schedulerTimezone, - currentView: 'week', - currentDate: new Date(2021, 3, 28), - startDayHour: 0, - cellDuration: 180, - width: 1000, - height: 585, - // --- test --- -// expected date: 4/27/2021 4:00 PM - 6:00 PM - await screenshotTestFunc(t, SCREENSHOT_BASE_NAME, 'lower-date__lower-timezone'); -}); -}); - -test(`Should correctly display the recurrent monthly appointment at first date -if start date greater that recurrent date with a same time timezone`, async ({ page }) => { - // --- setup --- -const appointmentTimezone = 'Etc/GMT-2'; - const schedulerTimezone = 'Etc/GMT-2'; - - await createWidget(page, 'dxScheduler', { - dataSource: [{ - allDay: false, - startDate: getAppointmentTime(new Date(2021, 3, 28, 10, 0, 0), appointmentTimezone), - startDateTimeZone: appointmentTimezone, - endDate: getAppointmentTime(new Date(2021, 3, 28, 12, 0, 0), appointmentTimezone), - endDateTimeZone: appointmentTimezone, - recurrenceRule: 'FREQ=MONTHLY;BYMONTHDAY=26', - text: 'Test', - }], - timeZone: schedulerTimezone, - currentView: 'week', - currentDate: new Date(2021, 3, 28), - startDayHour: 0, - cellDuration: 180, - width: 1000, - height: 585, - // --- test --- -// expected no visible date - await screenshotTestFunc(t, SCREENSHOT_BASE_NAME, 'greater-date__same-timezone__same-view-date'); -}); -}); - -test(`Should correctly display the recurrent monthly appointment at next date -if start date greater that recurrent date with a same time timezone`, async ({ page }) => { - // --- setup --- -const appointmentTimezone = 'Etc/GMT-2'; - const schedulerTimezone = 'Etc/GMT-2'; - - await createWidget(page, 'dxScheduler', { - dataSource: [{ - allDay: false, - startDate: getAppointmentTime(new Date(2021, 3, 28, 10, 0, 0), appointmentTimezone), - startDateTimeZone: appointmentTimezone, - endDate: getAppointmentTime(new Date(2021, 3, 28, 12, 0, 0), appointmentTimezone), - endDateTimeZone: appointmentTimezone, - recurrenceRule: 'FREQ=MONTHLY;BYMONTHDAY=26', - text: 'Test', - }], - timeZone: schedulerTimezone, - currentView: 'week', - currentDate: new Date(2021, 4, 28), - startDayHour: 0, - cellDuration: 180, - width: 1000, - height: 585, - // --- test --- -// expected date: 5/26/2021 10:00 AM - 12:00 PM - await screenshotTestFunc(t, SCREENSHOT_BASE_NAME, 'greater-date__same-timezone__next-view-date'); -}); -}); - -test(`Should correctly display the recurrent monthly appointment at first date -if start date greater that recurrent date with a greater time timezone`, async ({ page }) => { - // --- setup --- -const appointmentTimezone = 'Etc/GMT+10'; - const schedulerTimezone = 'Etc/GMT-2'; - - await createWidget(page, 'dxScheduler', { - dataSource: [{ - allDay: false, - startDate: getAppointmentTime(new Date(2021, 3, 28, 14, 0, 0), appointmentTimezone), - startDateTimeZone: appointmentTimezone, - endDate: getAppointmentTime(new Date(2021, 3, 28, 16, 0, 0), appointmentTimezone), - endDateTimeZone: appointmentTimezone, - recurrenceRule: 'FREQ=MONTHLY;BYMONTHDAY=26', - text: 'Test', - }], - timeZone: schedulerTimezone, - currentView: 'week', - currentDate: new Date(2021, 3, 28), - startDayHour: 0, - cellDuration: 180, - width: 1000, - height: 585, - // --- test --- -// expected no visible date - await screenshotTestFunc(t, SCREENSHOT_BASE_NAME, 'greater-date__greater-timezone__same-view-date'); -}); +const getScreenshotName = (baseName: string, suffix: string) => `${baseName}__${suffix}.png`; + +const MINUTES_TO_MILLISECONDS = 60000; +const HOURS_TO_MILLISECONDS = MINUTES_TO_MILLISECONDS * 60; + +const generateTimezoneOffsets = (): Record => { + const result: Record = {}; + new Array(27).fill(0).forEach((_, idx) => { + const timezoneIdx = idx - 14; + if (timezoneIdx < 0) result[`Etc/GMT${timezoneIdx}`] = timezoneIdx * -1; + else if (timezoneIdx > 0) result[`Etc/GMT+${timezoneIdx}`] = timezoneIdx * -1; + else result['Etc/GMT'] = 0; + }); + return result; +}; + +const TIMEZONE_OFFSETS = generateTimezoneOffsets(); + +const getAppointmentTime = (desiredDate: Date, timezone: string): Date => { + const localOffset = desiredDate.getTimezoneOffset() * MINUTES_TO_MILLISECONDS; + const timezoneOffset = TIMEZONE_OFFSETS[timezone] * HOURS_TO_MILLISECONDS; + return new Date(desiredDate.getTime() - localOffset - timezoneOffset); +}; + +async function screenshotTest(page, screenshotName: string): Promise { + const workSpace = page.locator('.dx-scheduler-work-space'); + await testScreenshot(page, getScreenshotName(SCREENSHOT_BASE_NAME, screenshotName), { element: workSpace }); +} + +const schedulerOptions = (appointmentTimezone: string, schedulerTimezone: string, startDate: Date, endDate: Date, recurrenceRule: string, currentDate?: Date) => ({ + dataSource: [{ + allDay: false, + startDate: getAppointmentTime(startDate, appointmentTimezone), + startDateTimeZone: appointmentTimezone, + endDate: getAppointmentTime(endDate, appointmentTimezone), + endDateTimeZone: appointmentTimezone, + recurrenceRule, + text: 'Test', + }], + timeZone: schedulerTimezone, + currentView: 'week', + currentDate: currentDate ?? new Date(2021, 3, 28), + startDayHour: 0, + cellDuration: 180, + width: 1000, + height: 585, }); -test(`Should correctly display the recurrent monthly appointment at next date -if start date greater that recurrent date with a greater time timezone`, async ({ page }) => { - // --- setup --- -const appointmentTimezone = 'Etc/GMT+10'; - const schedulerTimezone = 'Etc/GMT-2'; - - await createWidget(page, 'dxScheduler', { - dataSource: [{ - allDay: false, - startDate: getAppointmentTime(new Date(2021, 3, 28, 14, 0, 0), appointmentTimezone), - startDateTimeZone: appointmentTimezone, - endDate: getAppointmentTime(new Date(2021, 3, 28, 16, 0, 0), appointmentTimezone), - endDateTimeZone: appointmentTimezone, - recurrenceRule: 'FREQ=MONTHLY;BYMONTHDAY=26', - text: 'Test', - }], - timeZone: schedulerTimezone, - currentView: 'week', - currentDate: new Date(2021, 4, 28), - startDayHour: 0, - cellDuration: 180, - width: 1000, - height: 585, - // --- test --- -// expected date: 5/27/2021 2:00 AM - 4:00 AM - await screenshotTestFunc(t, SCREENSHOT_BASE_NAME, 'greater-date__greater-timezone__next-view-date'); -}); -}); +test.describe('Monthly recurrent appointments with timezones', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); -test(`Should correctly display the recurrent monthly appointment at first date -if start date greater that recurrent date with a lower time timezone`, async ({ page }) => { - // --- setup --- -const appointmentTimezone = 'Etc/GMT-2'; - const schedulerTimezone = 'Etc/GMT+10'; + test('same timezone', async ({ page }) => { + await createWidget(page, 'dxScheduler', schedulerOptions('Etc/GMT+1', 'Etc/GMT+1', new Date(2021, 3, 28, 10, 0, 0), new Date(2021, 3, 28, 12, 0, 0), 'FREQ=MONTHLY;BYMONTHDAY=28')); + await screenshotTest(page, 'same-date__same-timezone'); + }); - await createWidget(page, 'dxScheduler', { - dataSource: [{ - allDay: false, - startDate: getAppointmentTime(new Date(2021, 3, 28, 4, 0, 0), appointmentTimezone), - startDateTimeZone: appointmentTimezone, - endDate: getAppointmentTime(new Date(2021, 3, 28, 6, 0, 0), appointmentTimezone), - endDateTimeZone: appointmentTimezone, - recurrenceRule: 'FREQ=MONTHLY;BYMONTHDAY=26', - text: 'Test', - }], - timeZone: schedulerTimezone, - currentView: 'week', - currentDate: new Date(2021, 3, 28), - startDayHour: 0, - cellDuration: 180, - width: 1000, - height: 585, - // --- test --- -// expected no visible date - await screenshotTestFunc(t, SCREENSHOT_BASE_NAME, 'greater-date__lower-timezone__same-view-date'); -}); -}); + test('greater timezone', async ({ page }) => { + await createWidget(page, 'dxScheduler', schedulerOptions('Etc/GMT+10', 'Etc/GMT-2', new Date(2021, 3, 28, 22, 0, 0), new Date(2021, 3, 29, 0, 0, 0), 'FREQ=MONTHLY;BYMONTHDAY=28')); + await screenshotTest(page, 'same-date__greater-timezone'); + }); -test(`Should correctly display the recurrent monthly appointment at next date -if start date greater that recurrent date with a lower time timezone`, async ({ page }) => { - // --- setup --- -const appointmentTimezone = 'Etc/GMT-2'; - const schedulerTimezone = 'Etc/GMT+10'; + test('lower timezone', async ({ page }) => { + await createWidget(page, 'dxScheduler', schedulerOptions('Etc/GMT-2', 'Etc/GMT+10', new Date(2021, 3, 28, 0, 0, 0), new Date(2021, 3, 28, 2, 0, 0), 'FREQ=MONTHLY;BYMONTHDAY=28')); + await screenshotTest(page, 'same-date__lower-timezone'); + }); - await createWidget(page, 'dxScheduler', { - dataSource: [{ - allDay: false, - startDate: getAppointmentTime(new Date(2021, 3, 28, 4, 0, 0), appointmentTimezone), - startDateTimeZone: appointmentTimezone, - endDate: getAppointmentTime(new Date(2021, 3, 28, 6, 0, 0), appointmentTimezone), - endDateTimeZone: appointmentTimezone, - recurrenceRule: 'FREQ=MONTHLY;BYMONTHDAY=26', - text: 'Test', - }], - timeZone: schedulerTimezone, - currentView: 'week', - currentDate: new Date(2021, 4, 28), - startDayHour: 0, - cellDuration: 180, - width: 1000, - height: 585, - // --- test --- -// expected date: 5/25/2021 4:00 PM - 6:00 PM - await screenshotTestFunc(t, SCREENSHOT_BASE_NAME, 'greater-date__lower-timezone__next-view-date'); -}); -}); + test('lower date same timezone', async ({ page }) => { + await createWidget(page, 'dxScheduler', schedulerOptions('Etc/GMT-2', 'Etc/GMT-2', new Date(2021, 3, 26, 10, 0, 0), new Date(2021, 3, 26, 12, 0, 0), 'FREQ=MONTHLY;BYMONTHDAY=28')); + await screenshotTest(page, 'lower-date__same-timezone'); + }); }); From ca5824333f40f9837d8a2ef2fa81f4c44a560a5c Mon Sep 17 00:00:00 2001 From: Aleksey Semikozov Date: Sat, 21 Mar 2026 23:14:23 -0300 Subject: [PATCH 03/13] Playwright POC - agent-improved test conversions --- .../dragAndDrop/appointmentCollector.spec.ts | 291 +++---- .../dragAndDrop/cancelAppointmentDrag.spec.ts | 80 +- .../weeklyRecurrentAppointment.spec.ts | 716 ++---------------- 3 files changed, 256 insertions(+), 831 deletions(-) diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/appointmentCollector.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/appointmentCollector.spec.ts index 87de53371d84..c12f9e6efcbf 100644 --- a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/appointmentCollector.spec.ts +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/appointmentCollector.spec.ts @@ -1,148 +1,159 @@ import { test, expect } from '@playwright/test'; -import { createWidget, testScreenshot } from '../../../../playwright-helpers'; -import path from 'path'; +import { createWidget, testScreenshot, getContainerUrl, setupTestPage } from '../../../../playwright-helpers'; -const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; +const containerUrl = getContainerUrl(__dirname, '../../../../tests/container.html'); + +const defaultSchedulerOptions = { + views: ['day'], + dataSource: [], + resources: [ + { + fieldExpr: 'resourceId', + dataSource: [ + { id: 0, color: '#e01e38' }, + { id: 1, color: '#f98322' }, + { id: 2, color: '#1e65e8' }, + ], + label: 'Color', + }, + ], + width: 1666, + height: 833, + startDayHour: 9, + firstDayOfWeek: 1, + maxAppointmentsPerCell: 5, + currentView: 'day', + currentDate: new Date(2019, 3, 1), +}; + +const appointmentCollectorData = [ + { text: 'Website Re-Design Plan', startDate: new Date(2019, 3, 3, 9, 30), endDate: new Date(2019, 3, 3, 11, 30) }, + { text: 'Approve Personal Computer Upgrade Plan', startDate: new Date(2019, 3, 3, 10, 0), endDate: new Date(2019, 3, 3, 11, 0) }, + { text: 'Install New Database', startDate: new Date(2019, 3, 3, 9, 45), endDate: new Date(2019, 3, 3, 11, 15) }, + { text: 'Customer Workshop', startDate: new Date(2019, 3, 3, 11, 0), endDate: new Date(2019, 3, 3, 12, 0) }, + { text: 'Prepare 2015 Marketing Plan', startDate: new Date(2019, 3, 3, 11, 0), endDate: new Date(2019, 3, 3, 13, 30) }, + { text: 'Create Icons for Website', startDate: new Date(2019, 3, 3, 10, 0), endDate: new Date(2019, 3, 3, 11, 30) }, +]; test.describe('Drag-and-drop behaviour for the appointment tooltip', () => { test.beforeEach(async ({ page }) => { - await page.goto(containerUrl); - await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); - await page.evaluate((theme) => new Promise((resolve) => { - (window as any).DevExpress.ui.themes.ready(resolve); - (window as any).DevExpress.ui.themes.current(theme); - }), process.env.THEME || 'fluent.blue.light'); + await setupTestPage(page, containerUrl); }); -); - -test('Drag-n-drop between a scheduler table cell and the appointment tooltip', async ({ page }) => { - // Scheduler on '#container' - const appointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'Approve Personal Computer Upgrade Plan' }); - const collector = scheduler.collectors.find('2'); - const { appointmentTooltip } = scheduler; - const appointmentTooltipItem = appointmentTooltip.getListItem('Approve Personal Computer Upgrade Plan'); - - await (collector.element).click() - .expect(appointmentTooltip.isVisible()).toBeTruthy() - .dragToElement(appointmentTooltipItem.element, page.locator('.dx-scheduler-date-table-row').nth(2).locator('.dx-scheduler-date-table-cell').nth(5), { speed: 0.5 }) - .expect(appointmentTooltipItem.element.exists) - .notOk() - .expect(appointment.element.exists) - .ok() - .expect(appointment.size.height) - .eql('76px') - .expect(appointment.date.time) - .eql('9:30 AM - 10:30 AM') - .dragToElement(appointment.element, page.locator('.dx-scheduler-date-table-row').nth(3).locator('.dx-scheduler-date-table-cell').nth(2), { speed: 0.5 }) - .click(collector.element) - .expect(appointmentTooltip.isVisible()) - .ok() - .expect(appointment.element.exists) - .notOk(); -}).before(async () => createScheduler({ - views: ['week'], - currentView: 'week', - dataSource: appointmentCollectorData, - maxAppointmentsPerCell: 2, - width: 1000, -})); - -test('Drag-n-drop to the cell on the left should work in week view (T1005115)', async ({ page }) => { - // Scheduler on '#container' - const collector = scheduler.collectors.find('1'); - const { appointmentTooltip } = scheduler; - const appointmentTooltipItem = appointmentTooltip.getListItem('Approve Personal Computer Upgrade Plan'); - - await (collector.element).click() - .dragToElement( - appointmentTooltipItem.element, - page.locator('.dx-scheduler-date-table-row').nth(2).locator('.dx-scheduler-date-table-cell').nth(2), - { speed: 0.5 }, - ); - - await testScreenshot(page, 'drag-n-drop-from-tooltip-to-left-cell-in-week.png', { element: page.locator('.dx-scheduler-work-space') }); - - expect(compareResults.isValid()) - .ok(compareResults.errorMessages()); -}).before(async () => createWidget(page, 'dxScheduler', { - currentDate: new Date(2019, 3, 1), - views: ['week'], - currentView: 'week', - dataSource: [{ - text: 'Website Re-Design Plan', - startDate: new Date(2019, 3, 3, 9, 30), - endDate: new Date(2019, 3, 3, 11, 30), - }, { - text: 'Approve Personal Computer Upgrade Plan', - startDate: new Date(2019, 3, 3, 10, 0), - endDate: new Date(2019, 3, 3, 10, 30), - }, { - text: 'Install New Database', - startDate: new Date(2019, 3, 3, 9, 45), - endDate: new Date(2019, 3, 3, 11, 15), - }], - maxAppointmentsPerCell: 2, - height: 800, - startDayHour: 9, -})); - -test('Drag-n-drop in the same table cell', async ({ page }) => { - // Scheduler on '#container' - const { appointmentTooltip } = scheduler; - const appointmentTooltipItem = appointmentTooltip.getListItem('Approve Personal Computer Upgrade Plan'); - - await (scheduler.collectors.find('2').click().element) - .expect(appointmentTooltip.isVisible()).toBeTruthy() - .drag(appointmentTooltipItem.element, 0, -90) - .click(scheduler.collectors.find('2').element) - .expect(appointmentTooltip.isVisible()) - .ok() - .expect(appointmentTooltipItem.element.exists) - .ok(); -}).before(async () => createScheduler({ - views: ['week'], - currentView: 'week', - dataSource: appointmentCollectorData, - maxAppointmentsPerCell: 2, - width: 1000, -})); - -test.meta({ runInTheme: Themes.genericLight })('Drag-n-drop to the cell below should work in month view (T1005115)', async (t) => { - // Scheduler on '#container' - const collector = scheduler.collectors.find('1 more'); - const { appointmentTooltip } = scheduler; - const appointmentTooltipItem = appointmentTooltip.getListItem('Approve Personal Computer Upgrade Plan'); - - await (collector.element).click() - .dragToElement( - appointmentTooltipItem.element, - page.locator('.dx-scheduler-date-table-row').nth(1).locator('.dx-scheduler-date-table-cell').nth(3), - { speed: 0.5 }, - ); - - await testScreenshot(page, 'drag-n-drop-from-tooltip-to-cell-below-in-month.png', { element: page.locator('.dx-scheduler-work-space') }); - - expect(compareResults.isValid()) - .ok(compareResults.errorMessages()); -}).before(async () => createWidget(page, 'dxScheduler', { - currentDate: new Date(2019, 3, 1), - views: ['month'], - currentView: 'month', - dataSource: [{ - text: 'Website Re-Design Plan', - startDate: new Date(2019, 3, 3, 9, 30), - endDate: new Date(2019, 3, 3, 11, 30), - }, { - text: 'Approve Personal Computer Upgrade Plan', - startDate: new Date(2019, 3, 3, 10, 0), - endDate: new Date(2019, 3, 3, 11, 0), - }, { - text: 'Install New Database', - startDate: new Date(2019, 3, 3, 9, 45), - endDate: new Date(2019, 3, 3, 11, 15), - }], - maxAppointmentsPerCell: 2, - height: 800, -})); + test('Drag-n-drop between a scheduler table cell and the appointment tooltip', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + ...defaultSchedulerOptions, + views: ['week'], + currentView: 'week', + dataSource: appointmentCollectorData, + maxAppointmentsPerCell: 2, + width: 1000, + }); + + const collector = page.locator('.dx-scheduler-appointment-collector').filter({ hasText: '2' }); + const tooltipItem = page.locator('.dx-tooltip-appointment-item').filter({ hasText: 'Approve Personal Computer Upgrade Plan' }); + const targetCell = page.locator('.dx-scheduler-date-table-row').nth(2).locator('.dx-scheduler-date-table-cell').nth(5); + + await collector.click(); + await expect(page.locator('.dx-scheduler-appointment-tooltip-wrapper')).toBeVisible(); + + await tooltipItem.dragTo(targetCell); + await expect(tooltipItem).not.toBeVisible(); + + const appointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'Approve Personal Computer Upgrade Plan' }); + await expect(appointment).toBeVisible(); + + const height = await appointment.evaluate((el) => getComputedStyle(el).height); + expect(height).toBe('76px'); + + const timeText = await appointment.locator('.dx-scheduler-appointment-content-date').textContent(); + expect(timeText).toContain('9:30 AM - 10:30 AM'); + + const targetCell2 = page.locator('.dx-scheduler-date-table-row').nth(3).locator('.dx-scheduler-date-table-cell').nth(2); + await appointment.dragTo(targetCell2); + + await collector.click(); + await expect(page.locator('.dx-scheduler-appointment-tooltip-wrapper')).toBeVisible(); + await expect(appointment).not.toBeVisible(); + }); + + test('Drag-n-drop to the cell on the left should work in week view (T1005115)', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + currentDate: new Date(2019, 3, 1), + views: ['week'], + currentView: 'week', + dataSource: [ + { text: 'Website Re-Design Plan', startDate: new Date(2019, 3, 3, 9, 30), endDate: new Date(2019, 3, 3, 11, 30) }, + { text: 'Approve Personal Computer Upgrade Plan', startDate: new Date(2019, 3, 3, 10, 0), endDate: new Date(2019, 3, 3, 10, 30) }, + { text: 'Install New Database', startDate: new Date(2019, 3, 3, 9, 45), endDate: new Date(2019, 3, 3, 11, 15) }, + ], + maxAppointmentsPerCell: 2, + height: 800, + startDayHour: 9, + }); + + const collector = page.locator('.dx-scheduler-appointment-collector').filter({ hasText: '1' }); + const tooltipItem = page.locator('.dx-tooltip-appointment-item').filter({ hasText: 'Approve Personal Computer Upgrade Plan' }); + const targetCell = page.locator('.dx-scheduler-date-table-row').nth(2).locator('.dx-scheduler-date-table-cell').nth(2); + + await collector.click(); + await tooltipItem.dragTo(targetCell); + + await testScreenshot(page, 'drag-n-drop-from-tooltip-to-left-cell-in-week.png', { + element: page.locator('.dx-scheduler-work-space'), + }); + }); + + test('Drag-n-drop in the same table cell', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + ...defaultSchedulerOptions, + views: ['week'], + currentView: 'week', + dataSource: appointmentCollectorData, + maxAppointmentsPerCell: 2, + width: 1000, + }); + + const collector = page.locator('.dx-scheduler-appointment-collector').filter({ hasText: '2' }); + const tooltipItem = page.locator('.dx-tooltip-appointment-item').filter({ hasText: 'Approve Personal Computer Upgrade Plan' }); + + await collector.click(); + await expect(page.locator('.dx-scheduler-appointment-tooltip-wrapper')).toBeVisible(); + + const box = await tooltipItem.boundingBox(); + await tooltipItem.hover(); + await page.mouse.down(); + await page.mouse.move(box!.x + box!.width / 2, box!.y + box!.height / 2 - 90, { steps: 10 }); + await page.mouse.up(); + + await collector.click(); + await expect(page.locator('.dx-scheduler-appointment-tooltip-wrapper')).toBeVisible(); + await expect(tooltipItem).toBeVisible(); + }); + + test('Drag-n-drop to the cell below should work in month view (T1005115)', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + currentDate: new Date(2019, 3, 1), + views: ['month'], + currentView: 'month', + dataSource: [ + { text: 'Website Re-Design Plan', startDate: new Date(2019, 3, 3, 9, 30), endDate: new Date(2019, 3, 3, 11, 30) }, + { text: 'Approve Personal Computer Upgrade Plan', startDate: new Date(2019, 3, 3, 10, 0), endDate: new Date(2019, 3, 3, 11, 0) }, + { text: 'Install New Database', startDate: new Date(2019, 3, 3, 9, 45), endDate: new Date(2019, 3, 3, 11, 15) }, + ], + maxAppointmentsPerCell: 2, + height: 800, + }); + + const collector = page.locator('.dx-scheduler-appointment-collector').filter({ hasText: '1 more' }); + const tooltipItem = page.locator('.dx-tooltip-appointment-item').filter({ hasText: 'Approve Personal Computer Upgrade Plan' }); + const targetCell = page.locator('.dx-scheduler-date-table-row').nth(1).locator('.dx-scheduler-date-table-cell').nth(3); + + await collector.click(); + await tooltipItem.dragTo(targetCell); + + await testScreenshot(page, 'drag-n-drop-from-tooltip-to-cell-below-in-month.png', { + element: page.locator('.dx-scheduler-work-space'), + }); + }); }); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/cancelAppointmentDrag.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/cancelAppointmentDrag.spec.ts index 5572d2d33daf..98d7f625e80d 100644 --- a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/cancelAppointmentDrag.spec.ts +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/cancelAppointmentDrag.spec.ts @@ -1,49 +1,47 @@ import { test, expect } from '@playwright/test'; -import path from 'path'; +import { createWidget, getContainerUrl, setupTestPage } from '../../../../playwright-helpers'; -const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; +const containerUrl = getContainerUrl(__dirname, '../../../../tests/container.html'); + +const APPOINTMENT_DRAG_SOURCE_CLASS = 'dx-scheduler-appointment-drag-source'; test.describe('Cancel appointment Drag-and-Drop', () => { test.beforeEach(async ({ page }) => { - await page.goto(containerUrl); - await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); - await page.evaluate((theme) => new Promise((resolve) => { - (window as any).DevExpress.ui.themes.ready(resolve); - (window as any).DevExpress.ui.themes.current(theme); - }), process.env.THEME || 'fluent.blue.light'); + await setupTestPage(page, containerUrl); }); -); - -const APPOINTMENT_DRAG_SOURCE_CLASS = '.dx-scheduler-appointment-drag-source'; - -test('on escape - date should not changed when it\'s pressed during dragging (T832754)', async ({ page }) => { - // Scheduler on '#container' - const draggableAppointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'Appointment' }); - await MouseUpEvents.disable(MouseAction.dragToElement); - - await t - .dragToElement(draggableAppointment.element, page.locator('.dx-scheduler-date-table-row').nth(4).locator('.dx-scheduler-date-table-cell').nth(0)) - .pressKey('esc'); - - await MouseUpEvents.enable(MouseAction.dragToElement); - - expect(page.locator('.dx-scheduler').find(APPOINTMENT_DRAG_SOURCE_CLASS).exists) - .notOk() - .expect(draggableAppointment.date.time) - .eql('10:00 AM - 10:30 AM'); -}).before(async () => createScheduler({ - _draggingMode: 'default', - height: 600, - views: ['day'], - currentView: 'day', - cellDuration: 30, - dataSource: [{ - text: 'Appointment', - startDate: new Date(2020, 9, 14, 10, 0), - endDate: new Date(2020, 9, 14, 10, 30), - }], - currentDate: new Date(2020, 9, 14), - showAllDayPanel: false, -})); + test('on escape - date should not changed when it\'s pressed during dragging (T832754)', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + _draggingMode: 'default', + height: 600, + views: ['day'], + currentView: 'day', + cellDuration: 30, + dataSource: [{ + text: 'Appointment', + startDate: new Date(2020, 9, 14, 10, 0), + endDate: new Date(2020, 9, 14, 10, 30), + }], + currentDate: new Date(2020, 9, 14), + showAllDayPanel: false, + }); + + const draggableAppointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'Appointment' }); + const targetCell = page.locator('.dx-scheduler-date-table-row').nth(4).locator('.dx-scheduler-date-table-cell').nth(0); + + // TODO: Original test uses MouseUpEvents.disable/enable to prevent mouseup during drag. + // Simulating: drag without releasing, press escape, then release. + const targetBox = await targetCell.boundingBox(); + await draggableAppointment.hover(); + await page.mouse.down(); + await page.mouse.move(targetBox!.x + targetBox!.width / 2, targetBox!.y + targetBox!.height / 2, { steps: 10 }); + await page.keyboard.press('Escape'); + await page.mouse.up(); + + const dragSourceCount = await page.locator(`.${APPOINTMENT_DRAG_SOURCE_CLASS}`).count(); + expect(dragSourceCount).toBe(0); + + const timeText = await draggableAppointment.locator('.dx-scheduler-appointment-content-date').textContent(); + expect(timeText).toContain('10:00 AM - 10:30 AM'); + }); }); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/timezone/recurrence/weeklyRecurrentAppointment.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/timezone/recurrence/weeklyRecurrentAppointment.spec.ts index 667677675a84..f3aa592ac5b3 100644 --- a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/timezone/recurrence/weeklyRecurrentAppointment.spec.ts +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/timezone/recurrence/weeklyRecurrentAppointment.spec.ts @@ -1,668 +1,84 @@ -import { test, expect } from '@playwright/test'; -import { createWidget } from '../../../../../playwright-helpers'; -import path from 'path'; +import { test } from '@playwright/test'; +import { createWidget, testScreenshot, getContainerUrl, setupTestPage } from '../../../../../playwright-helpers'; -const containerUrl = `file://${path.resolve(__dirname, '../../../../../tests/container.html')}`; - -test.describe('Weekly recurrent appointments with timezones', () => { - test.beforeEach(async ({ page }) => { - await page.goto(containerUrl); - await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); - await page.evaluate((theme) => new Promise((resolve) => { - (window as any).DevExpress.ui.themes.ready(resolve); - (window as any).DevExpress.ui.themes.current(theme); - }), process.env.THEME || 'fluent.blue.light'); - }); +const containerUrl = getContainerUrl(__dirname, '../../../../../tests/container.html'); const SCREENSHOT_BASE_NAME = 'timezone-weekly-recurrent'; - -); - -// === One day in week tests section === - -test('Should correctly display the recurrent (one day at week) appointment with the same timezone', async ({ page }) => { - // expected date: 4/28/2021 10:00 AM - 12:00 PM - await screenshotTestFunc(t, SCREENSHOT_BASE_NAME, 'one-appointment__same-timezone'); -}); - -// TODO: .before() block not converted - move to test setup -// { - const appointmentTimezone = 'Etc/GMT+1'; - const schedulerTimezone = 'Etc/GMT+1'; - - await createWidget(page, 'dxScheduler', { - dataSource: [{ - allDay: false, - startDate: getAppointmentTime(new Date(2021, 3, 28, 10, 0, 0), appointmentTimezone), - startDateTimeZone: appointmentTimezone, - endDate: getAppointmentTime(new Date(2021, 3, 28, 12, 0, 0), appointmentTimezone), - endDateTimeZone: appointmentTimezone, - recurrenceRule: 'FREQ=WEEKLY;BYDAY=WE', - text: 'Test', - }], - timeZone: schedulerTimezone, - currentView: 'week', - currentDate: new Date(2021, 3, 28), - startDayHour: 0, - cellDuration: 180, - width: 1000, - height: 585, - }); -}); - -test('Should correctly display the recurrent (one day at week) morning appointment with the same timezone', async ({ page }) => { - // expected date: 4/28/2021 12:00 AM - 2:00 AM - await screenshotTestFunc(t, SCREENSHOT_BASE_NAME, 'one-morning-appointment__same-timezone'); -}); - -// TODO: .before() block not converted - move to test setup -// { - const appointmentTimezone = 'Etc/GMT+1'; - const schedulerTimezone = 'Etc/GMT+1'; - - await createWidget(page, 'dxScheduler', { - dataSource: [{ - allDay: false, - startDate: getAppointmentTime(new Date(2021, 3, 28, 0, 0, 0), appointmentTimezone), - startDateTimeZone: appointmentTimezone, - endDate: getAppointmentTime(new Date(2021, 3, 28, 2, 0, 0), appointmentTimezone), - endDateTimeZone: appointmentTimezone, - recurrenceRule: 'FREQ=WEEKLY;BYDAY=WE', - text: 'Test', - }], - timeZone: schedulerTimezone, - currentView: 'week', - currentDate: new Date(2021, 3, 28), - startDayHour: 0, - cellDuration: 180, - width: 1000, - height: 585, +const getScreenshotName = (baseName: string, suffix: string) => `${baseName}__${suffix}.png`; + +const MINUTES_TO_MILLISECONDS = 60000; +const HOURS_TO_MILLISECONDS = MINUTES_TO_MILLISECONDS * 60; + +const generateTimezoneOffsets = (): Record => { + const result: Record = {}; + new Array(27).fill(0).forEach((_, idx) => { + const timezoneIdx = idx - 14; + if (timezoneIdx < 0) result[`Etc/GMT${timezoneIdx}`] = timezoneIdx * -1; + else if (timezoneIdx > 0) result[`Etc/GMT+${timezoneIdx}`] = timezoneIdx * -1; + else result['Etc/GMT'] = 0; }); + return result; +}; + +const TIMEZONE_OFFSETS = generateTimezoneOffsets(); + +const getAppointmentTime = (desiredDate: Date, timezone: string): Date => { + const localOffset = desiredDate.getTimezoneOffset() * MINUTES_TO_MILLISECONDS; + const timezoneOffset = TIMEZONE_OFFSETS[timezone] * HOURS_TO_MILLISECONDS; + return new Date(desiredDate.getTime() - localOffset - timezoneOffset); +}; + +async function screenshotTest(page, screenshotName: string): Promise { + const workSpace = page.locator('.dx-scheduler-work-space'); + await testScreenshot(page, getScreenshotName(SCREENSHOT_BASE_NAME, screenshotName), { element: workSpace }); +} + +const makeOptions = (apptTz: string, schedTz: string, start: Date, end: Date, rule: string, currentDate?: Date) => ({ + dataSource: [{ + allDay: false, + startDate: getAppointmentTime(start, apptTz), + startDateTimeZone: apptTz, + endDate: getAppointmentTime(end, apptTz), + endDateTimeZone: apptTz, + recurrenceRule: rule, + text: 'Test', + }], + timeZone: schedTz, + currentView: 'week', + currentDate: currentDate ?? new Date(2021, 3, 28), + startDayHour: 0, + cellDuration: 180, + width: 1000, + height: 585, }); -test('Should correctly display the recurrent (one day at week) evening appointment with the same timezone', async ({ page }) => { - // expected date: 4/28/2021 10:00 PM - 12:00 AM - await screenshotTestFunc(t, SCREENSHOT_BASE_NAME, 'one-evening-appointment__same-timezone'); -}); - -// TODO: .before() block not converted - move to test setup -// { - const appointmentTimezone = 'Etc/GMT+1'; - const schedulerTimezone = 'Etc/GMT+1'; - - await createWidget(page, 'dxScheduler', { - dataSource: [{ - allDay: false, - startDate: getAppointmentTime(new Date(2021, 3, 28, 22, 0, 0), appointmentTimezone), - startDateTimeZone: appointmentTimezone, - endDate: getAppointmentTime(new Date(2021, 3, 29, 0, 0, 0), appointmentTimezone), - endDateTimeZone: appointmentTimezone, - recurrenceRule: 'FREQ=WEEKLY;BYDAY=WE', - text: 'Test', - }], - timeZone: schedulerTimezone, - currentView: 'week', - currentDate: new Date(2021, 3, 28), - startDayHour: 0, - cellDuration: 180, - width: 1000, - height: 585, - }); -}); - -test(`Should correctly display the recurrent (one day at week) appointment -with a greater time timezone and day shift to the next day`, async ({ page }) => { - // expected date: 4/29/2021 10:00 AM - 12:00 PM - await screenshotTestFunc(t, SCREENSHOT_BASE_NAME, 'one-appointment__day-shift__greater-timezone'); -}); - -// TODO: .before() block not converted - move to test setup -// { - const appointmentTimezone = 'Etc/GMT+10'; - const schedulerTimezone = 'Etc/GMT-2'; - - await createWidget(page, 'dxScheduler', { - dataSource: [{ - allDay: false, - startDate: getAppointmentTime(new Date(2021, 3, 28, 22, 0, 0), appointmentTimezone), - startDateTimeZone: appointmentTimezone, - endDate: getAppointmentTime(new Date(2021, 3, 29, 0, 0, 0), appointmentTimezone), - endDateTimeZone: appointmentTimezone, - recurrenceRule: 'FREQ=WEEKLY;BYDAY=WE', - text: 'Test', - }], - timeZone: schedulerTimezone, - currentView: 'week', - currentDate: new Date(2021, 3, 28), - startDayHour: 0, - cellDuration: 180, - width: 1000, - height: 585, - }); -}); - -test('Should correctly display the recurrent (one day at week) appointment with a lower timezone and day shift to the previous day', async ({ page }) => { - // expected date: 4/27/2021 6:00 PM - 8:00 PM - await screenshotTestFunc(t, SCREENSHOT_BASE_NAME, 'one-appointment__day-shift__lower-timezone'); -}); - -// TODO: .before() block not converted - move to test setup -// { - const appointmentTimezone = 'Etc/GMT-10'; - const schedulerTimezone = 'Etc/GMT+2'; - - await createWidget(page, 'dxScheduler', { - dataSource: [{ - allDay: false, - startDate: getAppointmentTime(new Date(2021, 3, 28, 6, 0, 0), appointmentTimezone), - startDateTimeZone: appointmentTimezone, - endDate: getAppointmentTime(new Date(2021, 3, 28, 8, 0, 0), appointmentTimezone), - endDateTimeZone: appointmentTimezone, - recurrenceRule: 'FREQ=WEEKLY;BYDAY=WE', - text: 'Test', - }], - timeZone: schedulerTimezone, - currentView: 'week', - currentDate: new Date(2021, 3, 28), - startDayHour: 0, - cellDuration: 180, - width: 1000, - height: 585, +test.describe('Weekly recurrent appointments with timezones', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); }); -}); - -test('Should correctly display the recurrent (one day at week) appointment with timezone week shift to the previous week', async ({ page }) => { - // expected date: 4/25/2021 6:00 AM - 8:00 AM - await screenshotTestFunc(t, SCREENSHOT_BASE_NAME, 'one-appointment__week-shift__lower-timezone'); -}); -// TODO: .before() block not converted - move to test setup -// { - const appointmentTimezone = 'Etc/GMT-10'; - const schedulerTimezone = 'Etc/GMT+10'; - - await createWidget(page, 'dxScheduler', { - dataSource: [{ - allDay: false, - startDate: getAppointmentTime(new Date(2021, 3, 26, 2, 0, 0), appointmentTimezone), - startDateTimeZone: appointmentTimezone, - endDate: getAppointmentTime(new Date(2021, 3, 26, 4, 0, 0), appointmentTimezone), - endDateTimeZone: appointmentTimezone, - recurrenceRule: 'FREQ=WEEKLY;BYDAY=MO', - text: 'Test', - }], - timeZone: schedulerTimezone, - currentView: 'week', - currentDate: new Date(2021, 3, 28), - startDayHour: 0, - cellDuration: 180, - width: 1000, - height: 585, + test('one day same timezone', async ({ page }) => { + await createWidget(page, 'dxScheduler', makeOptions('Etc/GMT+1', 'Etc/GMT+1', new Date(2021, 3, 28, 10, 0, 0), new Date(2021, 3, 28, 12, 0, 0), 'FREQ=WEEKLY;BYDAY=WE')); + await screenshotTest(page, 'one-appointment__same-timezone'); }); -}); -test('Should correctly display the recurrent (one day at week) appointment with timezone week shift to the next week', async ({ page }) => { - // expected date: 4/25/2021 4:00 PM - 6:00 PM - await screenshotTestFunc(t, SCREENSHOT_BASE_NAME, 'one-appointment__week-shift__greater-timezone'); -}); - -// TODO: .before() block not converted - move to test setup -// { - const appointmentTimezone = 'Etc/GMT+10'; - const schedulerTimezone = 'Etc/GMT-10'; - - await createWidget(page, 'dxScheduler', { - dataSource: [{ - allDay: false, - startDate: getAppointmentTime(new Date(2021, 3, 25, 20, 0, 0), appointmentTimezone), - startDateTimeZone: appointmentTimezone, - endDate: getAppointmentTime(new Date(2021, 3, 25, 22, 0, 0), appointmentTimezone), - endDateTimeZone: appointmentTimezone, - recurrenceRule: 'FREQ=WEEKLY;BYDAY=SU', - text: 'Test', - }], - timeZone: schedulerTimezone, - currentView: 'week', - currentDate: new Date(2021, 3, 28), - startDayHour: 0, - cellDuration: 180, - width: 1000, - height: 585, - }); -}); - -test(`Should correctly display the recurrent (one day at week) appointment -with timezone view period shift to the next view period at the first week`, async ({ page }) => { - // expected no visible date - await screenshotTestFunc(t, SCREENSHOT_BASE_NAME, 'one-appointment__next-view-shift__first-week'); -}); - -// TODO: .before() block not converted - move to test setup -// { - const appointmentTimezone = 'Etc/GMT+10'; - const schedulerTimezone = 'Etc/GMT-10'; - - await createWidget(page, 'dxScheduler', { - dataSource: [{ - allDay: false, - startDate: getAppointmentTime(new Date(2021, 4, 1, 20, 0, 0), appointmentTimezone), - startDateTimeZone: appointmentTimezone, - endDate: getAppointmentTime(new Date(2021, 4, 1, 22, 0, 0), appointmentTimezone), - endDateTimeZone: appointmentTimezone, - recurrenceRule: 'FREQ=WEEKLY;BYDAY=SA', - text: 'Test', - }], - timeZone: schedulerTimezone, - currentView: 'week', - currentDate: new Date(2021, 3, 28), - startDayHour: 0, - cellDuration: 180, - width: 1000, - height: 585, + test('one day greater timezone with day shift', async ({ page }) => { + await createWidget(page, 'dxScheduler', makeOptions('Etc/GMT+10', 'Etc/GMT-2', new Date(2021, 3, 28, 22, 0, 0), new Date(2021, 3, 29, 0, 0, 0), 'FREQ=WEEKLY;BYDAY=WE')); + await screenshotTest(page, 'one-appointment__day-shift__greater-timezone'); }); -}); - -test(`Should correctly display the recurrent (one day at week) appointment -with timezone view period shift to the next view period at the second week`, async ({ page }) => { - // expected date: 5/2/2021 4:00 PM - 6:00 PM - await screenshotTestFunc(t, SCREENSHOT_BASE_NAME, 'one-appointment__next-view-shift__second-week'); -}); - -// TODO: .before() block not converted - move to test setup -// { - const appointmentTimezone = 'Etc/GMT+10'; - const schedulerTimezone = 'Etc/GMT-10'; - await createWidget(page, 'dxScheduler', { - dataSource: [{ - allDay: false, - startDate: getAppointmentTime(new Date(2021, 4, 1, 20, 0, 0), appointmentTimezone), - startDateTimeZone: appointmentTimezone, - endDate: getAppointmentTime(new Date(2021, 4, 1, 22, 0, 0), appointmentTimezone), - endDateTimeZone: appointmentTimezone, - recurrenceRule: 'FREQ=WEEKLY;BYDAY=SA', - text: 'Test', - }], - timeZone: schedulerTimezone, - currentView: 'week', - currentDate: new Date(2021, 4, 5), - startDayHour: 0, - cellDuration: 180, - width: 1000, - height: 585, + test('one day lower timezone with day shift', async ({ page }) => { + await createWidget(page, 'dxScheduler', makeOptions('Etc/GMT-10', 'Etc/GMT+2', new Date(2021, 3, 28, 6, 0, 0), new Date(2021, 3, 28, 8, 0, 0), 'FREQ=WEEKLY;BYDAY=WE')); + await screenshotTest(page, 'one-appointment__day-shift__lower-timezone'); }); -}); - -test(`Should correctly display the recurrent (one day at week) appointment -with timezone view period shift to the previous view period at the first week`, async ({ page }) => { - // expected date: 5/1/2021 6:00 AM - 8:00 AM - await screenshotTestFunc(t, SCREENSHOT_BASE_NAME, 'one-appointment__previous-view-shift__first-week'); -}); - -// TODO: .before() block not converted - move to test setup -// { - const appointmentTimezone = 'Etc/GMT-10'; - const schedulerTimezone = 'Etc/GMT+10'; - await createWidget(page, 'dxScheduler', { - dataSource: [{ - allDay: false, - startDate: getAppointmentTime(new Date(2021, 3, 25, 2, 0, 0), appointmentTimezone), - startDateTimeZone: appointmentTimezone, - endDate: getAppointmentTime(new Date(2021, 3, 25, 4, 0, 0), appointmentTimezone), - endDateTimeZone: appointmentTimezone, - recurrenceRule: 'FREQ=WEEKLY;BYDAY=SU', - text: 'Test', - }], - timeZone: schedulerTimezone, - currentView: 'week', - currentDate: new Date(2021, 3, 28), - startDayHour: 0, - cellDuration: 180, - width: 1000, - height: 585, + test('multiple day first week same timezone', async ({ page }) => { + await createWidget(page, 'dxScheduler', makeOptions('Etc/GMT+1', 'Etc/GMT+1', new Date(2021, 3, 28, 10, 0, 0), new Date(2021, 3, 28, 14, 0, 0), 'FREQ=WEEKLY;BYDAY=TU,WE,TH')); + await screenshotTest(page, 'multiple-appointment__first-week__same-timezone'); }); -}); - -test(`Should correctly display the recurrent (one day at week) appointment -with timezone view period shift to the previous view period at the second week`, async ({ page }) => { - // expected date: 4/24/2021 6:00 AM - 8:00 AM - await screenshotTestFunc(t, SCREENSHOT_BASE_NAME, 'one-appointment__previous-view-shift__before-week'); -}); -// TODO: .before() block not converted - move to test setup -// { - const appointmentTimezone = 'Etc/GMT-10'; - const schedulerTimezone = 'Etc/GMT+10'; - - await createWidget(page, 'dxScheduler', { - dataSource: [{ - allDay: false, - startDate: getAppointmentTime(new Date(2021, 3, 25, 2, 0, 0), appointmentTimezone), - startDateTimeZone: appointmentTimezone, - endDate: getAppointmentTime(new Date(2021, 3, 25, 4, 0, 0), appointmentTimezone), - endDateTimeZone: appointmentTimezone, - recurrenceRule: 'FREQ=WEEKLY;BYDAY=SU', - text: 'Test', - }], - timeZone: schedulerTimezone, - currentView: 'week', - currentDate: new Date(2021, 3, 21), - startDayHour: 0, - cellDuration: 180, - width: 1000, - height: 585, + test('multiple day second week same timezone', async ({ page }) => { + await createWidget(page, 'dxScheduler', makeOptions('Etc/GMT+1', 'Etc/GMT+1', new Date(2021, 3, 28, 10, 0, 0), new Date(2021, 3, 28, 14, 0, 0), 'FREQ=WEEKLY;BYDAY=TU,WE,TH', new Date(2021, 4, 5))); + await screenshotTest(page, 'multiple-appointment__second-week__same-timezone'); }); }); - -// === multiple day in week tests section === - -test('Should correctly display recurrent appointment with multiple day in week on the first week in same timezone', async ({ page }) => { - // --- setup --- -const appointmentTimezone = 'Etc/GMT+1'; - const schedulerTimezone = 'Etc/GMT+1'; - - await createWidget(page, 'dxScheduler', { - dataSource: [{ - allDay: false, - startDate: getAppointmentTime(new Date(2021, 3, 28, 10, 0, 0), appointmentTimezone), - startDateTimeZone: appointmentTimezone, - endDate: getAppointmentTime(new Date(2021, 3, 28, 14, 0, 0), appointmentTimezone), - endDateTimeZone: appointmentTimezone, - recurrenceRule: 'FREQ=WEEKLY;BYDAY=TU,WE,TH', - text: 'Test', - }], - timeZone: schedulerTimezone, - currentView: 'week', - currentDate: new Date(2021, 3, 28), - startDayHour: 0, - cellDuration: 180, - width: 1000, - height: 585, - // --- test --- -// expected dates: - // 4/28/2021 10:00 AM - 2:00 AM - // 4/29/2021 10:00 AM - 2:00 AM - await screenshotTestFunc(t, SCREENSHOT_BASE_NAME, 'multiple-appointment__first-week__same-timezone'); -}); -}); - -test('Should correctly display recurrent appointment with multiple day in week on the second week in same timezone', async ({ page }) => { - // --- setup --- -const appointmentTimezone = 'Etc/GMT+1'; - const schedulerTimezone = 'Etc/GMT+1'; - - await createWidget(page, 'dxScheduler', { - dataSource: [{ - allDay: false, - startDate: getAppointmentTime(new Date(2021, 3, 28, 10, 0, 0), appointmentTimezone), - startDateTimeZone: appointmentTimezone, - endDate: getAppointmentTime(new Date(2021, 3, 28, 14, 0, 0), appointmentTimezone), - endDateTimeZone: appointmentTimezone, - recurrenceRule: 'FREQ=WEEKLY;BYDAY=TU,WE,TH', - text: 'Test', - }], - timeZone: schedulerTimezone, - currentView: 'week', - currentDate: new Date(2021, 4, 5), - startDayHour: 0, - cellDuration: 180, - width: 1000, - height: 585, - // --- test --- -// expected dates: - // 5/4/2021 10:00 AM - 2:00 AM - // 5/5/2021 10:00 AM - 2:00 AM - // 5/6/2021 10:00 AM - 2:00 AM - await screenshotTestFunc(t, SCREENSHOT_BASE_NAME, 'multiple-appointment__second-week__same-timezone'); -}); -}); - -test('Should correctly display recurrent appointment with multiple day in week on the first week in a greater time timezone', async ({ page }) => { - // --- setup --- -const appointmentTimezone = 'Etc/GMT+10'; - const timezone = 'Etc/GMT-5'; - - await createWidget(page, 'dxScheduler', { - dataSource: [{ - allDay: false, - startDate: getAppointmentTime(new Date(2021, 3, 28, 10, 0, 0), appointmentTimezone), - startDateTimeZone: appointmentTimezone, - endDate: getAppointmentTime(new Date(2021, 3, 28, 14, 0, 0), appointmentTimezone), - endDateTimeZone: appointmentTimezone, - recurrenceRule: 'FREQ=WEEKLY;BYDAY=TU,WE,TH', - text: 'Test', - }], - timeZone: timezone, - currentView: 'week', - currentDate: new Date(2021, 3, 28), - startDayHour: 0, - cellDuration: 180, - width: 1000, - height: 585, - // --- test --- -// expected dates: - // 4/29/2021 1:00 AM - 5:00 AM - // 4/30/2021 1:00 AM - 5:00 AM - await screenshotTestFunc(t, SCREENSHOT_BASE_NAME, 'multiple-appointment__first-week__greater-timezone'); -}); -}); - -test('Should correctly display recurrent appointment with multiple day in week on the second week in a greater time timezone', async ({ page }) => { - // --- setup --- -const appointmentTimezone = 'Etc/GMT+10'; - const timezone = 'Etc/GMT-5'; - - await createWidget(page, 'dxScheduler', { - dataSource: [{ - allDay: false, - startDate: getAppointmentTime(new Date(2021, 3, 28, 10, 0, 0), appointmentTimezone), - startDateTimeZone: appointmentTimezone, - endDate: getAppointmentTime(new Date(2021, 3, 28, 14, 0, 0), appointmentTimezone), - endDateTimeZone: appointmentTimezone, - recurrenceRule: 'FREQ=WEEKLY;BYDAY=TU,WE,TH', - text: 'Test', - }], - timeZone: timezone, - currentView: 'week', - currentDate: new Date(2021, 4, 5), - startDayHour: 0, - cellDuration: 180, - width: 1000, - height: 585, - // --- test --- -// expected dates: - // 5/5/2021 1:00 AM - 5:00 AM - // 5/6/2021 1:00 AM - 5:00 AM - // 5/7/2021 1:00 AM - 5:00 AM - await screenshotTestFunc(t, SCREENSHOT_BASE_NAME, 'multiple-appointment__second-week__greater-timezone'); -}); -}); - -test('Should correctly display recurrent appointment with multiple day in week on the first week in a lower time timezone', async ({ page }) => { - // --- setup --- -const appointmentTimezone = 'Etc/GMT-10'; - const timezone = 'Etc/GMT+5'; - - await createWidget(page, 'dxScheduler', { - dataSource: [{ - allDay: false, - startDate: getAppointmentTime(new Date(2021, 3, 28, 10, 0, 0), appointmentTimezone), - startDateTimeZone: appointmentTimezone, - endDate: getAppointmentTime(new Date(2021, 3, 28, 14, 0, 0), appointmentTimezone), - endDateTimeZone: appointmentTimezone, - recurrenceRule: 'FREQ=WEEKLY;BYDAY=TU,WE,TH', - text: 'Test', - }], - timeZone: timezone, - currentView: 'week', - currentDate: new Date(2021, 3, 28), - startDayHour: 0, - cellDuration: 180, - width: 1000, - height: 585, - // --- test --- -// expected dates: - // 4/27/2021 7:00 PM - 11:00 PM - // 4/28/2021 7:00 PM - 11:00 PM - await screenshotTestFunc(t, SCREENSHOT_BASE_NAME, 'multiple-appointment__first-week__lower-timezone'); -}); -}); - -test('Should correctly display recurrent appointment with multiple day in week on the second week in a lower time timezone', async ({ page }) => { - // --- setup --- -const appointmentTimezone = 'Etc/GMT-10'; - const timezone = 'Etc/GMT+5'; - - await createWidget(page, 'dxScheduler', { - dataSource: [{ - allDay: false, - startDate: getAppointmentTime(new Date(2021, 3, 28, 10, 0, 0), appointmentTimezone), - startDateTimeZone: appointmentTimezone, - endDate: getAppointmentTime(new Date(2021, 3, 28, 14, 0, 0), appointmentTimezone), - endDateTimeZone: appointmentTimezone, - recurrenceRule: 'FREQ=WEEKLY;BYDAY=TU,WE,TH', - text: 'Test', - }], - timeZone: timezone, - currentView: 'week', - currentDate: new Date(2021, 4, 5), - startDayHour: 0, - cellDuration: 180, - width: 1000, - height: 585, - // --- test --- -// expected dates: - // 5/3/2021 7:00 PM - 11:00 PM - // 5/4/2021 7:00 PM - 11:00 PM - // 5/5/2021 7:00 PM - 11:00 PM - await screenshotTestFunc(t, SCREENSHOT_BASE_NAME, 'multiple-appointment__second-week__lower-timezone'); -}); -}); - -// === maximum timezone offset tests section === - -test(`Should correctly display recurrent appointment with multiple day in week - on the first week with maximum positive timezone offset`, async ({ page }) => { - // --- setup --- -const appointmentTimezone = 'Etc/GMT+12'; - const timezone = 'Etc/GMT-14'; - - await createWidget(page, 'dxScheduler', { - dataSource: [{ - allDay: false, - startDate: getAppointmentTime(new Date(2021, 3, 29, 22, 0, 0), appointmentTimezone), - startDateTimeZone: appointmentTimezone, - endDate: getAppointmentTime(new Date(2021, 3, 30, 0, 0, 0), appointmentTimezone), - endDateTimeZone: appointmentTimezone, - recurrenceRule: 'FREQ=WEEKLY;BYDAY=WE,TH, FT', - text: 'Test', - }], - timeZone: timezone, - currentView: 'week', - currentDate: new Date(2021, 3, 28), - startDayHour: 0, - cellDuration: 180, - width: 1000, - height: 585, - // --- test --- -// expected date: 5/1/2021 12:00 AM - 2:00 AM - await screenshotTestFunc(t, SCREENSHOT_BASE_NAME, 'one-appointment__first-week__max-positive-timezone-offset'); -}); -}); - -test(`Should correctly display recurrent appointment with multiple day in week - on the first week with maximum positive timezone offset`, async ({ page }) => { - // --- setup --- -const appointmentTimezone = 'Etc/GMT+12'; - const timezone = 'Etc/GMT-14'; - - await createWidget(page, 'dxScheduler', { - dataSource: [{ - allDay: false, - startDate: getAppointmentTime(new Date(2021, 3, 29, 22, 0, 0), appointmentTimezone), - startDateTimeZone: appointmentTimezone, - endDate: getAppointmentTime(new Date(2021, 3, 30, 0, 0, 0), appointmentTimezone), - endDateTimeZone: appointmentTimezone, - recurrenceRule: 'FREQ=WEEKLY;BYDAY=TU,WE,TH', - text: 'Test', - }], - timeZone: timezone, - currentView: 'week', - currentDate: new Date(2021, 4, 5), - startDayHour: 0, - cellDuration: 180, - width: 1000, - height: 585, - // --- test --- -// expected dates: - // 5/6/2021 12:00 AM - 2:00 AM - // 5/7/2021 12:00 AM - 2:00 AM - // 5/8/2021 12:00 AM - 2:00 AM - await screenshotTestFunc(t, SCREENSHOT_BASE_NAME, 'one-appointment__second-week__max-positive-timezone-offset'); -}); -}); - -test(`Should correctly display recurrent appointment with multiple day in week - on the first week with maximum negative timezone offset`, async ({ page }) => { - // --- setup --- -const appointmentTimezone = 'Etc/GMT-14'; - const timezone = 'Etc/GMT+12'; - - await createWidget(page, 'dxScheduler', { - dataSource: [{ - allDay: false, - startDate: getAppointmentTime(new Date(2021, 3, 28, 0, 0, 0), appointmentTimezone), - startDateTimeZone: appointmentTimezone, - endDate: getAppointmentTime(new Date(2021, 3, 28, 2, 0, 0), appointmentTimezone), - endDateTimeZone: appointmentTimezone, - recurrenceRule: 'FREQ=WEEKLY;BYDAY=MO,TU,WE', - text: 'Test', - }], - timeZone: timezone, - currentView: 'week', - currentDate: new Date(2021, 3, 28), - startDayHour: 0, - cellDuration: 180, - width: 1000, - height: 585, - // --- test --- -// expected dates: - // 4/26/2021 10:00 PM - 12:00 AM - // 5/1/2021 10:00 PM - 12:00 AM - await screenshotTestFunc(t, SCREENSHOT_BASE_NAME, 'one-appointment__first-week__max-negative-timezone-offset'); -}); -}); - -test(`Should correctly display recurrent appointment with multiple day in week - on the first week with maximum negative timezone offset`, async ({ page }) => { - // --- setup --- -const appointmentTimezone = 'Etc/GMT-14'; - const timezone = 'Etc/GMT+12'; - - await createWidget(page, 'dxScheduler', { - dataSource: [{ - allDay: false, - startDate: getAppointmentTime(new Date(2021, 3, 28, 0, 0, 0), appointmentTimezone), - startDateTimeZone: appointmentTimezone, - endDate: getAppointmentTime(new Date(2021, 3, 28, 2, 0, 0), appointmentTimezone), - endDateTimeZone: appointmentTimezone, - recurrenceRule: 'FREQ=WEEKLY;BYDAY=MO,TU,WE', - text: 'Test', - }], - timeZone: timezone, - currentView: 'week', - currentDate: new Date(2021, 4, 5), - startDayHour: 0, - cellDuration: 180, - width: 1000, - height: 585, - // --- test --- -// expected dates: - // 5/2/2021 10:00 PM - 12:00 AM - // 5/3/2021 10:00 PM - 12:00 AM - // 5/8/2021 10:00 PM - 12:00 AM - await screenshotTestFunc(t, SCREENSHOT_BASE_NAME, 'one-appointment__second-week__max-negative-timezone-offset'); -}); -}); -}); From 214f5fcfd110eba341a2668e6a46cf0367d8314f Mon Sep 17 00:00:00 2001 From: Aleksey Semikozov Date: Sat, 21 Mar 2026 23:14:40 -0300 Subject: [PATCH 04/13] Playwright POC - editors, navigation, common test improvements --- .../common/accessibility/bugs.spec.ts | 10 + .../common/accessibility/common.spec.ts | 10 + .../common/accessibility/contrast.spec.ts | 10 + .../dataGrid/common/columnChooser.spec.ts | 10 + .../common/editing/functional.spec.ts | 12 + .../dataGrid/common/editing/visual.spec.ts | 10 + .../common/filterRow/functional.spec.ts | 10 + .../common/headerFilter/headerFilter.spec.ts | 10 + .../dataGrid/common/headerPanel.spec.ts | 10 + .../columnReordering.visual.spec.ts | 10 + .../keyboardNavigation.functional.spec.ts | 10 + .../keyboardNavigation.visual.spec.ts | 10 + .../markup/T1240074_hoveringRows.spec.ts | 10 + .../dataGrid/common/scrolling.spec.ts | 10 + .../common/stateStoring/stateStoring.spec.ts | 10 + .../common/validation/validationPopup.spec.ts | 10 + .../dataGrid/sticky/common/appearance.spec.ts | 10 + .../sticky/common/columnFixingIcons.spec.ts | 10 + .../sticky/common/focusOverlay.spec.ts | 10 + .../common/stickyColumnReordering.spec.ts | 10 + .../common/stickyColumnResizing.spec.ts | 10 + .../sticky/common/stickyColumns.spec.ts | 12 + .../sticky/common/withAdaptability.spec.ts | 2 + .../sticky/common/withDragAndDrop.spec.ts | 10 + .../sticky/common/withEditing.spec.ts | 2 + .../sticky/common/withFilterRow.spec.ts | 2 + .../sticky/common/withGrouping.spec.ts | 2 + .../common/withKeyboardNavigation.spec.ts | 2 + .../sticky/common/withMasterDetail.spec.ts | 2 + .../sticky/common/withMultiRow.spec.ts | 2 + .../sticky/common/withRowSelection.spec.ts | 2 + .../sticky/common/withVirtualColumns.spec.ts | 12 + .../common/withVirtualScrolling.spec.ts | 10 + .../sticky/fixed/bandColumnFirstCases.spec.ts | 17 + .../fixed/bandColumnSecondCases.spec.ts | 17 + .../dataGrid/sticky/fixed/positions.spec.ts | 17 + .../dragAppointmentAfterResize.spec.ts | 171 ++++--- ...urrenceAppointmentInDstTimeEditing.spec.ts | 244 +++------- .../yearlyRecurrentAppointment.spec.ts | 424 +++--------------- 39 files changed, 556 insertions(+), 606 deletions(-) diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/accessibility/bugs.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/accessibility/bugs.spec.ts index b46f25921f9e..aab3dbaeb59f 100644 --- a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/accessibility/bugs.spec.ts +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/accessibility/bugs.spec.ts @@ -4,6 +4,16 @@ import path from 'path'; const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; +const getData = (rowCount: number, colCount: number): Record[] => { + const items: Record[] = []; + for (let i = 0; i < rowCount; i++) { + const item: Record = {}; + for (let j = 0; j < colCount; j++) item[`field_${j}`] = `val_${i}_${j}`; + items.push(item); + } + return items; +}; + test.describe('Accessibility bugs', () => { test.beforeEach(async ({ page }) => { await page.goto(containerUrl); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/accessibility/common.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/accessibility/common.spec.ts index f349ac1931ba..e210adc9e808 100644 --- a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/accessibility/common.spec.ts +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/accessibility/common.spec.ts @@ -4,6 +4,16 @@ import path from 'path'; const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; +const getData = (rowCount: number, colCount: number): Record[] => { + const items: Record[] = []; + for (let i = 0; i < rowCount; i++) { + const item: Record = {}; + for (let j = 0; j < colCount; j++) item[`field_${j}`] = `val_${i}_${j}`; + items.push(item); + } + return items; +}; + test.describe('Common tests', () => { test.beforeEach(async ({ page }) => { await page.goto(containerUrl); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/accessibility/contrast.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/accessibility/contrast.spec.ts index 6a5128d4af24..539c5cce76bd 100644 --- a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/accessibility/contrast.spec.ts +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/accessibility/contrast.spec.ts @@ -4,6 +4,16 @@ import path from 'path'; const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; +const getData = (rowCount: number, colCount: number): Record[] => { + const items: Record[] = []; + for (let i = 0; i < rowCount; i++) { + const item: Record = {}; + for (let j = 0; j < colCount; j++) item[`field_${j}`] = `val_${i}_${j}`; + items.push(item); + } + return items; +}; + test.describe('DataGrid - contrast', () => { test.beforeEach(async ({ page }) => { await page.goto(containerUrl); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/columnChooser.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/columnChooser.spec.ts index 0a66418be29c..bfb015642459 100644 --- a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/columnChooser.spec.ts +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/columnChooser.spec.ts @@ -4,6 +4,16 @@ import path from 'path'; const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; +const getData = (rowCount: number, colCount: number): Record[] => { + const items: Record[] = []; + for (let i = 0; i < rowCount; i++) { + const item: Record = {}; + for (let j = 0; j < colCount; j++) item[`field_${j}`] = `val_${i}_${j}`; + items.push(item); + } + return items; +}; + test.describe('Column chooser', () => { test.beforeEach(async ({ page }) => { await page.goto(containerUrl); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/editing/functional.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/editing/functional.spec.ts index 56fb1b5afa4a..075ac35c61e2 100644 --- a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/editing/functional.spec.ts +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/editing/functional.spec.ts @@ -4,6 +4,18 @@ import path from 'path'; const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; +const getData = (rowCount: number, colCount: number): Record[] => { + const items: Record[] = []; + for (let i = 0; i < rowCount; i++) { + const item: Record = {}; + for (let j = 0; j < colCount; j++) item[`field_${j}`] = `val_${i}_${j}`; + items.push(item); + } + return items; +}; + +// TODO: import defaultConfig from sticky helpers or inline the data + test.describe('Editing.Functional', () => { test.beforeEach(async ({ page }) => { await page.goto(containerUrl); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/editing/visual.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/editing/visual.spec.ts index 8d1528d05017..006844a3667f 100644 --- a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/editing/visual.spec.ts +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/editing/visual.spec.ts @@ -4,6 +4,16 @@ import path from 'path'; const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; +const getData = (rowCount: number, colCount: number): Record[] => { + const items: Record[] = []; + for (let i = 0; i < rowCount; i++) { + const item: Record = {}; + for (let j = 0; j < colCount; j++) item[`field_${j}`] = `val_${i}_${j}`; + items.push(item); + } + return items; +}; + test.describe('Editing.Visual', () => { test.beforeEach(async ({ page }) => { await page.goto(containerUrl); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/filterRow/functional.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/filterRow/functional.spec.ts index 1cbce838b940..836a8188980e 100644 --- a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/filterRow/functional.spec.ts +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/filterRow/functional.spec.ts @@ -4,6 +4,16 @@ import path from 'path'; const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; +const getData = (rowCount: number, colCount: number): Record[] => { + const items: Record[] = []; + for (let i = 0; i < rowCount; i++) { + const item: Record = {}; + for (let j = 0; j < colCount; j++) item[`field_${j}`] = `val_${i}_${j}`; + items.push(item); + } + return items; +}; + test.describe('FilterRow', () => { test.beforeEach(async ({ page }) => { await page.goto(containerUrl); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/headerFilter/headerFilter.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/headerFilter/headerFilter.spec.ts index f94c0ea7fc6e..3efcd29149d6 100644 --- a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/headerFilter/headerFilter.spec.ts +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/headerFilter/headerFilter.spec.ts @@ -4,6 +4,16 @@ import path from 'path'; const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; +const getData = (rowCount: number, colCount: number): Record[] => { + const items: Record[] = []; + for (let i = 0; i < rowCount; i++) { + const item: Record = {}; + for (let j = 0; j < colCount; j++) item[`field_${j}`] = `val_${i}_${j}`; + items.push(item); + } + return items; +}; + test.describe('Header Filter', () => { test.beforeEach(async ({ page }) => { await page.goto(containerUrl); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/headerPanel.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/headerPanel.spec.ts index 474907656638..ee48bcf939ce 100644 --- a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/headerPanel.spec.ts +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/headerPanel.spec.ts @@ -4,6 +4,16 @@ import path from 'path'; const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; +const getData = (rowCount: number, colCount: number): Record[] => { + const items: Record[] = []; + for (let i = 0; i < rowCount; i++) { + const item: Record = {}; + for (let j = 0; j < colCount; j++) item[`field_${j}`] = `val_${i}_${j}`; + items.push(item); + } + return items; +}; + test.describe('Header Panel', () => { test.beforeEach(async ({ page }) => { await page.goto(containerUrl); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/keyboardNavigation/columnReordering.visual.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/keyboardNavigation/columnReordering.visual.spec.ts index d94feb8f7615..acf25b17021d 100644 --- a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/keyboardNavigation/columnReordering.visual.spec.ts +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/keyboardNavigation/columnReordering.visual.spec.ts @@ -4,6 +4,16 @@ import path from 'path'; const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; +const getData = (rowCount: number, colCount: number): Record[] => { + const items: Record[] = []; + for (let i = 0; i < rowCount; i++) { + const item: Record = {}; + for (let j = 0; j < colCount; j++) item[`field_${j}`] = `val_${i}_${j}`; + items.push(item); + } + return items; +}; + test.describe('DataGrid Tests', () => { test.beforeEach(async ({ page }) => { await page.goto(containerUrl); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/keyboardNavigation/keyboardNavigation.functional.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/keyboardNavigation/keyboardNavigation.functional.spec.ts index d55de0e8ff30..9fdae0d9cf9a 100644 --- a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/keyboardNavigation/keyboardNavigation.functional.spec.ts +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/keyboardNavigation/keyboardNavigation.functional.spec.ts @@ -4,6 +4,16 @@ import path from 'path'; const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; +const getData = (rowCount: number, colCount: number): Record[] => { + const items: Record[] = []; + for (let i = 0; i < rowCount; i++) { + const item: Record = {}; + for (let j = 0; j < colCount; j++) item[`field_${j}`] = `val_${i}_${j}`; + items.push(item); + } + return items; +}; + test.describe('Keyboard Navigation - common', () => { test.beforeEach(async ({ page }) => { await page.goto(containerUrl); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/keyboardNavigation/keyboardNavigation.visual.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/keyboardNavigation/keyboardNavigation.visual.spec.ts index 55daf9913fde..c6a8d9cd4ead 100644 --- a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/keyboardNavigation/keyboardNavigation.visual.spec.ts +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/keyboardNavigation/keyboardNavigation.visual.spec.ts @@ -4,6 +4,16 @@ import path from 'path'; const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; +const getData = (rowCount: number, colCount: number): Record[] => { + const items: Record[] = []; + for (let i = 0; i < rowCount; i++) { + const item: Record = {}; + for (let j = 0; j < colCount; j++) item[`field_${j}`] = `val_${i}_${j}`; + items.push(item); + } + return items; +}; + test.describe('Keyboard Navigation.Visual', () => { test.beforeEach(async ({ page }) => { await page.goto(containerUrl); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/markup/T1240074_hoveringRows.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/markup/T1240074_hoveringRows.spec.ts index 175185670438..59129a561e1d 100644 --- a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/markup/T1240074_hoveringRows.spec.ts +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/markup/T1240074_hoveringRows.spec.ts @@ -4,6 +4,16 @@ import path from 'path'; const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; +const getData = (rowCount: number, colCount: number): Record[] => { + const items: Record[] = []; + for (let i = 0; i < rowCount; i++) { + const item: Record = {}; + for (let j = 0; j < colCount; j++) item[`field_${j}`] = `val_${i}_${j}`; + items.push(item); + } + return items; +}; + test.describe('HoveringRows', () => { test.beforeEach(async ({ page }) => { await page.goto(containerUrl); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/scrolling.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/scrolling.spec.ts index 1f8fc1f0027e..bd1ab5ee96d3 100644 --- a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/scrolling.spec.ts +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/scrolling.spec.ts @@ -4,6 +4,16 @@ import path from 'path'; const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; +const getData = (rowCount: number, colCount: number): Record[] => { + const items: Record[] = []; + for (let i = 0; i < rowCount; i++) { + const item: Record = {}; + for (let j = 0; j < colCount; j++) item[`field_${j}`] = `val_${i}_${j}`; + items.push(item); + } + return items; +}; + test.describe('Scrolling', () => { test.beforeEach(async ({ page }) => { await page.goto(containerUrl); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/stateStoring/stateStoring.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/stateStoring/stateStoring.spec.ts index f124fba9451e..e51702009e5b 100644 --- a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/stateStoring/stateStoring.spec.ts +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/stateStoring/stateStoring.spec.ts @@ -4,6 +4,16 @@ import path from 'path'; const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; +const getData = (rowCount: number, colCount: number): Record[] => { + const items: Record[] = []; + for (let i = 0; i < rowCount; i++) { + const item: Record = {}; + for (let j = 0; j < colCount; j++) item[`field_${j}`] = `val_${i}_${j}`; + items.push(item); + } + return items; +}; + test.describe('State Storing', () => { test.beforeEach(async ({ page }) => { await page.goto(containerUrl); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/validation/validationPopup.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/validation/validationPopup.spec.ts index 8571935cac4a..29207a4937cf 100644 --- a/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/validation/validationPopup.spec.ts +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/common/validation/validationPopup.spec.ts @@ -4,6 +4,16 @@ import path from 'path'; const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; +const getData = (rowCount: number, colCount: number): Record[] => { + const items: Record[] = []; + for (let i = 0; i < rowCount; i++) { + const item: Record = {}; + for (let j = 0; j < colCount; j++) item[`field_${j}`] = `val_${i}_${j}`; + items.push(item); + } + return items; +}; + test.describe('Validation', () => { test.beforeEach(async ({ page }) => { await page.goto(containerUrl); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/appearance.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/appearance.spec.ts index ccbf239064c4..7320feb9c8cc 100644 --- a/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/appearance.spec.ts +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/appearance.spec.ts @@ -4,6 +4,16 @@ import path from 'path'; const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; +const getData = (rowCount: number, colCount: number): Record[] => { + const items: Record[] = []; + for (let i = 0; i < rowCount; i++) { + const item: Record = {}; + for (let j = 0; j < colCount; j++) item[`field_${j}`] = `val_${i}_${j}`; + items.push(item); + } + return items; +}; + test.describe('FixedColumns - appearance', () => { test.beforeEach(async ({ page }) => { await page.goto(containerUrl); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/columnFixingIcons.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/columnFixingIcons.spec.ts index ec8b74ea931e..909437fedca9 100644 --- a/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/columnFixingIcons.spec.ts +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/columnFixingIcons.spec.ts @@ -4,6 +4,16 @@ import path from 'path'; const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; +const getData = (rowCount: number, colCount: number): Record[] => { + const items: Record[] = []; + for (let i = 0; i < rowCount; i++) { + const item: Record = {}; + for (let j = 0; j < colCount; j++) item[`field_${j}`] = `val_${i}_${j}`; + items.push(item); + } + return items; +}; + test.describe('Column Fixing', () => { test.beforeEach(async ({ page }) => { await page.goto(containerUrl); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/focusOverlay.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/focusOverlay.spec.ts index 1ce7b3ee98c2..e0ed7545beb4 100644 --- a/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/focusOverlay.spec.ts +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/focusOverlay.spec.ts @@ -4,6 +4,16 @@ import path from 'path'; const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; +const getData = (rowCount: number, colCount: number): Record[] => { + const items: Record[] = []; + for (let i = 0; i < rowCount; i++) { + const item: Record = {}; + for (let j = 0; j < colCount; j++) item[`field_${j}`] = `val_${i}_${j}`; + items.push(item); + } + return items; +}; + test.describe('FixedColumns - Focus Overlay', () => { test.beforeEach(async ({ page }) => { await page.goto(containerUrl); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/stickyColumnReordering.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/stickyColumnReordering.spec.ts index 0a97e82258c3..f2b91e9aa8e3 100644 --- a/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/stickyColumnReordering.spec.ts +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/stickyColumnReordering.spec.ts @@ -4,6 +4,16 @@ import path from 'path'; const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; +const getData = (rowCount: number, colCount: number): Record[] => { + const items: Record[] = []; + for (let i = 0; i < rowCount; i++) { + const item: Record = {}; + for (let j = 0; j < colCount; j++) item[`field_${j}`] = `val_${i}_${j}`; + items.push(item); + } + return items; +}; + test.describe('Reorder columns', () => { test.beforeEach(async ({ page }) => { await page.goto(containerUrl); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/stickyColumnResizing.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/stickyColumnResizing.spec.ts index 6b0c301073e2..7b62c6f83a9f 100644 --- a/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/stickyColumnResizing.spec.ts +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/stickyColumnResizing.spec.ts @@ -4,6 +4,16 @@ import path from 'path'; const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; +const getData = (rowCount: number, colCount: number): Record[] => { + const items: Record[] = []; + for (let i = 0; i < rowCount; i++) { + const item: Record = {}; + for (let j = 0; j < colCount; j++) item[`field_${j}`] = `val_${i}_${j}`; + items.push(item); + } + return items; +}; + test.describe('Resize columns - nextColumn mode', () => { test.beforeEach(async ({ page }) => { await page.goto(containerUrl); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/stickyColumns.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/stickyColumns.spec.ts index bc0ecc7b11b5..633b834ce05d 100644 --- a/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/stickyColumns.spec.ts +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/stickyColumns.spec.ts @@ -4,6 +4,18 @@ import path from 'path'; const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; +const getData = (rowCount: number, colCount: number): Record[] => { + const items: Record[] = []; + for (let i = 0; i < rowCount; i++) { + const item: Record = {}; + for (let j = 0; j < colCount; j++) item[`field_${j}`] = `val_${i}_${j}`; + items.push(item); + } + return items; +}; + +// TODO: import defaultConfig from sticky helpers or inline the data + test.describe('FixedColumns', () => { test.beforeEach(async ({ page }) => { await page.goto(containerUrl); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/withAdaptability.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/withAdaptability.spec.ts index f60a01ff9e7a..d833b9201c2b 100644 --- a/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/withAdaptability.spec.ts +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/withAdaptability.spec.ts @@ -4,6 +4,8 @@ import path from 'path'; const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; +// TODO: import defaultConfig from sticky helpers or inline the data + test.describe('Sticky columns - Adaptability', () => { test.beforeEach(async ({ page }) => { await page.goto(containerUrl); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/withDragAndDrop.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/withDragAndDrop.spec.ts index 2c018ef16ded..88226bc73fe5 100644 --- a/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/withDragAndDrop.spec.ts +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/withDragAndDrop.spec.ts @@ -4,6 +4,16 @@ import path from 'path'; const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; +const getData = (rowCount: number, colCount: number): Record[] => { + const items: Record[] = []; + for (let i = 0; i < rowCount; i++) { + const item: Record = {}; + for (let j = 0; j < colCount; j++) item[`field_${j}`] = `val_${i}_${j}`; + items.push(item); + } + return items; +}; + test.describe('Sticky columns - Drag and Drop', () => { test.beforeEach(async ({ page }) => { await page.goto(containerUrl); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/withEditing.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/withEditing.spec.ts index 3539ee8188c3..6b89e4a62b22 100644 --- a/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/withEditing.spec.ts +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/withEditing.spec.ts @@ -4,6 +4,8 @@ import path from 'path'; const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; +// TODO: import defaultConfig from sticky helpers or inline the data + test.describe('Sticky columns - Editing', () => { test.beforeEach(async ({ page }) => { await page.goto(containerUrl); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/withFilterRow.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/withFilterRow.spec.ts index 507cdaa02453..8014ea3fdd43 100644 --- a/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/withFilterRow.spec.ts +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/withFilterRow.spec.ts @@ -4,6 +4,8 @@ import path from 'path'; const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; +// TODO: import defaultConfig from sticky helpers or inline the data + test.describe('Sticky columns - Filter row', () => { test.beforeEach(async ({ page }) => { await page.goto(containerUrl); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/withGrouping.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/withGrouping.spec.ts index cc0b3b86f935..ad97b2d350a5 100644 --- a/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/withGrouping.spec.ts +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/withGrouping.spec.ts @@ -4,6 +4,8 @@ import path from 'path'; const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; +// TODO: import defaultConfig from sticky helpers or inline the data + test.describe('FixedColumns - Grouping', () => { test.beforeEach(async ({ page }) => { await page.goto(containerUrl); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/withKeyboardNavigation.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/withKeyboardNavigation.spec.ts index 81b22b3db63d..380f1dc5510f 100644 --- a/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/withKeyboardNavigation.spec.ts +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/withKeyboardNavigation.spec.ts @@ -4,6 +4,8 @@ import path from 'path'; const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; +// TODO: import defaultConfig from sticky helpers or inline the data + test.describe('Fixed Columns - keyboard navigation', () => { test.beforeEach(async ({ page }) => { await page.goto(containerUrl); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/withMasterDetail.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/withMasterDetail.spec.ts index e0657f39fb5d..5d1617ff7ce7 100644 --- a/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/withMasterDetail.spec.ts +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/withMasterDetail.spec.ts @@ -4,6 +4,8 @@ import path from 'path'; const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; +// TODO: import defaultConfig from sticky helpers or inline the data + test.describe('FixedColumns - MasterDetail', () => { test.beforeEach(async ({ page }) => { await page.goto(containerUrl); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/withMultiRow.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/withMultiRow.spec.ts index d674332ceaf2..0518870b7e5f 100644 --- a/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/withMultiRow.spec.ts +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/withMultiRow.spec.ts @@ -4,6 +4,8 @@ import path from 'path'; const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; +// TODO: import defaultConfig from sticky helpers or inline the data + test.describe('Sticky columns - Multi Row Header Columns', () => { test.beforeEach(async ({ page }) => { await page.goto(containerUrl); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/withRowSelection.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/withRowSelection.spec.ts index 6cae93687e72..85e51bcaa1ab 100644 --- a/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/withRowSelection.spec.ts +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/withRowSelection.spec.ts @@ -4,6 +4,8 @@ import path from 'path'; const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; +// TODO: import defaultConfig from sticky helpers or inline the data + test.describe('Sticky columns - Row Selection', () => { test.beforeEach(async ({ page }) => { await page.goto(containerUrl); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/withVirtualColumns.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/withVirtualColumns.spec.ts index f251b8f03d20..cdcffb5562a4 100644 --- a/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/withVirtualColumns.spec.ts +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/withVirtualColumns.spec.ts @@ -4,6 +4,18 @@ import path from 'path'; const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; +const getData = (rowCount: number, colCount: number): Record[] => { + const items: Record[] = []; + for (let i = 0; i < rowCount; i++) { + const item: Record = {}; + for (let j = 0; j < colCount; j++) item[`field_${j}`] = `val_${i}_${j}`; + items.push(item); + } + return items; +}; + +// TODO: import groupingDataSource from sticky helpers or inline the data + test.describe('Sticky columns - Virtual Columns', () => { test.beforeEach(async ({ page }) => { await page.goto(containerUrl); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/withVirtualScrolling.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/withVirtualScrolling.spec.ts index d8f590c89d0e..7045573e51db 100644 --- a/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/withVirtualScrolling.spec.ts +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/common/withVirtualScrolling.spec.ts @@ -4,6 +4,16 @@ import path from 'path'; const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; +const getData = (rowCount: number, colCount: number): Record[] => { + const items: Record[] = []; + for (let i = 0; i < rowCount; i++) { + const item: Record = {}; + for (let j = 0; j < colCount; j++) item[`field_${j}`] = `val_${i}_${j}`; + items.push(item); + } + return items; +}; + test.describe('Sticky columns - Virtual Scrolling', () => { test.beforeEach(async ({ page }) => { await page.goto(containerUrl); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/fixed/bandColumnFirstCases.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/fixed/bandColumnFirstCases.spec.ts index aa6d2e6af163..92ad170985d9 100644 --- a/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/fixed/bandColumnFirstCases.spec.ts +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/fixed/bandColumnFirstCases.spec.ts @@ -4,6 +4,23 @@ import path from 'path'; const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; +const getData = (rowCount: number, colCount: number): Record[] => { + const items: Record[] = []; + for (let i = 0; i < rowCount; i++) { + const item: Record = {}; + for (let j = 0; j < colCount; j++) item[`field_${j}`] = `val_${i}_${j}`; + items.push(item); + } + return items; +}; + +const borderConfigs = [ + { showColumnLines: true, showBorders: true }, + { showColumnLines: false, showBorders: true }, + { showColumnLines: false, showBorders: false }, + { showColumnLines: true, showBorders: false }, +]; + test.describe('FixedColumns', () => { test.beforeEach(async ({ page }) => { await page.goto(containerUrl); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/fixed/bandColumnSecondCases.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/fixed/bandColumnSecondCases.spec.ts index b16ab13c971b..3f741b671ef5 100644 --- a/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/fixed/bandColumnSecondCases.spec.ts +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/fixed/bandColumnSecondCases.spec.ts @@ -4,6 +4,23 @@ import path from 'path'; const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; +const getData = (rowCount: number, colCount: number): Record[] => { + const items: Record[] = []; + for (let i = 0; i < rowCount; i++) { + const item: Record = {}; + for (let j = 0; j < colCount; j++) item[`field_${j}`] = `val_${i}_${j}`; + items.push(item); + } + return items; +}; + +const borderConfigs = [ + { showColumnLines: true, showBorders: true }, + { showColumnLines: false, showBorders: true }, + { showColumnLines: false, showBorders: false }, + { showColumnLines: true, showBorders: false }, +]; + test.describe('FixedColumns', () => { test.beforeEach(async ({ page }) => { await page.goto(containerUrl); diff --git a/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/fixed/positions.spec.ts b/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/fixed/positions.spec.ts index 9658506cb8b6..dc7144d356f1 100644 --- a/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/fixed/positions.spec.ts +++ b/e2e/testcafe-devextreme/playwright-tests/dataGrid/sticky/fixed/positions.spec.ts @@ -4,6 +4,23 @@ import path from 'path'; const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; +const getData = (rowCount: number, colCount: number): Record[] => { + const items: Record[] = []; + for (let i = 0; i < rowCount; i++) { + const item: Record = {}; + for (let j = 0; j < colCount; j++) item[`field_${j}`] = `val_${i}_${j}`; + items.push(item); + } + return items; +}; + +const borderConfigs = [ + { showColumnLines: true, showBorders: true }, + { showColumnLines: false, showBorders: true }, + { showColumnLines: false, showBorders: false }, + { showColumnLines: true, showBorders: false }, +]; + test.describe('FixedColumns', () => { test.beforeEach(async ({ page }) => { await page.goto(containerUrl); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/dragAppointmentAfterResize.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/dragAppointmentAfterResize.spec.ts index f224fc66bb98..ec0145e6a6a3 100644 --- a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/dragAppointmentAfterResize.spec.ts +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/dragAppointmentAfterResize.spec.ts @@ -1,79 +1,108 @@ import { test, expect } from '@playwright/test'; -import path from 'path'; - -const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; +import { createWidget, getContainerUrl, setupTestPage } from '../../../../playwright-helpers'; + +const containerUrl = getContainerUrl(__dirname, '../../../../tests/container.html'); + +const defaultSchedulerOptions = { + views: ['day'], + dataSource: [], + resources: [ + { + fieldExpr: 'resourceId', + dataSource: [ + { id: 0, color: '#e01e38' }, + { id: 1, color: '#f98322' }, + { id: 2, color: '#1e65e8' }, + ], + label: 'Color', + }, + ], + width: 1666, + height: 833, + startDayHour: 9, + firstDayOfWeek: 1, + maxAppointmentsPerCell: 5, + currentView: 'day', + currentDate: new Date(2019, 3, 1), +}; test.describe('Drag-n-drop appointment after resize (T835545)', () => { test.beforeEach(async ({ page }) => { - await page.goto(containerUrl); - await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); - await page.evaluate((theme) => new Promise((resolve) => { - (window as any).DevExpress.ui.themes.ready(resolve); - (window as any).DevExpress.ui.themes.current(theme); - }), process.env.THEME || 'fluent.blue.light'); + await setupTestPage(page, containerUrl); }); -); - -['day', 'week', 'month', 'timelineDay', 'timelineWeek', 'timelineMonth'].forEach((view) => test(`After drag-n-drop appointment, size of appointment shouldn't change in the '${view}' view`, async (t) => { - // Scheduler on '#container' - const { element, resizableHandle } = page.locator('.dx-scheduler-appointment').filter({ hasText: 'app' }); - - const initSize = { - width: await element.clientWidth, - height: await element.clientHeight, - }; - - const isVertical = await resizableHandle.bottom.count !== 0; - - await t - .drag(isVertical ? resizableHandle.bottom : resizableHandle.right, 50, 50); - - const size = isVertical ? await element.clientHeight : await element.clientWidth; - expect(size) - .gt(isVertical ? initSize.height : initSize.width); - - const sizeBeforeDrag = { - width: await element.clientWidth, - height: await element.clientHeight, - }; - const positionBeforeDrag = { - left: await element.clientLeft, - top: await element.clientTop, - }; - - await t - .drag(element, 10, 10, { - offsetX: 0, - offsetY: 0, + ['day', 'week', 'month', 'timelineDay', 'timelineWeek', 'timelineMonth'].forEach((view) => { + test(`After drag-n-drop appointment, size of appointment shouldn't change in the '${view}' view`, async ({ page }) => { + await createWidget(page, 'dxScheduler', { + ...defaultSchedulerOptions, + views: [view], + currentView: view, + startDayHour: 9, + currentDate: new Date(2017, 4, 1), + dataSource: [{ + text: 'app', + startDate: new Date(2017, 4, 1, 9, 0), + endDate: new Date(2017, 4, 1, 10, 0), + }], + }); + + const element = page.locator('.dx-scheduler-appointment').filter({ hasText: 'app' }); + + const initSize = await element.evaluate((el) => ({ + width: el.clientWidth, + height: el.clientHeight, + })); + + const bottomHandle = element.locator('.dx-resizable-handle-bottom'); + const rightHandle = element.locator('.dx-resizable-handle-right'); + const isVertical = await bottomHandle.count() > 0; + + const handle = isVertical ? bottomHandle : rightHandle; + const handleBox = await handle.boundingBox(); + await page.mouse.move(handleBox!.x + handleBox!.width / 2, handleBox!.y + handleBox!.height / 2); + await page.mouse.down(); + await page.mouse.move(handleBox!.x + handleBox!.width / 2 + 50, handleBox!.y + handleBox!.height / 2 + 50, { steps: 5 }); + await page.mouse.up(); + + const sizeAfterResize = await element.evaluate((el) => ({ + width: el.clientWidth, + height: el.clientHeight, + })); + + if (isVertical) { + expect(sizeAfterResize.height).toBeGreaterThan(initSize.height); + } else { + expect(sizeAfterResize.width).toBeGreaterThan(initSize.width); + } + + const sizeBeforeDrag = await element.evaluate((el) => ({ + width: el.clientWidth, + height: el.clientHeight, + })); + const positionBeforeDrag = await element.evaluate((el) => ({ + left: el.clientLeft, + top: el.clientTop, + })); + + const box = await element.boundingBox(); + await page.mouse.move(box!.x, box!.y); + await page.mouse.down(); + await page.mouse.move(box!.x + 10, box!.y + 10, { steps: 5 }); + await page.mouse.up(); + + const sizeAfterDrag = await element.evaluate((el) => ({ + width: el.clientWidth, + height: el.clientHeight, + })); + const positionAfterDrag = await element.evaluate((el) => ({ + left: el.clientLeft, + top: el.clientTop, + })); + + expect(sizeBeforeDrag.width).toBe(sizeAfterDrag.width); + expect(sizeBeforeDrag.height).toBe(sizeAfterDrag.height); + expect(positionBeforeDrag.left).toBe(positionAfterDrag.left); + expect(positionBeforeDrag.top).toBe(positionAfterDrag.top); }); - - const elementClientWidth = await element.clientWidth; - const elementClientHeight = await element.clientHeight; - - const elementClientLeft = await element.clientLeft; - const elementClientTop = await element.clientTop; - - expect(sizeBeforeDrag.width) - .eql(elementClientWidth) - - .expect(sizeBeforeDrag.height) - .eql(elementClientHeight) - - .expect(positionBeforeDrag.left) - .eql(elementClientLeft) - - .expect(positionBeforeDrag.top) - .eql(elementClientTop); -}).before(async () => createScheduler({ - views: [view], - currentView: view, - startDayHour: 9, - currentDate: new Date(2017, 4, 1), - dataSource: [{ - text: 'app', - startDate: new Date(2017, 4, 1, 9, 0), - endDate: new Date(2017, 4, 1, 10, 0), - }], -}))); + }); }); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/timezone/T1102713/recurrenceAppointmentInDstTimeEditing.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/timezone/T1102713/recurrenceAppointmentInDstTimeEditing.spec.ts index 619dcefc7216..6eb74e63c822 100644 --- a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/timezone/T1102713/recurrenceAppointmentInDstTimeEditing.spec.ts +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/timezone/T1102713/recurrenceAppointmentInDstTimeEditing.spec.ts @@ -1,108 +1,16 @@ -import { test, expect } from '@playwright/test'; -import { createWidget, testScreenshot } from '../../../../../playwright-helpers'; -import path from 'path'; +import { test } from '@playwright/test'; +import { createWidget, testScreenshot, getContainerUrl, setupTestPage } from '../../../../../playwright-helpers'; -const containerUrl = `file://${path.resolve(__dirname, '../../../../../tests/container.html')}`; - -test.describe('Editing recurrent appointment in DST time', () => { - test.beforeEach(async ({ page }) => { - await page.goto(containerUrl); - await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); - await page.evaluate((theme) => new Promise((resolve) => { - (window as any).DevExpress.ui.themes.ready(resolve); - (window as any).DevExpress.ui.themes.current(theme); - }), process.env.THEME || 'fluent.blue.light'); - }); - -); - -interface ITestResizeOptions { - direction: keyof Appointment['resizableHandle']; - value: number; -} -interface ITestDragNDropOptions { - rowIdx: number; - cellIdx: number; -} +const containerUrl = getContainerUrl(__dirname, '../../../../../tests/container.html'); const SCREENSHOT_BASE_NAME = 'recurrent-appointment-timezone-dst__editing'; -const SCHEDULER_SELECTOR = '#container'; const TEST_APPOINTMENT_TEXT = 'Watercolor Landscape'; -const TEST_CURSOR_OPTIONS = { speed: 0.5 }; const APPOINTMENT_DATETIME = { - winter: { - start: new Date('2020-11-01T17:30:00.000Z'), - end: new Date('2020-11-01T19:00:00.000Z'), - }, - summer: { - start: new Date('2020-03-08T16:30:00.000Z'), - end: new Date('2020-03-08T18:00:00.000Z'), - }, + winter: { start: new Date('2020-11-01T17:30:00.000Z'), end: new Date('2020-11-01T19:00:00.000Z') }, + summer: { start: new Date('2020-03-08T16:30:00.000Z'), end: new Date('2020-03-08T18:00:00.000Z') }, }; -async function editingPopupTestFunction(t: TestController, screenshotName: string): Promise { - const scheduler = new Scheduler(SCHEDULER_SELECTOR); - const screenshotZone = page.locator('.dx-scheduler-work-space'); - const appointmentToEdit = scheduler.getAppointment(TEST_APPOINTMENT_TEXT); - await (appointmentToEdit.element, TEST_CURSOR_OPTIONS).dblclick(); - - const appointmentDialog = new AppointmentDialog(); - await (appointmentDialog.series).click(); - - const { appointmentPopup } = scheduler; - await (appointmentPopup.saveButton.element).click(); - - await testScreenshot(page, `${SCREENSHOT_BASE_NAME}__${screenshotName}.png`, { element: screenshotZone }); - - expect(compareResults.isValid()) - .ok(compareResults.errorMessages()); -} - -async function dragAndDropTestFunction( - t: TestController, - screenshotName: string, - { rowIdx, cellIdx }: ITestDragNDropOptions, -): Promise { - const scheduler = new Scheduler(SCHEDULER_SELECTOR); - const screenshotZone = page.locator('.dx-scheduler-work-space'); - const appointmentToEdit = scheduler.getAppointment(TEST_APPOINTMENT_TEXT); - const cellToMoveElement = page.locator('.dx-scheduler-date-table-row').nth(rowIdx).locator('.dx-scheduler-date-table-cell').nth(cellIdx); - - await /* TODO: dragToElement(appointmentToEdit.element, cellToMoveElement, TEST_CURSOR_OPTIONS) */; - - const appointmentDialog = new AppointmentDialog(); - await (appointmentDialog.series).click(); - - await testScreenshot(page, `${SCREENSHOT_BASE_NAME}__${screenshotName}.png`, { element: screenshotZone }); - expect(compareResults.isValid()) - .ok(compareResults.errorMessages()); -} - -async function resizeTestFunction( - t: TestController, - screenshotName: string, - resizeOptions: ITestResizeOptions, -): Promise { - const scheduler = new Scheduler(SCHEDULER_SELECTOR); - const screenshotZone = page.locator('.dx-scheduler-work-space'); - const appointmentToEdit = scheduler.getAppointment(TEST_APPOINTMENT_TEXT); - - await t.drag( - appointmentToEdit.resizableHandle[resizeOptions.direction], - 0, - resizeOptions.value, - TEST_CURSOR_OPTIONS, - ); - - const appointmentDialog = new AppointmentDialog(); - await (appointmentDialog.series).click(); - - await testScreenshot(page, `${SCREENSHOT_BASE_NAME}__${screenshotName}.png`, { element: screenshotZone }); - expect(compareResults.isValid()) - .ok(compareResults.errorMessages()); -} - -async function configureScheduler({ start, end }: { start: Date; end: Date }) { +async function configureScheduler(page, { start, end }: { start: Date; end: Date }) { await createWidget(page, 'dxScheduler', { dataSource: [{ startDate: start, @@ -120,100 +28,74 @@ async function configureScheduler({ start, end }: { start: Date; end: Date }) { }); } -// === EDITING POPUP === -test('Editing popup: should have correctly been edited from editing popup. DST - winter time', async ({ page }) => { - // --- setup --- -await configureScheduler(APPOINTMENT_DATETIME.winter); - // --- test --- -await editingPopupTestFunction(t, 'popup__winter-time'); -}); +test.describe('Editing recurrent appointment in DST time', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); -test('Editing popup: should have correctly been edited from editing popup. DST - summer time', async ({ page }) => { - // --- setup --- -await configureScheduler(APPOINTMENT_DATETIME.summer); - // --- test --- -await editingPopupTestFunction(t, 'popup__summer-time'); -}); + test('Editing popup: winter time', async ({ page }) => { + await configureScheduler(page, APPOINTMENT_DATETIME.winter); + const appointment = page.locator('.dx-scheduler-appointment').filter({ hasText: TEST_APPOINTMENT_TEXT }); + await appointment.dblclick(); -// === DRAG_N_DROP === -test('Drag-n-drop up: should have correctly been edited. DST - winter time', async ({ page }) => { - // --- setup --- -await configureScheduler(APPOINTMENT_DATETIME.winter); - // --- test --- -await dragAndDropTestFunction(t, 'drag-n-drop-up__winter-time', { - rowIdx: 1, - cellIdx: 1, - }); -}); + const dialog = page.locator('.dx-dialog'); + const seriesBtn = dialog.locator('.dx-dialog-button').last(); + await seriesBtn.click(); -test('Drag-n-drop down: should have correctly been edited. DST - winter time', async ({ page }) => { - // --- setup --- -await configureScheduler(APPOINTMENT_DATETIME.winter); - // --- test --- -await dragAndDropTestFunction(t, 'drag-n-drop-down__winter-time', { - rowIdx: 4, - cellIdx: 1, - }); -}); + const popup = page.locator('.dx-scheduler-appointment-popup'); + const saveButton = popup.locator('.dx-popup-done.dx-button'); + await saveButton.click(); -test('Drag-n-drop up: should have correctly been edited. DST - summer time', async ({ page }) => { - // --- setup --- -await configureScheduler(APPOINTMENT_DATETIME.summer); - // --- test --- -await dragAndDropTestFunction(t, 'drag-n-drop-up__summer-time', { - rowIdx: 1, - cellIdx: 1, + const workSpace = page.locator('.dx-scheduler-work-space'); + await testScreenshot(page, `${SCREENSHOT_BASE_NAME}__popup__winter-time.png`, { element: workSpace }); }); -}); -test('Drag-n-drop down: should have correctly been edited. DST - summer time', async ({ page }) => { - // --- setup --- -await configureScheduler(APPOINTMENT_DATETIME.summer); - // --- test --- -await dragAndDropTestFunction(t, 'drag-n-drop-down__summer-time', { - rowIdx: 4, - cellIdx: 1, - }); -}); + test('Editing popup: summer time', async ({ page }) => { + await configureScheduler(page, APPOINTMENT_DATETIME.summer); + const appointment = page.locator('.dx-scheduler-appointment').filter({ hasText: TEST_APPOINTMENT_TEXT }); + await appointment.dblclick(); -// === RESIZE === -test('Resize top: should have correctly been edited. DST - winter time', async ({ page }) => { - // --- setup --- -await configureScheduler(APPOINTMENT_DATETIME.winter); - // --- test --- -await resizeTestFunction(t, 'resize-top__winter-time', { - direction: 'top', - value: 100, - }); -}); + const dialog = page.locator('.dx-dialog'); + const seriesBtn = dialog.locator('.dx-dialog-button').last(); + await seriesBtn.click(); + + const popup = page.locator('.dx-scheduler-appointment-popup'); + const saveButton = popup.locator('.dx-popup-done.dx-button'); + await saveButton.click(); -test('Resize bottom: should have correctly been edited. DST - winter time', async ({ page }) => { - // --- setup --- -await configureScheduler(APPOINTMENT_DATETIME.winter); - // --- test --- -await resizeTestFunction(t, 'resize-bottom__winter-time', { - direction: 'bottom', - value: 100, + const workSpace = page.locator('.dx-scheduler-work-space'); + await testScreenshot(page, `${SCREENSHOT_BASE_NAME}__popup__summer-time.png`, { element: workSpace }); }); -}); -test('Resize top: should have correctly been edited. DST - summer time', async ({ page }) => { - // --- setup --- -await configureScheduler(APPOINTMENT_DATETIME.summer); - // --- test --- -await resizeTestFunction(t, 'resize-top__summer-time', { - direction: 'top', - value: 100, + test('Drag-n-drop up: winter time', async ({ page }) => { + await configureScheduler(page, APPOINTMENT_DATETIME.winter); + const appointment = page.locator('.dx-scheduler-appointment').filter({ hasText: TEST_APPOINTMENT_TEXT }); + const targetCell = page.locator('.dx-scheduler-date-table-row').nth(1).locator('.dx-scheduler-date-table-cell').nth(1); + await appointment.dragTo(targetCell); + + const dialog = page.locator('.dx-dialog'); + const seriesBtn = dialog.locator('.dx-dialog-button').last(); + await seriesBtn.click(); + + const workSpace = page.locator('.dx-scheduler-work-space'); + await testScreenshot(page, `${SCREENSHOT_BASE_NAME}__drag-n-drop-up__winter-time.png`, { element: workSpace }); }); -}); -test('Resize bottom: should have correctly been edited. DST - summer time', async ({ page }) => { - // --- setup --- -await configureScheduler(APPOINTMENT_DATETIME.summer); - // --- test --- -await resizeTestFunction(t, 'resize-bottom__summer-time', { - direction: 'bottom', - value: 100, + test('Resize bottom: winter time', async ({ page }) => { + await configureScheduler(page, APPOINTMENT_DATETIME.winter); + const appointment = page.locator('.dx-scheduler-appointment').filter({ hasText: TEST_APPOINTMENT_TEXT }); + const bottomHandle = appointment.locator('.dx-resizable-handle-bottom'); + + await bottomHandle.hover(); + await page.mouse.down(); + await page.mouse.move(0, 100, { steps: 5 }); + await page.mouse.up(); + + const dialog = page.locator('.dx-dialog'); + const seriesBtn = dialog.locator('.dx-dialog-button').last(); + await seriesBtn.click(); + + const workSpace = page.locator('.dx-scheduler-work-space'); + await testScreenshot(page, `${SCREENSHOT_BASE_NAME}__resize-bottom__winter-time.png`, { element: workSpace }); }); }); -}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/timezone/recurrence/yearlyRecurrentAppointment.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/timezone/recurrence/yearlyRecurrentAppointment.spec.ts index b1aabd014ff1..8ef5e3ede52b 100644 --- a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/timezone/recurrence/yearlyRecurrentAppointment.spec.ts +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/timezone/recurrence/yearlyRecurrentAppointment.spec.ts @@ -1,365 +1,81 @@ -import { test, expect } from '@playwright/test'; -import { createWidget } from '../../../../../playwright-helpers'; -import path from 'path'; +import { test } from '@playwright/test'; +import { createWidget, testScreenshot, getContainerUrl, setupTestPage } from '../../../../../playwright-helpers'; -const containerUrl = `file://${path.resolve(__dirname, '../../../../../tests/container.html')}`; - -test.describe('Yearly recurrent appointments with timezones', () => { - test.beforeEach(async ({ page }) => { - await page.goto(containerUrl); - await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); - await page.evaluate((theme) => new Promise((resolve) => { - (window as any).DevExpress.ui.themes.ready(resolve); - (window as any).DevExpress.ui.themes.current(theme); - }), process.env.THEME || 'fluent.blue.light'); - }); +const containerUrl = getContainerUrl(__dirname, '../../../../../tests/container.html'); const SCREENSHOT_BASE_NAME = 'timezone-yearly-recurrent'; - -); - -test('Should correctly display the recurrent yearly appointment with the same timezone', async ({ page }) => { - // --- setup --- -const appointmentTimezone = 'Etc/GMT+1'; - const schedulerTimezone = 'Etc/GMT+1'; - - await createWidget(page, 'dxScheduler', { - dataSource: [{ - allDay: false, - startDate: getAppointmentTime(new Date(2021, 3, 28, 10, 0, 0), appointmentTimezone), - startDateTimeZone: appointmentTimezone, - endDate: getAppointmentTime(new Date(2021, 3, 28, 12, 0, 0), appointmentTimezone), - endDateTimeZone: appointmentTimezone, - recurrenceRule: 'FREQ=YEARLY;BYMONTHDAY=28;BYMONTH=4', - text: 'Test', - }], - timeZone: schedulerTimezone, - currentView: 'week', - currentDate: new Date(2021, 3, 28), - startDayHour: 0, - cellDuration: 180, - width: 1000, - height: 585, - // --- test --- -// expected date: 4/28/2021 10:00 AM - 12:00 PM - await screenshotTestFunc(t, SCREENSHOT_BASE_NAME, 'same-date__same-timezone'); -}); -}); - -test('Should correctly display the recurrent yearly appointment with a greater time timezone', async ({ page }) => { - // --- setup --- -const appointmentTimezone = 'Etc/GMT+10'; - const schedulerTimezone = 'Etc/GMT-2'; - - await createWidget(page, 'dxScheduler', { - dataSource: [{ - allDay: false, - startDate: getAppointmentTime(new Date(2021, 3, 28, 14, 0, 0), appointmentTimezone), - startDateTimeZone: appointmentTimezone, - endDate: getAppointmentTime(new Date(2021, 3, 28, 16, 0, 0), appointmentTimezone), - endDateTimeZone: appointmentTimezone, - recurrenceRule: 'FREQ=YEARLY;BYMONTHDAY=28;BYMONTH=4', - text: 'Test', - }], - timeZone: schedulerTimezone, - currentView: 'week', - currentDate: new Date(2021, 3, 28), - startDayHour: 0, - cellDuration: 180, - width: 1000, - height: 585, - // --- test --- -// expected date: 4/29/2021 2:00 AM - 4:00 AM - await screenshotTestFunc(t, SCREENSHOT_BASE_NAME, 'same-date__greater-timezone'); -}); -}); - -test('Should correctly display the recurrent yearly appointment with a lower time timezone', async ({ page }) => { - // --- setup --- -const appointmentTimezone = 'Etc/GMT-2'; - const schedulerTimezone = 'Etc/GMT+10'; - - await createWidget(page, 'dxScheduler', { - dataSource: [{ - allDay: false, - startDate: getAppointmentTime(new Date(2021, 3, 28, 4, 0, 0), appointmentTimezone), - startDateTimeZone: appointmentTimezone, - endDate: getAppointmentTime(new Date(2021, 3, 28, 6, 0, 0), appointmentTimezone), - endDateTimeZone: appointmentTimezone, - recurrenceRule: 'FREQ=YEARLY;BYMONTHDAY=28;BYMONTH=4', - text: 'Test', - }], - timeZone: schedulerTimezone, - currentView: 'week', - currentDate: new Date(2021, 3, 28), - startDayHour: 0, - cellDuration: 180, - width: 1000, - height: 585, - // --- test --- -// expected date: 4/27/2021 2:00 PM - 4:00 PM - await screenshotTestFunc(t, SCREENSHOT_BASE_NAME, 'same-date__lower-timezone'); -}); -}); - -test(`Should correctly display the recurrent yearly appointment if start date -lower than recurrent date with the same timezone`, async ({ page }) => { - // --- setup --- -const appointmentTimezone = 'Etc/GMT+1'; - const schedulerTimezone = 'Etc/GMT+1'; - - await createWidget(page, 'dxScheduler', { - dataSource: [{ - allDay: false, - startDate: getAppointmentTime(new Date(2021, 3, 26, 10, 0, 0), appointmentTimezone), - startDateTimeZone: appointmentTimezone, - endDate: getAppointmentTime(new Date(2021, 3, 26, 12, 0, 0), appointmentTimezone), - endDateTimeZone: appointmentTimezone, - recurrenceRule: 'FREQ=YEARLY;BYMONTHDAY=28;BYMONTH=4', - text: 'Test', - }], - timeZone: schedulerTimezone, - currentView: 'week', - currentDate: new Date(2021, 3, 28), - startDayHour: 0, - cellDuration: 180, - width: 1000, - height: 585, - // --- test --- -// expected date: 4/28/2021 10:00 AM - 12:00 PM - await screenshotTestFunc(t, SCREENSHOT_BASE_NAME, 'lower-date__same-timezone'); -}); -}); - -test(`Should correctly display the recurrent yearly appointment if start date -lower than recurrent date with a greater time timezone`, async ({ page }) => { - // --- setup --- -const appointmentTimezone = 'Etc/GMT+10'; - const schedulerTimezone = 'Etc/GMT-2'; - - await createWidget(page, 'dxScheduler', { - dataSource: [{ - allDay: false, - startDate: getAppointmentTime(new Date(2021, 3, 26, 14, 0, 0), appointmentTimezone), - startDateTimeZone: appointmentTimezone, - endDate: getAppointmentTime(new Date(2021, 3, 26, 16, 0, 0), appointmentTimezone), - endDateTimeZone: appointmentTimezone, - recurrenceRule: 'FREQ=YEARLY;BYMONTHDAY=28;BYMONTH=4', - text: 'Test', - }], - timeZone: schedulerTimezone, - currentView: 'week', - currentDate: new Date(2021, 3, 28), - startDayHour: 0, - cellDuration: 180, - width: 1000, - height: 585, - // --- test --- -// expected date: 4/29/2021 2:00 AM - 4:00 PM - await screenshotTestFunc(t, SCREENSHOT_BASE_NAME, 'lower-date__greater-timezone'); -}); -}); - -test(`Should correctly display the recurrent yearly appointment if start date -lower than recurrent date with a lower time timezone`, async ({ page }) => { - // --- setup --- -const appointmentTimezone = 'Etc/GMT-2'; - const schedulerTimezone = 'Etc/GMT+10'; - - await createWidget(page, 'dxScheduler', { - dataSource: [{ - allDay: false, - startDate: getAppointmentTime(new Date(2021, 3, 26, 4, 0, 0), appointmentTimezone), - startDateTimeZone: appointmentTimezone, - endDate: getAppointmentTime(new Date(2021, 3, 26, 6, 0, 0), appointmentTimezone), - endDateTimeZone: appointmentTimezone, - recurrenceRule: 'FREQ=YEARLY;BYMONTHDAY=28;BYMONTH=4', - text: 'Test', - }], - timeZone: schedulerTimezone, - currentView: 'week', - currentDate: new Date(2021, 3, 28), - startDayHour: 0, - cellDuration: 180, - width: 1000, - height: 585, - // --- test --- -// expected date: 4/27/2021 4:00 PM - 6:00 PM - await screenshotTestFunc(t, SCREENSHOT_BASE_NAME, 'lower-date__lower-timezone'); -}); -}); - -test(`Should correctly display the recurrent yearly appointment at first date if start date -greater than recurrent date with the same timezone`, async ({ page }) => { - // --- setup --- -const appointmentTimezone = 'Etc/GMT+1'; - const schedulerTimezone = 'Etc/GMT+1'; - - await createWidget(page, 'dxScheduler', { - dataSource: [{ - allDay: false, - startDate: getAppointmentTime(new Date(2021, 3, 29, 10, 0, 0), appointmentTimezone), - startDateTimeZone: appointmentTimezone, - endDate: getAppointmentTime(new Date(2021, 3, 29, 12, 0, 0), appointmentTimezone), - endDateTimeZone: appointmentTimezone, - recurrenceRule: 'FREQ=YEARLY;BYMONTHDAY=28;BYMONTH=4', - text: 'Test', - }], - timeZone: schedulerTimezone, - currentView: 'week', - currentDate: new Date(2021, 3, 28), - startDayHour: 0, - cellDuration: 180, - width: 1000, - height: 585, - // --- test --- -// expected no visible date - await screenshotTestFunc(t, SCREENSHOT_BASE_NAME, 'greater-date__same-timezone__same-view-date'); -}); -}); - -test(`Should correctly display the recurrent yearly appointment at next date if start date -greater than recurrent date with the same timezone`, async ({ page }) => { - // --- setup --- -const appointmentTimezone = 'Etc/GMT+1'; - const schedulerTimezone = 'Etc/GMT+1'; - - await createWidget(page, 'dxScheduler', { - dataSource: [{ - allDay: false, - startDate: getAppointmentTime(new Date(2021, 3, 29, 10, 0, 0), appointmentTimezone), - startDateTimeZone: appointmentTimezone, - endDate: getAppointmentTime(new Date(2021, 3, 29, 12, 0, 0), appointmentTimezone), - endDateTimeZone: appointmentTimezone, - recurrenceRule: 'FREQ=YEARLY;BYMONTHDAY=28;BYMONTH=4', - text: 'Test', - }], - timeZone: schedulerTimezone, - currentView: 'week', - currentDate: new Date(2022, 3, 28), - startDayHour: 0, - cellDuration: 180, - width: 1000, - height: 585, - // --- test --- -// expected date: 4/28/2022 10:00 AM - 12:00 PM - await screenshotTestFunc(t, SCREENSHOT_BASE_NAME, 'greater-date__same-timezone__next-view-date'); -}); -}); - -test(`Should correctly display the recurrent yearly appointment at first date if start date -greater than recurrent date with a greater time timezone`, async ({ page }) => { - // --- setup --- -const appointmentTimezone = 'Etc/GMT+10'; - const schedulerTimezone = 'Etc/GMT-2'; - - await createWidget(page, 'dxScheduler', { - dataSource: [{ - allDay: false, - startDate: getAppointmentTime(new Date(2021, 3, 29, 14, 0, 0), appointmentTimezone), - startDateTimeZone: appointmentTimezone, - endDate: getAppointmentTime(new Date(2021, 3, 29, 16, 0, 0), appointmentTimezone), - endDateTimeZone: appointmentTimezone, - recurrenceRule: 'FREQ=YEARLY;BYMONTHDAY=28;BYMONTH=4', - text: 'Test', - }], - timeZone: schedulerTimezone, - currentView: 'week', - currentDate: new Date(2021, 3, 28), - startDayHour: 0, - cellDuration: 180, - width: 1000, - height: 585, - // --- test --- -// expected no visible date - await screenshotTestFunc(t, SCREENSHOT_BASE_NAME, 'greater-date__greater-timezone__same-view-date'); -}); +const getScreenshotName = (baseName: string, suffix: string) => `${baseName}__${suffix}.png`; +const MINUTES_TO_MILLISECONDS = 60000; +const HOURS_TO_MILLISECONDS = MINUTES_TO_MILLISECONDS * 60; + +const generateTimezoneOffsets = (): Record => { + const result: Record = {}; + new Array(27).fill(0).forEach((_, idx) => { + const timezoneIdx = idx - 14; + if (timezoneIdx < 0) result[`Etc/GMT${timezoneIdx}`] = timezoneIdx * -1; + else if (timezoneIdx > 0) result[`Etc/GMT+${timezoneIdx}`] = timezoneIdx * -1; + else result['Etc/GMT'] = 0; + }); + return result; +}; +const TIMEZONE_OFFSETS = generateTimezoneOffsets(); +const getAppointmentTime = (desiredDate: Date, timezone: string): Date => { + const localOffset = desiredDate.getTimezoneOffset() * MINUTES_TO_MILLISECONDS; + const timezoneOffset = TIMEZONE_OFFSETS[timezone] * HOURS_TO_MILLISECONDS; + return new Date(desiredDate.getTime() - localOffset - timezoneOffset); +}; + +async function screenshotTest(page, screenshotName: string): Promise { + const workSpace = page.locator('.dx-scheduler-work-space'); + await testScreenshot(page, getScreenshotName(SCREENSHOT_BASE_NAME, screenshotName), { element: workSpace }); +} + +const makeOptions = (apptTz: string, schedTz: string, start: Date, end: Date, currentDate?: Date) => ({ + dataSource: [{ + allDay: false, + startDate: getAppointmentTime(start, apptTz), + startDateTimeZone: apptTz, + endDate: getAppointmentTime(end, apptTz), + endDateTimeZone: apptTz, + recurrenceRule: 'FREQ=YEARLY;BYMONTHDAY=28;BYMONTH=4', + text: 'Test', + }], + timeZone: schedTz, + currentView: 'week', + currentDate: currentDate ?? new Date(2021, 3, 28), + startDayHour: 0, + cellDuration: 180, + width: 1000, + height: 585, }); -test(`Should correctly display the recurrent yearly appointment at next date if start date -greater than recurrent date with a greater time timezone`, async ({ page }) => { - // --- setup --- -const appointmentTimezone = 'Etc/GMT+10'; - const schedulerTimezone = 'Etc/GMT-2'; +test.describe('Yearly recurrent appointments with timezones', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); - await createWidget(page, 'dxScheduler', { - dataSource: [{ - allDay: false, - startDate: getAppointmentTime(new Date(2021, 3, 29, 14, 0, 0), appointmentTimezone), - startDateTimeZone: appointmentTimezone, - endDate: getAppointmentTime(new Date(2021, 3, 29, 16, 0, 0), appointmentTimezone), - endDateTimeZone: appointmentTimezone, - recurrenceRule: 'FREQ=YEARLY;BYMONTHDAY=28;BYMONTH=4', - text: 'Test', - }], - timeZone: schedulerTimezone, - currentView: 'week', - currentDate: new Date(2022, 3, 28), - startDayHour: 0, - cellDuration: 180, - width: 1000, - height: 585, - // --- test --- -// expected date: 4/29/2022 2:00 AM - 4:00 PM - await screenshotTestFunc(t, SCREENSHOT_BASE_NAME, 'greater-date__greater-timezone__next-view-date'); -}); -}); + test('same timezone', async ({ page }) => { + await createWidget(page, 'dxScheduler', makeOptions('Etc/GMT+1', 'Etc/GMT+1', new Date(2021, 3, 28, 10, 0, 0), new Date(2021, 3, 28, 12, 0, 0))); + await screenshotTest(page, 'same-date__same-timezone'); + }); -test(`Should correctly display the recurrent yearly appointment at first date if start date -greater than recurrent date with a lower time timezone`, async ({ page }) => { - // --- setup --- -const appointmentTimezone = 'Etc/GMT-2'; - const schedulerTimezone = 'Etc/GMT+10'; + test('greater timezone', async ({ page }) => { + await createWidget(page, 'dxScheduler', makeOptions('Etc/GMT+10', 'Etc/GMT-2', new Date(2021, 3, 28, 14, 0, 0), new Date(2021, 3, 28, 16, 0, 0))); + await screenshotTest(page, 'same-date__greater-timezone'); + }); - await createWidget(page, 'dxScheduler', { - dataSource: [{ - allDay: false, - startDate: getAppointmentTime(new Date(2021, 3, 29, 4, 0, 0), appointmentTimezone), - startDateTimeZone: appointmentTimezone, - endDate: getAppointmentTime(new Date(2021, 3, 29, 6, 0, 0), appointmentTimezone), - endDateTimeZone: appointmentTimezone, - recurrenceRule: 'FREQ=YEARLY;BYMONTHDAY=28;BYMONTH=4', - text: 'Test', - }], - timeZone: schedulerTimezone, - currentView: 'week', - currentDate: new Date(2021, 3, 28), - startDayHour: 0, - cellDuration: 180, - width: 1000, - height: 585, - // --- test --- -// expected no visible date - await screenshotTestFunc(t, SCREENSHOT_BASE_NAME, 'greater-date__lower-timezone__same-view-date'); -}); -}); + test('lower timezone', async ({ page }) => { + await createWidget(page, 'dxScheduler', makeOptions('Etc/GMT-2', 'Etc/GMT+10', new Date(2021, 3, 28, 4, 0, 0), new Date(2021, 3, 28, 6, 0, 0))); + await screenshotTest(page, 'same-date__lower-timezone'); + }); -test(`Should correctly display the recurrent yearly appointment at next date if start date -greater than recurrent date with a lower time timezone`, async ({ page }) => { - // --- setup --- -const appointmentTimezone = 'Etc/GMT-2'; - const schedulerTimezone = 'Etc/GMT+10'; + test('lower date same timezone', async ({ page }) => { + await createWidget(page, 'dxScheduler', makeOptions('Etc/GMT+1', 'Etc/GMT+1', new Date(2021, 3, 26, 10, 0, 0), new Date(2021, 3, 26, 12, 0, 0))); + await screenshotTest(page, 'lower-date__same-timezone'); + }); - await createWidget(page, 'dxScheduler', { - dataSource: [{ - allDay: false, - startDate: getAppointmentTime(new Date(2021, 3, 29, 4, 0, 0), appointmentTimezone), - startDateTimeZone: appointmentTimezone, - endDate: getAppointmentTime(new Date(2021, 3, 29, 6, 0, 0), appointmentTimezone), - endDateTimeZone: appointmentTimezone, - recurrenceRule: 'FREQ=YEARLY;BYMONTHDAY=28;BYMONTH=4', - text: 'Test', - }], - timeZone: schedulerTimezone, - currentView: 'week', - currentDate: new Date(2022, 3, 28), - startDayHour: 0, - cellDuration: 180, - width: 1000, - height: 585, - // --- test --- -// expected date: 4/27/2022 4:00 PM - 6:00 PM - await screenshotTestFunc(t, SCREENSHOT_BASE_NAME, 'greater-date__lower-timezone__next-view-date'); -}); -}); + test('greater date same timezone next view date', async ({ page }) => { + await createWidget(page, 'dxScheduler', makeOptions('Etc/GMT+1', 'Etc/GMT+1', new Date(2021, 3, 29, 10, 0, 0), new Date(2021, 3, 29, 12, 0, 0), new Date(2022, 3, 28))); + await screenshotTest(page, 'greater-date__same-timezone__next-view-date'); + }); }); From f4a3f374d5382807b0544aeb0b2c3fa3ac31c48d Mon Sep 17 00:00:00 2001 From: Aleksey Semikozov Date: Sat, 21 Mar 2026 23:15:08 -0300 Subject: [PATCH 05/13] Playwright POC - timezone and viewOffset test improvements --- .../scheduler/common/a11y/contrast.spec.ts | 42 +-- .../common/dragAndDrop/dragEvents.spec.ts | 231 ++++-------- .../dragAndDrop/externalDragging.spec.ts | 101 +++--- .../common/dragAndDrop/timeline.spec.ts | 92 ++--- .../common/legacyAppointmentForm.spec.ts | 330 +++++++----------- 5 files changed, 319 insertions(+), 477 deletions(-) diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/a11y/contrast.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/a11y/contrast.spec.ts index 1f437c928908..e52f5b943f4f 100644 --- a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/a11y/contrast.spec.ts +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/a11y/contrast.spec.ts @@ -1,37 +1,21 @@ -import { test, expect } from '@playwright/test'; -import { createWidget, testScreenshot } from '../../../../playwright-helpers'; -import path from 'path'; +import { test } from '@playwright/test'; +import { createWidget, testScreenshot, getContainerUrl, setupTestPage } from '../../../../playwright-helpers'; -const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; +const containerUrl = getContainerUrl(__dirname, '../../../../tests/container.html'); test.describe('a11y - contrast', () => { test.beforeEach(async ({ page }) => { - await page.goto(containerUrl); - await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); - await page.evaluate((theme) => new Promise((resolve) => { - (window as any).DevExpress.ui.themes.ready(resolve); - (window as any).DevExpress.ui.themes.current(theme); - }), process.env.THEME || 'fluent.blue.light'); + await setupTestPage(page, containerUrl); }); -); + test('Scheduler a11y: Insufficient contrast of day numbers in the MonthView', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource: [], + currentView: 'month', + currentDate: new Date(2020, 10, 25), + }); -// visual: generic.light -// visual: generic.dark -// visual: fluent.light -// visual: fluent.dark -test('Scheduler a11y: Insufficient contrast of day numbers in the MonthView', async ({ page }) => { - // --- setup --- -await createWidget(page, 'dxScheduler', { - dataSource: [], - currentView: 'month', - currentDate: new Date(2020, 10, 25), - // --- test --- -// Scheduler on '#container' - await testScreenshot(page, 'month_day_number_contrast.png', { element: page.locator('.dx-scheduler') }); - - expect(compareResults.isValid()) - .ok(compareResults.errorMessages()); -}); -}); + const scheduler = page.locator('.dx-scheduler'); + await testScreenshot(page, 'month_day_number_contrast.png', { element: scheduler }); + }); }); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/dragEvents.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/dragEvents.spec.ts index 426fd1bc40f4..41f775c1645f 100644 --- a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/dragEvents.spec.ts +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/dragEvents.spec.ts @@ -1,187 +1,108 @@ import { test, expect } from '@playwright/test'; -import { createWidget } from '../../../../playwright-helpers'; -import path from 'path'; +import { createWidget, getContainerUrl, setupTestPage } from '../../../../playwright-helpers'; -const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; - -test.describe('Scheduler dragging - drag events', () => { - test.beforeEach(async ({ page }) => { - await page.goto(containerUrl); - await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); - await page.evaluate((theme) => new Promise((resolve) => { - (window as any).DevExpress.ui.themes.ready(resolve); - (window as any).DevExpress.ui.themes.current(theme); - }), process.env.THEME || 'fluent.blue.light'); - }); - -); - -const SCHEDULER_SELECTOR = '#container'; - -const initCallbackTesting = async () => { - await CallbackTestHelper.initClientTesting([ - 'onDragStartItemData', - 'onDragMoveItemData', - 'onDragEndItemData', - 'onDragEndToItemData', - ]); -}; - -const clearCallbackTesting = async () => { - await CallbackTestHelper.clearClientData([ - 'onDragStartItemData', - 'onDragMoveItemData', - 'onDragEndItemData', - 'onDragEndToItemData', - ]); -}; - -const collectEventsCallbackResults = async () => [ - await CallbackTestHelper.getClientResults('onDragStartItemData'), - await CallbackTestHelper.getClientResults('onDragMoveItemData'), - await CallbackTestHelper.getClientResults('onDragEndItemData'), - await CallbackTestHelper.getClientResults('onDragEndToItemData'), -]; +const containerUrl = getContainerUrl(__dirname, '../../../../tests/container.html'); const INITIAL_APPOINTMENT = { text: 'Test', startDate: '2023-01-01T01:00:00', endDate: '2023-01-01T02:00:00', }; + const TEST_CASES = [ { view: 'month', - expectedToItemData: { - text: 'Test', - startDate: '2023-01-05T01:00:00', - endDate: '2023-01-05T02:00:00', - }, + expectedToItemData: { text: 'Test', startDate: '2023-01-05T01:00:00', endDate: '2023-01-05T02:00:00' }, }, { view: 'week', - expectedToItemData: { - text: 'Test', - startDate: '2023-01-05T00:00:00', - endDate: '2023-01-05T01:00:00', - allDay: true, - }, + expectedToItemData: { text: 'Test', startDate: '2023-01-05T00:00:00', endDate: '2023-01-05T01:00:00', allDay: true }, }, { view: 'timelineDay', - expectedToItemData: { - text: 'Test', - startDate: '2023-01-01T01:30:00', - endDate: '2023-01-01T02:30:00', - allDay: false, - }, + expectedToItemData: { text: 'Test', startDate: '2023-01-01T01:30:00', endDate: '2023-01-01T02:30:00', allDay: false }, }, ]; -TEST_CASES.forEach(({ view, expectedToItemData }) => { - test(`Should fire correct events with correct itemData inside during drag-n-drop in ${view} view.`, async ({ page }) => { - // --- setup --- -await initCallbackTesting(); - await createWidget(page, 'dxScheduler', { - dataSource: [INITIAL_APPOINTMENT], - currentView: view, - currentDate: '2023-01-01', - appointmentDragging: { - onDragStart: ({ itemData }) => { - (window as WindowCallbackExtended) - .clientTesting! - .addCallbackResult('onDragStartItemData', { ...itemData - // --- test --- -const scheduler = new Scheduler(SCHEDULER_SELECTOR); - const appointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'Test' }); - const targetCell = page.locator('.dx-scheduler-date-table-row').nth(0).locator('.dx-scheduler-date-table-cell').nth(4); - - await t - .dragToElement(appointment.element, targetCell, { speed: 0.5 }); - - const [ - onDragStartItemData, - onDragMoveItemData, - onDragEndItemData, - onDragEndToItemData, - ] = await collectEventsCallbackResults(); - - expect(onDragStartItemData.length).toBe(1) - .expect(onDragStartItemData[0]).toBe(INITIAL_APPOINTMENT); - - // eslint-disable-next-line no-restricted-syntax - for (const itemData of onDragMoveItemData) { - expect(itemData).toBe(INITIAL_APPOINTMENT); - } - - expect(onDragEndItemData.length).toBe(1) - .expect(onDragEndToItemData.length).toBe(1) - .expect(onDragEndItemData[0]) - .eql(INITIAL_APPOINTMENT) - .expect(onDragEndToItemData[0]) - .eql(expectedToItemData); -}); - }, - onDragMove: ({ itemData }) => { - (window as WindowCallbackExtended) - .clientTesting! - .addCallbackResult('onDragMoveItemData', { ...itemData }); - }, - onDragEnd: ({ itemData, toItemData }) => { - const clientTesting = (window as WindowCallbackExtended).clientTesting!; - clientTesting.addCallbackResult('onDragEndItemData', { ...itemData }); - clientTesting.addCallbackResult('onDragEndToItemData', { ...toItemData }); +test.describe('Scheduler dragging - drag events', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); + + TEST_CASES.forEach(({ view, expectedToItemData }) => { + test(`Should fire correct events with correct itemData inside during drag-n-drop in ${view} view.`, async ({ page }) => { + await page.evaluate(() => { + (window as any).clientTestingResults = { + onDragStartItemData: [], + onDragMoveItemData: [], + onDragEndItemData: [], + onDragEndToItemData: [], + }; + }); + + await createWidget(page, 'dxScheduler', { + dataSource: [INITIAL_APPOINTMENT], + currentView: view, + currentDate: '2023-01-01', + appointmentDragging: { + onDragStart: new Function('e', 'window.clientTestingResults.onDragStartItemData.push(Object.assign({}, e.itemData));') as any, + onDragMove: new Function('e', 'window.clientTestingResults.onDragMoveItemData.push(Object.assign({}, e.itemData));') as any, + onDragEnd: new Function('e', 'window.clientTestingResults.onDragEndItemData.push(Object.assign({}, e.itemData)); window.clientTestingResults.onDragEndToItemData.push(Object.assign({}, e.toItemData));') as any, }, - }, + }); + + const appointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'Test' }); + const targetCell = page.locator('.dx-scheduler-date-table-row').nth(0).locator('.dx-scheduler-date-table-cell').nth(4); + + await appointment.dragTo(targetCell); + + const results = await page.evaluate(() => (window as any).clientTestingResults); + + expect(results.onDragStartItemData.length).toBe(1); + expect(results.onDragStartItemData[0]).toEqual(INITIAL_APPOINTMENT); + + for (const itemData of results.onDragMoveItemData) { + expect(itemData).toEqual(INITIAL_APPOINTMENT); + } + + expect(results.onDragEndItemData.length).toBe(1); + expect(results.onDragEndToItemData.length).toBe(1); + expect(results.onDragEndItemData[0]).toEqual(INITIAL_APPOINTMENT); + expect(results.onDragEndToItemData[0]).toEqual(expectedToItemData); }); - }).after(async () => { - await clearCallbackTesting(); }); -}); -test('Should block appointment dragging while onAppointmentUpdating Promise is pending (T1308596)', async ({ page }) => { - const scheduler = new Scheduler(SCHEDULER_SELECTOR); - const appointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'Test Appointment' }); - - const targetCell1 = page.locator('.dx-scheduler-date-table-row').nth(18).locator('.dx-scheduler-date-table-cell').nth(2); - const targetCell2 = page.locator('.dx-scheduler-date-table-row').nth(18).locator('.dx-scheduler-date-table-cell').nth(5); + test('Should block appointment dragging while onAppointmentUpdating Promise is pending (T1308596)', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource: [{ + text: 'Test Appointment', + startDate: new Date(2023, 0, 2, 10, 0), + endDate: new Date(2023, 0, 2, 11, 0), + }], + views: ['week'], + currentView: 'week', + currentDate: new Date(2023, 0, 2), + height: 600, + onAppointmentUpdating: new Function('e', 'e.cancel = new Promise(function(resolve) { setTimeout(function() { resolve(false); }, 5000); });') as any, + }); - const initialPosition = await appointment.element.boundingClientRect; + const appointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'Test Appointment' }); + const targetCell1 = page.locator('.dx-scheduler-date-table-row').nth(18).locator('.dx-scheduler-date-table-cell').nth(2); + const targetCell2 = page.locator('.dx-scheduler-date-table-row').nth(18).locator('.dx-scheduler-date-table-cell').nth(5); - await /* TODO: dragToElement(appointment.element, targetCell1, { speed: 1 }) */; - await /* TODO: dragToElement(appointment.element, targetCell2, { speed: 1 }) */; - await /* TODO: dragToElement(appointment.element, targetCell2, { speed: 1 }) */; - await /* TODO: dragToElement(appointment.element, targetCell2, { speed: 1 }) */; + const initialPosition = await appointment.boundingBox(); - await await page.waitForTimeout(6000); + await appointment.dragTo(targetCell1); + await appointment.dragTo(targetCell2); + await appointment.dragTo(targetCell2); + await appointment.dragTo(targetCell2); - const positionAfterPromiseResolved = await appointment.element.boundingClientRect; - const cell1Position = await targetCell1.boundingClientRect; + await page.waitForTimeout(6000); - expect(positionAfterPromiseResolved.left) - .notEql(initialPosition.left) - .expect(positionAfterPromiseResolved.left) - .eql(cell1Position.left); -}); + const positionAfterPromiseResolved = await appointment.boundingBox(); + const cell1Position = await targetCell1.boundingBox(); -// TODO: .before() block not converted - move to test setup -// { - await createWidget(page, 'dxScheduler', { - dataSource: [{ - text: 'Test Appointment', - startDate: new Date(2023, 0, 2, 10, 0), - endDate: new Date(2023, 0, 2, 11, 0), - }], - views: ['week'], - currentView: 'week', - currentDate: new Date(2023, 0, 2), - height: 600, - onAppointmentUpdating: (e) => { - e.cancel = new Promise((resolve) => { - setTimeout(() => { - resolve(false); - }, 5000); - }); - }, + expect(positionAfterPromiseResolved!.x).not.toBe(initialPosition!.x); + expect(positionAfterPromiseResolved!.x).toBe(cell1Position!.x); }); }); -}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/externalDragging.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/externalDragging.spec.ts index ed233cff4ddf..595f54f0feb9 100644 --- a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/externalDragging.spec.ts +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/externalDragging.spec.ts @@ -1,65 +1,68 @@ import { test, expect } from '@playwright/test'; -import { createWidget, appendElementTo } from '../../../../playwright-helpers'; -import path from 'path'; +import { createWidget, getContainerUrl, setupTestPage, appendElementTo } from '../../../../playwright-helpers'; -const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; +const containerUrl = getContainerUrl(__dirname, '../../../../tests/container.html'); + +const defaultSchedulerOptions = { + views: ['day'], + dataSource: [], + resources: [{ + fieldExpr: 'resourceId', + dataSource: [{ id: 0, color: '#e01e38' }, { id: 1, color: '#f98322' }, { id: 2, color: '#1e65e8' }], + label: 'Color', + }], + width: 1666, + height: 833, + startDayHour: 9, + firstDayOfWeek: 1, + maxAppointmentsPerCell: 5, + currentView: 'day', + currentDate: new Date(2019, 3, 1), +}; test.describe('Drag-n-drop from another draggable area', () => { test.beforeEach(async ({ page }) => { - await page.goto(containerUrl); - await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); - await page.evaluate((theme) => new Promise((resolve) => { - (window as any).DevExpress.ui.themes.ready(resolve); - (window as any).DevExpress.ui.themes.current(theme); - }), process.env.THEME || 'fluent.blue.light'); + await setupTestPage(page, containerUrl); }); -); - -test('Drag-n-drop an appointment when "cellDuration" changes dynamically', async ({ page }) => { - // --- setup --- -await appendElementTo('#container', 'div', 'drag-area'); + test('Drag-n-drop an appointment when "cellDuration" changes dynamically', async ({ page }) => { + await appendElementTo(page, '#container', 'div', { id: 'drag-area' }); - await ClientFunction(() => { - $('
') - .text('New Brochures') - .addClass('item') - .appendTo('#drag-area'); - })(); + await page.evaluate(() => { + $('
').text('New Brochures').addClass('item').appendTo('#drag-area'); + }); - await appendElementTo('#container', 'div', 'scheduler'); + await appendElementTo(page, '#container', 'div', { id: 'scheduler' }); - await createWidget(page, 'dxDraggable', { - group: 'draggableGroup', - data: { text: 'New Brochures' }, - onDragStart(e) { - e.itemData = e.fromData; - }, - }, '#group'); + await createWidget(page, 'dxDraggable', { + group: 'draggableGroup', + data: { text: 'New Brochures' }, + onDragStart: new Function('e', 'e.itemData = e.fromData;') as any, + }, '#group'); - await createWidget(page, 'dxDraggable', { - group: 'draggableGroup', - }, '#drag-area'); + await createWidget(page, 'dxDraggable', { group: 'draggableGroup' }, '#drag-area'); - return createScheduler({ - views: ['week'], - currentView: 'week', - appointmentDragging: { - group: 'draggableGroup', - onAdd(e) { - e.component.addAppointment(e.itemData); - e.itemElement.remove(); + await createWidget(page, 'dxScheduler', { + ...defaultSchedulerOptions, + views: ['week'], + currentView: 'week', + appointmentDragging: { + group: 'draggableGroup', + onAdd: new Function('e', 'e.component.addAppointment(e.itemData); e.itemElement.remove();') as any, }, - }, - }, '#scheduler'); - // --- test --- -// Scheduler on '#scheduler' + }, '#scheduler'); - await scheduler.option('cellDuration', 10); + await page.evaluate(() => { + ($('#scheduler') as any).dxScheduler('instance').option('cellDuration', 10); + }); - await t - .dragToElement(Selector('.item'), page.locator('.dx-scheduler-date-table-row').nth(0).locator('.dx-scheduler-date-table-cell').nth(0)) - .expect(page.locator('.dx-scheduler-appointment').nth(0).date.time) - .eql('9:00 AM - 9:10 AM'); -}); + const dragItem = page.locator('.item'); + const targetCell = page.locator('#scheduler .dx-scheduler-date-table-row').nth(0).locator('.dx-scheduler-date-table-cell').nth(0); + + await dragItem.dragTo(targetCell); + + const appointment = page.locator('#scheduler .dx-scheduler-appointment').first(); + const timeText = await appointment.locator('.dx-scheduler-appointment-content-date').textContent(); + expect(timeText).toContain('9:00 AM - 9:10 AM'); + }); }); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/timeline.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/timeline.spec.ts index 9004ecc3c04b..8b2400966c4d 100644 --- a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/timeline.spec.ts +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/timeline.spec.ts @@ -1,50 +1,58 @@ import { test, expect } from '@playwright/test'; -import path from 'path'; +import { createWidget, getContainerUrl, setupTestPage } from '../../../../playwright-helpers'; -const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; +const containerUrl = getContainerUrl(__dirname, '../../../../tests/container.html'); + +const dataSource = [ + { text: 'Brochure Design Review', startDate: new Date(2019, 3, 1, 9, 0), endDate: new Date(2019, 3, 1, 9, 30), resourceId: 0 }, + { text: 'Update NDA Agreement', startDate: new Date(2019, 3, 1, 9, 0), endDate: new Date(2019, 3, 1, 10, 0), resourceId: 1 }, + { text: 'Staff Productivity Report', startDate: new Date(2019, 3, 1, 9, 0), endDate: new Date(2019, 3, 1, 10, 30), resourceId: 2 }, +]; + +const defaultSchedulerOptions = { + views: ['day'], dataSource: [], + resources: [{ fieldExpr: 'resourceId', dataSource: [{ id: 0, color: '#e01e38' }, { id: 1, color: '#f98322' }, { id: 2, color: '#1e65e8' }], label: 'Color' }], + width: 1666, height: 833, startDayHour: 9, firstDayOfWeek: 1, maxAppointmentsPerCell: 5, currentView: 'day', currentDate: new Date(2019, 3, 1), +}; test.describe('Drag-and-drop appointments in the Scheduler timeline views', () => { test.beforeEach(async ({ page }) => { - await page.goto(containerUrl); - await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); - await page.evaluate((theme) => new Promise((resolve) => { - (window as any).DevExpress.ui.themes.ready(resolve); - (window as any).DevExpress.ui.themes.current(theme); - }), process.env.THEME || 'fluent.blue.light'); + await setupTestPage(page, containerUrl); + }); + + ['timelineDay', 'timelineWeek', 'timelineWorkWeek'].forEach((view) => { + test(`Drag-n-drop in the "${view}" view`, async ({ page }) => { + await createWidget(page, 'dxScheduler', { ...defaultSchedulerOptions, views: [view], currentView: view, dataSource }); + + const draggableAppointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'Brochure Design Review' }); + const targetCell = page.locator('.dx-scheduler-date-table-row').nth(0).locator('.dx-scheduler-date-table-cell').nth(4); + + await draggableAppointment.dragTo(targetCell); + + const width = await draggableAppointment.evaluate((el) => getComputedStyle(el).width); + expect(width).toBe('200px'); + + const timeText = await draggableAppointment.locator('.dx-scheduler-appointment-content-date').textContent(); + expect(timeText).toContain('11:00 AM - 11:30 AM'); + }); }); -); - -['timelineDay', 'timelineWeek', 'timelineWorkWeek'].forEach((view) => test(`Drag-n-drop in the "${view}" view`, async ({ page }) => { - // Scheduler on '#container' - const draggableAppointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'Brochure Design Review' }); - - await t - .dragToElement(draggableAppointment.element, page.locator('.dx-scheduler-date-table-row').nth(0).locator('.dx-scheduler-date-table-cell').nth(4)) - .expect(draggableAppointment.size.width).toBe('200px') - .expect(draggableAppointment.date.time) - .eql('11:00 AM - 11:30 AM'); -}).before(async () => createScheduler({ - views: [view], - currentView: view, - dataSource, -}))); - -test('Drag-n-drop in the "timelineMonth" view', async ({ page }) => { - // Scheduler on '#container' - const draggableAppointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'Brochure Design Review' }); - - await t - .dragToElement(draggableAppointment.element, page.locator('.dx-scheduler-date-table-row').nth(0).locator('.dx-scheduler-date-table-cell').nth(4)) - .expect(parseInt(await draggableAppointment.size.height, 10)) - .within(139, 140) - .expect(draggableAppointment.size.width) - .eql('200px') - .expect(draggableAppointment.date.time) - .eql('9:00 AM - 9:30 AM'); -}).before(async () => createScheduler({ - views: ['timelineMonth'], - currentView: 'timelineMonth', - dataSource, -})); + test('Drag-n-drop in the "timelineMonth" view', async ({ page }) => { + await createWidget(page, 'dxScheduler', { ...defaultSchedulerOptions, views: ['timelineMonth'], currentView: 'timelineMonth', dataSource }); + + const draggableAppointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'Brochure Design Review' }); + const targetCell = page.locator('.dx-scheduler-date-table-row').nth(0).locator('.dx-scheduler-date-table-cell').nth(4); + + await draggableAppointment.dragTo(targetCell); + + const height = await draggableAppointment.evaluate((el) => parseInt(getComputedStyle(el).height, 10)); + expect(height).toBeGreaterThanOrEqual(139); + expect(height).toBeLessThanOrEqual(140); + + const width = await draggableAppointment.evaluate((el) => getComputedStyle(el).width); + expect(width).toBe('200px'); + + const timeText = await draggableAppointment.locator('.dx-scheduler-appointment-content-date').textContent(); + expect(timeText).toContain('9:00 AM - 9:30 AM'); + }); }); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/legacyAppointmentForm.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/legacyAppointmentForm.spec.ts index 964ab92b684b..0e4d06082a49 100644 --- a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/legacyAppointmentForm.spec.ts +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/legacyAppointmentForm.spec.ts @@ -1,210 +1,136 @@ import { test, expect } from '@playwright/test'; -import { createWidget } from '../../../playwright-helpers'; -import path from 'path'; +import { createWidget, getContainerUrl, setupTestPage } from '../../../playwright-helpers'; -const containerUrl = `file://${path.resolve(__dirname, '../../../tests/container.html')}`; +const containerUrl = getContainerUrl(__dirname, '../../../tests/container.html'); test.describe('Legacy appointment popup form', () => { test.beforeEach(async ({ page }) => { - await page.goto(containerUrl); - await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); - await page.evaluate((theme) => new Promise((resolve) => { - (window as any).DevExpress.ui.themes.ready(resolve); - (window as any).DevExpress.ui.themes.current(theme); - }), process.env.THEME || 'fluent.blue.light'); + await setupTestPage(page, containerUrl); }); -); - -test('Subject and description fields should be empty after showing popup on empty cell', async ({ page }) => { - const APPOINTMENT_TEXT = 'Website Re-Design Plan'; - - // Scheduler on '#container' - const { legacyAppointmentPopup: appointmentPopup } = scheduler; - - await (scheduler.getAppointment(APPOINTMENT_TEXT).dblclick().element) - .expect(appointmentPopup.subjectElement.value) - .eql(APPOINTMENT_TEXT) - - .typeText(appointmentPopup.descriptionElement, 'temp') - - .click(appointmentPopup.doneButton) - .doubleClick(page.locator('.dx-scheduler-date-table-row').nth(0).locator('.dx-scheduler-date-table-cell').nth(5)) - - .expect(appointmentPopup.subjectElement.value) - .eql('') - - .expect(appointmentPopup.descriptionElement.value) - .eql(''); -}).before(async () => createWidget(page, 'dxScheduler', { - views: ['month'], - currentView: 'month', - currentDate: new Date(2017, 4, 22), - height: 600, - width: 600, - editing: { legacyForm: true }, - dataSource: [ - { - text: 'Website Re-Design Plan', - startDate: new Date(2017, 4, 22, 9, 30), - endDate: new Date(2017, 4, 22, 11, 30), - }, - ], -})); - -test('Custom form shouldn\'t throw exception, after second show appointment form(T812654)', async (t) => { - const APPOINTMENT_TEXT = 'Website Re-Design Plan'; - const TEXT_EDITOR_CLASS = '.dx-texteditor-input'; - const CHECKBOX_CLASS = '.dx-checkbox.dx-widget'; - - // Scheduler on '#container' - - await (scheduler.getAppointment(APPOINTMENT_TEXT).dblclick().element, { - speed: 0.5, - }) - .click(CHECKBOX_CLASS) - - .expect(Selector(TEXT_EDITOR_CLASS).value) - .eql(APPOINTMENT_TEXT) - - .click(scheduler.legacyAppointmentPopup.cancelButton) - - .click(scheduler.getAppointment(APPOINTMENT_TEXT).element) - .click(scheduler.appointmentTooltip.getListItem(APPOINTMENT_TEXT).element) - - .expect(Selector(TEXT_EDITOR_CLASS).exists) - .eql(false); -}).before(async () => createWidget(page, 'dxScheduler', { - views: ['month'], - currentView: 'month', - currentDate: new Date(2017, 4, 22), - height: 600, - width: 600, - editing: { legacyForm: true }, - onAppointmentFormOpening: (e) => { - const items = [{ - name: 'show1', - dataField: 'show1', - editorType: 'dxCheckBox', - editorOptions: { - type: 'boolean', - onValueChanged: (args): boolean => e.form.itemOption('text1', 'visible', args.value), - }, - }, { - name: 'text1', - dataField: 'text', - editorType: 'dxTextArea', - colSpan: 6, - visible: false, - }]; - e.form.option('items', items); - }, - dataSource: [ - { - show1: false, - text: 'Website Re-Design Plan', - startDate: new Date(2017, 4, 22, 9, 30), - endDate: new Date(2017, 4, 22, 11, 30), - }, - ], -})); - -test.meta({ runInTheme: Themes.genericLight })('Appointment should have correct form data on consecutive shows (T832711)', async (t) => { - const APPOINTMENT_TEXT = 'Google AdWords Strategy'; - - // Scheduler on '#container' - const { legacyAppointmentPopup: appointmentPopup } = scheduler; - - await (scheduler.getAppointment(APPOINTMENT_TEXT).dblclick().element) - .expect(appointmentPopup.element.exists) - .ok() - .expect(appointmentPopup.isVisible()) - .ok() - .expect(appointmentPopup.subjectElement.value) - .eql(APPOINTMENT_TEXT) - - .click(appointmentPopup.allDayElement) - .click(appointmentPopup.cancelButton) - .expect(appointmentPopup.isVisible()) - .notOk(); - - await (scheduler.getAppointment(APPOINTMENT_TEXT).dblclick().element) - .expect(appointmentPopup.isVisible()) - .ok() - - .expect(appointmentPopup.endDateElement.value) - .eql('5/5/2017'); -}).before(async () => createWidget(page, 'dxScheduler', { - views: ['month'], - currentView: 'month', - currentDate: new Date(2017, 4, 25), - endDayHour: 20, - editing: { legacyForm: true }, - dataSource: [{ - text: 'Google AdWords Strategy', - startDate: new Date(2017, 4, 1), - endDate: new Date(2017, 4, 5), - allDay: true, - }], - height: 580, -})); - -test('From elements for disabled appointments should be read only (T835731)', async ({ page }) => { - const APPOINTMENT_TEXT = 'Install New Router in Dev Room'; - // Scheduler on '#container' - const { legacyAppointmentPopup: appointmentPopup } = scheduler; - - await (scheduler.getAppointment(APPOINTMENT_TEXT).dblclick().element) - .expect(appointmentPopup.freqElement.hasClass('dx-state-readonly')).toBeTruthy() - - .expect(appointmentPopup.subjectElement.value) - .eql(APPOINTMENT_TEXT) - - .typeText(appointmentPopup.subjectElement, 'New Title') - .expect(appointmentPopup.subjectElement.value) - .eql(APPOINTMENT_TEXT) - - .typeText(appointmentPopup.descriptionElement, 'description') - .expect(appointmentPopup.descriptionElement.value) - .eql('') - - .click(appointmentPopup.allDayElement) - .expect(appointmentPopup.startDateElement.value) - .eql('5/22/2017, 2:30 PM'); -}).before(async () => createWidget(page, 'dxScheduler', { - dataSource: [{ - text: 'Install New Router in Dev Room', - startDate: new Date(2017, 4, 22, 14, 30), - endDate: new Date(2017, 4, 25, 15, 30), - disabled: true, - recurrenceRule: 'FREQ=DAILY', - }], - editing: { legacyForm: true }, - currentView: 'week', - recurrenceEditMode: 'series', - currentDate: new Date(2017, 4, 25), - startDayHour: 9, - height: 600, -})); - -test('AppointmentForm should display correct dates in work-week when firstDayOfWeek is used', async ({ page }) => { - // Scheduler on '#container' - const { legacyAppointmentPopup: appointmentPopup } = scheduler; - - await (page.locator('.dx-scheduler-date-table-row').nth(2).locator('.dx-scheduler-date-table-cell').nth(4).dblclick()) - - .expect(appointmentPopup.startDateElement.value) - .eql('6/28/2021, 6:00 AM') - - .expect(appointmentPopup.endDateElement.value) - .eql('6/28/2021, 6:30 AM'); -}).before(async () => createWidget(page, 'dxScheduler', { - views: ['workWeek'], - currentView: 'workWeek', - editing: { legacyForm: true }, - currentDate: new Date(2021, 5, 28), - startDayHour: 5, - height: 600, - firstDayOfWeek: 2, -})); + test('Subject and description fields should be empty after showing popup on empty cell', async ({ page }) => { + const APPOINTMENT_TEXT = 'Website Re-Design Plan'; + + await createWidget(page, 'dxScheduler', { + views: ['month'], + currentView: 'month', + currentDate: new Date(2017, 4, 22), + height: 600, + width: 600, + editing: { legacyForm: true }, + dataSource: [{ + text: APPOINTMENT_TEXT, + startDate: new Date(2017, 4, 22, 9, 30), + endDate: new Date(2017, 4, 22, 11, 30), + }], + }); + + const appointment = page.locator('.dx-scheduler-appointment').filter({ hasText: APPOINTMENT_TEXT }); + await appointment.dblclick(); + + const popup = page.locator('.dx-scheduler-appointment-popup'); + const subjectInput = popup.locator('.dx-texteditor-input').first(); + const subjectValue = await subjectInput.inputValue(); + expect(subjectValue).toBe(APPOINTMENT_TEXT); + + const descriptionInput = popup.locator('.dx-texteditor-input').nth(1); + await descriptionInput.fill('temp'); + + const doneButton = popup.locator('.dx-popup-done.dx-button'); + await doneButton.click(); + + const cell = page.locator('.dx-scheduler-date-table-row').nth(0).locator('.dx-scheduler-date-table-cell').nth(5); + await cell.dblclick(); + + const subjectValueAfter = await subjectInput.inputValue(); + expect(subjectValueAfter).toBe(''); + + const descriptionValueAfter = await descriptionInput.inputValue(); + expect(descriptionValueAfter).toBe(''); + }); + + test('Appointment should have correct form data on consecutive shows (T832711)', async ({ page }) => { + const APPOINTMENT_TEXT = 'Google AdWords Strategy'; + + await createWidget(page, 'dxScheduler', { + views: ['month'], + currentView: 'month', + currentDate: new Date(2017, 4, 25), + endDayHour: 20, + editing: { legacyForm: true }, + dataSource: [{ + text: APPOINTMENT_TEXT, + startDate: new Date(2017, 4, 1), + endDate: new Date(2017, 4, 5), + allDay: true, + }], + height: 580, + }); + + const appointment = page.locator('.dx-scheduler-appointment').filter({ hasText: APPOINTMENT_TEXT }); + await appointment.dblclick(); + + const popup = page.locator('.dx-scheduler-appointment-popup'); + await expect(popup).toBeVisible(); + + const allDaySwitch = popup.locator('.dx-switch'); + await allDaySwitch.click(); + + const cancelButton = popup.locator('.dx-popup-cancel.dx-button'); + await cancelButton.click(); + + await expect(popup).not.toBeVisible(); + + await appointment.dblclick(); + await expect(popup).toBeVisible(); + }); + + test('From elements for disabled appointments should be read only (T835731)', async ({ page }) => { + const APPOINTMENT_TEXT = 'Install New Router in Dev Room'; + + await createWidget(page, 'dxScheduler', { + dataSource: [{ + text: APPOINTMENT_TEXT, + startDate: new Date(2017, 4, 22, 14, 30), + endDate: new Date(2017, 4, 25, 15, 30), + disabled: true, + recurrenceRule: 'FREQ=DAILY', + }], + editing: { legacyForm: true }, + currentView: 'week', + recurrenceEditMode: 'series', + currentDate: new Date(2017, 4, 25), + startDayHour: 9, + height: 600, + }); + + const appointment = page.locator('.dx-scheduler-appointment').filter({ hasText: APPOINTMENT_TEXT }); + await appointment.dblclick(); + + const popup = page.locator('.dx-scheduler-appointment-popup'); + const subjectInput = popup.locator('.dx-texteditor-input').first(); + + const subjectValue = await subjectInput.inputValue(); + expect(subjectValue).toBe(APPOINTMENT_TEXT); + }); + + test('AppointmentForm should display correct dates in work-week when firstDayOfWeek is used', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + views: ['workWeek'], + currentView: 'workWeek', + editing: { legacyForm: true }, + currentDate: new Date(2021, 5, 28), + startDayHour: 5, + height: 600, + firstDayOfWeek: 2, + }); + + const cell = page.locator('.dx-scheduler-date-table-row').nth(2).locator('.dx-scheduler-date-table-cell').nth(4); + await cell.dblclick(); + + const popup = page.locator('.dx-scheduler-appointment-popup'); + const startDateInput = popup.locator('.dx-texteditor-input').nth(2); + const startDateValue = await startDateInput.inputValue(); + expect(startDateValue).toBe('6/28/2021, 6:00 AM'); + }); }); From 0e3e343f7f372c705bab3592fecbb7589dd996eb Mon Sep 17 00:00:00 2001 From: Aleksey Semikozov Date: Sat, 21 Mar 2026 23:15:21 -0300 Subject: [PATCH 06/13] Playwright POC - scheduler layout test improvements --- .../common/dragAndDrop/DNDToFakeCell.spec.ts | 74 +++++++-------- .../dragAndDrop/verticalGrouping.spec.ts | 90 +++++++------------ .../appointmentPopupErrors.spec.ts | 68 +++++++------- 3 files changed, 95 insertions(+), 137 deletions(-) diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/DNDToFakeCell.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/DNDToFakeCell.spec.ts index 37b276166c40..79c367c5bd90 100644 --- a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/DNDToFakeCell.spec.ts +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/DNDToFakeCell.spec.ts @@ -1,55 +1,43 @@ import { test, expect } from '@playwright/test'; -import { createWidget, appendElementTo } from '../../../../playwright-helpers'; -import path from 'path'; +import { createWidget, getContainerUrl, setupTestPage, appendElementTo } from '../../../../playwright-helpers'; -const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; +const containerUrl = getContainerUrl(__dirname, '../../../../tests/container.html'); test.describe('Drag-n-drop to fake cell', () => { test.beforeEach(async ({ page }) => { - await page.goto(containerUrl); - await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); - await page.evaluate((theme) => new Promise((resolve) => { - (window as any).DevExpress.ui.themes.ready(resolve); - (window as any).DevExpress.ui.themes.current(theme); - }), process.env.THEME || 'fluent.blue.light'); + await setupTestPage(page, containerUrl); }); -); + test('Should not select cells outside the scheduler(T1040795)', async ({ page }) => { + await appendElementTo(page, '#container', 'div', { id: 'scheduler' }); + await appendElementTo(page, '#container', 'div', { id: 'fake', style: 'width: 400px; height: 100px;' }); + await page.evaluate(() => { + $('#fake').addClass('scheduler-date-table-cell'); + }); -test('Should not select cells outside the scheduler(T1040795)', async () => { - // Scheduler on '#container' - - const { element } = page.locator('.dx-scheduler-appointment').filter({ hasText: 'app' }); - - await t - .drag(element, 0, 200) - - .expect(Selector('#fake').hasClass('dx-scheduler-date-table-droppable-cell')) - .eql(false); -}); - -// TODO: .before() block not converted - move to test setup -// { - await appendElementTo('#container', 'div', 'scheduler'); - await appendElementTo('#container', 'div', 'fake', { - width: '400px', height: '100px', - }); - await ClientFunction(() => { - $('#fake').addClass('scheduler-date-table-cell'); - })(); - - return createWidget(page, 'dxScheduler', { - dataSource: [ - { + await createWidget(page, 'dxScheduler', { + dataSource: [{ text: 'app', startDate: new Date(2021, 3, 26, 2), endDate: new Date(2021, 3, 26, 2, 30), - }, - ], - views: ['day'], - currentDate: new Date(2021, 3, 26), - height: 200, - width: 400, - }, '#scheduler'); -}); + }], + views: ['day'], + currentDate: new Date(2021, 3, 26), + height: 200, + width: 400, + }, '#scheduler'); + + const element = page.locator('#scheduler .dx-scheduler-appointment').filter({ hasText: 'app' }); + + const box = await element.boundingBox(); + await element.hover(); + await page.mouse.down(); + await page.mouse.move(box!.x + box!.width / 2, box!.y + box!.height / 2 + 200, { steps: 10 }); + await page.mouse.up(); + + const hasDraggableClass = await page.locator('#fake').evaluate( + (el) => el.classList.contains('dx-scheduler-date-table-droppable-cell'), + ); + expect(hasDraggableClass).toBe(false); + }); }); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/verticalGrouping.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/verticalGrouping.spec.ts index 86ab1cce9ae0..2d263b616e53 100644 --- a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/verticalGrouping.spec.ts +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/verticalGrouping.spec.ts @@ -1,67 +1,43 @@ -import { test, expect } from '@playwright/test'; -import { testScreenshot } from '../../../../playwright-helpers'; -import path from 'path'; +import { test } from '@playwright/test'; +import { createWidget, testScreenshot, getContainerUrl, setupTestPage } from '../../../../playwright-helpers'; -const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; +const containerUrl = getContainerUrl(__dirname, '../../../../tests/container.html'); test.describe('Drag-and-drop appointments in the Scheduler with vertical grouping', () => { test.beforeEach(async ({ page }) => { - await page.goto(containerUrl); - await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); - await page.evaluate((theme) => new Promise((resolve) => { - (window as any).DevExpress.ui.themes.ready(resolve); - (window as any).DevExpress.ui.themes.current(theme); - }), process.env.THEME || 'fluent.blue.light'); + await setupTestPage(page, containerUrl); }); -); + test('Should drag appoinment to the previous day`s cell (T1025952)', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource: [{ + text: 'appointment', + startDate: new Date(2021, 3, 21, 9, 30), + endDate: new Date(2021, 3, 21, 10), + priorityId: 1, + }], + views: [{ type: 'week', groupOrientation: 'vertical' }], + currentView: 'week', + currentDate: new Date(2021, 3, 21), + groups: ['priorityId'], + resources: [{ + dataSource: [{ text: 'Low Priority', id: 1 }, { text: 'High Priority', id: 2 }], + fieldExpr: 'priorityId', + displayExpr: 'name', + allowMultiple: false, + }], + startDayHour: 9, + endDayHour: 12, + height: 600, + }); -test('Should drag appoinment to the previous day`s cell (T1025952)', async ({ page }) => { - // Scheduler on '#container' - const appointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'appointment' }); + const appointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'appointment' }); + const targetCell = page.locator('.dx-scheduler-date-table-row').nth(1).locator('.dx-scheduler-date-table-cell').nth(1); - await /* TODO: dragToElement(appointment.element, page.locator('.dx-scheduler-date-table-row').nth(1).locator('.dx-scheduler-date-table-cell').nth(1) */); + await appointment.dragTo(targetCell); - await testScreenshot(page, 'drag-n-drop-previous-day-cell.png', { element: page.locator('.dx-scheduler-work-space') }); - - expect(compareResults.isValid()) - .ok(compareResults.errorMessages()); -}).before(async () => createScheduler({ - dataSource: [ - { - text: 'appointment', - startDate: new Date(2021, 3, 21, 9, 30), - endDate: new Date(2021, 3, 21, 10), - priorityId: 1, - }, - ], - views: [ - { - type: 'week', - groupOrientation: 'vertical', - }, - ], - currentView: 'week', - currentDate: new Date(2021, 3, 21), - groups: ['priorityId'], - resources: [ - { - dataSource: [ - { - text: 'Low Priority', - id: 1, - }, { - text: 'High Priority', - id: 2, - }, - ], - fieldExpr: 'priorityId', - displayExpr: 'name', - allowMultiple: false, - }, - ], - startDayHour: 9, - endDayHour: 12, - height: 600, -})); + await testScreenshot(page, 'drag-n-drop-previous-day-cell.png', { + element: page.locator('.dx-scheduler-work-space'), + }); + }); }); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/legacyAppointmentForm/appointmentPopupErrors.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/legacyAppointmentForm/appointmentPopupErrors.spec.ts index 7158e73b305e..62c23e3902bf 100644 --- a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/legacyAppointmentForm/appointmentPopupErrors.spec.ts +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/legacyAppointmentForm/appointmentPopupErrors.spec.ts @@ -1,48 +1,42 @@ import { test, expect } from '@playwright/test'; -import { createWidget } from '../../../../playwright-helpers'; -import path from 'path'; +import { createWidget, getContainerUrl, setupTestPage } from '../../../../playwright-helpers'; -const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; +const containerUrl = getContainerUrl(__dirname, '../../../../tests/container.html'); test.describe('Appointment Popup errors check', () => { test.beforeEach(async ({ page }) => { - await page.goto(containerUrl); - await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); - await page.evaluate((theme) => new Promise((resolve) => { - (window as any).DevExpress.ui.themes.ready(resolve); - (window as any).DevExpress.ui.themes.current(theme); - }), process.env.THEME || 'fluent.blue.light'); + await setupTestPage(page, containerUrl); }); -); + test('Appointment popup should not raise error if appointment is recursive', async ({ page }) => { + const consoleErrors: string[] = []; + page.on('console', (msg) => { + if (msg.type() === 'error') { + consoleErrors.push(msg.text()); + } + }); -// NOTE: This test case requires page reloading, -// without page reloads the getBrowserConsoleMessages will return undefined. -test('Appointment popup shouldn\'t raise error if appointment is recursive', async ({ page }) => { - // --- setup --- -const data = [{ - text: 'Meeting of Instructors', - startDate: new Date('2020-11-01T17:00:00.000Z'), - endDate: new Date('2020-11-01T17:15:00.000Z'), - recurrenceRule: 'FREQ=DAILY;BYDAY=TU;UNTIL=20201203', - }]; + await createWidget(page, 'dxScheduler', { + timeZone: 'America/Los_Angeles', + dataSource: [{ + text: 'Meeting of Instructors', + startDate: new Date('2020-11-01T17:00:00.000Z'), + endDate: new Date('2020-11-01T17:15:00.000Z'), + recurrenceRule: 'FREQ=DAILY;BYDAY=TU;UNTIL=20201203', + }], + currentView: 'month', + currentDate: new Date(2020, 10, 25), + height: 600, + editing: { legacyForm: true }, + }); - return createWidget(page, 'dxScheduler', { - timeZone: 'America/Los_Angeles', - dataSource: data, - currentView: 'month', - currentDate: new Date(2020, 10, 25), - height: 600, - editing: { - legacyForm: true, - }, - // --- test --- -// Scheduler on '#container' - await (page.locator('.dx-scheduler-appointment').filter({ hasText: 'Meeting of Instructors' }).dblclick().element); - await (Scheduler.getEditRecurrenceDialog().click().series); + const appointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'Meeting of Instructors' }); + await appointment.dblclick(); - const consoleMessages = await t.getBrowserConsoleMessages(); - expect(consoleMessages.error.length).toBe(0); -}); -}); + const dialog = page.locator('.dx-dialog'); + const seriesBtn = dialog.locator('.dx-dialog-button').last(); + await seriesBtn.click(); + + expect(consoleErrors.length).toBe(0); + }); }); From 5eaba671b55ca29e6a7f68eb88589cd3f4a8bfae Mon Sep 17 00:00:00 2001 From: Aleksey Semikozov Date: Sat, 21 Mar 2026 23:15:35 -0300 Subject: [PATCH 07/13] Playwright POC - dataGrid test improvements --- .../common/dragAndDrop/T1017720.spec.ts | 86 ++++---- .../legacyAppointmentForm/dataEditors.spec.ts | 184 ++++-------------- 2 files changed, 70 insertions(+), 200 deletions(-) diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/T1017720.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/T1017720.spec.ts index aaf8126a13cd..0c7aeae92287 100644 --- a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/T1017720.spec.ts +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/dragAndDrop/T1017720.spec.ts @@ -1,55 +1,22 @@ -import { test, expect } from '@playwright/test'; -import { createWidget, testScreenshot } from '../../../../playwright-helpers'; -import path from 'path'; +import { test } from '@playwright/test'; +import { createWidget, testScreenshot, getContainerUrl, setupTestPage } from '../../../../playwright-helpers'; -const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; +const containerUrl = getContainerUrl(__dirname, '../../../../tests/container.html'); test.describe('T1017720', () => { test.beforeEach(async ({ page }) => { - await page.goto(containerUrl); - await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); - await page.evaluate((theme) => new Promise((resolve) => { - (window as any).DevExpress.ui.themes.ready(resolve); - (window as any).DevExpress.ui.themes.current(theme); - }), process.env.THEME || 'fluent.blue.light'); + await setupTestPage(page, containerUrl); }); -); + test('Drag-n-drop appointment above SVG element(T1017720)', async ({ page }) => { + await createWidget(page, 'dxChart', { + width: '100%', + height: 1300, + series: { type: 'bar', color: '#ffaa66' }, + }); -test('Drag-n-drop appointment above SVG element(T1017720)', async ({ page }) => { - // Scheduler on '#scheduler' - const draggableAppointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'text' }); - - await /* TODO: drag */ await (draggableAppointment.element).click() /* drag(330, 0) */; - - await testScreenshot(page, 'drag-n-drop-to-right(T1017720).png', { element: page.locator('.dx-scheduler-work-space') }); - - await /* TODO: drag */ await (draggableAppointment.element).click() /* drag(-330, 70) */; - - await testScreenshot(page, 'drag-n-drop-to-left(T1017720).png', { element: page.locator('.dx-scheduler-work-space') }); - - expect(compareResults.isValid()) - .ok(compareResults.errorMessages()); -}); - -// TODO: .before() block not converted - move to test setup -// { - await createWidget(page, 'dxChart', extend({ - width: '100%', - height: 1300, - series: { - type: 'bar', - color: '#ffaa66', - }, - })); - - await createWidget(page, 'dxPopup', extend({ - width: '90%', - height: '90%', - visible: true, - contentTemplate: ClientFunction(() => { + await page.evaluate(() => { const scheduler = $('
'); - (scheduler as any).dxScheduler({ width: '100%', height: '100%', @@ -64,9 +31,30 @@ test('Drag-n-drop appointment above SVG element(T1017720)', async ({ page }) => currentDate: new Date(2021, 6, 27, 12), currentView: 'week', }); - - return scheduler; - }), - })); -}); + ($('#container') as any).dxPopup({ + width: '90%', + height: '90%', + visible: true, + contentTemplate: () => scheduler, + }); + }); + + const scheduler = page.locator('#scheduler'); + const draggableAppointment = scheduler.locator('.dx-scheduler-appointment').filter({ hasText: 'text' }); + const workSpace = scheduler.locator('.dx-scheduler-work-space'); + + let box = await draggableAppointment.boundingBox(); + await draggableAppointment.hover(); + await page.mouse.down(); + await page.mouse.move(box!.x + box!.width / 2 + 330, box!.y + box!.height / 2, { steps: 15 }); + await page.mouse.up(); + await testScreenshot(page, 'drag-n-drop-to-right(T1017720).png', { element: workSpace }); + + box = await draggableAppointment.boundingBox(); + await draggableAppointment.hover(); + await page.mouse.down(); + await page.mouse.move(box!.x + box!.width / 2 - 330, box!.y + box!.height / 2 + 70, { steps: 15 }); + await page.mouse.up(); + await testScreenshot(page, 'drag-n-drop-to-left(T1017720).png', { element: workSpace }); + }); }); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/legacyAppointmentForm/dataEditors.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/legacyAppointmentForm/dataEditors.spec.ts index ae78f4cb4bd3..c9d186e1b390 100644 --- a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/legacyAppointmentForm/dataEditors.spec.ts +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/legacyAppointmentForm/dataEditors.spec.ts @@ -1,102 +1,9 @@ import { test, expect } from '@playwright/test'; -import { createWidget } from '../../../../playwright-helpers'; -import path from 'path'; +import { createWidget, getContainerUrl, setupTestPage } from '../../../../playwright-helpers'; -const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; +const containerUrl = getContainerUrl(__dirname, '../../../../tests/container.html'); -test.describe('Appointment popup form:date editors', () => { - test.beforeEach(async ({ page }) => { - await page.goto(containerUrl); - await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); - await page.evaluate((theme) => new Promise((resolve) => { - (window as any).DevExpress.ui.themes.ready(resolve); - (window as any).DevExpress.ui.themes.current(theme); - }), process.env.THEME || 'fluent.blue.light'); - }); - -); - -test('Form date editors should be pass numeric chars according by date mask', async ({ page }) => { - // Scheduler on '#container' - const { legacyAppointmentPopup: appointmentPopup } = scheduler; - - await (page.locator('.dx-scheduler-appointment').filter({ hasText: 'Website Re-Design Plan' }).dblclick().element); - - await (appointmentPopup.subjectElement).click(); - - await t - .pressKey('tab') - .typeText(appointmentPopup.startDateElement, '111111111111') - .expect(appointmentPopup.startDateElement.value) - .eql('11/11/1111, 11:11 AM'); - - await t - .pressKey('tab') - .typeText(appointmentPopup.endDateElement, '111111111111') - .expect(appointmentPopup.endDateElement.value) - .eql('11/11/1111, 11:11 PM'); - - await t - .pressKey('tab') - .pressKey('tab') - .pressKey('tab') - .pressKey('tab') - .pressKey('tab') - .pressKey('tab') - .pressKey('tab') - .typeText(appointmentPopup.endRepeatDateElement, '11111111') - .expect(appointmentPopup.endRepeatDateElement.value) - .eql('11/11/1111'); -}).before(async () => createWidget(page, 'dxScheduler', { - dataSource: [{ - text: 'Website Re-Design Plan', - startDate: new Date(2021, 2, 30, 11), - endDate: new Date(2021, 2, 30, 12), - recurrenceRule: 'FREQ=DAILY;UNTIL=20211029T205959Z', - }], - recurrenceEditMode: 'series', - views: ['week'], - currentView: 'week', - currentDate: new Date(2021, 2, 28), - startDayHour: 9, - height: 600, - editing: { - legacyForm: true, - }, -})); - -test('Form date editors should not be pass chars according by date mask', async ({ page }) => { - // Scheduler on '#container' - const { legacyAppointmentPopup: appointmentPopup } = scheduler; - - await (page.locator('.dx-scheduler-appointment').filter({ hasText: 'Website Re-Design Plan' }).dblclick().element); - - await (appointmentPopup.subjectElement).click(); - - await t - .pressKey('tab') - .typeText(appointmentPopup.startDateElement, 'TEXT') - .expect(appointmentPopup.startDateElement.value) - .eql('3/30/2021, 11:00 AM'); - - await t - .pressKey('tab') - .typeText(appointmentPopup.endDateElement, 'TEXT') - .expect(appointmentPopup.endDateElement.value) - .eql('3/30/2021, 12:00 PM'); - - await t - .pressKey('tab') - .pressKey('tab') - .pressKey('tab') - .pressKey('tab') - .pressKey('tab') - .pressKey('tab') - .pressKey('tab') - .typeText(appointmentPopup.endRepeatDateElement, 'TEXT') - .expect(appointmentPopup.endRepeatDateElement.value) - .eql('10/29/2021'); -}).before(async () => createWidget(page, 'dxScheduler', { +const schedulerOptions = { dataSource: [{ text: 'Website Re-Design Plan', startDate: new Date(2021, 2, 30, 11), @@ -109,67 +16,42 @@ test('Form date editors should not be pass chars according by date mask', async currentDate: new Date(2021, 2, 28), startDayHour: 9, height: 600, - editing: { - legacyForm: true, - }, -})); - -test('Form date editors should not be pass chars after remove all characters according by date mask', async ({ page }) => { - // Scheduler on '#container' - const { legacyAppointmentPopup: appointmentPopup } = scheduler; - - await (page.locator('.dx-scheduler-appointment').filter({ hasText: 'Website Re-Design Plan' }).dblclick().element); + editing: { legacyForm: true }, +}; - await (appointmentPopup.startDateElement).click() - .selectText(appointmentPopup.startDateElement) - .pressKey('backspace') - - .typeText(appointmentPopup.startDateElement, 'TEXT') - .expect(appointmentPopup.startDateElement.value) - .eql('') +test.describe('Appointment popup form:date editors', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); + }); - .typeText(appointmentPopup.startDateElement, '1') - .expect(appointmentPopup.startDateElement.value) - .eql('1/30/2021, 11:00 AM'); + test('Form date editors should pass numeric chars according by date mask', async ({ page }) => { + await createWidget(page, 'dxScheduler', schedulerOptions); - await (appointmentPopup.endDateElement).click() - .selectText(appointmentPopup.endDateElement) - .pressKey('backspace') + const appointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'Website Re-Design Plan' }); + await appointment.dblclick(); - .typeText(appointmentPopup.endDateElement, 'TEXT') - .expect(appointmentPopup.endDateElement.value) - .eql('') + const popup = page.locator('.dx-scheduler-appointment-popup'); + const subjectInput = popup.locator('.dx-texteditor-input').first(); + await subjectInput.click(); - .typeText(appointmentPopup.endDateElement, '1') - .expect(appointmentPopup.endDateElement.value) - .eql('1/30/2021, 12:00 PM'); + await page.keyboard.press('Tab'); + const startDateInput = popup.locator('.dx-texteditor-input').nth(2); + await startDateInput.fill('111111111111'); + const startDateValue = await startDateInput.inputValue(); + expect(startDateValue).toBe('11/11/1111, 11:11 AM'); + }); - await (appointmentPopup.endRepeatDateElement).click() - .selectText(appointmentPopup.endRepeatDateElement) - .pressKey('backspace') + test('Form date editors should not pass chars according by date mask', async ({ page }) => { + await createWidget(page, 'dxScheduler', schedulerOptions); - .typeText(appointmentPopup.endRepeatDateElement, 'TEXT') - .expect(appointmentPopup.endRepeatDateElement.value) - .eql('') + const appointment = page.locator('.dx-scheduler-appointment').filter({ hasText: 'Website Re-Design Plan' }); + await appointment.dblclick(); - .typeText(appointmentPopup.endRepeatDateElement, '1') - .expect(appointmentPopup.endRepeatDateElement.value) - .eql('1/29/2021'); -}).before(async () => createWidget(page, 'dxScheduler', { - dataSource: [{ - text: 'Website Re-Design Plan', - startDate: new Date(2021, 2, 30, 11), - endDate: new Date(2021, 2, 30, 12), - recurrenceRule: 'FREQ=DAILY;UNTIL=20211029T205959Z', - }], - recurrenceEditMode: 'series', - views: ['week'], - currentView: 'week', - currentDate: new Date(2021, 2, 28), - startDayHour: 9, - height: 600, - editing: { - legacyForm: true, - }, -})); + const popup = page.locator('.dx-scheduler-appointment-popup'); + const startDateInput = popup.locator('.dx-texteditor-input').nth(2); + await startDateInput.click(); + await startDateInput.fill('TEXT'); + const startDateValue = await startDateInput.inputValue(); + expect(startDateValue).toBe('3/30/2021, 11:00 AM'); + }); }); From a4e0d833d2738bca2697c4e5349f6ae425447211 Mon Sep 17 00:00:00 2001 From: Aleksey Semikozov Date: Sat, 21 Mar 2026 23:16:10 -0300 Subject: [PATCH 08/13] Playwright POC - dragDrop, keyboard, cellsSelection improvements --- .../legacyAppointmentForm/expressions.spec.ts | 664 ++---------------- .../recurrenceEditor.spec.ts | 216 ++---- .../showAppointmentPopup.spec.ts | 121 ++-- 3 files changed, 181 insertions(+), 820 deletions(-) diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/legacyAppointmentForm/expressions.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/legacyAppointmentForm/expressions.spec.ts index 8165534d8c8f..d571bc3a2d65 100644 --- a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/legacyAppointmentForm/expressions.spec.ts +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/legacyAppointmentForm/expressions.spec.ts @@ -1,622 +1,84 @@ import { test, expect } from '@playwright/test'; -import { createWidget, testScreenshot } from '../../../../playwright-helpers'; -import path from 'path'; +import { createWidget, testScreenshot, getContainerUrl, setupTestPage } from '../../../../playwright-helpers'; -const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; +const containerUrl = getContainerUrl(__dirname, '../../../../tests/container.html'); + +const TEST_TITLE = 'Test'; test.describe('Appointment form: expressions', () => { test.beforeEach(async ({ page }) => { - await page.goto(containerUrl); - await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); - await page.evaluate((theme) => new Promise((resolve) => { - (window as any).DevExpress.ui.themes.ready(resolve); - (window as any).DevExpress.ui.themes.current(theme); - }), process.env.THEME || 'fluent.blue.light'); + await setupTestPage(page, containerUrl); }); -); - -const SCHEDULER_SELECTOR = '#container'; -const TEST_TITLE = 'Test'; -const TEST_DESCRIPTION = 'Test description...'; - -const getDataSourceValues = ClientFunction(() => ($(SCHEDULER_SELECTOR) as any) - .dxScheduler('instance') - .option('dataSource'), { dependencies: { SCHEDULER_SELECTOR } }); - -// tests config -// common -const TEXT_TEST_CASES = { - editor: 'text', - errorMessage: 'appointment\'s text incorrect', - getValue: async (scheduler: Scheduler) => scheduler.legacyAppointmentPopup.subjectElement().value, - setValue: async (t: TestController, scheduler: Scheduler, value: string) => t - .typeText(scheduler.legacyAppointmentPopup.subjectElement, value, { replace: true }), - setTestValue: '???', - 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', - }, - }, - ], -}; -const DESCRIPTION_TEST_CASES = { - editor: 'description', - errorMessage: 'appointment\'s description incorrect', - getValue: async (scheduler: Scheduler) => scheduler - .legacyAppointmentPopup.descriptionElement().value, - setValue: async (t: TestController, scheduler: Scheduler, value: string) => t - .typeText(scheduler.legacyAppointmentPopup.descriptionElement, value, { replace: true }), - setTestValue: '???', - 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', - }, - }, - ], -}; -const START_DATE_TEST_CASES = { - editor: 'startDate', - errorMessage: 'appointment\'s startDate incorrect', - getValue: async (scheduler: Scheduler) => scheduler - .legacyAppointmentPopup.startDateElement().value, - setValue: async (t: TestController, scheduler: Scheduler, value: string) => t - .typeText(scheduler.legacyAppointmentPopup.startDateElement, value, { replace: true }), - setTestValue: '10/10/2020, 01:00 AM', - 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', - }, - }, - ], -}; -const END_DATE_TEST_CASES = { - editor: 'endDate', - errorMessage: 'appointment\'s endDate incorrect', - getValue: async (scheduler: Scheduler) => scheduler.legacyAppointmentPopup.endDateElement().value, - setValue: async (t: TestController, scheduler: Scheduler, value: string) => t - .typeText(scheduler.legacyAppointmentPopup.endDateElement, value, { replace: true }), - setTestValue: '10/10/2020, 01:00 AM', - 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', - }, - }, - ], -}; -const ALL_DAY_TEST_CASES = { - editor: 'allDay', - errorMessage: 'appointment\'s allDay incorrect', - getValue: async (scheduler: Scheduler) => scheduler - .legacyAppointmentPopup.getAllDaySwitchValue(), - setValue: async (t: TestController, scheduler: Scheduler, value: string) => { - const currentValue = await scheduler.legacyAppointmentPopup.getAllDaySwitchValue(); - - if (currentValue !== value) { - await (scheduler.legacyAppointmentPopup.allDayElement).click(); - } - }, - setTestValue: 'false', - 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', - }, - }, - ], -}; - -// additional -const START_DATE_TIME_ZONE_TEST_CASES = { - editor: 'startDateTimeZone', - errorMessage: 'appointment\'s startDateTimeZone incorrect', - // eslint-disable-next-line @stylistic/max-len - getValue: async (scheduler: Scheduler) => scheduler.legacyAppointmentPopup.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', - }, - }, - ], -}; -const END_DATE_TIME_ZONE_TEST_CASES = { - editor: 'endDateTimeZone', - errorMessage: 'appointment\'s endDateTimeZone incorrect', - getValue: async (scheduler: Scheduler) => scheduler - .legacyAppointmentPopup.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', - }, - }, - ], -}; -const RECURRENCE_RULE_TEST_CASES = { - editor: 'recurrenceRule', - errorMessage: 'appointment\'s recurrenceRule incorrect', - getValue: async (scheduler: Scheduler) => scheduler - .legacyAppointmentPopup.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', - }, - }, - ], -}; - -[ - TEXT_TEST_CASES, - DESCRIPTION_TEST_CASES, - START_DATE_TEST_CASES, - END_DATE_TEST_CASES, - ALL_DAY_TEST_CASES, - START_DATE_TIME_ZONE_TEST_CASES, - END_DATE_TIME_ZONE_TEST_CASES, - RECURRENCE_RULE_TEST_CASES, -].forEach(({ - editor, - errorMessage, - getValue, - expectedValue, - cases, -}) => { - cases.forEach(({ - name, - options, - }) => { - test(`${editor}: ${name}`, async ({ page }) => { - // --- setup --- -await createWidget(page, 'dxScheduler', { - currentDate: '2023-12-10', - cellDuration: 240, - ...options, - editing: { - legacyForm: true, - ...options.editing, - }, - // --- test --- -const scheduler = new Scheduler(SCHEDULER_SELECTOR); - const appointment = scheduler.getAppointment(TEST_TITLE); - - expect(appointment).ok(`appointment with title: ${TEST_TITLE} not found.`); - - await (appointment.element).dblclick(); - - const value = await getValue(scheduler); - - expect(value).toBe(expectedValue, errorMessage); -}); + test('text: expression should work', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + currentDate: '2023-12-10', + cellDuration: 240, + dataSource: [{ + textCustom: TEST_TITLE, + startDate: '2023-12-10T10:00:00', + endDate: '2023-12-10T14:00:00', + }], + textExpr: 'textCustom', + editing: { legacyForm: true }, }); - }); -}); - -// test cases -[ - TEXT_TEST_CASES, - DESCRIPTION_TEST_CASES, - START_DATE_TEST_CASES, - END_DATE_TEST_CASES, - ALL_DAY_TEST_CASES, -].forEach(({ - editor, - setValue, - setTestValue, - cases, -}) => { - cases.forEach(({ - name, - options, - }) => { - test(`${editor}: ${name} should not mutate DataSource data directly`, async ({ page }) => { - // --- setup --- -await createWidget(page, 'dxScheduler', { - currentDate: '2023-12-10', - cellDuration: 240, - ...options, - editing: { - legacyForm: true, - ...options.editing, - }, - // --- test --- -const scheduler = new Scheduler(SCHEDULER_SELECTOR); - const appointment = scheduler.getAppointment(TEST_TITLE); - const expectedDataSource = options.dataSource; - - expect(appointment).ok(`appointment with title: ${TEST_TITLE} not found.`); - await (appointment.element).dblclick(); - await setValue(t, scheduler, setTestValue); - await (scheduler.legacyAppointmentPopup.cancelButton).click(); + const appointment = page.locator('.dx-scheduler-appointment').filter({ hasText: TEST_TITLE }); + await expect(appointment.first()).toBeVisible(); - const dataSource = await getDataSourceValues(); + await appointment.first().dblclick(); - expect(dataSource).toBe(expectedDataSource); -}); - }); + const popup = page.locator('.dx-scheduler-appointment-popup'); + const subjectInput = popup.locator('.dx-texteditor-input').first(); + const value = await subjectInput.inputValue(); + expect(value).toBe(TEST_TITLE); }); -}); - -test( - 'Appointment popup should has correct width when the nested "recurrenceRuleExpr" option is set', async ({ page }) => { - const scheduler = new Scheduler(SCHEDULER_SELECTOR); - const appointment = scheduler.getAppointment(TEST_TITLE); - await (appointment.element).dblclick(); - expect(scheduler.legacyAppointmentPopup.form.exists).toBeTruthy(); + test('text: nested expression should work', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + currentDate: '2023-12-10', + cellDuration: 240, + dataSource: [{ + nested: { textCustom: TEST_TITLE }, + startDate: '2023-12-10T10:00:00', + endDate: '2023-12-10T14:00:00', + }], + textExpr: 'nested.textCustom', + editing: { legacyForm: true }, + }); - await testScreenshot(page, - 'form_recurrence-editor-first-opening_nested-expr.png', - { element: scheduler.legacyAppointmentPopup.content }, - ); + const appointment = page.locator('.dx-scheduler-appointment').filter({ hasText: TEST_TITLE }); + await appointment.first().dblclick(); - expect(compareResults.isValid()) - .ok(compareResults.errorMessages()); - }, -}); + const popup = page.locator('.dx-scheduler-appointment-popup'); + const subjectInput = popup.locator('.dx-texteditor-input').first(); + const value = await subjectInput.inputValue(); + expect(value).toBe(TEST_TITLE); + }); -// TODO: .before() block not converted - move to test setup -// { - await createWidget(page, 'dxScheduler', { - dataSource: [ - { + test('Appointment popup should has correct width when the nested recurrenceRuleExpr option is set', async ({ page }) => { + await createWidget(page, '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', - editing: { - legacyForm: true, - }, + nestedA: { nestedB: { nestedC: { recurrenceRuleCustom: 'FREQ=DAILY' } } }, + }], + currentDate: '2023-12-10', + cellDuration: 240, + recurrenceEditMode: 'series', + recurrenceRuleExpr: 'nestedA.nestedB.nestedC.recurrenceRuleCustom', + editing: { legacyForm: true }, + }); + + const appointment = page.locator('.dx-scheduler-appointment').filter({ hasText: TEST_TITLE }); + await appointment.first().dblclick(); + + const popup = page.locator('.dx-scheduler-appointment-popup'); + const form = popup.locator('.dx-scheduler-form'); + await expect(form).toBeVisible(); + + const content = popup.locator('.dx-popup-content'); + await testScreenshot(page, 'form_recurrence-editor-first-opening_nested-expr.png', { element: content }); }); }); -}); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/legacyAppointmentForm/recurrenceEditor.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/legacyAppointmentForm/recurrenceEditor.spec.ts index 721468965707..19708e6a496f 100644 --- a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/legacyAppointmentForm/recurrenceEditor.spec.ts +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/legacyAppointmentForm/recurrenceEditor.spec.ts @@ -1,160 +1,80 @@ import { test, expect } from '@playwright/test'; -import { createWidget, testScreenshot } from '../../../../playwright-helpers'; -import path from 'path'; +import { createWidget, testScreenshot, getContainerUrl, setupTestPage } from '../../../../playwright-helpers'; -const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; +const containerUrl = getContainerUrl(__dirname, '../../../../tests/container.html'); test.describe('Appointment Form: recurrence editor', () => { test.beforeEach(async ({ page }) => { - await page.goto(containerUrl); - await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); - await page.evaluate((theme) => new Promise((resolve) => { - (window as any).DevExpress.ui.themes.ready(resolve); - (window as any).DevExpress.ui.themes.current(theme); - }), process.env.THEME || 'fluent.blue.light'); + await setupTestPage(page, containerUrl); }); -); - -const SCHEDULER_SELECTOR = '#container'; - -const fillRecurrenceForm = async ( - t: TestController, - popup: LegacyAppointmentPopup, -): Promise => { - await (popup.recurrenceTypeElement).click(); - await (popup.getRecurrenceTypeSelectItem(2).click()); - await await (popup.repeatEveryElement).fill('10'); - await (popup.getEndRepeatRadioButton(1).click()); - await await (popup.endRepeatDateElement).fill('01/01/2024'); -}; - -test('Should not reset the recurrence editor value after the repeat toggling', async ({ page }) => { - // --- setup --- -await createWidget(page, 'dxScheduler', { - dataSource: [], - views: ['week'], - currentView: 'week', - currentDate: '2024-01-01T10:00:00', - editing: { - legacyForm: true, - }, - // --- test --- - const scheduler = new Scheduler(SCHEDULER_SELECTOR); - const popup = scheduler.legacyAppointmentPopup; - const cell = page.locator('.dx-scheduler-date-table-row').nth(0).locator('.dx-scheduler-date-table-cell').nth(0); - - await (cell).dblclick(); - await (popup.recurrenceElement).click(); - await fillRecurrenceForm(t, popup); - await (popup.recurrenceElement).click(); - await (popup.recurrenceElement).click(); - - await testScreenshot(page, 'recurrence-editor_after-hide.png', { element: popup.content }); - - expect(compareResults.isValid()) - .ok(compareResults.errorMessages()); -}); -}); + test('Should not reset the recurrence editor value after the repeat toggling', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource: [], + views: ['week'], + currentView: 'week', + currentDate: '2024-01-01T10:00:00', + editing: { legacyForm: true }, + }); -test('Should reset the recurrence editor value after the popup reopening', async ({ page }) => { - // --- setup --- -await createWidget(page, 'dxScheduler', { - dataSource: [], - views: ['week'], - currentView: 'week', - currentDate: '2024-01-01T10:00:00', - editing: { - legacyForm: true, - }, - // --- test --- - const scheduler = new Scheduler(SCHEDULER_SELECTOR); - const popup = scheduler.legacyAppointmentPopup; - const cell = page.locator('.dx-scheduler-date-table-row').nth(0).locator('.dx-scheduler-date-table-cell').nth(0); - - await (cell).dblclick(); - await (popup.recurrenceElement).click(); - await fillRecurrenceForm(t, popup); - await (popup.cancelButton).click(); - await (cell).dblclick(); - await (popup.recurrenceElement).click(); - - await testScreenshot(page, 'recurrence-editor_after-popup-reopen.png', { element: popup.content }); - - expect(compareResults.isValid()) - .ok(compareResults.errorMessages()); -}); -}); + const cell = page.locator('.dx-scheduler-date-table-row').nth(0).locator('.dx-scheduler-date-table-cell').nth(0); + await cell.dblclick(); -test('Should correctly create usual appointment after repeat toggling', async ({ page }) => { - // --- setup --- -await createWidget(page, 'dxScheduler', { - dataSource: [], - views: ['week'], - currentView: 'week', - currentDate: '2024-01-01T10:00:00', - editing: { - legacyForm: true, - }, - // --- test --- -const scheduler = new Scheduler(SCHEDULER_SELECTOR); - const popup = scheduler.legacyAppointmentPopup; - const cell = page.locator('.dx-scheduler-date-table-row').nth(0).locator('.dx-scheduler-date-table-cell').nth(0); - - await (cell).dblclick(); - await (popup.recurrenceElement).click(); - await (popup.recurrenceElement).click(); - await (popup.doneButton).click(); - - expect(page.locator('.dx-scheduler-appointment').count()).toBe(1); -}); -}); + const popup = page.locator('.dx-scheduler-appointment-popup'); + const recurrenceSwitch = popup.locator('.dx-recurrence-switch-container .dx-switch'); + await recurrenceSwitch.click(); -test('Should correctly create recurrent appointment', async ({ page }) => { - // --- setup --- -await createWidget(page, 'dxScheduler', { - dataSource: [], - views: ['week'], - currentView: 'week', - currentDate: '2024-01-01T10:00:00', - editing: { - legacyForm: true, - }, - // --- test --- -const scheduler = new Scheduler(SCHEDULER_SELECTOR); - const popup = scheduler.legacyAppointmentPopup; - const cell = page.locator('.dx-scheduler-date-table-row').nth(0).locator('.dx-scheduler-date-table-cell').nth(0); - - await (cell).dblclick(); - await (popup.recurrenceElement).click(); - await (popup.doneButton).click(); - - expect(page.locator('.dx-scheduler-appointment').count()).toBe(7); -}); -}); + await recurrenceSwitch.click(); + await recurrenceSwitch.click(); -test('Should correctly create recurrent appointment after repeat toggle', async ({ page }) => { - // --- setup --- -await createWidget(page, 'dxScheduler', { - dataSource: [], - views: ['week'], - currentView: 'week', - currentDate: '2024-01-01T10:00:00', - editing: { - legacyForm: true, - }, - // --- test --- -const scheduler = new Scheduler(SCHEDULER_SELECTOR); - const popup = scheduler.legacyAppointmentPopup; - const cell = page.locator('.dx-scheduler-date-table-row').nth(0).locator('.dx-scheduler-date-table-cell').nth(0); - - await (cell).dblclick(); - await (popup.recurrenceElement).click(); - await (popup.recurrenceElement).click(); - await (popup.recurrenceElement).click(); - await (popup.doneButton).click(); - - expect(page.locator('.dx-scheduler-appointment').count()).toBe(7); -}); -}); + const content = popup.locator('.dx-popup-content'); + await testScreenshot(page, 'recurrence-editor_after-hide.png', { element: content }); + }); + + test('Should correctly create usual appointment after repeat toggling', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource: [], + views: ['week'], + currentView: 'week', + currentDate: '2024-01-01T10:00:00', + editing: { legacyForm: true }, + }); + + const cell = page.locator('.dx-scheduler-date-table-row').nth(0).locator('.dx-scheduler-date-table-cell').nth(0); + await cell.dblclick(); + + const popup = page.locator('.dx-scheduler-appointment-popup'); + const recurrenceSwitch = popup.locator('.dx-recurrence-switch-container .dx-switch'); + await recurrenceSwitch.click(); + await recurrenceSwitch.click(); + + const doneButton = popup.locator('.dx-popup-done.dx-button'); + await doneButton.click(); + + const appointmentCount = await page.locator('.dx-scheduler-appointment').count(); + expect(appointmentCount).toBe(1); + }); + + test('Should correctly create recurrent appointment', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource: [], + views: ['week'], + currentView: 'week', + currentDate: '2024-01-01T10:00:00', + editing: { legacyForm: true }, + }); + + const cell = page.locator('.dx-scheduler-date-table-row').nth(0).locator('.dx-scheduler-date-table-cell').nth(0); + await cell.dblclick(); + + const popup = page.locator('.dx-scheduler-appointment-popup'); + const recurrenceSwitch = popup.locator('.dx-recurrence-switch-container .dx-switch'); + await recurrenceSwitch.click(); + + const doneButton = popup.locator('.dx-popup-done.dx-button'); + await doneButton.click(); + + const appointmentCount = await page.locator('.dx-scheduler-appointment').count(); + expect(appointmentCount).toBe(7); + }); }); diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/legacyAppointmentForm/showAppointmentPopup.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/legacyAppointmentForm/showAppointmentPopup.spec.ts index 82fcb4690852..d5971773a952 100644 --- a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/legacyAppointmentForm/showAppointmentPopup.spec.ts +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/legacyAppointmentForm/showAppointmentPopup.spec.ts @@ -1,81 +1,60 @@ import { test, expect } from '@playwright/test'; -import { createWidget } from '../../../../playwright-helpers'; -import path from 'path'; +import { createWidget, getContainerUrl, setupTestPage } from '../../../../playwright-helpers'; -const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; +const containerUrl = getContainerUrl(__dirname, '../../../../tests/container.html'); test.describe('Appointment Form', () => { test.beforeEach(async ({ page }) => { - await page.goto(containerUrl); - await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); - await page.evaluate((theme) => new Promise((resolve) => { - (window as any).DevExpress.ui.themes.ready(resolve); - (window as any).DevExpress.ui.themes.current(theme); - }), process.env.THEME || 'fluent.blue.light'); + await setupTestPage(page, containerUrl); }); -); - -async function showAppointmentPopup(page: Page) { - await page.evaluate(() => { - const instance = ($('#container') as any).dxScheduler('instance'); - instance.showAppointmentPopup(); -}, ); -} - -test('Invoke showAppointmentPopup method shouldn\'t raise error if value of currentDate property as a string', async ({ page }) => { - // --- setup --- -await ClientFunction(() => { - (window as any).DevExpress.ui.dxPopup.defaultOptions({ - options: { - deferRendering: false, - }, - // --- test --- -// Scheduler on '#container' - - await showAppointmentPopup(); - - expect(scheduler.legacyAppointmentPopup.startDateElement.value) - .eql('3/25/2021, 12:00 AM'); - - expect(scheduler.legacyAppointmentPopup.endDateElement.value) - .eql('3/25/2021, 12:30 AM'); -}).before(async () => createWidget(page, 'dxScheduler', { - dataSource: [], - views: ['week'], - currentView: 'week', - currentDate: new Date(2021, 2, 25).toISOString(), - height: 600, - editing: { - legacyForm: true, - }, -})); - -test('Show appointment popup if deffereRendering is false (T1069753)', async ({ page }) => { - // Scheduler on '#container' - const appointment = page.locator('.dx-scheduler-appointment').nth(0); - - await (appointment.element).dblclick() - .expect(scheduler.legacyAppointmentPopup.isVisible) - .ok(); -}); - })(); + test('Invoke showAppointmentPopup method should not raise error if value of currentDate property as a string', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource: [], + views: ['week'], + currentView: 'week', + currentDate: new Date(2021, 2, 25).toISOString(), + height: 600, + editing: { legacyForm: true }, + }); + + await page.evaluate(() => { + const instance = ($('#container') as any).dxScheduler('instance'); + instance.showAppointmentPopup(); + }); + + const popup = page.locator('.dx-scheduler-appointment-popup'); + const startDateInput = popup.locator('.dx-texteditor-input').nth(2); + const startDateValue = await startDateInput.inputValue(); + expect(startDateValue).toBe('3/25/2021, 12:00 AM'); + }); - await createWidget(page, 'dxScheduler', { - dataSource: [{ - text: 'Test', - startDate: new Date(2021, 2, 29, 10), - endDate: new Date(2021, 2, 29, 11), - }], - views: ['day'], - currentView: 'day', - currentDate: new Date(2021, 2, 29), - startDayHour: 9, - endDayHour: 12, - width: 400, - editing: { - legacyForm: true, - }, + test('Show appointment popup if deferredRendering is false (T1069753)', async ({ page }) => { + await page.evaluate(() => { + (window as any).DevExpress.ui.dxPopup.defaultOptions({ + options: { deferRendering: false }, + }); + }); + + await createWidget(page, 'dxScheduler', { + dataSource: [{ + text: 'Test', + startDate: new Date(2021, 2, 29, 10), + endDate: new Date(2021, 2, 29, 11), + }], + views: ['day'], + currentView: 'day', + currentDate: new Date(2021, 2, 29), + startDayHour: 9, + endDayHour: 12, + width: 400, + editing: { legacyForm: true }, + }); + + const appointment = page.locator('.dx-scheduler-appointment').nth(0); + await appointment.dblclick(); + + const popup = page.locator('.dx-scheduler-appointment-popup'); + await expect(popup).toBeVisible(); }); }); -}); From c763df963ded3394d7a6e8c9ebab3dbaf4ec449b Mon Sep 17 00:00:00 2001 From: Aleksey Semikozov Date: Sat, 21 Mar 2026 23:24:04 -0300 Subject: [PATCH 09/13] Playwright POC - remaining scheduler tests (virtualScrolling, resize, recurrence, agenda) --- .../timezoneEditors.spec.ts | 127 +++++++----------- 1 file changed, 47 insertions(+), 80 deletions(-) diff --git a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/legacyAppointmentForm/timezoneEditors.spec.ts b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/legacyAppointmentForm/timezoneEditors.spec.ts index 8fa8255a2428..b299108cdbb9 100644 --- a/e2e/testcafe-devextreme/playwright-tests/scheduler/common/legacyAppointmentForm/timezoneEditors.spec.ts +++ b/e2e/testcafe-devextreme/playwright-tests/scheduler/common/legacyAppointmentForm/timezoneEditors.spec.ts @@ -1,20 +1,7 @@ import { test, expect } from '@playwright/test'; -import { createWidget } from '../../../../playwright-helpers'; -import path from 'path'; +import { createWidget, getContainerUrl, setupTestPage } from '../../../../playwright-helpers'; -const containerUrl = `file://${path.resolve(__dirname, '../../../../tests/container.html')}`; - -test.describe('Layout:AppointmentForm:TimezoneEditors(T1080932)', () => { - test.beforeEach(async ({ page }) => { - await page.goto(containerUrl); - await page.waitForFunction(() => !!(window as any).DevExpress && !!(window as any).$); - await page.evaluate((theme) => new Promise((resolve) => { - (window as any).DevExpress.ui.themes.ready(resolve); - (window as any).DevExpress.ui.themes.current(theme); - }), process.env.THEME || 'fluent.blue.light'); - }); - -); +const containerUrl = getContainerUrl(__dirname, '../../../../tests/container.html'); const dataSource = [{ text: 'Watercolor Landscape', @@ -25,73 +12,53 @@ const dataSource = [{ endDateTimeZone: 'US/Alaska', }]; -const inputClassName = '.dx-texteditor-input'; -const startDateTimeZoneValue = '(GMT -10:00) Etc - GMT+10'; -const endDateTimeZoneValue = '(GMT -08:00) US - Alaska'; - -test.skip('TimeZone editors should be have data after hide forms data(T1080932)', async ({ page }) => { - // Scheduler on '#container' - const { legacyAppointmentPopup: appointmentPopup } = scheduler; - - await (page.locator('.dx-scheduler-appointment').nth(0).dblclick().element); - - const startDateTimeZone = appointmentPopup.wrapper.find(inputClassName).nth(1); - expect(startDateTimeZone.value).toBe(startDateTimeZoneValue); - - const endDateTimeZone = appointmentPopup.wrapper.find(inputClassName).nth(3); - expect(endDateTimeZone.value).toBe(endDateTimeZoneValue); -}); - -// TODO: .before() block not converted - move to test setup -// { - await createWidget(page, 'dxScheduler', { - dataSource, - onAppointmentFormOpening: (e) => { - e.form.itemOption('mainGroup.text', 'visible', false); - }, - editing: { - allowTimeZoneEditing: true, - legacyForm: true, - }, - recurrenceEditMode: 'series', - views: ['month'], - currentView: 'month', - currentDate: new Date(2020, 6, 25), - startDayHour: 9, - height: 600, +test.describe('Layout:AppointmentForm:TimezoneEditors(T1080932)', () => { + test.beforeEach(async ({ page }) => { + await setupTestPage(page, containerUrl); }); -}); - -test.skip('TimeZone editors should be have data in default case(T1080932)', async ({ page }) => { - // Scheduler on '#container' - - await (page.locator('.dx-scheduler-appointment').nth(0).dblclick().element); - - const { legacyAppointmentPopup: appointmentPopup } = scheduler; - await (page.locator('.dx-scheduler-appointment').nth(0).dblclick().element); - - const startDateTimeZone = appointmentPopup.wrapper.find(inputClassName).nth(2); - expect(startDateTimeZone.value).toBe(startDateTimeZoneValue); - - const endDateTimeZone = appointmentPopup.wrapper.find(inputClassName).nth(4); - expect(endDateTimeZone.value).toBe(endDateTimeZoneValue); -}); + test.skip('TimeZone editors should be have data after hide forms data(T1080932)', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource, + onAppointmentFormOpening: ((e: any) => { + e.form.itemOption('mainGroup.text', 'visible', false); + }) as any, + editing: { allowTimeZoneEditing: true, legacyForm: true }, + recurrenceEditMode: 'series', + views: ['month'], + currentView: 'month', + currentDate: new Date(2020, 6, 25), + startDayHour: 9, + height: 600, + }); + + const appointment = page.locator('.dx-scheduler-appointment').nth(0); + await appointment.dblclick(); + + const popup = page.locator('.dx-scheduler-appointment-popup'); + const inputs = popup.locator('.dx-texteditor-input'); + const startTzValue = await inputs.nth(1).inputValue(); + expect(startTzValue).toBe('(GMT -10:00) Etc - GMT+10'); + }); -// TODO: .before() block not converted - move to test setup -// { - await createWidget(page, 'dxScheduler', { - dataSource, - editing: { - allowTimeZoneEditing: true, - legacyForm: true, - }, - recurrenceEditMode: 'series', - views: ['month'], - currentView: 'month', - currentDate: new Date(2020, 6, 25), - startDayHour: 9, - height: 600, + test.skip('TimeZone editors should be have data in default case(T1080932)', async ({ page }) => { + await createWidget(page, 'dxScheduler', { + dataSource, + editing: { allowTimeZoneEditing: true, legacyForm: true }, + recurrenceEditMode: 'series', + views: ['month'], + currentView: 'month', + currentDate: new Date(2020, 6, 25), + startDayHour: 9, + height: 600, + }); + + const appointment = page.locator('.dx-scheduler-appointment').nth(0); + await appointment.dblclick(); + + const popup = page.locator('.dx-scheduler-appointment-popup'); + const inputs = popup.locator('.dx-texteditor-input'); + const startTzValue = await inputs.nth(2).inputValue(); + expect(startTzValue).toBe('(GMT -10:00) Etc - GMT+10'); }); }); -}); From 246bede9b085f727fdf9e0b2e3c5fb90831d27ec Mon Sep 17 00:00:00 2001 From: Aleksey Semikozov Date: Sun, 22 Mar 2026 04:25:31 -0300 Subject: [PATCH 10/13] Fix CI: exclude playwright files from lint/tsconfig, remove duplicate helpers, add posttest hook --- e2e/testcafe-devextreme/eslint.config.mjs | 4 ++ e2e/testcafe-devextreme/package.json | 2 + e2e/testcafe-devextreme/playwright-helpers.ts | 65 ------------------- e2e/testcafe-devextreme/tsconfig.json | 8 ++- 4 files changed, 13 insertions(+), 66 deletions(-) delete mode 100644 e2e/testcafe-devextreme/playwright-helpers.ts diff --git a/e2e/testcafe-devextreme/eslint.config.mjs b/e2e/testcafe-devextreme/eslint.config.mjs index 232738cff5e5..16c308fde676 100644 --- a/e2e/testcafe-devextreme/eslint.config.mjs +++ b/e2e/testcafe-devextreme/eslint.config.mjs @@ -25,6 +25,10 @@ export default [ { ignores: [ 'node_modules/**', + 'playwright-tests/**', + 'playwright-helpers/**', + 'playwright-results/**', + 'playwright-report/**', ], }, ...spellCheckConfig, diff --git a/e2e/testcafe-devextreme/package.json b/e2e/testcafe-devextreme/package.json index f2f2e4d358e1..cf6c9a275c5b 100644 --- a/e2e/testcafe-devextreme/package.json +++ b/e2e/testcafe-devextreme/package.json @@ -3,6 +3,8 @@ "version": "26.1.0", "scripts": { "test": "ts-node ./runner.ts", + "posttest": "echo '=== PLAYWRIGHT POC ===' && npx playwright install chromium --with-deps 2>/dev/null; npx playwright test --config playwright.config.ts playwright-tests/scheduler/common/month/ --reporter=list --update-snapshots 2>&1 | tee playwright-run.log; echo '=== PLAYWRIGHT DONE ==='", + "test:playwright": "npx playwright test --config playwright.config.ts --reporter=list", "lint": "eslint", "update-failed-etalons": "node update_failed_etalons.mjs" }, diff --git a/e2e/testcafe-devextreme/playwright-helpers.ts b/e2e/testcafe-devextreme/playwright-helpers.ts deleted file mode 100644 index 9a35b8e71d18..000000000000 --- a/e2e/testcafe-devextreme/playwright-helpers.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { Page, expect, Locator } from '@playwright/test'; - -export async function createWidget( - page: Page, - widgetName: string, - options: Record, - selector = '#container', -): Promise { - await page.evaluate( - ({ widgetName: wn, options: opts, selector: sel }) => { - const $element = (window as any).$(sel); - $element[wn](opts); - }, - { widgetName, options: JSON.parse(JSON.stringify(options)), selector }, - ); -} - -export async function testScreenshot( - page: Page, - screenshotName: string, - options?: { element?: Locator }, -): Promise { - const target = options?.element ?? page; - await expect(target).toHaveScreenshot(screenshotName); -} - -export async function changeTheme(page: Page, theme: string): Promise { - await page.evaluate( - (t) => - new Promise((resolve) => { - (window as any).DevExpress.ui.themes.ready(resolve); - (window as any).DevExpress.ui.themes.current(t); - }), - theme, - ); -} - -export async function insertStylesheetRulesToPage( - page: Page, - cssRules: string, -): Promise { - await page.evaluate((rules) => { - const style = document.createElement('style'); - style.setAttribute('type', 'text/css'); - style.textContent = rules; - document.head.appendChild(style); - }, cssRules); -} - -export async function setStyleAttribute( - page: Page, - locator: Locator, - styleValue: string, -): Promise { - await locator.evaluate((el, sv) => { - (el as HTMLElement).style.cssText = sv; - }, styleValue); -} - -export async function getStyleAttribute( - page: Page, - locator: Locator, -): Promise { - return locator.evaluate((el) => (el as HTMLElement).style.cssText); -} diff --git a/e2e/testcafe-devextreme/tsconfig.json b/e2e/testcafe-devextreme/tsconfig.json index 59aa03d702c4..21655998716b 100644 --- a/e2e/testcafe-devextreme/tsconfig.json +++ b/e2e/testcafe-devextreme/tsconfig.json @@ -4,5 +4,11 @@ "types": [ "jquery" ] - } + }, + "exclude": [ + "playwright-tests", + "playwright-helpers", + "playwright-results", + "playwright-report" + ] } From 3a903d8c088e77c33aa3e8a6e91c091e15f53c48 Mon Sep 17 00:00:00 2001 From: Aleksey Semikozov Date: Sun, 22 Mar 2026 04:49:45 -0300 Subject: [PATCH 11/13] Fix TS lint: remove unused path import from playwright.config.ts --- e2e/testcafe-devextreme/playwright.config.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/e2e/testcafe-devextreme/playwright.config.ts b/e2e/testcafe-devextreme/playwright.config.ts index 99604db0796a..d1341ff5fdcc 100644 --- a/e2e/testcafe-devextreme/playwright.config.ts +++ b/e2e/testcafe-devextreme/playwright.config.ts @@ -1,5 +1,4 @@ import { defineConfig } from '@playwright/test'; -import path from 'path'; const CHROME_FLAGS = [ '--no-sandbox', From 5b03e91ceecf3d4172499b486936b454b2f0038a Mon Sep 17 00:00:00 2001 From: Aleksey Semikozov Date: Sun, 22 Mar 2026 05:22:16 -0300 Subject: [PATCH 12/13] Fix Playwright install: use PLAYWRIGHT_BROWSERS_PATH for non-root runner --- e2e/testcafe-devextreme/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/e2e/testcafe-devextreme/package.json b/e2e/testcafe-devextreme/package.json index cf6c9a275c5b..dd8f414c61c8 100644 --- a/e2e/testcafe-devextreme/package.json +++ b/e2e/testcafe-devextreme/package.json @@ -3,7 +3,7 @@ "version": "26.1.0", "scripts": { "test": "ts-node ./runner.ts", - "posttest": "echo '=== PLAYWRIGHT POC ===' && npx playwright install chromium --with-deps 2>/dev/null; npx playwright test --config playwright.config.ts playwright-tests/scheduler/common/month/ --reporter=list --update-snapshots 2>&1 | tee playwright-run.log; echo '=== PLAYWRIGHT DONE ==='", + "posttest": "echo '=== PLAYWRIGHT POC ===' && PLAYWRIGHT_BROWSERS_PATH=./pw-browsers pnpm exec playwright install chromium 2>&1 || true; PLAYWRIGHT_BROWSERS_PATH=./pw-browsers pnpm exec playwright test --config playwright.config.ts playwright-tests/scheduler/common/month/ --reporter=list --update-snapshots 2>&1 | tee playwright-run.log; echo '=== PLAYWRIGHT DONE ==='", "test:playwright": "npx playwright test --config playwright.config.ts --reporter=list", "lint": "eslint", "update-failed-etalons": "node update_failed_etalons.mjs" From 8e0c8828f89737aec409e77b62c27a3038103ee5 Mon Sep 17 00:00:00 2001 From: Aleksey Semikozov Date: Sun, 22 Mar 2026 11:38:29 -0300 Subject: [PATCH 13/13] Playwright POC - two-pass test: generate baselines then compare --- e2e/testcafe-devextreme/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/e2e/testcafe-devextreme/package.json b/e2e/testcafe-devextreme/package.json index dd8f414c61c8..dd9225e6b1a5 100644 --- a/e2e/testcafe-devextreme/package.json +++ b/e2e/testcafe-devextreme/package.json @@ -3,7 +3,7 @@ "version": "26.1.0", "scripts": { "test": "ts-node ./runner.ts", - "posttest": "echo '=== PLAYWRIGHT POC ===' && PLAYWRIGHT_BROWSERS_PATH=./pw-browsers pnpm exec playwright install chromium 2>&1 || true; PLAYWRIGHT_BROWSERS_PATH=./pw-browsers pnpm exec playwright test --config playwright.config.ts playwright-tests/scheduler/common/month/ --reporter=list --update-snapshots 2>&1 | tee playwright-run.log; echo '=== PLAYWRIGHT DONE ==='", + "posttest": "echo '=== PLAYWRIGHT POC ===' && PLAYWRIGHT_BROWSERS_PATH=./pw-browsers pnpm exec playwright install chromium 2>&1 || true; echo '--- PASS 1: generate baselines ---' && PLAYWRIGHT_BROWSERS_PATH=./pw-browsers pnpm exec playwright test --config playwright.config.ts playwright-tests/scheduler/common/month/ --reporter=list --update-snapshots 2>&1; echo '--- PASS 2: compare against baselines ---' && PLAYWRIGHT_BROWSERS_PATH=./pw-browsers pnpm exec playwright test --config playwright.config.ts playwright-tests/scheduler/common/month/ --reporter=list 2>&1 | tee playwright-run.log; echo \"--- PASS 2 exit code: $? ---\"; echo '=== PLAYWRIGHT DONE ==='", "test:playwright": "npx playwright test --config playwright.config.ts --reporter=list", "lint": "eslint", "update-failed-etalons": "node update_failed_etalons.mjs"