From 452effb0409d7ac3c4c1e17125bc7aa545547093 Mon Sep 17 00:00:00 2001 From: Alyar Date: Fri, 30 Jan 2026 14:19:58 +0400 Subject: [PATCH 1/3] DataGrid - Testcafe tests - Row dragging: unskip tests --- .../helpers/mouseUpEvents.ts | 34 ++++ ...lling-dragging-row (fluent.blue.light).png | Bin 0 -> 9631 bytes .../functional.ts} | 169 +++++++++--------- .../dataGrid/common/rowDragging/visual.ts | 60 +++++++ .../dataGrid/helpers/rowDraggingHelpers.ts | 85 +++++++++ packages/testcafe-models/dataGrid/index.ts | 17 ++ 6 files changed, 283 insertions(+), 82 deletions(-) create mode 100644 e2e/testcafe-devextreme/tests/dataGrid/common/rowDragging/etalons/T1179218-virtual-scrolling-dragging-row (fluent.blue.light).png rename e2e/testcafe-devextreme/tests/dataGrid/common/{rowDragging.ts => rowDragging/functional.ts} (85%) create mode 100644 e2e/testcafe-devextreme/tests/dataGrid/common/rowDragging/visual.ts create mode 100644 e2e/testcafe-devextreme/tests/dataGrid/helpers/rowDraggingHelpers.ts diff --git a/e2e/testcafe-devextreme/helpers/mouseUpEvents.ts b/e2e/testcafe-devextreme/helpers/mouseUpEvents.ts index 7d8de221373f..65d50bfb1e8a 100644 --- a/e2e/testcafe-devextreme/helpers/mouseUpEvents.ts +++ b/e2e/testcafe-devextreme/helpers/mouseUpEvents.ts @@ -33,3 +33,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/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 0000000000000000000000000000000000000000..f4ec46b97a53c04609e44f935e8c73efa50b0f91 GIT binary patch literal 9631 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[] => { 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 }); @@ -573,16 +572,38 @@ 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) => { 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,76 @@ 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); - 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); + // Waiting for autoscrolling + await t.wait(AUTOSCROLL_WAIT_TIME); - const expectedFinalIndex = (toIndex - 1) - (sourceInitialIndex < toIndex ? 1 : 0); + // assert + await t + .expect(dataGrid.getDataRow(0).getDataCell(1).element.textContent) + .eql('1-1') + .expect(dataGrid.getScrollTop()) + .eql(0); - await t.expect(finalIndex) - .eql(expectedFinalIndex, `Dragged row key ${sourceKey} expected at ${expectedFinalIndex} but ended up at index ${finalIndex}`); + // act - drag and drop the row to the third position (after row 2-1). + const rowHeight = await dataGrid.getDataRow(0).element.offsetHeight; + + await dataGrid.moveRow(draggableRow.rowIndex, 0, rowHeight / 2); + await dataGrid.dropRow(); + + // 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 +826,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..c2b9b8bfd2e0 --- /dev/null +++ b/e2e/testcafe-devextreme/tests/dataGrid/helpers/rowDraggingHelpers.ts @@ -0,0 +1,85 @@ +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']; + + // Subtract 1 from scrollSize to account for sub-pixel rendering and rounding errors. + // Due to browser rounding, scrollPosition + clientSize may not exactly equal scrollSize + // even when visually scrolled to the end. + 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. + // Add 1 to ensure the cursor is inside the autoscroll zone, not on its boundary. + // Without this offset, boundary values may not reliably trigger autoscroll due to rounding. + const targetY = containerOffset.top + distance + 1; + + return Math.round(targetY - rowOffset.top); + } + + // Target Y-coordinate for downward autoscroll. + // Subtract 1 to ensure the cursor is inside the autoscroll zone, not on its boundary. + // Without this offset, boundary values may not reliably trigger autoscroll due to rounding. + 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 012873f4fcfc..c022f311e268 100644 --- a/packages/testcafe-models/dataGrid/index.ts +++ b/packages/testcafe-models/dataGrid/index.ts @@ -75,6 +75,7 @@ export const CLASS = { summaryTotal: 'dx-datagrid-summary-item', scrollableContainer: 'dx-scrollable-container', columnsSeparator: 'dx-datagrid-columns-separator', + sortableDragging: 'dx-sortable-dragging', }; const E2E_ATTRIBUTES = { @@ -569,6 +570,7 @@ export default class DataGrid extends GridCore { return dataGrid.getVisibleRows().map((r) => ({ key: r.key, rowType: r.rowType, + rowIndex: r.rowIndex, })); }, { dependencies: { getInstance } })(); } @@ -750,6 +752,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; From 8b98dbb879b90d451b93d5e0973054a8feff3215 Mon Sep 17 00:00:00 2001 From: Alyar <> Date: Fri, 30 Jan 2026 14:45:22 +0400 Subject: [PATCH 2/3] remove etalon --- ...rolling-dragging-row (fluent.blue.light).png | Bin 10266 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 e2e/testcafe-devextreme/tests/dataGrid/common/etalons/T1179218-virtual-scrolling-dragging-row (fluent.blue.light).png 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 aa6627a080bf96b969f1a76537a093a3049a398a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 10266 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 From 3d4dc43dc79cc1cc1dd4fe1ef5ea019515122834 Mon Sep 17 00:00:00 2001 From: Alyar <> Date: Fri, 30 Jan 2026 15:59:15 +0400 Subject: [PATCH 3/3] unskip test --- .../tests/dataGrid/common/rowDragging/functional.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/e2e/testcafe-devextreme/tests/dataGrid/common/rowDragging/functional.ts b/e2e/testcafe-devextreme/tests/dataGrid/common/rowDragging/functional.ts index 3f043a1de0a7..04d9c638a701 100644 --- a/e2e/testcafe-devextreme/tests/dataGrid/common/rowDragging/functional.ts +++ b/e2e/testcafe-devextreme/tests/dataGrid/common/rowDragging/functional.ts @@ -569,7 +569,7 @@ 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 t.expect(dataGrid.isReady()).ok();