From bcc06b01f50fc9cee2a07dfbf26ef866c5773a87 Mon Sep 17 00:00:00 2001 From: Alyar <> Date: Wed, 3 Jun 2026 02:34:15 +0400 Subject: [PATCH 1/7] TreeList - AI Assistant: parity e2e tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds e2e TestCafe tests verifying the AI Assistant behaves identically on TreeList (it reuses grid_core): toolbar/popup/chat, sorting, searching, paging, column visibility, selection, row focusing, error paths and the in-flight lock; plus the TreeList-only schema differences (grouping/summary commands absent) and hierarchical-data cases (plan §6). Includes the AI Assistant testcafe-models POM accessors the tests depend on. Co-Authored-By: Claude Opus 4.8 --- .../common/treeList/aiAssistant/functional.ts | 708 ++++++++++++++++++ packages/testcafe-models/chat.ts | 6 +- .../dataGrid/aiAssistantChat.ts | 67 +- packages/testcafe-models/dataGrid/index.ts | 20 + 4 files changed, 790 insertions(+), 11 deletions(-) create mode 100644 e2e/testcafe-devextreme/tests/common/treeList/aiAssistant/functional.ts diff --git a/e2e/testcafe-devextreme/tests/common/treeList/aiAssistant/functional.ts b/e2e/testcafe-devextreme/tests/common/treeList/aiAssistant/functional.ts new file mode 100644 index 000000000000..9597b3701ece --- /dev/null +++ b/e2e/testcafe-devextreme/tests/common/treeList/aiAssistant/functional.ts @@ -0,0 +1,708 @@ +/* eslint-disable no-underscore-dangle */ +import { ClientFunction } from 'testcafe'; +import TreeList from 'devextreme-testcafe-models/treeList'; +import { createWidget } from '../../../../helpers/createWidget'; +import url from '../../../../helpers/getPageUrl'; + +const GRID_SELECTOR = '#container'; +const AI_INTEGRATION_PAGE = url(__dirname, '../../../container-ai-integration.html'); + +// TreeList reuses grid_core, so the AI Assistant feature is expected to behave like DataGrid. +// These tests verify that parity and the TreeList-only differences (no grouping/summary commands). + +const getSchemaCommandNames = ClientFunction(() => ((window as any).__aiRequests[0] + .data.responseSchema.properties.actions.items.anyOf as any[]) + .map((branch) => branch.properties.name.enum[0])); + +// === §6.1 grid_core-level commands behave identically on TreeList === + +fixture.disablePageReloads`AI Assistant - TreeList Parity` + .page(AI_INTEGRATION_PAGE); + +// 6.1.1 +test('Toolbar button, popup and chat behave like DataGrid (sorting)', async (t) => { + const treeList = new TreeList(GRID_SELECTOR); + + await t.expect(treeList.isReady()).ok(); + await t.expect(treeList.getAIAssistantButton().exists).ok(); + + await t.click(treeList.getAIAssistantButton()); + + const aiChat = treeList.getAIAssistantChat(); + + await t.expect(aiChat.element.visible).ok(); + + await t + .typeText(aiChat.getInput(), 'Sort by name') + .pressKey('enter'); + + await t.expect(aiChat.getSuccessMessages().count).eql(1); + await t.expect(aiChat.getSuccessActionItems(0).count).eql(1); + await t.expect(treeList.apiColumnOption('name', 'sortOrder')).eql('asc'); +}).before(async () => createWidget('dxTreeList', () => ({ + dataSource: [ + { + id: 1, parentId: 0, name: 'Alice', value: 30, + }, + { + id: 2, parentId: 1, name: 'Bob', value: 20, + }, + { + id: 3, parentId: 1, name: 'Charlie', value: 10, + }, + { + id: 4, parentId: 0, name: 'Dave', value: 40, + }, + ], + keyExpr: 'id', + parentIdExpr: 'parentId', + autoExpandAll: true, + columns: ['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 => {}, + }; + }, + }), + }, +}))); + +// 6.1.2 +test('Searching behaves like DataGrid', async (t) => { + const treeList = new TreeList(GRID_SELECTOR); + + await t.expect(treeList.isReady()).ok(); + + await t.click(treeList.getAIAssistantButton()); + + const aiChat = treeList.getAIAssistantChat(); + + await t + .typeText(aiChat.getInput(), 'Search for Bob') + .pressKey('enter'); + + await t.expect(aiChat.getSuccessMessages().count).eql(1); + await t.expect(treeList.apiOption('searchPanel.text')).eql('Bob'); +}).before(async () => createWidget('dxTreeList', () => ({ + dataSource: [ + { + id: 1, parentId: 0, name: 'Alice', value: 30, + }, + { + id: 2, parentId: 1, name: 'Bob', value: 20, + }, + { + id: 3, parentId: 1, name: 'Charlie', value: 10, + }, + { + id: 4, parentId: 0, name: 'Dave', value: 40, + }, + ], + keyExpr: 'id', + parentIdExpr: 'parentId', + autoExpandAll: true, + columns: ['name', 'value'], + showBorders: true, + aiAssistant: { + enabled: true, + aiIntegration: new (window as any).DevExpress.aiIntegration.AIIntegration({ + sendRequest() { + return { + promise: Promise.resolve({ + actions: [{ name: 'searching', args: { text: 'Bob' } }], + }), + abort: (): void => {}, + }; + }, + }), + }, +}))); + +// 6.1.3 +test('Paging (pageSize) behaves like DataGrid', async (t) => { + const treeList = new TreeList(GRID_SELECTOR); + + await t.expect(treeList.isReady()).ok(); + + await t.click(treeList.getAIAssistantButton()); + + const aiChat = treeList.getAIAssistantChat(); + + await t + .typeText(aiChat.getInput(), 'Show 50 rows per page') + .pressKey('enter'); + + await t.expect(aiChat.getSuccessMessages().count).eql(1); + await t.expect(treeList.apiOption('paging.pageSize')).eql(50); +}).before(async () => createWidget('dxTreeList', () => ({ + dataSource: [ + { + id: 1, parentId: 0, name: 'Alice', value: 30, + }, + { + id: 2, parentId: 1, name: 'Bob', value: 20, + }, + { + id: 3, parentId: 1, name: 'Charlie', value: 10, + }, + { + id: 4, parentId: 0, name: 'Dave', value: 40, + }, + ], + keyExpr: 'id', + parentIdExpr: 'parentId', + autoExpandAll: true, + columns: ['name', 'value'], + showBorders: true, + paging: { enabled: true, pageSize: 10 }, + aiAssistant: { + enabled: true, + aiIntegration: new (window as any).DevExpress.aiIntegration.AIIntegration({ + sendRequest() { + return { + promise: Promise.resolve({ + actions: [{ name: 'pageSize', args: { pageSize: 50 } }], + }), + abort: (): void => {}, + }; + }, + }), + }, +}))); + +// 6.1.4 +// Column visibility behaves like DataGrid. columnsReorder is broken on TreeList (issue 4294), +// so reordering parity is not exercised here. +test('Column visibility behaves like DataGrid', async (t) => { + const treeList = new TreeList(GRID_SELECTOR); + + await t.expect(treeList.isReady()).ok(); + + await t.click(treeList.getAIAssistantButton()); + + const aiChat = treeList.getAIAssistantChat(); + + await t + .typeText(aiChat.getInput(), 'Hide value column') + .pressKey('enter'); + + await t.expect(aiChat.getSuccessMessages().count).eql(1); + await t.expect(treeList.apiColumnOption('value', 'visible')).eql(false); +}).before(async () => createWidget('dxTreeList', () => ({ + dataSource: [ + { + id: 1, parentId: 0, name: 'Alice', value: 30, + }, + { + id: 2, parentId: 1, name: 'Bob', value: 20, + }, + { + id: 3, parentId: 1, name: 'Charlie', value: 10, + }, + { + id: 4, parentId: 0, name: 'Dave', value: 40, + }, + ], + keyExpr: 'id', + parentIdExpr: 'parentId', + autoExpandAll: true, + columns: ['name', 'value'], + showBorders: true, + columnChooser: { enabled: true }, + aiAssistant: { + enabled: true, + aiIntegration: new (window as any).DevExpress.aiIntegration.AIIntegration({ + sendRequest() { + return { + promise: Promise.resolve({ + actions: [{ name: 'columnsVisibility', args: { dataField: 'value', visible: false } }], + }), + abort: (): void => {}, + }; + }, + }), + }, +}))); + +// 6.1.5 +test('Selection (selectByKeys) behaves like DataGrid', async (t) => { + const treeList = new TreeList(GRID_SELECTOR); + + await t.expect(treeList.isReady()).ok(); + + await t.click(treeList.getAIAssistantButton()); + + const aiChat = treeList.getAIAssistantChat(); + + await t + .typeText(aiChat.getInput(), 'Select Bob') + .pressKey('enter'); + + await t.expect(aiChat.getSuccessMessages().count).eql(1); + await t.expect(treeList.apiOption('selectedRowKeys')).eql([2]); +}).before(async () => createWidget('dxTreeList', () => ({ + dataSource: [ + { + id: 1, parentId: 0, name: 'Alice', value: 30, + }, + { + id: 2, parentId: 1, name: 'Bob', value: 20, + }, + { + id: 3, parentId: 1, name: 'Charlie', value: 10, + }, + { + id: 4, parentId: 0, name: 'Dave', value: 40, + }, + ], + keyExpr: 'id', + parentIdExpr: 'parentId', + autoExpandAll: true, + columns: ['name', 'value'], + showBorders: true, + selection: { mode: 'multiple', recursive: false }, + aiAssistant: { + enabled: true, + aiIntegration: new (window as any).DevExpress.aiIntegration.AIIntegration({ + sendRequest() { + return { + promise: Promise.resolve({ + actions: [{ name: 'selectByKeys', args: { keys: [2], preserve: false } }], + }), + abort: (): void => {}, + }; + }, + }), + }, +}))); + +// 6.1.6 +test('Row focusing (focusRowByKey) behaves like DataGrid', async (t) => { + const treeList = new TreeList(GRID_SELECTOR); + + await t.expect(treeList.isReady()).ok(); + + await t.click(treeList.getAIAssistantButton()); + + const aiChat = treeList.getAIAssistantChat(); + + await t + .typeText(aiChat.getInput(), 'Focus Bob') + .pressKey('enter'); + + await t.expect(aiChat.getSuccessMessages().count).eql(1); + await t.expect(treeList.apiOption('focusedRowKey')).eql(2); +}).before(async () => createWidget('dxTreeList', () => ({ + dataSource: [ + { + id: 1, parentId: 0, name: 'Alice', value: 30, + }, + { + id: 2, parentId: 1, name: 'Bob', value: 20, + }, + { + id: 3, parentId: 1, name: 'Charlie', value: 10, + }, + { + id: 4, parentId: 0, name: 'Dave', value: 40, + }, + ], + keyExpr: 'id', + parentIdExpr: 'parentId', + autoExpandAll: true, + focusedRowEnabled: true, + columns: ['name', 'value'], + showBorders: true, + aiAssistant: { + enabled: true, + aiIntegration: new (window as any).DevExpress.aiIntegration.AIIntegration({ + sendRequest() { + return { + promise: Promise.resolve({ + actions: [{ name: 'focusRowByKey', args: { key: 2 } }], + }), + abort: (): void => {}, + }; + }, + }), + }, +}))); + +// 6.1.8 +test('Error paths behave like DataGrid (invalid response)', async (t) => { + const treeList = new TreeList(GRID_SELECTOR); + + await t.expect(treeList.isReady()).ok(); + + await t.click(treeList.getAIAssistantButton()); + + const aiChat = treeList.getAIAssistantChat(); + + await t + .typeText(aiChat.getInput(), 'Sort by name') + .pressKey('enter'); + + await t.expect(aiChat.getMessages().count).eql(2); + await t.expect(aiChat.getErrorMessages().count).eql(1); + await t.expect(aiChat.getSuccessMessages().count).eql(0); + await t.expect(aiChat.getActionItems(0).count).eql(0); + await t.expect(treeList.apiColumnOption('name', 'sortOrder')).notOk(); +}).before(async () => createWidget('dxTreeList', () => ({ + dataSource: [ + { + id: 1, parentId: 0, name: 'Alice', value: 30, + }, + { + id: 2, parentId: 1, name: 'Bob', value: 20, + }, + ], + keyExpr: 'id', + parentIdExpr: 'parentId', + autoExpandAll: true, + columns: ['name', 'value'], + showBorders: true, + aiAssistant: { + enabled: true, + aiIntegration: new (window as any).DevExpress.aiIntegration.AIIntegration({ + sendRequest() { + return { + promise: Promise.resolve({ actions: null }), + abort: (): void => {}, + }; + }, + }), + }, +}))); + +// 6.1.9 +test('In-flight lock behaves like DataGrid', async (t) => { + const treeList = new TreeList(GRID_SELECTOR); + + await t.expect(treeList.isReady()).ok(); + + await t.click(treeList.getAIAssistantButton()); + + const aiChat = treeList.getAIAssistantChat(); + + await t + .typeText(aiChat.getInput(), 'Sort by name') + .pressKey('enter'); + + await t.expect(aiChat.getPendingMessages().count).eql(1); + await t.expect(aiChat.isInputDisabled()).ok(); + await t.expect(aiChat.isClearChatDisabled()).ok(); +}).before(async () => createWidget('dxTreeList', () => ({ + dataSource: [ + { + id: 1, parentId: 0, name: 'Alice', value: 30, + }, + { + id: 2, parentId: 1, name: 'Bob', value: 20, + }, + ], + keyExpr: 'id', + parentIdExpr: 'parentId', + autoExpandAll: true, + columns: ['name', 'value'], + showBorders: true, + aiAssistant: { + enabled: true, + aiIntegration: new (window as any).DevExpress.aiIntegration.AIIntegration({ + sendRequest() { + return { + promise: new Promise(() => {}), + abort: (): void => {}, + }; + }, + }), + }, +}))); + +// === §6.2 DataGrid-only commands are not offered for TreeList === + +fixture.disablePageReloads`AI Assistant - TreeList Schema` + .page(AI_INTEGRATION_PAGE); + +// 6.2.1 +test('grouping command is absent from the TreeList response schema', async (t) => { + const treeList = new TreeList(GRID_SELECTOR); + + await t.expect(treeList.isReady()).ok(); + + await t.click(treeList.getAIAssistantButton()); + + const aiChat = treeList.getAIAssistantChat(); + + await t + .typeText(aiChat.getInput(), 'Sort by name') + .pressKey('enter'); + + await t.expect(aiChat.getMessages().count).eql(2); + + const commandNames = await getSchemaCommandNames(); + + await t.expect(commandNames).contains('sorting'); + await t.expect(commandNames).notContains('grouping'); + await t.expect(commandNames).notContains('clearGrouping'); +}).before(async () => createWidget('dxTreeList', () => { + (window as any).__aiRequests = []; + + return { + dataSource: [ + { + id: 1, parentId: 0, name: 'Alice', value: 30, + }, + { + id: 2, parentId: 1, name: 'Bob', value: 20, + }, + ], + keyExpr: 'id', + parentIdExpr: 'parentId', + autoExpandAll: true, + columns: ['name', 'value'], + showBorders: true, + aiAssistant: { + enabled: true, + aiIntegration: new (window as any).DevExpress.aiIntegration.AIIntegration({ + sendRequest(params: any) { + (window as any).__aiRequests.push(params); + + return { + promise: Promise.resolve({ + actions: [{ name: 'sorting', args: { dataField: 'name', sortOrder: 'asc' } }], + }), + abort: (): void => {}, + }; + }, + }), + }, + }; +})); + +// 6.2.2 +test('summary commands are absent from the TreeList response schema', async (t) => { + const treeList = new TreeList(GRID_SELECTOR); + + await t.expect(treeList.isReady()).ok(); + + await t.click(treeList.getAIAssistantButton()); + + const aiChat = treeList.getAIAssistantChat(); + + await t + .typeText(aiChat.getInput(), 'Sort by name') + .pressKey('enter'); + + await t.expect(aiChat.getMessages().count).eql(2); + + const commandNames = await getSchemaCommandNames(); + + await t.expect(commandNames).notContains('summary'); + await t.expect(commandNames).notContains('clearSummary'); +}).before(async () => createWidget('dxTreeList', () => { + (window as any).__aiRequests = []; + + return { + dataSource: [ + { + id: 1, parentId: 0, name: 'Alice', value: 30, + }, + { + id: 2, parentId: 1, name: 'Bob', value: 20, + }, + ], + keyExpr: 'id', + parentIdExpr: 'parentId', + autoExpandAll: true, + columns: ['name', 'value'], + showBorders: true, + aiAssistant: { + enabled: true, + aiIntegration: new (window as any).DevExpress.aiIntegration.AIIntegration({ + sendRequest(params: any) { + (window as any).__aiRequests.push(params); + + return { + promise: Promise.resolve({ + actions: [{ name: 'sorting', args: { dataField: 'name', sortOrder: 'asc' } }], + }), + abort: (): void => {}, + }; + }, + }), + }, + }; +})); + +// === §6.3 Hierarchical-data peculiarities === + +fixture.disablePageReloads`AI Assistant - TreeList Hierarchy` + .page(AI_INTEGRATION_PAGE); + +// 6.3.1 +test('Filtering applies on hierarchical data', async (t) => { + const treeList = new TreeList(GRID_SELECTOR); + + await t.expect(treeList.isReady()).ok(); + + await t.click(treeList.getAIAssistantButton()); + + const aiChat = treeList.getAIAssistantChat(); + + await t + .typeText(aiChat.getInput(), 'Filter value greater than 15') + .pressKey('enter'); + + await t.expect(aiChat.getSuccessMessages().count).eql(1); + await t.expect(treeList.apiOption('filterValue')).eql(['value', '>', 15]); +}).before(async () => createWidget('dxTreeList', () => ({ + dataSource: [ + { + id: 1, parentId: 0, name: 'Alice', value: 30, + }, + { + id: 2, parentId: 1, name: 'Bob', value: 20, + }, + { + id: 3, parentId: 1, name: 'Charlie', value: 10, + }, + { + id: 4, parentId: 0, name: 'Dave', value: 40, + }, + ], + keyExpr: 'id', + parentIdExpr: 'parentId', + autoExpandAll: true, + columns: ['name', 'value'], + showBorders: true, + aiAssistant: { + enabled: true, + aiIntegration: new (window as any).DevExpress.aiIntegration.AIIntegration({ + sendRequest() { + return { + promise: Promise.resolve({ + actions: [{ + name: 'filterValue', + args: { + expression: { + type: 'basic', field: 'value', operator: '>', value: 15, + }, + }, + }], + }), + abort: (): void => {}, + }; + }, + }), + }, +}))); + +// 6.3.2 +test('Sorting applies with nested children', async (t) => { + const treeList = new TreeList(GRID_SELECTOR); + + await t.expect(treeList.isReady()).ok(); + + await t.click(treeList.getAIAssistantButton()); + + const aiChat = treeList.getAIAssistantChat(); + + await t + .typeText(aiChat.getInput(), 'Sort by value descending') + .pressKey('enter'); + + await t.expect(aiChat.getSuccessMessages().count).eql(1); + await t.expect(treeList.apiColumnOption('value', 'sortOrder')).eql('desc'); +}).before(async () => createWidget('dxTreeList', () => ({ + dataSource: [ + { + id: 1, parentId: 0, name: 'Alice', value: 30, + }, + { + id: 2, parentId: 1, name: 'Bob', value: 20, + }, + { + id: 3, parentId: 1, name: 'Charlie', value: 10, + }, + { + id: 4, parentId: 0, name: 'Dave', value: 40, + }, + ], + keyExpr: 'id', + parentIdExpr: 'parentId', + autoExpandAll: true, + columns: ['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: 'value', sortOrder: 'desc' } }], + }), + abort: (): void => {}, + }; + }, + }), + }, +}))); + +// 6.3.3 +test('Selecting a parent row applies per TreeList selection rules', async (t) => { + const treeList = new TreeList(GRID_SELECTOR); + + await t.expect(treeList.isReady()).ok(); + + await t.click(treeList.getAIAssistantButton()); + + const aiChat = treeList.getAIAssistantChat(); + + await t + .typeText(aiChat.getInput(), 'Select Alice') + .pressKey('enter'); + + await t.expect(aiChat.getSuccessMessages().count).eql(1); + await t.expect(treeList.apiOption('selectedRowKeys')).eql([1]); +}).before(async () => createWidget('dxTreeList', () => ({ + dataSource: [ + { + id: 1, parentId: 0, name: 'Alice', value: 30, + }, + { + id: 2, parentId: 1, name: 'Bob', value: 20, + }, + { + id: 3, parentId: 1, name: 'Charlie', value: 10, + }, + { + id: 4, parentId: 0, name: 'Dave', value: 40, + }, + ], + keyExpr: 'id', + parentIdExpr: 'parentId', + autoExpandAll: true, + columns: ['name', 'value'], + showBorders: true, + selection: { mode: 'multiple', recursive: false }, + aiAssistant: { + enabled: true, + aiIntegration: new (window as any).DevExpress.aiIntegration.AIIntegration({ + sendRequest() { + return { + promise: Promise.resolve({ + actions: [{ name: 'selectByKeys', args: { keys: [1], preserve: false } }], + }), + 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 e5a7d1e5d7ed32df3e6ce20b6f768d2c5364009c Mon Sep 17 00:00:00 2001 From: Alyar <> Date: Fri, 5 Jun 2026 17:29:15 +0400 Subject: [PATCH 2/7] DataGrid - AI Assistant: refactor parity e2e tests for consistency --- .../common/treeList/aiAssistant/functional.ts | 633 +++++------------- packages/testcafe-models/chat.ts | 6 +- .../dataGrid/aiAssistantChat.ts | 53 +- packages/testcafe-models/dataGrid/index.ts | 20 - 4 files changed, 187 insertions(+), 525 deletions(-) diff --git a/e2e/testcafe-devextreme/tests/common/treeList/aiAssistant/functional.ts b/e2e/testcafe-devextreme/tests/common/treeList/aiAssistant/functional.ts index 9597b3701ece..1f4e13c7d5a8 100644 --- a/e2e/testcafe-devextreme/tests/common/treeList/aiAssistant/functional.ts +++ b/e2e/testcafe-devextreme/tests/common/treeList/aiAssistant/functional.ts @@ -6,17 +6,88 @@ import url from '../../../../helpers/getPageUrl'; const GRID_SELECTOR = '#container'; const AI_INTEGRATION_PAGE = url(__dirname, '../../../container-ai-integration.html'); - -// TreeList reuses grid_core, so the AI Assistant feature is expected to behave like DataGrid. -// These tests verify that parity and the TreeList-only differences (no grouping/summary commands). +const PENDING = '__pending__'; +const hierarchyRows = [ + { + id: 1, parentId: 0, name: 'Alice', value: 30, + }, + { + id: 2, parentId: 1, name: 'Bob', value: 20, + }, + { + id: 3, parentId: 1, name: 'Charlie', value: 10, + }, + { + id: 4, parentId: 0, name: 'Dave', value: 40, + }, +]; const getSchemaCommandNames = ClientFunction(() => ((window as any).__aiRequests[0] .data.responseSchema.properties.actions.items.anyOf as any[]) .map((branch) => branch.properties.name.enum[0])); +const setupAIState = ClientFunction((base: Record, responses: unknown[]) => { + (window as any).__aiBase = base; + (window as any).__aiResponses = responses; + (window as any).__aiCallCount = 0; + (window as any).__aiRequests = []; +}); + +const aiTreeListOptions = (): any => ({ + ...(window as any).__aiBase, + aiAssistant: { + enabled: true, + aiIntegration: new (window as any).DevExpress.aiIntegration.AIIntegration({ + sendRequest(params: any) { + const w = window as any; + + w.__aiRequests.push(params); + + const response = w.__aiResponses[w.__aiCallCount]; + + w.__aiCallCount += 1; + + if (response === '__pending__') { + return { promise: new Promise(() => {}), abort: (): void => {} }; + } + + if (response === undefined) { + return { + promise: Promise.reject(new Error('Unexpected AI call')), + abort: (): void => {}, + }; + } + + return { promise: Promise.resolve(response), abort: (): void => {} }; + }, + }), + }, +}); + +const treeListBase = ( + overrides: Record = {}, +): Record => ({ + dataSource: hierarchyRows, + keyExpr: 'id', + parentIdExpr: 'parentId', + autoExpandAll: true, + columns: ['name', 'value'], + showBorders: true, + ...overrides, +}); + +const createTreeListWithAIAssistant = async ( + base: Record, + responses: unknown[], +): Promise => { + await setupAIState(base, responses); + + return createWidget('dxTreeList', aiTreeListOptions); +}; + // === §6.1 grid_core-level commands behave identically on TreeList === -fixture.disablePageReloads`AI Assistant - TreeList Parity` +fixture`AI Assistant - TreeList Parity` .page(AI_INTEGRATION_PAGE); // 6.1.1 @@ -38,41 +109,10 @@ test('Toolbar button, popup and chat behave like DataGrid (sorting)', async (t) await t.expect(aiChat.getSuccessMessages().count).eql(1); await t.expect(aiChat.getSuccessActionItems(0).count).eql(1); - await t.expect(treeList.apiColumnOption('name', 'sortOrder')).eql('asc'); -}).before(async () => createWidget('dxTreeList', () => ({ - dataSource: [ - { - id: 1, parentId: 0, name: 'Alice', value: 30, - }, - { - id: 2, parentId: 1, name: 'Bob', value: 20, - }, - { - id: 3, parentId: 1, name: 'Charlie', value: 10, - }, - { - id: 4, parentId: 0, name: 'Dave', value: 40, - }, - ], - keyExpr: 'id', - parentIdExpr: 'parentId', - autoExpandAll: true, - columns: ['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 t.expect(await treeList.apiColumnOption('name', 'sortOrder')).eql('asc'); +}).before(async () => createTreeListWithAIAssistant(treeListBase(), [ + { actions: [{ name: 'sorting', args: { dataField: 'name', sortOrder: 'asc' } }] }, +])); // 6.1.2 test('Searching behaves like DataGrid', async (t) => { @@ -89,41 +129,11 @@ test('Searching behaves like DataGrid', async (t) => { .pressKey('enter'); await t.expect(aiChat.getSuccessMessages().count).eql(1); - await t.expect(treeList.apiOption('searchPanel.text')).eql('Bob'); -}).before(async () => createWidget('dxTreeList', () => ({ - dataSource: [ - { - id: 1, parentId: 0, name: 'Alice', value: 30, - }, - { - id: 2, parentId: 1, name: 'Bob', value: 20, - }, - { - id: 3, parentId: 1, name: 'Charlie', value: 10, - }, - { - id: 4, parentId: 0, name: 'Dave', value: 40, - }, - ], - keyExpr: 'id', - parentIdExpr: 'parentId', - autoExpandAll: true, - columns: ['name', 'value'], - showBorders: true, - aiAssistant: { - enabled: true, - aiIntegration: new (window as any).DevExpress.aiIntegration.AIIntegration({ - sendRequest() { - return { - promise: Promise.resolve({ - actions: [{ name: 'searching', args: { text: 'Bob' } }], - }), - abort: (): void => {}, - }; - }, - }), - }, -}))); + await t.expect(aiChat.getSuccessActionItems(0).count).eql(1); + await t.expect(await treeList.apiOption('searchPanel.text')).eql('Bob'); +}).before(async () => createTreeListWithAIAssistant(treeListBase(), [ + { actions: [{ name: 'searching', args: { text: 'Bob' } }] }, +])); // 6.1.3 test('Paging (pageSize) behaves like DataGrid', async (t) => { @@ -140,46 +150,14 @@ test('Paging (pageSize) behaves like DataGrid', async (t) => { .pressKey('enter'); await t.expect(aiChat.getSuccessMessages().count).eql(1); - await t.expect(treeList.apiOption('paging.pageSize')).eql(50); -}).before(async () => createWidget('dxTreeList', () => ({ - dataSource: [ - { - id: 1, parentId: 0, name: 'Alice', value: 30, - }, - { - id: 2, parentId: 1, name: 'Bob', value: 20, - }, - { - id: 3, parentId: 1, name: 'Charlie', value: 10, - }, - { - id: 4, parentId: 0, name: 'Dave', value: 40, - }, - ], - keyExpr: 'id', - parentIdExpr: 'parentId', - autoExpandAll: true, - columns: ['name', 'value'], - showBorders: true, - paging: { enabled: true, pageSize: 10 }, - aiAssistant: { - enabled: true, - aiIntegration: new (window as any).DevExpress.aiIntegration.AIIntegration({ - sendRequest() { - return { - promise: Promise.resolve({ - actions: [{ name: 'pageSize', args: { pageSize: 50 } }], - }), - abort: (): void => {}, - }; - }, - }), - }, -}))); + await t.expect(aiChat.getSuccessActionItems(0).count).eql(1); + await t.expect(await treeList.apiOption('paging.pageSize')).eql(50); +}).before(async () => createTreeListWithAIAssistant( + treeListBase({ paging: { enabled: true, pageSize: 10 } }), + [{ actions: [{ name: 'pageSize', args: { pageSize: 50 } }] }], +)); // 6.1.4 -// Column visibility behaves like DataGrid. columnsReorder is broken on TreeList (issue 4294), -// so reordering parity is not exercised here. test('Column visibility behaves like DataGrid', async (t) => { const treeList = new TreeList(GRID_SELECTOR); @@ -194,42 +172,12 @@ test('Column visibility behaves like DataGrid', async (t) => { .pressKey('enter'); await t.expect(aiChat.getSuccessMessages().count).eql(1); - await t.expect(treeList.apiColumnOption('value', 'visible')).eql(false); -}).before(async () => createWidget('dxTreeList', () => ({ - dataSource: [ - { - id: 1, parentId: 0, name: 'Alice', value: 30, - }, - { - id: 2, parentId: 1, name: 'Bob', value: 20, - }, - { - id: 3, parentId: 1, name: 'Charlie', value: 10, - }, - { - id: 4, parentId: 0, name: 'Dave', value: 40, - }, - ], - keyExpr: 'id', - parentIdExpr: 'parentId', - autoExpandAll: true, - columns: ['name', 'value'], - showBorders: true, - columnChooser: { enabled: true }, - aiAssistant: { - enabled: true, - aiIntegration: new (window as any).DevExpress.aiIntegration.AIIntegration({ - sendRequest() { - return { - promise: Promise.resolve({ - actions: [{ name: 'columnsVisibility', args: { dataField: 'value', visible: false } }], - }), - abort: (): void => {}, - }; - }, - }), - }, -}))); + await t.expect(aiChat.getSuccessActionItems(0).count).eql(1); + await t.expect(await treeList.apiColumnOption('value', 'visible')).eql(false); +}).before(async () => createTreeListWithAIAssistant( + treeListBase({ columnChooser: { enabled: true } }), + [{ actions: [{ name: 'columnsVisibility', args: { dataField: 'value', visible: false } }] }], +)); // 6.1.5 test('Selection (selectByKeys) behaves like DataGrid', async (t) => { @@ -246,42 +194,12 @@ test('Selection (selectByKeys) behaves like DataGrid', async (t) => { .pressKey('enter'); await t.expect(aiChat.getSuccessMessages().count).eql(1); - await t.expect(treeList.apiOption('selectedRowKeys')).eql([2]); -}).before(async () => createWidget('dxTreeList', () => ({ - dataSource: [ - { - id: 1, parentId: 0, name: 'Alice', value: 30, - }, - { - id: 2, parentId: 1, name: 'Bob', value: 20, - }, - { - id: 3, parentId: 1, name: 'Charlie', value: 10, - }, - { - id: 4, parentId: 0, name: 'Dave', value: 40, - }, - ], - keyExpr: 'id', - parentIdExpr: 'parentId', - autoExpandAll: true, - columns: ['name', 'value'], - showBorders: true, - selection: { mode: 'multiple', recursive: false }, - aiAssistant: { - enabled: true, - aiIntegration: new (window as any).DevExpress.aiIntegration.AIIntegration({ - sendRequest() { - return { - promise: Promise.resolve({ - actions: [{ name: 'selectByKeys', args: { keys: [2], preserve: false } }], - }), - abort: (): void => {}, - }; - }, - }), - }, -}))); + await t.expect(aiChat.getSuccessActionItems(0).count).eql(1); + await t.expect(await treeList.apiOption('selectedRowKeys')).eql([2]); +}).before(async () => createTreeListWithAIAssistant( + treeListBase({ selection: { mode: 'multiple', recursive: false } }), + [{ actions: [{ name: 'selectByKeys', args: { keys: [2], preserve: false } }] }], +)); // 6.1.6 test('Row focusing (focusRowByKey) behaves like DataGrid', async (t) => { @@ -298,42 +216,12 @@ test('Row focusing (focusRowByKey) behaves like DataGrid', async (t) => { .pressKey('enter'); await t.expect(aiChat.getSuccessMessages().count).eql(1); - await t.expect(treeList.apiOption('focusedRowKey')).eql(2); -}).before(async () => createWidget('dxTreeList', () => ({ - dataSource: [ - { - id: 1, parentId: 0, name: 'Alice', value: 30, - }, - { - id: 2, parentId: 1, name: 'Bob', value: 20, - }, - { - id: 3, parentId: 1, name: 'Charlie', value: 10, - }, - { - id: 4, parentId: 0, name: 'Dave', value: 40, - }, - ], - keyExpr: 'id', - parentIdExpr: 'parentId', - autoExpandAll: true, - focusedRowEnabled: true, - columns: ['name', 'value'], - showBorders: true, - aiAssistant: { - enabled: true, - aiIntegration: new (window as any).DevExpress.aiIntegration.AIIntegration({ - sendRequest() { - return { - promise: Promise.resolve({ - actions: [{ name: 'focusRowByKey', args: { key: 2 } }], - }), - abort: (): void => {}, - }; - }, - }), - }, -}))); + await t.expect(aiChat.getSuccessActionItems(0).count).eql(1); + await t.expect(await treeList.apiOption('focusedRowKey')).eql(2); +}).before(async () => createTreeListWithAIAssistant( + treeListBase({ focusedRowEnabled: true }), + [{ actions: [{ name: 'focusRowByKey', args: { key: 2 } }] }], +)); // 6.1.8 test('Error paths behave like DataGrid (invalid response)', async (t) => { @@ -349,37 +237,13 @@ test('Error paths behave like DataGrid (invalid response)', async (t) => { .typeText(aiChat.getInput(), 'Sort by name') .pressKey('enter'); - await t.expect(aiChat.getMessages().count).eql(2); await t.expect(aiChat.getErrorMessages().count).eql(1); await t.expect(aiChat.getSuccessMessages().count).eql(0); await t.expect(aiChat.getActionItems(0).count).eql(0); - await t.expect(treeList.apiColumnOption('name', 'sortOrder')).notOk(); -}).before(async () => createWidget('dxTreeList', () => ({ - dataSource: [ - { - id: 1, parentId: 0, name: 'Alice', value: 30, - }, - { - id: 2, parentId: 1, name: 'Bob', value: 20, - }, - ], - keyExpr: 'id', - parentIdExpr: 'parentId', - autoExpandAll: true, - columns: ['name', 'value'], - showBorders: true, - aiAssistant: { - enabled: true, - aiIntegration: new (window as any).DevExpress.aiIntegration.AIIntegration({ - sendRequest() { - return { - promise: Promise.resolve({ actions: null }), - abort: (): void => {}, - }; - }, - }), - }, -}))); + await t.expect(await treeList.apiColumnOption('name', 'sortOrder')).notOk(); +}).before(async () => createTreeListWithAIAssistant(treeListBase(), [ + { actions: null }, +])); // 6.1.9 test('In-flight lock behaves like DataGrid', async (t) => { @@ -398,36 +262,11 @@ test('In-flight lock behaves like DataGrid', async (t) => { await t.expect(aiChat.getPendingMessages().count).eql(1); await t.expect(aiChat.isInputDisabled()).ok(); await t.expect(aiChat.isClearChatDisabled()).ok(); -}).before(async () => createWidget('dxTreeList', () => ({ - dataSource: [ - { - id: 1, parentId: 0, name: 'Alice', value: 30, - }, - { - id: 2, parentId: 1, name: 'Bob', value: 20, - }, - ], - keyExpr: 'id', - parentIdExpr: 'parentId', - autoExpandAll: true, - columns: ['name', 'value'], - showBorders: true, - aiAssistant: { - enabled: true, - aiIntegration: new (window as any).DevExpress.aiIntegration.AIIntegration({ - sendRequest() { - return { - promise: new Promise(() => {}), - abort: (): void => {}, - }; - }, - }), - }, -}))); +}).before(async () => createTreeListWithAIAssistant(treeListBase(), [PENDING])); // === §6.2 DataGrid-only commands are not offered for TreeList === -fixture.disablePageReloads`AI Assistant - TreeList Schema` +fixture`AI Assistant - TreeList Schema` .page(AI_INTEGRATION_PAGE); // 6.2.1 @@ -444,47 +283,16 @@ test('grouping command is absent from the TreeList response schema', async (t) = .typeText(aiChat.getInput(), 'Sort by name') .pressKey('enter'); - await t.expect(aiChat.getMessages().count).eql(2); + await t.expect(aiChat.getSuccessMessages().count).eql(1); const commandNames = await getSchemaCommandNames(); await t.expect(commandNames).contains('sorting'); await t.expect(commandNames).notContains('grouping'); await t.expect(commandNames).notContains('clearGrouping'); -}).before(async () => createWidget('dxTreeList', () => { - (window as any).__aiRequests = []; - - return { - dataSource: [ - { - id: 1, parentId: 0, name: 'Alice', value: 30, - }, - { - id: 2, parentId: 1, name: 'Bob', value: 20, - }, - ], - keyExpr: 'id', - parentIdExpr: 'parentId', - autoExpandAll: true, - columns: ['name', 'value'], - showBorders: true, - aiAssistant: { - enabled: true, - aiIntegration: new (window as any).DevExpress.aiIntegration.AIIntegration({ - sendRequest(params: any) { - (window as any).__aiRequests.push(params); - - return { - promise: Promise.resolve({ - actions: [{ name: 'sorting', args: { dataField: 'name', sortOrder: 'asc' } }], - }), - abort: (): void => {}, - }; - }, - }), - }, - }; -})); +}).before(async () => createTreeListWithAIAssistant(treeListBase(), [ + { actions: [{ name: 'sorting', args: { dataField: 'name', sortOrder: 'asc' } }] }, +])); // 6.2.2 test('summary commands are absent from the TreeList response schema', async (t) => { @@ -500,50 +308,20 @@ test('summary commands are absent from the TreeList response schema', async (t) .typeText(aiChat.getInput(), 'Sort by name') .pressKey('enter'); - await t.expect(aiChat.getMessages().count).eql(2); + await t.expect(aiChat.getSuccessMessages().count).eql(1); const commandNames = await getSchemaCommandNames(); + await t.expect(commandNames).contains('sorting'); await t.expect(commandNames).notContains('summary'); await t.expect(commandNames).notContains('clearSummary'); -}).before(async () => createWidget('dxTreeList', () => { - (window as any).__aiRequests = []; - - return { - dataSource: [ - { - id: 1, parentId: 0, name: 'Alice', value: 30, - }, - { - id: 2, parentId: 1, name: 'Bob', value: 20, - }, - ], - keyExpr: 'id', - parentIdExpr: 'parentId', - autoExpandAll: true, - columns: ['name', 'value'], - showBorders: true, - aiAssistant: { - enabled: true, - aiIntegration: new (window as any).DevExpress.aiIntegration.AIIntegration({ - sendRequest(params: any) { - (window as any).__aiRequests.push(params); - - return { - promise: Promise.resolve({ - actions: [{ name: 'sorting', args: { dataField: 'name', sortOrder: 'asc' } }], - }), - abort: (): void => {}, - }; - }, - }), - }, - }; -})); +}).before(async () => createTreeListWithAIAssistant(treeListBase(), [ + { actions: [{ name: 'sorting', args: { dataField: 'name', sortOrder: 'asc' } }] }, +])); // === §6.3 Hierarchical-data peculiarities === -fixture.disablePageReloads`AI Assistant - TreeList Hierarchy` +fixture`AI Assistant - TreeList Hierarchy` .page(AI_INTEGRATION_PAGE); // 6.3.1 @@ -561,51 +339,32 @@ test('Filtering applies on hierarchical data', async (t) => { .pressKey('enter'); await t.expect(aiChat.getSuccessMessages().count).eql(1); - await t.expect(treeList.apiOption('filterValue')).eql(['value', '>', 15]); -}).before(async () => createWidget('dxTreeList', () => ({ - dataSource: [ - { - id: 1, parentId: 0, name: 'Alice', value: 30, - }, - { - id: 2, parentId: 1, name: 'Bob', value: 20, - }, - { - id: 3, parentId: 1, name: 'Charlie', value: 10, - }, - { - id: 4, parentId: 0, name: 'Dave', value: 40, - }, - ], - keyExpr: 'id', - parentIdExpr: 'parentId', - autoExpandAll: true, - columns: ['name', 'value'], - showBorders: true, - aiAssistant: { - enabled: true, - aiIntegration: new (window as any).DevExpress.aiIntegration.AIIntegration({ - sendRequest() { - return { - promise: Promise.resolve({ - actions: [{ - name: 'filterValue', - args: { - expression: { - type: 'basic', field: 'value', operator: '>', value: 15, - }, - }, - }], - }), - abort: (): void => {}, - }; + await t.expect(aiChat.getSuccessActionItems(0).count).eql(1); + await t.expect(await treeList.apiOption('filterValue')).eql(['value', '>', 15]); +}).before(async () => createTreeListWithAIAssistant(treeListBase(), [ + { + actions: [{ + name: 'filterValue', + args: { + expression: { + rootId: 'n1', + nodes: [{ + id: 'n1', + expr: { + type: 'basic', field: 'value', operator: '>', value: 15, + }, + }], + }, }, - }), + }], }, -}))); +])); // 6.3.2 -test('Sorting applies with nested children', async (t) => { +// Children must sort within their parent, not globally. The dataset is chosen so a flat sort +// and a hierarchical sort produce a different visible order: flat desc by value would be +// Bob(100), Charlie(50), Dave(10), Alice(5); hierarchical keeps Bob/Charlie under Alice. +test('Sorting keeps nested children grouped under their parent', async (t) => { const treeList = new TreeList(GRID_SELECTOR); await t.expect(treeList.isReady()).ok(); @@ -619,41 +378,31 @@ test('Sorting applies with nested children', async (t) => { .pressKey('enter'); await t.expect(aiChat.getSuccessMessages().count).eql(1); - await t.expect(treeList.apiColumnOption('value', 'sortOrder')).eql('desc'); -}).before(async () => createWidget('dxTreeList', () => ({ - dataSource: [ - { - id: 1, parentId: 0, name: 'Alice', value: 30, - }, - { - id: 2, parentId: 1, name: 'Bob', value: 20, - }, - { - id: 3, parentId: 1, name: 'Charlie', value: 10, - }, - { - id: 4, parentId: 0, name: 'Dave', value: 40, - }, - ], - keyExpr: 'id', - parentIdExpr: 'parentId', - autoExpandAll: true, - columns: ['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: 'value', sortOrder: 'desc' } }], - }), - abort: (): void => {}, - }; + await t.expect(aiChat.getSuccessActionItems(0).count).eql(1); + await t.expect(await treeList.apiColumnOption('value', 'sortOrder')).eql('desc'); + await t.expect(treeList.getDataCell(0, 1).element.textContent).eql('10'); + await t.expect(treeList.getDataCell(1, 1).element.textContent).eql('5'); + await t.expect(treeList.getDataCell(2, 1).element.textContent).eql('100'); + await t.expect(treeList.getDataCell(3, 1).element.textContent).eql('50'); +}).before(async () => createTreeListWithAIAssistant( + treeListBase({ + dataSource: [ + { + id: 1, parentId: 0, name: 'Alice', value: 5, }, - }), - }, -}))); + { + id: 2, parentId: 1, name: 'Bob', value: 100, + }, + { + id: 3, parentId: 1, name: 'Charlie', value: 50, + }, + { + id: 4, parentId: 0, name: 'Dave', value: 10, + }, + ], + }), + [{ actions: [{ name: 'sorting', args: { dataField: 'value', sortOrder: 'desc' } }] }], +)); // 6.3.3 test('Selecting a parent row applies per TreeList selection rules', async (t) => { @@ -670,39 +419,9 @@ test('Selecting a parent row applies per TreeList selection rules', async (t) => .pressKey('enter'); await t.expect(aiChat.getSuccessMessages().count).eql(1); - await t.expect(treeList.apiOption('selectedRowKeys')).eql([1]); -}).before(async () => createWidget('dxTreeList', () => ({ - dataSource: [ - { - id: 1, parentId: 0, name: 'Alice', value: 30, - }, - { - id: 2, parentId: 1, name: 'Bob', value: 20, - }, - { - id: 3, parentId: 1, name: 'Charlie', value: 10, - }, - { - id: 4, parentId: 0, name: 'Dave', value: 40, - }, - ], - keyExpr: 'id', - parentIdExpr: 'parentId', - autoExpandAll: true, - columns: ['name', 'value'], - showBorders: true, - selection: { mode: 'multiple', recursive: false }, - aiAssistant: { - enabled: true, - aiIntegration: new (window as any).DevExpress.aiIntegration.AIIntegration({ - sendRequest() { - return { - promise: Promise.resolve({ - actions: [{ name: 'selectByKeys', args: { keys: [1], preserve: false } }], - }), - abort: (): void => {}, - }; - }, - }), - }, -}))); + await t.expect(aiChat.getSuccessActionItems(0).count).eql(1); + await t.expect(await treeList.apiOption('selectedRowKeys')).eql([1]); +}).before(async () => createTreeListWithAIAssistant( + treeListBase({ selection: { mode: 'multiple', recursive: false } }), + [{ actions: [{ name: 'selectByKeys', args: { keys: [1], preserve: false } }] }], +)); diff --git a/packages/testcafe-models/chat.ts b/packages/testcafe-models/chat.ts index 4e1cf2ed543a..d5cf8baf8365 100644 --- a/packages/testcafe-models/chat.ts +++ b/packages/testcafe-models/chat.ts @@ -43,12 +43,8 @@ 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.getMessageBubbles().nth(index); + return this.element.find(`.${CLASS.messageBubble}`).nth(index); } getContextMenuContent(): Selector { diff --git a/packages/testcafe-models/dataGrid/aiAssistantChat.ts b/packages/testcafe-models/dataGrid/aiAssistantChat.ts index 623cf0336434..5ea76ba39370 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', @@ -27,8 +26,6 @@ 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 { @@ -48,29 +45,11 @@ 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 { - 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}`); } @@ -86,40 +65,40 @@ export class AIAssistantChat extends Popup { return this.element.find(`.${CLASS.messageError}`); } - getAIMessage(index: number): Selector { - return this.getAIMessages().nth(index); + getMessage(index: number): Selector { + return this.getMessages().nth(index); } getMessageHeader(index: number): Selector { - return this.getAIMessage(index).find(`.${CLASS.messageHeader}`); + return this.getMessage(index).find(`.${CLASS.messageHeader}`); } getMessageErrorText(index: number): Selector { - return this.getAIMessage(index).find(`.${CLASS.messageErrorText}`); + return this.getMessage(index).find(`.${CLASS.messageErrorText}`); } getMessageProgressBar(index: number): Selector { - return this.getAIMessage(index).find(`.${CLASS.messageProgressBar}`); + return this.getMessage(index).find(`.${CLASS.messageProgressBar}`); } getMessageRegenerateButton(index: number): Selector { - return this.getAIMessage(index).find(`.${CLASS.messageRegenerateButton}`); + return this.getMessage(index).find(`.${CLASS.messageRegenerateButton}`); } getActionList(messageIndex: number): Selector { - return this.getAIMessage(messageIndex).find(`.${CLASS.actionList}`); + return this.getMessage(messageIndex).find(`.${CLASS.actionList}`); } getActionItems(messageIndex: number): Selector { - return this.getAIMessage(messageIndex).find(`.${CLASS.actionListItem}`); + return this.getMessage(messageIndex).find(`.${CLASS.actionListItem}`); } getSuccessActionItems(messageIndex: number): Selector { - return this.getAIMessage(messageIndex).find(`.${CLASS.actionListItemSuccess}`); + return this.getMessage(messageIndex).find(`.${CLASS.actionListItemSuccess}`); } getErrorActionItems(messageIndex: number): Selector { - return this.getAIMessage(messageIndex).find(`.${CLASS.actionListItemError}`); + return this.getMessage(messageIndex).find(`.${CLASS.actionListItemError}`); } getActionItemText(messageIndex: number, actionIndex: number): Selector { @@ -130,10 +109,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; } @@ -143,12 +118,4 @@ export class AIAssistantChat extends Popup { .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 99aec7ee5f3d..097e5e921b15 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; @@ -1054,15 +1045,4 @@ 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 99b57a388d0c636fbd08a078497dab0c440915ec Mon Sep 17 00:00:00 2001 From: Alyar <> Date: Mon, 8 Jun 2026 00:04:17 +0400 Subject: [PATCH 3/7] TreeList - AI Assistant: add column reorder, pinning, and resize parity e2e tests --- .../common/treeList/aiAssistant/functional.ts | 76 ++++++++++++++++++- 1 file changed, 73 insertions(+), 3 deletions(-) diff --git a/e2e/testcafe-devextreme/tests/common/treeList/aiAssistant/functional.ts b/e2e/testcafe-devextreme/tests/common/treeList/aiAssistant/functional.ts index 1f4e13c7d5a8..af0e1e1fd1b2 100644 --- a/e2e/testcafe-devextreme/tests/common/treeList/aiAssistant/functional.ts +++ b/e2e/testcafe-devextreme/tests/common/treeList/aiAssistant/functional.ts @@ -179,6 +179,79 @@ test('Column visibility behaves like DataGrid', async (t) => { [{ actions: [{ name: 'columnsVisibility', args: { dataField: 'value', visible: false } }] }], )); +// 6.1.4 +test('Column reorder behaves like DataGrid', async (t) => { + const treeList = new TreeList(GRID_SELECTOR); + + await t.expect(treeList.isReady()).ok(); + + await t.click(treeList.getAIAssistantButton()); + + const aiChat = treeList.getAIAssistantChat(); + + await t + .typeText(aiChat.getInput(), 'Move the value column to the first position') + .pressKey('enter'); + + await t.expect(aiChat.getSuccessMessages().count).eql(1); + await t.expect(aiChat.getSuccessActionItems(0).count).eql(1); + await t.expect(await treeList.apiColumnOption('value', 'visibleIndex')).eql(0); + await t.expect(await treeList.apiColumnOption('name', 'visibleIndex')).eql(1); +}).before(async () => createTreeListWithAIAssistant( + treeListBase({ allowColumnReordering: true }), + [{ actions: [{ name: 'columnsReorder', args: { dataField: 'value', visibleIndex: 0 } }] }], +)); + +// 6.1.4 — columnsPinning parity. +test('Column pinning behaves like DataGrid', async (t) => { + const treeList = new TreeList(GRID_SELECTOR); + + await t.expect(treeList.isReady()).ok(); + + await t.click(treeList.getAIAssistantButton()); + + const aiChat = treeList.getAIAssistantChat(); + + await t + .typeText(aiChat.getInput(), 'Pin the value column to the left') + .pressKey('enter'); + + await t.expect(aiChat.getSuccessMessages().count).eql(1); + await t.expect(aiChat.getSuccessActionItems(0).count).eql(1); + await t.expect(await treeList.apiColumnOption('value', 'fixed')).eql(true); + await t.expect(await treeList.apiColumnOption('value', 'fixedPosition')).eql('left'); +}).before(async () => createTreeListWithAIAssistant( + treeListBase({ columnFixing: { enabled: true } }), + [{ + actions: [{ + name: 'columnsPinning', + args: { dataField: 'value', fixed: true, fixedPosition: 'left' }, + }], + }], +)); + +// 6.1.4 — columnsResize parity. +test('Column resize behaves like DataGrid', async (t) => { + const treeList = new TreeList(GRID_SELECTOR); + + await t.expect(treeList.isReady()).ok(); + + await t.click(treeList.getAIAssistantButton()); + + const aiChat = treeList.getAIAssistantChat(); + + await t + .typeText(aiChat.getInput(), 'Set the value column width to 250') + .pressKey('enter'); + + await t.expect(aiChat.getSuccessMessages().count).eql(1); + await t.expect(aiChat.getSuccessActionItems(0).count).eql(1); + await t.expect(await treeList.apiColumnOption('value', 'width')).eql(250); +}).before(async () => createTreeListWithAIAssistant( + treeListBase({ allowColumnResizing: true }), + [{ actions: [{ name: 'columnsResize', args: { dataField: 'value', width: 250 } }] }], +)); + // 6.1.5 test('Selection (selectByKeys) behaves like DataGrid', async (t) => { const treeList = new TreeList(GRID_SELECTOR); @@ -361,9 +434,6 @@ test('Filtering applies on hierarchical data', async (t) => { ])); // 6.3.2 -// Children must sort within their parent, not globally. The dataset is chosen so a flat sort -// and a hierarchical sort produce a different visible order: flat desc by value would be -// Bob(100), Charlie(50), Dave(10), Alice(5); hierarchical keeps Bob/Charlie under Alice. test('Sorting keeps nested children grouped under their parent', async (t) => { const treeList = new TreeList(GRID_SELECTOR); From bbb8a428e8c6138a3991ccc7dbd9aa8ef20b5cfd Mon Sep 17 00:00:00 2001 From: Alyar <> Date: Mon, 8 Jun 2026 05:55:10 +0400 Subject: [PATCH 4/7] TreeList - AI Assistant: align AIAssistantChat POM to canonical shape Use the canonical getMessages (chat bubbles) / getAIMessages semantics and keep only the POM methods this PR's tests use (functional + visual), dropping unused ones. --- .../dataGrid/aiAssistantChat.ts | 75 +++---------------- 1 file changed, 9 insertions(+), 66 deletions(-) diff --git a/packages/testcafe-models/dataGrid/aiAssistantChat.ts b/packages/testcafe-models/dataGrid/aiAssistantChat.ts index 5ea76ba39370..4b7eb939ef03 100644 --- a/packages/testcafe-models/dataGrid/aiAssistantChat.ts +++ b/packages/testcafe-models/dataGrid/aiAssistantChat.ts @@ -1,38 +1,19 @@ import { Selector } from 'testcafe'; import Popup from '../popup'; -import Button from '../button'; import Chat from '../chat'; const CLASS = { - aiChat: 'dx-ai-chat', aiChatContent: 'dx-ai-chat__content', message: 'dx-ai-chat__message', messagePending: 'dx-ai-chat__message--pending', messageSuccess: 'dx-ai-chat__message--success', messageError: 'dx-ai-chat__message--error', - messageIcon: 'dx-ai-chat__message-icon', - messageHeader: 'dx-ai-chat__message-header', - messageHeaderRow: 'dx-ai-chat__message-header-row', - messageContent: 'dx-ai-chat__message-content', - messageStatus: 'dx-ai-chat__message-status', - messageErrorText: 'dx-ai-chat__message-error-text', - messageProgressBar: 'dx-ai-chat__message-progressbar', - messageRegenerateButton: 'dx-ai-chat__message-regenerate-button', - actionList: 'dx-ai-chat__action-list', actionListItem: 'dx-ai-chat__action-list-item', actionListItemSuccess: 'dx-ai-chat__action-list-item--success', - actionListItemError: 'dx-ai-chat__action-list-item--error', - actionListItemIcon: 'dx-ai-chat__action-list-item-icon', - actionListItemText: 'dx-ai-chat__action-list-item-text', - closeButton: 'dx-closebutton', clearChatButton: 'dx-icon-clearhistory', }; export class AIAssistantChat extends Popup { - getWrapper(): Selector { - return this.element; - } - getChat(): Chat { return new Chat(this.element.find(`.${CLASS.aiChatContent}`)); } @@ -41,18 +22,18 @@ export class AIAssistantChat extends Popup { return this.getChat().getInput(); } - getCloseButton(): Button { - return new Button(this.element.find(`.${CLASS.closeButton}`)); - } - getClearChatButton(): Selector { return this.element.find(`.${CLASS.clearChatButton}`); } - getMessages(): Selector { + getAIMessages(): Selector { return this.element.find(`.${CLASS.message}`); } + getAIMessage(index: number): Selector { + return this.getAIMessages().nth(index); + } + getPendingMessages(): Selector { return this.element.find(`.${CLASS.messagePending}`); } @@ -65,57 +46,19 @@ export class AIAssistantChat extends Popup { return this.element.find(`.${CLASS.messageError}`); } - getMessage(index: number): Selector { - return this.getMessages().nth(index); - } - - getMessageHeader(index: number): Selector { - return this.getMessage(index).find(`.${CLASS.messageHeader}`); - } - - getMessageErrorText(index: number): Selector { - return this.getMessage(index).find(`.${CLASS.messageErrorText}`); - } - - getMessageProgressBar(index: number): Selector { - return this.getMessage(index).find(`.${CLASS.messageProgressBar}`); - } - - getMessageRegenerateButton(index: number): Selector { - return this.getMessage(index).find(`.${CLASS.messageRegenerateButton}`); - } - - getActionList(messageIndex: number): Selector { - return this.getMessage(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}`); - } - - getErrorActionItems(messageIndex: number): Selector { - return this.getMessage(messageIndex).find(`.${CLASS.actionListItemError}`); + return this.getAIMessage(messageIndex).find(`.${CLASS.actionListItemSuccess}`); } - getActionItemText(messageIndex: number, actionIndex: number): Selector { - return this.getActionItems(messageIndex).nth(actionIndex).find(`.${CLASS.actionListItemText}`); - } - - getActionItemIcon(messageIndex: number, actionIndex: number): Selector { - return this.getActionItems(messageIndex).nth(actionIndex).find(`.${CLASS.actionListItemIcon}`); + isClearChatDisabled(): Promise { + return this.getClearChatButton().parent('.dx-button').hasClass('dx-state-disabled'); } isInputDisabled(): Promise { return this.getChat().getTextArea().isDisabled; } - - isClearChatDisabled(): Promise { - return this.getClearChatButton() - .parent('.dx-button') - .hasClass('dx-state-disabled'); - } } From 602fc22cf3bbd18c36de4758cd3b98bd7fb43cf7 Mon Sep 17 00:00:00 2001 From: Alyar <> Date: Mon, 8 Jun 2026 14:02:58 +0400 Subject: [PATCH 5/7] TreeList - AI Assistant: merge fixtures into one per file MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Collapse the per-section fixtures into a single named fixture; keep the // === §X === markers as in-file section dividers. --- .../tests/common/treeList/aiAssistant/functional.ts | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/e2e/testcafe-devextreme/tests/common/treeList/aiAssistant/functional.ts b/e2e/testcafe-devextreme/tests/common/treeList/aiAssistant/functional.ts index af0e1e1fd1b2..73e7252472ba 100644 --- a/e2e/testcafe-devextreme/tests/common/treeList/aiAssistant/functional.ts +++ b/e2e/testcafe-devextreme/tests/common/treeList/aiAssistant/functional.ts @@ -85,11 +85,11 @@ const createTreeListWithAIAssistant = async ( return createWidget('dxTreeList', aiTreeListOptions); }; -// === §6.1 grid_core-level commands behave identically on TreeList === - -fixture`AI Assistant - TreeList Parity` +fixture`AI Assistant - TreeList` .page(AI_INTEGRATION_PAGE); +// === §6.1 grid_core-level commands behave identically on TreeList === + // 6.1.1 test('Toolbar button, popup and chat behave like DataGrid (sorting)', async (t) => { const treeList = new TreeList(GRID_SELECTOR); @@ -339,9 +339,6 @@ test('In-flight lock behaves like DataGrid', async (t) => { // === §6.2 DataGrid-only commands are not offered for TreeList === -fixture`AI Assistant - TreeList Schema` - .page(AI_INTEGRATION_PAGE); - // 6.2.1 test('grouping command is absent from the TreeList response schema', async (t) => { const treeList = new TreeList(GRID_SELECTOR); @@ -394,9 +391,6 @@ test('summary commands are absent from the TreeList response schema', async (t) // === §6.3 Hierarchical-data peculiarities === -fixture`AI Assistant - TreeList Hierarchy` - .page(AI_INTEGRATION_PAGE); - // 6.3.1 test('Filtering applies on hierarchical data', async (t) => { const treeList = new TreeList(GRID_SELECTOR); From a0be18147d7d9fa27866ba10a00cccd3222240ee Mon Sep 17 00:00:00 2001 From: Alyar <> Date: Mon, 8 Jun 2026 14:36:14 +0400 Subject: [PATCH 6/7] TreeList - AI Assistant: restore scoped getWrapper override + pass async grid reads to t.expect Restore the scoped getWrapper override; drop await inside t.expect so TestCafe retries async grid-state assertions (Copilot review). --- .../common/treeList/aiAssistant/functional.ts | 30 +++++++++---------- .../dataGrid/aiAssistantChat.ts | 4 +++ 2 files changed, 19 insertions(+), 15 deletions(-) diff --git a/e2e/testcafe-devextreme/tests/common/treeList/aiAssistant/functional.ts b/e2e/testcafe-devextreme/tests/common/treeList/aiAssistant/functional.ts index 73e7252472ba..1a13bdb54a79 100644 --- a/e2e/testcafe-devextreme/tests/common/treeList/aiAssistant/functional.ts +++ b/e2e/testcafe-devextreme/tests/common/treeList/aiAssistant/functional.ts @@ -109,7 +109,7 @@ test('Toolbar button, popup and chat behave like DataGrid (sorting)', async (t) await t.expect(aiChat.getSuccessMessages().count).eql(1); await t.expect(aiChat.getSuccessActionItems(0).count).eql(1); - await t.expect(await treeList.apiColumnOption('name', 'sortOrder')).eql('asc'); + await t.expect(treeList.apiColumnOption('name', 'sortOrder')).eql('asc'); }).before(async () => createTreeListWithAIAssistant(treeListBase(), [ { actions: [{ name: 'sorting', args: { dataField: 'name', sortOrder: 'asc' } }] }, ])); @@ -130,7 +130,7 @@ test('Searching behaves like DataGrid', async (t) => { await t.expect(aiChat.getSuccessMessages().count).eql(1); await t.expect(aiChat.getSuccessActionItems(0).count).eql(1); - await t.expect(await treeList.apiOption('searchPanel.text')).eql('Bob'); + await t.expect(treeList.apiOption('searchPanel.text')).eql('Bob'); }).before(async () => createTreeListWithAIAssistant(treeListBase(), [ { actions: [{ name: 'searching', args: { text: 'Bob' } }] }, ])); @@ -151,7 +151,7 @@ test('Paging (pageSize) behaves like DataGrid', async (t) => { await t.expect(aiChat.getSuccessMessages().count).eql(1); await t.expect(aiChat.getSuccessActionItems(0).count).eql(1); - await t.expect(await treeList.apiOption('paging.pageSize')).eql(50); + await t.expect(treeList.apiOption('paging.pageSize')).eql(50); }).before(async () => createTreeListWithAIAssistant( treeListBase({ paging: { enabled: true, pageSize: 10 } }), [{ actions: [{ name: 'pageSize', args: { pageSize: 50 } }] }], @@ -173,7 +173,7 @@ test('Column visibility behaves like DataGrid', async (t) => { await t.expect(aiChat.getSuccessMessages().count).eql(1); await t.expect(aiChat.getSuccessActionItems(0).count).eql(1); - await t.expect(await treeList.apiColumnOption('value', 'visible')).eql(false); + await t.expect(treeList.apiColumnOption('value', 'visible')).eql(false); }).before(async () => createTreeListWithAIAssistant( treeListBase({ columnChooser: { enabled: true } }), [{ actions: [{ name: 'columnsVisibility', args: { dataField: 'value', visible: false } }] }], @@ -195,8 +195,8 @@ test('Column reorder behaves like DataGrid', async (t) => { await t.expect(aiChat.getSuccessMessages().count).eql(1); await t.expect(aiChat.getSuccessActionItems(0).count).eql(1); - await t.expect(await treeList.apiColumnOption('value', 'visibleIndex')).eql(0); - await t.expect(await treeList.apiColumnOption('name', 'visibleIndex')).eql(1); + await t.expect(treeList.apiColumnOption('value', 'visibleIndex')).eql(0); + await t.expect(treeList.apiColumnOption('name', 'visibleIndex')).eql(1); }).before(async () => createTreeListWithAIAssistant( treeListBase({ allowColumnReordering: true }), [{ actions: [{ name: 'columnsReorder', args: { dataField: 'value', visibleIndex: 0 } }] }], @@ -218,8 +218,8 @@ test('Column pinning behaves like DataGrid', async (t) => { await t.expect(aiChat.getSuccessMessages().count).eql(1); await t.expect(aiChat.getSuccessActionItems(0).count).eql(1); - await t.expect(await treeList.apiColumnOption('value', 'fixed')).eql(true); - await t.expect(await treeList.apiColumnOption('value', 'fixedPosition')).eql('left'); + await t.expect(treeList.apiColumnOption('value', 'fixed')).eql(true); + await t.expect(treeList.apiColumnOption('value', 'fixedPosition')).eql('left'); }).before(async () => createTreeListWithAIAssistant( treeListBase({ columnFixing: { enabled: true } }), [{ @@ -246,7 +246,7 @@ test('Column resize behaves like DataGrid', async (t) => { await t.expect(aiChat.getSuccessMessages().count).eql(1); await t.expect(aiChat.getSuccessActionItems(0).count).eql(1); - await t.expect(await treeList.apiColumnOption('value', 'width')).eql(250); + await t.expect(treeList.apiColumnOption('value', 'width')).eql(250); }).before(async () => createTreeListWithAIAssistant( treeListBase({ allowColumnResizing: true }), [{ actions: [{ name: 'columnsResize', args: { dataField: 'value', width: 250 } }] }], @@ -268,7 +268,7 @@ test('Selection (selectByKeys) behaves like DataGrid', async (t) => { await t.expect(aiChat.getSuccessMessages().count).eql(1); await t.expect(aiChat.getSuccessActionItems(0).count).eql(1); - await t.expect(await treeList.apiOption('selectedRowKeys')).eql([2]); + await t.expect(treeList.apiOption('selectedRowKeys')).eql([2]); }).before(async () => createTreeListWithAIAssistant( treeListBase({ selection: { mode: 'multiple', recursive: false } }), [{ actions: [{ name: 'selectByKeys', args: { keys: [2], preserve: false } }] }], @@ -290,7 +290,7 @@ test('Row focusing (focusRowByKey) behaves like DataGrid', async (t) => { await t.expect(aiChat.getSuccessMessages().count).eql(1); await t.expect(aiChat.getSuccessActionItems(0).count).eql(1); - await t.expect(await treeList.apiOption('focusedRowKey')).eql(2); + await t.expect(treeList.apiOption('focusedRowKey')).eql(2); }).before(async () => createTreeListWithAIAssistant( treeListBase({ focusedRowEnabled: true }), [{ actions: [{ name: 'focusRowByKey', args: { key: 2 } }] }], @@ -313,7 +313,7 @@ test('Error paths behave like DataGrid (invalid response)', async (t) => { await t.expect(aiChat.getErrorMessages().count).eql(1); await t.expect(aiChat.getSuccessMessages().count).eql(0); await t.expect(aiChat.getActionItems(0).count).eql(0); - await t.expect(await treeList.apiColumnOption('name', 'sortOrder')).notOk(); + await t.expect(treeList.apiColumnOption('name', 'sortOrder')).notOk(); }).before(async () => createTreeListWithAIAssistant(treeListBase(), [ { actions: null }, ])); @@ -407,7 +407,7 @@ test('Filtering applies on hierarchical data', async (t) => { await t.expect(aiChat.getSuccessMessages().count).eql(1); await t.expect(aiChat.getSuccessActionItems(0).count).eql(1); - await t.expect(await treeList.apiOption('filterValue')).eql(['value', '>', 15]); + await t.expect(treeList.apiOption('filterValue')).eql(['value', '>', 15]); }).before(async () => createTreeListWithAIAssistant(treeListBase(), [ { actions: [{ @@ -443,7 +443,7 @@ test('Sorting keeps nested children grouped under their parent', async (t) => { await t.expect(aiChat.getSuccessMessages().count).eql(1); await t.expect(aiChat.getSuccessActionItems(0).count).eql(1); - await t.expect(await treeList.apiColumnOption('value', 'sortOrder')).eql('desc'); + await t.expect(treeList.apiColumnOption('value', 'sortOrder')).eql('desc'); await t.expect(treeList.getDataCell(0, 1).element.textContent).eql('10'); await t.expect(treeList.getDataCell(1, 1).element.textContent).eql('5'); await t.expect(treeList.getDataCell(2, 1).element.textContent).eql('100'); @@ -484,7 +484,7 @@ test('Selecting a parent row applies per TreeList selection rules', async (t) => await t.expect(aiChat.getSuccessMessages().count).eql(1); await t.expect(aiChat.getSuccessActionItems(0).count).eql(1); - await t.expect(await treeList.apiOption('selectedRowKeys')).eql([1]); + await t.expect(treeList.apiOption('selectedRowKeys')).eql([1]); }).before(async () => createTreeListWithAIAssistant( treeListBase({ selection: { mode: 'multiple', recursive: false } }), [{ actions: [{ name: 'selectByKeys', args: { keys: [1], preserve: false } }] }], diff --git a/packages/testcafe-models/dataGrid/aiAssistantChat.ts b/packages/testcafe-models/dataGrid/aiAssistantChat.ts index 4b7eb939ef03..adb71bc3ea22 100644 --- a/packages/testcafe-models/dataGrid/aiAssistantChat.ts +++ b/packages/testcafe-models/dataGrid/aiAssistantChat.ts @@ -14,6 +14,10 @@ const CLASS = { }; export class AIAssistantChat extends Popup { + getWrapper(): Selector { + return this.element; + } + getChat(): Chat { return new Chat(this.element.find(`.${CLASS.aiChatContent}`)); } From 9223ce8fa4d1ad6c768034ce09ea2fbbe62214b6 Mon Sep 17 00:00:00 2001 From: Alyar <> Date: Mon, 8 Jun 2026 22:32:13 +0400 Subject: [PATCH 7/7] DataGrid - AI Assistant: address Copilot review (clearChatButton via dedicated css class) --- packages/testcafe-models/dataGrid/aiAssistantChat.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/testcafe-models/dataGrid/aiAssistantChat.ts b/packages/testcafe-models/dataGrid/aiAssistantChat.ts index adb71bc3ea22..a5351771bf6c 100644 --- a/packages/testcafe-models/dataGrid/aiAssistantChat.ts +++ b/packages/testcafe-models/dataGrid/aiAssistantChat.ts @@ -10,7 +10,7 @@ const CLASS = { messageError: 'dx-ai-chat__message--error', actionListItem: 'dx-ai-chat__action-list-item', actionListItemSuccess: 'dx-ai-chat__action-list-item--success', - clearChatButton: 'dx-icon-clearhistory', + clearChatButton: 'dx-ai-chat__clear-button', }; export class AIAssistantChat extends Popup { @@ -59,7 +59,7 @@ export class AIAssistantChat extends Popup { } isClearChatDisabled(): Promise { - return this.getClearChatButton().parent('.dx-button').hasClass('dx-state-disabled'); + return this.getClearChatButton().find('.dx-button').hasClass('dx-state-disabled'); } isInputDisabled(): Promise {