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 871fe7237300..3215b3485d70 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 ( @@ -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 ee2616dc2f39..560ba3201f1e 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, @@ -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/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 bcdd5b04266f..d077fb1d4add 100644 --- a/e2e/testcafe-devextreme/tests/cardView/columnSortable/functional.ts +++ b/e2e/testcafe-devextreme/tests/cardView/columnSortable/functional.ts @@ -315,16 +315,12 @@ 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'); + 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 9a3f5bd57824..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 ( @@ -124,7 +139,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 +152,10 @@ export const expectColumns = async ( column = treeView.getNodeItem(i); } - if (await column?.exists) { - actualColumns.push(await column.innerText); - } + await t + .expect(column.exists) + .ok({ timeout: DRAG_ASSERTION_TIMEOUT }) + .expect(column.innerText) + .eql(adjustedExpectedColumns[i], { timeout: DRAG_ASSERTION_TIMEOUT }); } - - const adjustedExpectedColumns = expectedColumns.map((columnIndex) => `Column ${columnIndex}`); - - await t.expect(actualColumns).eql(adjustedExpectedColumns); }; 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/editing/functional.ts b/e2e/testcafe-devextreme/tests/dataGrid/common/editing/functional.ts index a8515054c98b..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, @@ -66,7 +68,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({ 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/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/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 9671329a19fa..1748e4572333 100644 Binary files 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 and 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 differ 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 90291c906397..09df34eb3aa6 100644 Binary files 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 and 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 differ 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 22d091b9b903..000000000000 Binary files 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 and /dev/null differ 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 29f837bd4ad7..cdb56b5780e9 100644 --- a/e2e/testcafe-devextreme/tests/dataGrid/common/keyboardNavigation/keyboardNavigation.visual.ts +++ b/e2e/testcafe-devextreme/tests/dataGrid/common/keyboardNavigation/keyboardNavigation.visual.ts @@ -1,13 +1,62 @@ 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'; +import { isScrollAtEnd } from '../../helpers/rowDraggingHelpers'; 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 waitForPaint = ClientFunction(() => new Promise((resolve) => { + requestAnimationFrame(() => { + requestAnimationFrame(() => resolve()); + }); +})); + +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,11 +571,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 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 @@ -546,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); @@ -565,11 +614,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 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 @@ -598,15 +647,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 }); @@ -643,15 +689,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 }); @@ -680,28 +723,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).element.focused) - .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).element.focused) - .ok() .expect(compareResults.isValid()) .ok(compareResults.errorMessages()); }).before(async () => createWidget('dxDataGrid', { @@ -729,10 +769,22 @@ test('Navigate to first cell in the first row when virtual scrolling and columns .ok(); // act + await focusDataCell(t, dataGrid, 0, 1); + await t.pressKey('ctrl+end'); + await waitForKeyboardNavigation(t); + + await expectDataCellFocusState(t, dataGrid, 199, 34); await t - .click(dataGrid.getDataCell(0, 0).element) - .pressKey('ctrl+end') - .wait(1000); + .expect(dataGrid.isReady()) + .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/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..4c1067e1de0a 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,37 +777,63 @@ 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, }); }); // 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(); - await t.drag(dataGrid.getDataRow(2).getDragCommand(), 0, 32, { speed: 0.95 }); + const rowHeight = await dataGrid.getDataRow(2).element.offsetHeight; + const scrollOffsetForAutoScroll = await getOffsetToTriggerAutoScroll(2, 0.5, 'down'); + + await dataGrid.moveRow(2, 0, rowHeight, true); + await dataGrid.moveRow(2, 0, scrollOffsetForAutoScroll); - const visibleRows = await dataGrid.apiGetVisibleRows(); - const visibleRowKeys = visibleRows.map((row) => row.key); const expectedSequence = ['5-1', '3-1', '6-1']; + const isTargetPageRendered = ClientFunction(() => { + const visibleRowKeys = (($('#container') as any) + .dxDataGrid('instance') + .getVisibleRows() as any[]) + .map((row: any) => row.key); - const startIndex = visibleRowKeys.findIndex( - (_, i) => expectedSequence.every((val, j) => visibleRowKeys[i + j] === val), - ); + 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) => dataSourceKeys[i + j] === val, + ), + ); + }, { dependencies: { expectedSequence } }); - await t.expect(startIndex).gte(0); + await t + .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', { @@ -824,16 +849,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 8dae14cdbc05..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); @@ -1094,8 +1094,54 @@ 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 + .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); + const text = rows.map((row) => row.textContent).join(' '); + + return text.includes('998') + && text.includes('item 998') + && Math.max(...tops) - Math.min(...tops) < 1; + }); + + 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/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..328e7b940cf4 100644 --- a/e2e/testcafe-devextreme/tests/editors/dateRangeBox/focus.ts +++ b/e2e/testcafe-devextreme/tests/editors/dateRangeBox/focus.ts @@ -2,12 +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, 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) @@ -27,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'); @@ -260,7 +285,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 +296,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/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/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/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 26f0e400d2fa..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'); }); }); @@ -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(); } 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.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 023eb4e58434..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 @@ -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'; @@ -70,6 +71,31 @@ 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); +}); + +// 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)); + + 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'; export const MOCK_CURRENT_USER_ID = 'CURRENT_USER_ID'; export const NOW = 1721747399083; @@ -1004,9 +1030,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 +1053,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 +1084,30 @@ 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); + 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); + + assert.strictEqual(this.getEditingPreview().length, 1, `Editing preview remains when cancel=${cancel}`); + } else { + await waitForEditingPreviewToHide(() => this.getEditingPreview()); + + assert.strictEqual(this.getEditingPreview().length, 0, `Editing preview 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 +1130,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 +1163,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 +1176,19 @@ 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 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.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 +1213,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 +1247,25 @@ QUnit.module('Chat', () => { const $editButton = this.getContextMenuItems().eq(0); $editButton.trigger('dxclick'); + // Make sure the editing mode is established before sending. + await waitForCondition(() => this.getEditingPreview().length === 1); + 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); + // 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'); + 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 +1286,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 +1316,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 +1343,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 +1363,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 +1389,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..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 @@ -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') => { @@ -1626,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({ @@ -1644,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) { @@ -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(); + 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.instance.option('height', 300); + this.instance.option('height', 300); - setTimeout(() => { - const scrollTop = this.getScrollView().scrollTop(); + await waitForCondition(() => Math.abs(this.getScrollView().scrollTop() - this.getScrollOffsetMax()) <= 1); - assert.roughEqual(scrollTop, this.getScrollOffsetMax(), 1, 'max scroll position should be saved after reducing height'); - - 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'); + assert.roughEqual(this.getScrollView().scrollTop(), this.getScrollOffsetMax(), 1, 'scroll position should be at the bottom after initialization'); - this.instance.option('height', 700); + this.instance.option('height', 700); - setTimeout(() => { - const scrollTop = this.getScrollView().scrollTop(); + await waitForCondition(() => Math.abs(this.getScrollView().scrollTop() - this.getScrollOffsetMax()) <= 1); - assert.roughEqual(scrollTop, this.getScrollOffsetMax(), 1, 'max scroll position should be saved after increasing height'); - - 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(); - - 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.getScrollView().scrollTo({ top: this.getScrollOffsetMax() - 200 }); - 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.getScrollView().scrollTo({ top: this.getScrollOffsetMax() - 200 }); + this.instance.option('height', 300); - 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(); + 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'); - const newScrollTop = this.getScrollOffsetMax() - 200; - this.getScrollView().scrollTo({ top: newScrollTop }); - this.instance.option('height', 600); - - 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) { @@ -1986,4 +1977,3 @@ QUnit.module('MessageList', () => { }); }); - 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'; 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) {