diff --git a/apps/chat-e2e/src/assertions/promptModalAssertion.ts b/apps/chat-e2e/src/assertions/promptModalAssertion.ts new file mode 100644 index 0000000000..b19aa03889 --- /dev/null +++ b/apps/chat-e2e/src/assertions/promptModalAssertion.ts @@ -0,0 +1,51 @@ +import { ExpectedMessages } from '@/src/testData'; +import { Colors } from '@/src/ui/domData'; +import { PromptModalDialog } from '@/src/ui/webElements'; +import { expect } from '@playwright/test'; + +export class PromptModalAssertion { + readonly promptModalDialog: PromptModalDialog; + + constructor(promptModalDialog: PromptModalDialog) { + this.promptModalDialog = promptModalDialog; + } + + public async assertNameFieldIsInvalid(expectedErrorMessage: string) { + const nameBorderColors = + await this.promptModalDialog.name.getAllBorderColors(); + Object.values(nameBorderColors).forEach((borders) => { + borders.forEach((borderColor) => { + expect + .soft(borderColor, ExpectedMessages.fieldIsHighlightedWithRed) + .toBe(Colors.textError); + }); + }); + + const nameFieldErrorMessage = this.promptModalDialog.getFieldBottomMessage( + this.promptModalDialog.name, + ); + await nameFieldErrorMessage.waitFor(); + + await expect + .soft(nameFieldErrorMessage, ExpectedMessages.promptNameInvalid) + .toHaveText(expectedErrorMessage); + } + + public async assertNameFieldIsEmpty() { + expect + .soft( + await this.promptModalDialog.getName(), + ExpectedMessages.charactersAreNotDisplayed, + ) + .toBe(''); + } + + public async assertPromptNameIsValid(expectedPromptName: string) { + expect + .soft( + await this.promptModalDialog.getName(), + ExpectedMessages.promptNameValid, + ) + .toBe(expectedPromptName); + } +} diff --git a/apps/chat-e2e/src/core/dialFixtures.ts b/apps/chat-e2e/src/core/dialFixtures.ts index b627f4cda2..892a3a0b77 100644 --- a/apps/chat-e2e/src/core/dialFixtures.ts +++ b/apps/chat-e2e/src/core/dialFixtures.ts @@ -27,6 +27,7 @@ import { FolderAssertion } from '@/src/assertions/folderAssertion'; import { FooterAssertion } from '@/src/assertions/footerAssertion'; import { MenuAssertion } from '@/src/assertions/menuAssertion'; import { PromptListAssertion } from '@/src/assertions/promptListAssertion'; +import { PromptModalAssertion } from '@/src/assertions/promptModalAssertion'; import { SendMessageAssertion } from '@/src/assertions/sendMessageAssertion'; import { SettingsModalAssertion } from '@/src/assertions/settingsModalAssertion'; import { SideBarAssertion } from '@/src/assertions/sideBarAssertion'; @@ -187,6 +188,7 @@ const dialTest = test.extend< conversationAssertion: SideBarEntityAssertion; chatBarFolderAssertion: FolderAssertion; errorToastAssertion: ErrorToastAssertion; + promptModalAssertion: PromptModalAssertion; tooltipAssertion: TooltipAssertion; confirmationDialogAssertion: ConfirmationDialogAssertion; chatBarAssertion: SideBarAssertion; @@ -625,6 +627,10 @@ const dialTest = test.extend< const promptErrorToastAssertion = new ErrorToastAssertion(errorToast); await use(promptErrorToastAssertion); }, + promptModalAssertion: async ({ promptModalDialog }, use) => { + const promptModalAssertion = new PromptModalAssertion(promptModalDialog); + await use(promptModalAssertion); + }, tooltipAssertion: async ({ tooltip }, use) => { const tooltipAssertion = new TooltipAssertion(tooltip); await use(tooltipAssertion); diff --git a/apps/chat-e2e/src/testData/expectedMessages.ts b/apps/chat-e2e/src/testData/expectedMessages.ts index dc79cd0754..43cc40622a 100644 --- a/apps/chat-e2e/src/testData/expectedMessages.ts +++ b/apps/chat-e2e/src/testData/expectedMessages.ts @@ -58,6 +58,7 @@ export enum ExpectedMessages { promptModalClosed = 'Prompt modal dialog is closed', promptNotUpdated = 'Prompt is not updated', promptNameValid = 'Prompt name is valid', + promptNameInvalid = 'Prompt name is not valid', promptDescriptionValid = 'Prompt description is valid', promptContentValid = 'Prompt content is valid', promptVariablePlaceholderValid = 'Prompt variable placeholder is valid', diff --git a/apps/chat-e2e/src/testData/prompts/promptData.ts b/apps/chat-e2e/src/testData/prompts/promptData.ts index 82177d5c42..f0849098cb 100644 --- a/apps/chat-e2e/src/testData/prompts/promptData.ts +++ b/apps/chat-e2e/src/testData/prompts/promptData.ts @@ -24,7 +24,11 @@ export class PromptData extends FolderData { public prepareDefaultPrompt(name?: string) { const promptName = name ?? GeneratorUtil.randomString(10); - return this.promptBuilder.withName(promptName).withId(promptName).build(); + return this.promptBuilder + .withName(promptName) + .withId(promptName) + .withContent(promptName) + .build(); } public prepareDefaultSharedPrompt(name?: string) { diff --git a/apps/chat-e2e/src/tests/promptFoldersSpecialChars.test.ts b/apps/chat-e2e/src/tests/promptFoldersSpecialChars.test.ts index 5a876d472e..daf70bfce1 100644 --- a/apps/chat-e2e/src/tests/promptFoldersSpecialChars.test.ts +++ b/apps/chat-e2e/src/tests/promptFoldersSpecialChars.test.ts @@ -111,7 +111,7 @@ dialTest( { name: expectedFolderName }, 'visible', ); - errorToastAssertion.assertToastIsHidden(); + await errorToastAssertion.assertToastIsHidden(); }, ); diff --git a/apps/chat-e2e/src/tests/promptMaximumNameLength.test.ts b/apps/chat-e2e/src/tests/promptMaximumNameLength.test.ts new file mode 100644 index 0000000000..098d06ae1a --- /dev/null +++ b/apps/chat-e2e/src/tests/promptMaximumNameLength.test.ts @@ -0,0 +1,172 @@ +import dialTest from '@/src/core/dialFixtures'; +import { + ExpectedConstants, + ExpectedMessages, + MenuOptions, +} from '@/src/testData'; +import { Overflow, Styles } from '@/src/ui/domData'; +import { expect } from '@playwright/test'; + +dialTest( + 'Prompt name consists of a maximum of 160 symbols.\n' + + 'Long prompt name is cut in the panel.\n' + + 'Prompt folder name consists of a maximum of 160 symbols', + async ({ + dialHomePage, + promptData, + dataInjector, + prompts, + promptDropdownMenu, + promptModalDialog, + errorToastAssertion, + promptAssertion, + setTestIds, + promptBarFolderAssertion, + promptBar, + folderPrompts, + }) => { + setTestIds('EPMRTC-3171', 'EPMRTC-958', 'EPMRTC-3168'); + const prompt = promptData.prepareDefaultPrompt(); + await dataInjector.createPrompts([prompt]); + const longName = + 'Lorem ipsum dolor sit amett consectetur adipiscing elit. Nullam ultricies ipsum nullaa nec viverra lectus rutrum id. Sed volutpat ante ac fringilla turpis duis!ABC'; + const expectedName = longName.substring( + 0, + ExpectedConstants.maxEntityNameLength, + ); + const nameUnder160Symbols = + 'This prompt is renamed to very long-long-long name to see how the system cuts the name'; + + await dialTest.step( + 'Create a prompt and enter text longer than 160 symbols', + async () => { + await dialHomePage.openHomePage(); + await dialHomePage.waitForPageLoaded({ + isNewConversationVisible: true, + }); + await promptBar.createNewPrompt(); + await promptModalDialog.setField(promptModalDialog.name, longName); + await promptModalDialog.setField( + promptModalDialog.prompt, + ExpectedConstants.newPromptTitle(1), + ); + }, + ); + + await dialTest.step('Save the prompt', async () => { + await promptModalDialog.saveButton.click(); + }); + + await dialTest.step( + 'Verify the prompt name is cut to 160 symbols and no error toast is shown', + async () => { + await promptAssertion.assertEntityState( + { name: expectedName }, + 'visible', + ); + await errorToastAssertion.assertToastIsHidden(); + }, + ); + + await dialTest.step('Rename the prompt to a long name', async () => { + await prompts.openEntityDropdownMenu(expectedName); + await promptDropdownMenu.selectMenuOption(MenuOptions.edit); + await promptModalDialog.setField( + promptModalDialog.name, + nameUnder160Symbols, + ); + // Wait for the API request to update the prompt name + await promptModalDialog.updatePromptDetailsWithButton( + nameUnder160Symbols, + prompt.description, + prompt.content!, + ); + prompt.name = nameUnder160Symbols; + }); + + await dialTest.step('Check the prompt name in the panel', async () => { + const promptNameElement = prompts.getPromptName(prompt.name); + const promptNameOverflow = + await promptNameElement.getComputedStyleProperty(Styles.text_overflow); + expect + .soft(promptNameOverflow[0], ExpectedMessages.entityNameIsTruncated) + .toBe(Overflow.ellipsis); + }); + + await dialTest.step( + 'Hover over the prompt name and check the name in the panel', + async () => { + await prompts.getPromptName(prompt.name).hoverOver(); + await promptAssertion.assertEntityDotsMenuState( + { name: prompt.name }, + 'visible', + ); + }, + ); + + await dialTest.step( + 'Create two folders: Folder_parent -> Folder_child', + async () => { + for (let i = 1; i <= 2; i++) { + await promptBar.createNewFolder(); + await promptBarFolderAssertion.assertFolderState( + { name: ExpectedConstants.newPromptFolderWithIndexTitle(i) }, + 'visible', + ); + } + + await promptBar.dragAndDropEntityToFolder( + folderPrompts.getFolderByName( + ExpectedConstants.newPromptFolderWithIndexTitle(2), + ), + folderPrompts.getFolderByName( + ExpectedConstants.newPromptFolderWithIndexTitle(1), + ), + ); + }, + ); + + await dialTest.step( + 'Edit both folder names with more than 160 symbols names', + async () => { + // Rename Folder_parent + await folderPrompts.openFolderDropdownMenu( + ExpectedConstants.newPromptFolderWithIndexTitle(1), + ); + await promptDropdownMenu.selectMenuOption(MenuOptions.rename); + await folderPrompts.editFolderNameWithTick(longName); + + // Rename folder_child + await folderPrompts.openFolderDropdownMenu( + ExpectedConstants.newPromptFolderWithIndexTitle(2), + ); + await promptDropdownMenu.selectMenuOption(MenuOptions.rename); + await folderPrompts.editFolderNameWithTick(longName); + }, + ); + + await dialTest.step( + 'Check that the folder names are cut to 160 symbols and no error message appears', + async () => { + // Get the actual folder names + const parentFolderName = await folderPrompts + .getFolderName(expectedName, 1) + .getElementInnerContent(); + const childFolderName = await folderPrompts + .getFolderName(expectedName, 2) + .getElementInnerContent(); + + // Assert that the names are truncated to the expectedName + expect + .soft(parentFolderName, ExpectedMessages.folderNameUpdated) + .toBe(expectedName); + expect + .soft(childFolderName, ExpectedMessages.folderNameUpdated) + .toBe(expectedName); + + // Assert that no error toast is shown + await errorToastAssertion.assertToastIsHidden(); + }, + ); + }, +); diff --git a/apps/chat-e2e/src/tests/promptsNames.test.ts b/apps/chat-e2e/src/tests/promptsNames.test.ts new file mode 100644 index 0000000000..0408e68194 --- /dev/null +++ b/apps/chat-e2e/src/tests/promptsNames.test.ts @@ -0,0 +1,196 @@ +import dialTest from '@/src/core/dialFixtures'; +import { + ExpectedConstants, + ExpectedMessages, + MenuOptions, +} from '@/src/testData'; + +dialTest( + 'Error message appears if to add a dot to the end of prompt name.\n' + + 'Prompt name: allowed special characters.\n' + + 'Prompt name: restricted special characters are not allowed to be entered while renaming.\n' + + 'Prompt name: restricted special characters are removed from prompt name if to copy-paste.\n' + + 'Prompt name: smiles, hieroglyph, specific letters in name.\n' + + 'Prompt name: spaces in the middle of prompt name stay', + async ({ + dialHomePage, + promptData, + dataInjector, + prompts, + promptDropdownMenu, + promptModalDialog, + errorToast, + errorToastAssertion, + promptAssertion, + setTestIds, + promptModalAssertion, + }) => { + setTestIds( + 'EPMRTC-2991', + 'EPMRTC-1278', + 'EPMRTC-2993', + 'EPMRTC-2994', + 'EPMRTC-2997', + 'EPMRTC-3085', + ); + const prompt = promptData.prepareDefaultPrompt(); + await dataInjector.createPrompts([prompt]); + const newNameWithDot = `${ExpectedConstants.newPromptTitle(1)}.`; + const nameWithRestrictedChars = `Prompt${ExpectedConstants.restrictedNameChars}_name`; + const expectedPromptName = 'Prompt_name'; + const longNameWithEmojis = + '๐Ÿ˜‚๐Ÿ‘๐Ÿฅณ ๐Ÿ˜ท ๐Ÿคง ๐Ÿค  ๐Ÿฅด๐Ÿ˜‡ ๐Ÿ˜ˆ โญใ‚ใŠใ…ใ„นรฑยฟรครŸ๐Ÿ˜‚๐Ÿ‘๐Ÿฅณ ๐Ÿ˜ท ๐Ÿคง ๐Ÿค  ๐Ÿฅด๐Ÿ˜‡ ๐Ÿ˜ˆ โญใ‚ใŠใ…ใ„นรฑยฟรครŸ๐Ÿ˜‚๐Ÿ‘๐Ÿฅณ ๐Ÿ˜ท ๐Ÿคง ๐Ÿค  ๐Ÿฅด๐Ÿ˜‡ ๐Ÿ˜ˆ โญใ‚ใŠใ…ใ„นรฑยฟรครŸ'; + const nameWithSpaces = ' Prompt 1 '; + const expectedNameWithSpaces = nameWithSpaces.trim(); + + await dialTest.step('Add a dot at the end of a prompt name', async () => { + await dialHomePage.openHomePage(); + await dialHomePage.waitForPageLoaded({ + isNewConversationVisible: true, + }); + await prompts.openEntityDropdownMenu(prompt.name); + await promptDropdownMenu.selectMenuOption(MenuOptions.edit); + await promptModalDialog.setField(promptModalDialog.name, newNameWithDot); + }); + + await dialTest.step( + 'Check that the name field is red-bordered and an error message appears', + async () => { + await promptModalAssertion.assertNameFieldIsInvalid( + ExpectedConstants.nameWithDotErrorMessage, + ); + }, + ); + + await dialTest.step( + 'Fill in the prompt body and click the Save button', + async () => { + await promptModalDialog.setField( + promptModalDialog.prompt, + ExpectedConstants.newPromptTitle(1), + ); + await promptModalDialog.saveButton.click(); + }, + ); + + await dialTest.step('Check that a UI error appears', async () => { + await errorToastAssertion.assertToastIsVisible(); + await errorToastAssertion.assertToastMessage( + ExpectedConstants.nameWithDotErrorMessage, + ExpectedMessages.notAllowedNameErrorShown, + ); + // Wating for (Closing) the toast to move forward + await errorToast.waitForState({ state: 'hidden' }); + }); + + await dialTest.step( + 'Type restricted characters one by one in the Rename prompt dialog', + async () => { + for (const char of ExpectedConstants.restrictedNameChars.split('')) { + await promptModalDialog.setField(promptModalDialog.name, char); + await promptModalAssertion.assertNameFieldIsEmpty(); + } + }, + ); + + await dialTest.step( + 'Copy and paste restricted characters to the prompt name and verify the name', + async () => { + await dialHomePage.copyToClipboard(nameWithRestrictedChars); + await promptModalDialog.name.click(); + await dialHomePage.pasteFromClipboard(); + await promptModalAssertion.assertPromptNameIsValid(expectedPromptName); + await promptModalDialog.saveButton.click(); + prompt.name = expectedPromptName; + }, + ); + + await dialTest.step( + 'Verify the prompt is created and no error toast is shown', + async () => { + await promptAssertion.assertEntityState( + { name: expectedPromptName }, + 'visible', + ); + await errorToastAssertion.assertToastIsHidden(); + }, + ); + + await dialTest.step( + 'Add special characters to the prompt name', + async () => { + await prompts.openEntityDropdownMenu(prompt.name); + await promptDropdownMenu.selectMenuOption(MenuOptions.edit); + await promptModalDialog.setField( + promptModalDialog.name, + ExpectedConstants.allowedSpecialSymbolsInName(), + ); + await promptModalDialog.setField( + promptModalDialog.prompt, + ExpectedConstants.newPromptTitle(1), + ); + await promptModalDialog.saveButton.click(); + prompt.name = ExpectedConstants.allowedSpecialSymbolsInName(); + }, + ); + + await dialTest.step( + 'Verify the prompt is created and no error toast is shown', + async () => { + await promptAssertion.assertEntityState( + { name: prompt.name }, + 'visible', + ); + await errorToastAssertion.assertToastIsHidden(); + }, + ); + + await dialTest.step( + 'Update the prompt name to a long name with emojis', + async () => { + await prompts.openEntityDropdownMenu(prompt.name); + await promptDropdownMenu.selectMenuOption(MenuOptions.edit); + await promptModalDialog.setField( + promptModalDialog.name, + longNameWithEmojis, + ); + await promptModalDialog.setField( + promptModalDialog.prompt, + ExpectedConstants.newPromptTitle(1), + ); + await promptModalDialog.saveButton.click(); + prompt.name = longNameWithEmojis; + }, + ); + + await dialTest.step( + 'Verify the prompt is renamed successfully and the name looks fine on the Prompt panel', + async () => { + await promptAssertion.assertEntityState( + { name: prompt.name }, + 'visible', + ); + await errorToastAssertion.assertToastIsHidden(); + }, + ); + + await dialTest.step( + 'Update the prompt name to " Prompt 1 " (spaces before, after, and in the middle)', + async () => { + await prompts.openEntityDropdownMenu(prompt.name); + await promptDropdownMenu.selectMenuOption(MenuOptions.edit); + await promptModalDialog.setField( + promptModalDialog.name, + nameWithSpaces, + ); + await promptModalDialog.saveButton.click(); + prompt.name = expectedNameWithSpaces; + }, + ); + + await dialTest.step('Verify the prompt name is "Prompt 1"', async () => { + await promptAssertion.assertEntityState({ name: prompt.name }, 'visible'); + await errorToastAssertion.assertToastIsHidden(); + }); + }, +); diff --git a/apps/chat-e2e/src/ui/selectors/sideBarSelectors.ts b/apps/chat-e2e/src/ui/selectors/sideBarSelectors.ts index c753bd7eec..abdc3dbf11 100644 --- a/apps/chat-e2e/src/ui/selectors/sideBarSelectors.ts +++ b/apps/chat-e2e/src/ui/selectors/sideBarSelectors.ts @@ -41,6 +41,7 @@ export const PromptBarSelectors = { newPromptButton: '[data-qa="new-prompt"]', prompts: '[data-qa="prompts-section-container"] >> [data-qa="prompts"]', prompt: '[data-qa="prompt"]', + promptName: '[data-qa="prompt-name"]', deletePrompts: '[data-qa="delete-prompts"]', pinnedChats: () => `${PromptBarSelectors.promptFolders} > [data-qa="pinned-prompts-container"]`, diff --git a/apps/chat-e2e/src/ui/webElements/promptModalDialog.ts b/apps/chat-e2e/src/ui/webElements/promptModalDialog.ts index 5e27ad5cd8..f9e470e9e0 100644 --- a/apps/chat-e2e/src/ui/webElements/promptModalDialog.ts +++ b/apps/chat-e2e/src/ui/webElements/promptModalDialog.ts @@ -26,7 +26,7 @@ export class PromptModalDialog extends BaseElement { public async fillPromptDetails( name: string, - description: string, + description: string | undefined, value: string, ) { await this.name.click(); @@ -34,7 +34,9 @@ export class PromptModalDialog extends BaseElement { await this.name.typeInInput(name); await this.description.click(); await this.page.keyboard.press(keys.ctrlPlusA); - await this.description.typeInInput(description); + if (description !== undefined) { + await this.description.typeInInput(description); + } await this.prompt.click(); await this.page.keyboard.press(keys.ctrlPlusA); await this.prompt.typeInInput(value); @@ -49,7 +51,7 @@ export class PromptModalDialog extends BaseElement { public async updatePromptDetailsWithButton( name: string, - description: string, + description: string | undefined, value: string, ) { await this.updatePromptDetails(name, description, value, () => @@ -69,7 +71,7 @@ export class PromptModalDialog extends BaseElement { public async updatePromptDetails( name: string, - description: string, + description: string | undefined, value: string, method: () => Promise, ) { diff --git a/apps/chat-e2e/src/ui/webElements/prompts.ts b/apps/chat-e2e/src/ui/webElements/prompts.ts index 0e2a0ffe7c..8570f65203 100644 --- a/apps/chat-e2e/src/ui/webElements/prompts.ts +++ b/apps/chat-e2e/src/ui/webElements/prompts.ts @@ -17,4 +17,7 @@ export class Prompts extends SideBarEntities { return response.request().postDataJSON(); } } + public getPromptName(name: string, index?: number) { + return this.getEntityName(PromptBarSelectors.promptName, name, index); + } } diff --git a/apps/chat/src/components/Promptbar/components/Prompt.tsx b/apps/chat/src/components/Promptbar/components/Prompt.tsx index d6997e1ef6..079a47a952 100644 --- a/apps/chat/src/components/Promptbar/components/Prompt.tsx +++ b/apps/chat/src/components/Promptbar/components/Prompt.tsx @@ -406,7 +406,10 @@ export const PromptComponent = ({ -
+