From e3423b4b3bc5a79d840220270eac231e11cf6dd1 Mon Sep 17 00:00:00 2001 From: Alyar <> Date: Wed, 3 Jun 2026 02:30:28 +0400 Subject: [PATCH 01/10] DataGrid - AI Assistant: request lifecycle e2e tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit the in-flight lock — input, clear-chat and suggestions disabled while an LLM request or command runs, re-enabled afterwards, plus rapid sequential prompts (plan §3.8–§3.11) Co-Authored-By: Claude Opus 4.8 --- .../requestLifecycle.functional.ts | 632 ++++++++++++++++++ .../common/aiAssistant/testHelpers.ts | 16 + packages/testcafe-models/chat.ts | 6 +- .../dataGrid/aiAssistantChat.ts | 67 +- packages/testcafe-models/dataGrid/index.ts | 20 + 5 files changed, 730 insertions(+), 11 deletions(-) create mode 100644 e2e/testcafe-devextreme/tests/dataGrid/common/aiAssistant/requestLifecycle.functional.ts create mode 100644 e2e/testcafe-devextreme/tests/dataGrid/common/aiAssistant/testHelpers.ts diff --git a/e2e/testcafe-devextreme/tests/dataGrid/common/aiAssistant/requestLifecycle.functional.ts b/e2e/testcafe-devextreme/tests/dataGrid/common/aiAssistant/requestLifecycle.functional.ts new file mode 100644 index 000000000000..fe3ace0c14d2 --- /dev/null +++ b/e2e/testcafe-devextreme/tests/dataGrid/common/aiAssistant/requestLifecycle.functional.ts @@ -0,0 +1,632 @@ +import DataGrid from 'devextreme-testcafe-models/dataGrid'; +import { createWidget } from '../../../../helpers/createWidget'; +import { AI_INTEGRATION_PAGE, GRID_SELECTOR } from './testHelpers'; + +// === §3.8 Rapid sequential prompts === + +fixture.disablePageReloads`AI Assistant - Sequential Prompts` + .page(AI_INTEGRATION_PAGE); + +// 3.8.1 +test('N prompts back-to-back should all execute in order', async (t) => { + const dataGrid = new DataGrid(GRID_SELECTOR); + + await t.expect(dataGrid.isReady()).ok(); + + await t.click(dataGrid.getAIAssistantButton()); + + const aiChat = dataGrid.getAIAssistantChat(); + + for (let i = 0; i < 4; i += 1) { + await t + .typeText(aiChat.getInput(), `Prompt ${i + 1}`) + .pressKey('enter'); + + await t.expect(aiChat.getSuccessMessages().count).eql(i + 1); + await t.expect(aiChat.getSuccessActionItems(i).count).eql(1); + } + + await t.expect(aiChat.getMessages().count).eql(8); +}).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: 'clearSorting', args: {} }], + }), + abort: (): void => {}, + }; + }, + }), + }, +}))); + +// === §3.9 Input disabled while in flight === + +fixture.disablePageReloads`AI Assistant - Input In Flight` + .page(AI_INTEGRATION_PAGE); + +// 3.9.1 +test('Input should be disabled during LLM phase', 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.getPendingMessages().count).eql(1); + await t.expect(aiChat.isInputDisabled()).ok(); +}).before(async () => createWidget('dxDataGrid', () => ({ + dataSource: [ + { id: 1, name: 'Alice', value: 30 }, + { id: 2, name: 'Bob', value: 20 }, + ], + 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 => {}, + }; + }, + }), + }, +}))); + +// 3.9.2 +test('Input should be disabled during command execution phase', 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(), 'Select all rows') + .pressKey('enter'); + + // LLM resolved immediately; selectAll command is awaiting a server key-load + // that never resolves, so the request stays in flight during command execution. + await t.expect(aiChat.isInputDisabled()).ok(); + await t.expect(aiChat.getAIMessages().count).eql(1); + await t.expect(aiChat.getPendingMessages().count).eql(1); +}).before(async () => createWidget('dxDataGrid', () => { + const data = Array.from({ length: 50 }, (_, i) => ({ + id: i + 1, + name: `Name ${i + 1}`, + value: (i + 1) * 10, + })); + + const store = new (window as any).DevExpress.data.CustomStore({ + key: 'id', + load(loadOptions: any) { + // Paged render load carries `take`; selectAll's all-pages key-load does not. + // Hang the key-load to keep command execution in flight deterministically. + if (loadOptions.take !== undefined) { + const skip = loadOptions.skip ?? 0; + + return Promise.resolve({ + data: data.slice(skip, skip + loadOptions.take), + totalCount: data.length, + }); + } + + return new Promise(() => {}); + }, + totalCount: () => data.length, + }); + + return { + dataSource: store, + remoteOperations: true, + columns: ['id', 'name', 'value'], + showBorders: true, + selection: { mode: 'multiple' }, + aiAssistant: { + enabled: true, + aiIntegration: new (window as any).DevExpress.aiIntegration.AIIntegration({ + sendRequest() { + return { + promise: Promise.resolve({ + actions: [{ name: 'selectAll', args: {} }], + }), + abort: (): void => {}, + }; + }, + }), + }, + }; +})); + +// 3.9.3 +test('Input should be re-enabled after fulfillment', 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.getSuccessActionItems(0).count).eql(1); + await t.expect(aiChat.isInputDisabled()).notOk(); +}).before(async () => createWidget('dxDataGrid', () => ({ + dataSource: [ + { id: 1, name: 'Alice', value: 30 }, + { id: 2, name: 'Bob', value: 20 }, + ], + 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 => {}, + }; + }, + }), + }, +}))); + +// 3.9.4 +test('Input should be re-enabled after failure', 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.getErrorMessages().count).eql(1); + await t.expect(aiChat.isInputDisabled()).notOk(); +}).before(async () => createWidget('dxDataGrid', () => ({ + dataSource: [ + { id: 1, name: 'Alice', value: 30 }, + { id: 2, name: 'Bob', value: 20 }, + ], + keyExpr: 'id', + columns: ['id', 'name', 'value'], + showBorders: true, + aiAssistant: { + enabled: true, + aiIntegration: new (window as any).DevExpress.aiIntegration.AIIntegration({ + sendRequest() { + return { + promise: Promise.reject(new Error('AI error')), + abort: (): void => {}, + }; + }, + }), + }, +}))); + +// 3.9.5 +test('Input should be re-enabled after abort via 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.getPendingMessages().count).eql(1); + await t.expect(aiChat.isInputDisabled()).ok(); + + await t.click(aiChat.getCloseButton().element); + + await t.expect(aiChat.getAbortConfirmDialog().exists).ok(); + + await t.click(aiChat.getAbortConfirmYesButton()); + await t.click(dataGrid.getAIAssistantButton()); + + await t.expect(aiChat.getErrorMessages().count).eql(1); + await t.expect(aiChat.isInputDisabled()).notOk(); +}).before(async () => createWidget('dxDataGrid', () => ({ + dataSource: [ + { id: 1, name: 'Alice', value: 30 }, + { id: 2, name: 'Bob', value: 20 }, + ], + 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 => {}, + }; + }, + }), + }, +}))); + +// === §3.10 Clear-chat disabled while in flight === + +fixture.disablePageReloads`AI Assistant - Clear Chat In Flight` + .page(AI_INTEGRATION_PAGE); + +// 3.10.1 +test('Clear-chat button should be disabled during LLM phase', 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.getPendingMessages().count).eql(1); + await t.expect(aiChat.isClearChatDisabled()).ok(); +}).before(async () => createWidget('dxDataGrid', () => ({ + dataSource: [ + { id: 1, name: 'Alice', value: 30 }, + { id: 2, name: 'Bob', value: 20 }, + ], + 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 => {}, + }; + }, + }), + }, +}))); + +// 3.10.2 +test('Clear-chat button should be disabled during command execution phase', 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(), 'Select all rows') + .pressKey('enter'); + + // selectAll command is awaiting a server key-load that never resolves. + await t.expect(aiChat.isClearChatDisabled()).ok(); + await t.expect(aiChat.getAIMessages().count).eql(1); + await t.expect(aiChat.getPendingMessages().count).eql(1); +}).before(async () => createWidget('dxDataGrid', () => { + const data = Array.from({ length: 50 }, (_, i) => ({ + id: i + 1, + name: `Name ${i + 1}`, + value: (i + 1) * 10, + })); + + const store = new (window as any).DevExpress.data.CustomStore({ + key: 'id', + load(loadOptions: any) { + if (loadOptions.take !== undefined) { + const skip = loadOptions.skip ?? 0; + + return Promise.resolve({ + data: data.slice(skip, skip + loadOptions.take), + totalCount: data.length, + }); + } + + return new Promise(() => {}); + }, + totalCount: () => data.length, + }); + + return { + dataSource: store, + remoteOperations: true, + columns: ['id', 'name', 'value'], + showBorders: true, + selection: { mode: 'multiple' }, + aiAssistant: { + enabled: true, + aiIntegration: new (window as any).DevExpress.aiIntegration.AIIntegration({ + sendRequest() { + return { + promise: Promise.resolve({ + actions: [{ name: 'selectAll', args: {} }], + }), + abort: (): void => {}, + }; + }, + }), + }, + }; +})); + +// 3.10.3 +test('Clear-chat button should be re-enabled after fulfillment', 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.isClearChatDisabled()).notOk(); +}).before(async () => createWidget('dxDataGrid', () => ({ + dataSource: [ + { id: 1, name: 'Alice', value: 30 }, + { id: 2, name: 'Bob', value: 20 }, + ], + 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 => {}, + }; + }, + }), + }, +}))); + +// 3.10.4 +test('Clear-chat should remove all messages from chat', 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.click(aiChat.getClearChatButton()); + + await t.expect(aiChat.getMessages().count).eql(0); +}).before(async () => createWidget('dxDataGrid', () => ({ + dataSource: [ + { id: 1, name: 'Alice', value: 30 }, + { id: 2, name: 'Bob', value: 20 }, + ], + 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 => {}, + }; + }, + }), + }, +}))); + +// === §3.11 Suggestions disabled while in flight === + +fixture.disablePageReloads`AI Assistant - Suggestions In Flight` + .page(AI_INTEGRATION_PAGE); + +// 3.11.1 +test('Suggestions should be disabled during LLM phase', 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.getPendingMessages().count).eql(1); + await t.expect(aiChat.isSuggestionDisabled(0)).ok(); +}).before(async () => createWidget('dxDataGrid', () => ({ + dataSource: [ + { id: 1, name: 'Alice', value: 30 }, + { id: 2, name: 'Bob', value: 20 }, + ], + 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 => {}, + }; + }, + }), + chat: { + suggestions: { + items: [{ text: 'Sort by name' }], + }, + }, + }, +}))); + +// 3.11.2 +test('Suggestions should be disabled during command execution phase', 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(), 'Select all rows') + .pressKey('enter'); + + // selectAll command is awaiting a server key-load that never resolves. + await t.expect(aiChat.isSuggestionDisabled(0)).ok(); + await t.expect(aiChat.getAIMessages().count).eql(1); + await t.expect(aiChat.getPendingMessages().count).eql(1); +}).before(async () => createWidget('dxDataGrid', () => { + const data = Array.from({ length: 50 }, (_, i) => ({ + id: i + 1, + name: `Name ${i + 1}`, + value: (i + 1) * 10, + })); + + const store = new (window as any).DevExpress.data.CustomStore({ + key: 'id', + load(loadOptions: any) { + if (loadOptions.take !== undefined) { + const skip = loadOptions.skip ?? 0; + + return Promise.resolve({ + data: data.slice(skip, skip + loadOptions.take), + totalCount: data.length, + }); + } + + return new Promise(() => {}); + }, + totalCount: () => data.length, + }); + + return { + dataSource: store, + remoteOperations: true, + columns: ['id', 'name', 'value'], + showBorders: true, + selection: { mode: 'multiple' }, + aiAssistant: { + enabled: true, + aiIntegration: new (window as any).DevExpress.aiIntegration.AIIntegration({ + sendRequest() { + return { + promise: Promise.resolve({ + actions: [{ name: 'selectAll', args: {} }], + }), + abort: (): void => {}, + }; + }, + }), + chat: { + suggestions: { + items: [{ text: 'Sort by name' }], + }, + }, + }, + }; +})); + +// 3.11.3 +test('Suggestions should be re-enabled after resolution', 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.isSuggestionDisabled(0)).notOk(); +}).before(async () => createWidget('dxDataGrid', () => ({ + dataSource: [ + { id: 1, name: 'Alice', value: 30 }, + { id: 2, name: 'Bob', value: 20 }, + ], + 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 => {}, + }; + }, + }), + chat: { + suggestions: { + items: [{ text: 'Sort by name' }], + }, + }, + }, +}))); 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/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 e2dc29cbd244f8a832b68a652b3f8c577199db5a Mon Sep 17 00:00:00 2001 From: Alyar <> Date: Fri, 5 Jun 2026 03:37:53 +0400 Subject: [PATCH 02/10] DataGrid - AI Assistant: refine request lifecycle e2e tests - Add shared mocked sendRequest queue with unexpected-call guard - Verify commands apply to grid state and no second request leaks - Split 3.10.3 into fulfillment/failure/abort re-enable cases - Check grid state unchanged after clear-chat (3.10.4) - Remove unused POM methods and test helpers Co-Authored-By: Claude Opus 4.8 --- .../requestLifecycle.functional.ts | 616 +++++++----------- .../common/aiAssistant/testHelpers.ts | 11 - .../dataGrid/aiAssistantChat.ts | 8 - 3 files changed, 242 insertions(+), 393 deletions(-) diff --git a/e2e/testcafe-devextreme/tests/dataGrid/common/aiAssistant/requestLifecycle.functional.ts b/e2e/testcafe-devextreme/tests/dataGrid/common/aiAssistant/requestLifecycle.functional.ts index fe3ace0c14d2..b9b73c1d6bb6 100644 --- a/e2e/testcafe-devextreme/tests/dataGrid/common/aiAssistant/requestLifecycle.functional.ts +++ b/e2e/testcafe-devextreme/tests/dataGrid/common/aiAssistant/requestLifecycle.functional.ts @@ -1,6 +1,161 @@ +/* eslint-disable no-underscore-dangle */ import DataGrid from 'devextreme-testcafe-models/dataGrid'; -import { createWidget } from '../../../../helpers/createWidget'; +import { ClientFunction } from 'testcafe'; import { AI_INTEGRATION_PAGE, GRID_SELECTOR } from './testHelpers'; +import { createWidget } from '../../../../helpers/createWidget'; + +// Sentinels recognised by the mocked `sendRequest` queue: +// HANG — return a never-resolving promise (keeps the request in the LLM phase), +// FAIL — reject the request (LLM-phase failure). +// Any other queue entry is resolved as a canned `{ actions: [...] }` response. +const HANG = '__HANG__'; +const FAIL = '__FAIL__'; + +const setupAIState = ClientFunction(( + base: Record, + responses: unknown[], + assistant: Record, +) => { + (window as any).__aiBase = base; + (window as any).__aiResponses = responses; + (window as any).__aiAssistant = assistant; + (window as any).__aiCallCount = 0; +}); + +const getAICallCount = ClientFunction( + () => (window as any).__aiCallCount as number, +); + +const aiGridOptions = (): any => ({ + ...(window as any).__aiBase, + aiAssistant: { + enabled: true, + ...(window as any).__aiAssistant, + aiIntegration: new (window as any).DevExpress.aiIntegration.AIIntegration({ + sendRequest() { + const responses = (window as any).__aiResponses; + const count = (window as any).__aiCallCount; + const response = responses[count]; + + (window as any).__aiCallCount = count + 1; + + if (response === '__HANG__') { + return { promise: new Promise(() => {}), abort: (): void => {} }; + } + + if (response === '__FAIL__') { + return { promise: Promise.reject(new Error('AI error')), abort: (): void => {} }; + } + + if (response === undefined) { + return { + promise: Promise.reject(new Error(`Unexpected AI call #${count}`)), + abort: (): void => {}, + }; + } + + return { promise: Promise.resolve(response), abort: (): void => {} }; + }, + }), + }, +}); + +const hangingCommandGridOptions = (): any => { + const data = Array.from({ length: 50 }, (_, i) => ({ + id: i + 1, + name: `Name ${i + 1}`, + value: (i + 1) * 10, + })); + + const store = new (window as any).DevExpress.data.CustomStore({ + key: 'id', + load(loadOptions: any) { + // Paged render load carries `take`; selectAll's all-pages key-load does not. + if (loadOptions.take !== undefined) { + const skip = loadOptions.skip ?? 0; + + return Promise.resolve({ + data: data.slice(skip, skip + loadOptions.take), + totalCount: data.length, + }); + } + + return new Promise(() => {}); + }, + totalCount: () => data.length, + }); + + return { + dataSource: store, + remoteOperations: true, + columns: ['id', 'name', 'value'], + showBorders: true, + selection: { mode: 'multiple' }, + aiAssistant: { + enabled: true, + ...(window as any).__aiAssistant, + aiIntegration: new (window as any).DevExpress.aiIntegration.AIIntegration({ + sendRequest() { + const responses = (window as any).__aiResponses; + const count = (window as any).__aiCallCount; + const response = responses[count]; + + (window as any).__aiCallCount = count + 1; + + if (response === undefined) { + return { + promise: Promise.reject(new Error(`Unexpected AI call #${count}`)), + abort: (): void => {}, + }; + } + + return { promise: Promise.resolve(response), abort: (): void => {} }; + }, + }), + }, + }; +}; + +const createGridWithAIAssistant = async ( + base: Record, + responses: unknown[], + assistant: Record = {}, +): Promise => { + await setupAIState(base, responses, assistant); + + return createWidget('dxDataGrid', aiGridOptions); +}; + +const createGridWithHangingCommand = async ( + responses: unknown[], + assistant: Record = {}, +): Promise => { + await setupAIState({}, responses, assistant); + + return createWidget('dxDataGrid', hangingCommandGridOptions); +}; + +const twoRows = [ + { id: 1, name: 'Alice', value: 30 }, + { id: 2, name: 'Bob', value: 20 }, +]; + +const threeRows = [ + { id: 1, name: 'Alice', value: 30 }, + { id: 2, name: 'Bob', value: 20 }, + { id: 3, name: 'Charlie', value: 10 }, +]; + +const baseGrid = (rows: unknown[]): Record => ({ + dataSource: rows, + keyExpr: 'id', + columns: ['id', 'name', 'value'], + showBorders: true, +}); + +const sortByName = { actions: [{ name: 'sorting', args: { dataField: 'name', sortOrder: 'asc' } }] }; +const selectAll = { actions: [{ name: 'selectAll', args: {} }] }; +const suggestionConfig = { chat: { suggestions: { items: [{ text: 'Sort by name' }] } } }; // === §3.8 Rapid sequential prompts === @@ -8,7 +163,7 @@ fixture.disablePageReloads`AI Assistant - Sequential Prompts` .page(AI_INTEGRATION_PAGE); // 3.8.1 -test('N prompts back-to-back should all execute in order', async (t) => { +test('N distinct prompts back-to-back should each execute once, in order, and apply to the grid', async (t) => { const dataGrid = new DataGrid(GRID_SELECTOR); await t.expect(dataGrid.isReady()).ok(); @@ -17,39 +172,33 @@ test('N prompts back-to-back should all execute in order', async (t) => { const aiChat = dataGrid.getAIAssistantChat(); - for (let i = 0; i < 4; i += 1) { + const steps = [ + { prompt: 'Sort by name', verify: async () => t.expect(await dataGrid.apiColumnOption('name', 'sortOrder')).eql('asc') }, + { prompt: 'Sort by value', verify: async () => t.expect(await dataGrid.apiColumnOption('value', 'sortOrder')).eql('desc') }, + { prompt: 'Clear sorting', verify: async () => t.expect(await dataGrid.apiColumnOption('value', 'sortOrder')).notOk() }, + { prompt: 'Group by name', verify: async () => t.expect(await dataGrid.apiColumnOption('name', 'groupIndex')).eql(0) }, + ]; + + for (let i = 0; i < steps.length; i += 1) { await t - .typeText(aiChat.getInput(), `Prompt ${i + 1}`) + .typeText(aiChat.getInput(), steps[i].prompt) .pressKey('enter'); await t.expect(aiChat.getSuccessMessages().count).eql(i + 1); await t.expect(aiChat.getSuccessActionItems(i).count).eql(1); + await t.expect(aiChat.isInputDisabled()).notOk(); + + await steps[i].verify(); } - await t.expect(aiChat.getMessages().count).eql(8); -}).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: 'clearSorting', args: {} }], - }), - abort: (): void => {}, - }; - }, - }), - }, -}))); + await t.expect(await getAICallCount()).eql(steps.length); + await t.expect(aiChat.getMessages().count).eql(steps.length * 2); +}).before(async () => createGridWithAIAssistant(baseGrid(threeRows), [ + { actions: [{ name: 'sorting', args: { dataField: 'name', sortOrder: 'asc' } }] }, + { actions: [{ name: 'sorting', args: { dataField: 'value', sortOrder: 'desc' } }] }, + { actions: [{ name: 'clearSorting', args: {} }] }, + { actions: [{ name: 'grouping', args: { dataField: 'name', groupIndex: 0 } }] }, +])); // === §3.9 Input disabled while in flight === @@ -72,26 +221,7 @@ test('Input should be disabled during LLM phase', async (t) => { await t.expect(aiChat.getPendingMessages().count).eql(1); await t.expect(aiChat.isInputDisabled()).ok(); -}).before(async () => createWidget('dxDataGrid', () => ({ - dataSource: [ - { id: 1, name: 'Alice', value: 30 }, - { id: 2, name: 'Bob', value: 20 }, - ], - 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 () => createGridWithAIAssistant(baseGrid(twoRows), [HANG])); // 3.9.2 test('Input should be disabled during command execution phase', async (t) => { @@ -107,58 +237,10 @@ test('Input should be disabled during command execution phase', async (t) => { .typeText(aiChat.getInput(), 'Select all rows') .pressKey('enter'); - // LLM resolved immediately; selectAll command is awaiting a server key-load - // that never resolves, so the request stays in flight during command execution. await t.expect(aiChat.isInputDisabled()).ok(); await t.expect(aiChat.getAIMessages().count).eql(1); await t.expect(aiChat.getPendingMessages().count).eql(1); -}).before(async () => createWidget('dxDataGrid', () => { - const data = Array.from({ length: 50 }, (_, i) => ({ - id: i + 1, - name: `Name ${i + 1}`, - value: (i + 1) * 10, - })); - - const store = new (window as any).DevExpress.data.CustomStore({ - key: 'id', - load(loadOptions: any) { - // Paged render load carries `take`; selectAll's all-pages key-load does not. - // Hang the key-load to keep command execution in flight deterministically. - if (loadOptions.take !== undefined) { - const skip = loadOptions.skip ?? 0; - - return Promise.resolve({ - data: data.slice(skip, skip + loadOptions.take), - totalCount: data.length, - }); - } - - return new Promise(() => {}); - }, - totalCount: () => data.length, - }); - - return { - dataSource: store, - remoteOperations: true, - columns: ['id', 'name', 'value'], - showBorders: true, - selection: { mode: 'multiple' }, - aiAssistant: { - enabled: true, - aiIntegration: new (window as any).DevExpress.aiIntegration.AIIntegration({ - sendRequest() { - return { - promise: Promise.resolve({ - actions: [{ name: 'selectAll', args: {} }], - }), - abort: (): void => {}, - }; - }, - }), - }, - }; -})); +}).before(async () => createGridWithHangingCommand([selectAll])); // 3.9.3 test('Input should be re-enabled after fulfillment', async (t) => { @@ -177,28 +259,8 @@ test('Input should be re-enabled after fulfillment', async (t) => { await t.expect(aiChat.getSuccessMessages().count).eql(1); await t.expect(aiChat.getSuccessActionItems(0).count).eql(1); await t.expect(aiChat.isInputDisabled()).notOk(); -}).before(async () => createWidget('dxDataGrid', () => ({ - dataSource: [ - { id: 1, name: 'Alice', value: 30 }, - { id: 2, name: 'Bob', value: 20 }, - ], - 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 t.expect(await dataGrid.apiColumnOption('name', 'sortOrder')).eql('asc'); +}).before(async () => createGridWithAIAssistant(baseGrid(twoRows), [sortByName])); // 3.9.4 test('Input should be re-enabled after failure', async (t) => { @@ -216,26 +278,8 @@ test('Input should be re-enabled after failure', async (t) => { await t.expect(aiChat.getErrorMessages().count).eql(1); await t.expect(aiChat.isInputDisabled()).notOk(); -}).before(async () => createWidget('dxDataGrid', () => ({ - dataSource: [ - { id: 1, name: 'Alice', value: 30 }, - { id: 2, name: 'Bob', value: 20 }, - ], - keyExpr: 'id', - columns: ['id', 'name', 'value'], - showBorders: true, - aiAssistant: { - enabled: true, - aiIntegration: new (window as any).DevExpress.aiIntegration.AIIntegration({ - sendRequest() { - return { - promise: Promise.reject(new Error('AI error')), - abort: (): void => {}, - }; - }, - }), - }, -}))); + await t.expect(await dataGrid.apiColumnOption('name', 'sortOrder')).notOk(); +}).before(async () => createGridWithAIAssistant(baseGrid(twoRows), [FAIL])); // 3.9.5 test('Input should be re-enabled after abort via popup close', async (t) => { @@ -263,26 +307,7 @@ test('Input should be re-enabled after abort via popup close', async (t) => { await t.expect(aiChat.getErrorMessages().count).eql(1); await t.expect(aiChat.isInputDisabled()).notOk(); -}).before(async () => createWidget('dxDataGrid', () => ({ - dataSource: [ - { id: 1, name: 'Alice', value: 30 }, - { id: 2, name: 'Bob', value: 20 }, - ], - 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 () => createGridWithAIAssistant(baseGrid(twoRows), [HANG])); // === §3.10 Clear-chat disabled while in flight === @@ -305,26 +330,7 @@ test('Clear-chat button should be disabled during LLM phase', async (t) => { await t.expect(aiChat.getPendingMessages().count).eql(1); await t.expect(aiChat.isClearChatDisabled()).ok(); -}).before(async () => createWidget('dxDataGrid', () => ({ - dataSource: [ - { id: 1, name: 'Alice', value: 30 }, - { id: 2, name: 'Bob', value: 20 }, - ], - 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 () => createGridWithAIAssistant(baseGrid(twoRows), [HANG])); // 3.10.2 test('Clear-chat button should be disabled during command execution phase', async (t) => { @@ -340,58 +346,31 @@ test('Clear-chat button should be disabled during command execution phase', asyn .typeText(aiChat.getInput(), 'Select all rows') .pressKey('enter'); - // selectAll command is awaiting a server key-load that never resolves. await t.expect(aiChat.isClearChatDisabled()).ok(); await t.expect(aiChat.getAIMessages().count).eql(1); await t.expect(aiChat.getPendingMessages().count).eql(1); -}).before(async () => createWidget('dxDataGrid', () => { - const data = Array.from({ length: 50 }, (_, i) => ({ - id: i + 1, - name: `Name ${i + 1}`, - value: (i + 1) * 10, - })); +}).before(async () => createGridWithHangingCommand([selectAll])); - const store = new (window as any).DevExpress.data.CustomStore({ - key: 'id', - load(loadOptions: any) { - if (loadOptions.take !== undefined) { - const skip = loadOptions.skip ?? 0; +// 3.10.3 — after fulfillment +test('Clear-chat button should be re-enabled after fulfillment', async (t) => { + const dataGrid = new DataGrid(GRID_SELECTOR); - return Promise.resolve({ - data: data.slice(skip, skip + loadOptions.take), - totalCount: data.length, - }); - } + await t.expect(dataGrid.isReady()).ok(); - return new Promise(() => {}); - }, - totalCount: () => data.length, - }); + await t.click(dataGrid.getAIAssistantButton()); - return { - dataSource: store, - remoteOperations: true, - columns: ['id', 'name', 'value'], - showBorders: true, - selection: { mode: 'multiple' }, - aiAssistant: { - enabled: true, - aiIntegration: new (window as any).DevExpress.aiIntegration.AIIntegration({ - sendRequest() { - return { - promise: Promise.resolve({ - actions: [{ name: 'selectAll', args: {} }], - }), - abort: (): void => {}, - }; - }, - }), - }, - }; -})); + const aiChat = dataGrid.getAIAssistantChat(); -// 3.10.3 -test('Clear-chat button should be re-enabled after fulfillment', async (t) => { + await t + .typeText(aiChat.getInput(), 'Sort by name') + .pressKey('enter'); + + await t.expect(aiChat.getSuccessMessages().count).eql(1); + await t.expect(aiChat.isClearChatDisabled()).notOk(); +}).before(async () => createGridWithAIAssistant(baseGrid(twoRows), [sortByName])); + +// 3.10.3 — after failure +test('Clear-chat button should be re-enabled after failure', async (t) => { const dataGrid = new DataGrid(GRID_SELECTOR); await t.expect(dataGrid.isReady()).ok(); @@ -404,33 +383,38 @@ test('Clear-chat button should be re-enabled after fulfillment', async (t) => { .typeText(aiChat.getInput(), 'Sort by name') .pressKey('enter'); - await t.expect(aiChat.getSuccessMessages().count).eql(1); + await t.expect(aiChat.getErrorMessages().count).eql(1); await t.expect(aiChat.isClearChatDisabled()).notOk(); -}).before(async () => createWidget('dxDataGrid', () => ({ - dataSource: [ - { id: 1, name: 'Alice', value: 30 }, - { id: 2, name: 'Bob', value: 20 }, - ], - 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 () => createGridWithAIAssistant(baseGrid(twoRows), [FAIL])); + +// 3.10.3 — after abort +test('Clear-chat button should be re-enabled after abort via 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.getPendingMessages().count).eql(1); + await t.expect(aiChat.isClearChatDisabled()).ok(); + + await t.click(aiChat.getCloseButton().element); + await t.expect(aiChat.getAbortConfirmDialog().exists).ok(); + await t.click(aiChat.getAbortConfirmYesButton()); + await t.click(dataGrid.getAIAssistantButton()); + + await t.expect(aiChat.getErrorMessages().count).eql(1); + await t.expect(aiChat.isClearChatDisabled()).notOk(); +}).before(async () => createGridWithAIAssistant(baseGrid(twoRows), [HANG])); // 3.10.4 -test('Clear-chat should remove all messages from chat', async (t) => { +test('Clear-chat should remove all messages from chat and leave grid state unchanged', async (t) => { const dataGrid = new DataGrid(GRID_SELECTOR); await t.expect(dataGrid.isReady()).ok(); @@ -444,32 +428,13 @@ test('Clear-chat should remove all messages from chat', async (t) => { .pressKey('enter'); await t.expect(aiChat.getSuccessMessages().count).eql(1); + await t.expect(await dataGrid.apiColumnOption('name', 'sortOrder')).eql('asc'); await t.click(aiChat.getClearChatButton()); await t.expect(aiChat.getMessages().count).eql(0); -}).before(async () => createWidget('dxDataGrid', () => ({ - dataSource: [ - { id: 1, name: 'Alice', value: 30 }, - { id: 2, name: 'Bob', value: 20 }, - ], - 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 t.expect(await dataGrid.apiColumnOption('name', 'sortOrder')).eql('asc'); +}).before(async () => createGridWithAIAssistant(baseGrid(twoRows), [sortByName])); // === §3.11 Suggestions disabled while in flight === @@ -477,7 +442,7 @@ fixture.disablePageReloads`AI Assistant - Suggestions In Flight` .page(AI_INTEGRATION_PAGE); // 3.11.1 -test('Suggestions should be disabled during LLM phase', async (t) => { +test('Suggestions should be disabled during LLM phase and dispatch no second request', async (t) => { const dataGrid = new DataGrid(GRID_SELECTOR); await t.expect(dataGrid.isReady()).ok(); @@ -492,34 +457,11 @@ test('Suggestions should be disabled during LLM phase', async (t) => { await t.expect(aiChat.getPendingMessages().count).eql(1); await t.expect(aiChat.isSuggestionDisabled(0)).ok(); -}).before(async () => createWidget('dxDataGrid', () => ({ - dataSource: [ - { id: 1, name: 'Alice', value: 30 }, - { id: 2, name: 'Bob', value: 20 }, - ], - 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 => {}, - }; - }, - }), - chat: { - suggestions: { - items: [{ text: 'Sort by name' }], - }, - }, - }, -}))); + await t.expect(await getAICallCount()).eql(1); +}).before(async () => createGridWithAIAssistant(baseGrid(twoRows), [HANG], suggestionConfig)); // 3.11.2 -test('Suggestions should be disabled during command execution phase', async (t) => { +test('Suggestions should be disabled during command execution phase and dispatch no second request', async (t) => { const dataGrid = new DataGrid(GRID_SELECTOR); await t.expect(dataGrid.isReady()).ok(); @@ -532,60 +474,11 @@ test('Suggestions should be disabled during command execution phase', async (t) .typeText(aiChat.getInput(), 'Select all rows') .pressKey('enter'); - // selectAll command is awaiting a server key-load that never resolves. await t.expect(aiChat.isSuggestionDisabled(0)).ok(); await t.expect(aiChat.getAIMessages().count).eql(1); await t.expect(aiChat.getPendingMessages().count).eql(1); -}).before(async () => createWidget('dxDataGrid', () => { - const data = Array.from({ length: 50 }, (_, i) => ({ - id: i + 1, - name: `Name ${i + 1}`, - value: (i + 1) * 10, - })); - - const store = new (window as any).DevExpress.data.CustomStore({ - key: 'id', - load(loadOptions: any) { - if (loadOptions.take !== undefined) { - const skip = loadOptions.skip ?? 0; - - return Promise.resolve({ - data: data.slice(skip, skip + loadOptions.take), - totalCount: data.length, - }); - } - - return new Promise(() => {}); - }, - totalCount: () => data.length, - }); - - return { - dataSource: store, - remoteOperations: true, - columns: ['id', 'name', 'value'], - showBorders: true, - selection: { mode: 'multiple' }, - aiAssistant: { - enabled: true, - aiIntegration: new (window as any).DevExpress.aiIntegration.AIIntegration({ - sendRequest() { - return { - promise: Promise.resolve({ - actions: [{ name: 'selectAll', args: {} }], - }), - abort: (): void => {}, - }; - }, - }), - chat: { - suggestions: { - items: [{ text: 'Sort by name' }], - }, - }, - }, - }; -})); + await t.expect(await getAICallCount()).eql(1); +}).before(async () => createGridWithHangingCommand([selectAll], suggestionConfig)); // 3.11.3 test('Suggestions should be re-enabled after resolution', async (t) => { @@ -603,30 +496,5 @@ test('Suggestions should be re-enabled after resolution', async (t) => { await t.expect(aiChat.getSuccessMessages().count).eql(1); await t.expect(aiChat.isSuggestionDisabled(0)).notOk(); -}).before(async () => createWidget('dxDataGrid', () => ({ - dataSource: [ - { id: 1, name: 'Alice', value: 30 }, - { id: 2, name: 'Bob', value: 20 }, - ], - 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 => {}, - }; - }, - }), - chat: { - suggestions: { - items: [{ text: 'Sort by name' }], - }, - }, - }, -}))); + await t.expect(await dataGrid.apiColumnOption('name', 'sortOrder')).eql('asc'); +}).before(async () => createGridWithAIAssistant(baseGrid(twoRows), [sortByName], suggestionConfig)); 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), -); diff --git a/packages/testcafe-models/dataGrid/aiAssistantChat.ts b/packages/testcafe-models/dataGrid/aiAssistantChat.ts index 623cf0336434..59f4f22287ab 100644 --- a/packages/testcafe-models/dataGrid/aiAssistantChat.ts +++ b/packages/testcafe-models/dataGrid/aiAssistantChat.ts @@ -66,10 +66,6 @@ export class AIAssistantChat extends Popup { 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}`); } @@ -147,8 +143,4 @@ export class AIAssistantChat extends Popup { isSuggestionDisabled(index: number): Promise { return this.getSuggestions().nth(index).hasClass('dx-state-disabled'); } - - getTitle(): Selector { - return this.topToolbar; - } } From 218f5f5868895b9f3c04e772edfc62f55a38c019 Mon Sep 17 00:00:00 2001 From: Alyar <> Date: Mon, 8 Jun 2026 00:14:12 +0400 Subject: [PATCH 03/10] DataGrid - AI Assistant: select abort-confirm Yes button by index Avoid the locale-dependent withExactText('Yes') selector (Copilot review); match the index-based approach used in the internal integration tests and on the interruption branch. --- packages/testcafe-models/dataGrid/aiAssistantChat.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/testcafe-models/dataGrid/aiAssistantChat.ts b/packages/testcafe-models/dataGrid/aiAssistantChat.ts index 59f4f22287ab..ad6188ac4c98 100644 --- a/packages/testcafe-models/dataGrid/aiAssistantChat.ts +++ b/packages/testcafe-models/dataGrid/aiAssistantChat.ts @@ -55,7 +55,7 @@ export class AIAssistantChat extends Popup { // eslint-disable-next-line class-methods-use-this getAbortConfirmYesButton(): Selector { - return Selector(`.${CLASS.abortConfirmDialog} .dx-button`).withExactText('Yes'); + return Selector(`.${CLASS.abortConfirmDialog} .dx-button`).nth(1); } getClearChatButton(): Selector { From 77b00010d2df55ab958f634c085551adf0ed60e8 Mon Sep 17 00:00:00 2001 From: Alyar <> Date: Mon, 8 Jun 2026 05:50:38 +0400 Subject: [PATCH 04/10] DataGrid - 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 | 80 +++---------------- 1 file changed, 12 insertions(+), 68 deletions(-) diff --git a/packages/testcafe-models/dataGrid/aiAssistantChat.ts b/packages/testcafe-models/dataGrid/aiAssistantChat.ts index ad6188ac4c98..ede1120ac3bc 100644 --- a/packages/testcafe-models/dataGrid/aiAssistantChat.ts +++ b/packages/testcafe-models/dataGrid/aiAssistantChat.ts @@ -4,27 +4,13 @@ import Button from '../button'; 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', 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', suggestion: 'dx-chat-suggestions', @@ -32,10 +18,6 @@ const CLASS = { }; export class AIAssistantChat extends Popup { - getWrapper(): Selector { - return this.element; - } - getChat(): Chat { return new Chat(this.element.find(`.${CLASS.aiChatContent}`)); } @@ -48,6 +30,10 @@ export class AIAssistantChat extends Popup { return new Button(this.element.find(`.${CLASS.closeButton}`)); } + getClearChatButton(): Selector { + return this.element.find(`.${CLASS.clearChatButton}`); + } + // eslint-disable-next-line class-methods-use-this getAbortConfirmDialog(): Selector { return Selector(`.${CLASS.abortConfirmDialog}`); @@ -58,10 +44,6 @@ export class AIAssistantChat extends Popup { return Selector(`.${CLASS.abortConfirmDialog} .dx-button`).nth(1); } - getClearChatButton(): Selector { - return this.element.find(`.${CLASS.clearChatButton}`); - } - getMessages(): Selector { return this.getChat().getMessageBubbles(); } @@ -70,6 +52,10 @@ export class AIAssistantChat extends Popup { return this.element.find(`.${CLASS.message}`); } + getAIMessage(index: number): Selector { + return this.getAIMessages().nth(index); + } + getPendingMessages(): Selector { return this.element.find(`.${CLASS.messagePending}`); } @@ -82,62 +68,20 @@ export class AIAssistantChat extends Popup { return this.element.find(`.${CLASS.messageError}`); } - getAIMessage(index: number): Selector { - return this.getAIMessages().nth(index); - } - - getMessageHeader(index: number): Selector { - return this.getAIMessage(index).find(`.${CLASS.messageHeader}`); - } - - getMessageErrorText(index: number): Selector { - return this.getAIMessage(index).find(`.${CLASS.messageErrorText}`); - } - - getMessageProgressBar(index: number): Selector { - return this.getAIMessage(index).find(`.${CLASS.messageProgressBar}`); - } - - getMessageRegenerateButton(index: number): Selector { - return this.getAIMessage(index).find(`.${CLASS.messageRegenerateButton}`); - } - - getActionList(messageIndex: number): Selector { - return this.getAIMessage(messageIndex).find(`.${CLASS.actionList}`); - } - - getActionItems(messageIndex: number): Selector { - return this.getAIMessage(messageIndex).find(`.${CLASS.actionListItem}`); - } - getSuccessActionItems(messageIndex: number): Selector { return this.getAIMessage(messageIndex).find(`.${CLASS.actionListItemSuccess}`); } - getErrorActionItems(messageIndex: number): Selector { - return this.getAIMessage(messageIndex).find(`.${CLASS.actionListItemError}`); - } - - 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}`); - } - 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'); } - isClearChatDisabled(): Promise { - return this.getClearChatButton() - .parent('.dx-button') - .hasClass('dx-state-disabled'); + isInputDisabled(): Promise { + return this.getChat().getTextArea().isDisabled; } isSuggestionDisabled(index: number): Promise { From d9e9e3c3a83299648925e062a6e96379e69736d3 Mon Sep 17 00:00:00 2001 From: Alyar <> Date: Mon, 8 Jun 2026 06:31:54 +0400 Subject: [PATCH 05/10] DataGrid - AI Assistant: use shared AI mock setup from testHelpers Drop the per-file test data and queue-based sendRequest mock in favor of the shared testHelpers implementation; keep the hanging-command store local. --- .../requestLifecycle.functional.ts | 105 ++++-------------- .../common/aiAssistant/testHelpers.ts | 96 ++++++++++++++++ 2 files changed, 120 insertions(+), 81 deletions(-) diff --git a/e2e/testcafe-devextreme/tests/dataGrid/common/aiAssistant/requestLifecycle.functional.ts b/e2e/testcafe-devextreme/tests/dataGrid/common/aiAssistant/requestLifecycle.functional.ts index b9b73c1d6bb6..16c33cc27eaf 100644 --- a/e2e/testcafe-devextreme/tests/dataGrid/common/aiAssistant/requestLifecycle.functional.ts +++ b/e2e/testcafe-devextreme/tests/dataGrid/common/aiAssistant/requestLifecycle.functional.ts @@ -1,65 +1,35 @@ /* eslint-disable no-underscore-dangle */ import DataGrid from 'devextreme-testcafe-models/dataGrid'; import { ClientFunction } from 'testcafe'; -import { AI_INTEGRATION_PAGE, GRID_SELECTOR } from './testHelpers'; import { createWidget } from '../../../../helpers/createWidget'; - -// Sentinels recognised by the mocked `sendRequest` queue: -// HANG — return a never-resolving promise (keeps the request in the LLM phase), -// FAIL — reject the request (LLM-phase failure). -// Any other queue entry is resolved as a canned `{ actions: [...] }` response. -const HANG = '__HANG__'; -const FAIL = '__FAIL__'; - -const setupAIState = ClientFunction(( - base: Record, - responses: unknown[], - assistant: Record, -) => { - (window as any).__aiBase = base; - (window as any).__aiResponses = responses; - (window as any).__aiAssistant = assistant; - (window as any).__aiCallCount = 0; -}); +import { + AI_INTEGRATION_PAGE, + FAIL, + GRID_SELECTOR, + HANG, + baseGrid as gridDefaults, + createGridWithAIAssistant, + setupAIState, + threeRows, + twoRows, +} from './testHelpers'; const getAICallCount = ClientFunction( () => (window as any).__aiCallCount as number, ); -const aiGridOptions = (): any => ({ - ...(window as any).__aiBase, - aiAssistant: { - enabled: true, - ...(window as any).__aiAssistant, - aiIntegration: new (window as any).DevExpress.aiIntegration.AIIntegration({ - sendRequest() { - const responses = (window as any).__aiResponses; - const count = (window as any).__aiCallCount; - const response = responses[count]; - - (window as any).__aiCallCount = count + 1; - - if (response === '__HANG__') { - return { promise: new Promise(() => {}), abort: (): void => {} }; - } - - if (response === '__FAIL__') { - return { promise: Promise.reject(new Error('AI error')), abort: (): void => {} }; - } - - if (response === undefined) { - return { - promise: Promise.reject(new Error(`Unexpected AI call #${count}`)), - abort: (): void => {}, - }; - } - - return { promise: Promise.resolve(response), abort: (): void => {} }; - }, - }), - }, +const baseGrid = (rows: unknown[]): Record => ({ + ...gridDefaults, + dataSource: rows, }); +const setAssistantExtra = ( + assistant: Record, +): Promise => ClientFunction( + () => { (window as any).__aiAssistantExtra = assistant; }, + { dependencies: { assistant } }, +)(); + const hangingCommandGridOptions = (): any => { const data = Array.from({ length: 50 }, (_, i) => ({ id: i + 1, @@ -93,7 +63,6 @@ const hangingCommandGridOptions = (): any => { selection: { mode: 'multiple' }, aiAssistant: { enabled: true, - ...(window as any).__aiAssistant, aiIntegration: new (window as any).DevExpress.aiIntegration.AIIntegration({ sendRequest() { const responses = (window as any).__aiResponses; @@ -112,47 +81,21 @@ const hangingCommandGridOptions = (): any => { return { promise: Promise.resolve(response), abort: (): void => {} }; }, }), + ...((window as any).__aiAssistantExtra ?? {}), }, }; }; -const createGridWithAIAssistant = async ( - base: Record, - responses: unknown[], - assistant: Record = {}, -): Promise => { - await setupAIState(base, responses, assistant); - - return createWidget('dxDataGrid', aiGridOptions); -}; - const createGridWithHangingCommand = async ( responses: unknown[], assistant: Record = {}, ): Promise => { - await setupAIState({}, responses, assistant); + await setupAIState({}, responses); + await setAssistantExtra(assistant); return createWidget('dxDataGrid', hangingCommandGridOptions); }; -const twoRows = [ - { id: 1, name: 'Alice', value: 30 }, - { id: 2, name: 'Bob', value: 20 }, -]; - -const threeRows = [ - { id: 1, name: 'Alice', value: 30 }, - { id: 2, name: 'Bob', value: 20 }, - { id: 3, name: 'Charlie', value: 10 }, -]; - -const baseGrid = (rows: unknown[]): Record => ({ - dataSource: rows, - keyExpr: 'id', - columns: ['id', 'name', 'value'], - showBorders: true, -}); - const sortByName = { actions: [{ name: 'sorting', args: { dataField: 'name', sortOrder: 'asc' } }] }; const selectAll = { actions: [{ name: 'selectAll', args: {} }] }; const suggestionConfig = { chat: { suggestions: { items: [{ text: 'Sort by name' }] } } }; diff --git a/e2e/testcafe-devextreme/tests/dataGrid/common/aiAssistant/testHelpers.ts b/e2e/testcafe-devextreme/tests/dataGrid/common/aiAssistant/testHelpers.ts index 104aa729b219..58f7612c5e73 100644 --- a/e2e/testcafe-devextreme/tests/dataGrid/common/aiAssistant/testHelpers.ts +++ b/e2e/testcafe-devextreme/tests/dataGrid/common/aiAssistant/testHelpers.ts @@ -1,5 +1,101 @@ +/* eslint-disable no-underscore-dangle */ +import { ClientFunction } from 'testcafe'; +import { createWidget } from '../../../../helpers/createWidget'; import url from '../../../../helpers/getPageUrl'; export const GRID_SELECTOR = '#container'; export const AI_INTEGRATION_PAGE = url(__dirname, '../../../container-ai-integration.html'); + +export const threeRows = [ + { id: 1, name: 'Alice', value: 30 }, + { id: 2, name: 'Bob', value: 20 }, + { id: 3, name: 'Charlie', value: 10 }, +]; + +export const twoRows = [ + { id: 1, name: 'Alice', value: 30 }, + { id: 2, name: 'Bob', value: 20 }, +]; + +export const baseGrid = { + keyExpr: 'id', + columns: ['id', 'name', 'value'], + showBorders: true, +}; + +export const HANG = '__HANG__'; + +export const FAIL = '__FAIL__'; + +export 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 = []; + (window as any).__aiAbortCalled = false; + (window as any).__aiAssistantExtra = {}; + (window as any).__aiGridExtra = {}; +}); + +const aiGridOptions = (): any => ({ + ...(window as any).__aiBase, + ...((window as any).__aiGridExtra ?? {}), + aiAssistant: { + enabled: true, + aiIntegration: new (window as any).DevExpress.aiIntegration.AIIntegration({ + sendRequest(params: any) { + const count = (window as any).__aiCallCount; + const response = (window as any).__aiResponses[count]; + + (window as any).__aiCallCount = count + 1; + (window as any).__aiRequests.push(params); + + const abort = (): void => { (window as any).__aiAbortCalled = true; }; + + if (response === '__HANG__') { + return { promise: new Promise(() => {}), abort }; + } + + if (response === '__FAIL__') { + return { promise: Promise.reject(new Error('AI error')), abort }; + } + + if (response === undefined) { + return { promise: Promise.reject(new Error(`Unexpected AI call #${count}`)), abort }; + } + + return { promise: Promise.resolve(response), abort }; + }, + }), + ...((window as any).__aiAssistantExtra ?? {}), + }, +}); + +const setAIExtras = ( + assistantExtra: Record, + gridExtra: Record, +): Promise => ClientFunction( + () => { + (window as any).__aiAssistantExtra = assistantExtra; + (window as any).__aiGridExtra = gridExtra; + }, + { dependencies: { assistantExtra, gridExtra } }, +)(); + +export const createGridWithAIAssistant = async ( + base: Record, + responses: unknown[], + assistantExtra: Record = {}, + gridExtra: Record = {}, +): Promise => { + await setupAIState(base, responses); + await setAIExtras(assistantExtra, gridExtra); + + return createWidget('dxDataGrid', aiGridOptions); +}; + +export const getRequests = ClientFunction(() => (window as any).__aiRequests); From 467e9ff4b834e02ba54e455380d8889b971536cc Mon Sep 17 00:00:00 2001 From: Alyar <> Date: Mon, 8 Jun 2026 13:59:47 +0400 Subject: [PATCH 06/10] DataGrid - AI Assistant: merge fixtures into one per file and drop disablePageReloads 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 dividers. disablePageReloads is unnecessary (per-test createWidget re-inits the grid), so it is removed. --- .../aiAssistant/requestLifecycle.functional.ts | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/e2e/testcafe-devextreme/tests/dataGrid/common/aiAssistant/requestLifecycle.functional.ts b/e2e/testcafe-devextreme/tests/dataGrid/common/aiAssistant/requestLifecycle.functional.ts index 16c33cc27eaf..3c6965fb7dab 100644 --- a/e2e/testcafe-devextreme/tests/dataGrid/common/aiAssistant/requestLifecycle.functional.ts +++ b/e2e/testcafe-devextreme/tests/dataGrid/common/aiAssistant/requestLifecycle.functional.ts @@ -100,11 +100,11 @@ const sortByName = { actions: [{ name: 'sorting', args: { dataField: 'name', sor const selectAll = { actions: [{ name: 'selectAll', args: {} }] }; const suggestionConfig = { chat: { suggestions: { items: [{ text: 'Sort by name' }] } } }; -// === §3.8 Rapid sequential prompts === - -fixture.disablePageReloads`AI Assistant - Sequential Prompts` +fixture`AI Assistant - Request Lifecycle` .page(AI_INTEGRATION_PAGE); +// === §3.8 Rapid sequential prompts === + // 3.8.1 test('N distinct prompts back-to-back should each execute once, in order, and apply to the grid', async (t) => { const dataGrid = new DataGrid(GRID_SELECTOR); @@ -145,9 +145,6 @@ test('N distinct prompts back-to-back should each execute once, in order, and ap // === §3.9 Input disabled while in flight === -fixture.disablePageReloads`AI Assistant - Input In Flight` - .page(AI_INTEGRATION_PAGE); - // 3.9.1 test('Input should be disabled during LLM phase', async (t) => { const dataGrid = new DataGrid(GRID_SELECTOR); @@ -254,9 +251,6 @@ test('Input should be re-enabled after abort via popup close', async (t) => { // === §3.10 Clear-chat disabled while in flight === -fixture.disablePageReloads`AI Assistant - Clear Chat In Flight` - .page(AI_INTEGRATION_PAGE); - // 3.10.1 test('Clear-chat button should be disabled during LLM phase', async (t) => { const dataGrid = new DataGrid(GRID_SELECTOR); @@ -381,9 +375,6 @@ test('Clear-chat should remove all messages from chat and leave grid state uncha // === §3.11 Suggestions disabled while in flight === -fixture.disablePageReloads`AI Assistant - Suggestions In Flight` - .page(AI_INTEGRATION_PAGE); - // 3.11.1 test('Suggestions should be disabled during LLM phase and dispatch no second request', async (t) => { const dataGrid = new DataGrid(GRID_SELECTOR); From d01a9704e182bbe23917be6f02a596d3311fcfa4 Mon Sep 17 00:00:00 2001 From: Alyar <> Date: Mon, 8 Jun 2026 14:32:09 +0400 Subject: [PATCH 07/10] DataGrid - 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). --- .../requestLifecycle.functional.ts | 24 +++++++++---------- .../dataGrid/aiAssistantChat.ts | 4 ++++ 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/e2e/testcafe-devextreme/tests/dataGrid/common/aiAssistant/requestLifecycle.functional.ts b/e2e/testcafe-devextreme/tests/dataGrid/common/aiAssistant/requestLifecycle.functional.ts index 3c6965fb7dab..9bde3159892c 100644 --- a/e2e/testcafe-devextreme/tests/dataGrid/common/aiAssistant/requestLifecycle.functional.ts +++ b/e2e/testcafe-devextreme/tests/dataGrid/common/aiAssistant/requestLifecycle.functional.ts @@ -116,10 +116,10 @@ test('N distinct prompts back-to-back should each execute once, in order, and ap const aiChat = dataGrid.getAIAssistantChat(); const steps = [ - { prompt: 'Sort by name', verify: async () => t.expect(await dataGrid.apiColumnOption('name', 'sortOrder')).eql('asc') }, - { prompt: 'Sort by value', verify: async () => t.expect(await dataGrid.apiColumnOption('value', 'sortOrder')).eql('desc') }, - { prompt: 'Clear sorting', verify: async () => t.expect(await dataGrid.apiColumnOption('value', 'sortOrder')).notOk() }, - { prompt: 'Group by name', verify: async () => t.expect(await dataGrid.apiColumnOption('name', 'groupIndex')).eql(0) }, + { prompt: 'Sort by name', verify: async () => t.expect(dataGrid.apiColumnOption('name', 'sortOrder')).eql('asc') }, + { prompt: 'Sort by value', verify: async () => t.expect(dataGrid.apiColumnOption('value', 'sortOrder')).eql('desc') }, + { prompt: 'Clear sorting', verify: async () => t.expect(dataGrid.apiColumnOption('value', 'sortOrder')).notOk() }, + { prompt: 'Group by name', verify: async () => t.expect(dataGrid.apiColumnOption('name', 'groupIndex')).eql(0) }, ]; for (let i = 0; i < steps.length; i += 1) { @@ -134,7 +134,7 @@ test('N distinct prompts back-to-back should each execute once, in order, and ap await steps[i].verify(); } - await t.expect(await getAICallCount()).eql(steps.length); + await t.expect(getAICallCount()).eql(steps.length); await t.expect(aiChat.getMessages().count).eql(steps.length * 2); }).before(async () => createGridWithAIAssistant(baseGrid(threeRows), [ { actions: [{ name: 'sorting', args: { dataField: 'name', sortOrder: 'asc' } }] }, @@ -199,7 +199,7 @@ test('Input should be re-enabled after fulfillment', async (t) => { await t.expect(aiChat.getSuccessMessages().count).eql(1); await t.expect(aiChat.getSuccessActionItems(0).count).eql(1); await t.expect(aiChat.isInputDisabled()).notOk(); - await t.expect(await dataGrid.apiColumnOption('name', 'sortOrder')).eql('asc'); + await t.expect(dataGrid.apiColumnOption('name', 'sortOrder')).eql('asc'); }).before(async () => createGridWithAIAssistant(baseGrid(twoRows), [sortByName])); // 3.9.4 @@ -218,7 +218,7 @@ test('Input should be re-enabled after failure', async (t) => { await t.expect(aiChat.getErrorMessages().count).eql(1); await t.expect(aiChat.isInputDisabled()).notOk(); - await t.expect(await dataGrid.apiColumnOption('name', 'sortOrder')).notOk(); + await t.expect(dataGrid.apiColumnOption('name', 'sortOrder')).notOk(); }).before(async () => createGridWithAIAssistant(baseGrid(twoRows), [FAIL])); // 3.9.5 @@ -365,12 +365,12 @@ test('Clear-chat should remove all messages from chat and leave grid state uncha .pressKey('enter'); await t.expect(aiChat.getSuccessMessages().count).eql(1); - await t.expect(await dataGrid.apiColumnOption('name', 'sortOrder')).eql('asc'); + await t.expect(dataGrid.apiColumnOption('name', 'sortOrder')).eql('asc'); await t.click(aiChat.getClearChatButton()); await t.expect(aiChat.getMessages().count).eql(0); - await t.expect(await dataGrid.apiColumnOption('name', 'sortOrder')).eql('asc'); + await t.expect(dataGrid.apiColumnOption('name', 'sortOrder')).eql('asc'); }).before(async () => createGridWithAIAssistant(baseGrid(twoRows), [sortByName])); // === §3.11 Suggestions disabled while in flight === @@ -391,7 +391,7 @@ test('Suggestions should be disabled during LLM phase and dispatch no second req await t.expect(aiChat.getPendingMessages().count).eql(1); await t.expect(aiChat.isSuggestionDisabled(0)).ok(); - await t.expect(await getAICallCount()).eql(1); + await t.expect(getAICallCount()).eql(1); }).before(async () => createGridWithAIAssistant(baseGrid(twoRows), [HANG], suggestionConfig)); // 3.11.2 @@ -411,7 +411,7 @@ test('Suggestions should be disabled during command execution phase and dispatch await t.expect(aiChat.isSuggestionDisabled(0)).ok(); await t.expect(aiChat.getAIMessages().count).eql(1); await t.expect(aiChat.getPendingMessages().count).eql(1); - await t.expect(await getAICallCount()).eql(1); + await t.expect(getAICallCount()).eql(1); }).before(async () => createGridWithHangingCommand([selectAll], suggestionConfig)); // 3.11.3 @@ -430,5 +430,5 @@ test('Suggestions should be re-enabled after resolution', async (t) => { await t.expect(aiChat.getSuccessMessages().count).eql(1); await t.expect(aiChat.isSuggestionDisabled(0)).notOk(); - await t.expect(await dataGrid.apiColumnOption('name', 'sortOrder')).eql('asc'); + await t.expect(dataGrid.apiColumnOption('name', 'sortOrder')).eql('asc'); }).before(async () => createGridWithAIAssistant(baseGrid(twoRows), [sortByName], suggestionConfig)); diff --git a/packages/testcafe-models/dataGrid/aiAssistantChat.ts b/packages/testcafe-models/dataGrid/aiAssistantChat.ts index ede1120ac3bc..4647ce526a56 100644 --- a/packages/testcafe-models/dataGrid/aiAssistantChat.ts +++ b/packages/testcafe-models/dataGrid/aiAssistantChat.ts @@ -18,6 +18,10 @@ const CLASS = { }; export class AIAssistantChat extends Popup { + getWrapper(): Selector { + return this.element; + } + getChat(): Chat { return new Chat(this.element.find(`.${CLASS.aiChatContent}`)); } From c1b75d2c2e35ddd262521f931aec65c1b5dd9d1b Mon Sep 17 00:00:00 2001 From: Alyar <> Date: Mon, 8 Jun 2026 22:30:40 +0400 Subject: [PATCH 08/10] 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 4647ce526a56..ea013a87eb16 100644 --- a/packages/testcafe-models/dataGrid/aiAssistantChat.ts +++ b/packages/testcafe-models/dataGrid/aiAssistantChat.ts @@ -12,7 +12,7 @@ const CLASS = { messageError: 'dx-ai-chat__message--error', actionListItemSuccess: 'dx-ai-chat__action-list-item--success', closeButton: 'dx-closebutton', - clearChatButton: 'dx-icon-clearhistory', + clearChatButton: 'dx-ai-chat__clear-button', suggestion: 'dx-chat-suggestions', suggestionButton: 'dx-button', }; @@ -81,7 +81,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 { From 991e9217fc7c935fabea5d3694be8e83ad44308f Mon Sep 17 00:00:00 2001 From: Alyar <> Date: Mon, 8 Jun 2026 22:21:41 +0400 Subject: [PATCH 09/10] DataGrid - AI Assistant: address Copilot review (HANG/FAIL via window markers) --- .../tests/dataGrid/common/aiAssistant/testHelpers.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 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 58f7612c5e73..9ba16903336b 100644 --- a/e2e/testcafe-devextreme/tests/dataGrid/common/aiAssistant/testHelpers.ts +++ b/e2e/testcafe-devextreme/tests/dataGrid/common/aiAssistant/testHelpers.ts @@ -31,6 +31,8 @@ export const FAIL = '__FAIL__'; export const setupAIState = ClientFunction(( base: Record, responses: unknown[], + hangMarker?: string, + failMarker?: string, ) => { (window as any).__aiBase = base; (window as any).__aiResponses = responses; @@ -39,6 +41,8 @@ export const setupAIState = ClientFunction(( (window as any).__aiAbortCalled = false; (window as any).__aiAssistantExtra = {}; (window as any).__aiGridExtra = {}; + (window as any).__aiHangMarker = hangMarker; + (window as any).__aiFailMarker = failMarker; }); const aiGridOptions = (): any => ({ @@ -56,11 +60,11 @@ const aiGridOptions = (): any => ({ const abort = (): void => { (window as any).__aiAbortCalled = true; }; - if (response === '__HANG__') { + if (response === (window as any).__aiHangMarker) { return { promise: new Promise(() => {}), abort }; } - if (response === '__FAIL__') { + if (response === (window as any).__aiFailMarker) { return { promise: Promise.reject(new Error('AI error')), abort }; } @@ -92,7 +96,7 @@ export const createGridWithAIAssistant = async ( assistantExtra: Record = {}, gridExtra: Record = {}, ): Promise => { - await setupAIState(base, responses); + await setupAIState(base, responses, HANG, FAIL); await setAIExtras(assistantExtra, gridExtra); return createWidget('dxDataGrid', aiGridOptions); From de9de22cbd655a9f563bd42a2dc75f8cb5b4f23a Mon Sep 17 00:00:00 2001 From: Alyar <> Date: Thu, 11 Jun 2026 17:37:04 +0400 Subject: [PATCH 10/10] DataGrid - AI Assistant: remove scenario comments from e2e tests --- .../requestLifecycle.functional.ts | 24 ------------------- 1 file changed, 24 deletions(-) diff --git a/e2e/testcafe-devextreme/tests/dataGrid/common/aiAssistant/requestLifecycle.functional.ts b/e2e/testcafe-devextreme/tests/dataGrid/common/aiAssistant/requestLifecycle.functional.ts index 9bde3159892c..120e204831c7 100644 --- a/e2e/testcafe-devextreme/tests/dataGrid/common/aiAssistant/requestLifecycle.functional.ts +++ b/e2e/testcafe-devextreme/tests/dataGrid/common/aiAssistant/requestLifecycle.functional.ts @@ -40,7 +40,6 @@ const hangingCommandGridOptions = (): any => { const store = new (window as any).DevExpress.data.CustomStore({ key: 'id', load(loadOptions: any) { - // Paged render load carries `take`; selectAll's all-pages key-load does not. if (loadOptions.take !== undefined) { const skip = loadOptions.skip ?? 0; @@ -103,9 +102,6 @@ const suggestionConfig = { chat: { suggestions: { items: [{ text: 'Sort by name' fixture`AI Assistant - Request Lifecycle` .page(AI_INTEGRATION_PAGE); -// === §3.8 Rapid sequential prompts === - -// 3.8.1 test('N distinct prompts back-to-back should each execute once, in order, and apply to the grid', async (t) => { const dataGrid = new DataGrid(GRID_SELECTOR); @@ -143,9 +139,6 @@ test('N distinct prompts back-to-back should each execute once, in order, and ap { actions: [{ name: 'grouping', args: { dataField: 'name', groupIndex: 0 } }] }, ])); -// === §3.9 Input disabled while in flight === - -// 3.9.1 test('Input should be disabled during LLM phase', async (t) => { const dataGrid = new DataGrid(GRID_SELECTOR); @@ -163,7 +156,6 @@ test('Input should be disabled during LLM phase', async (t) => { await t.expect(aiChat.isInputDisabled()).ok(); }).before(async () => createGridWithAIAssistant(baseGrid(twoRows), [HANG])); -// 3.9.2 test('Input should be disabled during command execution phase', async (t) => { const dataGrid = new DataGrid(GRID_SELECTOR); @@ -182,7 +174,6 @@ test('Input should be disabled during command execution phase', async (t) => { await t.expect(aiChat.getPendingMessages().count).eql(1); }).before(async () => createGridWithHangingCommand([selectAll])); -// 3.9.3 test('Input should be re-enabled after fulfillment', async (t) => { const dataGrid = new DataGrid(GRID_SELECTOR); @@ -202,7 +193,6 @@ test('Input should be re-enabled after fulfillment', async (t) => { await t.expect(dataGrid.apiColumnOption('name', 'sortOrder')).eql('asc'); }).before(async () => createGridWithAIAssistant(baseGrid(twoRows), [sortByName])); -// 3.9.4 test('Input should be re-enabled after failure', async (t) => { const dataGrid = new DataGrid(GRID_SELECTOR); @@ -221,7 +211,6 @@ test('Input should be re-enabled after failure', async (t) => { await t.expect(dataGrid.apiColumnOption('name', 'sortOrder')).notOk(); }).before(async () => createGridWithAIAssistant(baseGrid(twoRows), [FAIL])); -// 3.9.5 test('Input should be re-enabled after abort via popup close', async (t) => { const dataGrid = new DataGrid(GRID_SELECTOR); @@ -249,9 +238,6 @@ test('Input should be re-enabled after abort via popup close', async (t) => { await t.expect(aiChat.isInputDisabled()).notOk(); }).before(async () => createGridWithAIAssistant(baseGrid(twoRows), [HANG])); -// === §3.10 Clear-chat disabled while in flight === - -// 3.10.1 test('Clear-chat button should be disabled during LLM phase', async (t) => { const dataGrid = new DataGrid(GRID_SELECTOR); @@ -269,7 +255,6 @@ test('Clear-chat button should be disabled during LLM phase', async (t) => { await t.expect(aiChat.isClearChatDisabled()).ok(); }).before(async () => createGridWithAIAssistant(baseGrid(twoRows), [HANG])); -// 3.10.2 test('Clear-chat button should be disabled during command execution phase', async (t) => { const dataGrid = new DataGrid(GRID_SELECTOR); @@ -288,7 +273,6 @@ test('Clear-chat button should be disabled during command execution phase', asyn await t.expect(aiChat.getPendingMessages().count).eql(1); }).before(async () => createGridWithHangingCommand([selectAll])); -// 3.10.3 — after fulfillment test('Clear-chat button should be re-enabled after fulfillment', async (t) => { const dataGrid = new DataGrid(GRID_SELECTOR); @@ -306,7 +290,6 @@ test('Clear-chat button should be re-enabled after fulfillment', async (t) => { await t.expect(aiChat.isClearChatDisabled()).notOk(); }).before(async () => createGridWithAIAssistant(baseGrid(twoRows), [sortByName])); -// 3.10.3 — after failure test('Clear-chat button should be re-enabled after failure', async (t) => { const dataGrid = new DataGrid(GRID_SELECTOR); @@ -324,7 +307,6 @@ test('Clear-chat button should be re-enabled after failure', async (t) => { await t.expect(aiChat.isClearChatDisabled()).notOk(); }).before(async () => createGridWithAIAssistant(baseGrid(twoRows), [FAIL])); -// 3.10.3 — after abort test('Clear-chat button should be re-enabled after abort via popup close', async (t) => { const dataGrid = new DataGrid(GRID_SELECTOR); @@ -350,7 +332,6 @@ test('Clear-chat button should be re-enabled after abort via popup close', async await t.expect(aiChat.isClearChatDisabled()).notOk(); }).before(async () => createGridWithAIAssistant(baseGrid(twoRows), [HANG])); -// 3.10.4 test('Clear-chat should remove all messages from chat and leave grid state unchanged', async (t) => { const dataGrid = new DataGrid(GRID_SELECTOR); @@ -373,9 +354,6 @@ test('Clear-chat should remove all messages from chat and leave grid state uncha await t.expect(dataGrid.apiColumnOption('name', 'sortOrder')).eql('asc'); }).before(async () => createGridWithAIAssistant(baseGrid(twoRows), [sortByName])); -// === §3.11 Suggestions disabled while in flight === - -// 3.11.1 test('Suggestions should be disabled during LLM phase and dispatch no second request', async (t) => { const dataGrid = new DataGrid(GRID_SELECTOR); @@ -394,7 +372,6 @@ test('Suggestions should be disabled during LLM phase and dispatch no second req await t.expect(getAICallCount()).eql(1); }).before(async () => createGridWithAIAssistant(baseGrid(twoRows), [HANG], suggestionConfig)); -// 3.11.2 test('Suggestions should be disabled during command execution phase and dispatch no second request', async (t) => { const dataGrid = new DataGrid(GRID_SELECTOR); @@ -414,7 +391,6 @@ test('Suggestions should be disabled during command execution phase and dispatch await t.expect(getAICallCount()).eql(1); }).before(async () => createGridWithHangingCommand([selectAll], suggestionConfig)); -// 3.11.3 test('Suggestions should be re-enabled after resolution', async (t) => { const dataGrid = new DataGrid(GRID_SELECTOR);