-
Notifications
You must be signed in to change notification settings - Fork 672
DataGrid - AI Assistant: error handling e2e tests #33809
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Alyar666
merged 9 commits into
DevExpress:26_1
from
Alyar666:ai_assistant_e2e_error_handling_26_1
Jun 16, 2026
+359
−0
Merged
Changes from all commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
9c18213
DataGrid - AI Assistant: error handling e2e tests
68e32e6
DataGrid - AI Assistant: dedup error handling e2e tests and drop unus…
00c1dfd
Fix copilot comment
8922119
DataGrid - AI Assistant: fix 2.4.3 to exercise selectByKeys with abse…
8b955d4
DataGrid - AI Assistant: use shared AI mock setup from testHelpers
ef66f52
DataGrid - AI Assistant: merge fixtures into one per file
d6e58e4
DataGrid - AI Assistant: restore scoped getWrapper override + pass as…
d5941cf
Fix comments
15ac4c3
Fix comment (part 2)
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
351 changes: 351 additions & 0 deletions
351
e2e/testcafe-devextreme/tests/dataGrid/common/aiAssistant/errorHandling.functional.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,351 @@ | ||
| /* eslint-disable no-underscore-dangle */ | ||
| import DataGrid from 'devextreme-testcafe-models/dataGrid'; | ||
| import { ClientFunction } from 'testcafe'; | ||
| import { createWidget } from '../../../../helpers/createWidget'; | ||
| import { | ||
| AI_INTEGRATION_PAGE, | ||
| FAIL, | ||
| GRID_SELECTOR, | ||
| baseGrid as gridDefaults, | ||
| createGridWithAIAssistant, | ||
| getLoggedErrorIds, | ||
| setupAIState, | ||
| threeRows, | ||
| } from './testHelpers'; | ||
|
|
||
| type AIChat = ReturnType<DataGrid['getAIAssistantChat']>; | ||
|
|
||
| const baseGrid = { ...gridDefaults, dataSource: threeRows }; | ||
|
|
||
| const groupingLockedColumns = [ | ||
| { dataField: 'id' }, | ||
| { dataField: 'name', allowGrouping: false }, | ||
| { dataField: 'value' }, | ||
| ]; | ||
|
|
||
| const formatMessage = ClientFunction( | ||
| (key: string) => (window as any).DevExpress.localization.formatMessage(key), | ||
| ); | ||
|
|
||
| const invalidResponse = (): Promise<string> => formatMessage( | ||
| 'dxDataGrid-aiAssistantInvalidResponseMessage', | ||
| ); | ||
|
|
||
| const errorHeader = (): Promise<string> => formatMessage( | ||
| 'dxDataGrid-aiAssistantErrorMessageHeader', | ||
| ); | ||
|
|
||
| const getSelectedRowsCount = ClientFunction( | ||
| () => (window as any).widget.getSelectedRowsData().length, | ||
| ); | ||
|
|
||
| const noIntegrationOptions = (): any => ({ | ||
| ...(window as any).__aiBase, | ||
| aiAssistant: { enabled: true }, | ||
| }); | ||
|
|
||
| const createGridWithoutIntegration = async ( | ||
| base: Record<string, unknown>, | ||
| ): Promise<void> => { | ||
| await setupAIState(base, []); | ||
|
|
||
| return createWidget('dxDataGrid', noIntegrationOptions); | ||
| }; | ||
|
|
||
| const openChatAndSubmit = async ( | ||
| t: TestController, | ||
| prompt: string, | ||
| ): Promise<{ dataGrid: DataGrid; aiChat: AIChat }> => { | ||
| 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(), prompt) | ||
| .pressKey('enter'); | ||
|
|
||
| return { dataGrid, aiChat }; | ||
| }; | ||
|
|
||
| const expectInvalidResponse = async ( | ||
| t: TestController, | ||
| aiChat: AIChat, | ||
| dataGrid: DataGrid, | ||
| ): Promise<void> => { | ||
| await t.expect(aiChat.getMessages().count).eql(2); | ||
| await t.expect(aiChat.getErrorMessages().count).eql(1); | ||
| await t.expect(aiChat.getMessageHeader(0).innerText).eql(await errorHeader()); | ||
| await t.expect(aiChat.getMessageErrorText(0).innerText).eql(await invalidResponse()); | ||
| await t.expect(aiChat.getSuccessMessages().count).eql(0); | ||
| await t.expect(aiChat.getActionItems(0).count).eql(0); | ||
| await t.expect(dataGrid.apiColumnOption('name', 'sortOrder')).notOk(); | ||
| }; | ||
|
|
||
| fixture`AI Assistant - Error Handling` | ||
| .page(AI_INTEGRATION_PAGE); | ||
|
|
||
| test('Empty actions array should show no-action message and leave grid unchanged', async (t) => { | ||
| const { dataGrid, aiChat } = await openChatAndSubmit(t, 'Tell me a joke'); | ||
|
|
||
| await expectInvalidResponse(t, aiChat, dataGrid); | ||
| }).before(async () => createGridWithAIAssistant( | ||
| baseGrid, | ||
| [{ actions: [] }], | ||
| )); | ||
|
|
||
| test('Sorting by a non-existent column should fail: schema is valid but incompatible with grid state', async (t) => { | ||
| const { dataGrid, aiChat } = await openChatAndSubmit(t, 'Sort by Salary'); | ||
|
|
||
| await t.expect(aiChat.getMessages().count).eql(2); | ||
| await t.expect(aiChat.getSuccessMessages().count).eql(0); | ||
| await t.expect(aiChat.getErrorMessages().count).eql(1); | ||
| await t.expect(aiChat.getErrorActionItems(0).count).eql(1); | ||
| await t.expect(dataGrid.apiGetDataSourceSortParams()).notOk(); | ||
| }).before(async () => createGridWithAIAssistant( | ||
|
Alyar666 marked this conversation as resolved.
|
||
| baseGrid, | ||
| [{ actions: [{ name: 'sorting', args: { dataField: 'Salary', sortOrder: 'asc' } }] }], | ||
| )); | ||
|
|
||
| test('Selecting non-existent keys should succeed: schema is valid and the missing data is not sent in the request', async (t) => { | ||
| const { aiChat } = await openChatAndSubmit(t, 'Select rows 999 and 1000'); | ||
|
|
||
| await t.expect(aiChat.getMessages().count).eql(2); | ||
| await t.expect(aiChat.getErrorMessages().count).eql(0); | ||
| await t.expect(aiChat.getSuccessMessages().count).eql(1); | ||
| await t.expect(aiChat.getSuccessActionItems(0).count).eql(1); | ||
| await t.expect(getSelectedRowsCount()).eql(0); | ||
| }).before(async () => createGridWithAIAssistant( | ||
| { ...baseGrid, selection: { mode: 'multiple' } }, | ||
| [{ actions: [{ name: 'selectByKeys', args: { keys: [999, 1000], preserve: false } }] }], | ||
| )); | ||
|
|
||
| test('sendRequest rejection should show error message and leave grid unchanged', async (t) => { | ||
| const { dataGrid, aiChat } = await openChatAndSubmit(t, 'Sort by name'); | ||
|
|
||
| await expectInvalidResponse(t, aiChat, dataGrid); | ||
| }).before(async () => createGridWithAIAssistant( | ||
| baseGrid, | ||
| [FAIL], | ||
| )); | ||
|
|
||
| test('Missing aiIntegration should show an error and leave grid unchanged', async (t) => { | ||
|
Alyar666 marked this conversation as resolved.
|
||
| const { dataGrid, aiChat } = await openChatAndSubmit(t, 'Sort by name'); | ||
|
|
||
| await expectInvalidResponse(t, aiChat, dataGrid); | ||
| await t.expect(await getLoggedErrorIds(t)).contains('E1068'); | ||
| }).before(async () => createGridWithoutIntegration(baseGrid)); | ||
|
|
||
| test('Non-JSON string response should show invalid-response error', async (t) => { | ||
| const { dataGrid, aiChat } = await openChatAndSubmit(t, 'Sort by name'); | ||
|
|
||
| await expectInvalidResponse(t, aiChat, dataGrid); | ||
| }).before(async () => createGridWithAIAssistant( | ||
| baseGrid, | ||
| ['not json'], | ||
| )); | ||
|
|
||
| test('Non-JSON actions string should show invalid-response error', async (t) => { | ||
| const { dataGrid, aiChat } = await openChatAndSubmit(t, 'Sort by name'); | ||
|
|
||
| await expectInvalidResponse(t, aiChat, dataGrid); | ||
| }).before(async () => createGridWithAIAssistant( | ||
| baseGrid, | ||
| [{ actions: 'not json' }], | ||
| )); | ||
|
|
||
| test('Empty string response should show invalid-response error', async (t) => { | ||
| const { dataGrid, aiChat } = await openChatAndSubmit(t, 'Sort by name'); | ||
|
|
||
| await expectInvalidResponse(t, aiChat, dataGrid); | ||
| }).before(async () => createGridWithAIAssistant( | ||
| baseGrid, | ||
| [''], | ||
| )); | ||
|
|
||
| test('Response missing actions should show invalid-response error', async (t) => { | ||
| const { dataGrid, aiChat } = await openChatAndSubmit(t, 'Sort by name'); | ||
|
|
||
| await expectInvalidResponse(t, aiChat, dataGrid); | ||
| }).before(async () => createGridWithAIAssistant( | ||
| baseGrid, | ||
| [{}], | ||
| )); | ||
|
|
||
| test('Object actions (not array) should show invalid-response error', async (t) => { | ||
| const { dataGrid, aiChat } = await openChatAndSubmit(t, 'Sort by name'); | ||
|
|
||
| await expectInvalidResponse(t, aiChat, dataGrid); | ||
| }).before(async () => createGridWithAIAssistant( | ||
| baseGrid, | ||
| [{ actions: { name: 'sorting' } }], | ||
| )); | ||
|
|
||
| test('Null actions should show invalid-response error', async (t) => { | ||
| const { dataGrid, aiChat } = await openChatAndSubmit(t, 'Sort by name'); | ||
|
|
||
| await expectInvalidResponse(t, aiChat, dataGrid); | ||
| }).before(async () => createGridWithAIAssistant( | ||
| baseGrid, | ||
| [{ actions: null }], | ||
| )); | ||
|
|
||
| test('Primitive actions should show invalid-response error', async (t) => { | ||
| const { dataGrid, aiChat } = await openChatAndSubmit(t, 'Sort by name'); | ||
|
|
||
| await expectInvalidResponse(t, aiChat, dataGrid); | ||
| }).before(async () => createGridWithAIAssistant( | ||
| baseGrid, | ||
| [{ actions: 42 }], | ||
| )); | ||
|
|
||
| test('Null response should show invalid-response error', async (t) => { | ||
| const { dataGrid, aiChat } = await openChatAndSubmit(t, 'Sort by name'); | ||
|
|
||
| await expectInvalidResponse(t, aiChat, dataGrid); | ||
| }).before(async () => createGridWithAIAssistant( | ||
| baseGrid, | ||
| [null], | ||
| )); | ||
|
|
||
| test('Unknown command name should show invalid-response error', async (t) => { | ||
| const { dataGrid, aiChat } = await openChatAndSubmit(t, 'Sort by name'); | ||
|
|
||
| await expectInvalidResponse(t, aiChat, dataGrid); | ||
| }).before(async () => createGridWithAIAssistant( | ||
| baseGrid, | ||
| [{ actions: [{ name: 'unknownCmd', args: {} }] }], | ||
| )); | ||
|
|
||
| test('Non-string command name should show invalid-response error', async (t) => { | ||
| const { dataGrid, aiChat } = await openChatAndSubmit(t, 'Sort by name'); | ||
|
|
||
| await expectInvalidResponse(t, aiChat, dataGrid); | ||
| }).before(async () => createGridWithAIAssistant( | ||
| baseGrid, | ||
| [{ actions: [{ name: 123, args: {} }] }], | ||
| )); | ||
|
|
||
| test('Empty command name should show invalid-response error', async (t) => { | ||
| const { dataGrid, aiChat } = await openChatAndSubmit(t, 'Sort by name'); | ||
|
|
||
| await expectInvalidResponse(t, aiChat, dataGrid); | ||
| }).before(async () => createGridWithAIAssistant( | ||
| baseGrid, | ||
| [{ actions: [{ name: '', args: {} }] }], | ||
| )); | ||
|
|
||
| test('Action without name should show invalid-response error', async (t) => { | ||
| const { dataGrid, aiChat } = await openChatAndSubmit(t, 'Sort by name'); | ||
|
|
||
| await expectInvalidResponse(t, aiChat, dataGrid); | ||
| }).before(async () => createGridWithAIAssistant( | ||
| baseGrid, | ||
| [{ actions: [{ args: {} }] }], | ||
| )); | ||
|
|
||
| test('Action without args should show invalid-response error', async (t) => { | ||
| const { dataGrid, aiChat } = await openChatAndSubmit(t, 'Sort by name'); | ||
|
|
||
| await expectInvalidResponse(t, aiChat, dataGrid); | ||
| }).before(async () => createGridWithAIAssistant( | ||
| baseGrid, | ||
| [{ actions: [{ name: 'sorting' }] }], | ||
| )); | ||
|
|
||
| test('Null args should show invalid-response error', async (t) => { | ||
| const { dataGrid, aiChat } = await openChatAndSubmit(t, 'Sort by name'); | ||
|
|
||
| await expectInvalidResponse(t, aiChat, dataGrid); | ||
| }).before(async () => createGridWithAIAssistant( | ||
| baseGrid, | ||
| [{ actions: [{ name: 'sorting', args: null }] }], | ||
| )); | ||
|
|
||
| test('Args missing a required property should show invalid-response error', async (t) => { | ||
| const { dataGrid, aiChat } = await openChatAndSubmit(t, 'Sort by name'); | ||
|
|
||
| await expectInvalidResponse(t, aiChat, dataGrid); | ||
| }).before(async () => createGridWithAIAssistant( | ||
| baseGrid, | ||
| [{ actions: [{ name: 'sorting', args: { sortOrder: 'asc' } }] }], | ||
| )); | ||
|
|
||
| test('Args with a wrong-typed property should show invalid-response error', async (t) => { | ||
| const { dataGrid, aiChat } = await openChatAndSubmit(t, 'Sort by name'); | ||
|
|
||
| await expectInvalidResponse(t, aiChat, dataGrid); | ||
| }).before(async () => createGridWithAIAssistant(baseGrid, [{ actions: [{ name: 'sorting', args: { dataField: 'name', sortOrder: 123 } }] }])); | ||
|
|
||
| test('Args with an extra property should show invalid-response error', async (t) => { | ||
| const { dataGrid, aiChat } = await openChatAndSubmit(t, 'Sort by name'); | ||
|
|
||
| await expectInvalidResponse(t, aiChat, dataGrid); | ||
| }).before(async () => createGridWithAIAssistant(baseGrid, [{ actions: [{ name: 'sorting', args: { dataField: 'name', sortOrder: 'asc', foo: 'bar' } }] }])); | ||
|
|
||
| test('Mix of valid and invalid actions should reject the whole response', async (t) => { | ||
| const { dataGrid, aiChat } = await openChatAndSubmit(t, 'Sort by name'); | ||
|
|
||
| // expectInvalidResponse also asserts the valid sorting action did NOT execute. | ||
| await expectInvalidResponse(t, aiChat, dataGrid); | ||
| }).before(async () => createGridWithAIAssistant(baseGrid, [{ | ||
| actions: [ | ||
| { name: 'sorting', args: { dataField: 'name', sortOrder: 'asc' } }, | ||
| { name: 'unknownCmd', args: {} }, | ||
| ], | ||
| }])); | ||
|
|
||
| test('No-arg command with non-empty args should show invalid-response error', async (t) => { | ||
| const { dataGrid, aiChat } = await openChatAndSubmit(t, 'Clear sorting'); | ||
|
|
||
| await expectInvalidResponse(t, aiChat, dataGrid); | ||
| }).before(async () => createGridWithAIAssistant( | ||
| baseGrid, | ||
| [{ actions: [{ name: 'clearSorting', args: { foo: 1 } }] }], | ||
| )); | ||
|
|
||
| test('Partial failure should report each action status and apply successes', async (t) => { | ||
| const { dataGrid, aiChat } = await openChatAndSubmit(t, 'Sort, group and search'); | ||
|
|
||
| await t.expect(aiChat.getMessages().count).eql(2); | ||
| await t.expect(aiChat.getErrorMessages().count).eql(1); | ||
| await t.expect(aiChat.getActionItems(0).count).eql(3); | ||
| await t.expect(aiChat.getSuccessActionItems(0).count).eql(2); | ||
| await t.expect(aiChat.getErrorActionItems(0).count).eql(1); | ||
| // The failed grouping action carries its predefined per-command message. | ||
| await t.expect(aiChat.getActionItemText(0, 1).innerText).contains('Group data against'); | ||
| // Successful actions (#1 sorting, #3 searching) took effect; failed grouping did not. | ||
| await t.expect(dataGrid.apiColumnOption('name', 'sortOrder')).eql('asc'); | ||
| await t.expect(dataGrid.apiColumnOption('name', 'groupIndex')).eql(undefined); | ||
| await t.expect(dataGrid.apiOption('searchPanel.text')).eql('Alice'); | ||
| }).before(async () => createGridWithAIAssistant({ ...baseGrid, columns: groupingLockedColumns }, [{ | ||
| actions: [ | ||
| { name: 'sorting', args: { dataField: 'name', sortOrder: 'asc' } }, | ||
| { name: 'grouping', args: { dataField: 'name', groupIndex: 0 } }, | ||
| { name: 'searching', args: { text: 'Alice' } }, | ||
| ], | ||
| }])); | ||
|
|
||
| test('All-commands failure should report failures and leave grid unchanged', async (t) => { | ||
| const { dataGrid, aiChat } = await openChatAndSubmit(t, 'Do impossible things'); | ||
|
|
||
| await t.expect(aiChat.getMessages().count).eql(2); | ||
| await t.expect(aiChat.getErrorMessages().count).eql(1); | ||
| await t.expect(aiChat.getActionItems(0).count).eql(3); | ||
| await t.expect(aiChat.getSuccessActionItems(0).count).eql(0); | ||
| await t.expect(aiChat.getErrorActionItems(0).count).eql(3); | ||
| // Each failed action keeps its own predefined per-command message. | ||
| await t.expect(aiChat.getActionItemText(0, 0).innerText).contains('Sort data against'); | ||
| await t.expect(aiChat.getActionItemText(0, 2).innerText).contains('Group data against'); | ||
| await t.expect(dataGrid.apiColumnOption('name', 'sortOrder')).notOk(); | ||
| await t.expect(dataGrid.apiColumnOption('name', 'groupIndex')).eql(undefined); | ||
| }).before(async () => createGridWithAIAssistant({ ...baseGrid, columns: groupingLockedColumns }, [{ | ||
| actions: [ | ||
| { name: 'sorting', args: { dataField: 'NopeOne', sortOrder: 'asc' } }, | ||
| { name: 'sorting', args: { dataField: 'NopeTwo', sortOrder: 'desc' } }, | ||
| { name: 'grouping', args: { dataField: 'name', groupIndex: 0 } }, | ||
| ], | ||
| }])); | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.