From c044403e380a636b419ae8b6c2a965a8f23e6cca Mon Sep 17 00:00:00 2001 From: "anna.shakhova" <68295572+anna-shakhova@users.noreply.github.com> Date: Mon, 27 Jul 2026 16:47:59 +0200 Subject: [PATCH 1/2] Grids: extract ai column api handlers tests --- .../__tests__/ai_column.api_handlers.test.ts | 389 ++++++++++++++++++ .../__tests__/ai_column.integration.test.ts | 367 ----------------- 2 files changed, 389 insertions(+), 367 deletions(-) create mode 100644 packages/devextreme/js/__internal/grids/grid_core/ai_column/__tests__/ai_column.api_handlers.test.ts diff --git a/packages/devextreme/js/__internal/grids/grid_core/ai_column/__tests__/ai_column.api_handlers.test.ts b/packages/devextreme/js/__internal/grids/grid_core/ai_column/__tests__/ai_column.api_handlers.test.ts new file mode 100644 index 000000000000..c65872d2e9f1 --- /dev/null +++ b/packages/devextreme/js/__internal/grids/grid_core/ai_column/__tests__/ai_column.api_handlers.test.ts @@ -0,0 +1,389 @@ +import { + afterEach, beforeEach, describe, expect, it, jest, +} from '@jest/globals'; +import type { GenerateGridColumnCommandResponse } from '@js/common/ai-integration'; +import errors from '@js/ui/widget/ui.errors'; +import { AIIntegration } from '@ts/core/ai_integration/core/ai_integration'; + +import { + afterTest, + beforeTest as baseBeforeTest, + createDataGrid, + GRID_CONTAINER_ID, +} from '../../__tests__/__mock__/helpers/utils'; + +interface RequestResult { + promise: Promise; + abort: () => void; +} + +const beforeTest = (): void => { + baseBeforeTest(); + jest.spyOn(errors, 'log').mockImplementation(jest.fn()); + jest.spyOn(errors, 'Error').mockImplementation(() => ({})); +}; +describe('API Handlers', () => { + const columnSendRequestStarted = jest.fn(); + const columnSendRequestResolved = jest.fn(); + const sendRequestPromptSpy = jest.fn(); + const sendRequestDataSpy = jest.fn(); + const abortSpy = jest.fn(); + + beforeEach(() => { + beforeTest(); + columnSendRequestStarted.mockClear(); + columnSendRequestResolved.mockClear(); + sendRequestPromptSpy.mockClear(); + sendRequestDataSpy.mockClear(); + abortSpy.mockClear(); + }); + + afterEach(afterTest); + + describe('onAIColumnRequestCreating', () => { + const aiIntegrationResult = (): RequestResult => ({ + promise: new Promise((resolve) => { + columnSendRequestStarted(); + // Timeouts are mocked and do not delay tests execution + setTimeout(() => { + columnSendRequestResolved(); + resolve('1'); + }, 10000); + }), + abort: (): void => { + abortSpy(); + }, + }); + const columnAIIntegration = new AIIntegration({ + sendRequest({ prompt, data }): RequestResult { + sendRequestPromptSpy(prompt); + sendRequestDataSpy(data); + return aiIntegrationResult(); + }, + }); + it('should be called by default', async () => { + const onAIColumnRequestCreating = jest.fn(); + const { instance } = await createDataGrid({ + dataSource: [ + { id: 1, name: 'Name 1', value: 10 }, + { id: 2, name: 'Name 2', value: 20 }, + ], + keyExpr: 'id', + columns: [ + { dataField: 'id', caption: 'ID' }, + { dataField: 'name', caption: 'Name' }, + { dataField: 'value', caption: 'Value' }, + { + type: 'ai', + caption: 'AI Column', + name: 'myColumn', + ai: { + aiIntegration: columnAIIntegration, + mode: 'manual', + prompt: 'Test prompt', + }, + }, + ], + onAIColumnRequestCreating, + }); + + instance.sendAIColumnRequest('myColumn'); + expect(columnSendRequestStarted).toHaveBeenCalledTimes(1); + expect(onAIColumnRequestCreating).toHaveBeenCalledTimes(1); + expect(onAIColumnRequestCreating).toHaveBeenCalledWith( + expect.objectContaining({ + component: expect.objectContaining({ NAME: 'dxDataGrid' }), + element: expect.objectContaining({ id: GRID_CONTAINER_ID }), + column: expect.objectContaining({ + name: 'myColumn', + ai: expect.objectContaining({ + mode: 'manual', + prompt: 'Test prompt', + }), + }), + data: expect.arrayContaining([ + { id: 1, name: 'Name 1', value: 10 }, + { id: 2, name: 'Name 2', value: 20 }, + ]), + useCache: false, + cancel: false, + additionalInfo: {}, + }), + ); + expect(abortSpy).toHaveBeenCalledTimes(0); + // There is enough time to resolve a promise + jest.advanceTimersByTime(10000); + expect(columnSendRequestResolved).toHaveBeenCalledTimes(1); + }); + + it('should cancel the request if e.cancel is true', async () => { + const { instance } = await createDataGrid({ + dataSource: [ + { id: 1, name: 'Name 1', value: 10 }, + { id: 2, name: 'Name 2', value: 20 }, + ], + keyExpr: 'id', + columns: [ + { dataField: 'id', caption: 'ID' }, + { dataField: 'name', caption: 'Name' }, + { dataField: 'value', caption: 'Value' }, + { + type: 'ai', + caption: 'AI Column', + name: 'myColumn', + ai: { + aiIntegration: columnAIIntegration, + mode: 'manual', + prompt: 'Test prompt', + }, + }, + ], + onAIColumnRequestCreating: (e) => { e.cancel = true; }, + }); + + instance.sendAIColumnRequest('myColumn'); + // There is enough time to resolve a promise + jest.advanceTimersByTime(10000); + expect(columnSendRequestStarted).toHaveBeenCalledTimes(0); + expect(abortSpy).toHaveBeenCalledTimes(0); + expect(columnSendRequestResolved).toHaveBeenCalledTimes(0); + }); + + it('should take into account reduced row count', async () => { + const { instance } = await createDataGrid({ + dataSource: [ + { id: 1, name: 'Name 1', value: 10 }, + { id: 2, name: 'Name 2', value: 20 }, + ], + keyExpr: 'id', + columns: [ + { dataField: 'id', caption: 'ID' }, + { dataField: 'name', caption: 'Name' }, + { dataField: 'value', caption: 'Value' }, + { + type: 'ai', + caption: 'AI Column', + name: 'myColumn', + ai: { + aiIntegration: columnAIIntegration, + mode: 'manual', + prompt: 'Test prompt', + }, + }, + ], + onAIColumnRequestCreating: (e) => { + const filtered = e.data.filter((item) => item.id === 2); + e.data.splice(0, e.data.length, ...filtered); + }, + }); + + instance.sendAIColumnRequest('myColumn'); + // There is enough time to resolve a promise + jest.advanceTimersByTime(10000); + expect(columnSendRequestStarted).toHaveBeenCalledTimes(1); + expect(columnSendRequestResolved).toHaveBeenCalledTimes(1); + expect(sendRequestPromptSpy).toHaveBeenCalledWith(expect.objectContaining({ + user: expect.stringContaining('Dataset: {"2":{"id":2,"name":"Name 2","value":20}}'), + })); + + await Promise.resolve(); + expect(abortSpy).toHaveBeenCalledTimes(1); + }); + + it('should take into account reduced column count', async () => { + const { instance } = await createDataGrid({ + dataSource: [ + { id: 1, name: 'Name 1', value: 10 }, + { id: 2, name: 'Name 2', value: 20 }, + ], + keyExpr: 'id', + columns: [ + { dataField: 'id', caption: 'ID' }, + { dataField: 'name', caption: 'Name' }, + { dataField: 'value', caption: 'Value' }, + { + type: 'ai', + caption: 'AI Column', + name: 'myColumn', + ai: { + aiIntegration: columnAIIntegration, + mode: 'manual', + prompt: 'Test prompt', + }, + }, + ], + onAIColumnRequestCreating: (e) => { + const reduced = e.data.map((item) => ({ id: item.id })); + e.data.splice(0, e.data.length, ...reduced); + }, + }); + + instance.sendAIColumnRequest('myColumn'); + // There is enough time to resolve a promise + jest.advanceTimersByTime(10000); + expect(columnSendRequestStarted).toHaveBeenCalledTimes(1); + expect(columnSendRequestResolved).toHaveBeenCalledTimes(1); + expect(sendRequestPromptSpy).toHaveBeenCalledWith(expect.objectContaining({ + user: expect.stringContaining('Dataset: {"1":{"id":1},"2":{"id":2}}.'), + })); + + await Promise.resolve(); + expect(abortSpy).toHaveBeenCalledTimes(1); + }); + + it('should pass additional info to the AI request', async () => { + const { instance } = await createDataGrid({ + dataSource: [ + { id: 1, name: 'Name 1', value: 10 }, + { id: 2, name: 'Name 2', value: 20 }, + ], + keyExpr: 'id', + columns: [ + { dataField: 'id', caption: 'ID' }, + { dataField: 'name', caption: 'Name' }, + { dataField: 'value', caption: 'Value' }, + { + type: 'ai', + caption: 'AI Column', + name: 'myColumn', + ai: { + aiIntegration: columnAIIntegration, + mode: 'manual', + prompt: 'Test prompt', + }, + }, + ], + onAIColumnRequestCreating: (e) => { + e.additionalInfo = { customData: 'My custom data' }; + }, + }); + + instance.sendAIColumnRequest('myColumn'); + // There is enough time to resolve a promise + jest.advanceTimersByTime(10000); + expect(columnSendRequestStarted).toHaveBeenCalledTimes(1); + expect(columnSendRequestResolved).toHaveBeenCalledTimes(1); + expect(sendRequestDataSpy).toHaveBeenCalledWith(expect.objectContaining({ + additionalInfo: { customData: 'My custom data' }, + })); + + await Promise.resolve(); + expect(abortSpy).toHaveBeenCalledTimes(1); + }); + + it('should have useCache property set to true by default', async () => { + const aiIntegration = new AIIntegration({ + sendRequest(prompt): RequestResult { + sendRequestDataSpy(); + + return { + promise: new Promise((resolve) => { + const result = {}; + Object.entries(prompt.data?.data).forEach(([key, value]) => { + result[key] = `Response ${(value as any).name}`; + }); + resolve(JSON.stringify(result)); + }), + abort: (): void => {}, + }; + }, + }); + const { instance } = await createDataGrid({ + dataSource: [ + { id: 1, name: 'Name 1', value: 10 }, + { id: 2, name: 'Name 2', value: 20 }, + ], + paging: { + pageSize: 1, + }, + keyExpr: 'id', + columns: [ + { dataField: 'id', caption: 'ID' }, + { dataField: 'name', caption: 'Name' }, + { dataField: 'value', caption: 'Value' }, + { + type: 'ai', + caption: 'AI Column', + name: 'myColumn', + ai: { + aiIntegration, + prompt: 'Test prompt', + }, + }, + ], + }); + + await Promise.resolve(); + expect(sendRequestDataSpy).toHaveBeenCalledTimes(1); + + instance.option('paging.pageIndex', 1); + jest.runAllTimers(); + await Promise.resolve(); + expect(sendRequestDataSpy).toHaveBeenCalledTimes(2); + + instance.option('paging.pageIndex', 0); + jest.runAllTimers(); + await Promise.resolve(); + expect(sendRequestDataSpy).toHaveBeenCalledTimes(2); + }); + + it('should not use cache when useCache property set to false', async () => { + const aiIntegration = new AIIntegration({ + sendRequest(prompt): RequestResult { + sendRequestDataSpy(); + + return { + promise: new Promise((resolve) => { + const result = {}; + Object.entries(prompt.data?.data).forEach(([key, value]) => { + result[key] = `Response ${(value as any).name}`; + }); + resolve(JSON.stringify(result)); + }), + abort: (): void => {}, + }; + }, + }); + const { instance } = await createDataGrid({ + dataSource: [ + { id: 1, name: 'Name 1', value: 10 }, + { id: 2, name: 'Name 2', value: 20 }, + ], + paging: { + pageSize: 1, + }, + keyExpr: 'id', + columns: [ + { dataField: 'id', caption: 'ID' }, + { dataField: 'name', caption: 'Name' }, + { dataField: 'value', caption: 'Value' }, + { + type: 'ai', + caption: 'AI Column', + name: 'myColumn', + ai: { + aiIntegration, + prompt: 'Test prompt', + }, + }, + ], + onAIColumnRequestCreating: (e) => { + e.useCache = false; + }, + }); + + await Promise.resolve(); + expect(sendRequestDataSpy).toHaveBeenCalledTimes(1); + + instance.option('paging.pageIndex', 1); + jest.runAllTimers(); + await Promise.resolve(); + expect(sendRequestDataSpy).toHaveBeenCalledTimes(2); + + instance.option('paging.pageIndex', 0); + jest.runAllTimers(); + await Promise.resolve(); + expect(sendRequestDataSpy).toHaveBeenCalledTimes(3); + }); + }); +}); diff --git a/packages/devextreme/js/__internal/grids/grid_core/ai_column/__tests__/ai_column.integration.test.ts b/packages/devextreme/js/__internal/grids/grid_core/ai_column/__tests__/ai_column.integration.test.ts index 82cec87414f0..ce845362f310 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/ai_column/__tests__/ai_column.integration.test.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/ai_column/__tests__/ai_column.integration.test.ts @@ -13,7 +13,6 @@ import { afterTest, beforeTest as baseBeforeTest, createDataGrid, - GRID_CONTAINER_ID, } from '../../__tests__/__mock__/helpers/utils'; import { CLASSES } from '../const'; @@ -2631,372 +2630,6 @@ describe('API Methods', () => { }); }); -describe('API Handlers', () => { - const columnSendRequestStarted = jest.fn(); - const columnSendRequestResolved = jest.fn(); - const sendRequestPromptSpy = jest.fn(); - const sendRequestDataSpy = jest.fn(); - const abortSpy = jest.fn(); - - beforeEach(() => { - beforeTest(); - columnSendRequestStarted.mockClear(); - columnSendRequestResolved.mockClear(); - sendRequestPromptSpy.mockClear(); - sendRequestDataSpy.mockClear(); - abortSpy.mockClear(); - }); - - afterEach(afterTest); - - describe('onAIColumnRequestCreating', () => { - const aiIntegrationResult = (): RequestResult => ({ - promise: new Promise((resolve) => { - columnSendRequestStarted(); - // Timeouts are mocked and do not delay tests execution - setTimeout(() => { - columnSendRequestResolved(); - resolve('1'); - }, 10000); - }), - abort: (): void => { - abortSpy(); - }, - }); - const columnAIIntegration = new AIIntegration({ - sendRequest({ prompt, data }): RequestResult { - sendRequestPromptSpy(prompt); - sendRequestDataSpy(data); - return aiIntegrationResult(); - }, - }); - it('should be called by default', async () => { - const onAIColumnRequestCreating = jest.fn(); - const { instance } = await createDataGrid({ - dataSource: [ - { id: 1, name: 'Name 1', value: 10 }, - { id: 2, name: 'Name 2', value: 20 }, - ], - keyExpr: 'id', - columns: [ - { dataField: 'id', caption: 'ID' }, - { dataField: 'name', caption: 'Name' }, - { dataField: 'value', caption: 'Value' }, - { - type: 'ai', - caption: 'AI Column', - name: 'myColumn', - ai: { - aiIntegration: columnAIIntegration, - mode: 'manual', - prompt: 'Test prompt', - }, - }, - ], - onAIColumnRequestCreating, - }); - - instance.sendAIColumnRequest('myColumn'); - expect(columnSendRequestStarted).toHaveBeenCalledTimes(1); - expect(onAIColumnRequestCreating).toHaveBeenCalledTimes(1); - expect(onAIColumnRequestCreating).toHaveBeenCalledWith( - expect.objectContaining({ - component: expect.objectContaining({ NAME: 'dxDataGrid' }), - element: expect.objectContaining({ id: GRID_CONTAINER_ID }), - column: expect.objectContaining({ - name: 'myColumn', - ai: expect.objectContaining({ - mode: 'manual', - prompt: 'Test prompt', - }), - }), - data: expect.arrayContaining([ - { id: 1, name: 'Name 1', value: 10 }, - { id: 2, name: 'Name 2', value: 20 }, - ]), - useCache: false, - cancel: false, - additionalInfo: {}, - }), - ); - expect(abortSpy).toHaveBeenCalledTimes(0); - // There is enough time to resolve a promise - jest.advanceTimersByTime(10000); - expect(columnSendRequestResolved).toHaveBeenCalledTimes(1); - }); - - it('should cancel the request if e.cancel is true', async () => { - const { instance } = await createDataGrid({ - dataSource: [ - { id: 1, name: 'Name 1', value: 10 }, - { id: 2, name: 'Name 2', value: 20 }, - ], - keyExpr: 'id', - columns: [ - { dataField: 'id', caption: 'ID' }, - { dataField: 'name', caption: 'Name' }, - { dataField: 'value', caption: 'Value' }, - { - type: 'ai', - caption: 'AI Column', - name: 'myColumn', - ai: { - aiIntegration: columnAIIntegration, - mode: 'manual', - prompt: 'Test prompt', - }, - }, - ], - onAIColumnRequestCreating: (e) => { e.cancel = true; }, - }); - - instance.sendAIColumnRequest('myColumn'); - // There is enough time to resolve a promise - jest.advanceTimersByTime(10000); - expect(columnSendRequestStarted).toHaveBeenCalledTimes(0); - expect(abortSpy).toHaveBeenCalledTimes(0); - expect(columnSendRequestResolved).toHaveBeenCalledTimes(0); - }); - - it('should take into account reduced row count', async () => { - const { instance } = await createDataGrid({ - dataSource: [ - { id: 1, name: 'Name 1', value: 10 }, - { id: 2, name: 'Name 2', value: 20 }, - ], - keyExpr: 'id', - columns: [ - { dataField: 'id', caption: 'ID' }, - { dataField: 'name', caption: 'Name' }, - { dataField: 'value', caption: 'Value' }, - { - type: 'ai', - caption: 'AI Column', - name: 'myColumn', - ai: { - aiIntegration: columnAIIntegration, - mode: 'manual', - prompt: 'Test prompt', - }, - }, - ], - onAIColumnRequestCreating: (e) => { - const filtered = e.data.filter((item) => item.id === 2); - e.data.splice(0, e.data.length, ...filtered); - }, - }); - - instance.sendAIColumnRequest('myColumn'); - // There is enough time to resolve a promise - jest.advanceTimersByTime(10000); - expect(columnSendRequestStarted).toHaveBeenCalledTimes(1); - expect(columnSendRequestResolved).toHaveBeenCalledTimes(1); - expect(sendRequestPromptSpy).toHaveBeenCalledWith(expect.objectContaining({ - user: expect.stringContaining('Dataset: {"2":{"id":2,"name":"Name 2","value":20}}'), - })); - - await Promise.resolve(); - expect(abortSpy).toHaveBeenCalledTimes(1); - }); - - it('should take into account reduced column count', async () => { - const { instance } = await createDataGrid({ - dataSource: [ - { id: 1, name: 'Name 1', value: 10 }, - { id: 2, name: 'Name 2', value: 20 }, - ], - keyExpr: 'id', - columns: [ - { dataField: 'id', caption: 'ID' }, - { dataField: 'name', caption: 'Name' }, - { dataField: 'value', caption: 'Value' }, - { - type: 'ai', - caption: 'AI Column', - name: 'myColumn', - ai: { - aiIntegration: columnAIIntegration, - mode: 'manual', - prompt: 'Test prompt', - }, - }, - ], - onAIColumnRequestCreating: (e) => { - const reduced = e.data.map((item) => ({ id: item.id })); - e.data.splice(0, e.data.length, ...reduced); - }, - }); - - instance.sendAIColumnRequest('myColumn'); - // There is enough time to resolve a promise - jest.advanceTimersByTime(10000); - expect(columnSendRequestStarted).toHaveBeenCalledTimes(1); - expect(columnSendRequestResolved).toHaveBeenCalledTimes(1); - expect(sendRequestPromptSpy).toHaveBeenCalledWith(expect.objectContaining({ - user: expect.stringContaining('Dataset: {"1":{"id":1},"2":{"id":2}}.'), - })); - - await Promise.resolve(); - expect(abortSpy).toHaveBeenCalledTimes(1); - }); - - it('should pass additional info to the AI request', async () => { - const { instance } = await createDataGrid({ - dataSource: [ - { id: 1, name: 'Name 1', value: 10 }, - { id: 2, name: 'Name 2', value: 20 }, - ], - keyExpr: 'id', - columns: [ - { dataField: 'id', caption: 'ID' }, - { dataField: 'name', caption: 'Name' }, - { dataField: 'value', caption: 'Value' }, - { - type: 'ai', - caption: 'AI Column', - name: 'myColumn', - ai: { - aiIntegration: columnAIIntegration, - mode: 'manual', - prompt: 'Test prompt', - }, - }, - ], - onAIColumnRequestCreating: (e) => { - e.additionalInfo = { customData: 'My custom data' }; - }, - }); - - instance.sendAIColumnRequest('myColumn'); - // There is enough time to resolve a promise - jest.advanceTimersByTime(10000); - expect(columnSendRequestStarted).toHaveBeenCalledTimes(1); - expect(columnSendRequestResolved).toHaveBeenCalledTimes(1); - expect(sendRequestDataSpy).toHaveBeenCalledWith(expect.objectContaining({ - additionalInfo: { customData: 'My custom data' }, - })); - - await Promise.resolve(); - expect(abortSpy).toHaveBeenCalledTimes(1); - }); - - it('should have useCache property set to true by default', async () => { - const aiIntegration = new AIIntegration({ - sendRequest(prompt): RequestResult { - sendRequestDataSpy(); - - return { - promise: new Promise((resolve) => { - const result = {}; - Object.entries(prompt.data?.data).forEach(([key, value]) => { - result[key] = `Response ${(value as any).name}`; - }); - resolve(JSON.stringify(result)); - }), - abort: (): void => {}, - }; - }, - }); - const { instance } = await createDataGrid({ - dataSource: [ - { id: 1, name: 'Name 1', value: 10 }, - { id: 2, name: 'Name 2', value: 20 }, - ], - paging: { - pageSize: 1, - }, - keyExpr: 'id', - columns: [ - { dataField: 'id', caption: 'ID' }, - { dataField: 'name', caption: 'Name' }, - { dataField: 'value', caption: 'Value' }, - { - type: 'ai', - caption: 'AI Column', - name: 'myColumn', - ai: { - aiIntegration, - prompt: 'Test prompt', - }, - }, - ], - }); - - await Promise.resolve(); - expect(sendRequestDataSpy).toHaveBeenCalledTimes(1); - - instance.option('paging.pageIndex', 1); - jest.runAllTimers(); - await Promise.resolve(); - expect(sendRequestDataSpy).toHaveBeenCalledTimes(2); - - instance.option('paging.pageIndex', 0); - jest.runAllTimers(); - await Promise.resolve(); - expect(sendRequestDataSpy).toHaveBeenCalledTimes(2); - }); - - it('should not use cache when useCache property set to false', async () => { - const aiIntegration = new AIIntegration({ - sendRequest(prompt): RequestResult { - sendRequestDataSpy(); - - return { - promise: new Promise((resolve) => { - const result = {}; - Object.entries(prompt.data?.data).forEach(([key, value]) => { - result[key] = `Response ${(value as any).name}`; - }); - resolve(JSON.stringify(result)); - }), - abort: (): void => {}, - }; - }, - }); - const { instance } = await createDataGrid({ - dataSource: [ - { id: 1, name: 'Name 1', value: 10 }, - { id: 2, name: 'Name 2', value: 20 }, - ], - paging: { - pageSize: 1, - }, - keyExpr: 'id', - columns: [ - { dataField: 'id', caption: 'ID' }, - { dataField: 'name', caption: 'Name' }, - { dataField: 'value', caption: 'Value' }, - { - type: 'ai', - caption: 'AI Column', - name: 'myColumn', - ai: { - aiIntegration, - prompt: 'Test prompt', - }, - }, - ], - onAIColumnRequestCreating: (e) => { - e.useCache = false; - }, - }); - - await Promise.resolve(); - expect(sendRequestDataSpy).toHaveBeenCalledTimes(1); - - instance.option('paging.pageIndex', 1); - jest.runAllTimers(); - await Promise.resolve(); - expect(sendRequestDataSpy).toHaveBeenCalledTimes(2); - - instance.option('paging.pageIndex', 0); - jest.runAllTimers(); - await Promise.resolve(); - expect(sendRequestDataSpy).toHaveBeenCalledTimes(3); - }); - }); -}); - describe('Popup', () => { beforeEach(beforeTest); afterEach(afterTest); From f945ab4145c2d51381dcd9ba0001975c85733722 Mon Sep 17 00:00:00 2001 From: "anna.shakhova" <68295572+anna-shakhova@users.noreply.github.com> Date: Mon, 27 Jul 2026 16:55:02 +0200 Subject: [PATCH 2/2] Grids: throw E1046 errow if key field was removed from data inside onAIColumnRequestCreating --- .../__tests__/ai_column.api_handlers.test.ts | 38 +++++++++++++++++++ .../m_ai_column_integration_controller.ts | 7 +++- .../grids/grid_core/ai_column/utils.ts | 13 +++++++ 3 files changed, 57 insertions(+), 1 deletion(-) diff --git a/packages/devextreme/js/__internal/grids/grid_core/ai_column/__tests__/ai_column.api_handlers.test.ts b/packages/devextreme/js/__internal/grids/grid_core/ai_column/__tests__/ai_column.api_handlers.test.ts index c65872d2e9f1..401ef300419f 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/ai_column/__tests__/ai_column.api_handlers.test.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/ai_column/__tests__/ai_column.api_handlers.test.ts @@ -385,5 +385,43 @@ describe('API Handlers', () => { await Promise.resolve(); expect(sendRequestDataSpy).toHaveBeenCalledTimes(3); }); + + it('should throw E1046 and not send the request when the handler removes the key field', async () => { + const onDataErrorOccurred = jest.fn(); + const { instance } = await createDataGrid({ + dataSource: [ + { id: 1, name: 'Name 1', value: 10 }, + { id: 2, name: 'Name 2', value: 20 }, + ], + keyExpr: 'id', + columns: [ + { dataField: 'id', caption: 'ID' }, + { dataField: 'name', caption: 'Name' }, + { dataField: 'value', caption: 'Value' }, + { + type: 'ai', + caption: 'AI Column', + name: 'myColumn', + ai: { + aiIntegration: columnAIIntegration, + mode: 'manual', + prompt: 'Test prompt', + }, + }, + ], + onAIColumnRequestCreating: (e) => { + const reduced = e.data.map((item) => ({ name: item.name, value: item.value })); + e.data.splice(0, e.data.length, ...reduced); + }, + onDataErrorOccurred, + }); + + instance.sendAIColumnRequest('myColumn'); + jest.advanceTimersByTime(10000); + + expect(columnSendRequestStarted).toHaveBeenCalledTimes(0); + expect(onDataErrorOccurred).toHaveBeenCalledTimes(1); + expect(errors.Error).toHaveBeenCalledWith('E1046', 'id'); + }); }); }); diff --git a/packages/devextreme/js/__internal/grids/grid_core/ai_column/controllers/m_ai_column_integration_controller.ts b/packages/devextreme/js/__internal/grids/grid_core/ai_column/controllers/m_ai_column_integration_controller.ts index 6308a13e038e..fffa1a230ebb 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/ai_column/controllers/m_ai_column_integration_controller.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/ai_column/controllers/m_ai_column_integration_controller.ts @@ -10,7 +10,7 @@ import type { DataController } from '../../data_controller/m_data_controller'; import type { ErrorHandlingController } from '../../error_handling/m_error_handling'; import { Controller } from '../../m_modules'; import type { InternalRequestCallbacks } from '../types'; -import { getDataFromRowItems, reduceDataCachedKeys } from '../utils'; +import { getDataFromRowItems, isKeyMissingInData, reduceDataCachedKeys } from '../utils'; import { AIColumnCacheController } from './m_ai_column_cache_controller'; export class AIColumnIntegrationController extends Controller { @@ -130,6 +130,11 @@ export class AIColumnIntegrationController extends Controller { } const keyField = this.dataController.key(); + if (isKeyMissingInData(args.data, keyField)) { + this.dataController.fireError('E1046', keyField); + return; + } + let cachedResponse: Record = {}; if (args.useCache) { const keys = data.map((item) => item[keyField] as PropertyKey); diff --git a/packages/devextreme/js/__internal/grids/grid_core/ai_column/utils.ts b/packages/devextreme/js/__internal/grids/grid_core/ai_column/utils.ts index ce5ffb741080..849c0d9a21de 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/ai_column/utils.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/ai_column/utils.ts @@ -33,6 +33,19 @@ export const reduceDataCachedKeys = ( return newData; }; +export const isKeyMissingInData = ( + data: UserData[], + keyField: string | string[], +): boolean => { + if (typeof keyField !== 'string') { + // The key field should be a string for AI Column functionality. + // Return false to avoid unnecessary errors for compound keys. + return false; + } + + return data.some((item) => !(keyField in item)); +}; + export const isAIColumnAutoMode = (column: Column): boolean => column.type === 'ai' && (!column.ai?.mode || column.ai.mode === 'auto'); export const isPopupOptions = (optionName: string, value: unknown): boolean => optionName.startsWith('ai.popup')