@@ -6,13 +6,28 @@ import {
66 it ,
77 jest ,
88} from '@jest/globals' ;
9+ import type {
10+ ExecuteGridAssistantCommandParams ,
11+ ExecuteGridAssistantCommandResult ,
12+ RequestCallbacks ,
13+ Response as SendRequestResult ,
14+ } from '@js/common/ai-integration' ;
15+ import type { dxElementWrapper } from '@js/core/renderer' ;
16+ import $ from '@js/core/renderer' ;
17+ import type { Message } from '@js/ui/chat' ;
18+ import { AIIntegration } from '@ts/core/ai_integration/core/ai_integration' ;
919
1020import {
1121 afterTest ,
1222 beforeTest ,
1323 createDataGrid ,
1424 type DataGridInstance ,
25+ flushAsync ,
1526} from '../../__tests__/__mock__/helpers/utils' ;
27+ import { CLASSES } from '../../ai_chat/const' ;
28+ import { MessageStatus } from '../const' ;
29+ import { GridCommands } from '../grid_commands' ;
30+ import type { CommandResult } from '../types' ;
1631
1732const AI_ASSISTANT_BUTTON_SELECTOR = '.dx-datagrid-ai-assistant-button' ;
1833const HIDDEN_CLASS = 'dx-hidden' ;
@@ -33,6 +48,44 @@ const isAiAssistantButtonVisible = (instance: DataGridInstance): boolean => {
3348 return ! button . closest ( `.${ HIDDEN_CLASS } ` ) ;
3449} ;
3550
51+ const createMockAIIntegration = ( ) : {
52+ aiIntegration : AIIntegration ;
53+ getLastCallbacks : ( ) => RequestCallbacks < ExecuteGridAssistantCommandResult > ;
54+ } => {
55+ let lastCallbacks : RequestCallbacks < ExecuteGridAssistantCommandResult > = { } ;
56+
57+ const aiIntegration = new AIIntegration ( {
58+ sendRequest ( ) : SendRequestResult {
59+ return {
60+ promise : Promise . resolve ( '{}' ) ,
61+ abort : jest . fn ( ) ,
62+ } ;
63+ } ,
64+ } ) ;
65+
66+ aiIntegration . executeGridAssistant = jest . fn ( (
67+ params : ExecuteGridAssistantCommandParams ,
68+ callbacks : RequestCallbacks < ExecuteGridAssistantCommandResult > ,
69+ ) : ( ( ) => void ) => {
70+ lastCallbacks = callbacks ;
71+ return jest . fn ( ) ;
72+ } ) ;
73+
74+ return {
75+ aiIntegration,
76+ getLastCallbacks : ( ) => lastCallbacks ,
77+ } ;
78+ } ;
79+
80+ const findMessageElements = ( ) : dxElementWrapper => $ ( `.${ CLASSES . aiChat } ` ) . find ( `.${ CLASSES . message } ` ) ;
81+
82+ const getMessageStatusClass = ( $message : dxElementWrapper ) : string => {
83+ if ( $message . hasClass ( CLASSES . messagePending ) ) return MessageStatus . Pending ;
84+ if ( $message . hasClass ( CLASSES . messageSuccess ) ) return MessageStatus . Success ;
85+ if ( $message . hasClass ( CLASSES . messageError ) ) return MessageStatus . Failure ;
86+ return '' ;
87+ } ;
88+
3689describe ( 'AIAssistantViewController' , ( ) => {
3790 beforeEach ( beforeTest ) ;
3891 afterEach ( afterTest ) ;
@@ -128,4 +181,181 @@ describe('AIAssistantViewController', () => {
128181 expect ( button ?. getAttribute ( 'title' ) ) . toBe ( 'My Custom Title' ) ;
129182 } ) ;
130183 } ) ;
184+
185+ describe ( 'message rendering' , ( ) => {
186+ // eslint-disable-next-line @typescript-eslint/init-declarations
187+ let validateSpy ;
188+ // eslint-disable-next-line @typescript-eslint/init-declarations
189+ let executeCommandsSpy ;
190+
191+ beforeEach ( ( ) => {
192+ validateSpy = jest . spyOn ( GridCommands . prototype , 'validate' )
193+ . mockReturnValue ( true ) ;
194+ executeCommandsSpy = jest . spyOn ( GridCommands . prototype , 'executeCommands' )
195+ . mockResolvedValue ( [
196+ { status : 'success' , message : 'Sorted by Name ascending' } ,
197+ ] as CommandResult [ ] ) ;
198+ } ) ;
199+
200+ afterEach ( ( ) => {
201+ validateSpy . mockRestore ( ) ;
202+ executeCommandsSpy . mockRestore ( ) ;
203+ } ) ;
204+
205+ const createDataGridWithAIAssistant = async ( ) : Promise < {
206+ instance : DataGridInstance ;
207+ getLastCallbacks : ( ) => RequestCallbacks < ExecuteGridAssistantCommandResult > ;
208+ } > => {
209+ const { aiIntegration, getLastCallbacks } = createMockAIIntegration ( ) ;
210+
211+ const { instance } = await createDataGrid ( {
212+ dataSource : [
213+ { id : 1 , name : 'Name 1' } ,
214+ { id : 2 , name : 'Name 2' } ,
215+ ] ,
216+ columns : [
217+ { dataField : 'id' , caption : 'ID' , dataType : 'number' } ,
218+ { dataField : 'name' , caption : 'Name' , dataType : 'string' } ,
219+ ] ,
220+ aiAssistant : { enabled : true , aiIntegration, title : 'AI Assistant' } ,
221+ } ) ;
222+
223+ // Open the AI assistant popup so Chat renders into the DOM
224+ const viewController = instance . getController ( 'aiAssistantViewController' ) ;
225+
226+ await viewController . toggle ( ) ;
227+ jest . runAllTimers ( ) ;
228+
229+ return { instance, getLastCallbacks } ;
230+ } ;
231+
232+ const sendAIRequest = (
233+ instance : DataGridInstance ,
234+ text : string ,
235+ ) : void => {
236+ const controller = instance . getController ( 'aiAssistant' ) ;
237+
238+ controller . sendRequestToAI ( {
239+ author : { id : 'user' , name : 'User' } ,
240+ text,
241+ timestamp : new Date ( ) . toISOString ( ) ,
242+ } as Message ) . catch ( ( ) => { } ) ;
243+ jest . runAllTimers ( ) ;
244+ } ;
245+
246+ it ( 'should render pending message after sendRequestToAI' , async ( ) => {
247+ const { instance } = await createDataGridWithAIAssistant ( ) ;
248+
249+ sendAIRequest ( instance , 'Sort by Name' ) ;
250+
251+ const $messages = findMessageElements ( ) ;
252+
253+ expect ( $messages . length ) . toBe ( 1 ) ;
254+ expect ( getMessageStatusClass ( $messages . eq ( 0 ) ) ) . toBe ( MessageStatus . Pending ) ;
255+ } ) ;
256+
257+ it ( 'should render success message with command list after AI completes' , async ( ) => {
258+ const { instance, getLastCallbacks } = await createDataGridWithAIAssistant ( ) ;
259+
260+ sendAIRequest ( instance , 'Sort by Name' ) ;
261+
262+ getLastCallbacks ( ) . onComplete ?.( {
263+ actions : [ { name : 'sort' , args : { column : 'Name' } } ] ,
264+ } ) ;
265+ await flushAsync ( ) ;
266+ await flushAsync ( ) ;
267+
268+ const $messages = findMessageElements ( ) ;
269+
270+ expect ( $messages . length ) . toBe ( 1 ) ;
271+ expect ( getMessageStatusClass ( $messages . eq ( 0 ) ) ) . toBe ( MessageStatus . Success ) ;
272+
273+ const $commandItems = $messages . eq ( 0 ) . find ( `.${ CLASSES . actionListItem } ` ) ;
274+
275+ expect ( $commandItems . length ) . toBe ( 1 ) ;
276+ expect ( $commandItems . find ( `.${ CLASSES . actionListItemText } ` ) . text ( ) )
277+ . toBe ( 'Sorted by Name ascending' ) ;
278+ } ) ;
279+
280+ it ( 'should render failure message with error text after AI errors' , async ( ) => {
281+ const { instance, getLastCallbacks } = await createDataGridWithAIAssistant ( ) ;
282+
283+ sendAIRequest ( instance , 'Sort by Name' ) ;
284+
285+ getLastCallbacks ( ) . onError ?.( new Error ( 'Network error' ) ) ;
286+ await flushAsync ( ) ;
287+
288+ const $messages = findMessageElements ( ) ;
289+
290+ expect ( $messages . length ) . toBe ( 1 ) ;
291+ expect ( getMessageStatusClass ( $messages . eq ( 0 ) ) ) . toBe ( MessageStatus . Failure ) ;
292+ expect ( $messages . eq ( 0 ) . find ( `.${ CLASSES . messageErrorText } ` ) . text ( ) )
293+ . toBe ( 'Network error' ) ;
294+ } ) ;
295+
296+ it ( 'should render multiple messages with correct statuses after sequential requests' , async ( ) => {
297+ const { instance, getLastCallbacks } = await createDataGridWithAIAssistant ( ) ;
298+
299+ // First request
300+ sendAIRequest ( instance , 'Sort by Name' ) ;
301+
302+ const firstCallbacks = getLastCallbacks ( ) ;
303+
304+ firstCallbacks . onComplete ?.( {
305+ actions : [ { name : 'sort' , args : { column : 'Name' } } ] ,
306+ } ) ;
307+ await flushAsync ( ) ;
308+ await flushAsync ( ) ;
309+
310+ // Second request
311+ sendAIRequest ( instance , 'Filter by Status' ) ;
312+
313+ const $messages = findMessageElements ( ) ;
314+
315+ expect ( $messages . length ) . toBe ( 2 ) ;
316+ expect ( getMessageStatusClass ( $messages . eq ( 0 ) ) ) . toBe ( MessageStatus . Success ) ;
317+ expect ( $messages . eq ( 0 ) . find ( `.${ CLASSES . actionListItem } ` ) . length ) . toBe ( 1 ) ;
318+ expect ( getMessageStatusClass ( $messages . eq ( 1 ) ) ) . toBe ( MessageStatus . Pending ) ;
319+ } ) ;
320+
321+ it ( 'should only re-render the updated message' , async ( ) => {
322+ const { instance, getLastCallbacks } = await createDataGridWithAIAssistant ( ) ;
323+
324+ // First request — complete it
325+ sendAIRequest ( instance , 'Sort by Name' ) ;
326+
327+ const firstCallbacks = getLastCallbacks ( ) ;
328+
329+ firstCallbacks . onComplete ?.( {
330+ actions : [ { name : 'sort' , args : { column : 'Name' } } ] ,
331+ } ) ;
332+ await flushAsync ( ) ;
333+ await flushAsync ( ) ;
334+
335+ // Second request — pending
336+ sendAIRequest ( instance , 'Filter by Status' ) ;
337+
338+ const $messagesBefore = findMessageElements ( ) ;
339+
340+ expect ( $messagesBefore . length ) . toBe ( 2 ) ;
341+
342+ const firstMessageNode = $messagesBefore . get ( 0 ) ;
343+
344+ // Complete second request — only message 2 should re-render
345+ const secondCallbacks = getLastCallbacks ( ) ;
346+
347+ secondCallbacks . onComplete ?.( {
348+ actions : [ { name : 'filter' , args : { column : 'Status' } } ] ,
349+ } ) ;
350+ await flushAsync ( ) ;
351+ await flushAsync ( ) ;
352+
353+ const $messagesAfter = findMessageElements ( ) ;
354+
355+ expect ( $messagesAfter . length ) . toBe ( 2 ) ;
356+ expect ( $messagesAfter . get ( 0 ) ) . toBe ( firstMessageNode ) ;
357+ expect ( getMessageStatusClass ( $messagesAfter . eq ( 1 ) ) )
358+ . toBe ( MessageStatus . Success ) ;
359+ } ) ;
360+ } ) ;
131361} ) ;
0 commit comments