diff --git a/apps/chat-e2e/src/testData/conversationHistory/conversationData.ts b/apps/chat-e2e/src/testData/conversationHistory/conversationData.ts index bb27dd079d..7bf5b937b9 100644 --- a/apps/chat-e2e/src/testData/conversationHistory/conversationData.ts +++ b/apps/chat-e2e/src/testData/conversationHistory/conversationData.ts @@ -261,7 +261,8 @@ export class ConversationData extends FolderData { return this.conversationBuilder.build(); } - public prepareConversationWithCodeContent( + public prepareConversationWithTextContent( + responseContent: string, model?: string | DialAIEntityModel, ) { const conversation = this.prepareDefaultConversation(model); @@ -272,14 +273,13 @@ export class ConversationData extends FolderData { }; const userMessage: Message = { role: Role.User, - content: 'provide an example of interface declaration in Java', + content: 'request', model: conversation.model, settings: messageSettings, }; const assistantMessage: Message = { role: Role.Assistant, - content: - 'Here is an example of an interface declaration in Java:\n\n```java\npublic interface Animal {\n void eat();\n void sleep();\n void makeSound();\n}\n```\n\nIn this example, `Animal` is an interface that declares three methods: `eat()`, `sleep()`, and `makeSound()`. Any class that implements the `Animal` interface will need to provide implementations for these three methods.', + content: responseContent, model: conversation.model, settings: messageSettings, }; @@ -287,6 +287,22 @@ export class ConversationData extends FolderData { return this.conversationBuilder.build(); } + public prepareConversationWithCodeContent( + model?: string | DialAIEntityModel, + ) { + const responseContent = + 'Here is an example of an interface declaration in Java:\n\n```java\npublic interface Animal {\n void eat();\n void sleep();\n void makeSound();\n}\n```\n\nIn this example, `Animal` is an interface that declares three methods: `eat()`, `sleep()`, and `makeSound()`. Any class that implements the `Animal` interface will need to provide implementations for these three methods.'; + return this.prepareConversationWithTextContent(responseContent, model); + } + + public prepareConversationWithMdTableContent( + model?: string | DialAIEntityModel, + ) { + const responseContent = + '| Country | Capital |\n| ------------- |-------------|\n| Canada | Ottawa |\n| United States | Washington, D.C. |\n'; + return this.prepareConversationWithTextContent(responseContent, model); + } + public prepareAssistantConversation( assistant: DialAIEntityModel, addons: string[], diff --git a/apps/chat-e2e/src/testData/expectedConstants.ts b/apps/chat-e2e/src/testData/expectedConstants.ts index dadb88a5a4..4778d2a33e 100644 --- a/apps/chat-e2e/src/testData/expectedConstants.ts +++ b/apps/chat-e2e/src/testData/expectedConstants.ts @@ -1,5 +1,6 @@ import config from '../../config/playwright.config'; +import { CopyTableType } from '@/chat/types/chat'; import path from 'path'; export const ExpectedConstants = { @@ -129,6 +130,8 @@ export const ExpectedConstants = { endDotFilenameError: (filename: string) => `Using a dot at the end of a name is not permitted. Please rename or delete them from uploading files list: ${filename}`, allFilesRoot: 'All files', + copyTableTooltip: (copyType: CopyTableType) => + `Copy as ${copyType.toUpperCase()}`, }; export enum Groups { diff --git a/apps/chat-e2e/src/testData/expectedMessages.ts b/apps/chat-e2e/src/testData/expectedMessages.ts index 80571ab80b..1b0215e747 100644 --- a/apps/chat-e2e/src/testData/expectedMessages.ts +++ b/apps/chat-e2e/src/testData/expectedMessages.ts @@ -282,4 +282,14 @@ export enum ExpectedMessages { uploadToPathIsTruncated = 'Upload to path is truncated', folderCheckboxIsNotVisible = 'Folder check-box is not visible', stopPlaybackButtonNotVisible = '"Stop playback" button is not visible', + tableIsVisible = 'Table is visible in chat message', + tableControlIconsNotVisible = 'Table control icons are not visible', + tableCopyAsCsvIconIsVisible = 'Table Copy As Csv icon is available', + tableCopyAsTxtIconIsVisible = 'Table Copy As Txt icon is available', + tableCopyAsMdIconIsVisible = 'Table Copy As MD icon is available', + tableColumnsCountIsValid = 'Table columns count is valid', + tableRowsCountIsValid = 'Table rows count is valid', + tableEntityBackgroundColorIsValid = 'Table entity background color is valid', + tableControlTooltipIsVisible = 'Table control tooltip is visible', + copiedContentIsValid = 'Copied content is valid', } diff --git a/apps/chat-e2e/src/tests/mdTableModelResponse.test.ts b/apps/chat-e2e/src/tests/mdTableModelResponse.test.ts new file mode 100644 index 0000000000..5ac372e217 --- /dev/null +++ b/apps/chat-e2e/src/tests/mdTableModelResponse.test.ts @@ -0,0 +1,235 @@ +import { Conversation, CopyTableType } from '@/chat/types/chat'; +import dialTest from '@/src/core/dialFixtures'; +import { + ExpectedConstants, + ExpectedMessages, + ModelIds, + Theme, +} from '@/src/testData'; +import { Colors } from '@/src/ui/domData'; +import { GeneratorUtil } from '@/src/utils'; +import { Locator, expect } from '@playwright/test'; + +const expectedChatMessageIndex = 2; + +dialTest( + 'Check md table in response.\n' + + 'Copy md table as CSV.\n' + + 'Copy md table as TXT.\n' + + 'Copy md table as MD', + async ({ + dialHomePage, + setTestIds, + chatMessages, + tooltip, + sendMessage, + localStorageManager, + conversationData, + dataInjector, + }) => { + setTestIds('EPMRTC-1153', 'EPMRTC-3124', 'EPMRTC-3125', 'EPMRTC-3126'); + let theme: string; + let tableConversation: Conversation; + let copyAsCsvIcon: Locator; + let copyAsTxtIcon: Locator; + let copyAsMdIcon: Locator; + let copyIcons: Locator[] = []; + + const expectedTableDimensions = 2; + const expectedCopyIconTooltips = [ + ExpectedConstants.copyTableTooltip(CopyTableType.CSV), + ExpectedConstants.copyTableTooltip(CopyTableType.TXT), + ExpectedConstants.copyTableTooltip(CopyTableType.MD), + ]; + const expectedCopiedContent = [ + '"Country","Capital"\n' + + '"Canada","Ottawa"\n' + + '"United States","Washington, D.C."', + 'Country\tCapital\n' + + 'Canada\tOttawa\n' + + 'United States\tWashington, D.C.', + '| Country | Capital |\n' + + '| :-- | :-- |\n' + + '| Canada | Ottawa |\n' + + '| United States | Washington, D.C. |', + ]; + + await dialTest.step('Set random application theme', async () => { + theme = GeneratorUtil.randomArrayElement(Object.keys(Theme)); + await localStorageManager.setSettings(theme); + }); + + await dialTest.step( + 'Prepare conversation with table response', + async () => { + tableConversation = + conversationData.prepareConversationWithMdTableContent(); + await dataInjector.createConversations([tableConversation]); + await localStorageManager.setSelectedConversation(tableConversation); + }, + ); + + await dialTest.step( + 'Verify table data is correctly displayed', + async () => { + await dialHomePage.openHomePage(); + await dialHomePage.waitForPageLoaded(); + await expect + .soft( + chatMessages.getChatMessageTable(expectedChatMessageIndex), + ExpectedMessages.tableIsVisible, + ) + .toBeVisible(); + + copyAsCsvIcon = chatMessages.getChatMessageTableCopyAsCsvIcon( + expectedChatMessageIndex, + ); + await expect + .soft(copyAsCsvIcon, ExpectedMessages.tableCopyAsCsvIconIsVisible) + .toBeVisible(); + + copyAsTxtIcon = chatMessages.getChatMessageTableCopyAsTxtIcon( + expectedChatMessageIndex, + ); + await expect + .soft(copyAsTxtIcon, ExpectedMessages.tableCopyAsTxtIconIsVisible) + .toBeVisible(); + + copyAsMdIcon = chatMessages.getChatMessageTableCopyAsMdIcon( + expectedChatMessageIndex, + ); + await expect + .soft(copyAsMdIcon, ExpectedMessages.tableCopyAsMdIconIsVisible) + .toBeVisible(); + expect + .soft( + await chatMessages.getChatMessageTableHeaderColumnsCount( + expectedChatMessageIndex, + ), + ExpectedMessages.tableColumnsCountIsValid, + ) + .toBe(expectedTableDimensions); + expect + .soft( + await chatMessages.getChatMessageTableRowsCount( + expectedChatMessageIndex, + ), + ExpectedMessages.tableRowsCountIsValid, + ) + .toBe(expectedTableDimensions * expectedTableDimensions); + }, + ); + + await dialTest.step( + 'Verify table rows background color is correct', + async () => { + const tableHeaderBackgroundColor = + await chatMessages.getChatMessageTableHeadersBackgroundColor( + expectedChatMessageIndex, + ); + expect + .soft( + tableHeaderBackgroundColor[0], + ExpectedMessages.tableEntityBackgroundColorIsValid, + ) + .toBe( + theme === Theme.dark + ? Colors.backgroundLayer4Dark + : Colors.backgroundLayer4Light, + ); + + const tableRowBackgroundColor = + await chatMessages.getChatMessageTableRowsBackgroundColor( + expectedChatMessageIndex, + ); + expect + .soft( + tableRowBackgroundColor[0], + ExpectedMessages.tableEntityBackgroundColorIsValid, + ) + .toBe( + theme === Theme.dark + ? Colors.backgroundLayer3Dark + : Colors.backgroundLayer3Light, + ); + }, + ); + + await dialTest.step( + 'Verify tooltip is shown on hover over table controls, valid content is copied by click on controls', + async () => { + copyIcons = [copyAsCsvIcon, copyAsTxtIcon, copyAsMdIcon]; + for (let i = 0; i < copyIcons.length; i++) { + await copyIcons[i].hover(); + await expect + .soft( + tooltip.getElementLocator(), + ExpectedMessages.tableControlTooltipIsVisible, + ) + .toBeVisible(); + expect + .soft( + await tooltip.getContent(), + ExpectedMessages.tooltipContentIsValid, + ) + .toBe(expectedCopyIconTooltips[i]); + + await copyIcons[i].click(); + await sendMessage.pasteDataIntoMessageInput(); + expect + .soft( + await sendMessage.getMessage(), + ExpectedMessages.copiedContentIsValid, + ) + .toBe(expectedCopiedContent[i]); + await sendMessage.clearMessageInput(); + } + }, + ); + }, +); + +dialTest( + 'Copy buttons are not shown in MD table if the response is being generated', + async ({ + dialHomePage, + setTestIds, + chatMessages, + chat, + localStorageManager, + conversationData, + dataInjector, + }) => { + setTestIds('EPMRTC-3123'); + let tableConversation: Conversation; + + await dialTest.step('Prepare empty conversation', async () => { + tableConversation = conversationData.prepareEmptyConversation( + ModelIds.GPT_4, + ); + await dataInjector.createConversations([tableConversation]); + await localStorageManager.setSelectedConversation(tableConversation); + }); + + await dialTest.step( + 'Send request to generate MD table and verify copy icons are not available while response is generating', + async () => { + await dialHomePage.openHomePage(); + await dialHomePage.waitForPageLoaded(); + await chat.sendRequestWithButton( + 'Create md table with all countries, its capitals and population', + false, + ); + await chatMessages + .getChatMessageTable(expectedChatMessageIndex) + .waitFor(); + await expect + .soft( + chatMessages.getChatMessageTableControls(expectedChatMessageIndex), + ExpectedMessages.tableControlIconsNotVisible, + ) + .toBeHidden(); + }, + ); + }, +); diff --git a/apps/chat-e2e/src/ui/domData/colors.ts b/apps/chat-e2e/src/ui/domData/colors.ts index d3cbf34bdb..2583837f28 100644 --- a/apps/chat-e2e/src/ui/domData/colors.ts +++ b/apps/chat-e2e/src/ui/domData/colors.ts @@ -11,4 +11,9 @@ export enum Colors { defaultBackground = 'rgba(0, 0, 0, 0)', textPermanent = 'rgb(252, 252, 252)', backgroundAccentPrimaryAlpha = 'rgba(92, 141, 234, 0.17)', + backgroundLayer4Dark = 'rgb(51, 57, 66)', + backgroundLayer4Light = 'rgb(221, 225, 230)', + backgroundLayer3Dark = 'rgb(34, 41, 50)', + // eslint-disable-next-line @typescript-eslint/no-duplicate-enum-values + backgroundLayer3Light = 'rgb(252, 252, 252)', } diff --git a/apps/chat-e2e/src/ui/domData/tags.ts b/apps/chat-e2e/src/ui/domData/tags.ts index 718f076326..70d9da37f3 100644 --- a/apps/chat-e2e/src/ui/domData/tags.ts +++ b/apps/chat-e2e/src/ui/domData/tags.ts @@ -8,4 +8,9 @@ export enum Tags { a = 'a', desc = 'desc', closingTag = '/>', + table = 'table', + thead = 'thead', + th = 'th', + tbody = 'tbody', + td = 'td', } diff --git a/apps/chat-e2e/src/ui/selectors/chatSelectors.ts b/apps/chat-e2e/src/ui/selectors/chatSelectors.ts index ec4e209d73..42974fb320 100644 --- a/apps/chat-e2e/src/ui/selectors/chatSelectors.ts +++ b/apps/chat-e2e/src/ui/selectors/chatSelectors.ts @@ -142,3 +142,11 @@ export const ChatSelectors = { messageSpinner: '[data-qa="message-input-spinner"]', plotlyContainer: '.plot-container', }; + +export const TableSelectors = { + tableContainer: '[data-qa="table"]', + tableControls: '[data-qa="table-controls"]', + copyAsCsvIcon: '[data-qa="csv-icon"]', + copyAsTxtIcon: '[data-qa="txt-icon"]', + copyAsMdIcon: '[data-qa="md-icon"]', +}; diff --git a/apps/chat-e2e/src/ui/webElements/chatMessages.ts b/apps/chat-e2e/src/ui/webElements/chatMessages.ts index 7be9f65b4f..a1174f82ff 100644 --- a/apps/chat-e2e/src/ui/webElements/chatMessages.ts +++ b/apps/chat-e2e/src/ui/webElements/chatMessages.ts @@ -3,12 +3,13 @@ import { ChatSelectors, MessageInputSelectors, SideBarSelectors, + TableSelectors, } from '../selectors'; import { BaseElement } from './baseElement'; import { isApiStorageType } from '@/src/hooks/global-setup'; import { Rate, Side } from '@/src/testData'; -import { Attributes, Tags } from '@/src/ui/domData'; +import { Attributes, Styles, Tags } from '@/src/ui/domData'; import { keys } from '@/src/ui/keyboard'; import { IconSelectors } from '@/src/ui/selectors/iconSelectors'; import { MenuSelectors } from '@/src/ui/selectors/menuSelectors'; @@ -106,6 +107,48 @@ export class ChatMessages extends BaseElement { return this.getChatMessage(message).locator(ChatSelectors.codeBlock); } + public getChatMessageTable(message: string | number) { + return this.getChatMessage(message).locator(TableSelectors.tableContainer); + } + + public getChatMessageTableControls(message: string | number) { + return this.getChatMessageTable(message).locator( + TableSelectors.tableControls, + ); + } + + public getChatMessageTableCopyAsCsvIcon(message: string | number) { + return this.getChatMessageTableControls(message).locator( + TableSelectors.copyAsCsvIcon, + ); + } + + public getChatMessageTableCopyAsTxtIcon(message: string | number) { + return this.getChatMessageTableControls(message).locator( + TableSelectors.copyAsTxtIcon, + ); + } + + public getChatMessageTableCopyAsMdIcon(message: string | number) { + return this.getChatMessageTableControls(message).locator( + TableSelectors.copyAsMdIcon, + ); + } + + public getChatMessageTableHeaderColumns(message: string | number) { + return this.getChatMessageTable(message) + .locator(Tags.table) + .locator(Tags.thead) + .locator(Tags.th); + } + + public getChatMessageTableRows(message: string | number) { + return this.getChatMessageTable(message) + .locator(Tags.table) + .locator(Tags.tbody) + .locator(Tags.td); + } + public getMessageStage(messagesIndex: number, stageIndex: number) { return this.messageStage(messagesIndex, stageIndex).locator( ChatSelectors.openedStage, @@ -420,6 +463,30 @@ export class ChatMessages extends BaseElement { return this.getChatMessage(message).locator(MenuSelectors.menuTrigger); } + public async getChatMessageTableHeaderColumnsCount(message: string | number) { + return this.getChatMessageTableHeaderColumns(message).count(); + } + + public async getChatMessageTableHeadersBackgroundColor( + message: string | number, + ) { + return this.createElementFromLocator( + this.getChatMessageTableHeaderColumns(message).nth(1), + ).getComputedStyleProperty(Styles.backgroundColor); + } + + public async getChatMessageTableRowsCount(message: string | number) { + return this.getChatMessageTableRows(message).count(); + } + + public async getChatMessageTableRowsBackgroundColor( + message: string | number, + ) { + return this.createElementFromLocator( + this.getChatMessageTableRows(message).nth(1), + ).getComputedStyleProperty(Styles.backgroundColor); + } + public messageEditIcon = (messageLocator: Locator) => messageLocator.locator(IconSelectors.editIcon); public saveAndSubmit = this.getChildElementBySelector( diff --git a/apps/chat-e2e/src/ui/webElements/folders.ts b/apps/chat-e2e/src/ui/webElements/folders.ts index 8d57a34260..f29b1aabe2 100644 --- a/apps/chat-e2e/src/ui/webElements/folders.ts +++ b/apps/chat-e2e/src/ui/webElements/folders.ts @@ -127,9 +127,14 @@ export class Folders extends BaseElement { ); } + public getFolderExpandIcon(name: string, index?: number) { + return this.getFolderByName(name, index).locator( + `${Tags.span}[class='${Attributes.visible}']`, + ); + } + public async isFolderCaretExpanded(name: string, index?: number) { - return this.getFolderByName(name, index) - .locator(`${Tags.span}[class='${Attributes.visible}']`) + return this.getFolderExpandIcon(name, index) .locator(`.${Attributes.rotated}`) .isVisible(); } @@ -231,11 +236,7 @@ export class Folders extends BaseElement { const respPromise = this.page.waitForResponse((resp) => resp.url().includes(API.listingHost), ); - if (await this.getTooltip().isVisible()) { - await this.page.mouse.move(0, 0); - await this.getTooltip().waitForState({ state: 'hidden' }); - } - await folder.click(); + await this.getFolderExpandIcon(name, index).click(); return respPromise; } await folder.click(); diff --git a/apps/chat-e2e/src/ui/webElements/sendMessage.ts b/apps/chat-e2e/src/ui/webElements/sendMessage.ts index d0b890a945..5bd157166d 100644 --- a/apps/chat-e2e/src/ui/webElements/sendMessage.ts +++ b/apps/chat-e2e/src/ui/webElements/sendMessage.ts @@ -87,6 +87,10 @@ export class SendMessage extends BaseElement { await this.page.keyboard.press(keys.ctrlPlusV); } + public async clearMessageInput() { + await this.messageInput.getElementLocator().clear(); + } + public async fillRequestData(message?: string) { await this.messageInput.waitForState(); await this.sendMessageButton.waitForState(); diff --git a/apps/chat/src/components/Markdown/Table.tsx b/apps/chat/src/components/Markdown/Table.tsx index 1629097acd..9293e26826 100644 --- a/apps/chat/src/components/Markdown/Table.tsx +++ b/apps/chat/src/components/Markdown/Table.tsx @@ -18,15 +18,17 @@ interface CopyIconProps { Icon: FC; onClick: () => void; copied: boolean; + type: CopyTableType; } -const CopyIcon = ({ Icon, onClick, copied }: CopyIconProps) => { +const CopyIcon = ({ Icon, onClick, copied, type }: CopyIconProps) => { const IconComponent = copied ? IconCheck : Icon; return ( { if (!copied) { onClick(); @@ -147,15 +149,19 @@ export const Table = ({ children, isLastMessageStreaming }: Props) => { ); return ( -
+
{!isLastMessageStreaming && ( -
+
@@ -163,6 +169,7 @@ export const Table = ({ children, isLastMessageStreaming }: Props) => { Icon={IconTxt} onClick={copyTableToTXT} copied={CopyTableType.TXT === copiedType} + type={CopyTableType.TXT} /> @@ -170,6 +177,7 @@ export const Table = ({ children, isLastMessageStreaming }: Props) => { Icon={IconMarkdown} onClick={copyTableToMD} copied={CopyTableType.MD === copiedType} + type={CopyTableType.MD} />