Skip to content
Merged
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 @@ -72,6 +72,7 @@ describe('AIAssistantController', () => {
() => ({
validate: jest.fn().mockReturnValue(true),
executeCommands: jest.fn<() => Promise<CommandResult[]>>().mockResolvedValue([{ status: 'success', message: 'sort' }]),
abort: jest.fn(),
}),
);
});
Expand Down Expand Up @@ -274,4 +275,158 @@ describe('AIAssistantController', () => {
await expect(promise).rejects.toThrow('Default error message');
});
});

describe('isProcessing', () => {
it('should return false by default', () => {
const controller = createController({
'aiAssistant.aiIntegration': mockAIIntegration,
});

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

it('should return true after sendRequestToAI is called', () => {
const controller = createController({
'aiAssistant.aiIntegration': mockAIIntegration,
});

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

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

it('should return false after successful command completion', async () => {
const controller = createController({
'aiAssistant.aiIntegration': mockAIIntegration,
});

const promise = controller.sendRequestToAI({
author: { id: 'user', name: 'User' },
text: 'Generate values',
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 onError callback', async () => {
const controller = createController({
'aiAssistant.aiIntegration': mockAIIntegration,
});

const promise = controller.sendRequestToAI({
author: { id: 'user', name: 'User' },
text: 'Generate values',
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 failed command processing', async () => {
const controller = createController({
'aiAssistant.aiIntegration': mockAIIntegration,
});

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

sendRequestCallbacks.onComplete?.({} as ExecuteGridAssistantCommandResult);

await expect(promise).rejects.toThrow('Default error message');

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

describe('abortRequest', () => {
it('should fail message with abort error when request is aborted', async () => {
const controller = createController({
'aiAssistant.aiIntegration': mockAIIntegration,
});

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

controller.abortRequest();

const messages = await getStore(controller).load();

expect(messages).toEqual([
expect.objectContaining({
status: MessageStatus.Failure,
headerText: 'Failed to process request',
text: MessageStatus.Failure,
errorText: 'Request stopped.',
}),
]);

await expect(promise).rejects.toThrow('Request stopped.');
});

it('should set isProcessing to false when request is aborted', async () => {
const controller = createController({
'aiAssistant.aiIntegration': mockAIIntegration,
});

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

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

controller.abortRequest();

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

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

it('should call gridCommands.abort when request is aborted', async () => {
const controller = createController({
'aiAssistant.aiIntegration': mockAIIntegration,
});

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

const gridCommandsInstance = MockedGridCommands.mock.results[0].value as { abort: jest.Mock };

controller.abortRequest();

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

expect(gridCommandsInstance.abort).toHaveBeenCalledTimes(1);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,106 @@ describe('AIAssistantIntegrationController', () => {
});
});

describe('onAbort callback', () => {
it('should call onAbort callback when request is aborted', async () => {
const onAbort = jest.fn();
const aiIntegration = createMockAIIntegration();

const controller = await createController({
aiAssistant: { enabled: true, aiIntegration },
});

controller.sendRequest('Sort by name', {
onComplete: jest.fn(),
onError: jest.fn(),
onAbort,
});

controller.abortRequest();

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

it('should call onAbort callback when new request aborts previous one', async () => {
const onAbort = jest.fn();
const aiIntegration = createMockAIIntegration();

const controller = await createController({
aiAssistant: { enabled: true, aiIntegration },
});

controller.sendRequest('Sort by name', {
onComplete: jest.fn(),
onError: jest.fn(),
onAbort,
});

controller.sendRequest('Sort by id');

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

it('should not call onAbort when no callback is provided', async () => {
const aiIntegration = createMockAIIntegration();

const controller = await createController({
aiAssistant: { enabled: true, aiIntegration },
});

controller.sendRequest('Sort by name');

expect(() => {
controller.abortRequest();
}).not.toThrow();
});

it('should not call onAbort when request completes via onComplete', async () => {
let capturedCallbacks: RequestCallbacks<ExecuteGridAssistantCommandResult> = {};
const onAbort = jest.fn();
const aiIntegration = createMockAIIntegration((_params, callbacks) => {
capturedCallbacks = callbacks;
});

const controller = await createController({
aiAssistant: { enabled: true, aiIntegration },
});

controller.sendRequest('Sort by name', {
onComplete: jest.fn(),
onError: jest.fn(),
onAbort,
});

capturedCallbacks.onComplete?.({
actions: [{ name: 'sort', args: { column: 'Name' } }],
} as ExecuteGridAssistantCommandResult);

expect(onAbort).not.toHaveBeenCalled();
});

it('should not call onAbort when request completes via onError', async () => {
let capturedCallbacks: RequestCallbacks<ExecuteGridAssistantCommandResult> = {};
const onAbort = jest.fn();
const aiIntegration = createMockAIIntegration((_params, callbacks) => {
capturedCallbacks = callbacks;
});

const controller = await createController({
aiAssistant: { enabled: true, aiIntegration },
});

controller.sendRequest('Sort by name', {
onComplete: jest.fn(),
onError: jest.fn(),
onAbort,
});

capturedCallbacks.onError?.(new Error('Network error'));

expect(onAbort).not.toHaveBeenCalled();
});
});

describe('dispose', () => {
it('should abort request on dispose', async () => {
const abortSpy = jest.fn();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ const mockMessageDataSource = { store: new ArrayStore({ key: 'id' }), reshapeOnP
const mockAIAssistantController = {
getMessageDataSource: jest.fn().mockReturnValue(mockMessageDataSource),
sendRequestToAI: jest.fn(),
isProcessing: jest.fn().mockReturnValue(false),
abortRequest: jest.fn(),
};

const createAIAssistantView = ({
Expand Down Expand Up @@ -280,6 +282,17 @@ describe('AIAssistantView', () => {
});
});

describe('onHidden', () => {
it('should call abortRequest on controller when popup onHidden is triggered', () => {
createAIAssistantView();

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

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

describe('chat event handlers', () => {
describe('onChatCleared', () => {
it('should call clear on aiChatInstance when triggered', () => {
Expand Down Expand Up @@ -312,12 +325,10 @@ describe('AIAssistantView', () => {
expect(mockAIAssistantController.sendRequestToAI).toHaveBeenCalledWith(message);
});

it('should not send request when chat is disabled', () => {
it('should not send request when request is already processing', () => {
createAIAssistantView();

const aiChatInstance = (AIChat as jest.Mock)
.mock.results[0].value as { isDisabled: jest.Mock; setDisabled: jest.Mock };
aiChatInstance.isDisabled.mockReturnValue(true);
mockAIAssistantController.isProcessing.mockReturnValueOnce(true);

const aiChatConfig = (AIChat as jest.Mock).mock.calls[0][0] as AIChatOptions;
const message = {
Expand Down Expand Up @@ -368,8 +379,8 @@ describe('AIAssistantView', () => {
});

it('should call setDisabled(false) after request fails', async () => {
mockAIAssistantController.sendRequestToAI.mockReturnValue(
Promise.reject(new Error('Network error')),
mockAIAssistantController.sendRequestToAI.mockImplementation(
() => Promise.reject(new Error('Network error')),
);
createAIAssistantView();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -379,5 +379,33 @@ describe('AIAssistantViewController', () => {

expect(findMessageElements().length).toBe(0);
});

it('should render abort message after closing and reopening AI chat', async () => {
const { instance } = await createDataGridWithAIAssistant();

sendAIRequest(instance, 'Sort by Name');

expect(findMessageElements().length).toBe(1);
expect(getMessageStatusClass(findMessageElements().eq(0))).toBe(MessageStatus.Pending);

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

// Close the AI assistant popup
await viewController.toggle();
jest.runAllTimers();
await flushAsync();

// Reopen the AI assistant popup
await viewController.toggle();
jest.runAllTimers();
await flushAsync();

const $messages = findMessageElements();

expect($messages.length).toBe(1);
expect(getMessageStatusClass($messages.eq(0))).toBe(MessageStatus.Failure);
expect($messages.eq(0).find(`.${CLASSES.messageErrorText}`).text())
.toBe('Request stopped.');
});
});
});
Loading
Loading