diff --git a/e2e/testcafe-devextreme/tests/dataGrid/common/aiAssistant/testHelpers.ts b/e2e/testcafe-devextreme/tests/dataGrid/common/aiAssistant/testHelpers.ts new file mode 100644 index 000000000000..45c66d4f7edb --- /dev/null +++ b/e2e/testcafe-devextreme/tests/dataGrid/common/aiAssistant/testHelpers.ts @@ -0,0 +1,16 @@ +/* eslint-disable no-underscore-dangle */ +import { ClientFunction } from 'testcafe'; +import url from '../../../../helpers/getPageUrl'; + +export const GRID_SELECTOR = '#container'; + +export const AI_INTEGRATION_PAGE = url(__dirname, '../../../container-ai-integration.html'); + +export const getRequestColumnNames = ClientFunction( + (index: number) => (window as any).__aiRequests[index].data.context.columns + .map((c: any) => c.dataField), +); + +export const formatMessage = ClientFunction( + (key: string) => (window as any).DevExpress.localization.formatMessage(key), +); diff --git a/e2e/testcafe-devextreme/tests/dataGrid/common/aiAssistant/toolbarAndPopup.functional.ts b/e2e/testcafe-devextreme/tests/dataGrid/common/aiAssistant/toolbarAndPopup.functional.ts new file mode 100644 index 000000000000..f1df0bcd2b79 --- /dev/null +++ b/e2e/testcafe-devextreme/tests/dataGrid/common/aiAssistant/toolbarAndPopup.functional.ts @@ -0,0 +1,198 @@ +import DataGrid from 'devextreme-testcafe-models/dataGrid'; +import { + AI_INTEGRATION_PAGE, + GRID_SELECTOR, +} from './testHelpers'; +import { createWidget } from '../../../../helpers/createWidget'; + +// AI Assistant enabled with a request that never resolves — keeps the chat in the +// idle/empty state (no command runs) for visibility & open/close lifecycle tests. +const gridWithIdleAssistant = (): any => ({ + dataSource: [ + { id: 1, name: 'Alice', value: 30 }, + { id: 2, name: 'Bob', value: 20 }, + { id: 3, name: 'Charlie', value: 10 }, + ], + keyExpr: 'id', + columns: ['id', 'name', 'value'], + showBorders: true, + aiAssistant: { + enabled: true, + aiIntegration: new (window as any).DevExpress.aiIntegration.AIIntegration({ + sendRequest() { + return { promise: new Promise(() => {}), abort: (): void => {} }; + }, + }), + }, +}); + +// Grid without the `aiAssistant` option — toolbar button must not be rendered. +const gridWithoutAssistant = (): any => ({ + dataSource: [ + { id: 1, name: 'Alice', value: 30 }, + { id: 2, name: 'Bob', value: 20 }, + { id: 3, name: 'Charlie', value: 10 }, + ], + keyExpr: 'id', + columns: ['id', 'name', 'value'], + showBorders: true, +}); + +// AI Assistant whose mocked request resolves to a single `sorting` command, so a +// prompt actually mutates grid state (used to assert the command is applied). +const gridWithSortingAssistant = (): any => ({ + dataSource: [ + { id: 1, name: 'Alice', value: 30 }, + { id: 2, name: 'Bob', value: 20 }, + { id: 3, name: 'Charlie', value: 10 }, + ], + keyExpr: 'id', + columns: ['id', 'name', 'value'], + showBorders: true, + aiAssistant: { + enabled: true, + aiIntegration: new (window as any).DevExpress.aiIntegration.AIIntegration({ + sendRequest() { + return { + promise: Promise.resolve({ + actions: [{ name: 'sorting', args: { dataField: 'name', sortOrder: 'asc' } }], + }), + abort: (): void => {}, + }; + }, + }), + }, +}); + +// AI Assistant with a custom popup title (request stays pending — title only). +const gridWithTitledAssistant = (): any => ({ + dataSource: [ + { id: 1, name: 'Alice', value: 30 }, + { id: 2, name: 'Bob', value: 20 }, + { id: 3, name: 'Charlie', value: 10 }, + ], + keyExpr: 'id', + columns: ['id', 'name', 'value'], + showBorders: true, + aiAssistant: { + enabled: true, + title: 'My Custom Assistant', + aiIntegration: new (window as any).DevExpress.aiIntegration.AIIntegration({ + sendRequest() { + return { promise: new Promise(() => {}), abort: (): void => {} }; + }, + }), + }, +}); + +// === §1.1 Toolbar entry point & popup lifecycle === +fixture`AI Assistant - Toolbar` + .page(AI_INTEGRATION_PAGE); + +// 1.1.1 +test('Toolbar button should be visible when aiAssistant.enabled is true', async (t) => { + const dataGrid = new DataGrid(GRID_SELECTOR); + + await t.expect(dataGrid.isReady()).ok(); + + await t.expect(dataGrid.getAIAssistantButton().exists).ok(); +}).before(async () => createWidget('dxDataGrid', gridWithIdleAssistant)); + +// 1.1.2 +test('Toolbar button should be hidden when aiAssistant is not configured', async (t) => { + const dataGrid = new DataGrid(GRID_SELECTOR); + + await t.expect(dataGrid.isReady()).ok(); + + await t.expect(dataGrid.getAIAssistantButton().exists).notOk(); +}).before(async () => createWidget('dxDataGrid', gridWithoutAssistant)); + +// 1.1.3 +test('Popup should open on toolbar button click without changing grid state', async (t) => { + const dataGrid = new DataGrid(GRID_SELECTOR); + + await t.expect(dataGrid.isReady()).ok(); + + const initialState = await dataGrid.apiState(); + + await t.click(dataGrid.getAIAssistantButton()); + + const aiChat = dataGrid.getAIAssistantChat(); + const finalState = await dataGrid.apiState(); + + await t.expect(finalState).eql(initialState); + await t.expect(aiChat.element.visible).ok(); + await t.expect(aiChat.getChat().element.exists).ok(); + await t.expect(aiChat.getInput().visible).ok(); +}).before(async () => createWidget('dxDataGrid', gridWithIdleAssistant)); + +// 1.1.4 +test('AI Assistant-applied sorting should persist after popup close', async (t) => { + const dataGrid = new DataGrid(GRID_SELECTOR); + + await t.expect(dataGrid.isReady()).ok(); + + await t.click(dataGrid.getAIAssistantButton()); + + const aiChat = dataGrid.getAIAssistantChat(); + + await t + .typeText(aiChat.getInput(), 'Sort by name') + .pressKey('enter'); + + await t.expect(aiChat.getSuccessMessages().count).eql(1); + await t.expect(aiChat.getActionItems(0).count).eql(1); + + await t.click(aiChat.getCloseButton().element); + + const sortOrder = await dataGrid.apiColumnOption('name', 'sortOrder'); + + await t.expect(sortOrder).eql('asc'); +}).before(async () => createWidget('dxDataGrid', gridWithSortingAssistant)); + +// 1.1.6 +test('Custom title should be rendered in popup header', async (t) => { + const dataGrid = new DataGrid(GRID_SELECTOR); + + await t.expect(dataGrid.isReady()).ok(); + + await t.click(dataGrid.getAIAssistantButton()); + + const aiChat = dataGrid.getAIAssistantChat(); + + await t.expect(aiChat.getTitle().textContent).contains('My Custom Assistant'); +}).before(async () => createWidget('dxDataGrid', gridWithTitledAssistant)); + +// === §1.10 A11y / KBN === +fixture`AI Assistant - A11y` + .page(AI_INTEGRATION_PAGE); + +// 1.10.2 (Enter) — focus the toolbar button and activate it with Enter. +test('Toolbar button should activate via Enter key', async (t) => { + const dataGrid = new DataGrid(GRID_SELECTOR); + + await t.expect(dataGrid.isReady()).ok(); + + await dataGrid.focusAIAssistantButton(); + + await t.expect(dataGrid.getAIAssistantButton().focused).ok(); + + await t.pressKey('enter'); + + await t.expect(dataGrid.getAIAssistantChat().element.visible).ok(); +}).before(async () => createWidget('dxDataGrid', gridWithIdleAssistant)); + +// 1.10.2 (Space) — same scenario, activated with Space. +test('Toolbar button should activate via Space key', async (t) => { + const dataGrid = new DataGrid(GRID_SELECTOR); + + await t.expect(dataGrid.isReady()).ok(); + + await dataGrid.focusAIAssistantButton(); + + await t.expect(dataGrid.getAIAssistantButton().focused).ok(); + + await t.pressKey('space'); + + await t.expect(dataGrid.getAIAssistantChat().element.visible).ok(); +}).before(async () => createWidget('dxDataGrid', gridWithIdleAssistant)); diff --git a/packages/testcafe-models/chat.ts b/packages/testcafe-models/chat.ts index d5cf8baf8365..4e1cf2ed543a 100644 --- a/packages/testcafe-models/chat.ts +++ b/packages/testcafe-models/chat.ts @@ -43,8 +43,12 @@ export default class Chat extends Widget { return new Scrollable(this.element.find(`.${CLASS.scrollable}`)); } + getMessageBubbles(): Selector { + return this.element.find(`.${CLASS.messageBubble}`); + } + getMessage(index: number): Selector { - return this.element.find(`.${CLASS.messageBubble}`).nth(index); + return this.getMessageBubbles().nth(index); } getContextMenuContent(): Selector { diff --git a/packages/testcafe-models/dataGrid/aiAssistantChat.ts b/packages/testcafe-models/dataGrid/aiAssistantChat.ts index fe9d3621ac62..ce66205aa81a 100644 --- a/packages/testcafe-models/dataGrid/aiAssistantChat.ts +++ b/packages/testcafe-models/dataGrid/aiAssistantChat.ts @@ -37,6 +37,10 @@ export class AIAssistantChat extends Popup { return new Chat(this.element.find(`.${CLASS.aiChatContent}`)); } + getInput(): Selector { + return this.getChat().getInput(); + } + getCloseButton(): Button { return new Button(this.element.find(`.${CLASS.closeButton}`)); } @@ -46,6 +50,10 @@ export class AIAssistantChat extends Popup { } getMessages(): Selector { + return this.getChat().getMessageBubbles(); + } + + getAIMessages(): Selector { return this.element.find(`.${CLASS.message}`); } @@ -61,40 +69,40 @@ export class AIAssistantChat extends Popup { return this.element.find(`.${CLASS.messageError}`); } - getMessage(index: number): Selector { - return this.getMessages().nth(index); + getAIMessage(index: number): Selector { + return this.getAIMessages().nth(index); } getMessageHeader(index: number): Selector { - return this.getMessage(index).find(`.${CLASS.messageHeader}`); + return this.getAIMessage(index).find(`.${CLASS.messageHeader}`); } getMessageErrorText(index: number): Selector { - return this.getMessage(index).find(`.${CLASS.messageErrorText}`); + return this.getAIMessage(index).find(`.${CLASS.messageErrorText}`); } getMessageProgressBar(index: number): Selector { - return this.getMessage(index).find(`.${CLASS.messageProgressBar}`); + return this.getAIMessage(index).find(`.${CLASS.messageProgressBar}`); } getMessageRegenerateButton(index: number): Selector { - return this.getMessage(index).find(`.${CLASS.messageRegenerateButton}`); + return this.getAIMessage(index).find(`.${CLASS.messageRegenerateButton}`); } getActionList(messageIndex: number): Selector { - return this.getMessage(messageIndex).find(`.${CLASS.actionList}`); + return this.getAIMessage(messageIndex).find(`.${CLASS.actionList}`); } getActionItems(messageIndex: number): Selector { - return this.getMessage(messageIndex).find(`.${CLASS.actionListItem}`); + return this.getAIMessage(messageIndex).find(`.${CLASS.actionListItem}`); } getSuccessActionItems(messageIndex: number): Selector { - return this.getMessage(messageIndex).find(`.${CLASS.actionListItemSuccess}`); + return this.getAIMessage(messageIndex).find(`.${CLASS.actionListItemSuccess}`); } getErrorActionItems(messageIndex: number): Selector { - return this.getMessage(messageIndex).find(`.${CLASS.actionListItemError}`); + return this.getAIMessage(messageIndex).find(`.${CLASS.actionListItemError}`); } getActionItemText(messageIndex: number, actionIndex: number): Selector { @@ -104,4 +112,8 @@ export class AIAssistantChat extends Popup { getActionItemIcon(messageIndex: number, actionIndex: number): Selector { return this.getActionItems(messageIndex).nth(actionIndex).find(`.${CLASS.actionListItemIcon}`); } + + getTitle(): Selector { + return this.topToolbar; + } } diff --git a/packages/testcafe-models/dataGrid/index.ts b/packages/testcafe-models/dataGrid/index.ts index 097e5e921b15..f7455abdacaf 100644 --- a/packages/testcafe-models/dataGrid/index.ts +++ b/packages/testcafe-models/dataGrid/index.ts @@ -1029,7 +1029,20 @@ export default class DataGrid extends GridCore { { dependencies: { getInstance } }, )(); } - + + apiState(): Promise { + const { getInstance } = this; + + return ClientFunction( + () => (getInstance() as any).state(), + { + dependencies: { + getInstance, + }, + }, + )(); + } + getDraggableHeader() { return this.body.find(`.${this.addWidgetPrefix(CLASS.dragHeader)}`); } @@ -1045,4 +1058,15 @@ export default class DataGrid extends GridCore { getAIAssistantButton(): Selector { return this.getHeaderPanel().element.find(`.${this.addWidgetPrefix(CLASS.aiAssistantButton)}`); } + + focusAIAssistantButton(): Promise { + const buttonSelector = this.getAIAssistantButton(); + + return ClientFunction( + () => { + (buttonSelector() as unknown as HTMLElement)?.focus(); + }, + { dependencies: { buttonSelector } }, + )(); + } } diff --git a/packages/testcafe-models/gridCore/index.ts b/packages/testcafe-models/gridCore/index.ts index 3db91006fc21..5f04409a4f31 100644 --- a/packages/testcafe-models/gridCore/index.ts +++ b/packages/testcafe-models/gridCore/index.ts @@ -167,4 +167,4 @@ export default abstract class GridCore extends Widget { }, )(); } -} +} \ No newline at end of file