Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -55,14 +55,30 @@ const createAIAssistantView = ({

return undefined;
});
const $columnHeadersElement = $('<div>').appendTo($container);
const $rowsViewElement = $('<div>').css('height', '400px').appendTo($container);

const mockColumnHeadersView = {
getHeight: jest.fn().mockReturnValue(50),
element: jest.fn().mockReturnValue($columnHeadersElement),
};
const mockRowsView = {
element: jest.fn().mockReturnValue($rowsViewElement),
};

const mockComponent = {
element: (): any => $container.get(0),
_createComponent: createComponentMock,
_controllers: {},
_views: {
columnHeadersView: mockColumnHeadersView,
rowsView: mockRowsView,
},
option: optionMock,
};

const aiAssistantView = new AIAssistantView(mockComponent);
aiAssistantView.init();
if (render) {
aiAssistantView.render($container);
}
Expand Down Expand Up @@ -114,13 +130,16 @@ describe('AIAssistantView', () => {
expect(AIChat).toHaveBeenCalledTimes(1);
});

it('should pass container and createComponent to AIChat', () => {
it('should pass container, createComponent, popupOptions, chatOptions, and onChatCleared to AIChat', () => {
const { aiAssistantView } = createAIAssistantView();

expect(AIChat).toHaveBeenCalledWith(
expect.objectContaining({
container: aiAssistantView.element(),
createComponent: expect.any(Function),
popupOptions: expect.any(Object),
chatOptions: expect.any(Object),
onChatCleared: expect.any(Function),
}),
);
});
Expand Down Expand Up @@ -213,20 +232,158 @@ describe('AIAssistantView', () => {
});

describe('visibilityChanged', () => {
it('should fire visibilityChanged callback when popup visibility changes', () => {
it('should fire visibilityChanged callback with true when popup onShowing is triggered', () => {
const { aiAssistantView } = createAIAssistantView();
const callback = jest.fn();

aiAssistantView.visibilityChanged?.add(callback);

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

expect(callback).toHaveBeenCalledWith(true);
});

aiChatConfig.onVisibilityChanged?.(false);
it('should fire visibilityChanged callback with false when popup onHidden is triggered', () => {
const { aiAssistantView } = createAIAssistantView();
const callback = jest.fn();

aiAssistantView.visibilityChanged?.add(callback);

const aiChatConfig = (AIChat as jest.Mock).mock.calls[0][0] as AIChatOptions;
aiChatConfig.popupOptions?.onHidden?.({} as any);

expect(callback).toHaveBeenCalledWith(false);
});
});

describe('optionChanged', () => {
it('should set handled to true for aiAssistant options', () => {
const { aiAssistantView } = createAIAssistantView();

const args = {
name: 'aiAssistant' as const,
fullName: 'aiAssistant.title' as const,
value: 'New Title',
previousValue: 'Old Title',
handled: false,
};

aiAssistantView.optionChanged(args);

expect(args.handled).toBe(true);
});

it('should call _invalidate when aiAssistant.enabled changes to true', () => {
const { aiAssistantView } = createAIAssistantView();
const invalidateSpy = jest.spyOn(aiAssistantView, '_invalidate' as any);

aiAssistantView.optionChanged({
name: 'aiAssistant' as const,
fullName: 'aiAssistant.enabled' as const,
value: true,
previousValue: false,
handled: false,
});

expect(invalidateSpy).toHaveBeenCalledTimes(1);
});

it('should call hide when aiAssistant.enabled changes to false', () => {
const { aiAssistantView, setEnabled } = createAIAssistantView();
const hideSpy = jest.spyOn(aiAssistantView, 'hide');

setEnabled(false);

aiAssistantView.optionChanged({
name: 'aiAssistant' as const,
fullName: 'aiAssistant.enabled' as const,
value: false,
previousValue: true,
handled: false,
});

expect(hideSpy).toHaveBeenCalledTimes(1);
});

it('should call updateOptions on aiChatInstance for title change', () => {
const { aiAssistantView } = createAIAssistantView();

const aiChatInstance = (AIChat as jest.Mock)
.mock.results[0].value as { updateOptions: jest.Mock };

aiAssistantView.optionChanged({
name: 'aiAssistant' as const,
fullName: 'aiAssistant.title' as const,
value: 'New Title',
previousValue: 'Old Title',
handled: false,
});

expect(aiChatInstance.updateOptions).toHaveBeenCalledTimes(1);
expect(aiChatInstance.updateOptions).toHaveBeenCalledWith(
expect.any(Object),
true,
false,
);
});

it('should call updateOptions on aiChatInstance for chat options change', () => {
const { aiAssistantView } = createAIAssistantView();

const aiChatInstance = (AIChat as jest.Mock)
.mock.results[0].value as { updateOptions: jest.Mock };

aiAssistantView.optionChanged({
name: 'aiAssistant' as const,
fullName: 'aiAssistant.chat' as const,
value: { speechToTextEnabled: false },
previousValue: {},
handled: false,
});

expect(aiChatInstance.updateOptions).toHaveBeenCalledTimes(1);
expect(aiChatInstance.updateOptions).toHaveBeenCalledWith(
expect.any(Object),
false,
true,
);
});

it('should call updateOptions with both flags when object value contains title and chat', () => {
const { aiAssistantView } = createAIAssistantView();

const aiChatInstance = (AIChat as jest.Mock)
.mock.results[0].value as { updateOptions: jest.Mock };

aiAssistantView.optionChanged({
name: 'aiAssistant' as const,
fullName: 'aiAssistant' as const,
value: { title: 'New title', chat: { speechToTextEnabled: false } },
previousValue: { title: 'Old title' },
handled: false,
});

expect(aiChatInstance.updateOptions).toHaveBeenCalledTimes(1);
expect(aiChatInstance.updateOptions).toHaveBeenCalledWith(
expect.any(Object),
true,
true,
);
});

it('should not throw when aiChatInstance is not created for non-enabled sub-options', () => {
const { aiAssistantView } = createAIAssistantView({ render: false });

expect(() => {
aiAssistantView.optionChanged({
name: 'aiAssistant' as const,
fullName: 'aiAssistant.title' as const,
value: 'New Title',
previousValue: 'Old Title',
handled: false,
});
}).not.toThrow();
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,14 @@ import { AIAssistantViewController } from '../ai_assistant_view_controller';

interface MockVisibilityChangedCallback {
add: jest.Mock;
remove: jest.Mock;
fire: jest.Mock;
}

interface MockAIAssistantView {
toggle: jest.Mock<() => Promise<boolean>>;
hide: jest.Mock<() => Promise<boolean>>;
_invalidate: jest.Mock;
isShown: jest.Mock<() => boolean>;
visibilityChanged: MockVisibilityChangedCallback;
}

Expand All @@ -31,9 +32,10 @@ interface MockHeaderPanel {
const createMockAIAssistantView = (): MockAIAssistantView => ({
toggle: jest.fn<() => Promise<boolean>>().mockResolvedValue(true),
hide: jest.fn<() => Promise<boolean>>().mockResolvedValue(true),
_invalidate: jest.fn(),
isShown: jest.fn<() => boolean>().mockReturnValue(false),
visibilityChanged: {
add: jest.fn(),
remove: jest.fn(),
fire: jest.fn(),
},
});
Expand Down Expand Up @@ -113,10 +115,21 @@ describe('AIAssistantViewController', () => {
expect(mockView.visibilityChanged.add).toHaveBeenCalledTimes(1);
expect(mockView.visibilityChanged.add).toHaveBeenCalledWith(expect.any(Function));
});

it('should call remove before add to prevent duplicate subscriptions', () => {
const { mockView } = createAIAssistantViewController();

const removeOrder = mockView.visibilityChanged.remove.mock.invocationCallOrder[0];
const addOrder = mockView.visibilityChanged.add.mock.invocationCallOrder[0];

expect(mockView.visibilityChanged.remove).toHaveBeenCalledTimes(1);
expect(mockView.visibilityChanged.remove).toHaveBeenCalledWith(expect.any(Function));
expect(removeOrder).toBeLessThan(addOrder);
});
});

describe('optionChanged', () => {
it('should set handled to true for aiAssistant options', () => {
it('should set handled to true for aiAssistant.enabled option', () => {
const { controller } = createAIAssistantViewController();

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

it('should hide aiAssistantView when aiAssistant.enabled changes to false', () => {
it('should set handled to true for aiAssistant.title option', () => {
const { controller } = createAIAssistantViewController();

const args = {
name: 'aiAssistant' as const,
fullName: 'aiAssistant.title' as const,
value: 'New Title',
previousValue: 'Old Title',
handled: false,
};

controller.optionChanged(args);

expect(args.handled).toBe(true);
});

it('should not set handled for other aiAssistant sub-options', () => {
const { controller } = createAIAssistantViewController();

const args = {
name: 'aiAssistant' as const,
fullName: 'aiAssistant.popup' as const,
value: {},
previousValue: undefined,
handled: false,
};

controller.optionChanged(args);

expect(args.handled).toBe(false);
});

it('should sync toolbar item when aiAssistant.enabled changes to false', () => {
const options: Record<string, unknown> = { 'aiAssistant.enabled': true };
const { controller, mockView } = createAIAssistantViewController(options);
const { controller, mockHeaderPanel } = createAIAssistantViewController(options);

options['aiAssistant.enabled'] = false;

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

expect(mockView.hide).toHaveBeenCalledTimes(1);
expect(mockHeaderPanel.removeToolbarItem).toHaveBeenCalledTimes(1);
});

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

options['aiAssistant.enabled'] = true;

Expand All @@ -163,24 +208,61 @@ describe('AIAssistantViewController', () => {
handled: false,
});

expect(mockView._invalidate).toHaveBeenCalledTimes(1);
expect(mockHeaderPanel.applyToolbarItem).toHaveBeenCalledTimes(1);
});

it('should not invalidate aiAssistantView when disabling', () => {
it('should set handled to true when object value contains enabled', () => {
const options: Record<string, unknown> = { 'aiAssistant.enabled': false };
const { controller, mockHeaderPanel } = createAIAssistantViewController(options);

options['aiAssistant.enabled'] = true;

const args = {
name: 'aiAssistant' as const,
fullName: 'aiAssistant' as const,
value: { enabled: true },
previousValue: { enabled: false },
handled: false,
};

controller.optionChanged(args);

expect(args.handled).toBe(true);
expect(mockHeaderPanel.applyToolbarItem).toHaveBeenCalledTimes(1);
});

it('should set handled to true when object value contains title', () => {
const options: Record<string, unknown> = { 'aiAssistant.enabled': true };
const { controller, mockView } = createAIAssistantViewController(options);
const { controller, mockHeaderPanel } = createAIAssistantViewController(options);

options['aiAssistant.enabled'] = false;
const args = {
name: 'aiAssistant' as const,
fullName: 'aiAssistant' as const,
value: { title: 'New title', chat: { speechToTextEnabled: false } },
previousValue: { title: 'Old title' },
handled: false,
};

controller.optionChanged({
controller.optionChanged(args);

expect(args.handled).toBe(true);
expect(mockHeaderPanel.applyToolbarItem).toHaveBeenCalledTimes(1);
});

it('should not set handled when object value contains only chat/popup options', () => {
const { controller } = createAIAssistantViewController();

const args = {
name: 'aiAssistant' as const,
fullName: 'aiAssistant.enabled' as const,
value: false,
previousValue: true,
fullName: 'aiAssistant' as const,
value: { chat: { speechToTextEnabled: false, showMessageTimestamp: true } },
previousValue: {},
handled: false,
});
};

controller.optionChanged(args);

expect(mockView._invalidate).not.toHaveBeenCalled();
expect(args.handled).toBe(false);
});
});
});
Loading
Loading