Skip to content

Commit 78477dc

Browse files
committed
AIChat: adjust tests
1 parent ccf3cce commit 78477dc

File tree

5 files changed

+248
-65
lines changed

5 files changed

+248
-65
lines changed

packages/devextreme/js/__internal/grids/grid_core/ai_assistant/__tests__/ai_assistant_view.test.ts

Lines changed: 110 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -55,14 +55,30 @@ const createAIAssistantView = ({
5555

5656
return undefined;
5757
});
58+
const $columnHeadersElement = $('<div>').appendTo($container);
59+
const $rowsViewElement = $('<div>').css('height', '400px').appendTo($container);
60+
61+
const mockColumnHeadersView = {
62+
getHeight: jest.fn().mockReturnValue(50),
63+
element: jest.fn().mockReturnValue($columnHeadersElement),
64+
};
65+
const mockRowsView = {
66+
element: jest.fn().mockReturnValue($rowsViewElement),
67+
};
68+
5869
const mockComponent = {
5970
element: (): any => $container.get(0),
6071
_createComponent: createComponentMock,
6172
_controllers: {},
73+
_views: {
74+
columnHeadersView: mockColumnHeadersView,
75+
rowsView: mockRowsView,
76+
},
6277
option: optionMock,
6378
};
6479

6580
const aiAssistantView = new AIAssistantView(mockComponent);
81+
aiAssistantView.init();
6682
if (render) {
6783
aiAssistantView.render($container);
6884
}
@@ -114,13 +130,16 @@ describe('AIAssistantView', () => {
114130
expect(AIChat).toHaveBeenCalledTimes(1);
115131
});
116132

117-
it('should pass container and createComponent to AIChat', () => {
133+
it('should pass container, createComponent, popupOptions, chatOptions, and onChatCleared to AIChat', () => {
118134
const { aiAssistantView } = createAIAssistantView();
119135

120136
expect(AIChat).toHaveBeenCalledWith(
121137
expect.objectContaining({
122138
container: aiAssistantView.element(),
123139
createComponent: expect.any(Function),
140+
popupOptions: expect.any(Object),
141+
chatOptions: expect.any(Object),
142+
onChatCleared: expect.any(Function),
124143
}),
125144
);
126145
});
@@ -213,20 +232,107 @@ describe('AIAssistantView', () => {
213232
});
214233

215234
describe('visibilityChanged', () => {
216-
it('should fire visibilityChanged callback when popup visibility changes', () => {
235+
it('should fire visibilityChanged callback with true when popup onShowing is triggered', () => {
217236
const { aiAssistantView } = createAIAssistantView();
218237
const callback = jest.fn();
219238

220239
aiAssistantView.visibilityChanged?.add(callback);
221240

222241
const aiChatConfig = (AIChat as jest.Mock).mock.calls[0][0] as AIChatOptions;
223-
aiChatConfig.onVisibilityChanged?.(true);
242+
aiChatConfig.popupOptions?.onShowing?.({} as any);
224243

225244
expect(callback).toHaveBeenCalledWith(true);
245+
});
246+
247+
it('should fire visibilityChanged callback with false when popup onHidden is triggered', () => {
248+
const { aiAssistantView } = createAIAssistantView();
249+
const callback = jest.fn();
250+
251+
aiAssistantView.visibilityChanged?.add(callback);
226252

227-
aiChatConfig.onVisibilityChanged?.(false);
253+
const aiChatConfig = (AIChat as jest.Mock).mock.calls[0][0] as AIChatOptions;
254+
aiChatConfig.popupOptions?.onHidden?.({} as any);
228255

229256
expect(callback).toHaveBeenCalledWith(false);
230257
});
231258
});
259+
260+
describe('optionChanged', () => {
261+
it('should set handled to true for aiAssistant options', () => {
262+
const { aiAssistantView } = createAIAssistantView();
263+
264+
const args = {
265+
name: 'aiAssistant' as const,
266+
fullName: 'aiAssistant.title' as const,
267+
value: 'New Title',
268+
previousValue: 'Old Title',
269+
handled: false,
270+
};
271+
272+
aiAssistantView.optionChanged(args);
273+
274+
expect(args.handled).toBe(true);
275+
});
276+
277+
it('should call _invalidate when aiAssistant.enabled changes to true', () => {
278+
const { aiAssistantView } = createAIAssistantView();
279+
const invalidateSpy = jest.spyOn(aiAssistantView, '_invalidate' as any);
280+
281+
aiAssistantView.optionChanged({
282+
name: 'aiAssistant' as const,
283+
fullName: 'aiAssistant.enabled' as const,
284+
value: true,
285+
previousValue: false,
286+
handled: false,
287+
});
288+
289+
expect(invalidateSpy).toHaveBeenCalledTimes(1);
290+
});
291+
292+
it('should call hide when aiAssistant.enabled changes to false', () => {
293+
const { aiAssistantView } = createAIAssistantView();
294+
const hideSpy = jest.spyOn(aiAssistantView, 'hide');
295+
296+
aiAssistantView.optionChanged({
297+
name: 'aiAssistant' as const,
298+
fullName: 'aiAssistant.enabled' as const,
299+
value: false,
300+
previousValue: true,
301+
handled: false,
302+
});
303+
304+
expect(hideSpy).toHaveBeenCalledTimes(1);
305+
});
306+
307+
it('should call updateOptions on aiChatInstance for non-enabled sub-options', () => {
308+
const { aiAssistantView } = createAIAssistantView();
309+
310+
const aiChatInstance = (AIChat as jest.Mock)
311+
.mock.results[0].value as { updateOptions: jest.Mock };
312+
313+
aiAssistantView.optionChanged({
314+
name: 'aiAssistant' as const,
315+
fullName: 'aiAssistant.title' as const,
316+
value: 'New Title',
317+
previousValue: 'Old Title',
318+
handled: false,
319+
});
320+
321+
expect(aiChatInstance.updateOptions).toHaveBeenCalledTimes(1);
322+
});
323+
324+
it('should not throw when aiChatInstance is not created for non-enabled sub-options', () => {
325+
const { aiAssistantView } = createAIAssistantView({ render: false });
326+
327+
expect(() => {
328+
aiAssistantView.optionChanged({
329+
name: 'aiAssistant' as const,
330+
fullName: 'aiAssistant.title' as const,
331+
value: 'New Title',
332+
previousValue: 'Old Title',
333+
handled: false,
334+
});
335+
}).not.toThrow();
336+
});
337+
});
232338
});

packages/devextreme/js/__internal/grids/grid_core/ai_assistant/__tests__/ai_assistant_view_controller.test.ts

Lines changed: 54 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,14 @@ import { AIAssistantViewController } from '../ai_assistant_view_controller';
1111

1212
interface MockVisibilityChangedCallback {
1313
add: jest.Mock;
14+
remove: jest.Mock;
1415
fire: jest.Mock;
1516
}
1617

1718
interface MockAIAssistantView {
1819
toggle: jest.Mock<() => Promise<boolean>>;
1920
hide: jest.Mock<() => Promise<boolean>>;
20-
_invalidate: jest.Mock;
21+
isShown: jest.Mock<() => boolean>;
2122
visibilityChanged: MockVisibilityChangedCallback;
2223
}
2324

@@ -31,9 +32,10 @@ interface MockHeaderPanel {
3132
const createMockAIAssistantView = (): MockAIAssistantView => ({
3233
toggle: jest.fn<() => Promise<boolean>>().mockResolvedValue(true),
3334
hide: jest.fn<() => Promise<boolean>>().mockResolvedValue(true),
34-
_invalidate: jest.fn(),
35+
isShown: jest.fn<() => boolean>().mockReturnValue(false),
3536
visibilityChanged: {
3637
add: jest.fn(),
38+
remove: jest.fn(),
3739
fire: jest.fn(),
3840
},
3941
});
@@ -113,10 +115,21 @@ describe('AIAssistantViewController', () => {
113115
expect(mockView.visibilityChanged.add).toHaveBeenCalledTimes(1);
114116
expect(mockView.visibilityChanged.add).toHaveBeenCalledWith(expect.any(Function));
115117
});
118+
119+
it('should call remove before add to prevent duplicate subscriptions', () => {
120+
const { mockView } = createAIAssistantViewController();
121+
122+
const removeOrder = mockView.visibilityChanged.remove.mock.invocationCallOrder[0];
123+
const addOrder = mockView.visibilityChanged.add.mock.invocationCallOrder[0];
124+
125+
expect(mockView.visibilityChanged.remove).toHaveBeenCalledTimes(1);
126+
expect(mockView.visibilityChanged.remove).toHaveBeenCalledWith(expect.any(Function));
127+
expect(removeOrder).toBeLessThan(addOrder);
128+
});
116129
});
117130

118131
describe('optionChanged', () => {
119-
it('should set handled to true for aiAssistant options', () => {
132+
it('should set handled to true for aiAssistant.enabled option', () => {
120133
const { controller } = createAIAssistantViewController();
121134

122135
const args = {
@@ -132,9 +145,41 @@ describe('AIAssistantViewController', () => {
132145
expect(args.handled).toBe(true);
133146
});
134147

135-
it('should hide aiAssistantView when aiAssistant.enabled changes to false', () => {
148+
it('should set handled to true for aiAssistant.title option', () => {
149+
const { controller } = createAIAssistantViewController();
150+
151+
const args = {
152+
name: 'aiAssistant' as const,
153+
fullName: 'aiAssistant.title' as const,
154+
value: 'New Title',
155+
previousValue: 'Old Title',
156+
handled: false,
157+
};
158+
159+
controller.optionChanged(args);
160+
161+
expect(args.handled).toBe(true);
162+
});
163+
164+
it('should not set handled for other aiAssistant sub-options', () => {
165+
const { controller } = createAIAssistantViewController();
166+
167+
const args = {
168+
name: 'aiAssistant' as const,
169+
fullName: 'aiAssistant.popup' as const,
170+
value: {},
171+
previousValue: undefined,
172+
handled: false,
173+
};
174+
175+
controller.optionChanged(args);
176+
177+
expect(args.handled).toBe(false);
178+
});
179+
180+
it('should sync toolbar item when aiAssistant.enabled changes to false', () => {
136181
const options: Record<string, unknown> = { 'aiAssistant.enabled': true };
137-
const { controller, mockView } = createAIAssistantViewController(options);
182+
const { controller, mockHeaderPanel } = createAIAssistantViewController(options);
138183

139184
options['aiAssistant.enabled'] = false;
140185

@@ -146,12 +191,12 @@ describe('AIAssistantViewController', () => {
146191
handled: false,
147192
});
148193

149-
expect(mockView.hide).toHaveBeenCalledTimes(1);
194+
expect(mockHeaderPanel.removeToolbarItem).toHaveBeenCalledTimes(1);
150195
});
151196

152-
it('should invalidate aiAssistantView when enabling', () => {
197+
it('should sync toolbar item when aiAssistant.enabled changes to true', () => {
153198
const options: Record<string, unknown> = { 'aiAssistant.enabled': false };
154-
const { controller, mockView } = createAIAssistantViewController(options);
199+
const { controller, mockHeaderPanel } = createAIAssistantViewController(options);
155200

156201
options['aiAssistant.enabled'] = true;
157202

@@ -163,24 +208,7 @@ describe('AIAssistantViewController', () => {
163208
handled: false,
164209
});
165210

166-
expect(mockView._invalidate).toHaveBeenCalledTimes(1);
167-
});
168-
169-
it('should not invalidate aiAssistantView when disabling', () => {
170-
const options: Record<string, unknown> = { 'aiAssistant.enabled': true };
171-
const { controller, mockView } = createAIAssistantViewController(options);
172-
173-
options['aiAssistant.enabled'] = false;
174-
175-
controller.optionChanged({
176-
name: 'aiAssistant' as const,
177-
fullName: 'aiAssistant.enabled' as const,
178-
value: false,
179-
previousValue: true,
180-
handled: false,
181-
});
182-
183-
expect(mockView._invalidate).not.toHaveBeenCalled();
211+
expect(mockHeaderPanel.applyToolbarItem).toHaveBeenCalledTimes(1);
184212
});
185213
});
186214
});

packages/devextreme/js/__internal/grids/grid_core/ai_assistant/ai_assistant_view_controller.ts

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -27,18 +27,20 @@ export class AIAssistantViewController extends ViewController {
2727
this.aiAssistantView = this.getView('aiAssistantView');
2828
this.headerPanel = this.getView('headerPanel');
2929

30-
// todo add remove before add
31-
this.aiAssistantView.visibilityChanged?.add((visible: boolean): void => {
32-
this.getAiAssistantButton()?.toggleClass(ACTIVE_STATE_CLASS, visible);
33-
});
34-
35-
const isAiAssistantEnabled = this.option('aiAssistant.enabled'); // TODO clarify option name
30+
const isAiAssistantEnabled = this.option('aiAssistant.enabled');
3631

3732
if (isAiAssistantEnabled) {
3833
const aiAssistantToolbarItem = this.createAiAssistantToolbarItem();
3934

4035
this.headerPanel?.registerToolbarItem(AI_ASSISTANT_BUTTON_NAME, aiAssistantToolbarItem);
4136
}
37+
38+
const visibilityChangedHandler = (visible: boolean): void => {
39+
this.getAiAssistantButton()?.toggleClass(ACTIVE_STATE_CLASS, visible);
40+
};
41+
42+
this.aiAssistantView?.visibilityChanged?.remove(visibilityChangedHandler);
43+
this.aiAssistantView.visibilityChanged?.add(visibilityChangedHandler);
4244
}
4345

4446
public optionChanged(args: OptionChanged): void {
@@ -57,7 +59,7 @@ export class AIAssistantViewController extends ViewController {
5759
}
5860

5961
private syncAiAssistantItem(): void {
60-
const isAiAssistantEnabled = this.option('aiAssistant.enabled'); // TODO clarify option name
62+
const isAiAssistantEnabled = this.option('aiAssistant.enabled');
6163

6264
if (isAiAssistantEnabled) {
6365
const aiAssistantToolbarItem = this.createAiAssistantToolbarItem();
@@ -71,14 +73,14 @@ export class AIAssistantViewController extends ViewController {
7173
private createAiAssistantToolbarItem(): ToolbarItem {
7274
const onClickHandler = (): Promise<boolean> => this.toggle();
7375

74-
const hintText = this.option('aiAssistant.title'); // TODO clarify option name
76+
const hintText = this.option('aiAssistant.title');
7577

76-
const isShown = this.aiAssistantView?.isShown();
78+
const isActive = this.aiAssistantView?.isShown();
7779

7880
const aiAssistantToolbarItemClass = this.headerPanel?.getToolbarButtonClass(
7981
this.addWidgetPrefix(CLASSES.aiAssistantButton),
8082
);
81-
const aiAssistantToolbarItemStateClass = isShown ? ACTIVE_STATE_CLASS : '';
83+
const aiAssistantToolbarItemStateClass = isActive ? ACTIVE_STATE_CLASS : '';
8284

8385
return {
8486
widget: 'dxButton',

0 commit comments

Comments
 (0)