From dc1c553d76d83e1c395842e11e9a3e056a0fe7f2 Mon Sep 17 00:00:00 2001 From: Alyar <> Date: Wed, 3 Jun 2026 02:27:32 +0400 Subject: [PATCH 1/9] DataGrid - AI Assistant: toolbar & popup e2e tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds e2e TestCafe tests for the DataGrid AI Assistant toolbar button and popup lifecycle (plan §1.1, §1.10): button visibility, popup open, grid state preserved on close, custom title and toolbar keyboard a11y. Includes the shared testHelpers and AI Assistant POM accessors the suite depends on. Co-Authored-By: Claude Opus 4.8 --- .../common/aiAssistant/testHelpers.ts | 16 ++ .../aiAssistant/toolbarAndPopup.functional.ts | 230 ++++++++++++++++++ packages/testcafe-models/chat.ts | 6 +- .../dataGrid/aiAssistantChat.ts | 67 ++++- packages/testcafe-models/dataGrid/index.ts | 20 ++ 5 files changed, 328 insertions(+), 11 deletions(-) create mode 100644 e2e/testcafe-devextreme/tests/dataGrid/common/aiAssistant/testHelpers.ts create mode 100644 e2e/testcafe-devextreme/tests/dataGrid/common/aiAssistant/toolbarAndPopup.functional.ts diff --git a/e2e/testcafe-devextreme/tests/dataGrid/common/aiAssistant/testHelpers.ts b/e2e/testcafe-devextreme/tests/dataGrid/common/aiAssistant/testHelpers.ts new file mode 100644 index 000000000000..45c66d4f7edb --- /dev/null +++ b/e2e/testcafe-devextreme/tests/dataGrid/common/aiAssistant/testHelpers.ts @@ -0,0 +1,16 @@ +/* eslint-disable no-underscore-dangle */ +import { ClientFunction } from 'testcafe'; +import url from '../../../../helpers/getPageUrl'; + +export const GRID_SELECTOR = '#container'; + +export const AI_INTEGRATION_PAGE = url(__dirname, '../../../container-ai-integration.html'); + +export const getRequestColumnNames = ClientFunction( + (index: number) => (window as any).__aiRequests[index].data.context.columns + .map((c: any) => c.dataField), +); + +export const formatMessage = ClientFunction( + (key: string) => (window as any).DevExpress.localization.formatMessage(key), +); diff --git a/e2e/testcafe-devextreme/tests/dataGrid/common/aiAssistant/toolbarAndPopup.functional.ts b/e2e/testcafe-devextreme/tests/dataGrid/common/aiAssistant/toolbarAndPopup.functional.ts new file mode 100644 index 000000000000..74bd9324dbb8 --- /dev/null +++ b/e2e/testcafe-devextreme/tests/dataGrid/common/aiAssistant/toolbarAndPopup.functional.ts @@ -0,0 +1,230 @@ +import DataGrid from 'devextreme-testcafe-models/dataGrid'; +import { createWidget } from '../../../../helpers/createWidget'; +import { AI_INTEGRATION_PAGE, GRID_SELECTOR } from './testHelpers'; + +// === §1.1 Toolbar entry point & popup lifecycle === +fixture.disablePageReloads`AI Assistant - Toolbar` + .page(AI_INTEGRATION_PAGE); + +// 1.1.1 +test('Toolbar button should be visible when aiAssistant.enabled is true', async (t) => { + const dataGrid = new DataGrid(GRID_SELECTOR); + + await t.expect(dataGrid.isReady()).ok(); + + await t.expect(dataGrid.getAIAssistantButton().exists).ok(); +}).before(async () => createWidget('dxDataGrid', () => ({ + dataSource: [ + { id: 1, name: 'Alice', value: 30 }, + { id: 2, name: 'Bob', value: 20 }, + { id: 3, name: 'Charlie', value: 10 }, + ], + keyExpr: 'id', + columns: ['id', 'name', 'value'], + showBorders: true, + aiAssistant: { + enabled: true, + aiIntegration: new (window as any).DevExpress.aiIntegration.AIIntegration({ + sendRequest() { + return { promise: new Promise(() => {}), abort: (): void => {} }; + }, + }), + }, +}))); + +// 1.1.2 +test('Toolbar button should be hidden when aiAssistant is not configured', async (t) => { + const dataGrid = new DataGrid(GRID_SELECTOR); + + await t.expect(dataGrid.isReady()).ok(); + + await t.expect(dataGrid.getAIAssistantButton().exists).notOk(); +}).before(async () => createWidget('dxDataGrid', () => ({ + dataSource: [ + { id: 1, name: 'Alice', value: 30 }, + { id: 2, name: 'Bob', value: 20 }, + { id: 3, name: 'Charlie', value: 10 }, + ], + keyExpr: 'id', + columns: ['id', 'name', 'value'], + showBorders: true, +}))); + +// 1.1.3 +test('Popup should open on toolbar button click', async (t) => { + const dataGrid = new DataGrid(GRID_SELECTOR); + + await t.expect(dataGrid.isReady()).ok(); + + await t.click(dataGrid.getAIAssistantButton()); + + const aiChat = dataGrid.getAIAssistantChat(); + + await t + .expect(aiChat.element.visible).ok() + .expect(aiChat.getChat().element.exists).ok(); +}).before(async () => createWidget('dxDataGrid', () => ({ + dataSource: [ + { id: 1, name: 'Alice', value: 30 }, + { id: 2, name: 'Bob', value: 20 }, + { id: 3, name: 'Charlie', value: 10 }, + ], + keyExpr: 'id', + columns: ['id', 'name', 'value'], + showBorders: true, + aiAssistant: { + enabled: true, + aiIntegration: new (window as any).DevExpress.aiIntegration.AIIntegration({ + sendRequest() { + return { promise: new Promise(() => {}), abort: (): void => {} }; + }, + }), + }, +}))); + +// 1.1.4 +test('Grid state should be preserved after popup close', async (t) => { + const dataGrid = new DataGrid(GRID_SELECTOR); + + await t.expect(dataGrid.isReady()).ok(); + + await t.click(dataGrid.getAIAssistantButton()); + + const aiChat = dataGrid.getAIAssistantChat(); + + await t + .typeText(aiChat.getInput(), 'Sort by name') + .pressKey('enter'); + + await t.expect(aiChat.getSuccessMessages().count).eql(1); + await t.expect(aiChat.getActionItems(0).count).eql(1); + + await t.click(aiChat.getCloseButton().element); + + const sortOrder = await dataGrid.apiColumnOption('name', 'sortOrder'); + + await t.expect(sortOrder).eql('asc'); +}).before(async () => createWidget('dxDataGrid', () => ({ + dataSource: [ + { id: 1, name: 'Alice', value: 30 }, + { id: 2, name: 'Bob', value: 20 }, + { id: 3, name: 'Charlie', value: 10 }, + ], + keyExpr: 'id', + columns: ['id', 'name', 'value'], + showBorders: true, + aiAssistant: { + enabled: true, + aiIntegration: new (window as any).DevExpress.aiIntegration.AIIntegration({ + sendRequest() { + return { + promise: Promise.resolve({ + actions: [{ name: 'sorting', args: { dataField: 'name', sortOrder: 'asc' } }], + }), + abort: (): void => {}, + }; + }, + }), + }, +}))); + +// 1.1.6 +test('Custom title should be rendered in popup header', async (t) => { + const dataGrid = new DataGrid(GRID_SELECTOR); + + await t.expect(dataGrid.isReady()).ok(); + + await t.click(dataGrid.getAIAssistantButton()); + + const aiChat = dataGrid.getAIAssistantChat(); + + await t.expect(aiChat.getTitle().textContent).contains('My Custom Assistant'); +}).before(async () => createWidget('dxDataGrid', () => ({ + dataSource: [ + { id: 1, name: 'Alice', value: 30 }, + { id: 2, name: 'Bob', value: 20 }, + { id: 3, name: 'Charlie', value: 10 }, + ], + keyExpr: 'id', + columns: ['id', 'name', 'value'], + showBorders: true, + aiAssistant: { + enabled: true, + title: 'My Custom Assistant', + aiIntegration: new (window as any).DevExpress.aiIntegration.AIIntegration({ + sendRequest() { + return { promise: new Promise(() => {}), abort: (): void => {} }; + }, + }), + }, +}))); + +// === §1.10 A11y / KBN === + +fixture.disablePageReloads`AI Assistant - A11y` + .page(AI_INTEGRATION_PAGE); + +// 1.10.1 +test('Toolbar button should be accessible and clickable', async (t) => { + const dataGrid = new DataGrid(GRID_SELECTOR); + + await t.expect(dataGrid.isReady()).ok(); + + const button = dataGrid.getAIAssistantButton(); + + await t.expect(button.exists).ok(); + + await t.click(button); + + await t.expect(dataGrid.getAIAssistantChat().element.visible).ok(); +}).before(async () => createWidget('dxDataGrid', () => ({ + dataSource: [ + { id: 1, name: 'Alice', value: 30 }, + { id: 2, name: 'Bob', value: 20 }, + { id: 3, name: 'Charlie', value: 10 }, + ], + keyExpr: 'id', + columns: ['id', 'name', 'value'], + showBorders: true, + aiAssistant: { + enabled: true, + aiIntegration: new (window as any).DevExpress.aiIntegration.AIIntegration({ + sendRequest() { + return { promise: new Promise(() => {}), abort: (): void => {} }; + }, + }), + }, +}))); + +// 1.10.2 +test('Toolbar button should activate via Enter key', async (t) => { + const dataGrid = new DataGrid(GRID_SELECTOR); + + await t.expect(dataGrid.isReady()).ok(); + + const button = dataGrid.getAIAssistantButton(); + + await t.expect(button.exists).ok(); + + await dataGrid.focusAIAssistantButton(); + await t.pressKey('enter'); + + await t.expect(dataGrid.getAIAssistantChat().element.visible).ok(); +}).before(async () => createWidget('dxDataGrid', () => ({ + dataSource: [ + { id: 1, name: 'Alice', value: 30 }, + { id: 2, name: 'Bob', value: 20 }, + { id: 3, name: 'Charlie', value: 10 }, + ], + keyExpr: 'id', + columns: ['id', 'name', 'value'], + showBorders: true, + aiAssistant: { + enabled: true, + aiIntegration: new (window as any).DevExpress.aiIntegration.AIIntegration({ + sendRequest() { + return { promise: new Promise(() => {}), abort: (): void => {} }; + }, + }), + }, +}))); diff --git a/packages/testcafe-models/chat.ts b/packages/testcafe-models/chat.ts index d5cf8baf8365..4e1cf2ed543a 100644 --- a/packages/testcafe-models/chat.ts +++ b/packages/testcafe-models/chat.ts @@ -43,8 +43,12 @@ export default class Chat extends Widget { return new Scrollable(this.element.find(`.${CLASS.scrollable}`)); } + getMessageBubbles(): Selector { + return this.element.find(`.${CLASS.messageBubble}`); + } + getMessage(index: number): Selector { - return this.element.find(`.${CLASS.messageBubble}`).nth(index); + return this.getMessageBubbles().nth(index); } getContextMenuContent(): Selector { diff --git a/packages/testcafe-models/dataGrid/aiAssistantChat.ts b/packages/testcafe-models/dataGrid/aiAssistantChat.ts index fe9d3621ac62..623cf0336434 100644 --- a/packages/testcafe-models/dataGrid/aiAssistantChat.ts +++ b/packages/testcafe-models/dataGrid/aiAssistantChat.ts @@ -6,6 +6,7 @@ import Chat from '../chat'; const CLASS = { aiChat: 'dx-ai-chat', aiChatContent: 'dx-ai-chat__content', + abortConfirmDialog: 'dx-datagrid-ai-assistant-confirm-dialog', message: 'dx-ai-chat__message', messagePending: 'dx-ai-chat__message--pending', messageSuccess: 'dx-ai-chat__message--success', @@ -26,6 +27,8 @@ const CLASS = { actionListItemText: 'dx-ai-chat__action-list-item-text', closeButton: 'dx-closebutton', clearChatButton: 'dx-icon-clearhistory', + suggestion: 'dx-chat-suggestions', + suggestionButton: 'dx-button', }; export class AIAssistantChat extends Popup { @@ -37,15 +40,37 @@ export class AIAssistantChat extends Popup { return new Chat(this.element.find(`.${CLASS.aiChatContent}`)); } + getInput(): Selector { + return this.getChat().getInput(); + } + getCloseButton(): Button { return new Button(this.element.find(`.${CLASS.closeButton}`)); } + // eslint-disable-next-line class-methods-use-this + getAbortConfirmDialog(): Selector { + return Selector(`.${CLASS.abortConfirmDialog}`); + } + + // eslint-disable-next-line class-methods-use-this + getAbortConfirmYesButton(): Selector { + return Selector(`.${CLASS.abortConfirmDialog} .dx-button`).withExactText('Yes'); + } + getClearChatButton(): Selector { return this.element.find(`.${CLASS.clearChatButton}`); } getMessages(): Selector { + return this.getChat().getMessageBubbles(); + } + + getUserMessages(): Selector { + return this.getMessages().filter((node) => !node.querySelector('.dx-ai-chat__message')); + } + + getAIMessages(): Selector { return this.element.find(`.${CLASS.message}`); } @@ -61,40 +86,40 @@ export class AIAssistantChat extends Popup { return this.element.find(`.${CLASS.messageError}`); } - getMessage(index: number): Selector { - return this.getMessages().nth(index); + getAIMessage(index: number): Selector { + return this.getAIMessages().nth(index); } getMessageHeader(index: number): Selector { - return this.getMessage(index).find(`.${CLASS.messageHeader}`); + return this.getAIMessage(index).find(`.${CLASS.messageHeader}`); } getMessageErrorText(index: number): Selector { - return this.getMessage(index).find(`.${CLASS.messageErrorText}`); + return this.getAIMessage(index).find(`.${CLASS.messageErrorText}`); } getMessageProgressBar(index: number): Selector { - return this.getMessage(index).find(`.${CLASS.messageProgressBar}`); + return this.getAIMessage(index).find(`.${CLASS.messageProgressBar}`); } getMessageRegenerateButton(index: number): Selector { - return this.getMessage(index).find(`.${CLASS.messageRegenerateButton}`); + return this.getAIMessage(index).find(`.${CLASS.messageRegenerateButton}`); } getActionList(messageIndex: number): Selector { - return this.getMessage(messageIndex).find(`.${CLASS.actionList}`); + return this.getAIMessage(messageIndex).find(`.${CLASS.actionList}`); } getActionItems(messageIndex: number): Selector { - return this.getMessage(messageIndex).find(`.${CLASS.actionListItem}`); + return this.getAIMessage(messageIndex).find(`.${CLASS.actionListItem}`); } getSuccessActionItems(messageIndex: number): Selector { - return this.getMessage(messageIndex).find(`.${CLASS.actionListItemSuccess}`); + return this.getAIMessage(messageIndex).find(`.${CLASS.actionListItemSuccess}`); } getErrorActionItems(messageIndex: number): Selector { - return this.getMessage(messageIndex).find(`.${CLASS.actionListItemError}`); + return this.getAIMessage(messageIndex).find(`.${CLASS.actionListItemError}`); } getActionItemText(messageIndex: number, actionIndex: number): Selector { @@ -104,4 +129,26 @@ export class AIAssistantChat extends Popup { getActionItemIcon(messageIndex: number, actionIndex: number): Selector { return this.getActionItems(messageIndex).nth(actionIndex).find(`.${CLASS.actionListItemIcon}`); } + + getSuggestions(): Selector { + return this.element.find(`.${CLASS.suggestion} .${CLASS.suggestionButton}`); + } + + isInputDisabled(): Promise { + return this.getChat().getTextArea().isDisabled; + } + + isClearChatDisabled(): Promise { + return this.getClearChatButton() + .parent('.dx-button') + .hasClass('dx-state-disabled'); + } + + isSuggestionDisabled(index: number): Promise { + return this.getSuggestions().nth(index).hasClass('dx-state-disabled'); + } + + getTitle(): Selector { + return this.topToolbar; + } } diff --git a/packages/testcafe-models/dataGrid/index.ts b/packages/testcafe-models/dataGrid/index.ts index 097e5e921b15..99aec7ee5f3d 100644 --- a/packages/testcafe-models/dataGrid/index.ts +++ b/packages/testcafe-models/dataGrid/index.ts @@ -777,6 +777,15 @@ export default class DataGrid extends GridCore { )(); } + apiGetDataSourceSortParams(): Promise { + const { getInstance } = this; + + return ClientFunction( + () => (getInstance() as DataGridInstance).getDataSource().sort(), + { dependencies: { getInstance } }, + )(); + } + moveRow(rowIndex: number, x: number, y: number, isStart = false): Promise { const { getInstance } = this; @@ -1045,4 +1054,15 @@ export default class DataGrid extends GridCore { getAIAssistantButton(): Selector { return this.getHeaderPanel().element.find(`.${this.addWidgetPrefix(CLASS.aiAssistantButton)}`); } + + focusAIAssistantButton(): Promise { + const buttonSelector = this.getAIAssistantButton(); + + return ClientFunction( + () => { + (buttonSelector() as unknown as HTMLElement)?.focus(); + }, + { dependencies: { buttonSelector } }, + )(); + } } From fc61a50d4eb4e5bb00db8386bfacd770d8663cc2 Mon Sep 17 00:00:00 2001 From: Alyar <> Date: Wed, 3 Jun 2026 04:59:56 +0400 Subject: [PATCH 2/9] DataGrid - AI Assistant: await aiIntegration global before createWidget Fixes intermittent CI failures (`Cannot read properties of undefined (reading 'AIIntegration')`): the `dx.ai-integration.js` global can be defined a beat after navigation, so each `before` hook now awaits `aiIntegrationReady` before instantiating the mocked AIIntegration. Co-Authored-By: Claude Opus 4.8 --- .../common/aiAssistant/testHelpers.ts | 6 + .../aiAssistant/toolbarAndPopup.functional.ts | 278 ++++++++++-------- 2 files changed, 159 insertions(+), 125 deletions(-) diff --git a/e2e/testcafe-devextreme/tests/dataGrid/common/aiAssistant/testHelpers.ts b/e2e/testcafe-devextreme/tests/dataGrid/common/aiAssistant/testHelpers.ts index 45c66d4f7edb..051313fb3d43 100644 --- a/e2e/testcafe-devextreme/tests/dataGrid/common/aiAssistant/testHelpers.ts +++ b/e2e/testcafe-devextreme/tests/dataGrid/common/aiAssistant/testHelpers.ts @@ -14,3 +14,9 @@ export const getRequestColumnNames = ClientFunction( export const formatMessage = ClientFunction( (key: string) => (window as any).DevExpress.localization.formatMessage(key), ); + +// `dx.ai-integration.js` is loaded by the container page; in CI the global can be defined a beat +// after navigation, so tests await this in their `before` hook before instantiating AIIntegration. +export const aiIntegrationReady = ClientFunction( + () => typeof (window as any).DevExpress?.aiIntegration?.AIIntegration === 'function', +); diff --git a/e2e/testcafe-devextreme/tests/dataGrid/common/aiAssistant/toolbarAndPopup.functional.ts b/e2e/testcafe-devextreme/tests/dataGrid/common/aiAssistant/toolbarAndPopup.functional.ts index 74bd9324dbb8..18406b7db0d1 100644 --- a/e2e/testcafe-devextreme/tests/dataGrid/common/aiAssistant/toolbarAndPopup.functional.ts +++ b/e2e/testcafe-devextreme/tests/dataGrid/common/aiAssistant/toolbarAndPopup.functional.ts @@ -1,6 +1,6 @@ import DataGrid from 'devextreme-testcafe-models/dataGrid'; import { createWidget } from '../../../../helpers/createWidget'; -import { AI_INTEGRATION_PAGE, GRID_SELECTOR } from './testHelpers'; +import { aiIntegrationReady, AI_INTEGRATION_PAGE, GRID_SELECTOR } from './testHelpers'; // === §1.1 Toolbar entry point & popup lifecycle === fixture.disablePageReloads`AI Assistant - Toolbar` @@ -13,24 +13,28 @@ test('Toolbar button should be visible when aiAssistant.enabled is true', async await t.expect(dataGrid.isReady()).ok(); await t.expect(dataGrid.getAIAssistantButton().exists).ok(); -}).before(async () => createWidget('dxDataGrid', () => ({ - dataSource: [ - { id: 1, name: 'Alice', value: 30 }, - { id: 2, name: 'Bob', value: 20 }, - { id: 3, name: 'Charlie', value: 10 }, - ], - keyExpr: 'id', - columns: ['id', 'name', 'value'], - showBorders: true, - aiAssistant: { - enabled: true, - aiIntegration: new (window as any).DevExpress.aiIntegration.AIIntegration({ - sendRequest() { - return { promise: new Promise(() => {}), abort: (): void => {} }; - }, - }), - }, -}))); +}).before(async (t) => { + await t.expect(aiIntegrationReady()).ok(); + + await createWidget('dxDataGrid', () => ({ + dataSource: [ + { id: 1, name: 'Alice', value: 30 }, + { id: 2, name: 'Bob', value: 20 }, + { id: 3, name: 'Charlie', value: 10 }, + ], + keyExpr: 'id', + columns: ['id', 'name', 'value'], + showBorders: true, + aiAssistant: { + enabled: true, + aiIntegration: new (window as any).DevExpress.aiIntegration.AIIntegration({ + sendRequest() { + return { promise: new Promise(() => {}), abort: (): void => {} }; + }, + }), + }, + })); +}); // 1.1.2 test('Toolbar button should be hidden when aiAssistant is not configured', async (t) => { @@ -39,16 +43,20 @@ test('Toolbar button should be hidden when aiAssistant is not configured', async await t.expect(dataGrid.isReady()).ok(); await t.expect(dataGrid.getAIAssistantButton().exists).notOk(); -}).before(async () => createWidget('dxDataGrid', () => ({ - dataSource: [ - { id: 1, name: 'Alice', value: 30 }, - { id: 2, name: 'Bob', value: 20 }, - { id: 3, name: 'Charlie', value: 10 }, - ], - keyExpr: 'id', - columns: ['id', 'name', 'value'], - showBorders: true, -}))); +}).before(async (t) => { + await t.expect(aiIntegrationReady()).ok(); + + await createWidget('dxDataGrid', () => ({ + dataSource: [ + { id: 1, name: 'Alice', value: 30 }, + { id: 2, name: 'Bob', value: 20 }, + { id: 3, name: 'Charlie', value: 10 }, + ], + keyExpr: 'id', + columns: ['id', 'name', 'value'], + showBorders: true, + })); +}); // 1.1.3 test('Popup should open on toolbar button click', async (t) => { @@ -63,24 +71,28 @@ test('Popup should open on toolbar button click', async (t) => { await t .expect(aiChat.element.visible).ok() .expect(aiChat.getChat().element.exists).ok(); -}).before(async () => createWidget('dxDataGrid', () => ({ - dataSource: [ - { id: 1, name: 'Alice', value: 30 }, - { id: 2, name: 'Bob', value: 20 }, - { id: 3, name: 'Charlie', value: 10 }, - ], - keyExpr: 'id', - columns: ['id', 'name', 'value'], - showBorders: true, - aiAssistant: { - enabled: true, - aiIntegration: new (window as any).DevExpress.aiIntegration.AIIntegration({ - sendRequest() { - return { promise: new Promise(() => {}), abort: (): void => {} }; - }, - }), - }, -}))); +}).before(async (t) => { + await t.expect(aiIntegrationReady()).ok(); + + await createWidget('dxDataGrid', () => ({ + dataSource: [ + { id: 1, name: 'Alice', value: 30 }, + { id: 2, name: 'Bob', value: 20 }, + { id: 3, name: 'Charlie', value: 10 }, + ], + keyExpr: 'id', + columns: ['id', 'name', 'value'], + showBorders: true, + aiAssistant: { + enabled: true, + aiIntegration: new (window as any).DevExpress.aiIntegration.AIIntegration({ + sendRequest() { + return { promise: new Promise(() => {}), abort: (): void => {} }; + }, + }), + }, + })); +}); // 1.1.4 test('Grid state should be preserved after popup close', async (t) => { @@ -104,29 +116,33 @@ test('Grid state should be preserved after popup close', async (t) => { const sortOrder = await dataGrid.apiColumnOption('name', 'sortOrder'); await t.expect(sortOrder).eql('asc'); -}).before(async () => createWidget('dxDataGrid', () => ({ - dataSource: [ - { id: 1, name: 'Alice', value: 30 }, - { id: 2, name: 'Bob', value: 20 }, - { id: 3, name: 'Charlie', value: 10 }, - ], - keyExpr: 'id', - columns: ['id', 'name', 'value'], - showBorders: true, - aiAssistant: { - enabled: true, - aiIntegration: new (window as any).DevExpress.aiIntegration.AIIntegration({ - sendRequest() { - return { - promise: Promise.resolve({ - actions: [{ name: 'sorting', args: { dataField: 'name', sortOrder: 'asc' } }], - }), - abort: (): void => {}, - }; - }, - }), - }, -}))); +}).before(async (t) => { + await t.expect(aiIntegrationReady()).ok(); + + await createWidget('dxDataGrid', () => ({ + dataSource: [ + { id: 1, name: 'Alice', value: 30 }, + { id: 2, name: 'Bob', value: 20 }, + { id: 3, name: 'Charlie', value: 10 }, + ], + keyExpr: 'id', + columns: ['id', 'name', 'value'], + showBorders: true, + aiAssistant: { + enabled: true, + aiIntegration: new (window as any).DevExpress.aiIntegration.AIIntegration({ + sendRequest() { + return { + promise: Promise.resolve({ + actions: [{ name: 'sorting', args: { dataField: 'name', sortOrder: 'asc' } }], + }), + abort: (): void => {}, + }; + }, + }), + }, + })); +}); // 1.1.6 test('Custom title should be rendered in popup header', async (t) => { @@ -139,25 +155,29 @@ test('Custom title should be rendered in popup header', async (t) => { const aiChat = dataGrid.getAIAssistantChat(); await t.expect(aiChat.getTitle().textContent).contains('My Custom Assistant'); -}).before(async () => createWidget('dxDataGrid', () => ({ - dataSource: [ - { id: 1, name: 'Alice', value: 30 }, - { id: 2, name: 'Bob', value: 20 }, - { id: 3, name: 'Charlie', value: 10 }, - ], - keyExpr: 'id', - columns: ['id', 'name', 'value'], - showBorders: true, - aiAssistant: { - enabled: true, - title: 'My Custom Assistant', - aiIntegration: new (window as any).DevExpress.aiIntegration.AIIntegration({ - sendRequest() { - return { promise: new Promise(() => {}), abort: (): void => {} }; - }, - }), - }, -}))); +}).before(async (t) => { + await t.expect(aiIntegrationReady()).ok(); + + await createWidget('dxDataGrid', () => ({ + dataSource: [ + { id: 1, name: 'Alice', value: 30 }, + { id: 2, name: 'Bob', value: 20 }, + { id: 3, name: 'Charlie', value: 10 }, + ], + keyExpr: 'id', + columns: ['id', 'name', 'value'], + showBorders: true, + aiAssistant: { + enabled: true, + title: 'My Custom Assistant', + aiIntegration: new (window as any).DevExpress.aiIntegration.AIIntegration({ + sendRequest() { + return { promise: new Promise(() => {}), abort: (): void => {} }; + }, + }), + }, + })); +}); // === §1.10 A11y / KBN === @@ -177,24 +197,28 @@ test('Toolbar button should be accessible and clickable', async (t) => { await t.click(button); await t.expect(dataGrid.getAIAssistantChat().element.visible).ok(); -}).before(async () => createWidget('dxDataGrid', () => ({ - dataSource: [ - { id: 1, name: 'Alice', value: 30 }, - { id: 2, name: 'Bob', value: 20 }, - { id: 3, name: 'Charlie', value: 10 }, - ], - keyExpr: 'id', - columns: ['id', 'name', 'value'], - showBorders: true, - aiAssistant: { - enabled: true, - aiIntegration: new (window as any).DevExpress.aiIntegration.AIIntegration({ - sendRequest() { - return { promise: new Promise(() => {}), abort: (): void => {} }; - }, - }), - }, -}))); +}).before(async (t) => { + await t.expect(aiIntegrationReady()).ok(); + + await createWidget('dxDataGrid', () => ({ + dataSource: [ + { id: 1, name: 'Alice', value: 30 }, + { id: 2, name: 'Bob', value: 20 }, + { id: 3, name: 'Charlie', value: 10 }, + ], + keyExpr: 'id', + columns: ['id', 'name', 'value'], + showBorders: true, + aiAssistant: { + enabled: true, + aiIntegration: new (window as any).DevExpress.aiIntegration.AIIntegration({ + sendRequest() { + return { promise: new Promise(() => {}), abort: (): void => {} }; + }, + }), + }, + })); +}); // 1.10.2 test('Toolbar button should activate via Enter key', async (t) => { @@ -210,21 +234,25 @@ test('Toolbar button should activate via Enter key', async (t) => { await t.pressKey('enter'); await t.expect(dataGrid.getAIAssistantChat().element.visible).ok(); -}).before(async () => createWidget('dxDataGrid', () => ({ - dataSource: [ - { id: 1, name: 'Alice', value: 30 }, - { id: 2, name: 'Bob', value: 20 }, - { id: 3, name: 'Charlie', value: 10 }, - ], - keyExpr: 'id', - columns: ['id', 'name', 'value'], - showBorders: true, - aiAssistant: { - enabled: true, - aiIntegration: new (window as any).DevExpress.aiIntegration.AIIntegration({ - sendRequest() { - return { promise: new Promise(() => {}), abort: (): void => {} }; - }, - }), - }, -}))); +}).before(async (t) => { + await t.expect(aiIntegrationReady()).ok(); + + await createWidget('dxDataGrid', () => ({ + dataSource: [ + { id: 1, name: 'Alice', value: 30 }, + { id: 2, name: 'Bob', value: 20 }, + { id: 3, name: 'Charlie', value: 10 }, + ], + keyExpr: 'id', + columns: ['id', 'name', 'value'], + showBorders: true, + aiAssistant: { + enabled: true, + aiIntegration: new (window as any).DevExpress.aiIntegration.AIIntegration({ + sendRequest() { + return { promise: new Promise(() => {}), abort: (): void => {} }; + }, + }), + }, + })); +}); From 6dd380059e4e1e36aaabdf90f5bf940f836325a1 Mon Sep 17 00:00:00 2001 From: Alyar <> Date: Wed, 3 Jun 2026 05:59:04 +0400 Subject: [PATCH 3/9] DataGrid - AI Assistant: extract createWidgetWithAIIntegration test helper Replace the repeated 'await aiIntegrationReady() + createWidget' pattern in before hooks with a single createWidgetWithAIIntegration helper. Co-Authored-By: Claude Opus 4.8 --- .../common/aiAssistant/testHelpers.ts | 15 ++++++++- .../aiAssistant/toolbarAndPopup.functional.ts | 31 +++++-------------- 2 files changed, 22 insertions(+), 24 deletions(-) diff --git a/e2e/testcafe-devextreme/tests/dataGrid/common/aiAssistant/testHelpers.ts b/e2e/testcafe-devextreme/tests/dataGrid/common/aiAssistant/testHelpers.ts index 051313fb3d43..d6e1b56babcb 100644 --- a/e2e/testcafe-devextreme/tests/dataGrid/common/aiAssistant/testHelpers.ts +++ b/e2e/testcafe-devextreme/tests/dataGrid/common/aiAssistant/testHelpers.ts @@ -1,6 +1,8 @@ /* eslint-disable no-underscore-dangle */ import { ClientFunction } from 'testcafe'; +import type { WidgetName, WidgetOptions } from 'devextreme-testcafe-models/types'; import url from '../../../../helpers/getPageUrl'; +import { createWidget } from '../../../../helpers/createWidget'; export const GRID_SELECTOR = '#container'; @@ -16,7 +18,18 @@ export const formatMessage = ClientFunction( ); // `dx.ai-integration.js` is loaded by the container page; in CI the global can be defined a beat -// after navigation, so tests await this in their `before` hook before instantiating AIIntegration. +// after navigation, so tests await this before instantiating AIIntegration. export const aiIntegrationReady = ClientFunction( () => typeof (window as any).DevExpress?.aiIntegration?.AIIntegration === 'function', ); + +// Waits for the AI integration global, then creates the widget. Use in `before` hooks instead of a +// bare createWidget so the AIIntegration constructor is guaranteed to exist when options are built. +export async function createWidgetWithAIIntegration( + t: TestController, + widgetName: TName, + options: TName extends keyof WidgetOptions ? () => WidgetOptions[TName] : () => unknown, +): Promise { + await t.expect(aiIntegrationReady()).ok(); + await createWidget(widgetName, options as never); +} diff --git a/e2e/testcafe-devextreme/tests/dataGrid/common/aiAssistant/toolbarAndPopup.functional.ts b/e2e/testcafe-devextreme/tests/dataGrid/common/aiAssistant/toolbarAndPopup.functional.ts index 18406b7db0d1..fd842238efa2 100644 --- a/e2e/testcafe-devextreme/tests/dataGrid/common/aiAssistant/toolbarAndPopup.functional.ts +++ b/e2e/testcafe-devextreme/tests/dataGrid/common/aiAssistant/toolbarAndPopup.functional.ts @@ -1,6 +1,5 @@ import DataGrid from 'devextreme-testcafe-models/dataGrid'; -import { createWidget } from '../../../../helpers/createWidget'; -import { aiIntegrationReady, AI_INTEGRATION_PAGE, GRID_SELECTOR } from './testHelpers'; +import { createWidgetWithAIIntegration, AI_INTEGRATION_PAGE, GRID_SELECTOR } from './testHelpers'; // === §1.1 Toolbar entry point & popup lifecycle === fixture.disablePageReloads`AI Assistant - Toolbar` @@ -14,9 +13,7 @@ test('Toolbar button should be visible when aiAssistant.enabled is true', async await t.expect(dataGrid.getAIAssistantButton().exists).ok(); }).before(async (t) => { - await t.expect(aiIntegrationReady()).ok(); - - await createWidget('dxDataGrid', () => ({ + await createWidgetWithAIIntegration(t, 'dxDataGrid', () => ({ dataSource: [ { id: 1, name: 'Alice', value: 30 }, { id: 2, name: 'Bob', value: 20 }, @@ -44,9 +41,7 @@ test('Toolbar button should be hidden when aiAssistant is not configured', async await t.expect(dataGrid.getAIAssistantButton().exists).notOk(); }).before(async (t) => { - await t.expect(aiIntegrationReady()).ok(); - - await createWidget('dxDataGrid', () => ({ + await createWidgetWithAIIntegration(t, 'dxDataGrid', () => ({ dataSource: [ { id: 1, name: 'Alice', value: 30 }, { id: 2, name: 'Bob', value: 20 }, @@ -72,9 +67,7 @@ test('Popup should open on toolbar button click', async (t) => { .expect(aiChat.element.visible).ok() .expect(aiChat.getChat().element.exists).ok(); }).before(async (t) => { - await t.expect(aiIntegrationReady()).ok(); - - await createWidget('dxDataGrid', () => ({ + await createWidgetWithAIIntegration(t, 'dxDataGrid', () => ({ dataSource: [ { id: 1, name: 'Alice', value: 30 }, { id: 2, name: 'Bob', value: 20 }, @@ -117,9 +110,7 @@ test('Grid state should be preserved after popup close', async (t) => { await t.expect(sortOrder).eql('asc'); }).before(async (t) => { - await t.expect(aiIntegrationReady()).ok(); - - await createWidget('dxDataGrid', () => ({ + await createWidgetWithAIIntegration(t, 'dxDataGrid', () => ({ dataSource: [ { id: 1, name: 'Alice', value: 30 }, { id: 2, name: 'Bob', value: 20 }, @@ -156,9 +147,7 @@ test('Custom title should be rendered in popup header', async (t) => { await t.expect(aiChat.getTitle().textContent).contains('My Custom Assistant'); }).before(async (t) => { - await t.expect(aiIntegrationReady()).ok(); - - await createWidget('dxDataGrid', () => ({ + await createWidgetWithAIIntegration(t, 'dxDataGrid', () => ({ dataSource: [ { id: 1, name: 'Alice', value: 30 }, { id: 2, name: 'Bob', value: 20 }, @@ -198,9 +187,7 @@ test('Toolbar button should be accessible and clickable', async (t) => { await t.expect(dataGrid.getAIAssistantChat().element.visible).ok(); }).before(async (t) => { - await t.expect(aiIntegrationReady()).ok(); - - await createWidget('dxDataGrid', () => ({ + await createWidgetWithAIIntegration(t, 'dxDataGrid', () => ({ dataSource: [ { id: 1, name: 'Alice', value: 30 }, { id: 2, name: 'Bob', value: 20 }, @@ -235,9 +222,7 @@ test('Toolbar button should activate via Enter key', async (t) => { await t.expect(dataGrid.getAIAssistantChat().element.visible).ok(); }).before(async (t) => { - await t.expect(aiIntegrationReady()).ok(); - - await createWidget('dxDataGrid', () => ({ + await createWidgetWithAIIntegration(t, 'dxDataGrid', () => ({ dataSource: [ { id: 1, name: 'Alice', value: 30 }, { id: 2, name: 'Bob', value: 20 }, From 23d0e8453ad0b02a2f1585381887b6d6812eee39 Mon Sep 17 00:00:00 2001 From: Alyar <> Date: Wed, 3 Jun 2026 17:59:16 +0400 Subject: [PATCH 4/9] DataGrid - AI Assistant: Address Copilot review feedback on POM selectors - Anchor clear-chat button to dx-ai-chat__clear-button toolbar class - Return nested .dx-button from getClearChatButton and check disabled directly - Select confirm-dialog Yes button by position (locale-independent) - Fix comment typo Co-Authored-By: Claude Opus 4.8 --- .../dataGrid/common/aiAssistant/testHelpers.ts | 2 +- .../testcafe-models/dataGrid/aiAssistantChat.ts | 14 ++++++++------ 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/e2e/testcafe-devextreme/tests/dataGrid/common/aiAssistant/testHelpers.ts b/e2e/testcafe-devextreme/tests/dataGrid/common/aiAssistant/testHelpers.ts index d6e1b56babcb..18b05df9fc96 100644 --- a/e2e/testcafe-devextreme/tests/dataGrid/common/aiAssistant/testHelpers.ts +++ b/e2e/testcafe-devextreme/tests/dataGrid/common/aiAssistant/testHelpers.ts @@ -17,7 +17,7 @@ export const formatMessage = ClientFunction( (key: string) => (window as any).DevExpress.localization.formatMessage(key), ); -// `dx.ai-integration.js` is loaded by the container page; in CI the global can be defined a beat +// `dx.ai-integration.js` is loaded by the container page; in CI the global can be defined a bit // after navigation, so tests await this before instantiating AIIntegration. export const aiIntegrationReady = ClientFunction( () => typeof (window as any).DevExpress?.aiIntegration?.AIIntegration === 'function', diff --git a/packages/testcafe-models/dataGrid/aiAssistantChat.ts b/packages/testcafe-models/dataGrid/aiAssistantChat.ts index 623cf0336434..4710f7b8dd80 100644 --- a/packages/testcafe-models/dataGrid/aiAssistantChat.ts +++ b/packages/testcafe-models/dataGrid/aiAssistantChat.ts @@ -26,7 +26,7 @@ const CLASS = { actionListItemIcon: 'dx-ai-chat__action-list-item-icon', actionListItemText: 'dx-ai-chat__action-list-item-text', closeButton: 'dx-closebutton', - clearChatButton: 'dx-icon-clearhistory', + clearChatButton: 'dx-ai-chat__clear-button', suggestion: 'dx-chat-suggestions', suggestionButton: 'dx-button', }; @@ -55,11 +55,15 @@ export class AIAssistantChat extends Popup { // eslint-disable-next-line class-methods-use-this getAbortConfirmYesButton(): Selector { - return Selector(`.${CLASS.abortConfirmDialog} .dx-button`).withExactText('Yes'); + // The confirm dialog renders its buttons in [No, Yes] order with localized + // captions, so select the second button by position instead of by text. + return Selector(`.${CLASS.abortConfirmDialog} .dx-button`).nth(1); } getClearChatButton(): Selector { - return this.element.find(`.${CLASS.clearChatButton}`); + // `clearChatButton` is the toolbar item's cssClass; the actual dxButton (which + // carries the `dx-state-disabled` class) is nested inside it. + return this.element.find(`.${CLASS.clearChatButton} .dx-button`); } getMessages(): Selector { @@ -139,9 +143,7 @@ export class AIAssistantChat extends Popup { } isClearChatDisabled(): Promise { - return this.getClearChatButton() - .parent('.dx-button') - .hasClass('dx-state-disabled'); + return this.getClearChatButton().hasClass('dx-state-disabled'); } isSuggestionDisabled(index: number): Promise { From 25d0349ca3db307ccd745819783b2bbcc680a456 Mon Sep 17 00:00:00 2001 From: Alyar <> Date: Wed, 3 Jun 2026 21:34:29 +0400 Subject: [PATCH 5/9] DataGrid - AI Assistant: refine toolbar & popup e2e tests, trim POM - Deduplicate widget config into per-scenario option factories. - 1.1.3: assert grid state is unchanged when the popup opens. - 1.10.2: verify activation via both Enter and Space, with focus check. - Remove redundant 1.10.1 click-open test (covered by 1.1.3 and 1.10.2). - Drop AIAssistantChat POM helpers unused in this PR; add GridCore.apiState() and remove unused DataGrid.apiGetDataSourceSortParams(). Co-Authored-By: Claude Opus 4.8 --- .../aiAssistant/toolbarAndPopup.functional.ts | 251 ++++++++---------- .../dataGrid/aiAssistantChat.ts | 41 +-- packages/testcafe-models/dataGrid/index.ts | 9 - packages/testcafe-models/gridCore/index.ts | 15 +- 4 files changed, 126 insertions(+), 190 deletions(-) diff --git a/e2e/testcafe-devextreme/tests/dataGrid/common/aiAssistant/toolbarAndPopup.functional.ts b/e2e/testcafe-devextreme/tests/dataGrid/common/aiAssistant/toolbarAndPopup.functional.ts index fd842238efa2..c1651a505dcd 100644 --- a/e2e/testcafe-devextreme/tests/dataGrid/common/aiAssistant/toolbarAndPopup.functional.ts +++ b/e2e/testcafe-devextreme/tests/dataGrid/common/aiAssistant/toolbarAndPopup.functional.ts @@ -1,5 +1,89 @@ import DataGrid from 'devextreme-testcafe-models/dataGrid'; -import { createWidgetWithAIIntegration, AI_INTEGRATION_PAGE, GRID_SELECTOR } from './testHelpers'; +import { + createWidgetWithAIIntegration, + AI_INTEGRATION_PAGE, + GRID_SELECTOR, +} from './testHelpers'; + +// AI Assistant enabled with a request that never resolves — keeps the chat in the +// idle/empty state (no command runs) for visibility & open/close lifecycle tests. +const gridWithIdleAssistant = (): any => ({ + dataSource: [ + { id: 1, name: 'Alice', value: 30 }, + { id: 2, name: 'Bob', value: 20 }, + { id: 3, name: 'Charlie', value: 10 }, + ], + keyExpr: 'id', + columns: ['id', 'name', 'value'], + showBorders: true, + aiAssistant: { + enabled: true, + aiIntegration: new (window as any).DevExpress.aiIntegration.AIIntegration({ + sendRequest() { + return { promise: new Promise(() => {}), abort: (): void => {} }; + }, + }), + }, +}); + +// Grid without the `aiAssistant` option — toolbar button must not be rendered. +const gridWithoutAssistant = (): any => ({ + dataSource: [ + { id: 1, name: 'Alice', value: 30 }, + { id: 2, name: 'Bob', value: 20 }, + { id: 3, name: 'Charlie', value: 10 }, + ], + keyExpr: 'id', + columns: ['id', 'name', 'value'], + showBorders: true, +}); + +// AI Assistant whose mocked request resolves to a single `sorting` command, so a +// prompt actually mutates grid state (used to assert the command is applied). +const gridWithSortingAssistant = (): any => ({ + dataSource: [ + { id: 1, name: 'Alice', value: 30 }, + { id: 2, name: 'Bob', value: 20 }, + { id: 3, name: 'Charlie', value: 10 }, + ], + keyExpr: 'id', + columns: ['id', 'name', 'value'], + showBorders: true, + aiAssistant: { + enabled: true, + aiIntegration: new (window as any).DevExpress.aiIntegration.AIIntegration({ + sendRequest() { + return { + promise: Promise.resolve({ + actions: [{ name: 'sorting', args: { dataField: 'name', sortOrder: 'asc' } }], + }), + abort: (): void => {}, + }; + }, + }), + }, +}); + +// AI Assistant with a custom popup title (request stays pending — title only). +const gridWithTitledAssistant = (): any => ({ + dataSource: [ + { id: 1, name: 'Alice', value: 30 }, + { id: 2, name: 'Bob', value: 20 }, + { id: 3, name: 'Charlie', value: 10 }, + ], + keyExpr: 'id', + columns: ['id', 'name', 'value'], + showBorders: true, + aiAssistant: { + enabled: true, + title: 'My Custom Assistant', + aiIntegration: new (window as any).DevExpress.aiIntegration.AIIntegration({ + sendRequest() { + return { promise: new Promise(() => {}), abort: (): void => {} }; + }, + }), + }, +}); // === §1.1 Toolbar entry point & popup lifecycle === fixture.disablePageReloads`AI Assistant - Toolbar` @@ -13,24 +97,7 @@ test('Toolbar button should be visible when aiAssistant.enabled is true', async await t.expect(dataGrid.getAIAssistantButton().exists).ok(); }).before(async (t) => { - await createWidgetWithAIIntegration(t, 'dxDataGrid', () => ({ - dataSource: [ - { id: 1, name: 'Alice', value: 30 }, - { id: 2, name: 'Bob', value: 20 }, - { id: 3, name: 'Charlie', value: 10 }, - ], - keyExpr: 'id', - columns: ['id', 'name', 'value'], - showBorders: true, - aiAssistant: { - enabled: true, - aiIntegration: new (window as any).DevExpress.aiIntegration.AIIntegration({ - sendRequest() { - return { promise: new Promise(() => {}), abort: (): void => {} }; - }, - }), - }, - })); + await createWidgetWithAIIntegration(t, 'dxDataGrid', gridWithIdleAssistant); }); // 1.1.2 @@ -41,50 +108,28 @@ test('Toolbar button should be hidden when aiAssistant is not configured', async await t.expect(dataGrid.getAIAssistantButton().exists).notOk(); }).before(async (t) => { - await createWidgetWithAIIntegration(t, 'dxDataGrid', () => ({ - dataSource: [ - { id: 1, name: 'Alice', value: 30 }, - { id: 2, name: 'Bob', value: 20 }, - { id: 3, name: 'Charlie', value: 10 }, - ], - keyExpr: 'id', - columns: ['id', 'name', 'value'], - showBorders: true, - })); + await createWidgetWithAIIntegration(t, 'dxDataGrid', gridWithoutAssistant); }); // 1.1.3 -test('Popup should open on toolbar button click', async (t) => { +test('Popup should open on toolbar button click without changing grid state', async (t) => { const dataGrid = new DataGrid(GRID_SELECTOR); await t.expect(dataGrid.isReady()).ok(); + const initialState = await dataGrid.apiState(); + await t.click(dataGrid.getAIAssistantButton()); const aiChat = dataGrid.getAIAssistantChat(); + const finalState = await dataGrid.apiState(); - await t - .expect(aiChat.element.visible).ok() - .expect(aiChat.getChat().element.exists).ok(); + await t.expect(finalState).eql(initialState); + await t.expect(aiChat.element.visible).ok(); + await t.expect(aiChat.getChat().element.exists).ok(); + await t.expect(aiChat.getInput().visible).ok(); }).before(async (t) => { - await createWidgetWithAIIntegration(t, 'dxDataGrid', () => ({ - dataSource: [ - { id: 1, name: 'Alice', value: 30 }, - { id: 2, name: 'Bob', value: 20 }, - { id: 3, name: 'Charlie', value: 10 }, - ], - keyExpr: 'id', - columns: ['id', 'name', 'value'], - showBorders: true, - aiAssistant: { - enabled: true, - aiIntegration: new (window as any).DevExpress.aiIntegration.AIIntegration({ - sendRequest() { - return { promise: new Promise(() => {}), abort: (): void => {} }; - }, - }), - }, - })); + await createWidgetWithAIIntegration(t, 'dxDataGrid', gridWithIdleAssistant); }); // 1.1.4 @@ -110,29 +155,7 @@ test('Grid state should be preserved after popup close', async (t) => { await t.expect(sortOrder).eql('asc'); }).before(async (t) => { - await createWidgetWithAIIntegration(t, 'dxDataGrid', () => ({ - dataSource: [ - { id: 1, name: 'Alice', value: 30 }, - { id: 2, name: 'Bob', value: 20 }, - { id: 3, name: 'Charlie', value: 10 }, - ], - keyExpr: 'id', - columns: ['id', 'name', 'value'], - showBorders: true, - aiAssistant: { - enabled: true, - aiIntegration: new (window as any).DevExpress.aiIntegration.AIIntegration({ - sendRequest() { - return { - promise: Promise.resolve({ - actions: [{ name: 'sorting', args: { dataField: 'name', sortOrder: 'asc' } }], - }), - abort: (): void => {}, - }; - }, - }), - }, - })); + await createWidgetWithAIIntegration(t, 'dxDataGrid', gridWithSortingAssistant); }); // 1.1.6 @@ -147,97 +170,43 @@ test('Custom title should be rendered in popup header', async (t) => { await t.expect(aiChat.getTitle().textContent).contains('My Custom Assistant'); }).before(async (t) => { - await createWidgetWithAIIntegration(t, 'dxDataGrid', () => ({ - dataSource: [ - { id: 1, name: 'Alice', value: 30 }, - { id: 2, name: 'Bob', value: 20 }, - { id: 3, name: 'Charlie', value: 10 }, - ], - keyExpr: 'id', - columns: ['id', 'name', 'value'], - showBorders: true, - aiAssistant: { - enabled: true, - title: 'My Custom Assistant', - aiIntegration: new (window as any).DevExpress.aiIntegration.AIIntegration({ - sendRequest() { - return { promise: new Promise(() => {}), abort: (): void => {} }; - }, - }), - }, - })); + await createWidgetWithAIIntegration(t, 'dxDataGrid', gridWithTitledAssistant); }); // === §1.10 A11y / KBN === - fixture.disablePageReloads`AI Assistant - A11y` .page(AI_INTEGRATION_PAGE); -// 1.10.1 -test('Toolbar button should be accessible and clickable', async (t) => { +// 1.10.2 (Enter) — focus the toolbar button and activate it with Enter. +test('Toolbar button should activate via Enter key', async (t) => { const dataGrid = new DataGrid(GRID_SELECTOR); await t.expect(dataGrid.isReady()).ok(); - const button = dataGrid.getAIAssistantButton(); + await dataGrid.focusAIAssistantButton(); - await t.expect(button.exists).ok(); + await t.expect(dataGrid.getAIAssistantButton().focused).ok(); - await t.click(button); + await t.pressKey('enter'); await t.expect(dataGrid.getAIAssistantChat().element.visible).ok(); }).before(async (t) => { - await createWidgetWithAIIntegration(t, 'dxDataGrid', () => ({ - dataSource: [ - { id: 1, name: 'Alice', value: 30 }, - { id: 2, name: 'Bob', value: 20 }, - { id: 3, name: 'Charlie', value: 10 }, - ], - keyExpr: 'id', - columns: ['id', 'name', 'value'], - showBorders: true, - aiAssistant: { - enabled: true, - aiIntegration: new (window as any).DevExpress.aiIntegration.AIIntegration({ - sendRequest() { - return { promise: new Promise(() => {}), abort: (): void => {} }; - }, - }), - }, - })); + await createWidgetWithAIIntegration(t, 'dxDataGrid', gridWithIdleAssistant); }); -// 1.10.2 -test('Toolbar button should activate via Enter key', async (t) => { +// 1.10.2 (Space) — same scenario, activated with Space. +test('Toolbar button should activate via Space key', async (t) => { const dataGrid = new DataGrid(GRID_SELECTOR); await t.expect(dataGrid.isReady()).ok(); - const button = dataGrid.getAIAssistantButton(); + await dataGrid.focusAIAssistantButton(); - await t.expect(button.exists).ok(); + await t.expect(dataGrid.getAIAssistantButton().focused).ok(); - await dataGrid.focusAIAssistantButton(); - await t.pressKey('enter'); + await t.pressKey('space'); await t.expect(dataGrid.getAIAssistantChat().element.visible).ok(); }).before(async (t) => { - await createWidgetWithAIIntegration(t, 'dxDataGrid', () => ({ - dataSource: [ - { id: 1, name: 'Alice', value: 30 }, - { id: 2, name: 'Bob', value: 20 }, - { id: 3, name: 'Charlie', value: 10 }, - ], - keyExpr: 'id', - columns: ['id', 'name', 'value'], - showBorders: true, - aiAssistant: { - enabled: true, - aiIntegration: new (window as any).DevExpress.aiIntegration.AIIntegration({ - sendRequest() { - return { promise: new Promise(() => {}), abort: (): void => {} }; - }, - }), - }, - })); + await createWidgetWithAIIntegration(t, 'dxDataGrid', gridWithIdleAssistant); }); diff --git a/packages/testcafe-models/dataGrid/aiAssistantChat.ts b/packages/testcafe-models/dataGrid/aiAssistantChat.ts index 4710f7b8dd80..ce66205aa81a 100644 --- a/packages/testcafe-models/dataGrid/aiAssistantChat.ts +++ b/packages/testcafe-models/dataGrid/aiAssistantChat.ts @@ -6,7 +6,6 @@ import Chat from '../chat'; const CLASS = { aiChat: 'dx-ai-chat', aiChatContent: 'dx-ai-chat__content', - abortConfirmDialog: 'dx-datagrid-ai-assistant-confirm-dialog', message: 'dx-ai-chat__message', messagePending: 'dx-ai-chat__message--pending', messageSuccess: 'dx-ai-chat__message--success', @@ -26,9 +25,7 @@ const CLASS = { actionListItemIcon: 'dx-ai-chat__action-list-item-icon', actionListItemText: 'dx-ai-chat__action-list-item-text', closeButton: 'dx-closebutton', - clearChatButton: 'dx-ai-chat__clear-button', - suggestion: 'dx-chat-suggestions', - suggestionButton: 'dx-button', + clearChatButton: 'dx-icon-clearhistory', }; export class AIAssistantChat extends Popup { @@ -48,32 +45,14 @@ export class AIAssistantChat extends Popup { return new Button(this.element.find(`.${CLASS.closeButton}`)); } - // eslint-disable-next-line class-methods-use-this - getAbortConfirmDialog(): Selector { - return Selector(`.${CLASS.abortConfirmDialog}`); - } - - // eslint-disable-next-line class-methods-use-this - getAbortConfirmYesButton(): Selector { - // The confirm dialog renders its buttons in [No, Yes] order with localized - // captions, so select the second button by position instead of by text. - return Selector(`.${CLASS.abortConfirmDialog} .dx-button`).nth(1); - } - getClearChatButton(): Selector { - // `clearChatButton` is the toolbar item's cssClass; the actual dxButton (which - // carries the `dx-state-disabled` class) is nested inside it. - return this.element.find(`.${CLASS.clearChatButton} .dx-button`); + return this.element.find(`.${CLASS.clearChatButton}`); } getMessages(): Selector { return this.getChat().getMessageBubbles(); } - getUserMessages(): Selector { - return this.getMessages().filter((node) => !node.querySelector('.dx-ai-chat__message')); - } - getAIMessages(): Selector { return this.element.find(`.${CLASS.message}`); } @@ -134,22 +113,6 @@ export class AIAssistantChat extends Popup { return this.getActionItems(messageIndex).nth(actionIndex).find(`.${CLASS.actionListItemIcon}`); } - getSuggestions(): Selector { - return this.element.find(`.${CLASS.suggestion} .${CLASS.suggestionButton}`); - } - - isInputDisabled(): Promise { - return this.getChat().getTextArea().isDisabled; - } - - isClearChatDisabled(): Promise { - return this.getClearChatButton().hasClass('dx-state-disabled'); - } - - isSuggestionDisabled(index: number): Promise { - return this.getSuggestions().nth(index).hasClass('dx-state-disabled'); - } - getTitle(): Selector { return this.topToolbar; } diff --git a/packages/testcafe-models/dataGrid/index.ts b/packages/testcafe-models/dataGrid/index.ts index 99aec7ee5f3d..5767826d1ce2 100644 --- a/packages/testcafe-models/dataGrid/index.ts +++ b/packages/testcafe-models/dataGrid/index.ts @@ -777,15 +777,6 @@ export default class DataGrid extends GridCore { )(); } - apiGetDataSourceSortParams(): Promise { - const { getInstance } = this; - - return ClientFunction( - () => (getInstance() as DataGridInstance).getDataSource().sort(), - { dependencies: { getInstance } }, - )(); - } - moveRow(rowIndex: number, x: number, y: number, isStart = false): Promise { const { getInstance } = this; diff --git a/packages/testcafe-models/gridCore/index.ts b/packages/testcafe-models/gridCore/index.ts index 3db91006fc21..3b1be4a093bd 100644 --- a/packages/testcafe-models/gridCore/index.ts +++ b/packages/testcafe-models/gridCore/index.ts @@ -167,4 +167,17 @@ export default abstract class GridCore extends Widget { }, )(); } -} + + apiState(): Promise { + const { getInstance } = this; + + return ClientFunction( + () => (getInstance() as any).state(), + { + dependencies: { + getInstance, + }, + }, + )(); + } +} \ No newline at end of file From 643696b17a18958c3e64fb6e6d994854f55f4adb Mon Sep 17 00:00:00 2001 From: Alyar <> Date: Wed, 3 Jun 2026 22:22:30 +0400 Subject: [PATCH 6/9] Fix copilot comment --- packages/testcafe-models/dataGrid/index.ts | 15 ++++++++++++++- packages/testcafe-models/gridCore/index.ts | 13 ------------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/packages/testcafe-models/dataGrid/index.ts b/packages/testcafe-models/dataGrid/index.ts index 5767826d1ce2..f7455abdacaf 100644 --- a/packages/testcafe-models/dataGrid/index.ts +++ b/packages/testcafe-models/dataGrid/index.ts @@ -1029,7 +1029,20 @@ export default class DataGrid extends GridCore { { dependencies: { getInstance } }, )(); } - + + apiState(): Promise { + const { getInstance } = this; + + return ClientFunction( + () => (getInstance() as any).state(), + { + dependencies: { + getInstance, + }, + }, + )(); + } + getDraggableHeader() { return this.body.find(`.${this.addWidgetPrefix(CLASS.dragHeader)}`); } diff --git a/packages/testcafe-models/gridCore/index.ts b/packages/testcafe-models/gridCore/index.ts index 3b1be4a093bd..5f04409a4f31 100644 --- a/packages/testcafe-models/gridCore/index.ts +++ b/packages/testcafe-models/gridCore/index.ts @@ -167,17 +167,4 @@ export default abstract class GridCore extends Widget { }, )(); } - - apiState(): Promise { - const { getInstance } = this; - - return ClientFunction( - () => (getInstance() as any).state(), - { - dependencies: { - getInstance, - }, - }, - )(); - } } \ No newline at end of file From ad2443b33c07364c903123dacf3b7e2d7da06bd0 Mon Sep 17 00:00:00 2001 From: Alyar <> Date: Thu, 4 Jun 2026 00:34:08 +0400 Subject: [PATCH 7/9] Fix tests --- .../common/aiAssistant/testHelpers.ts | 17 ---------- .../aiAssistant/toolbarAndPopup.functional.ts | 34 ++++++------------- 2 files changed, 10 insertions(+), 41 deletions(-) diff --git a/e2e/testcafe-devextreme/tests/dataGrid/common/aiAssistant/testHelpers.ts b/e2e/testcafe-devextreme/tests/dataGrid/common/aiAssistant/testHelpers.ts index 18b05df9fc96..aa9995edd50c 100644 --- a/e2e/testcafe-devextreme/tests/dataGrid/common/aiAssistant/testHelpers.ts +++ b/e2e/testcafe-devextreme/tests/dataGrid/common/aiAssistant/testHelpers.ts @@ -16,20 +16,3 @@ export const getRequestColumnNames = ClientFunction( export const formatMessage = ClientFunction( (key: string) => (window as any).DevExpress.localization.formatMessage(key), ); - -// `dx.ai-integration.js` is loaded by the container page; in CI the global can be defined a bit -// after navigation, so tests await this before instantiating AIIntegration. -export const aiIntegrationReady = ClientFunction( - () => typeof (window as any).DevExpress?.aiIntegration?.AIIntegration === 'function', -); - -// Waits for the AI integration global, then creates the widget. Use in `before` hooks instead of a -// bare createWidget so the AIIntegration constructor is guaranteed to exist when options are built. -export async function createWidgetWithAIIntegration( - t: TestController, - widgetName: TName, - options: TName extends keyof WidgetOptions ? () => WidgetOptions[TName] : () => unknown, -): Promise { - await t.expect(aiIntegrationReady()).ok(); - await createWidget(widgetName, options as never); -} diff --git a/e2e/testcafe-devextreme/tests/dataGrid/common/aiAssistant/toolbarAndPopup.functional.ts b/e2e/testcafe-devextreme/tests/dataGrid/common/aiAssistant/toolbarAndPopup.functional.ts index c1651a505dcd..5ca4620850f2 100644 --- a/e2e/testcafe-devextreme/tests/dataGrid/common/aiAssistant/toolbarAndPopup.functional.ts +++ b/e2e/testcafe-devextreme/tests/dataGrid/common/aiAssistant/toolbarAndPopup.functional.ts @@ -1,9 +1,9 @@ import DataGrid from 'devextreme-testcafe-models/dataGrid'; import { - createWidgetWithAIIntegration, AI_INTEGRATION_PAGE, GRID_SELECTOR, } from './testHelpers'; +import { createWidget } from '../../../../helpers/createWidget'; // AI Assistant enabled with a request that never resolves — keeps the chat in the // idle/empty state (no command runs) for visibility & open/close lifecycle tests. @@ -86,7 +86,7 @@ const gridWithTitledAssistant = (): any => ({ }); // === §1.1 Toolbar entry point & popup lifecycle === -fixture.disablePageReloads`AI Assistant - Toolbar` +fixture`AI Assistant - Toolbar` .page(AI_INTEGRATION_PAGE); // 1.1.1 @@ -96,9 +96,7 @@ test('Toolbar button should be visible when aiAssistant.enabled is true', async await t.expect(dataGrid.isReady()).ok(); await t.expect(dataGrid.getAIAssistantButton().exists).ok(); -}).before(async (t) => { - await createWidgetWithAIIntegration(t, 'dxDataGrid', gridWithIdleAssistant); -}); +}).before(async () => createWidget('dxDataGrid', gridWithIdleAssistant)); // 1.1.2 test('Toolbar button should be hidden when aiAssistant is not configured', async (t) => { @@ -107,9 +105,7 @@ test('Toolbar button should be hidden when aiAssistant is not configured', async await t.expect(dataGrid.isReady()).ok(); await t.expect(dataGrid.getAIAssistantButton().exists).notOk(); -}).before(async (t) => { - await createWidgetWithAIIntegration(t, 'dxDataGrid', gridWithoutAssistant); -}); +}).before(async () => createWidget('dxDataGrid', gridWithoutAssistant)); // 1.1.3 test('Popup should open on toolbar button click without changing grid state', async (t) => { @@ -128,9 +124,7 @@ test('Popup should open on toolbar button click without changing grid state', as await t.expect(aiChat.element.visible).ok(); await t.expect(aiChat.getChat().element.exists).ok(); await t.expect(aiChat.getInput().visible).ok(); -}).before(async (t) => { - await createWidgetWithAIIntegration(t, 'dxDataGrid', gridWithIdleAssistant); -}); +}).before(async () => createWidget('dxDataGrid', gridWithIdleAssistant)); // 1.1.4 test('Grid state should be preserved after popup close', async (t) => { @@ -154,9 +148,7 @@ test('Grid state should be preserved after popup close', async (t) => { const sortOrder = await dataGrid.apiColumnOption('name', 'sortOrder'); await t.expect(sortOrder).eql('asc'); -}).before(async (t) => { - await createWidgetWithAIIntegration(t, 'dxDataGrid', gridWithSortingAssistant); -}); +}).before(async () => createWidget('dxDataGrid', gridWithSortingAssistant)); // 1.1.6 test('Custom title should be rendered in popup header', async (t) => { @@ -169,12 +161,10 @@ test('Custom title should be rendered in popup header', async (t) => { const aiChat = dataGrid.getAIAssistantChat(); await t.expect(aiChat.getTitle().textContent).contains('My Custom Assistant'); -}).before(async (t) => { - await createWidgetWithAIIntegration(t, 'dxDataGrid', gridWithTitledAssistant); -}); +}).before(async () => createWidget('dxDataGrid', gridWithTitledAssistant)); // === §1.10 A11y / KBN === -fixture.disablePageReloads`AI Assistant - A11y` +fixture`AI Assistant - A11y` .page(AI_INTEGRATION_PAGE); // 1.10.2 (Enter) — focus the toolbar button and activate it with Enter. @@ -190,9 +180,7 @@ test('Toolbar button should activate via Enter key', async (t) => { await t.pressKey('enter'); await t.expect(dataGrid.getAIAssistantChat().element.visible).ok(); -}).before(async (t) => { - await createWidgetWithAIIntegration(t, 'dxDataGrid', gridWithIdleAssistant); -}); +}).before(async () => createWidget('dxDataGrid', gridWithIdleAssistant)); // 1.10.2 (Space) — same scenario, activated with Space. test('Toolbar button should activate via Space key', async (t) => { @@ -207,6 +195,4 @@ test('Toolbar button should activate via Space key', async (t) => { await t.pressKey('space'); await t.expect(dataGrid.getAIAssistantChat().element.visible).ok(); -}).before(async (t) => { - await createWidgetWithAIIntegration(t, 'dxDataGrid', gridWithIdleAssistant); -}); +}).before(async () => createWidget('dxDataGrid', gridWithIdleAssistant)); From 2ea2d6a5ea6e211400c800eaaab855ddf6f58558 Mon Sep 17 00:00:00 2001 From: Alyar <> Date: Thu, 4 Jun 2026 00:41:51 +0400 Subject: [PATCH 8/9] Fix copilot comments --- .../tests/dataGrid/common/aiAssistant/testHelpers.ts | 2 -- .../dataGrid/common/aiAssistant/toolbarAndPopup.functional.ts | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/e2e/testcafe-devextreme/tests/dataGrid/common/aiAssistant/testHelpers.ts b/e2e/testcafe-devextreme/tests/dataGrid/common/aiAssistant/testHelpers.ts index aa9995edd50c..45c66d4f7edb 100644 --- a/e2e/testcafe-devextreme/tests/dataGrid/common/aiAssistant/testHelpers.ts +++ b/e2e/testcafe-devextreme/tests/dataGrid/common/aiAssistant/testHelpers.ts @@ -1,8 +1,6 @@ /* eslint-disable no-underscore-dangle */ import { ClientFunction } from 'testcafe'; -import type { WidgetName, WidgetOptions } from 'devextreme-testcafe-models/types'; import url from '../../../../helpers/getPageUrl'; -import { createWidget } from '../../../../helpers/createWidget'; export const GRID_SELECTOR = '#container'; diff --git a/e2e/testcafe-devextreme/tests/dataGrid/common/aiAssistant/toolbarAndPopup.functional.ts b/e2e/testcafe-devextreme/tests/dataGrid/common/aiAssistant/toolbarAndPopup.functional.ts index 5ca4620850f2..f1df0bcd2b79 100644 --- a/e2e/testcafe-devextreme/tests/dataGrid/common/aiAssistant/toolbarAndPopup.functional.ts +++ b/e2e/testcafe-devextreme/tests/dataGrid/common/aiAssistant/toolbarAndPopup.functional.ts @@ -127,7 +127,7 @@ test('Popup should open on toolbar button click without changing grid state', as }).before(async () => createWidget('dxDataGrid', gridWithIdleAssistant)); // 1.1.4 -test('Grid state should be preserved after popup close', async (t) => { +test('AI Assistant-applied sorting should persist after popup close', async (t) => { const dataGrid = new DataGrid(GRID_SELECTOR); await t.expect(dataGrid.isReady()).ok(); From beef8d93d86d10e34ba3dc4975a83f059c9daac7 Mon Sep 17 00:00:00 2001 From: Alyar <> Date: Thu, 4 Jun 2026 12:22:08 +0400 Subject: [PATCH 9/9] Remove unnecessary helper methods --- .../tests/dataGrid/common/aiAssistant/testHelpers.ts | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/e2e/testcafe-devextreme/tests/dataGrid/common/aiAssistant/testHelpers.ts b/e2e/testcafe-devextreme/tests/dataGrid/common/aiAssistant/testHelpers.ts index 45c66d4f7edb..104aa729b219 100644 --- a/e2e/testcafe-devextreme/tests/dataGrid/common/aiAssistant/testHelpers.ts +++ b/e2e/testcafe-devextreme/tests/dataGrid/common/aiAssistant/testHelpers.ts @@ -1,16 +1,5 @@ -/* eslint-disable no-underscore-dangle */ -import { ClientFunction } from 'testcafe'; import url from '../../../../helpers/getPageUrl'; export const GRID_SELECTOR = '#container'; export const AI_INTEGRATION_PAGE = url(__dirname, '../../../container-ai-integration.html'); - -export const getRequestColumnNames = ClientFunction( - (index: number) => (window as any).__aiRequests[index].data.context.columns - .map((c: any) => c.dataField), -); - -export const formatMessage = ClientFunction( - (key: string) => (window as any).DevExpress.localization.formatMessage(key), -);