Skip to content

Commit 2b1cfec

Browse files
Alyar666claude
andauthored
DataGrid - AI Assistant: toolbar & popup e2e tests (#33803)
Co-authored-by: Alyar <> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 9c65504 commit 2b1cfec

5 files changed

Lines changed: 279 additions & 66 deletions

File tree

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
/* eslint-disable no-underscore-dangle */
2+
import { ClientFunction } from 'testcafe';
3+
import { createWidget } from '../../../../helpers/createWidget';
4+
import url from '../../../../helpers/getPageUrl';
5+
6+
export const GRID_SELECTOR = '#container';
7+
8+
export const AI_INTEGRATION_PAGE = url(__dirname, '../../../container-ai-integration.html');
9+
10+
export const threeRows = [
11+
{ id: 1, name: 'Alice', value: 30 },
12+
{ id: 2, name: 'Bob', value: 20 },
13+
{ id: 3, name: 'Charlie', value: 10 },
14+
];
15+
16+
export const twoRows = [
17+
{ id: 1, name: 'Alice', value: 30 },
18+
{ id: 2, name: 'Bob', value: 20 },
19+
];
20+
21+
export const baseGrid = {
22+
keyExpr: 'id',
23+
columns: ['id', 'name', 'value'],
24+
showBorders: true,
25+
};
26+
27+
export const HANG = '__HANG__';
28+
29+
export const FAIL = '__FAIL__';
30+
31+
export const setupAIState = ClientFunction((
32+
base: Record<string, unknown>,
33+
responses: unknown[],
34+
hangMarker?: string,
35+
failMarker?: string,
36+
) => {
37+
(window as any).__aiBase = base;
38+
(window as any).__aiResponses = responses;
39+
(window as any).__aiCallCount = 0;
40+
(window as any).__aiRequests = [];
41+
(window as any).__aiAbortCalled = false;
42+
(window as any).__aiAssistantExtra = {};
43+
(window as any).__aiGridExtra = {};
44+
(window as any).__aiHangMarker = hangMarker;
45+
(window as any).__aiFailMarker = failMarker;
46+
});
47+
48+
const aiGridOptions = (): any => ({
49+
...(window as any).__aiBase,
50+
...((window as any).__aiGridExtra ?? {}),
51+
aiAssistant: {
52+
enabled: true,
53+
aiIntegration: new (window as any).DevExpress.aiIntegration.AIIntegration({
54+
sendRequest(params: any) {
55+
const count = (window as any).__aiCallCount;
56+
const response = (window as any).__aiResponses[count];
57+
58+
(window as any).__aiCallCount = count + 1;
59+
(window as any).__aiRequests.push(params);
60+
61+
const abort = (): void => { (window as any).__aiAbortCalled = true; };
62+
63+
if (response === (window as any).__aiHangMarker) {
64+
return { promise: new Promise(() => {}), abort };
65+
}
66+
67+
if (response === (window as any).__aiFailMarker) {
68+
return { promise: Promise.reject(new Error('AI error')), abort };
69+
}
70+
71+
if (response === undefined) {
72+
return { promise: Promise.reject(new Error(`Unexpected AI call #${count}`)), abort };
73+
}
74+
75+
return { promise: Promise.resolve(response), abort };
76+
},
77+
}),
78+
...((window as any).__aiAssistantExtra ?? {}),
79+
},
80+
});
81+
82+
const setAIExtras = (
83+
assistantExtra: Record<string, unknown>,
84+
gridExtra: Record<string, unknown>,
85+
): Promise<void> => ClientFunction(
86+
() => {
87+
(window as any).__aiAssistantExtra = assistantExtra;
88+
(window as any).__aiGridExtra = gridExtra;
89+
},
90+
{ dependencies: { assistantExtra, gridExtra } },
91+
)();
92+
93+
export const createGridWithAIAssistant = async (
94+
base: Record<string, unknown>,
95+
responses: unknown[],
96+
assistantExtra: Record<string, unknown> = {},
97+
gridExtra: Record<string, unknown> = {},
98+
): Promise<void> => {
99+
await setupAIState(base, responses, HANG, FAIL);
100+
await setAIExtras(assistantExtra, gridExtra);
101+
102+
return createWidget('dxDataGrid', aiGridOptions);
103+
};
104+
105+
export const getRequests = ClientFunction(() => (window as any).__aiRequests);
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
import DataGrid from 'devextreme-testcafe-models/dataGrid';
2+
import { createWidget } from '../../../../helpers/createWidget';
3+
import {
4+
AI_INTEGRATION_PAGE,
5+
GRID_SELECTOR,
6+
HANG,
7+
baseGrid,
8+
createGridWithAIAssistant,
9+
threeRows,
10+
} from './testHelpers';
11+
12+
const sortByNameResponse = {
13+
actions: [{ name: 'sorting', args: { dataField: 'name', sortOrder: 'asc' } }],
14+
};
15+
16+
const gridWithoutAssistant = (): any => ({
17+
dataSource: [
18+
{ id: 1, name: 'Alice', value: 30 },
19+
{ id: 2, name: 'Bob', value: 20 },
20+
{ id: 3, name: 'Charlie', value: 10 },
21+
],
22+
keyExpr: 'id',
23+
columns: ['id', 'name', 'value'],
24+
showBorders: true,
25+
});
26+
27+
fixture`AI Assistant - Toolbar & Popup`
28+
.page(AI_INTEGRATION_PAGE);
29+
30+
test('Toolbar button should be visible when aiAssistant.enabled is true', async (t) => {
31+
const dataGrid = new DataGrid(GRID_SELECTOR);
32+
33+
await t.expect(dataGrid.isReady()).ok();
34+
35+
await t.expect(dataGrid.getAIAssistantButton().exists).ok();
36+
}).before(async () => createGridWithAIAssistant({ ...baseGrid, dataSource: threeRows }, [HANG]));
37+
38+
test('Toolbar button should be hidden when aiAssistant is not configured', async (t) => {
39+
const dataGrid = new DataGrid(GRID_SELECTOR);
40+
41+
await t.expect(dataGrid.isReady()).ok();
42+
43+
await t.expect(dataGrid.getAIAssistantButton().exists).notOk();
44+
}).before(async () => createWidget('dxDataGrid', gridWithoutAssistant));
45+
46+
test('Popup should open on toolbar button click without changing grid state', async (t) => {
47+
const dataGrid = new DataGrid(GRID_SELECTOR);
48+
49+
await t.expect(dataGrid.isReady()).ok();
50+
51+
const initialState = await dataGrid.apiState();
52+
53+
await t.click(dataGrid.getAIAssistantButton());
54+
55+
const aiChat = dataGrid.getAIAssistantChat();
56+
const finalState = await dataGrid.apiState();
57+
58+
await t.expect(finalState).eql(initialState);
59+
await t.expect(aiChat.element.visible).ok();
60+
await t.expect(aiChat.getChat().element.exists).ok();
61+
await t.expect(aiChat.getInput().visible).ok();
62+
}).before(async () => createGridWithAIAssistant({ ...baseGrid, dataSource: threeRows }, [HANG]));
63+
64+
test('AI Assistant-applied sorting should persist after popup close', async (t) => {
65+
const dataGrid = new DataGrid(GRID_SELECTOR);
66+
67+
await t.expect(dataGrid.isReady()).ok();
68+
69+
await t.click(dataGrid.getAIAssistantButton());
70+
71+
const aiChat = dataGrid.getAIAssistantChat();
72+
73+
await t
74+
.typeText(aiChat.getInput(), 'Sort by name')
75+
.pressKey('enter');
76+
77+
await t.expect(aiChat.getSuccessMessages().count).eql(1);
78+
await t.expect(aiChat.getActionItems(0).count).eql(1);
79+
80+
await t.click(aiChat.getCloseButton().element);
81+
82+
const sortOrder = await dataGrid.apiColumnOption('name', 'sortOrder');
83+
84+
await t.expect(sortOrder).eql('asc');
85+
}).before(async () => createGridWithAIAssistant(
86+
{ ...baseGrid, dataSource: threeRows },
87+
[sortByNameResponse],
88+
));
89+
90+
test('Custom title should be rendered in popup header', async (t) => {
91+
const dataGrid = new DataGrid(GRID_SELECTOR);
92+
93+
await t.expect(dataGrid.isReady()).ok();
94+
95+
await t.click(dataGrid.getAIAssistantButton());
96+
97+
const aiChat = dataGrid.getAIAssistantChat();
98+
99+
await t.expect(aiChat.getTitle().textContent).contains('My Custom Assistant');
100+
}).before(async () => createGridWithAIAssistant(
101+
{ ...baseGrid, dataSource: threeRows },
102+
[HANG],
103+
{ title: 'My Custom Assistant' },
104+
));
105+
106+
test('Toolbar button should activate via Enter key', async (t) => {
107+
const dataGrid = new DataGrid(GRID_SELECTOR);
108+
109+
await t.expect(dataGrid.isReady()).ok();
110+
111+
await dataGrid.focusAIAssistantButton();
112+
113+
await t.expect(dataGrid.getAIAssistantButton().focused).ok();
114+
115+
await t.pressKey('enter');
116+
117+
await t.expect(dataGrid.getAIAssistantChat().element.visible).ok();
118+
}).before(async () => createGridWithAIAssistant({ ...baseGrid, dataSource: threeRows }, [HANG]));
119+
120+
test('Toolbar button should activate via Space key', async (t) => {
121+
const dataGrid = new DataGrid(GRID_SELECTOR);
122+
123+
await t.expect(dataGrid.isReady()).ok();
124+
125+
await dataGrid.focusAIAssistantButton();
126+
127+
await t.expect(dataGrid.getAIAssistantButton().focused).ok();
128+
129+
await t.pressKey('space');
130+
131+
await t.expect(dataGrid.getAIAssistantChat().element.visible).ok();
132+
}).before(async () => createGridWithAIAssistant({ ...baseGrid, dataSource: threeRows }, [HANG]));

packages/testcafe-models/chat.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,12 @@ export default class Chat extends Widget {
4343
return new Scrollable(this.element.find(`.${CLASS.scrollable}`));
4444
}
4545

46+
getMessageBubbles(): Selector {
47+
return this.element.find(`.${CLASS.messageBubble}`);
48+
}
49+
4650
getMessage(index: number): Selector {
47-
return this.element.find(`.${CLASS.messageBubble}`).nth(index);
51+
return this.getMessageBubbles().nth(index);
4852
}
4953

5054
getContextMenuContent(): Selector {

packages/testcafe-models/dataGrid/aiAssistantChat.ts

Lines changed: 12 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -4,28 +4,12 @@ import Button from '../button';
44
import Chat from '../chat';
55

66
const CLASS = {
7-
aiChat: 'dx-ai-chat',
87
aiChatContent: 'dx-ai-chat__content',
98
message: 'dx-ai-chat__message',
109
messagePending: 'dx-ai-chat__message--pending',
1110
messageSuccess: 'dx-ai-chat__message--success',
12-
messageError: 'dx-ai-chat__message--error',
13-
messageIcon: 'dx-ai-chat__message-icon',
14-
messageHeader: 'dx-ai-chat__message-header',
15-
messageHeaderRow: 'dx-ai-chat__message-header-row',
16-
messageContent: 'dx-ai-chat__message-content',
17-
messageStatus: 'dx-ai-chat__message-status',
18-
messageErrorText: 'dx-ai-chat__message-error-text',
19-
messageProgressBar: 'dx-ai-chat__message-progressbar',
20-
messageRegenerateButton: 'dx-ai-chat__message-regenerate-button',
21-
actionList: 'dx-ai-chat__action-list',
2211
actionListItem: 'dx-ai-chat__action-list-item',
23-
actionListItemSuccess: 'dx-ai-chat__action-list-item--success',
24-
actionListItemError: 'dx-ai-chat__action-list-item--error',
25-
actionListItemIcon: 'dx-ai-chat__action-list-item-icon',
26-
actionListItemText: 'dx-ai-chat__action-list-item-text',
2712
closeButton: 'dx-closebutton',
28-
clearChatButton: 'dx-icon-clearhistory',
2913
};
3014

3115
export class AIAssistantChat extends Popup {
@@ -37,18 +21,26 @@ export class AIAssistantChat extends Popup {
3721
return new Chat(this.element.find(`.${CLASS.aiChatContent}`));
3822
}
3923

24+
getInput(): Selector {
25+
return this.getChat().getInput();
26+
}
27+
4028
getCloseButton(): Button {
4129
return new Button(this.element.find(`.${CLASS.closeButton}`));
4230
}
4331

44-
getClearChatButton(): Selector {
45-
return this.element.find(`.${CLASS.clearChatButton}`);
32+
getTitle(): Selector {
33+
return this.topToolbar;
4634
}
4735

48-
getMessages(): Selector {
36+
getAIMessages(): Selector {
4937
return this.element.find(`.${CLASS.message}`);
5038
}
5139

40+
getAIMessage(index: number): Selector {
41+
return this.getAIMessages().nth(index);
42+
}
43+
5244
getPendingMessages(): Selector {
5345
return this.element.find(`.${CLASS.messagePending}`);
5446
}
@@ -57,51 +49,7 @@ export class AIAssistantChat extends Popup {
5749
return this.element.find(`.${CLASS.messageSuccess}`);
5850
}
5951

60-
getErrorMessages(): Selector {
61-
return this.element.find(`.${CLASS.messageError}`);
62-
}
63-
64-
getMessage(index: number): Selector {
65-
return this.getMessages().nth(index);
66-
}
67-
68-
getMessageHeader(index: number): Selector {
69-
return this.getMessage(index).find(`.${CLASS.messageHeader}`);
70-
}
71-
72-
getMessageErrorText(index: number): Selector {
73-
return this.getMessage(index).find(`.${CLASS.messageErrorText}`);
74-
}
75-
76-
getMessageProgressBar(index: number): Selector {
77-
return this.getMessage(index).find(`.${CLASS.messageProgressBar}`);
78-
}
79-
80-
getMessageRegenerateButton(index: number): Selector {
81-
return this.getMessage(index).find(`.${CLASS.messageRegenerateButton}`);
82-
}
83-
84-
getActionList(messageIndex: number): Selector {
85-
return this.getMessage(messageIndex).find(`.${CLASS.actionList}`);
86-
}
87-
8852
getActionItems(messageIndex: number): Selector {
89-
return this.getMessage(messageIndex).find(`.${CLASS.actionListItem}`);
90-
}
91-
92-
getSuccessActionItems(messageIndex: number): Selector {
93-
return this.getMessage(messageIndex).find(`.${CLASS.actionListItemSuccess}`);
94-
}
95-
96-
getErrorActionItems(messageIndex: number): Selector {
97-
return this.getMessage(messageIndex).find(`.${CLASS.actionListItemError}`);
98-
}
99-
100-
getActionItemText(messageIndex: number, actionIndex: number): Selector {
101-
return this.getActionItems(messageIndex).nth(actionIndex).find(`.${CLASS.actionListItemText}`);
102-
}
103-
104-
getActionItemIcon(messageIndex: number, actionIndex: number): Selector {
105-
return this.getActionItems(messageIndex).nth(actionIndex).find(`.${CLASS.actionListItemIcon}`);
53+
return this.getAIMessage(messageIndex).find(`.${CLASS.actionListItem}`);
10654
}
10755
}

0 commit comments

Comments
 (0)