From 3e0e74cd10dffaa20eff9e22527297053a671ae8 Mon Sep 17 00:00:00 2001 From: Alyar <> Date: Wed, 3 Jun 2026 02:30:49 +0400 Subject: [PATCH 1/8] DataGrid - AI Assistant: regenerate e2e tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit the Regenerate button — visibility after pre-execution failures, absence after command execution, resend-and-replace behavior and in-flight disabling (plan §1.12, §3.8.2) Co-Authored-By: Claude Opus 4.8 --- .../aiAssistant/regenerate.functional.ts | 507 ++++++++++++++++++ 1 file changed, 507 insertions(+) create mode 100644 e2e/testcafe-devextreme/tests/dataGrid/common/aiAssistant/regenerate.functional.ts diff --git a/e2e/testcafe-devextreme/tests/dataGrid/common/aiAssistant/regenerate.functional.ts b/e2e/testcafe-devextreme/tests/dataGrid/common/aiAssistant/regenerate.functional.ts new file mode 100644 index 000000000000..bd1136d983d8 --- /dev/null +++ b/e2e/testcafe-devextreme/tests/dataGrid/common/aiAssistant/regenerate.functional.ts @@ -0,0 +1,507 @@ +/* eslint-disable no-underscore-dangle */ +import DataGrid from 'devextreme-testcafe-models/dataGrid'; +import { createWidget } from '../../../../helpers/createWidget'; +import { AI_INTEGRATION_PAGE, GRID_SELECTOR } from './testHelpers'; + +// === §1.12 Regenerate button === + +fixture.disablePageReloads`AI Assistant - Regenerate` + .page(AI_INTEGRATION_PAGE); + +// 1.12.1 +test('Regenerate should be visible after AI integration 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.getMessageRegenerateButton(0).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: Promise.reject(new Error('HTTP 500 Internal Server Error')), + abort: (): void => {}, + }; + }, + }), + }, +}))); + +// 1.12.2 +test('Regenerate should be visible after response format 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.getMessageRegenerateButton(0).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: Promise.resolve({}), + abort: (): void => {}, + }; + }, + }), + }, +}))); + +// 1.12.3 +test('Regenerate should be visible after validation 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.getMessageRegenerateButton(0).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: Promise.resolve({ + actions: [{ name: 'unknownCommand', args: { foo: 'bar' } }], + }), + abort: (): void => {}, + }; + }, + }), + }, +}))); + +// 1.12.4 +test('Regenerate should be visible after empty actions', 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.getAIMessages().count).eql(1); + await t.expect(aiChat.getMessageRegenerateButton(0).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: Promise.resolve({ actions: [] }), + abort: (): void => {}, + }; + }, + }), + }, +}))); + +// 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(); + + await t + .typeText(aiChat.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(); + + await t + .typeText(aiChat.getInput(), 'Sort and filter') + .pressKey('enter'); + + await t.expect(aiChat.getAIMessages().count).eql(1); + await t.expect(aiChat.getActionItems(0).count).eql(2); + await t.expect(aiChat.getSuccessActionItems(0).count).eql(1); + await t.expect(aiChat.getErrorActionItems(0).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' } }, + { 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(); + + await t + .typeText(aiChat.getInput(), 'Sort by unknown') + .pressKey('enter'); + + await t.expect(aiChat.getAIMessages().count).eql(1); + await t.expect(aiChat.getErrorActionItems(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: '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(); + + await t + .typeText(aiChat.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.getAIMessages().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(); + + await t + .typeText(aiChat.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 => {}, + }; + }, + }), + }, + }; +})); + +// 1.12.11 +test('Regenerate is visible after a popup-close-driven abort', 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'); + + // The request never resolves — it is in flight when the popup is closed. + await t.expect(aiChat.getPendingMessages().count).eql(1); + + await t.click(aiChat.getCloseButton().element); + + await t.expect(aiChat.getAbortConfirmDialog().exists).ok(); + + await t.click(aiChat.getAbortConfirmYesButton()); + await t.click(dataGrid.getAIAssistantButton()); + + // The aborted response is rendered as a failure with no executed commands, + // so it currently offers Regenerate. + await t.expect(aiChat.getErrorMessages().count).eql(1); + await t.expect(aiChat.getMessageRegenerateButton(0).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 => {}, + }; + }, + }), + }, +}))); +// 3.8.2 +test('Sequential regenerate after pre-execution failures keeps exactly one response', 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.getMessages().count).eql(2); + 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.getMessages().count).eql(2); + 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.getMessages().count).eql(2); + await t.expect(aiChat.getErrorMessages().count).eql(1); + await t.expect(aiChat.getMessageRegenerateButton(0).exists).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: Promise.reject(new Error('AI service error')), + abort: (): void => {}, + }; + }, + }), + }, +}))); From efd96209a9613ce0d9e7e80a84df00921f4a7d82 Mon Sep 17 00:00:00 2001 From: Alyar <> Date: Fri, 5 Jun 2026 04:40:47 +0400 Subject: [PATCH 2/8] DataGrid - AI Assistant: verify grid state in regenerate e2e tests Add grid-state assertions (apiColumnOption) to every regenerate test so each case confirms whether the command applied or not, and verify 1.12.8 resends the same prompt with a freshly-built context. Switch 1.12.4 to getErrorMessages for consistency. Move the mock factory into the test file and drop POM/testHelpers members unused by this PR. Co-Authored-By: Claude Opus 4.8 --- .../aiAssistant/regenerate.functional.ts | 471 ++++++++---------- 1 file changed, 197 insertions(+), 274 deletions(-) diff --git a/e2e/testcafe-devextreme/tests/dataGrid/common/aiAssistant/regenerate.functional.ts b/e2e/testcafe-devextreme/tests/dataGrid/common/aiAssistant/regenerate.functional.ts index bd1136d983d8..6a9c0d06c905 100644 --- a/e2e/testcafe-devextreme/tests/dataGrid/common/aiAssistant/regenerate.functional.ts +++ b/e2e/testcafe-devextreme/tests/dataGrid/common/aiAssistant/regenerate.functional.ts @@ -1,8 +1,92 @@ /* eslint-disable no-underscore-dangle */ +import { ClientFunction } from 'testcafe'; import DataGrid from 'devextreme-testcafe-models/dataGrid'; import { createWidget } from '../../../../helpers/createWidget'; import { AI_INTEGRATION_PAGE, GRID_SELECTOR } from './testHelpers'; +const threeRows = [ + { id: 1, name: 'Alice', value: 30 }, + { id: 2, name: 'Bob', value: 20 }, + { id: 3, name: 'Charlie', value: 10 }, +]; + +const twoRows = [ + { id: 1, name: 'Alice', value: 30 }, + { id: 2, name: 'Bob', value: 20 }, +]; + +// A mocked AI response is described declaratively: +// { resolve: } → sendRequest resolves with +// { reject: } → sendRequest rejects with new Error() +// { hang: true } → sendRequest never settles (request stays in flight) +type AIResponse = { resolve: unknown } | { reject: string } | { hang: true }; + +const setupAIState = ClientFunction((base: Record, responses: AIResponse[]) => { + (window as any).__aiBase = base; + (window as any).__aiResponses = responses; + (window as any).__aiCallCount = 0; + (window as any).__aiRequests = []; +}); + +const aiGridOptions = (): any => ({ + ...(window as any).__aiBase, + aiAssistant: { + enabled: true, + aiIntegration: new (window as any).DevExpress.aiIntegration.AIIntegration({ + sendRequest(params: any) { + const responses = (window as any).__aiResponses; + const count = (window as any).__aiCallCount; + + (window as any).__aiCallCount = count + 1; + (window as any).__aiRequests.push(params); + + const response = count < responses.length + ? responses[count] + : responses[responses.length - 1]; + + if ('reject' in response) { + return { + promise: Promise.reject(new Error(response.reject)), + abort: (): void => {}, + }; + } + + if ('hang' in response) { + return { + promise: new Promise(() => {}), + abort: (): void => {}, + }; + } + + return { + promise: Promise.resolve(response.resolve), + abort: (): void => {}, + }; + }, + }), + }, +}); + +const createGridWithAIAssistant = async ( + base: Record, + responses: AIResponse[], +): Promise => { + await setupAIState(base, responses); + + return createWidget('dxDataGrid', aiGridOptions); +}; + +const getAIRequests = ClientFunction(() => ((window as any).__aiRequests ?? []).map((r: any) => ({ + text: r.data.text, + columns: (r.data.context.columns ?? []).map((c: any) => c.dataField), +}))); + +const gridBase = { + keyExpr: 'id', + columns: ['id', 'name', 'value'], + showBorders: true, +}; + // === §1.12 Regenerate button === fixture.disablePageReloads`AI Assistant - Regenerate` @@ -24,27 +108,13 @@ test('Regenerate should be visible after AI integration failure', async (t) => { await t.expect(aiChat.getErrorMessages().count).eql(1); await t.expect(aiChat.getMessageRegenerateButton(0).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: Promise.reject(new Error('HTTP 500 Internal Server Error')), - abort: (): void => {}, - }; - }, - }), - }, -}))); + + // Pre-execution failure: nothing was applied to the grid. + await t.expect(await dataGrid.apiColumnOption('name', 'sortOrder')).notOk(); +}).before(async () => createGridWithAIAssistant( + { dataSource: threeRows, ...gridBase }, + [{ reject: 'HTTP 500 Internal Server Error' }], +)); // 1.12.2 test('Regenerate should be visible after response format failure', async (t) => { @@ -62,27 +132,12 @@ test('Regenerate should be visible after response format failure', async (t) => await t.expect(aiChat.getErrorMessages().count).eql(1); await t.expect(aiChat.getMessageRegenerateButton(0).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: Promise.resolve({}), - abort: (): void => {}, - }; - }, - }), - }, -}))); + + await t.expect(await dataGrid.apiColumnOption('name', 'sortOrder')).notOk(); +}).before(async () => createGridWithAIAssistant( + { dataSource: threeRows, ...gridBase }, + [{ resolve: {} }], +)); // 1.12.3 test('Regenerate should be visible after validation failure', async (t) => { @@ -100,29 +155,12 @@ test('Regenerate should be visible after validation failure', async (t) => { await t.expect(aiChat.getErrorMessages().count).eql(1); await t.expect(aiChat.getMessageRegenerateButton(0).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: Promise.resolve({ - actions: [{ name: 'unknownCommand', args: { foo: 'bar' } }], - }), - abort: (): void => {}, - }; - }, - }), - }, -}))); + + await t.expect(await dataGrid.apiColumnOption('name', 'sortOrder')).notOk(); +}).before(async () => createGridWithAIAssistant( + { dataSource: threeRows, ...gridBase }, + [{ resolve: { actions: [{ name: 'unknownCommand', args: { foo: 'bar' } }] } }], +)); // 1.12.4 test('Regenerate should be visible after empty actions', async (t) => { @@ -138,29 +176,15 @@ test('Regenerate should be visible after empty actions', async (t) => { .typeText(aiChat.getInput(), 'Sort by name') .pressKey('enter'); - await t.expect(aiChat.getAIMessages().count).eql(1); + // Empty actions are rejected as an invalid response → failure message. + await t.expect(aiChat.getErrorMessages().count).eql(1); await t.expect(aiChat.getMessageRegenerateButton(0).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: Promise.resolve({ actions: [] }), - abort: (): void => {}, - }; - }, - }), - }, -}))); + + await t.expect(await dataGrid.apiColumnOption('name', 'sortOrder')).notOk(); +}).before(async () => createGridWithAIAssistant( + { dataSource: threeRows, ...gridBase }, + [{ resolve: { actions: [] } }], +)); // 1.12.5 test('Regenerate should NOT be visible after full success', async (t) => { @@ -178,29 +202,13 @@ test('Regenerate should NOT be visible after full success', async (t) => { 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 => {}, - }; - }, - }), - }, -}))); + + // The successful command actually changed the grid state. + await t.expect(await dataGrid.apiColumnOption('name', 'sortOrder')).eql('asc'); +}).before(async () => createGridWithAIAssistant( + { dataSource: threeRows, ...gridBase }, + [{ resolve: { actions: [{ name: 'sorting', args: { dataField: 'name', sortOrder: 'asc' } }] } }], +)); // 1.12.6 test('Regenerate should NOT be visible after partial-execution failure', async (t) => { @@ -221,32 +229,20 @@ test('Regenerate should NOT be visible after partial-execution failure', async ( await t.expect(aiChat.getSuccessActionItems(0).count).eql(1); await t.expect(aiChat.getErrorActionItems(0).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' } }, - { name: 'sorting', args: { dataField: 'nonExistent', sortOrder: 'asc' } }, - ], - }), - abort: (): void => {}, - }; - }, - }), - }, -}))); + + // No Regenerate because action #1 already mutated the grid. + await t.expect(await dataGrid.apiColumnOption('name', 'sortOrder')).eql('asc'); +}).before(async () => createGridWithAIAssistant( + { dataSource: threeRows, ...gridBase }, + [{ + resolve: { + actions: [ + { name: 'sorting', args: { dataField: 'name', sortOrder: 'asc' } }, + { name: 'sorting', args: { dataField: 'nonExistent', sortOrder: 'asc' } }, + ], + }, + }], +)); // 1.12.7 test('Regenerate should NOT be visible after all-execution failure', async (t) => { @@ -265,32 +261,20 @@ test('Regenerate should NOT be visible after all-execution failure', async (t) = await t.expect(aiChat.getAIMessages().count).eql(1); await t.expect(aiChat.getErrorActionItems(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: 'nonExistent1', sortOrder: 'asc' } }, - { name: 'sorting', args: { dataField: 'nonExistent2', sortOrder: 'desc' } }, - ], - }), - abort: (): void => {}, - }; - }, - }), - }, -}))); + + // Both commands targeted non-existent columns, so real columns stay unsorted. + await t.expect(await dataGrid.apiColumnOption('name', 'sortOrder')).notOk(); +}).before(async () => createGridWithAIAssistant( + { dataSource: threeRows, ...gridBase }, + [{ + resolve: { + actions: [ + { name: 'sorting', args: { dataField: 'nonExistent1', sortOrder: 'asc' } }, + { name: 'sorting', args: { dataField: 'nonExistent2', sortOrder: 'desc' } }, + ], + }, + }], +)); // 1.12.8 test('Regenerate should resend the same prompt and replace the previous response', async (t) => { @@ -311,45 +295,26 @@ test('Regenerate should resend the same prompt and replace the previous response await t.click(aiChat.getMessageRegenerateButton(0)); + // The failed response is replaced, not accumulated: still a single AI response. await t.expect(aiChat.getSuccessMessages().count).eql(1); await t.expect(aiChat.getAIMessages().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 => {}, - }; - }, - }), - }, - }; -})); + // The regenerated command applied to the grid. + await t.expect(await dataGrid.apiColumnOption('name', 'sortOrder')).eql('asc'); + + // The same prompt was resent with a freshly-built (current) grid context. + const requests = await getAIRequests(); + await t.expect(requests.length).eql(2); + await t.expect(requests[1].text).eql(requests[0].text); + await t.expect(requests[1].text).eql('Sort by name'); + await t.expect(requests[1].columns).eql(['id', 'name', 'value']); +}).before(async () => createGridWithAIAssistant( + { dataSource: threeRows, ...gridBase }, + [ + { reject: 'AI service error' }, + { resolve: { actions: [{ name: 'sorting', args: { dataField: 'name', sortOrder: 'asc' } }] } }, + ], +)); // 1.12.9 test('Regenerate should be disabled while request is in flight', async (t) => { @@ -371,41 +336,16 @@ test('Regenerate should be disabled while request is in flight', async (t) => { 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 => {}, - }; - }, - }), - }, - }; -})); + // Nothing was applied while the regenerate request is still pending. + await t.expect(await dataGrid.apiColumnOption('name', 'sortOrder')).notOk(); +}).before(async () => createGridWithAIAssistant( + { dataSource: threeRows, ...gridBase }, + [ + { reject: 'AI service error' }, + { hang: true }, + ], +)); // 1.12.11 test('Regenerate is visible after a popup-close-driven abort', async (t) => { @@ -432,30 +372,21 @@ test('Regenerate is visible after a popup-close-driven abort', async (t) => { await t.click(dataGrid.getAIAssistantButton()); // The aborted response is rendered as a failure with no executed commands, - // so it currently offers Regenerate. + // so it currently offers Regenerate (pins current behavior; see doc §1.12.11). await t.expect(aiChat.getErrorMessages().count).eql(1); await t.expect(aiChat.getMessageRegenerateButton(0).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 => {}, - }; - }, - }), - }, -}))); + + await t.expect(await dataGrid.apiColumnOption('name', 'sortOrder')).notOk(); +}).before(async () => createGridWithAIAssistant( + { dataSource: threeRows, ...gridBase }, + [{ hang: true }], +)); + +// === §3.8.2 Sequential resends after pre-execution failures === + +fixture.disablePageReloads`AI Assistant - Regenerate (sequential)` + .page(AI_INTEGRATION_PAGE); + // 3.8.2 test('Sequential regenerate after pre-execution failures keeps exactly one response', async (t) => { const dataGrid = new DataGrid(GRID_SELECTOR); @@ -485,23 +416,15 @@ test('Sequential regenerate after pre-execution failures keeps exactly one respo await t.expect(aiChat.getMessages().count).eql(2); await t.expect(aiChat.getErrorMessages().count).eql(1); await t.expect(aiChat.getMessageRegenerateButton(0).exists).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: Promise.reject(new Error('AI service error')), - abort: (): void => {}, - }; - }, - }), - }, -}))); + + // Every retry failed before execution, so the grid was never mutated. + await t.expect(await dataGrid.apiColumnOption('name', 'sortOrder')).notOk(); + + // Each Regenerate dispatched a fresh request with the same prompt. + const requests = await getAIRequests(); + await t.expect(requests.length).eql(3); + await t.expect(requests[2].text).eql('Sort by name'); +}).before(async () => createGridWithAIAssistant( + { dataSource: twoRows, ...gridBase }, + [{ reject: 'AI service error' }], +)); From 3c033c3f7e6416aa20d7273e93caf200bb5b6ad7 Mon Sep 17 00:00:00 2001 From: Alyar <> Date: Fri, 5 Jun 2026 12:44:57 +0400 Subject: [PATCH 3/8] Fix tests --- .../dataGrid/common/aiAssistant/regenerate.functional.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/e2e/testcafe-devextreme/tests/dataGrid/common/aiAssistant/regenerate.functional.ts b/e2e/testcafe-devextreme/tests/dataGrid/common/aiAssistant/regenerate.functional.ts index 6a9c0d06c905..4f9ba61de6cc 100644 --- a/e2e/testcafe-devextreme/tests/dataGrid/common/aiAssistant/regenerate.functional.ts +++ b/e2e/testcafe-devextreme/tests/dataGrid/common/aiAssistant/regenerate.functional.ts @@ -89,7 +89,7 @@ const gridBase = { // === §1.12 Regenerate button === -fixture.disablePageReloads`AI Assistant - Regenerate` +fixture`AI Assistant - Regenerate` .page(AI_INTEGRATION_PAGE); // 1.12.1 @@ -384,7 +384,7 @@ test('Regenerate is visible after a popup-close-driven abort', async (t) => { // === §3.8.2 Sequential resends after pre-execution failures === -fixture.disablePageReloads`AI Assistant - Regenerate (sequential)` +fixture`AI Assistant - Regenerate (sequential)` .page(AI_INTEGRATION_PAGE); // 3.8.2 From 663a712ff80fc77612c5917eb4cb810094f69387 Mon Sep 17 00:00:00 2001 From: Alyar <> Date: Mon, 8 Jun 2026 06:39:05 +0400 Subject: [PATCH 4/8] DataGrid - AI Assistant: use shared AI mock setup from testHelpers Replace the per-file tagged response mock with the shared queue helper and HANG/FAIL sentinels. --- .../aiAssistant/regenerate.functional.ts | 154 +++++------------- 1 file changed, 40 insertions(+), 114 deletions(-) diff --git a/e2e/testcafe-devextreme/tests/dataGrid/common/aiAssistant/regenerate.functional.ts b/e2e/testcafe-devextreme/tests/dataGrid/common/aiAssistant/regenerate.functional.ts index 4f9ba61de6cc..cc885364dd24 100644 --- a/e2e/testcafe-devextreme/tests/dataGrid/common/aiAssistant/regenerate.functional.ts +++ b/e2e/testcafe-devextreme/tests/dataGrid/common/aiAssistant/regenerate.functional.ts @@ -1,92 +1,22 @@ /* eslint-disable no-underscore-dangle */ import { ClientFunction } from 'testcafe'; import DataGrid from 'devextreme-testcafe-models/dataGrid'; -import { createWidget } from '../../../../helpers/createWidget'; -import { AI_INTEGRATION_PAGE, GRID_SELECTOR } from './testHelpers'; - -const threeRows = [ - { id: 1, name: 'Alice', value: 30 }, - { id: 2, name: 'Bob', value: 20 }, - { id: 3, name: 'Charlie', value: 10 }, -]; - -const twoRows = [ - { id: 1, name: 'Alice', value: 30 }, - { id: 2, name: 'Bob', value: 20 }, -]; - -// A mocked AI response is described declaratively: -// { resolve: } → sendRequest resolves with -// { reject: } → sendRequest rejects with new Error() -// { hang: true } → sendRequest never settles (request stays in flight) -type AIResponse = { resolve: unknown } | { reject: string } | { hang: true }; - -const setupAIState = ClientFunction((base: Record, responses: AIResponse[]) => { - (window as any).__aiBase = base; - (window as any).__aiResponses = responses; - (window as any).__aiCallCount = 0; - (window as any).__aiRequests = []; -}); - -const aiGridOptions = (): any => ({ - ...(window as any).__aiBase, - aiAssistant: { - enabled: true, - aiIntegration: new (window as any).DevExpress.aiIntegration.AIIntegration({ - sendRequest(params: any) { - const responses = (window as any).__aiResponses; - const count = (window as any).__aiCallCount; - - (window as any).__aiCallCount = count + 1; - (window as any).__aiRequests.push(params); - - const response = count < responses.length - ? responses[count] - : responses[responses.length - 1]; - - if ('reject' in response) { - return { - promise: Promise.reject(new Error(response.reject)), - abort: (): void => {}, - }; - } - - if ('hang' in response) { - return { - promise: new Promise(() => {}), - abort: (): void => {}, - }; - } - - return { - promise: Promise.resolve(response.resolve), - abort: (): void => {}, - }; - }, - }), - }, -}); - -const createGridWithAIAssistant = async ( - base: Record, - responses: AIResponse[], -): Promise => { - await setupAIState(base, responses); - - return createWidget('dxDataGrid', aiGridOptions); -}; +import { + AI_INTEGRATION_PAGE, + FAIL, + GRID_SELECTOR, + HANG, + baseGrid, + createGridWithAIAssistant, + threeRows, + twoRows, +} from './testHelpers'; const getAIRequests = ClientFunction(() => ((window as any).__aiRequests ?? []).map((r: any) => ({ text: r.data.text, columns: (r.data.context.columns ?? []).map((c: any) => c.dataField), }))); -const gridBase = { - keyExpr: 'id', - columns: ['id', 'name', 'value'], - showBorders: true, -}; - // === §1.12 Regenerate button === fixture`AI Assistant - Regenerate` @@ -112,8 +42,8 @@ test('Regenerate should be visible after AI integration failure', async (t) => { // Pre-execution failure: nothing was applied to the grid. await t.expect(await dataGrid.apiColumnOption('name', 'sortOrder')).notOk(); }).before(async () => createGridWithAIAssistant( - { dataSource: threeRows, ...gridBase }, - [{ reject: 'HTTP 500 Internal Server Error' }], + { dataSource: threeRows, ...baseGrid }, + [FAIL], )); // 1.12.2 @@ -135,8 +65,8 @@ test('Regenerate should be visible after response format failure', async (t) => await t.expect(await dataGrid.apiColumnOption('name', 'sortOrder')).notOk(); }).before(async () => createGridWithAIAssistant( - { dataSource: threeRows, ...gridBase }, - [{ resolve: {} }], + { dataSource: threeRows, ...baseGrid }, + [{}], )); // 1.12.3 @@ -158,8 +88,8 @@ test('Regenerate should be visible after validation failure', async (t) => { await t.expect(await dataGrid.apiColumnOption('name', 'sortOrder')).notOk(); }).before(async () => createGridWithAIAssistant( - { dataSource: threeRows, ...gridBase }, - [{ resolve: { actions: [{ name: 'unknownCommand', args: { foo: 'bar' } }] } }], + { dataSource: threeRows, ...baseGrid }, + [{ actions: [{ name: 'unknownCommand', args: { foo: 'bar' } }] }], )); // 1.12.4 @@ -182,8 +112,8 @@ test('Regenerate should be visible after empty actions', async (t) => { await t.expect(await dataGrid.apiColumnOption('name', 'sortOrder')).notOk(); }).before(async () => createGridWithAIAssistant( - { dataSource: threeRows, ...gridBase }, - [{ resolve: { actions: [] } }], + { dataSource: threeRows, ...baseGrid }, + [{ actions: [] }], )); // 1.12.5 @@ -206,8 +136,8 @@ test('Regenerate should NOT be visible after full success', async (t) => { // The successful command actually changed the grid state. await t.expect(await dataGrid.apiColumnOption('name', 'sortOrder')).eql('asc'); }).before(async () => createGridWithAIAssistant( - { dataSource: threeRows, ...gridBase }, - [{ resolve: { actions: [{ name: 'sorting', args: { dataField: 'name', sortOrder: 'asc' } }] } }], + { dataSource: threeRows, ...baseGrid }, + [{ actions: [{ name: 'sorting', args: { dataField: 'name', sortOrder: 'asc' } }] }], )); // 1.12.6 @@ -233,14 +163,12 @@ test('Regenerate should NOT be visible after partial-execution failure', async ( // No Regenerate because action #1 already mutated the grid. await t.expect(await dataGrid.apiColumnOption('name', 'sortOrder')).eql('asc'); }).before(async () => createGridWithAIAssistant( - { dataSource: threeRows, ...gridBase }, + { dataSource: threeRows, ...baseGrid }, [{ - resolve: { - actions: [ - { name: 'sorting', args: { dataField: 'name', sortOrder: 'asc' } }, - { name: 'sorting', args: { dataField: 'nonExistent', sortOrder: 'asc' } }, - ], - }, + actions: [ + { name: 'sorting', args: { dataField: 'name', sortOrder: 'asc' } }, + { name: 'sorting', args: { dataField: 'nonExistent', sortOrder: 'asc' } }, + ], }], )); @@ -265,14 +193,12 @@ test('Regenerate should NOT be visible after all-execution failure', async (t) = // Both commands targeted non-existent columns, so real columns stay unsorted. await t.expect(await dataGrid.apiColumnOption('name', 'sortOrder')).notOk(); }).before(async () => createGridWithAIAssistant( - { dataSource: threeRows, ...gridBase }, + { dataSource: threeRows, ...baseGrid }, [{ - resolve: { - actions: [ - { name: 'sorting', args: { dataField: 'nonExistent1', sortOrder: 'asc' } }, - { name: 'sorting', args: { dataField: 'nonExistent2', sortOrder: 'desc' } }, - ], - }, + actions: [ + { name: 'sorting', args: { dataField: 'nonExistent1', sortOrder: 'asc' } }, + { name: 'sorting', args: { dataField: 'nonExistent2', sortOrder: 'desc' } }, + ], }], )); @@ -309,10 +235,10 @@ test('Regenerate should resend the same prompt and replace the previous response await t.expect(requests[1].text).eql('Sort by name'); await t.expect(requests[1].columns).eql(['id', 'name', 'value']); }).before(async () => createGridWithAIAssistant( - { dataSource: threeRows, ...gridBase }, + { dataSource: threeRows, ...baseGrid }, [ - { reject: 'AI service error' }, - { resolve: { actions: [{ name: 'sorting', args: { dataField: 'name', sortOrder: 'asc' } }] } }, + FAIL, + { actions: [{ name: 'sorting', args: { dataField: 'name', sortOrder: 'asc' } }] }, ], )); @@ -340,10 +266,10 @@ test('Regenerate should be disabled while request is in flight', async (t) => { // Nothing was applied while the regenerate request is still pending. await t.expect(await dataGrid.apiColumnOption('name', 'sortOrder')).notOk(); }).before(async () => createGridWithAIAssistant( - { dataSource: threeRows, ...gridBase }, + { dataSource: threeRows, ...baseGrid }, [ - { reject: 'AI service error' }, - { hang: true }, + FAIL, + HANG, ], )); @@ -378,8 +304,8 @@ test('Regenerate is visible after a popup-close-driven abort', async (t) => { await t.expect(await dataGrid.apiColumnOption('name', 'sortOrder')).notOk(); }).before(async () => createGridWithAIAssistant( - { dataSource: threeRows, ...gridBase }, - [{ hang: true }], + { dataSource: threeRows, ...baseGrid }, + [HANG], )); // === §3.8.2 Sequential resends after pre-execution failures === @@ -425,6 +351,6 @@ test('Sequential regenerate after pre-execution failures keeps exactly one respo await t.expect(requests.length).eql(3); await t.expect(requests[2].text).eql('Sort by name'); }).before(async () => createGridWithAIAssistant( - { dataSource: twoRows, ...gridBase }, - [{ reject: 'AI service error' }], + { dataSource: twoRows, ...baseGrid }, + [FAIL], )); From 8a0f24a73eadb248dc7ec0971a45a903be2b07c6 Mon Sep 17 00:00:00 2001 From: Alyar <> Date: Mon, 8 Jun 2026 14:00:54 +0400 Subject: [PATCH 5/8] DataGrid - AI Assistant: merge fixtures into one per file MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Collapse the per-section fixtures into a single named fixture; keep the // === §X === markers as in-file section dividers. --- .../dataGrid/common/aiAssistant/regenerate.functional.ts | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/e2e/testcafe-devextreme/tests/dataGrid/common/aiAssistant/regenerate.functional.ts b/e2e/testcafe-devextreme/tests/dataGrid/common/aiAssistant/regenerate.functional.ts index cc885364dd24..dab149060148 100644 --- a/e2e/testcafe-devextreme/tests/dataGrid/common/aiAssistant/regenerate.functional.ts +++ b/e2e/testcafe-devextreme/tests/dataGrid/common/aiAssistant/regenerate.functional.ts @@ -17,11 +17,11 @@ const getAIRequests = ClientFunction(() => ((window as any).__aiRequests ?? []). columns: (r.data.context.columns ?? []).map((c: any) => c.dataField), }))); -// === §1.12 Regenerate button === - fixture`AI Assistant - Regenerate` .page(AI_INTEGRATION_PAGE); +// === §1.12 Regenerate button === + // 1.12.1 test('Regenerate should be visible after AI integration failure', async (t) => { const dataGrid = new DataGrid(GRID_SELECTOR); @@ -310,9 +310,6 @@ test('Regenerate is visible after a popup-close-driven abort', async (t) => { // === §3.8.2 Sequential resends after pre-execution failures === -fixture`AI Assistant - Regenerate (sequential)` - .page(AI_INTEGRATION_PAGE); - // 3.8.2 test('Sequential regenerate after pre-execution failures keeps exactly one response', async (t) => { const dataGrid = new DataGrid(GRID_SELECTOR); From efef0f237b147e4b1f1acaab743446c0160cc399 Mon Sep 17 00:00:00 2001 From: Alyar <> Date: Mon, 8 Jun 2026 14:34:03 +0400 Subject: [PATCH 6/8] DataGrid - AI Assistant: restore getWrapper, retry async asserts, explicit regenerate responses Restore scoped getWrapper; drop await inside t.expect for retry; give the sequential-regenerate test a failure response per expected call instead of relying on the unexpected-call fallback (Copilot review). --- .../aiAssistant/regenerate.functional.ts | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/e2e/testcafe-devextreme/tests/dataGrid/common/aiAssistant/regenerate.functional.ts b/e2e/testcafe-devextreme/tests/dataGrid/common/aiAssistant/regenerate.functional.ts index dab149060148..dc61e518da21 100644 --- a/e2e/testcafe-devextreme/tests/dataGrid/common/aiAssistant/regenerate.functional.ts +++ b/e2e/testcafe-devextreme/tests/dataGrid/common/aiAssistant/regenerate.functional.ts @@ -40,7 +40,7 @@ test('Regenerate should be visible after AI integration failure', async (t) => { await t.expect(aiChat.getMessageRegenerateButton(0).exists).ok(); // Pre-execution failure: nothing was applied to the grid. - await t.expect(await dataGrid.apiColumnOption('name', 'sortOrder')).notOk(); + await t.expect(dataGrid.apiColumnOption('name', 'sortOrder')).notOk(); }).before(async () => createGridWithAIAssistant( { dataSource: threeRows, ...baseGrid }, [FAIL], @@ -63,7 +63,7 @@ test('Regenerate should be visible after response format failure', async (t) => await t.expect(aiChat.getErrorMessages().count).eql(1); await t.expect(aiChat.getMessageRegenerateButton(0).exists).ok(); - await t.expect(await dataGrid.apiColumnOption('name', 'sortOrder')).notOk(); + await t.expect(dataGrid.apiColumnOption('name', 'sortOrder')).notOk(); }).before(async () => createGridWithAIAssistant( { dataSource: threeRows, ...baseGrid }, [{}], @@ -86,7 +86,7 @@ test('Regenerate should be visible after validation failure', async (t) => { await t.expect(aiChat.getErrorMessages().count).eql(1); await t.expect(aiChat.getMessageRegenerateButton(0).exists).ok(); - await t.expect(await dataGrid.apiColumnOption('name', 'sortOrder')).notOk(); + await t.expect(dataGrid.apiColumnOption('name', 'sortOrder')).notOk(); }).before(async () => createGridWithAIAssistant( { dataSource: threeRows, ...baseGrid }, [{ actions: [{ name: 'unknownCommand', args: { foo: 'bar' } }] }], @@ -110,7 +110,7 @@ test('Regenerate should be visible after empty actions', async (t) => { await t.expect(aiChat.getErrorMessages().count).eql(1); await t.expect(aiChat.getMessageRegenerateButton(0).exists).ok(); - await t.expect(await dataGrid.apiColumnOption('name', 'sortOrder')).notOk(); + await t.expect(dataGrid.apiColumnOption('name', 'sortOrder')).notOk(); }).before(async () => createGridWithAIAssistant( { dataSource: threeRows, ...baseGrid }, [{ actions: [] }], @@ -134,7 +134,7 @@ test('Regenerate should NOT be visible after full success', async (t) => { await t.expect(aiChat.getMessageRegenerateButton(0).exists).notOk(); // The successful command actually changed the grid state. - await t.expect(await dataGrid.apiColumnOption('name', 'sortOrder')).eql('asc'); + await t.expect(dataGrid.apiColumnOption('name', 'sortOrder')).eql('asc'); }).before(async () => createGridWithAIAssistant( { dataSource: threeRows, ...baseGrid }, [{ actions: [{ name: 'sorting', args: { dataField: 'name', sortOrder: 'asc' } }] }], @@ -161,7 +161,7 @@ test('Regenerate should NOT be visible after partial-execution failure', async ( await t.expect(aiChat.getMessageRegenerateButton(0).exists).notOk(); // No Regenerate because action #1 already mutated the grid. - await t.expect(await dataGrid.apiColumnOption('name', 'sortOrder')).eql('asc'); + await t.expect(dataGrid.apiColumnOption('name', 'sortOrder')).eql('asc'); }).before(async () => createGridWithAIAssistant( { dataSource: threeRows, ...baseGrid }, [{ @@ -191,7 +191,7 @@ test('Regenerate should NOT be visible after all-execution failure', async (t) = await t.expect(aiChat.getMessageRegenerateButton(0).exists).notOk(); // Both commands targeted non-existent columns, so real columns stay unsorted. - await t.expect(await dataGrid.apiColumnOption('name', 'sortOrder')).notOk(); + await t.expect(dataGrid.apiColumnOption('name', 'sortOrder')).notOk(); }).before(async () => createGridWithAIAssistant( { dataSource: threeRows, ...baseGrid }, [{ @@ -226,7 +226,7 @@ test('Regenerate should resend the same prompt and replace the previous response await t.expect(aiChat.getAIMessages().count).eql(1); // The regenerated command applied to the grid. - await t.expect(await dataGrid.apiColumnOption('name', 'sortOrder')).eql('asc'); + await t.expect(dataGrid.apiColumnOption('name', 'sortOrder')).eql('asc'); // The same prompt was resent with a freshly-built (current) grid context. const requests = await getAIRequests(); @@ -264,7 +264,7 @@ test('Regenerate should be disabled while request is in flight', async (t) => { await t.expect(aiChat.getMessageRegenerateButton(0).exists).notOk(); // Nothing was applied while the regenerate request is still pending. - await t.expect(await dataGrid.apiColumnOption('name', 'sortOrder')).notOk(); + await t.expect(dataGrid.apiColumnOption('name', 'sortOrder')).notOk(); }).before(async () => createGridWithAIAssistant( { dataSource: threeRows, ...baseGrid }, [ @@ -302,7 +302,7 @@ test('Regenerate is visible after a popup-close-driven abort', async (t) => { await t.expect(aiChat.getErrorMessages().count).eql(1); await t.expect(aiChat.getMessageRegenerateButton(0).exists).ok(); - await t.expect(await dataGrid.apiColumnOption('name', 'sortOrder')).notOk(); + await t.expect(dataGrid.apiColumnOption('name', 'sortOrder')).notOk(); }).before(async () => createGridWithAIAssistant( { dataSource: threeRows, ...baseGrid }, [HANG], @@ -341,7 +341,7 @@ test('Sequential regenerate after pre-execution failures keeps exactly one respo await t.expect(aiChat.getMessageRegenerateButton(0).exists).ok(); // Every retry failed before execution, so the grid was never mutated. - await t.expect(await dataGrid.apiColumnOption('name', 'sortOrder')).notOk(); + await t.expect(dataGrid.apiColumnOption('name', 'sortOrder')).notOk(); // Each Regenerate dispatched a fresh request with the same prompt. const requests = await getAIRequests(); @@ -349,5 +349,5 @@ test('Sequential regenerate after pre-execution failures keeps exactly one respo await t.expect(requests[2].text).eql('Sort by name'); }).before(async () => createGridWithAIAssistant( { dataSource: twoRows, ...baseGrid }, - [FAIL], + [FAIL, FAIL, FAIL], )); From 167a2a3d2d2b02537b184346eae40928099c7545 Mon Sep 17 00:00:00 2001 From: Alyar <> Date: Tue, 16 Jun 2026 13:58:44 +0400 Subject: [PATCH 7/8] Fix comments --- .../aiAssistant/regenerate.functional.ts | 49 +++++++++++++------ 1 file changed, 35 insertions(+), 14 deletions(-) diff --git a/e2e/testcafe-devextreme/tests/dataGrid/common/aiAssistant/regenerate.functional.ts b/e2e/testcafe-devextreme/tests/dataGrid/common/aiAssistant/regenerate.functional.ts index dc61e518da21..2d8df1e34331 100644 --- a/e2e/testcafe-devextreme/tests/dataGrid/common/aiAssistant/regenerate.functional.ts +++ b/e2e/testcafe-devextreme/tests/dataGrid/common/aiAssistant/regenerate.functional.ts @@ -20,9 +20,6 @@ const getAIRequests = ClientFunction(() => ((window as any).__aiRequests ?? []). fixture`AI Assistant - Regenerate` .page(AI_INTEGRATION_PAGE); -// === §1.12 Regenerate button === - -// 1.12.1 test('Regenerate should be visible after AI integration failure', async (t) => { const dataGrid = new DataGrid(GRID_SELECTOR); @@ -46,7 +43,6 @@ test('Regenerate should be visible after AI integration failure', async (t) => { [FAIL], )); -// 1.12.2 test('Regenerate should be visible after response format failure', async (t) => { const dataGrid = new DataGrid(GRID_SELECTOR); @@ -69,7 +65,6 @@ test('Regenerate should be visible after response format failure', async (t) => [{}], )); -// 1.12.3 test('Regenerate should be visible after validation failure', async (t) => { const dataGrid = new DataGrid(GRID_SELECTOR); @@ -92,7 +87,6 @@ test('Regenerate should be visible after validation failure', async (t) => { [{ actions: [{ name: 'unknownCommand', args: { foo: 'bar' } }] }], )); -// 1.12.4 test('Regenerate should be visible after empty actions', async (t) => { const dataGrid = new DataGrid(GRID_SELECTOR); @@ -116,7 +110,6 @@ test('Regenerate should be visible after empty actions', async (t) => { [{ actions: [] }], )); -// 1.12.5 test('Regenerate should NOT be visible after full success', async (t) => { const dataGrid = new DataGrid(GRID_SELECTOR); @@ -140,7 +133,6 @@ test('Regenerate should NOT be visible after full success', async (t) => { [{ actions: [{ name: 'sorting', args: { dataField: 'name', sortOrder: 'asc' } }] }], )); -// 1.12.6 test('Regenerate should NOT be visible after partial-execution failure', async (t) => { const dataGrid = new DataGrid(GRID_SELECTOR); @@ -172,7 +164,6 @@ test('Regenerate should NOT be visible after partial-execution failure', async ( }], )); -// 1.12.7 test('Regenerate should NOT be visible after all-execution failure', async (t) => { const dataGrid = new DataGrid(GRID_SELECTOR); @@ -202,7 +193,6 @@ test('Regenerate should NOT be visible after all-execution failure', async (t) = }], )); -// 1.12.8 test('Regenerate should resend the same prompt and replace the previous response', async (t) => { const dataGrid = new DataGrid(GRID_SELECTOR); @@ -242,7 +232,6 @@ test('Regenerate should resend the same prompt and replace the previous response ], )); -// 1.12.9 test('Regenerate should be disabled while request is in flight', async (t) => { const dataGrid = new DataGrid(GRID_SELECTOR); @@ -273,7 +262,6 @@ test('Regenerate should be disabled while request is in flight', async (t) => { ], )); -// 1.12.11 test('Regenerate is visible after a popup-close-driven abort', async (t) => { const dataGrid = new DataGrid(GRID_SELECTOR); @@ -308,9 +296,42 @@ test('Regenerate is visible after a popup-close-driven abort', async (t) => { [HANG], )); -// === §3.8.2 Sequential resends after pre-execution failures === +test('Regenerate after a column is removed should resend with the actual context', 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.getMessageRegenerateButton(0).exists).ok(); + + await dataGrid.apiOption('columns', ['id', 'name']); + + await t.click(aiChat.getMessageRegenerateButton(0)); + + await t.expect(aiChat.getAIMessages().count).eql(1); + await t.expect(aiChat.getSuccessMessages().count).eql(1); + await t.expect(dataGrid.apiColumnOption('name', 'sortOrder')).eql('asc'); + + const requests = await getAIRequests(); + await t.expect(requests.length).eql(2); + await t.expect(requests[0].columns).eql(['id', 'name', 'value']); + await t.expect(requests[1].columns).eql(['id', 'name']); +}).before(async () => createGridWithAIAssistant( + { dataSource: threeRows, ...baseGrid }, + [ + FAIL, + { actions: [{ name: 'sorting', args: { dataField: 'name', sortOrder: 'asc' } }] }, + ], +)); -// 3.8.2 test('Sequential regenerate after pre-execution failures keeps exactly one response', async (t) => { const dataGrid = new DataGrid(GRID_SELECTOR); From 9d139cef79a69d95705fcebef0ee8664e1ae714b Mon Sep 17 00:00:00 2001 From: Alyar <> Date: Tue, 16 Jun 2026 15:04:14 +0400 Subject: [PATCH 8/8] move cancel-aborted regenerate test to regenerate suite --- .../aiAssistant/chatExperience.functional.ts | 27 ------------------- .../aiAssistant/regenerate.functional.ts | 27 +++++++++++++++++++ 2 files changed, 27 insertions(+), 27 deletions(-) diff --git a/e2e/testcafe-devextreme/tests/dataGrid/common/aiAssistant/chatExperience.functional.ts b/e2e/testcafe-devextreme/tests/dataGrid/common/aiAssistant/chatExperience.functional.ts index 8fd5f01ee284..66d52991bd66 100644 --- a/e2e/testcafe-devextreme/tests/dataGrid/common/aiAssistant/chatExperience.functional.ts +++ b/e2e/testcafe-devextreme/tests/dataGrid/common/aiAssistant/chatExperience.functional.ts @@ -501,30 +501,3 @@ test('Clear-chat after re-open should remove all history', async (t) => { }, [ { actions: [{ name: 'sorting', args: { dataField: 'name', sortOrder: 'asc' } }] }, ])); - -test('cancel-aborted message currently shows a Regenerate button', 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.getMessageRegenerateButton(0).exists).ok(); -}).before(async () => createGridWithAIAssistant( - { - dataSource: threeRows, - keyExpr: 'id', - columns: ['id', 'name', 'value'], - showBorders: true, - }, - [{ actions: [{ name: 'sorting', args: { dataField: 'name', sortOrder: 'asc' } }] }], - {}, - { onAIAssistantRequestCreating: (e: any) => { e.cancel = true; } }, -)); diff --git a/e2e/testcafe-devextreme/tests/dataGrid/common/aiAssistant/regenerate.functional.ts b/e2e/testcafe-devextreme/tests/dataGrid/common/aiAssistant/regenerate.functional.ts index 2d8df1e34331..b30b0c373bb7 100644 --- a/e2e/testcafe-devextreme/tests/dataGrid/common/aiAssistant/regenerate.functional.ts +++ b/e2e/testcafe-devextreme/tests/dataGrid/common/aiAssistant/regenerate.functional.ts @@ -372,3 +372,30 @@ test('Sequential regenerate after pre-execution failures keeps exactly one respo { dataSource: twoRows, ...baseGrid }, [FAIL, FAIL, FAIL], )); + +test('cancel-aborted message currently shows a Regenerate button', 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.getMessageRegenerateButton(0).exists).ok(); +}).before(async () => createGridWithAIAssistant( + { + dataSource: threeRows, + keyExpr: 'id', + columns: ['id', 'name', 'value'], + showBorders: true, + }, + [{ actions: [{ name: 'sorting', args: { dataField: 'name', sortOrder: 'asc' } }] }], + {}, + { onAIAssistantRequestCreating: (e: any) => { e.cancel = true; } }, +));