Skip to content
Open
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 @@ -1041,4 +1041,8 @@
left: 0;
transform: translateY(-50%);
}

.dx-#{$widget-name}-ai-assistant-confirm-dialog .dx-dialog-button {
width: $grid-ai-assistant-confirm-dialog-button-width;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ $grid-column-header-indicator-width: 14px;
$grid-text-content-margin: 3px;
$grid-sort-index-width: 12px;
$grid-sort-index-offset: 3px;
$grid-ai-assistant-confirm-dialog-button-width: 50px;
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,75 @@ describe('AIAssistantController', () => {
});
});

describe('isProcessing', () => {
it('should return false initially', () => {
const controller = createController();

expect(controller.isProcessing()).toBe(false);
});

it('should return true while request is processing', () => {
const controller = createController();

// eslint-disable-next-line @typescript-eslint/no-floating-promises
controller.sendRequestToAI({
author: { id: 'user', name: 'User' },
text: 'Sort by name',
timestamp: '2026-04-16T10:00:00.000Z',
} as Message);

expect(controller.isProcessing()).toBe(true);
});

it('should return false after request completes successfully', async () => {
const controller = createController();

const promise = controller.sendRequestToAI({
author: { id: 'user', name: 'User' },
text: 'Sort by name',
timestamp: '2026-04-16T10:00:00.000Z',
} as Message);

const actions = [{ name: 'sort', args: { column: 'Name' } }];
sendRequestCallbacks.onComplete?.({ actions });
await promise;

expect(controller.isProcessing()).toBe(false);
});

it('should return false after request fails', async () => {
const controller = createController();

const promise = controller.sendRequestToAI({
author: { id: 'user', name: 'User' },
text: 'Sort by name',
timestamp: '2026-04-16T10:00:00.000Z',
} as Message);
promise.catch(() => {});

sendRequestCallbacks.onError?.(new Error('Network error'));
await expect(promise).rejects.toThrow('Network error');

expect(controller.isProcessing()).toBe(false);
});

it('should return false after request is aborted', async () => {
const controller = createController();

const promise = controller.sendRequestToAI({
author: { id: 'user', name: 'User' },
text: 'Sort by name',
timestamp: '2026-04-16T10:00:00.000Z',
} as Message);
promise.catch(() => {});

controller.abortRequest();
await expect(promise).rejects.toThrow();

expect(controller.isProcessing()).toBe(false);
});
});

describe('sendRequestToAI', () => {
it('should create pending message in store', async () => {
const controller = createController();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,16 @@ import wrapInstanceWithMocks from '@ts/grids/grid_core/__tests__/__mock__/helper
import { AIChat } from '../../ai_chat/ai_chat';
import type { AIChatOptions } from '../../ai_chat/types';
import { AIAssistantView } from '../ai_assistant_view';
import { createConfirmDialog } from '../utils';

jest.mock('../utils', (): any => {
const original = jest.requireActual<any>('../utils');

return {
...original,
createConfirmDialog: jest.fn(),
};
});

jest.mock('../../ai_chat/ai_chat', (): any => {
const original = jest.requireActual<any>('../../ai_chat/ai_chat');
Expand All @@ -40,6 +50,7 @@ const mockAIAssistantController = {
getMessageStore: jest.fn().mockReturnValue(mockMessageStore),
sendRequestToAI: jest.fn(),
abortRequest: jest.fn(),
isProcessing: jest.fn().mockReturnValue(false),
};

const createAIAssistantView = ({
Expand Down Expand Up @@ -75,6 +86,7 @@ const createAIAssistantView = ({
};

const mockComponent = {
NAME: 'dxDataGrid',
element: (): any => $container.get(0),
_createComponent: createComponentMock,
_controllers: {
Expand Down Expand Up @@ -301,14 +313,98 @@ describe('AIAssistantView', () => {
});
});

describe('onHidden', () => {
it('should call abortRequest on controller when popup onHidden is triggered', () => {
describe('onHiding', () => {
it('should not cancel hiding when controller is not processing', () => {
mockAIAssistantController.isProcessing.mockReturnValue(false);
createAIAssistantView();

const aiChatConfig = (AIChat as jest.Mock).mock.calls[0][0] as AIChatOptions;
aiChatConfig.popupOptions?.onHidden?.({} as any);
const event = { cancel: false, component: { hide: jest.fn() } };

aiChatConfig.popupOptions?.onHiding?.(event as any);

expect(event.cancel).toBe(false);
expect(createConfirmDialog).not.toHaveBeenCalled();
});

it('should cancel hiding and show confirm dialog when controller is processing', () => {
mockAIAssistantController.isProcessing.mockReturnValue(true);

const mockDialog = {
show: jest.fn().mockReturnValue({
done: jest.fn(),
}),
};
(createConfirmDialog as jest.Mock).mockReturnValue(mockDialog);
createAIAssistantView();

const aiChatConfig = (AIChat as jest.Mock).mock.calls[0][0] as AIChatOptions;
const event = { cancel: false, component: { hide: jest.fn() } };

aiChatConfig.popupOptions?.onHiding?.(event as any);

expect(event.cancel).toBe(true);
expect(createConfirmDialog).toHaveBeenCalledTimes(1);
expect(createConfirmDialog).toHaveBeenCalledWith(
expect.objectContaining({
popupOptions: expect.objectContaining({
elementAttr: expect.objectContaining({
class: expect.stringContaining('ai-assistant-confirm-dialog'),
}),
}),
}),
);
expect(mockDialog.show).toHaveBeenCalledTimes(1);
});

it('should abort request and hide popup when confirm result is true', () => {
mockAIAssistantController.isProcessing.mockReturnValue(true);

let doneCallback: (result: boolean) => void = () => {};
const mockDialog = {
show: jest.fn().mockReturnValue({
done: jest.fn((cb: (result: boolean) => void) => {
doneCallback = cb;
}),
}),
};
(createConfirmDialog as jest.Mock).mockReturnValue(mockDialog);
createAIAssistantView();

const aiChatConfig = (AIChat as jest.Mock).mock.calls[0][0] as AIChatOptions;
const hideMock = jest.fn();
const event = { cancel: false, component: { hide: hideMock } };

aiChatConfig.popupOptions?.onHiding?.(event as any);
doneCallback(true);

expect(mockAIAssistantController.abortRequest).toHaveBeenCalledTimes(1);
expect(hideMock).toHaveBeenCalledTimes(1);
});

it('should not abort request when confirm result is false', () => {
mockAIAssistantController.isProcessing.mockReturnValue(true);

let doneCallback: (result: boolean) => void = () => {};
const mockDialog = {
show: jest.fn().mockReturnValue({
done: jest.fn((cb: (result: boolean) => void) => {
doneCallback = cb;
}),
}),
};
(createConfirmDialog as jest.Mock).mockReturnValue(mockDialog);
createAIAssistantView();

const aiChatConfig = (AIChat as jest.Mock).mock.calls[0][0] as AIChatOptions;
const hideMock = jest.fn();
const event = { cancel: false, component: { hide: hideMock } };

aiChatConfig.popupOptions?.onHiding?.(event as any);
doneCallback(false);

expect(mockAIAssistantController.abortRequest).not.toHaveBeenCalled();
expect(hideMock).not.toHaveBeenCalled();
});
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -390,8 +390,17 @@ describe('AIAssistantViewController', () => {

const viewController = instance.getController('aiAssistantViewController');

// Close the AI assistant popup
await viewController.toggle();
// Close the AI assistant popup — triggers confirm dialog because request is processing.
// toggle() rejects with undefined when onHiding cancels the hide.
await viewController.toggle().catch(() => {});
jest.runAllTimers();
await flushAsync();

// Confirm abort by clicking "Yes" in the confirmation dialog
const confirmDialogSelector = '.dx-datagrid-ai-assistant-confirm-dialog';
const yesButton = document.querySelectorAll(`${confirmDialogSelector} .dx-button`)[1] as HTMLElement;

yesButton.click();
jest.runAllTimers();
await flushAsync();

Expand Down
Loading
Loading