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 new file mode 100644 index 000000000000..b30b0c373bb7 --- /dev/null +++ b/e2e/testcafe-devextreme/tests/dataGrid/common/aiAssistant/regenerate.functional.ts @@ -0,0 +1,401 @@ +/* eslint-disable no-underscore-dangle */ +import { ClientFunction } from 'testcafe'; +import DataGrid from 'devextreme-testcafe-models/dataGrid'; +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), +}))); + +fixture`AI Assistant - Regenerate` + .page(AI_INTEGRATION_PAGE); + +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(); + + // Pre-execution failure: nothing was applied to the grid. + await t.expect(dataGrid.apiColumnOption('name', 'sortOrder')).notOk(); +}).before(async () => createGridWithAIAssistant( + { dataSource: threeRows, ...baseGrid }, + [FAIL], +)); + +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(); + + await t.expect(dataGrid.apiColumnOption('name', 'sortOrder')).notOk(); +}).before(async () => createGridWithAIAssistant( + { dataSource: threeRows, ...baseGrid }, + [{}], +)); + +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(); + + await t.expect(dataGrid.apiColumnOption('name', 'sortOrder')).notOk(); +}).before(async () => createGridWithAIAssistant( + { dataSource: threeRows, ...baseGrid }, + [{ actions: [{ name: 'unknownCommand', args: { foo: 'bar' } }] }], +)); + +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'); + + // 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(); + + await t.expect(dataGrid.apiColumnOption('name', 'sortOrder')).notOk(); +}).before(async () => createGridWithAIAssistant( + { dataSource: threeRows, ...baseGrid }, + [{ actions: [] }], +)); + +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(); + + // The successful command actually changed the grid state. + await t.expect(dataGrid.apiColumnOption('name', 'sortOrder')).eql('asc'); +}).before(async () => createGridWithAIAssistant( + { dataSource: threeRows, ...baseGrid }, + [{ actions: [{ name: 'sorting', args: { dataField: 'name', sortOrder: 'asc' } }] }], +)); + +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(); + + // No Regenerate because action #1 already mutated the grid. + await t.expect(dataGrid.apiColumnOption('name', 'sortOrder')).eql('asc'); +}).before(async () => createGridWithAIAssistant( + { dataSource: threeRows, ...baseGrid }, + [{ + actions: [ + { name: 'sorting', args: { dataField: 'name', sortOrder: 'asc' } }, + { name: 'sorting', args: { dataField: 'nonExistent', sortOrder: 'asc' } }, + ], + }], +)); + +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(); + + // Both commands targeted non-existent columns, so real columns stay unsorted. + await t.expect(dataGrid.apiColumnOption('name', 'sortOrder')).notOk(); +}).before(async () => createGridWithAIAssistant( + { dataSource: threeRows, ...baseGrid }, + [{ + actions: [ + { name: 'sorting', args: { dataField: 'nonExistent1', sortOrder: 'asc' } }, + { name: 'sorting', args: { dataField: 'nonExistent2', sortOrder: 'desc' } }, + ], + }], +)); + +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)); + + // 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); + + // The regenerated command applied to the grid. + 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(); + 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, ...baseGrid }, + [ + FAIL, + { actions: [{ name: 'sorting', args: { dataField: 'name', sortOrder: 'asc' } }] }, + ], +)); + +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(); + + // Nothing was applied while the regenerate request is still pending. + await t.expect(dataGrid.apiColumnOption('name', 'sortOrder')).notOk(); +}).before(async () => createGridWithAIAssistant( + { dataSource: threeRows, ...baseGrid }, + [ + FAIL, + HANG, + ], +)); + +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 (pins current behavior; see doc §1.12.11). + await t.expect(aiChat.getErrorMessages().count).eql(1); + await t.expect(aiChat.getMessageRegenerateButton(0).exists).ok(); + + await t.expect(dataGrid.apiColumnOption('name', 'sortOrder')).notOk(); +}).before(async () => createGridWithAIAssistant( + { dataSource: threeRows, ...baseGrid }, + [HANG], +)); + +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' } }] }, + ], +)); + +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(); + + // Every retry failed before execution, so the grid was never mutated. + await t.expect(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, ...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; } }, +));