diff --git a/e2e/testcafe-devextreme/helpers/mouseUpEvents.ts b/e2e/testcafe-devextreme/helpers/mouseUpEvents.ts index 0dd1fc5efd24..3aa25a42a093 100644 --- a/e2e/testcafe-devextreme/helpers/mouseUpEvents.ts +++ b/e2e/testcafe-devextreme/helpers/mouseUpEvents.ts @@ -32,3 +32,37 @@ export const MouseUpEvents = { disable: disableMouseUpEvent, enable: enableMouseUpEvent, }; + +/** + * Performs a drag operation on a row with MouseUpEvents temporarily disabled. + * This is useful for autoscroll tests where we need to keep the row in drag state. + * + * @param t - TestController instance + * @param element - Selector for the element to drag + * @param options - drag options + * @param options.offsetX - X-offset for the drag operation + * @param options.offsetY - Y-offset for the drag operation + * @param options.speed - speed of the drag operation (default is 0.1) + */ +export const dragWithDisabledMouseUp = async ( + t: TestController, + element: Selector, + { + offsetX, + offsetY, + speed = 0.1, + }: { + offsetX: number; + offsetY: number; + speed?: number; + }, +): Promise => { + await MouseUpEvents.disable(MouseAction.dragToOffset); + await t.drag( + element, + offsetX, + offsetY, + { speed }, + ); + await MouseUpEvents.enable(MouseAction.dragToOffset); +}; diff --git a/e2e/testcafe-devextreme/tests/dataGrid/common/etalons/T1179218-virtual-scrolling-dragging-row (fluent.blue.light).png b/e2e/testcafe-devextreme/tests/dataGrid/common/etalons/T1179218-virtual-scrolling-dragging-row (fluent.blue.light).png deleted file mode 100644 index bb19c9664492..000000000000 Binary files a/e2e/testcafe-devextreme/tests/dataGrid/common/etalons/T1179218-virtual-scrolling-dragging-row (fluent.blue.light).png and /dev/null differ diff --git a/e2e/testcafe-devextreme/tests/dataGrid/common/rowDragging/etalons/T1179218-virtual-scrolling-dragging-row (fluent.blue.light).png b/e2e/testcafe-devextreme/tests/dataGrid/common/rowDragging/etalons/T1179218-virtual-scrolling-dragging-row (fluent.blue.light).png new file mode 100644 index 000000000000..f4ec46b97a53 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/dataGrid/common/rowDragging/etalons/T1179218-virtual-scrolling-dragging-row (fluent.blue.light).png differ diff --git a/e2e/testcafe-devextreme/tests/dataGrid/common/rowDragging.ts b/e2e/testcafe-devextreme/tests/dataGrid/common/rowDragging/functional.ts similarity index 84% rename from e2e/testcafe-devextreme/tests/dataGrid/common/rowDragging.ts rename to e2e/testcafe-devextreme/tests/dataGrid/common/rowDragging/functional.ts index 4b223aae8cf1..219fdba12b32 100644 --- a/e2e/testcafe-devextreme/tests/dataGrid/common/rowDragging.ts +++ b/e2e/testcafe-devextreme/tests/dataGrid/common/rowDragging/functional.ts @@ -1,12 +1,11 @@ /* eslint-disable @typescript-eslint/no-misused-promises */ import { ClientFunction, Selector } from 'testcafe'; -import { createScreenshotsComparer } from 'devextreme-screenshot-comparer'; import DataGrid, { CLASS as DataGridClassNames } from 'devextreme-testcafe-models/dataGrid'; import { ClassNames } from 'devextreme-testcafe-models/dataGrid/classNames'; -import { MouseUpEvents, MouseAction } from '../../../helpers/mouseUpEvents'; -import url from '../../../helpers/getPageUrl'; -import { createWidget } from '../../../helpers/createWidget'; -import { testScreenshot } from '../../../helpers/themeUtils'; +import { dragWithDisabledMouseUp } from '../../../../helpers/mouseUpEvents'; +import url from '../../../../helpers/getPageUrl'; +import { createWidget } from '../../../../helpers/createWidget'; +import { isScrollAtEnd, getOffsetToTriggerAutoScroll } from '../../helpers/rowDraggingHelpers'; const CLASS = { ...DataGridClassNames, ...ClassNames }; @@ -55,8 +54,8 @@ const generateData = (rowCount, columnCount): Record[] => { return items; }; -fixture`Row dragging` - .page(url(__dirname, '../../container.html')); +fixture`Row dragging.Functional` + .page(url(__dirname, '../../../container.html')); // T903351 test('The placeholder should appear when a cross-component dragging rows after scrolling the window', async (t) => { @@ -468,7 +467,7 @@ test('Headers should not be hidden during auto scrolling when virtual scrollling }); // T1078513 -test.meta({ unstable: true })('Footer should not be hidden during auto scrolling when virtual scrollling is specified', async (t) => { +test('Footer should not be hidden during auto scrolling when virtual scrolling is specified', async (t) => { const dataGrid = new DataGrid('#container'); await t.drag(dataGrid.getDataRow(0).getDragCommand(), 0, 90, { speed: 0.1 }); @@ -570,19 +569,41 @@ test('The draggable element should be displayed correctly after horizontal scrol }); }); -test.meta({ unstable: true })('Dragging with scrolling should be prevented by e.cancel (T1179555)', async (t) => { +test('Dragging with scrolling should be prevented by e.cancel (T1179555)', async (t) => { const dataGrid = new DataGrid('#container'); - await dataGrid.scrollBy(t, { top: 10000 }); await t.expect(dataGrid.isReady()).ok(); - await MouseUpEvents.disable(MouseAction.dragToOffset); + await dataGrid.scrollBy(t, { top: 10000 }); + + await t + .expect(dataGrid.getDataRow(99).getDataCell(1).element.textContent) + .eql('99') + .expect(isScrollAtEnd('vertical')) + .ok(); + + const visibleRows = await dataGrid.apiGetVisibleRows(); + const scrollTopOffsetByTheme = await getOffsetToTriggerAutoScroll( + visibleRows.length - 2, + 1, + ); - await t.drag(dataGrid.getDataRow(98).getDragCommand(), 0, -180, { speed: 0.1 }); + await dragWithDisabledMouseUp( + t, + dataGrid.getDataRow(98).getDragCommand(), + { offsetX: 0, offsetY: scrollTopOffsetByTheme, speed: 0.8 }, + ); - await t.expect(Selector('.dx-sortable-placeholder').visible).notOk(); + // Wait for autoscrolling + await t.wait(2000); - await MouseUpEvents.enable(MouseAction.dragToOffset); + await t + .expect(dataGrid.getDataRow(0).getDataCell(1).element.textContent) + .eql('0') + .expect(dataGrid.getScrollTop()) + .eql(0); + + await t.expect(Selector('.dx-sortable-placeholder').visible).notOk(); }).before(async (t) => { await t.maximizeWindow(); return createWidget('dxDataGrid', { @@ -661,56 +682,78 @@ test('The placeholder should have correct position after dragging the row to the })); // T1126013 -test.meta({ unstable: true })('toIndex should not be corrected when source item gets removed from DOM', async (t) => { - const fromIndex = 2; - const toIndex = 4; - +test('toIndex should not be corrected when source item gets removed from DOM', async (t) => { + // arrange const dataGrid = new DataGrid('#container'); + const AUTOSCROLL_WAIT_TIME = 2000; + const AUTOSCROLL_SPEED_FACTOR = 0.5; + + await t.expect(dataGrid.isReady()).ok(); + + // act - scroll to the bottom to make the last row visible await dataGrid.scrollTo(t, { y: 3000 }); - const sourceKey = await ClientFunction((grid, idx) => { - const instance: any = grid.getInstance(); - const visibleRows = instance.getVisibleRows(); - return visibleRows[idx]?.key; - }, { dependencies: {} })(dataGrid, fromIndex); + // assert + await t + .expect(dataGrid.getDataRow(49).getDataCell(1).element.textContent) + .eql('50-1') + .expect(isScrollAtEnd('vertical')) + .ok(); - const initialIndices = await ClientFunction((grid) => { - const instance: any = grid.getInstance(); - return instance.getVisibleRows().map((r: any) => r.key); - })(dataGrid); - const sourceInitialIndex = initialIndices.indexOf(sourceKey); + let visibleRows = await dataGrid.apiGetVisibleRows(); + const draggableRow = visibleRows[1]; - await dataGrid.moveRow(fromIndex, 0, 50, true); - await dataGrid.moveRow(fromIndex, 0, -20); - await dataGrid.moveRow(toIndex, 0, 5); + // Calculate offsetY to trigger upward autoscroll when dragging the row. + // Using speedFactor 0.5 to ensure medium scrolling speed. + const scrollOffsetForFastAutoScroll = await getOffsetToTriggerAutoScroll( + draggableRow.rowIndex, + AUTOSCROLL_SPEED_FACTOR, + ); - await ClientFunction((grid) => { - const instance = grid.getInstance(); - $(instance.element()).trigger($.Event('dxpointerup')); - })(dataGrid); + // act - drag a row up the grid to start auto-scrolling. + await dataGrid.moveRow(draggableRow.rowIndex, 0, 150, true); + await dataGrid.moveRow(draggableRow.rowIndex, 0, 100); + await dataGrid.moveRow(draggableRow.rowIndex, 0, scrollOffsetForFastAutoScroll); + + // Waiting for autoscrolling + await t.wait(AUTOSCROLL_WAIT_TIME); + + // assert + visibleRows = await dataGrid.apiGetVisibleRows(); + + await t + .expect(dataGrid.getDataRow(0).getDataCell(1).element.textContent) + .eql('1-1') + .expect(dataGrid.getScrollTop()) + .eql(0); - const finalIndex = await ClientFunction((grid, key) => { - const instance: any = grid.getInstance(); - const visibleRows = instance.getVisibleRows(); - return visibleRows.findIndex((r: any) => r.key === key); - })(dataGrid, sourceKey); + // act - drag and drop the row to the third position (after row 2-1). + const rowHeight = await dataGrid.getDataRow(0).element.offsetHeight; - const expectedFinalIndex = (toIndex - 1) - (sourceInitialIndex < toIndex ? 1 : 0); + await dataGrid.moveRow(draggableRow.rowIndex, 0, rowHeight / 2); + await dataGrid.dropRow(); - await t.expect(finalIndex) - .eql(expectedFinalIndex, `Dragged row key ${sourceKey} expected at ${expectedFinalIndex} but ended up at index ${finalIndex}`); + // assert + await t.expect(dataGrid.isReady()).ok(); + + visibleRows = await dataGrid.apiGetVisibleRows(); + + await t + .expect(visibleRows[0].key) + .eql('1-1') + .expect(visibleRows[1].key) + .eql('2-1') + .expect(visibleRows[2].key) + .eql(draggableRow.key); }).before(async (t) => { await t.maximizeWindow(); const items = generateData(50, 1); return createWidget('dxDataGrid', { - height: 250, + height: 260, keyExpr: 'field1', scrolling: { mode: 'virtual', }, - paging: { - pageSize: 4, - }, dataSource: items, rowDragging: { scrollSpeed: 300, @@ -785,39 +828,3 @@ test('Item should appear in a correct spot when dragging to a different page wit showBorders: true, }); }); - -// T1179218 -test.meta({ unstable: true })('Rows should appear correctly during dragging when virtual scrolling is enabled and rowDragging.dropFeedbackMode = "push"', async (t) => { - const dataGrid = new DataGrid('#container'); - const { takeScreenshot, compareResults } = createScreenshotsComparer(t); - - // drag the row down - await dataGrid.moveRow(0, 30, 150, true); - await dataGrid.moveRow(0, 30, 350); - - // waiting for autoscrolling - await t.wait(2000); - - // drag the row up - await dataGrid.moveRow(0, 30, 75); - - await testScreenshot(t, takeScreenshot, 'T1179218-virtual-scrolling-dragging-row.png', { element: dataGrid.element }); - await t - .expect(compareResults.isValid()) - .ok(compareResults.errorMessages()); -}).before(async (t) => { - await t.maximizeWindow(); - return createWidget('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', - }, - }); -}); diff --git a/e2e/testcafe-devextreme/tests/dataGrid/common/rowDragging/visual.ts b/e2e/testcafe-devextreme/tests/dataGrid/common/rowDragging/visual.ts new file mode 100644 index 000000000000..dc96f8ca76c1 --- /dev/null +++ b/e2e/testcafe-devextreme/tests/dataGrid/common/rowDragging/visual.ts @@ -0,0 +1,60 @@ +import { createScreenshotsComparer } from 'devextreme-screenshot-comparer'; +import DataGrid from 'devextreme-testcafe-models/dataGrid'; +import url from '../../../../helpers/getPageUrl'; +import { createWidget } from '../../../../helpers/createWidget'; +import { testScreenshot } from '../../../../helpers/themeUtils'; +import { getOffsetToTriggerAutoScroll, isScrollAtEnd } from '../../helpers/rowDraggingHelpers'; + +fixture`Row dragging.Visual` + .page(url(__dirname, '../../../container.html')); + +// T1179218 +test('Rows should appear correctly during dragging when virtual scrolling is enabled and rowDragging.dropFeedbackMode = "push"', async (t) => { + const dataGrid = new DataGrid('#container'); + const { takeScreenshot, compareResults } = createScreenshotsComparer(t); + + // 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 t.wait(2000); + + await t + .expect(dataGrid.getDataRow(99).getDataCell(1).element.textContent) + .eql('99') + .expect(isScrollAtEnd('vertical')) + .ok(); + + // drag the row up + await dataGrid.moveRow(0, 30, await getOffsetToTriggerAutoScroll(0, 1)); + + // waiting for autoscrolling + await t.wait(2000); + + await t + .expect(dataGrid.getDataRow(0).getDataCell(1).element.textContent) + .eql('0') + .expect(dataGrid.getScrollTop()) + .eql(0); + + await testScreenshot(t, takeScreenshot, 'T1179218-virtual-scrolling-dragging-row.png', { element: dataGrid.element }); + await t + .expect(compareResults.isValid()) + .ok(compareResults.errorMessages()); +}).before(async (t) => { + await t.maximizeWindow(); + return createWidget('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', + }, + }); +}); diff --git a/e2e/testcafe-devextreme/tests/dataGrid/helpers/rowDraggingHelpers.ts b/e2e/testcafe-devextreme/tests/dataGrid/helpers/rowDraggingHelpers.ts new file mode 100644 index 000000000000..a57db5a328e9 --- /dev/null +++ b/e2e/testcafe-devextreme/tests/dataGrid/helpers/rowDraggingHelpers.ts @@ -0,0 +1,78 @@ +import { ClientFunction } from 'testcafe'; + +export const isScrollAtEnd = ClientFunction((orientation: 'vertical' | 'horizontal' = 'vertical') => { + const element = $('.dx-datagrid-rowsview .dx-scrollable-container')[0]; + + if (!element) { + return false; + } + + const scrollSize = element[orientation === 'vertical' ? 'scrollHeight' : 'scrollWidth']; + const scrollPosition = element[orientation === 'vertical' ? 'scrollTop' : 'scrollLeft']; + const clientSize = element[orientation === 'vertical' ? 'clientHeight' : 'clientWidth']; + + return Math.round(scrollPosition + clientSize) >= scrollSize - 1; +}); + +/** + * Calculates offsetY for dragging a row to trigger autoscroll. + * Autoscroll triggers when the cursor is within scrollSensitivity (default 60px) + * from the edge of the container. + * Autoscroll speed is calculated by formula: + * Math.ceil(((sensitivity - distance) / sensitivity) ** 2 * maxSpeed) + * + * @param rowIndex - index of the row being dragged + * @param speedFactor - speed factor from 0 to 1 (default 0.5): + * - 0 = minimum speed (cursor at scrollSensitivity boundary) + * - 0.5 = medium speed (cursor halfway to the edge) + * - 1 = maximum speed (cursor at the container edge) + * @param direction - autoscroll direction (default 'up'): + * - 'up' = upward autoscroll (drag towards top edge) + * - 'down' = downward autoscroll (drag towards bottom edge) + * @returns offsetY relative to the current row position to trigger autoscroll + */ +export const getOffsetToTriggerAutoScroll = ClientFunction( + (rowIndex: number, speedFactor = 0.5, direction: 'up' | 'down' = 'up'): number => { + const gridInstance = $('#container').data('dxDataGrid'); + const $row = $(gridInstance.getRowElement(rowIndex)); + const $scrollContainer = $('.dx-datagrid-rowsview .dx-scrollable-container'); + + if (!$row.length || !$scrollContainer.length) { + return 0; + } + + const rowOffset = $row.offset(); + const containerOffset = $scrollContainer.offset(); + + if (!rowOffset || !containerOffset) { + return 0; + } + + // scrollSensitivity is 60px by default + // To trigger autoscroll, the row must be dragged so that + // the cursor is within scrollSensitivity from the edge of the container + const scrollSensitivity = gridInstance.option('rowDragging.scrollSensitivity') ?? 60; + + // Clamp speedFactor to [0, 1] range + const normalizedSpeedFactor = Math.max(0, Math.min(1, speedFactor)); + + // Calculate distance from the edge of the container: + // - speedFactor = 0: distance = scrollSensitivity (minimum speed) + // - speedFactor = 0.5: distance = scrollSensitivity / 2 (medium speed) + // - speedFactor = 1: distance = 0 (maximum speed, cursor at the edge) + const distance = scrollSensitivity * (1 - normalizedSpeedFactor); + + if (direction === 'up') { + // Target Y-coordinate for upward autoscroll + const targetY = containerOffset.top + distance + 1; + + return Math.round(targetY - rowOffset.top); + } + + // Target Y-coordinate for downward autoscroll + const containerHeight = $scrollContainer.height() ?? 0; + const targetY = containerOffset.top + containerHeight - distance - 1; + + return Math.round(targetY - rowOffset.top); + }, +); diff --git a/packages/testcafe-models/dataGrid/index.ts b/packages/testcafe-models/dataGrid/index.ts index 0b85751dc5f6..16bb9ef78898 100644 --- a/packages/testcafe-models/dataGrid/index.ts +++ b/packages/testcafe-models/dataGrid/index.ts @@ -76,6 +76,7 @@ export const CLASS = { toast: 'dx-toast-wrapper', dragHeader: 'drag-header', aiPromptEditor: 'dx-ai-prompt-editor', + sortableDragging: 'dx-sortable-dragging', }; const E2E_ATTRIBUTES = { @@ -570,6 +571,7 @@ export default class DataGrid extends GridCore { data: r.data, dataIndex: r.dataIndex, rowType: r.rowType, + rowIndex: r.rowIndex, })); }, { dependencies: { getInstance } })(); } @@ -759,6 +761,21 @@ export default class DataGrid extends GridCore { )(); } + dropRow(): Promise { + return ClientFunction((sortableDraggingClass) => { + const $dragElement = $(`.${sortableDraggingClass}`); + const dragOffset = $dragElement.offset(); + + triggerPointerUp($dragElement, dragOffset.left, dragOffset.top); + }, + { + dependencies: { + triggerPointerUp, + }, + }, + )(CLASS.sortableDragging); + } + resizeHeader(columnIndex: number, offset: number, needToTriggerPointerUp = true): Promise { const { getInstance } = this;