From f990e7344aabb74c7643393894cc901be0d92e8f Mon Sep 17 00:00:00 2001 From: EugeniyKiyashko Date: Thu, 11 Jun 2026 11:18:45 +0400 Subject: [PATCH 1/7] CI: test stability of tests --- apps/demos/utils/visual-tests/testcafe-runner.ts | 2 +- e2e/testcafe-devextreme/runner.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/demos/utils/visual-tests/testcafe-runner.ts b/apps/demos/utils/visual-tests/testcafe-runner.ts index 871fe7237300..c331bbb5f52a 100644 --- a/apps/demos/utils/visual-tests/testcafe-runner.ts +++ b/apps/demos/utils/visual-tests/testcafe-runner.ts @@ -1,7 +1,7 @@ import createTestCafe, { ClientFunction } from 'testcafe'; import fs from 'fs'; -const LAUNCH_RETRY_ATTEMPTS = 3; +const LAUNCH_RETRY_ATTEMPTS = 0; const LAUNCH_RETRY_TIMEOUT = 10000; const wait = async ( diff --git a/e2e/testcafe-devextreme/runner.ts b/e2e/testcafe-devextreme/runner.ts index ee2616dc2f39..57728a8b1fef 100644 --- a/e2e/testcafe-devextreme/runner.ts +++ b/e2e/testcafe-devextreme/runner.ts @@ -14,7 +14,7 @@ import { getCurrentTheme } from './helpers/themeUtils'; const LAUNCH_RETRY_ATTEMPTS = 3; const LAUNCH_RETRY_TIMEOUT = 10000; -const FAILED_TESTS_RETRY_ATTEMPTS = 2; +const FAILED_TESTS_RETRY_ATTEMPTS = 0; const wait = async ( timeout: number, From 95d1707ba84c02743617166a30150fe6c7680c58 Mon Sep 17 00:00:00 2001 From: EugeniyKiyashko Date: Fri, 12 Jun 2026 15:01:30 +0400 Subject: [PATCH 2/7] fix tests --- .../workflows/run-testcafe-on-gh-pages.yml | 2 +- .github/workflows/visual-tests-demos.yml | 6 ++--- .../utils/visual-tests/testcafe-runner.ts | 2 +- e2e/testcafe-devextreme/helpers/domUtils.ts | 10 +++---- e2e/testcafe-devextreme/runner.ts | 2 +- .../cardView/columnSortable/functional.ts | 8 +----- .../tests/cardView/columnSortable/utils.ts | 14 +++++----- .../dataGrid/common/editing/functional.ts | 4 ++- .../dataGrid/common/filtering/functional.ts | 5 ++-- .../focus/focusEvents/newRows_T1162227.ts | 21 ++++++--------- .../common/focus/focusedRow/markup.ts | 1 + .../keyboardNavigation.visual.ts | 7 +++-- .../markup/T838734_alternateRowSizes.ts | 1 + .../dataGrid/common/rowDragging/functional.ts | 26 +++++++++++++------ .../tests/dataGrid/common/scrolling.ts | 20 ++++++++++++++ .../common/stateStoring/stateStoring.ts | 2 +- .../common/validation/validationPopup.ts | 5 ++++ .../tests/editors/dateRangeBox/focus.ts | 6 ++++- packages/devextreme/docker-ci.sh | 2 +- .../appointment.editing.tests.js | 1 + 20 files changed, 90 insertions(+), 55 deletions(-) diff --git a/.github/workflows/run-testcafe-on-gh-pages.yml b/.github/workflows/run-testcafe-on-gh-pages.yml index 4f907d3ad8e9..73fe5f232bbc 100644 --- a/.github/workflows/run-testcafe-on-gh-pages.yml +++ b/.github/workflows/run-testcafe-on-gh-pages.yml @@ -78,7 +78,7 @@ jobs: working-directory: devextreme/apps/demos env: CHANGEDFILEINFOSPATH: changed-files.json - BROWSERS: chrome:headless --window-size=1200,800 --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 + BROWSERS: chrome:headless --window-size=1200,800 --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,KeyboardFocusableScrollers --js-flags=--random-seed=2147483647 --font-render-hinting=none --disable-font-subpixel-positioning #DEBUG: hammerhead:*,testcafe:* CONCURRENCY: 4 TCQUARANTINE: true diff --git a/.github/workflows/visual-tests-demos.yml b/.github/workflows/visual-tests-demos.yml index 10f7185ab952..58951533a79b 100644 --- a/.github/workflows/visual-tests-demos.yml +++ b/.github/workflows/visual-tests-demos.yml @@ -717,7 +717,7 @@ jobs: - name: Set Chrome flags id: chrome-flags run: | - BASE_FLAGS="chrome:headless --window-size=1200,800 --disable-gpu --no-sandbox --disable-dev-shm-usage --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" + BASE_FLAGS="chrome:headless --window-size=1200,800 --disable-gpu --no-sandbox --disable-dev-shm-usage --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,KeyboardFocusableScrollers --js-flags=--random-seed=2147483647" # For Material theme, enable better font rendering to avoid instability if [[ "${{ matrix.THEME }}" != *"material"* ]]; then @@ -902,7 +902,7 @@ jobs: working-directory: apps/demos env: NODE_OPTIONS: --max-old-space-size=8192 - BROWSERS: chrome:headless --window-size=1200,800 --disable-gpu --no-sandbox --disable-dev-shm-usage --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 + BROWSERS: chrome:headless --window-size=1200,800 --disable-gpu --no-sandbox --disable-dev-shm-usage --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,KeyboardFocusableScrollers --js-flags=--random-seed=2147483647 --font-render-hinting=none --disable-font-subpixel-positioning # DEBUG: hammerhead:*,testcafe:* CONCURRENCY: ${{ steps.set-concurrency.outputs.concurrency }} TCQUARANTINE: true @@ -1029,7 +1029,7 @@ jobs: working-directory: apps/demos env: CHANGEDFILEINFOSPATH: changed-files.json - BROWSERS: chrome:headless --window-size=1200,800 --disable-gpu --no-sandbox --disable-dev-shm-usage --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 + BROWSERS: chrome:headless --window-size=1200,800 --disable-gpu --no-sandbox --disable-dev-shm-usage --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,KeyboardFocusableScrollers --js-flags=--random-seed=2147483647 --font-render-hinting=none --disable-font-subpixel-positioning # DEBUG: hammerhead:*,testcafe:* CONCURRENCY: 1 TCQUARANTINE: true diff --git a/apps/demos/utils/visual-tests/testcafe-runner.ts b/apps/demos/utils/visual-tests/testcafe-runner.ts index c331bbb5f52a..3215b3485d70 100644 --- a/apps/demos/utils/visual-tests/testcafe-runner.ts +++ b/apps/demos/utils/visual-tests/testcafe-runner.ts @@ -96,7 +96,7 @@ async function main() { const failedCount = await retry(() => runner .reporter(reporters) - .browsers(process.env.BROWSERS || 'chrome --no-sandbox --disable-dev-shm-usage --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') + .browsers(process.env.BROWSERS || 'chrome --no-sandbox --disable-dev-shm-usage --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,KeyboardFocusableScrollers --js-flags=--random-seed=2147483647 --font-render-hinting=none --disable-font-subpixel-positioning') .concurrency(concurrency || 1) .run({ quarantineMode: getQuarantineMode(), diff --git a/e2e/testcafe-devextreme/helpers/domUtils.ts b/e2e/testcafe-devextreme/helpers/domUtils.ts index 085e8c0b183e..9914b9ed0b85 100644 --- a/e2e/testcafe-devextreme/helpers/domUtils.ts +++ b/e2e/testcafe-devextreme/helpers/domUtils.ts @@ -137,8 +137,8 @@ export const addFocusableElementBefore = ClientFunction(( return button.id; }); -export const hasHorizontalScroll = async (container: Selector): Promise => { - const scrollWidth = await container.scrollWidth; - const clientWidth = await container.clientWidth; - return scrollWidth > clientWidth; -}; +export const hasHorizontalScroll = ClientFunction((containerSelector) => { + const container = containerSelector(); + + return container.scrollWidth > container.clientWidth; +}); diff --git a/e2e/testcafe-devextreme/runner.ts b/e2e/testcafe-devextreme/runner.ts index 57728a8b1fef..560ba3201f1e 100644 --- a/e2e/testcafe-devextreme/runner.ts +++ b/e2e/testcafe-devextreme/runner.ts @@ -89,7 +89,7 @@ function setShadowDom(args: ParsedArgs): void { function expandBrowserAlias(browser: string): string { switch (browser) { case 'chrome:devextreme-shr2': - return 'chrome:headless --no-sandbox --disable-dev-shm-usage --disable-gpu --window-size=1200,800 --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'; + return 'chrome:headless --no-sandbox --disable-dev-shm-usage --disable-gpu --window-size=1200,800 --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,KeyboardFocusableScrollers --js-flags=--random-seed=2147483647 --font-render-hinting=none --disable-font-subpixel-positioning'; case 'chrome:docker': return 'chromium:headless --no-sandbox --disable-gpu --window-size=1200,800'; default: diff --git a/e2e/testcafe-devextreme/tests/cardView/columnSortable/functional.ts b/e2e/testcafe-devextreme/tests/cardView/columnSortable/functional.ts index bcdd5b04266f..9bd0f9e91926 100644 --- a/e2e/testcafe-devextreme/tests/cardView/columnSortable/functional.ts +++ b/e2e/testcafe-devextreme/tests/cardView/columnSortable/functional.ts @@ -315,15 +315,9 @@ test('cards should update when columns are reordered (T1324855)', async (t) => { const headerPanel = cardView.getHeaderPanel(); const firstHeader = headerPanel.getHeaderItem(0).element; - const secondHeader = headerPanel.getHeaderItem(1).element; - await t.dragToElement(firstHeader, secondHeader, { - destinationOffsetX: -5, - destinationOffsetY: -20, - speed: 0.5, - }); + await dragToHeaderPanel(t, cardView, firstHeader, 2); - // Wait for headers to update after drag await t.expect(cardView.getHeaders().getHeaderItemNth(0).element.innerText).notEql('A'); const headerCaptions: string[] = []; diff --git a/e2e/testcafe-devextreme/tests/cardView/columnSortable/utils.ts b/e2e/testcafe-devextreme/tests/cardView/columnSortable/utils.ts index 9a3f5bd57824..5ddc331e7094 100644 --- a/e2e/testcafe-devextreme/tests/cardView/columnSortable/utils.ts +++ b/e2e/testcafe-devextreme/tests/cardView/columnSortable/utils.ts @@ -124,7 +124,7 @@ export const expectColumns = async ( expectedColumns: number[], source: 'headerPanel' | 'columnChooser' = 'headerPanel', ): Promise => { - const actualColumns: string[] = []; + const adjustedExpectedColumns = expectedColumns.map((columnIndex) => `Column ${columnIndex}`); for (let i = 0; i < expectedColumns.length; i += 1) { // eslint-disable-next-line @typescript-eslint/init-declarations @@ -137,12 +137,10 @@ export const expectColumns = async ( column = treeView.getNodeItem(i); } - if (await column?.exists) { - actualColumns.push(await column.innerText); - } + await t + .expect(column.exists) + .ok() + .expect(column.innerText) + .eql(adjustedExpectedColumns[i]); } - - const adjustedExpectedColumns = expectedColumns.map((columnIndex) => `Column ${columnIndex}`); - - await t.expect(actualColumns).eql(adjustedExpectedColumns); }; diff --git a/e2e/testcafe-devextreme/tests/dataGrid/common/editing/functional.ts b/e2e/testcafe-devextreme/tests/dataGrid/common/editing/functional.ts index a8515054c98b..83f9216dab92 100644 --- a/e2e/testcafe-devextreme/tests/dataGrid/common/editing/functional.ts +++ b/e2e/testcafe-devextreme/tests/dataGrid/common/editing/functional.ts @@ -66,7 +66,9 @@ test('DataGrid - The "Cannot read properties of undefined error" occurs when usi .typeText(dataGrid.getDataCell(0, 0).element, 'new_value') .pressKey('enter tab tab'); await resolveOnSavingDeferred(); - await t.expect(dataGrid.getDataCell(2, 0).isFocused).ok(); + await t + .expect(dataGrid.isReady()).ok() + .expect(dataGrid.getDataCell(2, 0).isFocused).ok(); }).before(async () => { await ClientFunction(() => { (window as any).deferred = $.Deferred(); diff --git a/e2e/testcafe-devextreme/tests/dataGrid/common/filtering/functional.ts b/e2e/testcafe-devextreme/tests/dataGrid/common/filtering/functional.ts index 046b2e869c43..372f56fe2cc5 100644 --- a/e2e/testcafe-devextreme/tests/dataGrid/common/filtering/functional.ts +++ b/e2e/testcafe-devextreme/tests/dataGrid/common/filtering/functional.ts @@ -12,7 +12,6 @@ test('Don\'t calculate additional filter when filtering column list is empty', a // arrange const dataGrid = new DataGrid(GRID_CONTAINER); await t.expect(dataGrid.isReady()).ok(); - const consoleMessages = await t.getBrowserConsoleMessages(); // act await dataGrid.option({ @@ -37,8 +36,10 @@ test('Don\'t calculate additional filter when filtering column list is empty', a }); // assert + const consoleMessages = await t.getBrowserConsoleMessages(); + await t - .expect(consoleMessages.error.every((msg) => !msg.includes('E1047'))) + .expect((consoleMessages?.error ?? []).every((msg) => !msg.includes('E1047'))) .ok(); }).before(async () => createWidget('dxDataGrid', { keyExpr: 'id', diff --git a/e2e/testcafe-devextreme/tests/dataGrid/common/focus/focusEvents/newRows_T1162227.ts b/e2e/testcafe-devextreme/tests/dataGrid/common/focus/focusEvents/newRows_T1162227.ts index 98ec140b822a..2a5fdd2ea7ba 100644 --- a/e2e/testcafe-devextreme/tests/dataGrid/common/focus/focusEvents/newRows_T1162227.ts +++ b/e2e/testcafe-devextreme/tests/dataGrid/common/focus/focusEvents/newRows_T1162227.ts @@ -1,3 +1,4 @@ +import { ClientFunction } from 'testcafe'; import DataGrid from 'devextreme-testcafe-models/dataGrid'; import { createWidget } from '../../../../../helpers/createWidget'; import url from '../../../../../helpers/getPageUrl'; @@ -337,24 +338,18 @@ test('It should fire correct events on page change', async (t) => { test('It should fire row changed event and change page if focusedRowKey on another page', async (t) => { const expectedRowFocusChanged: FocusRowChangedData[] = [[1]]; + const getRowFocusChanged = ClientFunction(() => { + const extendedWindow = window as WindowCallbackExtended; - const dataGrid = new DataGrid(GRID_SELECTOR); - - await t.wait(100); - - const [ - , - , - , - rowFocusChanged, - ] = await collectEventsCallbackResults(); + return extendedWindow.clientTesting!.data.rowFocusChanged; + }); - const cellText = await dataGrid.getDataCell(3, 0).element().innerText; + const dataGrid = new DataGrid(GRID_SELECTOR); await t - .expect(rowFocusChanged) + .expect(getRowFocusChanged()) .eql(expectedRowFocusChanged) - .expect(cellText) + .expect(dataGrid.getDataCell(3, 0).element.innerText) .eql('dataA_3'); }).before(async () => { await initCallbackTesting(); diff --git a/e2e/testcafe-devextreme/tests/dataGrid/common/focus/focusedRow/markup.ts b/e2e/testcafe-devextreme/tests/dataGrid/common/focus/focusedRow/markup.ts index a38ca2a4855c..f05ebac2370e 100644 --- a/e2e/testcafe-devextreme/tests/dataGrid/common/focus/focusedRow/markup.ts +++ b/e2e/testcafe-devextreme/tests/dataGrid/common/focus/focusedRow/markup.ts @@ -72,6 +72,7 @@ test('Invalid cells in a focused row should have the correct background color (T await dataGrid.apiAddRow(); await dataGrid.apiSaveEditData(); // assert + await t.expect(dataGrid.isReady()).ok(); await testScreenshot(t, takeScreenshot, 'focused-row-invalid-cells.png'); await t.expect(compareResults.isValid()) .ok(compareResults.errorMessages()); diff --git a/e2e/testcafe-devextreme/tests/dataGrid/common/keyboardNavigation/keyboardNavigation.visual.ts b/e2e/testcafe-devextreme/tests/dataGrid/common/keyboardNavigation/keyboardNavigation.visual.ts index 29f837bd4ad7..ad54f03d4455 100644 --- a/e2e/testcafe-devextreme/tests/dataGrid/common/keyboardNavigation/keyboardNavigation.visual.ts +++ b/e2e/testcafe-devextreme/tests/dataGrid/common/keyboardNavigation/keyboardNavigation.visual.ts @@ -527,6 +527,7 @@ test('Navigate to last cell in the last row when virtual scrolling is enabled', .pressKey('ctrl+end') .wait(100); + await t.expect(dataGrid.getDataCell(199, 14).element.focused).ok(); await testScreenshot(t, takeScreenshot, 'navigate_to_last_cell_in_last_row_when_virtual_scrolling_is_enabled.png', { element: dataGrid.element }); // assert @@ -570,6 +571,7 @@ test('Navigate to first cell in the first row when virtual scrolling is enabled' .pressKey('ctrl+home') .wait(1000); + await t.expect(dataGrid.getDataCell(0, 0).element.focused).ok(); await testScreenshot(t, takeScreenshot, 'navigate_to_first_cell_in_first_row_when_virtual_scrolling_is_enabled_2.png', { element: dataGrid.element }); // assert @@ -687,7 +689,7 @@ test('Navigate to first cell in the first row when virtual scrolling and columns // assert await t - .expect(dataGrid.getDataCell(199, 35).element.focused) + .expect(dataGrid.getDataCell(199, 35).isFocused) .ok(); // act @@ -700,7 +702,7 @@ test('Navigate to first cell in the first row when virtual scrolling and columns // assert await t - .expect(dataGrid.getDataCell(0, 1).element.focused) + .expect(dataGrid.getDataCell(0, 1).isFocused) .ok() .expect(compareResults.isValid()) .ok(compareResults.errorMessages()); @@ -734,6 +736,7 @@ test('Navigate to first cell in the first row when virtual scrolling and columns .pressKey('ctrl+end') .wait(1000); + await t.expect(dataGrid.getDataCell(199, 35).isFocused).ok(); await testScreenshot(t, takeScreenshot, `${useNative ? 'native' : 'simulated'}_scrolling_-_navigate_to_last_cell_row_dragging__virtual_scrolling__virtual_columns.png`, { element: dataGrid.element }); // assert diff --git a/e2e/testcafe-devextreme/tests/dataGrid/common/markup/T838734_alternateRowSizes.ts b/e2e/testcafe-devextreme/tests/dataGrid/common/markup/T838734_alternateRowSizes.ts index c8a7ee1f8a9d..387b543598fc 100644 --- a/e2e/testcafe-devextreme/tests/dataGrid/common/markup/T838734_alternateRowSizes.ts +++ b/e2e/testcafe-devextreme/tests/dataGrid/common/markup/T838734_alternateRowSizes.ts @@ -19,6 +19,7 @@ test('Alternate rows should be the same size', async (t) => { const { takeScreenshot, compareResults } = createScreenshotsComparer(t); const dataGrid = new DataGrid(GRID_SELECTOR); + await t.expect(dataGrid.isReady()).ok(); await testScreenshot(t, takeScreenshot, 'T838734_alternate-rows-same-size.png', { element: dataGrid.element }); await t.expect(compareResults.isValid()) diff --git a/e2e/testcafe-devextreme/tests/dataGrid/common/rowDragging/functional.ts b/e2e/testcafe-devextreme/tests/dataGrid/common/rowDragging/functional.ts index d79c25f470ac..f83505a0d3b1 100644 --- a/e2e/testcafe-devextreme/tests/dataGrid/common/rowDragging/functional.ts +++ b/e2e/testcafe-devextreme/tests/dataGrid/common/rowDragging/functional.ts @@ -798,17 +798,27 @@ test('Item should appear in a correct spot when dragging to a different page wit const dataGrid = new DataGrid('#container'); await t.expect(dataGrid.isReady()).ok(); - await t.drag(dataGrid.getDataRow(2).getDragCommand(), 0, 32, { speed: 0.95 }); + const scrollOffsetForAutoScroll = await getOffsetToTriggerAutoScroll(2, 1, 'down'); - const visibleRows = await dataGrid.apiGetVisibleRows(); - const visibleRowKeys = visibleRows.map((row) => row.key); - const expectedSequence = ['5-1', '3-1', '6-1']; + await t.drag(dataGrid.getDataRow(2).getDragCommand(), 0, scrollOffsetForAutoScroll, { speed: 0.8 }); - const startIndex = visibleRowKeys.findIndex( - (_, i) => expectedSequence.every((val, j) => visibleRowKeys[i + j] === val), - ); + const expectedSequence = ['5-1', '3-1', '6-1']; + const containsExpectedSequence = ClientFunction(() => { + const visibleRowKeys = ($('#container') as any) + .dxDataGrid('instance') + .getVisibleRows() + .map((row: any) => row.key); + + return visibleRowKeys.some( + (_: string, i: number) => expectedSequence.every( + (val: string, j: number) => visibleRowKeys[i + j] === val, + ), + ); + }, { dependencies: { expectedSequence } }); - await t.expect(startIndex).gte(0); + await t + .expect(dataGrid.isReady()).ok() + .expect(containsExpectedSequence()).ok(); }).before(async () => { const items = generateData(20, 1); return createWidget('dxDataGrid', { diff --git a/e2e/testcafe-devextreme/tests/dataGrid/common/scrolling.ts b/e2e/testcafe-devextreme/tests/dataGrid/common/scrolling.ts index 8dae14cdbc05..416a039dafbc 100644 --- a/e2e/testcafe-devextreme/tests/dataGrid/common/scrolling.ts +++ b/e2e/testcafe-devextreme/tests/dataGrid/common/scrolling.ts @@ -1097,6 +1097,26 @@ test.meta({ browserSize: [800, 800] })('The scroll position of a fixed table sho .drag(scrollbarVerticalThumbTrack, 0, 600) .wait(1000); + const isTargetRowSynchronized = ClientFunction(() => { + const rows = Array + .from(document.querySelectorAll('tr[aria-rowindex="999"]')) + .filter((row) => { + const { width, height } = row.getBoundingClientRect(); + + return width > 0 && height > 0; + }); + + if (!rows.length) { + return false; + } + + const tops = rows.map((row) => row.getBoundingClientRect().top); + + return Math.max(...tops) - Math.min(...tops) < 1; + }); + + await t.expect(isTargetRowSynchronized()).ok(); + await testScreenshot(t, takeScreenshot, 'grid-virtual-scrolling_with_fixed_columns-T1166649.png', { element: 'tr[aria-rowindex="999"]' }); await t .expect(compareResults.isValid()) diff --git a/e2e/testcafe-devextreme/tests/dataGrid/common/stateStoring/stateStoring.ts b/e2e/testcafe-devextreme/tests/dataGrid/common/stateStoring/stateStoring.ts index a5ebf6db4d12..c7f4b9bcaa7a 100644 --- a/e2e/testcafe-devextreme/tests/dataGrid/common/stateStoring/stateStoring.ts +++ b/e2e/testcafe-devextreme/tests/dataGrid/common/stateStoring/stateStoring.ts @@ -136,7 +136,7 @@ test('The focused state of a row with the 0 key should be restored (T1252962)', })); test('DataGrid - Cannot read properties of undefined (reading \'done\') error occurs when column fixing and state storing are used (T1283168)', async (t) => { - await t.eval(() => location.reload()); + await t.navigateTo(url(__dirname, '../../../container.html')); await createWidget('dxDataGrid', { ...dataGridConfig }); // eslint-disable-next-line @stylistic/max-len // DataGrid is expected to load normally with the given configuration, so no other checks are required. diff --git a/e2e/testcafe-devextreme/tests/dataGrid/common/validation/validationPopup.ts b/e2e/testcafe-devextreme/tests/dataGrid/common/validation/validationPopup.ts index c9877d5bf986..fa50c278bdb9 100644 --- a/e2e/testcafe-devextreme/tests/dataGrid/common/validation/validationPopup.ts +++ b/e2e/testcafe-devextreme/tests/dataGrid/common/validation/validationPopup.ts @@ -102,6 +102,11 @@ test('Validation popup with open master detail and fixed columns', async (t) => .click(dataGrid.getDataCell(5, 2).element) .pressKey('ctrl+a backspace enter'); + await t.expect(dataGrid.getRevertTooltip().exists) + .ok() + .expect(dataGrid.getInvalidMessageTooltip().exists) + .ok(); + // act await testScreenshot(t, takeScreenshot, 'validation-popup_master-detail_fixed-column.png', { element: dataGrid.element }); await dataGrid.scrollTo(t, { y: 150 }); diff --git a/e2e/testcafe-devextreme/tests/editors/dateRangeBox/focus.ts b/e2e/testcafe-devextreme/tests/editors/dateRangeBox/focus.ts index 24232c27e6af..a273384a8b5f 100644 --- a/e2e/testcafe-devextreme/tests/editors/dateRangeBox/focus.ts +++ b/e2e/testcafe-devextreme/tests/editors/dateRangeBox/focus.ts @@ -2,6 +2,7 @@ import { ClientFunction, Selector } from 'testcafe'; import DateRangeBox from 'devextreme-testcafe-models/dateRangeBox'; import url from '../../../helpers/getPageUrl'; import { createWidget } from '../../../helpers/createWidget'; +import { addFocusableElementBefore } from '../../../helpers/domUtils'; fixture.disablePageReloads`DateRangeBox focus state` .page(url(__dirname, '../../container.html')); @@ -260,7 +261,7 @@ test('onFocusIn should be called only on focus of startDate input', async (t) => (window as any).onFocusOutCounter = 0; })(); - return createWidget('dxDateRangeBox', { + await createWidget('dxDateRangeBox', { value: [new Date('2021/09/17'), new Date('2021/10/24')], openOnFieldClick: true, width: 500, @@ -271,8 +272,11 @@ test('onFocusIn should be called only on focus of startDate input', async (t) => ((window as any).onFocusOutCounter as number) += 1; }, }); + + await addFocusableElementBefore('#container'); }).after(async () => { await ClientFunction(() => { + document.getElementById('focusable-start')?.remove(); delete (window as any).onFocusInCounter; delete (window as any).onFocusOutCounter; })(); diff --git a/packages/devextreme/docker-ci.sh b/packages/devextreme/docker-ci.sh index 37a7cec57486..0ea63c0cc48f 100755 --- a/packages/devextreme/docker-ci.sh +++ b/packages/devextreme/docker-ci.sh @@ -28,7 +28,7 @@ function run_test { local i local status - for i in {1..3}; do + for i in {1..1}; do set +e (set -e; run_test_impl); status=$? set -e diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets.scheduler/appointment.editing.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets.scheduler/appointment.editing.tests.js index 26f0e400d2fa..cf9c2d5d3b5e 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets.scheduler/appointment.editing.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets.scheduler/appointment.editing.tests.js @@ -304,6 +304,7 @@ module('Integration: Appointment editing', { await waitAsync(30); assert.ok(scrollTo.calledOnce, 'scrollTo was called'); + await waitAsync(30); } finally { workSpace.scrollTo.restore(); } From 5d10b0ce7a2d27cd155010818f84f5b0d4762cf9 Mon Sep 17 00:00:00 2001 From: EugeniyKiyashko Date: Fri, 12 Jun 2026 16:34:33 +0400 Subject: [PATCH 3/7] fix qunit tests --- .../accessibility/dataGrid/fixedColumns.ts | 9 +- .../cardView/columnChooser/functional.ts | 62 +++++-- .../cardView/columnSortable/functional.ts | 4 +- .../tests/cardView/columnSortable/utils.ts | 27 +++- .../tests/cardView/helpers/cardUtils.ts | 10 +- ...g__virtual_columns (fluent.blue.light).png | Bin 63147 -> 63128 bytes .../keyboardNavigation.visual.ts | 114 ++++++++----- .../dataGrid/common/rowDragging/functional.ts | 62 ++++--- .../tests/dataGrid/common/scrolling.ts | 34 +++- .../tests/editors/dateRangeBox/focus.ts | 38 ++++- .../tests/editors/dateRangeBox/keyboard.ts | 48 ++++-- .../tests/editors/selectBox/actionButton.ts | 31 +++- .../scheduler/common/dragAndDrop/T1118059.ts | 10 +- .../js/__internal/scheduler/m_scheduler.ts | 4 +- .../appointment.editing.tests.js | 4 +- .../appointment.monthView.tests.js | 11 +- .../chatParts/chat.tests.js | 153 ++++++++++++------ .../chatParts/messageList.tests.js | 109 ++++++------- .../listParts/commonTests.js | 24 ++- 19 files changed, 501 insertions(+), 253 deletions(-) diff --git a/e2e/testcafe-devextreme/tests/accessibility/dataGrid/fixedColumns.ts b/e2e/testcafe-devextreme/tests/accessibility/dataGrid/fixedColumns.ts index f059193b4961..48e1389f489e 100644 --- a/e2e/testcafe-devextreme/tests/accessibility/dataGrid/fixedColumns.ts +++ b/e2e/testcafe-devextreme/tests/accessibility/dataGrid/fixedColumns.ts @@ -160,15 +160,12 @@ test('Accessibility: Scrollable should have focusable element when navigate out test('Accessibility: Scrollable should have focusable when fixed on the right side columns are focused', async (t) => { const dataGrid = new DataGrid('#container'); + const targetCell = dataGrid.getFixedDataCell(0, COLUMNS_LENGTH - 1); - // focus through headers - await pressKey(t, 'tab', COLUMNS_LENGTH); - - // focus through data row till last cell (which is fixed) - await pressKey(t, 'tab', COLUMNS_LENGTH); + await t.click(targetCell.element); await t - .expect(dataGrid.getFixedDataCell(0, COLUMNS_LENGTH - 1).isFocused) + .expect(targetCell.element.focused) .ok(); await a11yCheck(t); diff --git a/e2e/testcafe-devextreme/tests/cardView/columnChooser/functional.ts b/e2e/testcafe-devextreme/tests/cardView/columnChooser/functional.ts index e28773ae4dd4..308b67c31bac 100644 --- a/e2e/testcafe-devextreme/tests/cardView/columnChooser/functional.ts +++ b/e2e/testcafe-devextreme/tests/cardView/columnChooser/functional.ts @@ -1,11 +1,53 @@ +import { Selector } from 'testcafe'; import CardView from 'devextreme-testcafe-models/cardView'; import url from '../../../helpers/getPageUrl'; import { createWidget } from '../../../helpers/createWidget'; import { getCardFieldCaptions } from '../helpers/cardUtils'; +const COLUMN_CHOOSER_DND_TIMEOUT = 3000; +const SORTABLE_DRAGGING_SELECTOR = '.dx-sortable-dragging'; + fixture`CardView - ColumnChooser.Functional` .page(url(__dirname, '../../container.html')); +const waitForDragEnd = async (t: TestController, cardView: CardView): Promise => { + await t + .expect(Selector(SORTABLE_DRAGGING_SELECTOR).exists) + .notOk({ timeout: COLUMN_CHOOSER_DND_TIMEOUT }) + .expect(cardView.isReady()) + .ok({ timeout: COLUMN_CHOOSER_DND_TIMEOUT }); +}; + +const dragHeaderColumnToColumnChooser = async ( + t: TestController, + cardView: CardView, + columnIndex: number, +): Promise => { + await t.dragToElement( + cardView.getHeaderPanel().getHeaderItem(columnIndex).element, + cardView.getColumnChooser().content, + { + speed: 0.5, + }, + ); + await waitForDragEnd(t, cardView); +}; + +const dragColumnChooserColumnToHeaderPanel = async ( + t: TestController, + cardView: CardView, + columnIndex: number, +): Promise => { + await t.dragToElement( + cardView.getColumnChooser().getColumn(columnIndex), + cardView.getHeaderPanel().element, + { + speed: 0.5, + }, + ); + await waitForDragEnd(t, cardView); +}; + function testsFactory(testModel: { name: string; hideFirstColumn: (t: TestController, cardView: CardView) => Promise; @@ -99,16 +141,10 @@ testsFactory({ }, }, async hideFirstColumn(t: TestController, cardView: CardView) { - await t.dragToElement( - cardView.getHeaderPanel().getHeaderItem(0).element, - cardView.getColumnChooser().content, - ); + await dragHeaderColumnToColumnChooser(t, cardView, 0); }, async showFirstColumn(t: TestController, cardView: CardView) { - await t.dragToElement( - cardView.getColumnChooser().getColumn(0), - cardView.getHeaderPanel().element, - ); + await dragColumnChooserColumnToHeaderPanel(t, cardView, 0); }, async assertFirstColumnVisible(t: TestController, cardView: CardView) { await t.expect( @@ -222,18 +258,12 @@ test('cards should update when column is hidden via column chooser (dragAndDrop await cardView.apiShowColumnChooser(); - await t.dragToElement( - cardView.getHeaderPanel().getHeaderItem(0).element, - cardView.getColumnChooser().content, - ); + await dragHeaderColumnToColumnChooser(t, cardView, 0); const captionsAfterHide = await getCardFieldCaptions(t, cardView, 2); await t.expect(captionsAfterHide).eql(['B', 'C']); - await t.dragToElement( - cardView.getColumnChooser().getColumn(0), - cardView.getHeaderPanel().element, - ); + await dragColumnChooserColumnToHeaderPanel(t, cardView, 0); const captionsAfterShow = await getCardFieldCaptions(t, cardView, 3); await t.expect(captionsAfterShow).eql(['A', 'B', 'C']); diff --git a/e2e/testcafe-devextreme/tests/cardView/columnSortable/functional.ts b/e2e/testcafe-devextreme/tests/cardView/columnSortable/functional.ts index 9bd0f9e91926..d077fb1d4add 100644 --- a/e2e/testcafe-devextreme/tests/cardView/columnSortable/functional.ts +++ b/e2e/testcafe-devextreme/tests/cardView/columnSortable/functional.ts @@ -318,7 +318,9 @@ test('cards should update when columns are reordered (T1324855)', async (t) => { await dragToHeaderPanel(t, cardView, firstHeader, 2); - await t.expect(cardView.getHeaders().getHeaderItemNth(0).element.innerText).notEql('A'); + await t + .expect(cardView.getHeaders().getHeaderItemNth(0).element.innerText) + .notEql('A', { timeout: 3000 }); const headerCaptions: string[] = []; const headersCount = await cardView.getHeaders().getHeaderItemsElements().count; diff --git a/e2e/testcafe-devextreme/tests/cardView/columnSortable/utils.ts b/e2e/testcafe-devextreme/tests/cardView/columnSortable/utils.ts index 5ddc331e7094..143d093a1c8f 100644 --- a/e2e/testcafe-devextreme/tests/cardView/columnSortable/utils.ts +++ b/e2e/testcafe-devextreme/tests/cardView/columnSortable/utils.ts @@ -1,7 +1,10 @@ -import { ClientFunction } from 'testcafe'; +import { ClientFunction, Selector } from 'testcafe'; import CardView from 'devextreme-testcafe-models/cardView'; import TreeView from 'devextreme-testcafe-models/treeView'; +const DRAG_ASSERTION_TIMEOUT = 3000; +const HEADER_DROP_OFFSET_Y = 5; + export const SELECTORS = { dragging: '.dx-sortable-dragging', treeView: '.dx-cardview-column-chooser .dx-treeview', @@ -80,7 +83,11 @@ export const dragToHeaderPanel = async ( await t.dragToElement( columnElement, insertBeforeColumn, - { destinationOffsetX: +5, destinationOffsetY: -20, speed: 0.5 }, + { + destinationOffsetX: 5, + destinationOffsetY: HEADER_DROP_OFFSET_Y, + speed: 0.5, + }, ); } else { const insertAfterColumn = headers.getHeaderItemNth(columnsNum - 1).element; @@ -88,11 +95,19 @@ export const dragToHeaderPanel = async ( await t.dragToElement( columnElement, insertAfterColumn, - { destinationOffsetX: -5, destinationOffsetY: -20, speed: 0.5 }, + { + destinationOffsetX: -5, + destinationOffsetY: HEADER_DROP_OFFSET_Y, + speed: 0.5, + }, ); } - await t.wait(300); + await t + .expect(Selector(SELECTORS.dragging).exists) + .notOk({ timeout: DRAG_ASSERTION_TIMEOUT }) + .expect(cardView.isReady()) + .ok({ timeout: DRAG_ASSERTION_TIMEOUT }); }; export const dragToColumnChooser = async ( @@ -139,8 +154,8 @@ export const expectColumns = async ( await t .expect(column.exists) - .ok() + .ok({ timeout: DRAG_ASSERTION_TIMEOUT }) .expect(column.innerText) - .eql(adjustedExpectedColumns[i]); + .eql(adjustedExpectedColumns[i], { timeout: DRAG_ASSERTION_TIMEOUT }); } }; diff --git a/e2e/testcafe-devextreme/tests/cardView/helpers/cardUtils.ts b/e2e/testcafe-devextreme/tests/cardView/helpers/cardUtils.ts index 58f08e347842..068a709b3a11 100644 --- a/e2e/testcafe-devextreme/tests/cardView/helpers/cardUtils.ts +++ b/e2e/testcafe-devextreme/tests/cardView/helpers/cardUtils.ts @@ -1,5 +1,8 @@ import CardView from 'devextreme-testcafe-models/cardView'; +const FIELD_CAPTION_SELECTOR = '.dx-cardview-field-caption'; +const CARD_FIELD_CAPTION_TIMEOUT = 3000; + const getCardFieldCaptions = async ( t: TestController, cardView: CardView, @@ -7,9 +10,12 @@ const getCardFieldCaptions = async ( cardIndex = 0, ): Promise => { const card = cardView.getCard(cardIndex); - const captions = await card.getCaptions(); - await t.expect(captions.length).eql(expectedCount); + await t + .expect(card.element.find(FIELD_CAPTION_SELECTOR).count) + .eql(expectedCount, { timeout: CARD_FIELD_CAPTION_TIMEOUT }); + + const captions = await card.getCaptions(); return captions; }; diff --git a/e2e/testcafe-devextreme/tests/dataGrid/common/keyboardNavigation/etalons/simulated_scrolling_-_navigate_to_first_cell_row_dragging__virtual_scrolling__virtual_columns (fluent.blue.light).png b/e2e/testcafe-devextreme/tests/dataGrid/common/keyboardNavigation/etalons/simulated_scrolling_-_navigate_to_first_cell_row_dragging__virtual_scrolling__virtual_columns (fluent.blue.light).png index 9671329a19fa3fb0251033e4aa85449d2a347b2e..1748e457233321583d3de23ceca552d5c75d5b52 100644 GIT binary patch delta 30662 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

HR~?o?hcCo<;gq$GNHNi`C5j=)1Gv(l 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 delta 30770 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 k^MmBj0yy2LqF8Q2dyAFlVV!Z diff --git a/e2e/testcafe-devextreme/tests/dataGrid/common/keyboardNavigation/keyboardNavigation.visual.ts b/e2e/testcafe-devextreme/tests/dataGrid/common/keyboardNavigation/keyboardNavigation.visual.ts index ad54f03d4455..a778d79d9f9e 100644 --- a/e2e/testcafe-devextreme/tests/dataGrid/common/keyboardNavigation/keyboardNavigation.visual.ts +++ b/e2e/testcafe-devextreme/tests/dataGrid/common/keyboardNavigation/keyboardNavigation.visual.ts @@ -1,5 +1,6 @@ import { createScreenshotsComparer } from 'devextreme-screenshot-comparer'; import DataGrid from 'devextreme-testcafe-models/dataGrid'; +import { ClientFunction } from 'testcafe'; import { createWidget } from '../../../../helpers/createWidget'; import url from '../../../../helpers/getPageUrl'; import { getData } from '../../helpers/generateDataSourceData'; @@ -8,6 +9,47 @@ import { testScreenshot } from '../../../../helpers/themeUtils'; fixture`Keyboard Navigation.Visual` .page(url(__dirname, '../../../container.html')); +const KEYBOARD_NAVIGATION_TIMEOUT = 3000; + +const isKeyboardNavigationInProgress = ClientFunction(() => { + const dataGrid = ($('#container') as any).dxDataGrid('instance'); + + return dataGrid + .getController('keyboardNavigation') + .navigationToCellInProgress(); +}); + +const focusDataCell = async ( + t: TestController, + dataGrid: DataGrid, + rowIndex: number, + columnIndex: number, +): Promise => { + const cell = dataGrid.getDataCell(rowIndex, columnIndex); + + await dataGrid.apiFocus(cell.element); + await t + .expect(cell.element.focused) + .ok({ timeout: KEYBOARD_NAVIGATION_TIMEOUT }); +}; + +const expectDataCellFocusState = async ( + t: TestController, + dataGrid: DataGrid, + rowIndex: number, + columnIndex: number, +): Promise => { + await t + .expect(dataGrid.getDataCell(rowIndex, columnIndex).isFocused) + .ok({ timeout: KEYBOARD_NAVIGATION_TIMEOUT }); +}; + +const waitForKeyboardNavigation = async (t: TestController): Promise => { + await t + .expect(isKeyboardNavigationInProgress()) + .notOk({ timeout: KEYBOARD_NAVIGATION_TIMEOUT }); +}; + // 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 (t) => { // arrange @@ -522,12 +564,11 @@ test('Navigate to last cell in the last row when virtual scrolling is enabled', .ok(); // act - await t - .click(dataGrid.getDataCell(0, 0).element) - .pressKey('ctrl+end') - .wait(100); + await focusDataCell(t, dataGrid, 0, 0); + await t.pressKey('ctrl+end'); + await waitForKeyboardNavigation(t); - await t.expect(dataGrid.getDataCell(199, 14).element.focused).ok(); + await expectDataCellFocusState(t, dataGrid, 199, 14); await testScreenshot(t, takeScreenshot, 'navigate_to_last_cell_in_last_row_when_virtual_scrolling_is_enabled.png', { element: dataGrid.element }); // assert @@ -566,12 +607,11 @@ test('Navigate to first cell in the first row when virtual scrolling is enabled' await testScreenshot(t, takeScreenshot, 'navigate_to_first_cell_in_first_row_when_virtual_scrolling_is_enabled_1.png', { element: dataGrid.element }); // act - await t - .click(dataGrid.getDataCell(199, 14).element) - .pressKey('ctrl+home') - .wait(1000); + await focusDataCell(t, dataGrid, 199, 14); + await t.pressKey('ctrl+home'); + await waitForKeyboardNavigation(t); - await t.expect(dataGrid.getDataCell(0, 0).element.focused).ok(); + await expectDataCellFocusState(t, dataGrid, 0, 0); await testScreenshot(t, takeScreenshot, 'navigate_to_first_cell_in_first_row_when_virtual_scrolling_is_enabled_2.png', { element: dataGrid.element }); // assert @@ -600,15 +640,12 @@ test('Navigate to last cell in the last row when virtual scrolling and columns a .ok(); // act - await t - .click(dataGrid.getDataCell(0, 0).element) - .pressKey('ctrl+end') - .wait(1000); + await focusDataCell(t, dataGrid, 0, 0); + await t.pressKey('ctrl+end'); + await waitForKeyboardNavigation(t); // assert - await t - .expect(dataGrid.getDataCell(199, 34).element.focused) - .ok(); + await expectDataCellFocusState(t, dataGrid, 199, 34); await testScreenshot(t, takeScreenshot, 'navigate_to_last_cell_in_last_row_when_virtual_scrolling_and_columns_are_enabled.png', { element: dataGrid.element }); @@ -645,15 +682,12 @@ test('Navigate to first cell in the first row when virtual scrolling and columns await testScreenshot(t, takeScreenshot, 'navigate_to_first_cell_in_first_row_when_virtual_scrolling_and_columns_are_enabled_1.png', { element: dataGrid.element }); // act - await t - .click(dataGrid.getDataCell(199, 34).element) - .pressKey('ctrl+home') - .wait(300); + await focusDataCell(t, dataGrid, 199, 34); + await t.pressKey('ctrl+home'); + await waitForKeyboardNavigation(t); // assert - await t - .expect(dataGrid.getDataCell(0, 0).element.focused) - .ok(); + await expectDataCellFocusState(t, dataGrid, 0, 0); await testScreenshot(t, takeScreenshot, 'navigate_to_first_cell_in_first_row_when_virtual_scrolling_and_columns_are_enabled_2.png', { element: dataGrid.element }); @@ -682,28 +716,25 @@ test('Navigate to first cell in the first row when virtual scrolling and columns .ok(); // act - await t - .click(dataGrid.getDataCell(0, 1).element) - .pressKey('ctrl+end') - .wait(1000); + await focusDataCell(t, dataGrid, 0, 1); + await t.pressKey('ctrl+end'); + await waitForKeyboardNavigation(t); // assert - await t - .expect(dataGrid.getDataCell(199, 35).isFocused) - .ok(); + await expectDataCellFocusState(t, dataGrid, 199, 35); // act - await t - .click(dataGrid.getDataCell(199, 35).element) - .pressKey('ctrl+home') - .wait(1000); + await focusDataCell(t, dataGrid, 199, 35); + await t.pressKey('ctrl+home'); + await waitForKeyboardNavigation(t); + + // assert + await expectDataCellFocusState(t, dataGrid, 0, 1); await testScreenshot(t, takeScreenshot, `${useNative ? 'native' : 'simulated'}_scrolling_-_navigate_to_first_cell_row_dragging__virtual_scrolling__virtual_columns.png`, { element: dataGrid.element }); // assert await t - .expect(dataGrid.getDataCell(0, 1).isFocused) - .ok() .expect(compareResults.isValid()) .ok(compareResults.errorMessages()); }).before(async () => createWidget('dxDataGrid', { @@ -731,12 +762,11 @@ test('Navigate to first cell in the first row when virtual scrolling and columns .ok(); // act - await t - .click(dataGrid.getDataCell(0, 0).element) - .pressKey('ctrl+end') - .wait(1000); + await focusDataCell(t, dataGrid, 0, 1); + await t.pressKey('ctrl+end'); + await waitForKeyboardNavigation(t); - await t.expect(dataGrid.getDataCell(199, 35).isFocused).ok(); + await expectDataCellFocusState(t, dataGrid, 199, 34); await testScreenshot(t, takeScreenshot, `${useNative ? 'native' : 'simulated'}_scrolling_-_navigate_to_last_cell_row_dragging__virtual_scrolling__virtual_columns.png`, { element: dataGrid.element }); // assert diff --git a/e2e/testcafe-devextreme/tests/dataGrid/common/rowDragging/functional.ts b/e2e/testcafe-devextreme/tests/dataGrid/common/rowDragging/functional.ts index f83505a0d3b1..039c50a4e02b 100644 --- a/e2e/testcafe-devextreme/tests/dataGrid/common/rowDragging/functional.ts +++ b/e2e/testcafe-devextreme/tests/dataGrid/common/rowDragging/functional.ts @@ -1,4 +1,3 @@ -/* eslint-disable @typescript-eslint/no-misused-promises */ import { ClientFunction, Selector } from 'testcafe'; import DataGrid, { CLASS as DataGridClassNames } from 'devextreme-testcafe-models/dataGrid'; import { ClassNames } from 'devextreme-testcafe-models/dataGrid/classNames'; @@ -778,16 +777,17 @@ test('toIndex should not be corrected when source item gets removed from DOM', a rowDragging: { scrollSpeed: 300, allowReordering: true, - onReorder: ClientFunction((e) => { + onReorder(e) { + const dataSource = e.component.option('dataSource') as Record[]; const visibleRows = e.component.getVisibleRows(); // eslint-disable-next-line @stylistic/max-len - const toIndex = items.findIndex((item) => item.field1 === visibleRows[e.toIndex].data.field1); - const fromIndex = items.findIndex((item) => item.field1 === e.itemData.field1); - items.splice(fromIndex, 1); - items.splice(toIndex, 0, e.itemData); + const toIndex = dataSource.findIndex((item) => item.field1 === visibleRows[e.toIndex].data.field1); + const fromIndex = dataSource.findIndex((item) => item.field1 === e.itemData.field1); + dataSource.splice(fromIndex, 1); + dataSource.splice(toIndex, 0, e.itemData); e.component.refresh(); - }, { dependencies: { items } }), + }, }, showBorders: true, }); @@ -798,27 +798,44 @@ test('Item should appear in a correct spot when dragging to a different page wit const dataGrid = new DataGrid('#container'); await t.expect(dataGrid.isReady()).ok(); - const scrollOffsetForAutoScroll = await getOffsetToTriggerAutoScroll(2, 1, 'down'); + const scrollOffsetForAutoScroll = await getOffsetToTriggerAutoScroll(2, 0.5, 'down'); - await t.drag(dataGrid.getDataRow(2).getDragCommand(), 0, scrollOffsetForAutoScroll, { speed: 0.8 }); + await dragWithDisabledMouseUp( + t, + dataGrid.getDataRow(2).getDragCommand(), + { offsetX: 0, offsetY: scrollOffsetForAutoScroll, speed: 0.5 }, + ); const expectedSequence = ['5-1', '3-1', '6-1']; - const containsExpectedSequence = ClientFunction(() => { - const visibleRowKeys = ($('#container') as any) + const isTargetPageRendered = ClientFunction(() => { + const visibleRowKeys = (($('#container') as any) .dxDataGrid('instance') - .getVisibleRows() + .getVisibleRows() as any[]) .map((row: any) => row.key); - return visibleRowKeys.some( + return visibleRowKeys.includes('5-1') && visibleRowKeys.includes('6-1'); + }); + const containsExpectedSequence = ClientFunction(() => { + const dataSourceKeys = (($('#container') as any) + .dxDataGrid('instance') + .option('dataSource') as any[]) + .map((item: any) => item.field1); + + return dataSourceKeys.some( (_: string, i: number) => expectedSequence.every( - (val: string, j: number) => visibleRowKeys[i + j] === val, + (val: string, j: number) => dataSourceKeys[i + j] === val, ), ); }, { dependencies: { expectedSequence } }); await t - .expect(dataGrid.isReady()).ok() - .expect(containsExpectedSequence()).ok(); + .expect(isTargetPageRendered()).ok({ timeout: 3000 }); + + await dataGrid.dropRow(); + + await t + .expect(dataGrid.isReady()).ok({ timeout: 3000 }) + .expect(containsExpectedSequence()).ok({ timeout: 3000 }); }).before(async () => { const items = generateData(20, 1); return createWidget('dxDataGrid', { @@ -834,16 +851,17 @@ test('Item should appear in a correct spot when dragging to a different page wit rowDragging: { scrollSpeed: 300, allowReordering: true, - onReorder: ClientFunction((e) => { + onReorder(e) { + const dataSource = e.component.option('dataSource') as Record[]; const visibleRows = e.component.getVisibleRows(); // eslint-disable-next-line @stylistic/max-len - const toIndex = items.findIndex((item) => item.field1 === visibleRows[e.toIndex].data.field1); - const fromIndex = items.findIndex((item) => item.field1 === e.itemData.field1); - items.splice(fromIndex, 1); - items.splice(toIndex, 0, e.itemData); + const toIndex = dataSource.findIndex((item) => item.field1 === visibleRows[e.toIndex].data.field1); + const fromIndex = dataSource.findIndex((item) => item.field1 === e.itemData.field1); + dataSource.splice(fromIndex, 1); + dataSource.splice(toIndex, 0, e.itemData); e.component.refresh(); - }, { dependencies: { items } }), + }, }, showBorders: true, }); diff --git a/e2e/testcafe-devextreme/tests/dataGrid/common/scrolling.ts b/e2e/testcafe-devextreme/tests/dataGrid/common/scrolling.ts index 416a039dafbc..d750dd4b380d 100644 --- a/e2e/testcafe-devextreme/tests/dataGrid/common/scrolling.ts +++ b/e2e/testcafe-devextreme/tests/dataGrid/common/scrolling.ts @@ -1094,8 +1094,28 @@ test.meta({ browserSize: [800, 800] })('The scroll position of a fixed table sho // act await t .hover(scrollbarVerticalThumbTrack) - .drag(scrollbarVerticalThumbTrack, 0, 600) - .wait(1000); + .drag(scrollbarVerticalThumbTrack, 0, 600); + + await dataGrid.scrollTo(t, { y: 100000 }); + await t.expect(dataGrid.isReady()).ok({ timeout: 3000 }); + + const isRowsViewScrolledToEnd = ClientFunction(() => { + const scrollableContainer = document.querySelector( + '#container .dx-datagrid-rowsview .dx-scrollable-container', + ); + + if (!scrollableContainer) { + return false; + } + + const { + scrollTop, + clientHeight, + scrollHeight, + } = scrollableContainer as HTMLElement; + + return Math.round(scrollTop + clientHeight) >= scrollHeight - 1; + }); const isTargetRowSynchronized = ClientFunction(() => { const rows = Array @@ -1111,11 +1131,17 @@ test.meta({ browserSize: [800, 800] })('The scroll position of a fixed table sho } const tops = rows.map((row) => row.getBoundingClientRect().top); + const text = rows.map((row) => row.textContent).join(' '); - return Math.max(...tops) - Math.min(...tops) < 1; + return text.includes('998') + && text.includes('item 998') + && Math.max(...tops) - Math.min(...tops) < 1; }); - await t.expect(isTargetRowSynchronized()).ok(); + await t + .expect(isRowsViewScrolledToEnd()).ok({ timeout: 3000 }) + .expect(isTargetRowSynchronized()).ok({ timeout: 3000 }) + .wait(100); await testScreenshot(t, takeScreenshot, 'grid-virtual-scrolling_with_fixed_columns-T1166649.png', { element: 'tr[aria-rowindex="999"]' }); await t diff --git a/e2e/testcafe-devextreme/tests/editors/dateRangeBox/focus.ts b/e2e/testcafe-devextreme/tests/editors/dateRangeBox/focus.ts index a273384a8b5f..328e7b940cf4 100644 --- a/e2e/testcafe-devextreme/tests/editors/dateRangeBox/focus.ts +++ b/e2e/testcafe-devextreme/tests/editors/dateRangeBox/focus.ts @@ -2,13 +2,21 @@ import { ClientFunction, Selector } from 'testcafe'; import DateRangeBox from 'devextreme-testcafe-models/dateRangeBox'; import url from '../../../helpers/getPageUrl'; import { createWidget } from '../../../helpers/createWidget'; -import { addFocusableElementBefore } from '../../../helpers/domUtils'; +import { addFocusableElementBefore, appendElementTo } from '../../../helpers/domUtils'; fixture.disablePageReloads`DateRangeBox focus state` .page(url(__dirname, '../../container.html')); +const FOCUSABLE_END_ID = 'focusable-end'; +const FOCUSABLE_END_SELECTOR = `#${FOCUSABLE_END_ID}`; + +const removeElementById = ClientFunction((elementId: string): void => { + document.getElementById(elementId)?.remove(); +}); + test('DateRangeBox & DateBoxes should have focus class if inputs are focused by tab', async (t) => { const dateRangeBox = new DateRangeBox('#container'); + const focusableEnd = Selector(FOCUSABLE_END_SELECTOR); await t .click(dateRangeBox.getStartDateBox().input) @@ -28,19 +36,35 @@ test('DateRangeBox & DateBoxes should have focus class if inputs are focused by .expect(dateRangeBox.getEndDateBox().isFocused) .ok(); + await t.pressKey('tab'); + + if (!await focusableEnd.focused) { + await t.pressKey('tab'); + } + await t - .pressKey('tab') + .expect(focusableEnd.focused) + .ok() .expect(dateRangeBox.isFocused) .notOk() .expect(dateRangeBox.getStartDateBox().isFocused) .notOk() .expect(dateRangeBox.getEndDateBox().isFocused) .notOk(); -}).before(async () => createWidget('dxDateRangeBox', { - value: ['2021/09/17', '2021/10/24'], - openOnFieldClick: false, - width: 500, -})); +}).before(async () => { + await createWidget('dxDateRangeBox', { + value: ['2021/09/17', '2021/10/24'], + openOnFieldClick: false, + width: 500, + }); + + await appendElementTo('body', 'button', FOCUSABLE_END_ID, { + position: 'fixed', + top: '0', + left: '0', + opacity: '0', + }); +}).after(async () => removeElementById(FOCUSABLE_END_ID)); test('DateRangeBox & DateBoxes should have focus class if inputs are focused by click', async (t) => { const dateRangeBox = new DateRangeBox('#container'); diff --git a/e2e/testcafe-devextreme/tests/editors/dateRangeBox/keyboard.ts b/e2e/testcafe-devextreme/tests/editors/dateRangeBox/keyboard.ts index c1afda6dc707..eac3103eb891 100644 --- a/e2e/testcafe-devextreme/tests/editors/dateRangeBox/keyboard.ts +++ b/e2e/testcafe-devextreme/tests/editors/dateRangeBox/keyboard.ts @@ -1,4 +1,4 @@ -import { Selector } from 'testcafe'; +import { ClientFunction, Selector } from 'testcafe'; import DateRangeBox from 'devextreme-testcafe-models/dateRangeBox'; import url from '../../../helpers/getPageUrl'; import { createWidget } from '../../../helpers/createWidget'; @@ -8,6 +8,12 @@ fixture.disablePageReloads`DateRangeBox keyboard navigation` .page(url(__dirname, '../../container.html')); const initialValue = [new Date('2021/10/17'), new Date('2021/11/24')]; +const FOCUSABLE_END_ID = 'focusable-end'; +const FOCUSABLE_END_SELECTOR = `#${FOCUSABLE_END_ID}`; + +const removeElementById = ClientFunction((elementId: string): void => { + document.getElementById(elementId)?.remove(); +}); const getDateByOffset = (date: Date | string, offset: number) => { const resultDate = new Date(date); @@ -426,6 +432,7 @@ test('DateRangeBox should be closed by press esc key when views wrapper in popup test('DateRangeBox should not be closed by press tab key on startDate input', async (t) => { const dateRangeBox = new DateRangeBox('#container'); + const focusableEnd = Selector(FOCUSABLE_END_SELECTOR); await t .click(dateRangeBox.getStartDateBox().input); @@ -448,23 +455,38 @@ test('DateRangeBox should not be closed by press tab key on startDate input', as await t .pressKey('tab'); + if (!await focusableEnd.focused) { + await t.pressKey('tab'); + } + await t .expect(dateRangeBox.option('opened')) .eql(false) + .expect(focusableEnd.focused) + .ok() .expect(dateRangeBox.isFocused) .notOk(); -}).before(async () => createWidget('dxDateRangeBox', { - value: ['2021/09/17', '2021/10/24'], - openOnFieldClick: true, - opened: true, - width: 500, - dropDownOptions: { - hideOnOutsideClick: false, - }, - calendarOptions: { - focusStateEnabled: false, - }, -})); +}).before(async () => { + await createWidget('dxDateRangeBox', { + value: ['2021/09/17', '2021/10/24'], + openOnFieldClick: true, + opened: true, + width: 500, + dropDownOptions: { + hideOnOutsideClick: false, + }, + calendarOptions: { + focusStateEnabled: false, + }, + }); + + await appendElementTo('body', 'button', FOCUSABLE_END_ID, { + position: 'fixed', + top: '0', + left: '0', + opacity: '0', + }); +}).after(async () => removeElementById(FOCUSABLE_END_ID)); test('DateRangeBox keyboard navigation via `tab` key if applyValueMode is useButtons, start -> end -> prev -> caption -> next -> views -> today -> apply -> cancel -> start -> end', async (t) => { const dateRangeBox = new DateRangeBox('#dateRangeBox'); diff --git a/e2e/testcafe-devextreme/tests/editors/selectBox/actionButton.ts b/e2e/testcafe-devextreme/tests/editors/selectBox/actionButton.ts index a6e4459aaaac..70f904432f23 100644 --- a/e2e/testcafe-devextreme/tests/editors/selectBox/actionButton.ts +++ b/e2e/testcafe-devextreme/tests/editors/selectBox/actionButton.ts @@ -1,4 +1,4 @@ -import { ClientFunction } from 'testcafe'; +import { ClientFunction, Selector } from 'testcafe'; import { createScreenshotsComparer } from 'devextreme-screenshot-comparer'; import SelectBox from 'devextreme-testcafe-models/selectBox'; import url from '../../../helpers/getPageUrl'; @@ -15,6 +15,13 @@ const purePressKey = async (t, key): Promise => { .wait(100); }; +const FOCUSABLE_END_ID = 'focusable-end'; +const FOCUSABLE_END_SELECTOR = `#${FOCUSABLE_END_ID}`; + +const removeElementById = ClientFunction((elementId: string): void => { + document.getElementById(elementId)?.remove(); +}); + test('Click on action button should correctly work with SelectBox containing the field template (T811890)', async (t) => { const selectBox = new SelectBox('#container'); const { getInstance } = selectBox; @@ -97,6 +104,7 @@ test('Click on action button after typing should correctly work with SelectBox c test('editor can be focused out after click on action button', async (t) => { const selectBox = new SelectBox('#container'); const { getInstance } = selectBox; + const focusableEnd = Selector(FOCUSABLE_END_SELECTOR); await ClientFunction( () => { @@ -123,11 +131,26 @@ test('editor can be focused out after click on action button', async (t) => { .expect(selectBox.isFocused).ok(); await purePressKey(t, 'tab'); + + if (!await focusableEnd.focused) { + await purePressKey(t, 'tab'); + } + await t + .expect(focusableEnd.focused).ok() .expect(selectBox.isFocused).notOk(); -}).before(async () => createWidget('dxSelectBox', { - items: ['item1', 'item2'], -})); +}).before(async () => { + await createWidget('dxSelectBox', { + items: ['item1', 'item2'], + }); + + await appendElementTo('body', 'button', FOCUSABLE_END_ID, { + position: 'fixed', + top: '0', + left: '0', + opacity: '0', + }); +}).after(async () => removeElementById(FOCUSABLE_END_ID)); test('selectbox should not be opened after click on disabled action button (T1117453)', async (t) => { const selectBox = new SelectBox('#container'); diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/dragAndDrop/T1118059.ts b/e2e/testcafe-devextreme/tests/scheduler/common/dragAndDrop/T1118059.ts index 65722c06a789..68e96421693e 100644 --- a/e2e/testcafe-devextreme/tests/scheduler/common/dragAndDrop/T1118059.ts +++ b/e2e/testcafe-devextreme/tests/scheduler/common/dragAndDrop/T1118059.ts @@ -25,19 +25,19 @@ const safeEvent = (value) => ClientFunction(() => { (window as any).eventName = value; }, { dependencies: { value } }); +const getEventName = ClientFunction(() => (window as any).eventName); + test('After drag to draggable component, should be called onAppointmentDeleting event only', async (t) => { const scheduler = new Scheduler(SCHEDULER_SELECTOR); await t .dragToElement(scheduler.getAppointment('Regular test app').element, Selector('#drag-container'), { speed: 0.5 }); - await t.wait(500); - await t - .expect(ClientFunction(() => (window as any).eventName)()) - .eql('onAppointmentDeleting'); + .expect(getEventName()) + .eql('onAppointmentDeleting', { timeout: 3000 }); }).before(async () => { - safeEvent(''); + await safeEvent('')(); await setStyleAttribute(Selector('#container'), 'display: flex; flex-direction: column;'); await ClientFunction(() => { diff --git a/packages/devextreme/js/__internal/scheduler/m_scheduler.ts b/packages/devextreme/js/__internal/scheduler/m_scheduler.ts index 9f6c5c7d607c..ff8932216d83 100644 --- a/packages/devextreme/js/__internal/scheduler/m_scheduler.ts +++ b/packages/devextreme/js/__internal/scheduler/m_scheduler.ts @@ -2385,9 +2385,9 @@ class Scheduler extends SchedulerOptionsBaseWidget { focus() { if (this.editAppointmentData) { - this._appointments.focus(); + this._appointments?.focus(); } else { - this._workSpace.focus(); + this._workSpace?.focus(); } } diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets.scheduler/appointment.editing.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets.scheduler/appointment.editing.tests.js index cf9c2d5d3b5e..bf4f9ee1bfb4 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets.scheduler/appointment.editing.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets.scheduler/appointment.editing.tests.js @@ -247,7 +247,7 @@ module('Integration: Appointment editing', { test('Scheduler should add only one appointment at multiple "done" button clicks on appointment form', async function(assert) { const a = { text: 'a', startDate: new Date(2017, 7, 9), endDate: new Date(2017, 7, 9, 0, 15) }; - const scheduler = await createWrapper({ + const scheduler = await this.createInstance({ dataSource: [], currentDate: new Date(2017, 7, 9), currentView: 'week', @@ -269,7 +269,7 @@ module('Integration: Appointment editing', { appointmentPopup.clickDoneButton(); appointmentPopup.clickDoneButton(); - await waitForAsync(() => scheduler.appointments.getAppointmentCount() === 1); + await waitForAsync(() => scheduler.appointments.getAppointmentCount() === 1, undefined, 2000); assert.equal(scheduler.appointments.getAppointmentCount(), 1, 'right appointment quantity'); }); }); diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets.scheduler/appointment.monthView.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets.scheduler/appointment.monthView.tests.js index 08035e6edd61..08c71e9d0d2b 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets.scheduler/appointment.monthView.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets.scheduler/appointment.monthView.tests.js @@ -12,7 +12,7 @@ import { createWrapper, supportedScrollingModes } from '../../helpers/scheduler/helpers.js'; -import { waitAsync } from '../../helpers/scheduler/waitForAsync.js'; +import { waitAsync, waitForAsync } from '../../helpers/scheduler/waitForAsync.js'; import '__internal/scheduler/m_scheduler'; import 'ui/switch'; @@ -136,7 +136,14 @@ module('Integration: Appointments in Month view', { ] }); - assert.deepEqual(scheduler.instance.$element().find('.' + APPOINTMENT_CLASS).length, 2, 'Appointments are rendered'); + const getAppointmentCount = () => scheduler.instance.$element().find('.' + APPOINTMENT_CLASS).length; + + // NOTE: the resource store resolves asynchronously (300ms), so appointments + // render only after the resources are loaded. Wait for them instead of relying + // on a fixed timing budget, which is flaky under CI load. + await waitForAsync(() => getAppointmentCount() === 2, undefined, 2000); + + assert.deepEqual(getAppointmentCount(), 2, 'Appointments are rendered'); }); }); diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets/chatParts/chat.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets/chatParts/chat.tests.js index 023eb4e58434..b4f9e03271c4 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets/chatParts/chat.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets/chatParts/chat.tests.js @@ -70,6 +70,16 @@ const TEXTEDITOR_INPUT_CLASS = 'dx-texteditor-input'; const RTL_CLASS = 'dx-rtl'; const ANIMATION_TIMEOUT = 250; +const waitForCondition = (condition, timeout = ANIMATION_TIMEOUT * 8, interval = 15) => new Promise((resolve) => { + const startTime = Date.now(); + const timer = setInterval(() => { + if(condition() || Date.now() - startTime >= timeout) { + clearInterval(timer); + resolve(); + } + }, interval); +}); + export const MOCK_COMPANION_USER_ID = 'COMPANION_USER_ID'; export const MOCK_CURRENT_USER_ID = 'CURRENT_USER_ID'; export const NOW = 1721747399083; @@ -1004,9 +1014,7 @@ QUnit.module('Chat', () => { QUnit.module('Message editing preview integration', moduleConfig, () => { [true, false].forEach((isPromise) => { [true, false].forEach((cancel) => { - QUnit.test(`editing preview should appear based on onMessageEditingStart cancel (isPromise=${isPromise}, cancel=${cancel})`, function(assert) { - const done = assert.async(); - + QUnit.test(`editing preview should appear based on onMessageEditingStart cancel (isPromise=${isPromise}, cancel=${cancel})`, async function(assert) { const items = [ { text: 'a', author: userFirst }, { text: 'b', author: userSecond }, @@ -1029,15 +1037,15 @@ QUnit.module('Chat', () => { const $editButton = this.getContextMenuItems().eq(0); $editButton.trigger('dxclick'); - setTimeout(() => { - assert.strictEqual(this.getEditingPreview().length, cancel ? 0 : 1); - done(); - }); - }); + // Entering the editing mode is asynchronous. When not cancelled, wait until the + // preview appears; when cancelled, give a bounded window for an erroneous preview + // to appear and then confirm it did not. + await waitForCondition(() => this.getEditingPreview().length === 1, cancel ? ANIMATION_TIMEOUT : undefined); - QUnit.test(`Editing preview should remain visible depending on onMessageUpdating cancellation (isPromise=${isPromise}, cancel=${cancel})`, function(assert) { - const done = assert.async(); + assert.strictEqual(this.getEditingPreview().length, cancel ? 0 : 1); + }); + QUnit.test(`Editing preview should remain visible depending on onMessageUpdating cancellation (isPromise=${isPromise}, cancel=${cancel})`, async function(assert) { const items = [ { text: 'a', author: userFirst }, { text: 'b', author: userSecond }, @@ -1060,23 +1068,28 @@ QUnit.module('Chat', () => { const $editButton = this.getContextMenuItems().eq(0); $editButton.trigger('dxclick'); + // Entering the editing mode (setting previewText) runs through the context + // menu click pipeline and is not guaranteed to finish synchronously. Clicking + // send before the editing preview is shown would take the regular-send path and + // leave the preview unmanaged, so wait until the editing mode is established. + await waitForCondition(() => this.getEditingPreview().length === 1); + this.$sendButton.trigger('dxclick'); - setTimeout(() => { - assert.strictEqual( - this.getEditingPreview().length, - cancel ? 1 : 0, - `Editing preview ${cancel ? 'remains' : 'is hidden'} when cancel=${cancel}` - ); - done(); - }, ANIMATION_TIMEOUT); + const expectedLength = cancel ? 1 : 0; + + await waitForCondition(() => this.getEditingPreview().length === expectedLength); + + assert.strictEqual( + this.getEditingPreview().length, + expectedLength, + `Editing preview ${cancel ? 'remains' : 'is hidden'} when cancel=${cancel}` + ); }); }); }); - QUnit.testInActiveWindow('editing preview should be shown after the Edit button is clicked if cancel promise rejected', function(assert) { - const done = assert.async(); - + QUnit.testInActiveWindow('editing preview should be shown after the Edit button is clicked if cancel promise rejected', async function(assert) { const items = [ { text: 'a', author: userFirst }, { text: 'b', author: userSecond }, @@ -1099,18 +1112,19 @@ QUnit.module('Chat', () => { const $editButton = this.getContextMenuItems().eq(0); $editButton.trigger('dxclick'); + // The rejected cancel promise resolves the editing mode asynchronously, and the + // input is focused only after the context menu hides — wait for both instead of + // assuming a fixed delay. + await waitForCondition(() => this.getEditingPreview().length === 1 + && this.textArea.option('text') === items[1].text + && this.$textArea.hasClass(FOCUSED_STATE_CLASS)); - setTimeout(() => { - assert.strictEqual(this.getEditingPreview().length, 1); - assert.strictEqual(this.textArea.option('text'), items[1].text, 'input contains edited text'); - assert.strictEqual(this.$textArea.hasClass(FOCUSED_STATE_CLASS), true, 'input is focused'); - done(); - }); + assert.strictEqual(this.getEditingPreview().length, 1); + assert.strictEqual(this.textArea.option('text'), items[1].text, 'input contains edited text'); + assert.strictEqual(this.$textArea.hasClass(FOCUSED_STATE_CLASS), true, 'input is focused'); }); - QUnit.test('editing preview should be hidden after the message is deleted', function(assert) { - const done = assert.async(); - + QUnit.testInActiveWindow('editing preview should be hidden after the message is deleted', async function(assert) { const items = [ { text: 'a', author: userFirst }, { text: 'b', author: userSecond }, @@ -1131,6 +1145,9 @@ QUnit.module('Chat', () => { const $editButton = this.getContextMenuItems().eq(0); $editButton.trigger('dxclick'); + // Make sure the editing mode is established before deleting the message. + await waitForCondition(() => this.getEditingPreview().length === 1); + $bubbles.eq(1).trigger('dxcontextmenu'); const $deleteButton = this.getContextMenuItems().eq(1); @@ -1141,15 +1158,17 @@ QUnit.module('Chat', () => { $applyButton.trigger('dxclick'); - setTimeout(() => { - assert.strictEqual(this.getEditingPreview().length, 0); - assert.strictEqual(this.textArea.option('value'), '', 'input is empty'); - assert.strictEqual(this.$textArea.hasClass(FOCUSED_STATE_CLASS), true, 'input is focused'); - done(); - }, ANIMATION_TIMEOUT); + // The input is refocused asynchronously, only after the confirmation popup hides. + await waitForCondition(() => this.getEditingPreview().length === 0 + && this.textArea.option('value') === '' + && this.$textArea.hasClass(FOCUSED_STATE_CLASS)); + + assert.strictEqual(this.getEditingPreview().length, 0); + assert.strictEqual(this.textArea.option('value'), '', 'input is empty'); + assert.strictEqual(this.$textArea.hasClass(FOCUSED_STATE_CLASS), true, 'input is focused'); }); - QUnit.test('send button should change its active state with update input value during editing', function(assert) { + QUnit.test('send button should change its active state with update input value during editing', async function(assert) { const items = [ { text: 'a', author: userFirst }, { text: 'b', author: userSecond }, @@ -1174,16 +1193,18 @@ QUnit.module('Chat', () => { const $editButton = this.getContextMenuItems().eq(0); $editButton.trigger('dxclick'); + await waitForCondition(() => sendButton.option('disabled') === false); + assert.strictEqual(sendButton.option('disabled'), false, 'send button is active after edit started'); this.getCancelEditingButton().trigger('dxclick'); + await waitForCondition(() => sendButton.option('disabled') === true); + assert.strictEqual(sendButton.option('disabled'), true, 'send button is disabled after edit cancelled'); }); - QUnit.test('editing preview should be enabled after the send button is clicked if cancel promise rejected', function(assert) { - const done = assert.async(); - + QUnit.testInActiveWindow('editing preview should be enabled after the send button is clicked if cancel promise rejected', async function(assert) { const items = [ { text: 'a', author: userFirst }, { text: 'b', author: userSecond }, @@ -1206,17 +1227,23 @@ QUnit.module('Chat', () => { const $editButton = this.getContextMenuItems().eq(0); $editButton.trigger('dxclick'); + // The input is focused asynchronously, only after the context menu finishes hiding. + // Wait until the editing mode is fully established (preview shown and input focused) + // before sending, so the focus assertion does not race with the menu hide animation. + await waitForCondition(() => this.getEditingPreview().length === 1 + && this.$textArea.hasClass(FOCUSED_STATE_CLASS)); + this.$sendButton.trigger('dxclick'); - setTimeout(() => { - assert.strictEqual(this.getEditingPreview().length, 0); - assert.strictEqual(this.textArea.option('value'), '', 'input is empty'); - assert.strictEqual(this.$textArea.hasClass(FOCUSED_STATE_CLASS), true, 'input is focused'); - done(); - }, ANIMATION_TIMEOUT); + await waitForCondition(() => this.getEditingPreview().length === 0 + && this.textArea.option('value') === ''); + + assert.strictEqual(this.getEditingPreview().length, 0); + assert.strictEqual(this.textArea.option('value'), '', 'input is empty'); + assert.strictEqual(this.$textArea.hasClass(FOCUSED_STATE_CLASS), true, 'input is focused'); }); - QUnit.testInActiveWindow('message box should have editing message text and focus after the Edit button is clicked and not cancelled', function(assert) { + QUnit.testInActiveWindow('message box should have editing message text and focus after the Edit button is clicked and not cancelled', async function(assert) { const items = [ { text: 'a', author: userFirst }, { text: 'b', author: userSecond }, @@ -1237,11 +1264,15 @@ QUnit.module('Chat', () => { const $editButton = this.getContextMenuItems().eq(0); $editButton.trigger('dxclick'); + // The input value and focus are applied asynchronously after the context menu hides. + await waitForCondition(() => this.textArea.option('value') === 'b' + && this.$textArea.hasClass(FOCUSED_STATE_CLASS)); + assert.strictEqual(this.textArea.option('value'), 'b', 'input contains editing message text'); assert.strictEqual(this.$textArea.hasClass(FOCUSED_STATE_CLASS), true, 'input is focused'); }); - QUnit.testInActiveWindow('message box should have editing message text and focus after the Edit was triggered from keyboard', function(assert) { + QUnit.testInActiveWindow('message box should have editing message text and focus after the Edit was triggered from keyboard', async function(assert) { const items = [ { text: 'a', author: userFirst }, { text: 'b', author: userSecond }, @@ -1263,11 +1294,15 @@ QUnit.module('Chat', () => { .press('down') .press('enter'); + // The input value and focus are applied asynchronously after the context menu hides. + await waitForCondition(() => this.textArea.option('value') === 'b' + && this.$textArea.hasClass(FOCUSED_STATE_CLASS)); + assert.strictEqual(this.textArea.option('value'), 'b', 'input contains editing message text'); assert.strictEqual(this.$textArea.hasClass(FOCUSED_STATE_CLASS), true, 'input is focused'); }); - QUnit.testInActiveWindow('attach button should be hidden after editing is started', function(assert) { + QUnit.testInActiveWindow('attach button should be hidden after editing is started', async function(assert) { this.reinit({ items: [{ text: 'f', author: userSecond }], focusStateEnabled: true, @@ -1286,10 +1321,12 @@ QUnit.module('Chat', () => { const $editButton = this.getContextMenuItems().eq(0); $editButton.trigger('dxclick'); + await waitForCondition(() => attachButton.option('visible') === false); + assert.strictEqual(attachButton.option('visible'), false, 'attach button is hidden'); }); - QUnit.testInActiveWindow('attach button should be visible after editing is done', function(assert) { + QUnit.testInActiveWindow('attach button should be visible after editing is done', async function(assert) { this.reinit({ items: [{ text: 'f', author: userSecond }], focusStateEnabled: true, @@ -1304,14 +1341,18 @@ QUnit.module('Chat', () => { const $editButton = this.getContextMenuItems().eq(0); $editButton.trigger('dxclick'); + + // Wait until the editing mode is established before sending the update. + await waitForCondition(() => this.getEditingPreview().length === 1); + this.$sendButton.trigger('dxclick'); - const attachButton = this.getAttachButton(); + await waitForCondition(() => this.getAttachButton().option('visible') === true); - assert.strictEqual(attachButton.option('visible'), true, 'attach button is visible'); + assert.strictEqual(this.getAttachButton().option('visible'), true, 'attach button is visible'); }); - QUnit.testInActiveWindow('attach button should be visible after editing is canceled', function(assert) { + QUnit.testInActiveWindow('attach button should be visible after editing is canceled', async function(assert) { this.reinit({ items: [{ text: 'f', author: userSecond }], focusStateEnabled: true, @@ -1326,8 +1367,14 @@ QUnit.module('Chat', () => { const $editButton = this.getContextMenuItems().eq(0); $editButton.trigger('dxclick'); + + // Wait until the editing mode is established before canceling. + await waitForCondition(() => this.getEditingPreview().length === 1); + this.getCancelEditingButton().trigger('dxclick'); + await waitForCondition(() => this.getAttachButton().option('visible') === true); + const attachButton = this.getAttachButton(); assert.strictEqual(attachButton.option('visible'), true, 'attach button is visible'); diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets/chatParts/messageList.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets/chatParts/messageList.tests.js index 1d991d89a08b..64498f42131f 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets/chatParts/messageList.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets/chatParts/messageList.tests.js @@ -47,6 +47,27 @@ const getStringDate = (date) => { }; const SCROLLVIEW_CLASS = 'dx-scrollview'; +// NOTE: scroll restoration after a height change is driven by an asynchronous +// ResizeObserver, so a fixed delay races with it under CI load. Poll until the +// expected condition holds (bounded) instead of guessing a timeout. +const waitForCondition = (condition, timeout = 3000, interval = 15) => new Promise((resolve) => { + const startTime = Date.now(); + const timer = setInterval(() => { + let satisfied = false; + + try { + satisfied = condition(); + } catch(e) { + satisfied = false; + } + + if(satisfied || Date.now() - startTime >= timeout) { + clearInterval(timer); + resolve(); + } + }, interval); +}); + const moduleConfig = { beforeEach: function() { const init = (options = {}, selector = '#component') => { @@ -1728,9 +1749,7 @@ QUnit.module('MessageList', () => { }); }); - QUnit.test('should be scrolled to the bottom after reducing height if it\'s initially scrolled to the bottom', function(assert) { - const done = assert.async(); - + QUnit.test('should be scrolled to the bottom after reducing height if it\'s initially scrolled to the bottom', async function(assert) { const items = generateMessages(31); this.reinit({ @@ -1739,26 +1758,18 @@ QUnit.module('MessageList', () => { items, }); - setTimeout(() => { - const scrollTop = this.getScrollView().scrollTop(); - - assert.roughEqual(scrollTop, this.getScrollOffsetMax(), 1, 'scroll position should be at the bottom after initialization'); + await waitForCondition(() => Math.abs(this.getScrollView().scrollTop() - this.getScrollOffsetMax()) <= 1); - this.instance.option('height', 300); + assert.roughEqual(this.getScrollView().scrollTop(), this.getScrollOffsetMax(), 1, 'scroll position should be at the bottom after initialization'); - setTimeout(() => { - const scrollTop = this.getScrollView().scrollTop(); + this.instance.option('height', 300); - assert.roughEqual(scrollTop, this.getScrollOffsetMax(), 1, 'max scroll position should be saved after reducing height'); + await waitForCondition(() => Math.abs(this.getScrollView().scrollTop() - this.getScrollOffsetMax()) <= 1); - done(); - }, this._resizeTimeout); - }, this._resizeTimeout); + assert.roughEqual(this.getScrollView().scrollTop(), this.getScrollOffsetMax(), 1, 'max scroll position should be saved after reducing height'); }); - QUnit.test('should be scrolled to the bottom after increasing height if it\'s initially scrolled to the bottom', function(assert) { - const done = assert.async(); - + QUnit.test('should be scrolled to the bottom after increasing height if it\'s initially scrolled to the bottom', async function(assert) { const items = generateMessages(31); this.reinit({ @@ -1767,26 +1778,18 @@ QUnit.module('MessageList', () => { items, }); - setTimeout(() => { - const scrollTop = this.getScrollView().scrollTop(); + await waitForCondition(() => Math.abs(this.getScrollView().scrollTop() - this.getScrollOffsetMax()) <= 1); - assert.roughEqual(scrollTop, this.getScrollOffsetMax(), 1, 'scroll position should be at the bottom after initialization'); - - this.instance.option('height', 700); + assert.roughEqual(this.getScrollView().scrollTop(), this.getScrollOffsetMax(), 1, 'scroll position should be at the bottom after initialization'); - setTimeout(() => { - const scrollTop = this.getScrollView().scrollTop(); + this.instance.option('height', 700); - assert.roughEqual(scrollTop, this.getScrollOffsetMax(), 1, 'max scroll position should be saved after increasing height'); + await waitForCondition(() => Math.abs(this.getScrollView().scrollTop() - this.getScrollOffsetMax()) <= 1); - done(); - }, this._resizeTimeout); - }, this._resizeTimeout); + assert.roughEqual(this.getScrollView().scrollTop(), this.getScrollOffsetMax(), 1, 'max scroll position should be saved after increasing height'); }); - QUnit.test('should update visual scroll position after reducing height if it\'s not scrolled to the bottom (fix viewport bottom point)', function(assert) { - const done = assert.async(); - + QUnit.test('should update visual scroll position after reducing height if it\'s not scrolled to the bottom (fix viewport bottom point)', async function(assert) { const items = generateMessages(31); this.reinit({ @@ -1795,27 +1798,21 @@ QUnit.module('MessageList', () => { items, }); - setTimeout(() => { - const scrollTop = this.getScrollView().scrollTop(); + await waitForCondition(() => Math.abs(this.getScrollView().scrollTop() - this.getScrollOffsetMax()) <= 1); - assert.roughEqual(scrollTop, this.getScrollOffsetMax(), 1, 'scroll position should be at the bottom after initialization'); + assert.roughEqual(this.getScrollView().scrollTop(), this.getScrollOffsetMax(), 1, 'scroll position should be at the bottom after initialization'); - this.getScrollView().scrollTo({ top: this.getScrollOffsetMax() - 200 }); - this.instance.option('height', 300); + this.getScrollView().scrollTo({ top: this.getScrollOffsetMax() - 200 }); + this.instance.option('height', 300); - setTimeout(() => { - const scrollTop = this.getScrollView().scrollTop(); - - assert.roughEqual(scrollTop, this.getScrollOffsetMax() - 200, 1, 'scroll position should be set correctly after reducing height'); + // Reducing the container height shifts the viewport bottom point, so the scroll + // position is expected to settle at (new max scroll offset - 200). + await waitForCondition(() => Math.abs(this.getScrollView().scrollTop() - (this.getScrollOffsetMax() - 200)) <= 1); - done(); - }, this._resizeTimeout); - }, this._resizeTimeout); + assert.roughEqual(this.getScrollView().scrollTop(), this.getScrollOffsetMax() - 200, 1, 'scroll position should be set correctly after reducing height'); }); - QUnit.test('should keep visual scroll position after increasing height if it\'s not scrolled to the bottom (fix viewport top point)', function(assert) { - const done = assert.async(); - + QUnit.test('should keep visual scroll position after increasing height if it\'s not scrolled to the bottom (fix viewport top point)', async function(assert) { const items = generateMessages(31); this.reinit({ @@ -1824,23 +1821,17 @@ QUnit.module('MessageList', () => { items, }); - setTimeout(() => { - const scrollTop = this.getScrollView().scrollTop(); - - assert.roughEqual(scrollTop, this.getScrollOffsetMax(), 1, 'scroll position should be at the bottom after initialization'); + await waitForCondition(() => Math.abs(this.getScrollView().scrollTop() - this.getScrollOffsetMax()) <= 1); - const newScrollTop = this.getScrollOffsetMax() - 200; - this.getScrollView().scrollTo({ top: newScrollTop }); - this.instance.option('height', 600); + assert.roughEqual(this.getScrollView().scrollTop(), this.getScrollOffsetMax(), 1, 'scroll position should be at the bottom after initialization'); - setTimeout(() => { - const scrollTop = this.getScrollView().scrollTop(); + const newScrollTop = this.getScrollOffsetMax() - 200; + this.getScrollView().scrollTo({ top: newScrollTop }); + this.instance.option('height', 600); - assert.roughEqual(scrollTop, newScrollTop, 1, 'scroll position should be saved correctly after increasing height'); + await waitForCondition(() => Math.abs(this.getScrollView().scrollTop() - newScrollTop) <= 1); - done(); - }, this._resizeTimeout); - }, this._resizeTimeout); + assert.roughEqual(this.getScrollView().scrollTop(), newScrollTop, 1, 'scroll position should be saved correctly after increasing height'); }); QUnit.test('should limit scroll position after increasing height more than scroll offset allows', function(assert) { diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets/listParts/commonTests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets/listParts/commonTests.js index 03f55e328e24..0e148d044dac 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets/listParts/commonTests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets/listParts/commonTests.js @@ -378,18 +378,28 @@ QUnit.module('collapsible groups', moduleSetup, () => { const $groupBody = $group.find('.' + LIST_GROUP_BODY_CLASS); const $groupHeader = $element.find('.' + LIST_GROUP_HEADER_CLASS); - const animationDuration = 200; - for(let i = 0; i < 11; i++) { $groupHeader.trigger('dxclick'); } - setTimeout(() => { - assert.strictEqual($group.hasClass(LIST_GROUP_COLLAPSED_CLASS), true, 'collapsed class is present'); - assert.strictEqual($groupBody.height(), 0, 'group items are hidden'); + const groupBodyElement = $groupBody.get(0); + const startTime = Date.now(); - done(); - }, 2 * animationDuration); + // NOTE: each click cancels the previous animation and starts a new one, so the + // final collapse animation may finish later than a fixed delay under CI load. + // Wait until the animation actually settles instead of guessing a timeout (T1282693). + const waitForSettled = () => { + if(!fx.isAnimating(groupBodyElement) || Date.now() - startTime >= 3000) { + assert.strictEqual($group.hasClass(LIST_GROUP_COLLAPSED_CLASS), true, 'collapsed class is present'); + assert.strictEqual($groupBody.height(), 0, 'group items are hidden'); + + done(); + } else { + setTimeout(waitForSettled, 16); + } + }; + + waitForSettled(); }); const LIST_GROUP_HEADER_INDICATOR_CLASS = 'dx-list-group-header-indicator'; From 3269255f1625e645ae4139f2e86f3e538b6724cf Mon Sep 17 00:00:00 2001 From: EugeniyKiyashko Date: Mon, 15 Jun 2026 23:00:37 +0400 Subject: [PATCH 4/7] update --- .../keyboardNavigation.visual.ts | 14 ++++++ .../dataGrid/common/rowDragging/functional.ts | 8 ++- .../common.tests.js | 2 + .../chatParts/chat.tests.js | 49 +++++++++++++------ 4 files changed, 52 insertions(+), 21 deletions(-) diff --git a/e2e/testcafe-devextreme/tests/dataGrid/common/keyboardNavigation/keyboardNavigation.visual.ts b/e2e/testcafe-devextreme/tests/dataGrid/common/keyboardNavigation/keyboardNavigation.visual.ts index a778d79d9f9e..1c7f3c1f0bb7 100644 --- a/e2e/testcafe-devextreme/tests/dataGrid/common/keyboardNavigation/keyboardNavigation.visual.ts +++ b/e2e/testcafe-devextreme/tests/dataGrid/common/keyboardNavigation/keyboardNavigation.visual.ts @@ -4,6 +4,7 @@ import { ClientFunction } from 'testcafe'; import { createWidget } from '../../../../helpers/createWidget'; import url from '../../../../helpers/getPageUrl'; import { getData } from '../../helpers/generateDataSourceData'; +import { isScrollAtEnd } from '../../helpers/rowDraggingHelpers'; import { testScreenshot } from '../../../../helpers/themeUtils'; fixture`Keyboard Navigation.Visual` @@ -19,6 +20,12 @@ const isKeyboardNavigationInProgress = ClientFunction(() => { .navigationToCellInProgress(); }); +const waitForPaint = ClientFunction(() => new Promise((resolve) => { + requestAnimationFrame(() => { + requestAnimationFrame(() => resolve()); + }); +})); + const focusDataCell = async ( t: TestController, dataGrid: DataGrid, @@ -767,6 +774,13 @@ test('Navigate to first cell in the first row when virtual scrolling and columns await waitForKeyboardNavigation(t); await expectDataCellFocusState(t, dataGrid, 199, 34); + await t + .expect(dataGrid.isReady()) + .ok({ timeout: KEYBOARD_NAVIGATION_TIMEOUT }) + .expect(isScrollAtEnd('horizontal')) + .ok({ timeout: KEYBOARD_NAVIGATION_TIMEOUT }); + await waitForPaint(); + await testScreenshot(t, takeScreenshot, `${useNative ? 'native' : 'simulated'}_scrolling_-_navigate_to_last_cell_row_dragging__virtual_scrolling__virtual_columns.png`, { element: dataGrid.element }); // assert diff --git a/e2e/testcafe-devextreme/tests/dataGrid/common/rowDragging/functional.ts b/e2e/testcafe-devextreme/tests/dataGrid/common/rowDragging/functional.ts index 039c50a4e02b..75f8fa44e57e 100644 --- a/e2e/testcafe-devextreme/tests/dataGrid/common/rowDragging/functional.ts +++ b/e2e/testcafe-devextreme/tests/dataGrid/common/rowDragging/functional.ts @@ -798,13 +798,11 @@ test('Item should appear in a correct spot when dragging to a different page wit const dataGrid = new DataGrid('#container'); await t.expect(dataGrid.isReady()).ok(); + const rowHeight = await dataGrid.getDataRow(2).element.offsetHeight; const scrollOffsetForAutoScroll = await getOffsetToTriggerAutoScroll(2, 0.5, 'down'); - await dragWithDisabledMouseUp( - t, - dataGrid.getDataRow(2).getDragCommand(), - { offsetX: 0, offsetY: scrollOffsetForAutoScroll, speed: 0.5 }, - ); + await dataGrid.moveRow(2, 0, rowHeight, true); + await dataGrid.moveRow(2, 0, scrollOffsetForAutoScroll); const expectedSequence = ['5-1', '3-1', '6-1']; const isTargetPageRendered = ClientFunction(() => { diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets.scheduler/common.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets.scheduler/common.tests.js index a449ebb290b6..d345cda2ffe3 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets.scheduler/common.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets.scheduler/common.tests.js @@ -77,6 +77,8 @@ QUnit.module('Loading', { await waitForAsync(() => count === 1); scheduler.instance.option('currentView', 'week'); await waitForAsync(() => count === 2); + // the panel hides after the load completes and the view re-renders, not exactly when count===2 + await waitForAsync(() => $('.dx-loadpanel-wrapper').length === 0); assert.equal($('.dx-loadpanel-wrapper').length, 0, 'loading panel hide'); }); diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets/chatParts/chat.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets/chatParts/chat.tests.js index b4f9e03271c4..2ca76c5e255b 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets/chatParts/chat.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets/chatParts/chat.tests.js @@ -35,6 +35,7 @@ import ArrayStore from 'common/data/array_store'; import { CHAT_EDITING_PREVIEW_CLASS, CHAT_EDITING_PREVIEW_CANCEL_BUTTON_CLASS, + CHAT_EDITING_PREVIEW_HIDING_CLASS, } from '__internal/ui/chat/message_box/editing_preview'; import { CHAT_CONFIRMATION_POPUP_WRAPPER_CLASS } from '__internal/ui/chat/confirmationpopup'; import { POPUP_CLASS } from '__internal/ui/popup/popup'; @@ -80,6 +81,16 @@ const waitForCondition = (condition, timeout = ANIMATION_TIMEOUT * 8, interval = }, interval); }); +// The editing preview removes its DOM node only on its CSS hide animation's animationend, +// which is unreliable under CI load. Wait for the hiding to start, then fire animationend +// so the node is removed synchronously. +const waitForEditingPreviewToHide = async(getEditingPreview) => { + await waitForCondition(() => getEditingPreview().length === 0 + || getEditingPreview().hasClass(CHAT_EDITING_PREVIEW_HIDING_CLASS)); + + getEditingPreview().get(0)?.dispatchEvent(new Event('animationend')); +}; + export const MOCK_COMPANION_USER_ID = 'COMPANION_USER_ID'; export const MOCK_CURRENT_USER_ID = 'CURRENT_USER_ID'; export const NOW = 1721747399083; @@ -1076,15 +1087,17 @@ QUnit.module('Chat', () => { this.$sendButton.trigger('dxclick'); - const expectedLength = cancel ? 1 : 0; + if(cancel) { + // The update is cancelled, so the preview must stay visible. Give a bounded + // window for an erroneous hide to start, then confirm it is still shown. + await waitForCondition(() => this.getEditingPreview().hasClass(CHAT_EDITING_PREVIEW_HIDING_CLASS), ANIMATION_TIMEOUT); - await waitForCondition(() => this.getEditingPreview().length === expectedLength); + assert.strictEqual(this.getEditingPreview().length, 1, `Editing preview remains when cancel=${cancel}`); + } else { + await waitForEditingPreviewToHide(() => this.getEditingPreview()); - assert.strictEqual( - this.getEditingPreview().length, - expectedLength, - `Editing preview ${cancel ? 'remains' : 'is hidden'} when cancel=${cancel}` - ); + assert.strictEqual(this.getEditingPreview().length, 0, `Editing preview is hidden when cancel=${cancel}`); + } }); }); }); @@ -1158,9 +1171,11 @@ QUnit.module('Chat', () => { $applyButton.trigger('dxclick'); + // The preview hides via a CSS animation; drive its removal deterministically. + await waitForEditingPreviewToHide(() => this.getEditingPreview()); + // The input is refocused asynchronously, only after the confirmation popup hides. - await waitForCondition(() => this.getEditingPreview().length === 0 - && this.textArea.option('value') === '' + await waitForCondition(() => this.textArea.option('value') === '' && this.$textArea.hasClass(FOCUSED_STATE_CLASS)); assert.strictEqual(this.getEditingPreview().length, 0); @@ -1227,16 +1242,18 @@ QUnit.module('Chat', () => { const $editButton = this.getContextMenuItems().eq(0); $editButton.trigger('dxclick'); - // The input is focused asynchronously, only after the context menu finishes hiding. - // Wait until the editing mode is fully established (preview shown and input focused) - // before sending, so the focus assertion does not race with the menu hide animation. - await waitForCondition(() => this.getEditingPreview().length === 1 - && this.$textArea.hasClass(FOCUSED_STATE_CLASS)); + // Make sure the editing mode is established before sending. + await waitForCondition(() => this.getEditingPreview().length === 1); this.$sendButton.trigger('dxclick'); - await waitForCondition(() => this.getEditingPreview().length === 0 - && this.textArea.option('value') === ''); + // The rejected cancel promise lets the update proceed, clearing the preview; its DOM + // node is removed only when the CSS hide animation ends, so drive that deterministically. + await waitForEditingPreviewToHide(() => this.getEditingPreview()); + + // The input is focused asynchronously, only after the context menu finishes hiding. + await waitForCondition(() => this.textArea.option('value') === '' + && this.$textArea.hasClass(FOCUSED_STATE_CLASS)); assert.strictEqual(this.getEditingPreview().length, 0); assert.strictEqual(this.textArea.option('value'), '', 'input is empty'); From 3d0508b76919770f8226c918402619fe2fe858eb Mon Sep 17 00:00:00 2001 From: EugeniyKiyashko Date: Mon, 15 Jun 2026 23:52:49 +0400 Subject: [PATCH 5/7] update --- ...g__virtual_columns (fluent.blue.light).png | Bin 68485 -> 68421 bytes ...rtual_columns (fluent.blue.light)_mask.png | Bin 7129 -> 0 bytes 2 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 e2e/testcafe-devextreme/tests/dataGrid/common/keyboardNavigation/etalons/simulated_scrolling_-_navigate_to_last_cell_row_dragging__virtual_scrolling__virtual_columns (fluent.blue.light)_mask.png diff --git a/e2e/testcafe-devextreme/tests/dataGrid/common/keyboardNavigation/etalons/simulated_scrolling_-_navigate_to_last_cell_row_dragging__virtual_scrolling__virtual_columns (fluent.blue.light).png b/e2e/testcafe-devextreme/tests/dataGrid/common/keyboardNavigation/etalons/simulated_scrolling_-_navigate_to_last_cell_row_dragging__virtual_scrolling__virtual_columns (fluent.blue.light).png index 90291c9063976906bfbe2018204d7c0ff321bb1a..09df34eb3aa6294e475ab5d1908661b982670655 100644 GIT binary patch delta 8811 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 C!P7SY diff --git a/e2e/testcafe-devextreme/tests/dataGrid/common/keyboardNavigation/etalons/simulated_scrolling_-_navigate_to_last_cell_row_dragging__virtual_scrolling__virtual_columns (fluent.blue.light)_mask.png b/e2e/testcafe-devextreme/tests/dataGrid/common/keyboardNavigation/etalons/simulated_scrolling_-_navigate_to_last_cell_row_dragging__virtual_scrolling__virtual_columns (fluent.blue.light)_mask.png deleted file mode 100644 index 22d091b9b9030189f5812e70eb178f5cf181cd0d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7129 zcmeHMeQZ6D>9jc=9DEQT}3e|h4gEyI)|hk)$(Cql0qfN z;Kof^s9GqVhm>h@ODHasEi@s-z9fz71S5(o%S}z7A@7CQnG&y6)!1{o;wLP7=RQOH z^^Bds*oXGd)w%B;-)G-@&hPxr&u=}oMUuVjfn^j$Wmjxm{|rSfdXS)&m9`*FZ%PU{-FKzKzR}>0A{>#UMiz&*LDS;2I z-a*VCFVNJe7NMP=Vy_9a%NWZn?Or7p49j%gCq<>*$ZUySP#u+2A4Z>FZpV{G+Pxpu z=T>*7F>M{#HFc17chT-nxlQqfSaC^Q^ODrO5BGPHxB9jk>|2fY`x)PA&S@1mIbxlj zU@RRUUE5UlJ_@d2b|}#7hnL$2WV-Tmczi2k>P74p+FjWrQjCh#Z=?PJ%swj@sId@E zXXV;VifYoPX<90gfbxEV1WiTUdHU|6oEA)}YMJm+!+c0Lmg72}?1#zuhFjy;XF^EE&n0I?bfkTlG zly)N5iJ77dis~D>m!e+Z_d{Y@|0~T>xW2&OXg5ShUc%21u58e)!EK|mlw;P6ORFPp z)3qdUCr8Q1{t2($CiNT1*m4XGKBwg)a{L|jWWSo!?*7w~iSr@@nD^xkWAxik9flO+>pgf&T49Kw!^QG3j7xJdmrfBZ`PYNOpl=a_%R1iH|q zEX8DEgg*qdm2}R`Hff&*O_t%dYnQK2TFcKx`3nbMlG1y3libh8M^WFKko_|;kNi84 zt;3~t8bq}QJDjwT1aIItn)nRA>ktoLZr6|q_UW;*klC?;DcvS5BVlaQ(C+u-w)@d6 z+{i`}%jFF^#i@C3vZUqcwM_&Mn{EXU7YL*A9|NGEyd5$f{5(a%_t8`xC&5j8yMA>*!uvQeSxva z5*H2~fa|ZCf}WuP>-m9pv1HU&2GWd`?2yKY<~{LmR!%*}m91T=$DzE^ z9XmOgc#6vtlw$ z6WE)8;&H$}Y!s>AkhF}wajx=FQl2~s-zUHS zDH4#T>eOS?Qq6c(YPaxK1ZJ0`S-4O@F}P5GH@Imt)I^!o5AYVZ;CBd%zThPW9+x)R z%2(RTh6wG5aFE!l9$nxAA=<5g9O*_a=dt%6DJGt;`ESo!h+<(jjJZP{e~t3jI+I}R z9P9@A@pPrv+DOkq>5n3)x{u$dM%9!bqQ!p!86 zZvYK>LrCGZ5}Z7Og3#j}N6^W!=dnqT+10eWfbksw;!vkk%WZ(1$(%9^l+Y7K!ZN~> zU6p^g7MwsqB=t8M?1M-U0_;1DCI=nj5EccBX}1%%g*Gq&278_G8V0~O{?bY^B&i<& zy6Gwl8t@JEa};Q4{BAyL8`p3FoIu-k)3LRrdCGlT`m!hvoE7|5TMY18r{lnO4b*Oe zu=}BA6TH=N>u;pA8)z($*#YSISfIE-bXP;)XFiBn;II1-^aROW&w7~l z@-xE0`Z?aaH$s6IXfCP#AMWqcY6krc8q-@?2`v!tKxlyoi$xr#HF69$Zh>R7$?=b5w{WrbJF1l58&sp&63C)a@gRsPyHZT9zRKU7PaTX>9wIV?tMZkIzt7ru z6ZwH7uE8qCSJ*sNjOO%&-$$f{1)4*hud45co-!+{UHrG1`5@&5cXC?EJqcezD&v=*7)CtRxMXS5M?J(n+-|M#Rpi89669q|u1E3N)(L1eJ~O zie`yM^$f2L5!3ib9b*1uD6N(&+>5Qt<5yML4`OAs>%r`QHi~tY`f~X-v`F_=K8gc( z=d+t5QrSbdY Date: Tue, 16 Jun 2026 00:29:01 +0400 Subject: [PATCH 6/7] update --- .../dataGrid/common/editing/functional.ts | 6 ++- .../keyboardNavigation.functional.ts | 15 ++++--- .../keyboardNavigation.visual.ts | 9 +++- .../chatParts/chat.tests.js | 7 ++- .../chatParts/messageList.tests.js | 33 +++++++------- .../mapParts/azureTests.js | 43 +++++++++++-------- 6 files changed, 67 insertions(+), 46 deletions(-) diff --git a/e2e/testcafe-devextreme/tests/dataGrid/common/editing/functional.ts b/e2e/testcafe-devextreme/tests/dataGrid/common/editing/functional.ts index 83f9216dab92..6c73dd387e1f 100644 --- a/e2e/testcafe-devextreme/tests/dataGrid/common/editing/functional.ts +++ b/e2e/testcafe-devextreme/tests/dataGrid/common/editing/functional.ts @@ -8,6 +8,8 @@ import { createWidget } from '../../../../helpers/createWidget'; fixture.disablePageReloads`Editing.Functional` .page(url(__dirname, '../../../container.html')); +const FOCUS_ASSERTION_TIMEOUT = 3000; + const getGridConfig = (config): Record => { const defaultConfig = { errorRowEnabled: true, @@ -67,8 +69,8 @@ test('DataGrid - The "Cannot read properties of undefined error" occurs when usi .pressKey('enter tab tab'); await resolveOnSavingDeferred(); await t - .expect(dataGrid.isReady()).ok() - .expect(dataGrid.getDataCell(2, 0).isFocused).ok(); + .expect(dataGrid.isReady()).ok({ timeout: FOCUS_ASSERTION_TIMEOUT }) + .expect(dataGrid.getDataCell(2, 0).isFocused).ok({ timeout: FOCUS_ASSERTION_TIMEOUT }); }).before(async () => { await ClientFunction(() => { (window as any).deferred = $.Deferred(); diff --git a/e2e/testcafe-devextreme/tests/dataGrid/common/keyboardNavigation/keyboardNavigation.functional.ts b/e2e/testcafe-devextreme/tests/dataGrid/common/keyboardNavigation/keyboardNavigation.functional.ts index cca663e01234..efe9b97e138b 100644 --- a/e2e/testcafe-devextreme/tests/dataGrid/common/keyboardNavigation/keyboardNavigation.functional.ts +++ b/e2e/testcafe-devextreme/tests/dataGrid/common/keyboardNavigation/keyboardNavigation.functional.ts @@ -21,6 +21,7 @@ import { testScreenshot } from '../../../../helpers/themeUtils'; import { addFocusableElementBefore } from '../../../../helpers/domUtils'; const CLASS = ClassNames; +const FOCUS_ASSERTION_TIMEOUT = 3000; const getOnKeyDownCallCount = ClientFunction(() => (window as any).onKeyDownCallCount); @@ -6801,10 +6802,12 @@ test('Focus should be set to the grid to allow keyboard navigation when the focu // act await t .click(searchPanel.input) - .pressKey('tab tab tab tab tab'); + .expect(searchPanel.isFocused) + .ok({ timeout: FOCUS_ASSERTION_TIMEOUT }) + .pressKey('tab tab tab tab tab', { speed: 0.5 }); // assert - await t.expect(secondIDCell.isFocused).ok(); + await t.expect(secondIDCell.isFocused).ok({ timeout: FOCUS_ASSERTION_TIMEOUT }); // act await searchPanel.focus(); @@ -6817,22 +6820,22 @@ test('Focus should be set to the grid to allow keyboard navigation when the focu .notOk('focus should be on the search panel'); // act - await t.pressKey('tab tab tab'); + await t.pressKey('tab tab tab', { speed: 0.5 }); // assert - await t.expect(secondIDCell.isFocused).ok(); + await t.expect(secondIDCell.isFocused).ok({ timeout: FOCUS_ASSERTION_TIMEOUT }); // act await t.pressKey('tab tab'); // assert - await t.expect(button.isFocused).ok(); + await t.expect(button.isFocused).ok({ timeout: FOCUS_ASSERTION_TIMEOUT }); // act await t.pressKey('shift+tab'); // assert - await t.expect(secondNameCell.isFocused).ok(); + await t.expect(secondNameCell.isFocused).ok({ timeout: FOCUS_ASSERTION_TIMEOUT }); }).before(async () => { await createWidget('dxDataGrid', { dataSource: [{ id: 1, name: 'test1' }, { id: 2, name: 'test2' }], diff --git a/e2e/testcafe-devextreme/tests/dataGrid/common/keyboardNavigation/keyboardNavigation.visual.ts b/e2e/testcafe-devextreme/tests/dataGrid/common/keyboardNavigation/keyboardNavigation.visual.ts index 1c7f3c1f0bb7..691549fac75a 100644 --- a/e2e/testcafe-devextreme/tests/dataGrid/common/keyboardNavigation/keyboardNavigation.visual.ts +++ b/e2e/testcafe-devextreme/tests/dataGrid/common/keyboardNavigation/keyboardNavigation.visual.ts @@ -776,9 +776,14 @@ test('Navigate to first cell in the first row when virtual scrolling and columns await expectDataCellFocusState(t, dataGrid, 199, 34); await t .expect(dataGrid.isReady()) - .ok({ timeout: KEYBOARD_NAVIGATION_TIMEOUT }) - .expect(isScrollAtEnd('horizontal')) .ok({ timeout: KEYBOARD_NAVIGATION_TIMEOUT }); + + if (!useNative) { + await t + .expect(isScrollAtEnd('horizontal')) + .ok({ timeout: KEYBOARD_NAVIGATION_TIMEOUT }); + } + await waitForPaint(); await testScreenshot(t, takeScreenshot, `${useNative ? 'native' : 'simulated'}_scrolling_-_navigate_to_last_cell_row_dragging__virtual_scrolling__virtual_columns.png`, { element: dataGrid.element }); diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets/chatParts/chat.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets/chatParts/chat.tests.js index 2ca76c5e255b..14bb3013b041 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets/chatParts/chat.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets/chatParts/chat.tests.js @@ -88,7 +88,12 @@ const waitForEditingPreviewToHide = async(getEditingPreview) => { await waitForCondition(() => getEditingPreview().length === 0 || getEditingPreview().hasClass(CHAT_EDITING_PREVIEW_HIDING_CLASS)); - getEditingPreview().get(0)?.dispatchEvent(new Event('animationend')); + const editingPreview = getEditingPreview().get(0); + if(editingPreview) { + editingPreview.dispatchEvent(new Event('animationend')); + } + + await waitForCondition(() => getEditingPreview().length === 0); }; export const MOCK_COMPANION_USER_ID = 'COMPANION_USER_ID'; diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets/chatParts/messageList.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets/chatParts/messageList.tests.js index 64498f42131f..68f17de76c1f 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets/chatParts/messageList.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets/chatParts/messageList.tests.js @@ -1647,8 +1647,7 @@ QUnit.module('MessageList', () => { }); }); - QUnit.test('should not be scroll down after render companion message if scroll position not at the bottom', function(assert) { - const done = assert.async(); + QUnit.test('should not be scroll down after render companion message if scroll position not at the bottom', async function(assert) { const items = generateMessages(52); this.reinit({ @@ -1665,24 +1664,25 @@ QUnit.module('MessageList', () => { text: 'NEW MESSAGE', }; - setTimeout(() => { - this.getScrollView().scrollBy({ top: -100 }); - setTimeout(() => { + await waitForCondition(() => Math.abs(this.getScrollView().scrollTop() - this.getScrollOffsetMax()) <= 1); - const scrollTopBefore = this.getScrollView().scrollTop(); - assert.roughEqual(scrollTopBefore, this.getScrollOffsetMax() - 100, 1, 'scroll position should not be at the bottom before rendering the message'); + const initialScrollTop = this.getScrollOffsetMax() - 100; + this.getScrollView().scrollTo({ top: initialScrollTop }); - setTimeout(() => { - this.instance.option('items', [...items, newMessage]); + await waitForCondition(() => Math.abs(this.getScrollView().scrollTop() - initialScrollTop) <= 1); - const scrollTop = this.getScrollView().scrollTop(); + const scrollTopBefore = this.getScrollView().scrollTop(); + assert.roughEqual(scrollTopBefore, initialScrollTop, 1, 'scroll position should not be at the bottom before rendering the message'); - assert.notEqual(scrollTop, 0, 'scroll position should not be 0 after a new message is rendered'); - assert.roughEqual(scrollTop, scrollTopBefore, 1, 'scroll position should be at the bottom after rendering the new message'); - done(); - }, this._resizeTimeout); - }, this._resizeTimeout); - }, this._resizeTimeout); + this.instance.option('items', [...items, newMessage]); + + await waitForCondition(() => this.getBubbles().length === items.length + 1 + && Math.abs(this.getScrollView().scrollTop() - scrollTopBefore) <= 1); + + const scrollTop = this.getScrollView().scrollTop(); + + assert.notEqual(scrollTop, 0, 'scroll position should not be 0 after a new message is rendered'); + assert.roughEqual(scrollTop, scrollTopBefore, 1, 'scroll position should remain the same after rendering the new message'); }); QUnit.test('should be scrolled down after showing if was initially rendered inside an invisible element', function(assert) { @@ -1977,4 +1977,3 @@ QUnit.module('MessageList', () => { }); }); - diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets/mapParts/azureTests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets/mapParts/azureTests.js index c77c9e9d83bd..468c1cfa1706 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets/mapParts/azureTests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets/mapParts/azureTests.js @@ -30,6 +30,13 @@ const prepareTestingAzureProvider = () => { atlas.popupOpened = false; }; +const loadAzureMock = () => $.getScript({ + url: '../../packages/devextreme/testing/helpers/forMap/azureMock.js', + scriptAttrs: { nonce: 'qunit-test' }, +}).done(() => { + prepareTestingAzureProvider(); +}); + const moduleConfig = { beforeEach: function() { const fakeURL = '/fakeAzureUrl'; @@ -43,12 +50,7 @@ const moduleConfig = { if(!azureMockCreated) { azureMockCreated = true; - $.getScript({ - url: '../../packages/devextreme/testing/helpers/forMap/azureMock.js', - scriptAttrs: { nonce: 'qunit-test' }, - }).done(() => { - prepareTestingAzureProvider(); - }); + loadAzureMock(); } }, responseText: { @@ -86,10 +88,7 @@ QUnit.module('map loading', moduleConfig, () => { QUnit.test('map initialize with loaded map', function(assert) { const done = assert.async(); - $.getScript({ - url: '../../packages/devextreme/testing/helpers/forMap/azureMock.js', - scriptAttrs: { nonce: 'qunit-test' } - }).done(function() { + loadAzureMock().done(function() { window.atlas.Map.customFlag = true; setTimeout(function() { @@ -308,15 +307,23 @@ QUnit.module('basic options', moduleConfig, () => { const done = assert.async(); const center = 'Cedar Park, Texas'; - $('#map').dxMap({ - provider: 'azure', - center, - onReady: () => { - assert.deepEqual(atlas.cameraOptions.center, this.geocodedCoordinates, 'center coordinates are correct'); + const initMap = () => { + $('#map').dxMap({ + provider: 'azure', + center, + onReady: () => { + assert.deepEqual(atlas.cameraOptions.center, this.geocodedCoordinates, 'center coordinates are correct'); - done(); - } - }); + done(); + } + }); + }; + + if(window.atlas && window.atlas.Map) { + initMap(); + } else { + loadAzureMock().done(initMap); + } }); QUnit.test('Previously geocoded location should be taken from cache instead of geocoding second time', function(assert) { From 2928ce2671d72389180ae951cadfc026f0fcc3de Mon Sep 17 00:00:00 2001 From: EugeniyKiyashko Date: Tue, 16 Jun 2026 10:23:17 +0400 Subject: [PATCH 7/7] skip 3 tests --- .../common/keyboardNavigation/keyboardNavigation.visual.ts | 2 +- .../tests/dataGrid/common/rowDragging/functional.ts | 2 +- e2e/testcafe-devextreme/tests/dataGrid/common/scrolling.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/e2e/testcafe-devextreme/tests/dataGrid/common/keyboardNavigation/keyboardNavigation.visual.ts b/e2e/testcafe-devextreme/tests/dataGrid/common/keyboardNavigation/keyboardNavigation.visual.ts index 691549fac75a..cdb56b5780e9 100644 --- a/e2e/testcafe-devextreme/tests/dataGrid/common/keyboardNavigation/keyboardNavigation.visual.ts +++ b/e2e/testcafe-devextreme/tests/dataGrid/common/keyboardNavigation/keyboardNavigation.visual.ts @@ -595,7 +595,7 @@ test('Navigate to last cell in the last row when virtual scrolling is enabled', }, })); -test('Navigate to first cell in the first row when virtual scrolling is enabled', async (t) => { +test.meta({ unstable: true })('Navigate to first cell in the first row when virtual scrolling is enabled', async (t) => { // arrange const dataGrid = new DataGrid('#container'); const { takeScreenshot, compareResults } = createScreenshotsComparer(t); diff --git a/e2e/testcafe-devextreme/tests/dataGrid/common/rowDragging/functional.ts b/e2e/testcafe-devextreme/tests/dataGrid/common/rowDragging/functional.ts index 75f8fa44e57e..4c1067e1de0a 100644 --- a/e2e/testcafe-devextreme/tests/dataGrid/common/rowDragging/functional.ts +++ b/e2e/testcafe-devextreme/tests/dataGrid/common/rowDragging/functional.ts @@ -794,7 +794,7 @@ test('toIndex should not be corrected when source item gets removed from DOM', a }); // T1139685 -test('Item should appear in a correct spot when dragging to a different page with scrolling.mode: "virtual"', async (t) => { +test.meta({ unstable: true })('Item should appear in a correct spot when dragging to a different page with scrolling.mode: "virtual"', async (t) => { const dataGrid = new DataGrid('#container'); await t.expect(dataGrid.isReady()).ok(); diff --git a/e2e/testcafe-devextreme/tests/dataGrid/common/scrolling.ts b/e2e/testcafe-devextreme/tests/dataGrid/common/scrolling.ts index d750dd4b380d..2032d50c027d 100644 --- a/e2e/testcafe-devextreme/tests/dataGrid/common/scrolling.ts +++ b/e2e/testcafe-devextreme/tests/dataGrid/common/scrolling.ts @@ -1085,7 +1085,7 @@ test('The data should display correctly after changing the dataSource and focuse })); // T1166649 -test.meta({ browserSize: [800, 800] })('The scroll position of a fixed table should be synchronized with the main table when fast scrolling to the end', async (t) => { +test.meta({ unstable: true, browserSize: [800, 800] })('The scroll position of a fixed table should be synchronized with the main table when fast scrolling to the end', async (t) => { // arrange const dataGrid = new DataGrid('#container'); const { takeScreenshot, compareResults } = createScreenshotsComparer(t);