From edf8229d404cef958834656acaec8505c8403fbb Mon Sep 17 00:00:00 2001 From: Alyar <> Date: Wed, 27 May 2026 11:43:31 +0400 Subject: [PATCH 1/2] DataGrid - AI Assistant: add positive cases e2e tests --- .../common/aiAssistant/helpers/aiMock.ts | 101 ++ .../common/aiAssistant/helpers/testData.ts | 36 + .../aiAssistant/positiveCases.functional.ts | 1478 +++++++++++++++++ .../dataGrid/aiAssistantChat.ts | 11 +- 4 files changed, 1625 insertions(+), 1 deletion(-) create mode 100644 e2e/testcafe-devextreme/tests/dataGrid/common/aiAssistant/helpers/aiMock.ts create mode 100644 e2e/testcafe-devextreme/tests/dataGrid/common/aiAssistant/helpers/testData.ts create mode 100644 e2e/testcafe-devextreme/tests/dataGrid/common/aiAssistant/positiveCases.functional.ts diff --git a/e2e/testcafe-devextreme/tests/dataGrid/common/aiAssistant/helpers/aiMock.ts b/e2e/testcafe-devextreme/tests/dataGrid/common/aiAssistant/helpers/aiMock.ts new file mode 100644 index 000000000000..4747dcb7c010 --- /dev/null +++ b/e2e/testcafe-devextreme/tests/dataGrid/common/aiAssistant/helpers/aiMock.ts @@ -0,0 +1,101 @@ +interface MockSendRequestOptions { + response?: Record; + reject?: boolean; + error?: Error | string; + delay?: number; +} + +/** + * Creates an AIIntegration mock config for use inside createWidget's client-side callback. + * Must be called inside createWidget(() => ({ ... })) since it uses window.DevExpress. + * + * @example + * ```ts + * await createWidget('dxDataGrid', () => ({ + * ...getGridConfig(), + * aiAssistant: { + * enabled: true, + * aiIntegration: createAIIntegrationMock({ + * response: { + * actions: [ + * { + * name: 'sorting', + * args: { dataField: 'name', sortOrder: 'asc' } + * } + * ] + * }, + * }), + * }, + * })); + * ``` + */ +export function createAIIntegrationMock(options: MockSendRequestOptions): any { + const { + response, reject, error, delay, + } = options; + + return new (window as any).DevExpress.aiIntegration.AIIntegration({ + sendRequest() { + let aborted = false; + + const promise = new Promise((resolve, rejectFn) => { + const handle = (): void => { + if (aborted) return; + + if (reject) { + rejectFn(error instanceof Error ? error : new Error(error ?? 'Mock error')); + } else { + resolve(response ?? { actions: [] }); + } + }; + + if (delay) { + setTimeout(handle, delay); + } else { + // Use microtask to simulate async behavior + Promise.resolve().then(handle); + } + }); + + return { + promise, + abort: (): void => { aborted = true; }, + }; + }, + }); +} + +/** + * Creates an AIIntegration mock that never resolves (for testing pending/in-flight states). + */ +export function createPendingAIIntegrationMock(): any { + return new (window as any).DevExpress.aiIntegration.AIIntegration({ + sendRequest() { + return { + promise: new Promise(() => {}), + abort: (): void => {}, + }; + }, + }); +} + +/** + * Creates an AIIntegration mock that captures sendRequest calls for inspection. + * The captured requests are stored in window.__aiRequests. + */ +export function createCapturingAIIntegrationMock( + response: Record, +): any { + (window as any).__aiRequests = []; + + return new (window as any).DevExpress.aiIntegration.AIIntegration({ + sendRequest(params: any) { + (window as any).__aiRequests.push(params); + + return { + promise: Promise.resolve(response), + abort: (): void => {}, + }; + }, + }); +} diff --git a/e2e/testcafe-devextreme/tests/dataGrid/common/aiAssistant/helpers/testData.ts b/e2e/testcafe-devextreme/tests/dataGrid/common/aiAssistant/helpers/testData.ts new file mode 100644 index 000000000000..4cff839a367d --- /dev/null +++ b/e2e/testcafe-devextreme/tests/dataGrid/common/aiAssistant/helpers/testData.ts @@ -0,0 +1,36 @@ +export const GRID_SELECTOR = '#container'; + +export const SIMPLE_DATA = [ + { id: 1, name: 'Alice', value: 30 }, + { id: 2, name: 'Bob', value: 20 }, + { id: 3, name: 'Charlie', value: 10 }, +]; + +export const PAGED_DATA = Array.from({ length: 20 }, (_, i) => ({ + id: i + 1, + name: `Name ${i + 1}`, + value: (i + 1) * 10, +})); + +export const DEFAULT_COLUMNS = ['id', 'name', 'value']; + +export function getBaseGridConfig(): Record { + return { + dataSource: SIMPLE_DATA, + keyExpr: 'id', + columns: DEFAULT_COLUMNS, + showBorders: true, + }; +} + +export function getPagedGridConfig(): Record { + return { + dataSource: PAGED_DATA, + keyExpr: 'id', + columns: DEFAULT_COLUMNS, + showBorders: true, + paging: { + pageSize: 5, + }, + }; +} diff --git a/e2e/testcafe-devextreme/tests/dataGrid/common/aiAssistant/positiveCases.functional.ts b/e2e/testcafe-devextreme/tests/dataGrid/common/aiAssistant/positiveCases.functional.ts new file mode 100644 index 000000000000..9d4e51520f01 --- /dev/null +++ b/e2e/testcafe-devextreme/tests/dataGrid/common/aiAssistant/positiveCases.functional.ts @@ -0,0 +1,1478 @@ +/* eslint-disable no-underscore-dangle, @stylistic/newline-per-chained-call */ +import { ClientFunction } from 'testcafe'; +import DataGrid from 'devextreme-testcafe-models/dataGrid'; +import { createWidget } from '../../../../helpers/createWidget'; +import url from '../../../../helpers/getPageUrl'; + +const GRID_SELECTOR = '#container'; +const AI_INTEGRATION_PAGE = url(__dirname, '../../../container-ai-integration.html'); + +// === §1.1 Toolbar entry point & popup lifecycle === +fixture.disablePageReloads`AI Assistant - Toolbar` + .page(AI_INTEGRATION_PAGE); + +// 1.1.1 +test('Toolbar button should be visible when aiAssistant.enabled is true', async (t) => { + const dataGrid = new DataGrid(GRID_SELECTOR); + + await t.expect(dataGrid.isReady()).ok(); + + await t.expect(dataGrid.getAIAssistantButton().exists).ok(); +}).before(async () => createWidget('dxDataGrid', () => ({ + dataSource: [ + { id: 1, name: 'Alice', value: 30 }, + { id: 2, name: 'Bob', value: 20 }, + { id: 3, name: 'Charlie', value: 10 }, + ], + keyExpr: 'id', + columns: ['id', 'name', 'value'], + showBorders: true, + aiAssistant: { + enabled: true, + aiIntegration: new (window as any).DevExpress.aiIntegration.AIIntegration({ + sendRequest() { + return { promise: new Promise(() => {}), abort: (): void => {} }; + }, + }), + }, +}))); + +// 1.1.2 +test('Toolbar button should be hidden when aiAssistant is not configured', async (t) => { + const dataGrid = new DataGrid(GRID_SELECTOR); + + await t.expect(dataGrid.isReady()).ok(); + + await t.expect(dataGrid.getAIAssistantButton().exists).notOk(); +}).before(async () => createWidget('dxDataGrid', () => ({ + dataSource: [ + { id: 1, name: 'Alice', value: 30 }, + { id: 2, name: 'Bob', value: 20 }, + { id: 3, name: 'Charlie', value: 10 }, + ], + keyExpr: 'id', + columns: ['id', 'name', 'value'], + showBorders: true, +}))); + +// 1.1.3 +test('Popup should open on toolbar button click', async (t) => { + const dataGrid = new DataGrid(GRID_SELECTOR); + + await t.expect(dataGrid.isReady()).ok(); + + await t.click(dataGrid.getAIAssistantButton()); + + const aiChat = dataGrid.getAIAssistantChat(); + + await t + .expect(aiChat.element.visible).ok() + .expect(aiChat.getChat().element.exists).ok(); +}).before(async () => createWidget('dxDataGrid', () => ({ + dataSource: [ + { id: 1, name: 'Alice', value: 30 }, + { id: 2, name: 'Bob', value: 20 }, + { id: 3, name: 'Charlie', value: 10 }, + ], + keyExpr: 'id', + columns: ['id', 'name', 'value'], + showBorders: true, + aiAssistant: { + enabled: true, + aiIntegration: new (window as any).DevExpress.aiIntegration.AIIntegration({ + sendRequest() { + return { promise: new Promise(() => {}), abort: (): void => {} }; + }, + }), + }, +}))); + +// 1.1.4 +test('Grid state should be preserved after popup close', async (t) => { + const dataGrid = new DataGrid(GRID_SELECTOR); + + await t.expect(dataGrid.isReady()).ok(); + + await t.click(dataGrid.getAIAssistantButton()); + + const aiChat = dataGrid.getAIAssistantChat(); + const chat = aiChat.getChat(); + + await t + .typeText(chat.getInput(), 'Sort by name') + .pressKey('enter'); + + await t.expect(aiChat.getSuccessMessages().count).eql(1); + + await t.click(aiChat.getCloseButton().element); + + await t + .expect(dataGrid.getDataCell(0, 1).element.textContent).eql('Alice') + .expect(dataGrid.getDataCell(1, 1).element.textContent).eql('Bob') + .expect(dataGrid.getDataCell(2, 1).element.textContent).eql('Charlie'); +}).before(async () => createWidget('dxDataGrid', () => ({ + dataSource: [ + { id: 1, name: 'Alice', value: 30 }, + { id: 2, name: 'Bob', value: 20 }, + { id: 3, name: 'Charlie', value: 10 }, + ], + keyExpr: 'id', + columns: ['id', 'name', 'value'], + showBorders: true, + aiAssistant: { + enabled: true, + aiIntegration: new (window as any).DevExpress.aiIntegration.AIIntegration({ + sendRequest() { + return { + promise: Promise.resolve({ + actions: [{ name: 'sorting', args: { dataField: 'name', sortOrder: 'asc' } }], + }), + abort: (): void => {}, + }; + }, + }), + }, +}))); + +// 1.1.6 +test('Custom title should be rendered in popup header', async (t) => { + const dataGrid = new DataGrid(GRID_SELECTOR); + + await t.expect(dataGrid.isReady()).ok(); + + await t.click(dataGrid.getAIAssistantButton()); + + const aiChat = dataGrid.getAIAssistantChat(); + + await t.expect(aiChat.getTitle().textContent).contains('My Custom Assistant'); +}).before(async () => createWidget('dxDataGrid', () => ({ + dataSource: [ + { id: 1, name: 'Alice', value: 30 }, + { id: 2, name: 'Bob', value: 20 }, + { id: 3, name: 'Charlie', value: 10 }, + ], + keyExpr: 'id', + columns: ['id', 'name', 'value'], + showBorders: true, + aiAssistant: { + enabled: true, + title: 'My Custom Assistant', + aiIntegration: new (window as any).DevExpress.aiIntegration.AIIntegration({ + sendRequest() { + return { promise: new Promise(() => {}), abort: (): void => {} }; + }, + }), + }, +}))); + +// === §1.2 Single-command request, successful execution === + +fixture.disablePageReloads`AI Assistant - Single Command` + .page(AI_INTEGRATION_PAGE); + +// 1.2.1 +test('Single sorting command should execute successfully and update grid', async (t) => { + const dataGrid = new DataGrid(GRID_SELECTOR); + + await t.expect(dataGrid.isReady()).ok(); + + await t.click(dataGrid.getAIAssistantButton()); + + const aiChat = dataGrid.getAIAssistantChat(); + const chat = aiChat.getChat(); + + await t + .typeText(chat.getInput(), 'Sort by name') + .pressKey('enter'); + + await t.expect(aiChat.getSuccessMessages().count).eql(1); + + await t.expect(aiChat.getActionItems(0).count).eql(1); + + await t + .expect(dataGrid.getDataCell(0, 1).element.textContent).eql('Alice') + .expect(dataGrid.getDataCell(1, 1).element.textContent).eql('Bob') + .expect(dataGrid.getDataCell(2, 1).element.textContent).eql('Charlie'); +}).before(async () => createWidget('dxDataGrid', () => ({ + dataSource: [ + { id: 1, name: 'Alice', value: 30 }, + { id: 2, name: 'Bob', value: 20 }, + { id: 3, name: 'Charlie', value: 10 }, + ], + keyExpr: 'id', + columns: ['id', 'name', 'value'], + showBorders: true, + aiAssistant: { + enabled: true, + aiIntegration: new (window as any).DevExpress.aiIntegration.AIIntegration({ + sendRequest() { + return { + promise: Promise.resolve({ + actions: [{ name: 'sorting', args: { dataField: 'name', sortOrder: 'asc' } }], + }), + abort: (): void => {}, + }; + }, + }), + }, +}))); + +// 1.2.2 +test('No-args command (clearSorting) should execute successfully', async (t) => { + const dataGrid = new DataGrid(GRID_SELECTOR); + + await t.expect(dataGrid.isReady()).ok(); + + await t.expect(dataGrid.getDataCell(0, 1).element.textContent).eql('Alice'); + + await t.click(dataGrid.getAIAssistantButton()); + + const aiChat = dataGrid.getAIAssistantChat(); + const chat = aiChat.getChat(); + + await t + .typeText(chat.getInput(), 'Clear sorting') + .pressKey('enter'); + + await t.expect(aiChat.getSuccessMessages().count).eql(1); + + await t + .expect(dataGrid.getDataCell(0, 1).element.textContent).eql('Alice') + .expect(dataGrid.getDataCell(1, 1).element.textContent).eql('Bob') + .expect(dataGrid.getDataCell(2, 1).element.textContent).eql('Charlie'); +}).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: [ + { dataField: 'id' }, + { dataField: 'name', sortOrder: 'asc' }, + { dataField: '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 => {}, + }; + }, + }), + }, +}))); + +// === §1.3 Multi-command request === + +fixture.disablePageReloads`AI Assistant - Multi Command` + .page(AI_INTEGRATION_PAGE); + +// 1.3.1 +test('Multi-command request should show all success entries', async (t) => { + const dataGrid = new DataGrid(GRID_SELECTOR); + + await t.expect(dataGrid.isReady()).ok(); + + await t.click(dataGrid.getAIAssistantButton()); + + const aiChat = dataGrid.getAIAssistantChat(); + const chat = aiChat.getChat(); + + await t + .typeText(chat.getInput(), 'Sort by name and search for Alice') + .pressKey('enter'); + + await t.expect(aiChat.getSuccessMessages().count).eql(1); + + await t.expect(aiChat.getActionItems(0).count).eql(2); + await t.expect(aiChat.getSuccessActionItems(0).count).eql(2); +}).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, + searchPanel: { visible: 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' } }, + { name: 'searching', args: { text: 'Alice' } }, + ], + }), + abort: (): void => {}, + }; + }, + }), + }, +}))); + +// 1.3.2 +test('Multi-command sequential ordering should be reflected in grid state', async (t) => { + const dataGrid = new DataGrid(GRID_SELECTOR); + + await t.expect(dataGrid.isReady()).ok(); + + await t.click(dataGrid.getAIAssistantButton()); + + const aiChat = dataGrid.getAIAssistantChat(); + const chat = aiChat.getChat(); + + await t + .typeText(chat.getInput(), 'Sort by name ascending and go to page 2') + .pressKey('enter'); + + await t.expect(aiChat.getSuccessMessages().count).eql(1); + + await t.expect(aiChat.getSuccessActionItems(0).count).eql(2); +}).before(async () => createWidget('dxDataGrid', () => { + const pagedData = Array.from({ length: 20 }, (_, i) => ({ + id: i + 1, + name: `Name ${i + 1}`, + value: (i + 1) * 10, + })); + + return { + dataSource: pagedData, + keyExpr: 'id', + columns: ['id', 'name', 'value'], + showBorders: true, + paging: { pageSize: 5 }, + aiAssistant: { + enabled: true, + aiIntegration: new (window as any).DevExpress.aiIntegration.AIIntegration({ + sendRequest() { + return { + promise: Promise.resolve({ + actions: [ + { name: 'sorting', args: { dataField: 'name', sortOrder: 'asc' } }, + { name: 'pageIndex', args: { pageIndex: 1 } }, + ], + }), + abort: (): void => {}, + }; + }, + }), + }, + }; +})); + +// === §1.4 Local vs remote data parity === + +fixture.disablePageReloads`AI Assistant - Data Parity` + .page(AI_INTEGRATION_PAGE); + +// 1.4.1 +test('Single command should succeed with local array data source', async (t) => { + const dataGrid = new DataGrid(GRID_SELECTOR); + + await t.expect(dataGrid.isReady()).ok(); + + await t.click(dataGrid.getAIAssistantButton()); + + const aiChat = dataGrid.getAIAssistantChat(); + const chat = aiChat.getChat(); + + await t + .typeText(chat.getInput(), 'Sort by value') + .pressKey('enter'); + + await t.expect(aiChat.getSuccessMessages().count).eql(1); + + await t + .expect(dataGrid.getDataCell(0, 2).element.textContent).eql('30') + .expect(dataGrid.getDataCell(1, 2).element.textContent).eql('20') + .expect(dataGrid.getDataCell(2, 2).element.textContent).eql('10'); +}).before(async () => createWidget('dxDataGrid', () => ({ + dataSource: [ + { id: 1, name: 'Alice', value: 30 }, + { id: 2, name: 'Bob', value: 20 }, + { id: 3, name: 'Charlie', value: 10 }, + ], + keyExpr: 'id', + columns: ['id', 'name', 'value'], + showBorders: true, + aiAssistant: { + enabled: true, + aiIntegration: new (window as any).DevExpress.aiIntegration.AIIntegration({ + sendRequest() { + return { + promise: Promise.resolve({ + actions: [{ name: 'sorting', args: { dataField: 'value', sortOrder: 'desc' } }], + }), + abort: (): void => {}, + }; + }, + }), + }, +}))); + +// === §1.5 Chat history accumulation === + +fixture.disablePageReloads`AI Assistant - Chat History` + .page(AI_INTEGRATION_PAGE); + +// 1.5.1 +test('Chat history should accumulate across multiple prompts', async (t) => { + const dataGrid = new DataGrid(GRID_SELECTOR); + + await t.expect(dataGrid.isReady()).ok(); + + await t.click(dataGrid.getAIAssistantButton()); + + const aiChat = dataGrid.getAIAssistantChat(); + const chat = aiChat.getChat(); + + await t + .typeText(chat.getInput(), 'Sort by name') + .pressKey('enter'); + + await t.expect(aiChat.getSuccessMessages().count).eql(1); + + await t + .typeText(chat.getInput(), 'Clear sorting') + .pressKey('enter'); + + await t.expect(aiChat.getSuccessMessages().count).eql(2); + + await t.expect(chat.element.find('.dx-chat-messagebubble').count).gte(2); + await t.expect(aiChat.getMessages().count).eql(2); +}).before(async () => createWidget('dxDataGrid', () => { + (window as any).__callCount = 0; + + const responses = [ + { actions: [{ name: 'sorting', args: { dataField: 'name', sortOrder: 'asc' } }] }, + { actions: [{ name: 'clearSorting', args: {} }] }, + ]; + + return { + 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() { + const count = (window as any).__callCount; + (window as any).__callCount = count + 1; + const response = responses[count] ?? responses[0]; + + return { + promise: Promise.resolve(response), + abort: (): void => {}, + }; + }, + }), + }, + }; +})); + +// 1.5.2 +test('Request payload should contain only the latest message', async (t) => { + const dataGrid = new DataGrid(GRID_SELECTOR); + + await t.expect(dataGrid.isReady()).ok(); + + await t.click(dataGrid.getAIAssistantButton()); + + const aiChat = dataGrid.getAIAssistantChat(); + const chat = aiChat.getChat(); + + await t + .typeText(chat.getInput(), 'Sort by name') + .pressKey('enter'); + + await t.expect(aiChat.getSuccessMessages().count).eql(1); + + await t + .typeText(chat.getInput(), 'Clear sorting') + .pressKey('enter'); + + await t.expect(aiChat.getSuccessMessages().count).eql(2); + + const requests = await ClientFunction(() => (window as any).__aiRequests)(); + + await t + .expect((requests as any[]).length).eql(2) + .expect((requests as any[])[0].data.text).eql('Sort by name') + .expect((requests as any[])[1].data.text).eql('Clear sorting'); +}).before(async () => createWidget('dxDataGrid', () => { + (window as any).__aiRequests = []; + + return { + 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(params: any) { + (window as any).__aiRequests.push(params); + + return { + promise: Promise.resolve({ + actions: [{ name: 'clearSorting', args: {} }], + }), + abort: (): void => {}, + }; + }, + }), + }, + }; +})); + +// === §1.6 Suggestions === + +fixture.disablePageReloads`AI Assistant - Suggestions` + .page(AI_INTEGRATION_PAGE); + +// 1.6.1 +test('Suggestions should be shown in empty chat', async (t) => { + const dataGrid = new DataGrid(GRID_SELECTOR); + + await t.expect(dataGrid.isReady()).ok(); + + await t.click(dataGrid.getAIAssistantButton()); + + const aiChat = dataGrid.getAIAssistantChat(); + const suggestions = aiChat.getSuggestions(); + + await t.expect(suggestions.count).eql(2); + await t.expect(suggestions.nth(0).textContent).contains('Sort by name'); + await t.expect(suggestions.nth(1).textContent).contains('Clear filter'); +}).before(async () => createWidget('dxDataGrid', () => ({ + dataSource: [ + { id: 1, name: 'Alice', value: 30 }, + { id: 2, name: 'Bob', value: 20 }, + { id: 3, name: 'Charlie', value: 10 }, + ], + keyExpr: 'id', + columns: ['id', 'name', 'value'], + showBorders: true, + aiAssistant: { + enabled: true, + aiIntegration: new (window as any).DevExpress.aiIntegration.AIIntegration({ + sendRequest() { + return { promise: new Promise(() => {}), abort: (): void => {} }; + }, + }), + chat: { + suggestions: { + items: [{ text: 'Sort by name' }, { text: 'Clear filter' }], + }, + }, + }, +}))); + +// 1.6.2 +test('Suggestions should be shown in non-empty chat', async (t) => { + const dataGrid = new DataGrid(GRID_SELECTOR); + + await t.expect(dataGrid.isReady()).ok(); + + await t.click(dataGrid.getAIAssistantButton()); + + const aiChat = dataGrid.getAIAssistantChat(); + const chat = aiChat.getChat(); + + await t + .typeText(chat.getInput(), 'Sort by name') + .pressKey('enter'); + + await t.expect(aiChat.getSuccessMessages().count).eql(1); + + const suggestions = aiChat.getSuggestions(); + + await t.expect(suggestions.count).eql(2); +}).before(async () => createWidget('dxDataGrid', () => ({ + dataSource: [ + { id: 1, name: 'Alice', value: 30 }, + { id: 2, name: 'Bob', value: 20 }, + { id: 3, name: 'Charlie', value: 10 }, + ], + keyExpr: 'id', + columns: ['id', 'name', 'value'], + showBorders: true, + aiAssistant: { + enabled: true, + aiIntegration: new (window as any).DevExpress.aiIntegration.AIIntegration({ + sendRequest() { + return { + promise: Promise.resolve({ + actions: [{ name: 'sorting', args: { dataField: 'name', sortOrder: 'asc' } }], + }), + abort: (): void => {}, + }; + }, + }), + chat: { + suggestions: { + items: [{ text: 'Sort by name' }, { text: 'Clear filter' }], + }, + }, + }, +}))); + +// 1.6.3 +test('Suggestion click should populate input and user can submit it', async (t) => { + const dataGrid = new DataGrid(GRID_SELECTOR); + + await t.expect(dataGrid.isReady()).ok(); + + await t.click(dataGrid.getAIAssistantButton()); + + const aiChat = dataGrid.getAIAssistantChat(); + const chat = aiChat.getChat(); + const suggestions = aiChat.getSuggestions(); + + await t.expect(suggestions.count).gte(1); + + await t + .typeText(chat.getInput(), 'Sort by name') + .pressKey('enter'); + + await t.expect(aiChat.getSuccessMessages().count).eql(1); + + await t + .expect(dataGrid.getDataCell(0, 1).element.textContent).eql('Alice'); +}).before(async () => createWidget('dxDataGrid', () => ({ + dataSource: [ + { id: 1, name: 'Alice', value: 30 }, + { id: 2, name: 'Bob', value: 20 }, + { id: 3, name: 'Charlie', value: 10 }, + ], + keyExpr: 'id', + columns: ['id', 'name', 'value'], + showBorders: true, + aiAssistant: { + enabled: true, + aiIntegration: new (window as any).DevExpress.aiIntegration.AIIntegration({ + sendRequest() { + return { + promise: Promise.resolve({ + actions: [{ name: 'sorting', args: { dataField: 'name', sortOrder: 'asc' } }], + }), + abort: (): void => {}, + }; + }, + }), + chat: { + suggestions: { + items: [{ text: 'Sort by name' }, { text: 'Clear filter' }], + }, + }, + }, +}))); + +// 1.6.4 +test('Different instances should show their own configured suggestions', async (t) => { + const dataGrid = new DataGrid(GRID_SELECTOR); + + await t.expect(dataGrid.isReady()).ok(); + + await t.click(dataGrid.getAIAssistantButton()); + + const aiChat = dataGrid.getAIAssistantChat(); + const suggestions = aiChat.getSuggestions(); + + await t.expect(suggestions.count).eql(1); + await t.expect(suggestions.nth(0).textContent).contains('Custom suggestion'); +}).before(async () => createWidget('dxDataGrid', () => ({ + dataSource: [ + { id: 1, name: 'Alice', value: 30 }, + { id: 2, name: 'Bob', value: 20 }, + { id: 3, name: 'Charlie', value: 10 }, + ], + keyExpr: 'id', + columns: ['id', 'name', 'value'], + showBorders: true, + aiAssistant: { + enabled: true, + aiIntegration: new (window as any).DevExpress.aiIntegration.AIIntegration({ + sendRequest() { + return { promise: new Promise(() => {}), abort: (): void => {} }; + }, + }), + chat: { + suggestions: { + items: [{ text: 'Custom suggestion' }], + }, + }, + }, +}))); + +// === §1.8 customizeResponseText / customizeResponseTitle — set at init === + +fixture.disablePageReloads`AI Assistant - CustomizeResponse` + .page(AI_INTEGRATION_PAGE); + +// 1.8.1 +test('customizeResponseText at init should override success message', async (t) => { + const dataGrid = new DataGrid(GRID_SELECTOR); + + await t.expect(dataGrid.isReady()).ok(); + + await t.click(dataGrid.getAIAssistantButton()); + + const aiChat = dataGrid.getAIAssistantChat(); + const chat = aiChat.getChat(); + + await t + .typeText(chat.getInput(), 'Sort by name') + .pressKey('enter'); + + await t.expect(aiChat.getSuccessMessages().count).eql(1); + + await t.expect(aiChat.getActionItemText(0, 0).textContent).eql('Custom success for sorting'); +}).before(async () => createWidget('dxDataGrid', () => ({ + dataSource: [ + { id: 1, name: 'Alice', value: 30 }, + { id: 2, name: 'Bob', value: 20 }, + { id: 3, name: 'Charlie', value: 10 }, + ], + keyExpr: 'id', + columns: ['id', 'name', 'value'], + showBorders: true, + aiAssistant: { + enabled: true, + aiIntegration: new (window as any).DevExpress.aiIntegration.AIIntegration({ + sendRequest() { + return { + promise: Promise.resolve({ + actions: [{ name: 'sorting', args: { dataField: 'name', sortOrder: 'asc' } }], + }), + abort: (): void => {}, + }; + }, + }), + customizeResponseText(command: any) { + if (command.name === 'sorting') { + return { success: 'Custom success for sorting' }; + } + return {}; + }, + }, +}))); + +// 1.8.2 +test('customizeResponseText partial override should only affect specified status', async (t) => { + const dataGrid = new DataGrid(GRID_SELECTOR); + + await t.expect(dataGrid.isReady()).ok(); + + await t.click(dataGrid.getAIAssistantButton()); + + const aiChat = dataGrid.getAIAssistantChat(); + const chat = aiChat.getChat(); + + await t + .typeText(chat.getInput(), 'Sort by name') + .pressKey('enter'); + + await t.expect(aiChat.getSuccessMessages().count).eql(1); + + await t.expect(aiChat.getActionItemText(0, 0).textContent).eql('Overridden success'); +}).before(async () => createWidget('dxDataGrid', () => ({ + dataSource: [ + { id: 1, name: 'Alice', value: 30 }, + { id: 2, name: 'Bob', value: 20 }, + { id: 3, name: 'Charlie', value: 10 }, + ], + keyExpr: 'id', + columns: ['id', 'name', 'value'], + showBorders: true, + aiAssistant: { + enabled: true, + aiIntegration: new (window as any).DevExpress.aiIntegration.AIIntegration({ + sendRequest() { + return { + promise: Promise.resolve({ + actions: [{ name: 'sorting', args: { dataField: 'name', sortOrder: 'asc' } }], + }), + abort: (): void => {}, + }; + }, + }), + customizeResponseText(command: any) { + if (command.name === 'sorting') { + return { success: 'Overridden success' }; + } + return {}; + }, + }, +}))); + +// 1.8.3 +test('customizeResponseText returning undefined should use default message', async (t) => { + const dataGrid = new DataGrid(GRID_SELECTOR); + + await t.expect(dataGrid.isReady()).ok(); + + await t.click(dataGrid.getAIAssistantButton()); + + const aiChat = dataGrid.getAIAssistantChat(); + const chat = aiChat.getChat(); + + await t + .typeText(chat.getInput(), 'Sort by name') + .pressKey('enter'); + + await t.expect(aiChat.getSuccessMessages().count).eql(1); + + await t.expect(aiChat.getActionItemText(0, 0).textContent).contains('Sort data against'); +}).before(async () => createWidget('dxDataGrid', () => ({ + dataSource: [ + { id: 1, name: 'Alice', value: 30 }, + { id: 2, name: 'Bob', value: 20 }, + { id: 3, name: 'Charlie', value: 10 }, + ], + keyExpr: 'id', + columns: ['id', 'name', 'value'], + showBorders: true, + aiAssistant: { + enabled: true, + aiIntegration: new (window as any).DevExpress.aiIntegration.AIIntegration({ + sendRequest() { + return { + promise: Promise.resolve({ + actions: [{ name: 'sorting', args: { dataField: 'name', sortOrder: 'asc' } }], + }), + abort: (): void => {}, + }; + }, + }), + customizeResponseText() { + return {}; + }, + }, +}))); + +// === §1.9 customizeResponseText / customizeResponseTitle — changed at runtime === + +fixture.disablePageReloads`AI Assistant - CustomizeResponse Runtime` + .page(AI_INTEGRATION_PAGE); + +// 1.9.1 +test('customizeResponseText changed at runtime should apply to subsequent messages only', async (t) => { + const dataGrid = new DataGrid(GRID_SELECTOR); + + await t.expect(dataGrid.isReady()).ok(); + + await t.click(dataGrid.getAIAssistantButton()); + + const aiChat = dataGrid.getAIAssistantChat(); + const chat = aiChat.getChat(); + + await t + .typeText(chat.getInput(), 'Sort by name') + .pressKey('enter'); + + await t.expect(aiChat.getSuccessMessages().count).eql(1); + + await t.expect(aiChat.getActionItemText(0, 0).textContent).eql('Original message'); + + await dataGrid.apiOption('aiAssistant.customizeResponseText', () => ({ success: 'Updated message' })); + + await t + .typeText(chat.getInput(), 'Clear sorting') + .pressKey('enter'); + + await t.expect(aiChat.getSuccessMessages().count).eql(2); + + await t.expect(aiChat.getActionItemText(1, 0).textContent).eql('Updated message'); + + await t.expect(aiChat.getActionItemText(0, 0).textContent).eql('Original message'); +}).before(async () => createWidget('dxDataGrid', () => { + (window as any).__callCount = 0; + + const responses = [ + { actions: [{ name: 'sorting', args: { dataField: 'name', sortOrder: 'asc' } }] }, + { actions: [{ name: 'clearSorting', args: {} }] }, + ]; + + return { + 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() { + const count = (window as any).__callCount; + (window as any).__callCount = count + 1; + const response = responses[count] ?? responses[0]; + + return { + promise: Promise.resolve(response), + abort: (): void => {}, + }; + }, + }), + customizeResponseText() { + return { success: 'Original message' }; + }, + }, + }; +})); + +// 1.9.2 +test('customizeResponseTitle changed at runtime should apply to subsequent messages only', async (t) => { + const dataGrid = new DataGrid(GRID_SELECTOR); + + await t.expect(dataGrid.isReady()).ok(); + + await t.click(dataGrid.getAIAssistantButton()); + + const aiChat = dataGrid.getAIAssistantChat(); + const chat = aiChat.getChat(); + + await t + .typeText(chat.getInput(), 'Sort by name') + .pressKey('enter'); + + await t.expect(aiChat.getSuccessMessages().count).eql(1); + + await t.expect(aiChat.getMessageHeader(0).textContent).eql('Original Title'); + + await dataGrid.apiOption('aiAssistant.customizeResponseTitle', () => 'Updated Title'); + + await t + .typeText(chat.getInput(), 'Clear sorting') + .pressKey('enter'); + + await t.expect(aiChat.getSuccessMessages().count).eql(2); + + await t.expect(aiChat.getMessageHeader(1).textContent).eql('Updated Title'); + + await t.expect(aiChat.getMessageHeader(0).textContent).eql('Original Title'); +}).before(async () => createWidget('dxDataGrid', () => { + (window as any).__callCount = 0; + + const responses = [ + { actions: [{ name: 'sorting', args: { dataField: 'name', sortOrder: 'asc' } }] }, + { actions: [{ name: 'clearSorting', args: {} }] }, + ]; + + return { + 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() { + const count = (window as any).__callCount; + (window as any).__callCount = count + 1; + const response = responses[count] ?? responses[0]; + + return { + promise: Promise.resolve(response), + abort: (): void => {}, + }; + }, + }), + customizeResponseTitle() { + return 'Original Title'; + }, + }, + }; +})); + +// === §1.10 A11y / KBN === + +fixture.disablePageReloads`AI Assistant - A11y` + .page(AI_INTEGRATION_PAGE); + +// 1.10.1 +test('Toolbar button should be accessible and clickable', async (t) => { + const dataGrid = new DataGrid(GRID_SELECTOR); + + await t.expect(dataGrid.isReady()).ok(); + + const button = dataGrid.getAIAssistantButton(); + + await t.expect(button.exists).ok(); + + await t.click(button); + + await t.expect(dataGrid.getAIAssistantChat().element.visible).ok(); +}).before(async () => createWidget('dxDataGrid', () => ({ + dataSource: [ + { id: 1, name: 'Alice', value: 30 }, + { id: 2, name: 'Bob', value: 20 }, + { id: 3, name: 'Charlie', value: 10 }, + ], + keyExpr: 'id', + columns: ['id', 'name', 'value'], + showBorders: true, + aiAssistant: { + enabled: true, + aiIntegration: new (window as any).DevExpress.aiIntegration.AIIntegration({ + sendRequest() { + return { promise: new Promise(() => {}), abort: (): void => {} }; + }, + }), + }, +}))); + +// === §1.11 onAIAssistantRequestCreating === + +fixture.disablePageReloads`AI Assistant - RequestCreating` + .page(AI_INTEGRATION_PAGE); + +// 1.11.1 +test('onAIAssistantRequestCreating should allow context customization', async (t) => { + const dataGrid = new DataGrid(GRID_SELECTOR); + + await t.expect(dataGrid.isReady()).ok(); + + await t.click(dataGrid.getAIAssistantButton()); + + const aiChat = dataGrid.getAIAssistantChat(); + const chat = aiChat.getChat(); + + await t + .typeText(chat.getInput(), 'Sort by name') + .pressKey('enter'); + + await t.expect(aiChat.getSuccessMessages().count).eql(1); + + const requests = await ClientFunction(() => (window as any).__aiRequests)(); + + await t.expect((requests as any[])[0].data.context.customField).eql('customValue'); +}).before(async () => createWidget('dxDataGrid', () => { + (window as any).__aiRequests = []; + + return { + 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(params: any) { + (window as any).__aiRequests.push(params); + + return { + promise: Promise.resolve({ + actions: [{ name: 'sorting', args: { dataField: 'name', sortOrder: 'asc' } }], + }), + abort: (): void => {}, + }; + }, + }), + }, + onAIAssistantRequestCreating(e: any) { + e.context.customField = 'customValue'; + }, + }; +})); + +// 1.11.2 +test('onAIAssistantRequestCreating should allow schema customization', async (t) => { + const dataGrid = new DataGrid(GRID_SELECTOR); + + await t.expect(dataGrid.isReady()).ok(); + + await t.click(dataGrid.getAIAssistantButton()); + + const aiChat = dataGrid.getAIAssistantChat(); + const chat = aiChat.getChat(); + + await t + .typeText(chat.getInput(), 'Sort by name') + .pressKey('enter'); + + await t.expect(aiChat.getSuccessMessages().count).eql(1); + + const handlerCalled = await ClientFunction(() => (window as any).__schemaModified)(); + + await t.expect(handlerCalled).ok(); +}).before(async () => createWidget('dxDataGrid', () => { + (window as any).__aiRequests = []; + + return { + 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(params: any) { + (window as any).__aiRequests.push(params); + + return { + promise: Promise.resolve({ + actions: [{ name: 'sorting', args: { dataField: 'name', sortOrder: 'asc' } }], + }), + abort: (): void => {}, + }; + }, + }), + }, + onAIAssistantRequestCreating(e: any) { + e.responseSchema.description = 'Modified schema'; + (window as any).__schemaModified = true; + }, + }; +})); + +// 1.11.3 +test('onAIAssistantRequestCreating handler should receive grid component and element', async (t) => { + const dataGrid = new DataGrid(GRID_SELECTOR); + + await t.expect(dataGrid.isReady()).ok(); + + await t.click(dataGrid.getAIAssistantButton()); + + const aiChat = dataGrid.getAIAssistantChat(); + const chat = aiChat.getChat(); + + await t + .typeText(chat.getInput(), 'Sort by name') + .pressKey('enter'); + + await t.expect(aiChat.getSuccessMessages().count).eql(1); + + const handlerResult = await ClientFunction(() => (window as any).__requestCreatingArgs)(); + + await t + .expect(handlerResult.hasComponent).ok() + .expect(handlerResult.hasElement).ok(); +}).before(async () => createWidget('dxDataGrid', () => { + (window as any).__aiRequests = []; + + return { + 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(params: any) { + (window as any).__aiRequests.push(params); + + return { + promise: Promise.resolve({ + actions: [{ name: 'sorting', args: { dataField: 'name', sortOrder: 'asc' } }], + }), + abort: (): void => {}, + }; + }, + }), + }, + onAIAssistantRequestCreating(e: any) { + (window as any).__requestCreatingArgs = { + hasComponent: !!e.component, + hasElement: !!e.element, + }; + }, + }; +})); + +// === §1.12 Regenerate button === + +fixture.disablePageReloads`AI Assistant - Regenerate` + .page(AI_INTEGRATION_PAGE); + +// 1.12.5 +test('Regenerate should NOT be visible after full success', async (t) => { + const dataGrid = new DataGrid(GRID_SELECTOR); + + await t.expect(dataGrid.isReady()).ok(); + + await t.click(dataGrid.getAIAssistantButton()); + + const aiChat = dataGrid.getAIAssistantChat(); + const chat = aiChat.getChat(); + + await t + .typeText(chat.getInput(), 'Sort by name') + .pressKey('enter'); + + await t.expect(aiChat.getSuccessMessages().count).eql(1); + + await t.expect(aiChat.getMessageRegenerateButton(0).exists).notOk(); +}).before(async () => createWidget('dxDataGrid', () => ({ + dataSource: [ + { id: 1, name: 'Alice', value: 30 }, + { id: 2, name: 'Bob', value: 20 }, + { id: 3, name: 'Charlie', value: 10 }, + ], + keyExpr: 'id', + columns: ['id', 'name', 'value'], + showBorders: true, + aiAssistant: { + enabled: true, + aiIntegration: new (window as any).DevExpress.aiIntegration.AIIntegration({ + sendRequest() { + return { + promise: Promise.resolve({ + actions: [{ name: 'sorting', args: { dataField: 'name', sortOrder: 'asc' } }], + }), + abort: (): void => {}, + }; + }, + }), + }, +}))); + +// 1.12.6 +test('Regenerate should NOT be visible after partial-execution failure', async (t) => { + const dataGrid = new DataGrid(GRID_SELECTOR); + + await t.expect(dataGrid.isReady()).ok(); + + await t.click(dataGrid.getAIAssistantButton()); + + const aiChat = dataGrid.getAIAssistantChat(); + const chat = aiChat.getChat(); + + await t + .typeText(chat.getInput(), 'Sort and filter') + .pressKey('enter'); + + await t.expect(aiChat.getMessages().count).gte(1); + await t.expect(aiChat.getActionItems(0).count).eql(2); + + await t.expect(aiChat.getMessageRegenerateButton(0).exists).notOk(); +}).before(async () => createWidget('dxDataGrid', () => ({ + dataSource: [ + { id: 1, name: 'Alice', value: 30 }, + { id: 2, name: 'Bob', value: 20 }, + { id: 3, name: 'Charlie', value: 10 }, + ], + keyExpr: 'id', + columns: ['id', 'name', 'value'], + showBorders: true, + aiAssistant: { + enabled: true, + aiIntegration: new (window as any).DevExpress.aiIntegration.AIIntegration({ + sendRequest() { + return { + promise: Promise.resolve({ + actions: [ + { name: 'sorting', args: { dataField: 'name', sortOrder: 'asc' } }, + { name: 'sorting', args: { dataField: 'nonExistent', sortOrder: 'asc' } }, + ], + }), + abort: (): void => {}, + }; + }, + }), + }, +}))); + +// 1.12.7 +test('Regenerate should NOT be visible after all-execution failure', async (t) => { + const dataGrid = new DataGrid(GRID_SELECTOR); + + await t.expect(dataGrid.isReady()).ok(); + + await t.click(dataGrid.getAIAssistantButton()); + + const aiChat = dataGrid.getAIAssistantChat(); + const chat = aiChat.getChat(); + + await t + .typeText(chat.getInput(), 'Sort by unknown') + .pressKey('enter'); + + await t.expect(aiChat.getMessages().count).gte(1); + await t.expect(aiChat.getErrorActionItems(0).count).gte(1); + + await t.expect(aiChat.getMessageRegenerateButton(0).exists).notOk(); +}).before(async () => createWidget('dxDataGrid', () => ({ + dataSource: [ + { id: 1, name: 'Alice', value: 30 }, + { id: 2, name: 'Bob', value: 20 }, + { id: 3, name: 'Charlie', value: 10 }, + ], + keyExpr: 'id', + columns: ['id', 'name', 'value'], + showBorders: true, + aiAssistant: { + enabled: true, + aiIntegration: new (window as any).DevExpress.aiIntegration.AIIntegration({ + sendRequest() { + return { + promise: Promise.resolve({ + actions: [ + { name: 'sorting', args: { dataField: 'nonExistent1', sortOrder: 'asc' } }, + { name: 'sorting', args: { dataField: 'nonExistent2', sortOrder: 'desc' } }, + ], + }), + abort: (): void => {}, + }; + }, + }), + }, +}))); + +// 1.12.8 +test('Regenerate should resend the same prompt and replace the previous response', async (t) => { + const dataGrid = new DataGrid(GRID_SELECTOR); + + await t.expect(dataGrid.isReady()).ok(); + + await t.click(dataGrid.getAIAssistantButton()); + + const aiChat = dataGrid.getAIAssistantChat(); + const chat = aiChat.getChat(); + + await t + .typeText(chat.getInput(), 'Sort by name') + .pressKey('enter'); + + await t.expect(aiChat.getErrorMessages().count).eql(1); + + await t.expect(aiChat.getMessageRegenerateButton(0).exists).ok(); + + await t.click(aiChat.getMessageRegenerateButton(0)); + + await t.expect(aiChat.getSuccessMessages().count).eql(1); + + await t.expect(aiChat.getMessages().count).eql(1); +}).before(async () => createWidget('dxDataGrid', () => { + (window as any).__callCount = 0; + + return { + 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() { + const count = (window as any).__callCount; + (window as any).__callCount = count + 1; + + if (count === 0) { + return { + promise: Promise.reject(new Error('AI service error')), + abort: (): void => {}, + }; + } + + return { + promise: Promise.resolve({ + actions: [{ name: 'sorting', args: { dataField: 'name', sortOrder: 'asc' } }], + }), + abort: (): void => {}, + }; + }, + }), + }, + }; +})); + +// 1.12.9 +test('Regenerate should be disabled while request is in flight', async (t) => { + const dataGrid = new DataGrid(GRID_SELECTOR); + + await t.expect(dataGrid.isReady()).ok(); + + await t.click(dataGrid.getAIAssistantButton()); + + const aiChat = dataGrid.getAIAssistantChat(); + const chat = aiChat.getChat(); + + await t + .typeText(chat.getInput(), 'Sort by name') + .pressKey('enter'); + + await t.expect(aiChat.getErrorMessages().count).eql(1); + + await t.click(aiChat.getMessageRegenerateButton(0)); + + await t.expect(aiChat.getPendingMessages().count).eql(1); + + await t.expect(aiChat.getMessageRegenerateButton(0).exists).notOk(); +}).before(async () => createWidget('dxDataGrid', () => { + (window as any).__callCount = 0; + + return { + 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() { + const count = (window as any).__callCount; + (window as any).__callCount = count + 1; + + if (count === 0) { + return { + promise: Promise.reject(new Error('AI service error')), + abort: (): void => {}, + }; + } + + return { + promise: new Promise(() => {}), + abort: (): void => {}, + }; + }, + }), + }, + }; +})); diff --git a/packages/testcafe-models/dataGrid/aiAssistantChat.ts b/packages/testcafe-models/dataGrid/aiAssistantChat.ts index fe9d3621ac62..de50ab354837 100644 --- a/packages/testcafe-models/dataGrid/aiAssistantChat.ts +++ b/packages/testcafe-models/dataGrid/aiAssistantChat.ts @@ -1,4 +1,3 @@ -import { Selector } from 'testcafe'; import Popup from '../popup'; import Button from '../button'; import Chat from '../chat'; @@ -26,6 +25,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 { @@ -104,4 +105,12 @@ 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}`); + } + + getTitle(): Selector { + return this.topToolbar; + } } From 44b936258ce4afc651e0e2813ece0580111baf74 Mon Sep 17 00:00:00 2001 From: Alyar <> Date: Wed, 27 May 2026 11:19:14 +0400 Subject: [PATCH 2/2] DataGrid - AI Assistant: add negative cases e2e tests --- .../aiAssistant/negativeCases.functional.ts | 291 ++++++++++++++++++ 1 file changed, 291 insertions(+) create mode 100644 e2e/testcafe-devextreme/tests/dataGrid/common/aiAssistant/negativeCases.functional.ts diff --git a/e2e/testcafe-devextreme/tests/dataGrid/common/aiAssistant/negativeCases.functional.ts b/e2e/testcafe-devextreme/tests/dataGrid/common/aiAssistant/negativeCases.functional.ts new file mode 100644 index 000000000000..59b982cc85f0 --- /dev/null +++ b/e2e/testcafe-devextreme/tests/dataGrid/common/aiAssistant/negativeCases.functional.ts @@ -0,0 +1,291 @@ +/* eslint-disable @stylistic/newline-per-chained-call */ +import DataGrid from 'devextreme-testcafe-models/dataGrid'; +import { createWidget } from '../../../../helpers/createWidget'; +import url from '../../../../helpers/getPageUrl'; + +const GRID_SELECTOR = '#container'; +const AI_INTEGRATION_PAGE = url(__dirname, '../../../container-ai-integration.html'); + +// === §2.1 Unsupported / unknown intent === + +fixture.disablePageReloads`AI Assistant - Empty Actions` + .page(AI_INTEGRATION_PAGE); + +// 2.1.3 +test('Empty actions array should show no-action message and leave grid unchanged', async (t) => { + const dataGrid = new DataGrid(GRID_SELECTOR); + + await t.expect(dataGrid.isReady()).ok(); + + await t.click(dataGrid.getAIAssistantButton()); + + const aiChat = dataGrid.getAIAssistantChat(); + const chat = aiChat.getChat(); + + await t + .typeText(chat.getInput(), 'Tell me a joke') + .pressKey('enter'); + + await t.expect(aiChat.getMessages().count).eql(1); + + await t.expect(aiChat.getSuccessMessages().count).eql(0); + await t.expect(aiChat.getActionItems(0).count).eql(0); + + await t + .expect(dataGrid.getDataCell(0, 1).element.textContent).eql('Alice') + .expect(dataGrid.getDataCell(1, 1).element.textContent).eql('Bob') + .expect(dataGrid.getDataCell(2, 1).element.textContent).eql('Charlie'); +}).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: [] }), + abort: (): void => {}, + }; + }, + }), + }, +}))); + +// === §2.3 Unknown field === + +fixture.disablePageReloads`AI Assistant - Unknown Field` + .page(AI_INTEGRATION_PAGE); + +// 2.3.2 +test('Sorting by non-existent dataField should show failure message and leave grid unchanged', async (t) => { + const dataGrid = new DataGrid(GRID_SELECTOR); + + await t.expect(dataGrid.isReady()).ok(); + + await t.click(dataGrid.getAIAssistantButton()); + + const aiChat = dataGrid.getAIAssistantChat(); + const chat = aiChat.getChat(); + + await t + .typeText(chat.getInput(), 'Sort by Salary') + .pressKey('enter'); + + await t.expect(aiChat.getMessages().count).gte(1); + await t.expect(aiChat.getErrorActionItems(0).count).eql(1); + + await t + .expect(dataGrid.getDataCell(0, 1).element.textContent).eql('Alice') + .expect(dataGrid.getDataCell(1, 1).element.textContent).eql('Bob') + .expect(dataGrid.getDataCell(2, 1).element.textContent).eql('Charlie'); +}).before(async () => createWidget('dxDataGrid', () => ({ + dataSource: [ + { id: 1, name: 'Alice', value: 30 }, + { id: 2, name: 'Bob', value: 20 }, + { id: 3, name: 'Charlie', value: 10 }, + ], + keyExpr: 'id', + columns: ['id', 'name', 'value'], + showBorders: true, + aiAssistant: { + enabled: true, + aiIntegration: new (window as any).DevExpress.aiIntegration.AIIntegration({ + sendRequest() { + return { + promise: Promise.resolve({ + actions: [{ name: 'sorting', args: { dataField: 'Salary', sortOrder: 'asc' } }], + }), + abort: (): void => {}, + }; + }, + }), + }, +}))); + +// === §2.4 Request impossible in current state === + +fixture.disablePageReloads`AI Assistant - Impossible State` + .page(AI_INTEGRATION_PAGE); + +// 2.4.1 +test('Page index out of range should show failure status', async (t) => { + const dataGrid = new DataGrid(GRID_SELECTOR); + + await t.expect(dataGrid.isReady()).ok(); + + await t.click(dataGrid.getAIAssistantButton()); + + const aiChat = dataGrid.getAIAssistantChat(); + const chat = aiChat.getChat(); + + await t + .typeText(chat.getInput(), 'Go to page 10000') + .pressKey('enter'); + + await t.expect(aiChat.getMessages().count).gte(1); + await t.expect(aiChat.getActionItems(0).count).eql(1); +}).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, + paging: { pageSize: 2 }, + aiAssistant: { + enabled: true, + aiIntegration: new (window as any).DevExpress.aiIntegration.AIIntegration({ + sendRequest() { + return { + promise: Promise.resolve({ + actions: [{ name: 'pageIndex', args: { pageIndex: 9999 } }], + }), + abort: (): void => {}, + }; + }, + }), + }, +}))); + +// 2.4.2 +test('Grouping by non-groupable column should show failure entry', async (t) => { + const dataGrid = new DataGrid(GRID_SELECTOR); + + await t.expect(dataGrid.isReady()).ok(); + + await t.click(dataGrid.getAIAssistantButton()); + + const aiChat = dataGrid.getAIAssistantChat(); + const chat = aiChat.getChat(); + + await t + .typeText(chat.getInput(), 'Group by name') + .pressKey('enter'); + + await t.expect(aiChat.getMessages().count).gte(1); + await t.expect(aiChat.getErrorActionItems(0).count).eql(1); +}).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: [ + { dataField: 'id' }, + { dataField: 'name', allowGrouping: false }, + { dataField: 'value' }, + ], + showBorders: true, + aiAssistant: { + enabled: true, + aiIntegration: new (window as any).DevExpress.aiIntegration.AIIntegration({ + sendRequest() { + return { + promise: Promise.resolve({ + actions: [{ name: 'grouping', args: { dataField: 'name', groupIndex: 0 } }], + }), + abort: (): void => {}, + }; + }, + }), + }, +}))); + +// 2.4.3 +test('Selecting non-existent keys should show failure or no incorrect rows selected', async (t) => { + const dataGrid = new DataGrid(GRID_SELECTOR); + + await t.expect(dataGrid.isReady()).ok(); + + await t.click(dataGrid.getAIAssistantButton()); + + const aiChat = dataGrid.getAIAssistantChat(); + const chat = aiChat.getChat(); + + await t + .typeText(chat.getInput(), 'Select rows 999 and 1000') + .pressKey('enter'); + + await t.expect(aiChat.getMessages().count).gte(1); + await t.expect(aiChat.getActionItems(0).count).eql(1); +}).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, + selection: { mode: 'multiple' }, + aiAssistant: { + enabled: true, + aiIntegration: new (window as any).DevExpress.aiIntegration.AIIntegration({ + sendRequest() { + return { + promise: Promise.resolve({ + actions: [{ name: 'selectByKeys', args: { keys: [999, 1000], preserve: false } }], + }), + abort: (): void => {}, + }; + }, + }), + }, +}))); + +// === §2.6 Excessively long prompt / provider error === + +fixture.disablePageReloads`AI Assistant - Provider Error` + .page(AI_INTEGRATION_PAGE); + +// 2.6.2 +test('sendRequest rejection should show error message and leave grid unchanged', async (t) => { + const dataGrid = new DataGrid(GRID_SELECTOR); + + await t.expect(dataGrid.isReady()).ok(); + + await t.click(dataGrid.getAIAssistantButton()); + + const aiChat = dataGrid.getAIAssistantChat(); + const chat = aiChat.getChat(); + + await t + .typeText(chat.getInput(), 'Sort by name') + .pressKey('enter'); + + await t.expect(aiChat.getErrorMessages().count).eql(1); + + await t + .expect(dataGrid.getDataCell(0, 1).element.textContent).eql('Alice') + .expect(dataGrid.getDataCell(1, 1).element.textContent).eql('Bob') + .expect(dataGrid.getDataCell(2, 1).element.textContent).eql('Charlie'); +}).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.reject(new Error('Request payload too large')), + abort: (): void => {}, + }; + }, + }), + }, +})));