diff --git a/apps/chat-e2e/src/assertions/agentInfoAssertion.ts b/apps/chat-e2e/src/assertions/agentInfoAssertion.ts index 0e9186ed0..b401eeb6b 100644 --- a/apps/chat-e2e/src/assertions/agentInfoAssertion.ts +++ b/apps/chat-e2e/src/assertions/agentInfoAssertion.ts @@ -1,3 +1,4 @@ +import { DialAIEntityModel } from '@/chat/types/models'; import { BaseAssertion } from '@/src/assertions/base/baseAssertion'; import { ExpectedMessages } from '@/src/testData'; import { AgentInfo } from '@/src/ui/webElements'; @@ -18,10 +19,10 @@ export class AgentInfoAssertion extends BaseAssertion { ); } - public async assertDescription(expectedDescription?: string) { + public async assertShortDescription(expectedModel: DialAIEntityModel) { const description = await this.agentInfo.getAgentDescription(); expect .soft(description, ExpectedMessages.agentDescriptionIsValid) - .toBe(expectedDescription ?? ''); + .toBe(expectedModel.description?.split(/\s*\n\s*\n\s*/g)[0] ?? ''); } } diff --git a/apps/chat-e2e/src/core/dialOverlayFixtures.ts b/apps/chat-e2e/src/core/dialOverlayFixtures.ts index b70f56abf..76ee7a4c8 100644 --- a/apps/chat-e2e/src/core/dialOverlayFixtures.ts +++ b/apps/chat-e2e/src/core/dialOverlayFixtures.ts @@ -30,13 +30,17 @@ import { } from '@/src/assertions'; import { OverlayAssertion } from '@/src/assertions/overlay/overlayAssertion'; import test from '@/src/core/baseFixtures'; +import { LocalStorageManager } from '@/src/core/localStorageManager'; +import { isApiStorageType } from '@/src/hooks/global-setup'; import { FileApiHelper, IconApiHelper, ItemApiHelper, PublicationApiHelper, + ShareApiHelper, } from '@/src/testData/api'; import { ApiInjector } from '@/src/testData/injector/apiInjector'; +import { BrowserStorageInjector } from '@/src/testData/injector/browserStorageInjector'; import { DataInjectorInterface } from '@/src/testData/injector/dataInjectorInterface'; import { OverlayHomePage } from '@/src/ui/pages/overlay/overlayHomePage'; import { OverlayMarketplacePage } from '@/src/ui/pages/overlay/overlayMarketplacePage'; @@ -48,11 +52,15 @@ import { import { ReportAnIssueModal } from '@/src/ui/webElements/footer/reportAnIssueModal'; import { RequestApiKeyModal } from '@/src/ui/webElements/footer/requestApiKeyModal'; import { Header } from '@/src/ui/webElements/header'; +import { Actions } from '@/src/ui/webElements/overlay/actions'; +import { Configuration } from '@/src/ui/webElements/overlay/configuration'; +import { Dialog } from '@/src/ui/webElements/overlay/dialog'; import { ProfilePanel } from '@/src/ui/webElements/overlay/profilePanel'; import { PlaybackControl } from '@/src/ui/webElements/playbackControl'; import { SettingsModal } from '@/src/ui/webElements/settingsModal'; import { ShareModal } from '@/src/ui/webElements/shareModal'; import { BucketUtil } from '@/src/utils'; +import { Page } from '@playwright/test'; import path from 'path'; import { APIRequestContext } from 'playwright-core'; import * as process from 'process'; @@ -106,8 +114,19 @@ const dialOverlayTest = test.extend<{ overlayAssertion: OverlayAssertion; overlayConversationAssertion: ConversationAssertion; overlayPromptAssertion: PromptAssertion; + overlayShareApiHelper: ShareApiHelper; adminUserRequestContext: APIRequestContext; adminPublicationApiHelper: PublicationApiHelper; + adminShareApiHelper: ShareApiHelper; + adminItemApiHelper: ItemApiHelper; + adminApiInjector: ApiInjector; + adminBrowserStorageInjector: BrowserStorageInjector; + adminPage: Page; + adminLocalStorageManager: LocalStorageManager; + adminDataInjector: DataInjectorInterface; + overlayActions: Actions; + overlayConfiguration: Configuration; + overlayDialog: Dialog; }>({ // eslint-disable-next-line no-empty-pattern storageState: async ({}, use) => { @@ -354,6 +373,10 @@ const dialOverlayTest = test.extend<{ const promptAssertion = new PromptAssertion(overlayPrompts); await use(promptAssertion); }, + overlayShareApiHelper: async ({ request }, use) => { + const overlayShareApiHelper = new ShareApiHelper(request); + await use(overlayShareApiHelper); + }, adminUserRequestContext: async ({ playwright }, use) => { const adminUserRequestContext = await playwright.request.newContext({ storageState: overlayStateFilePath(+config.workers!), @@ -367,6 +390,60 @@ const dialOverlayTest = test.extend<{ ); await use(adminPublicationApiHelper); }, + adminShareApiHelper: async ({ adminUserRequestContext }, use) => { + const adminShareApiHelper = new ShareApiHelper(adminUserRequestContext); + await use(adminShareApiHelper); + }, + adminItemApiHelper: async ({ adminUserRequestContext }, use) => { + const adminItemApiHelper = new ItemApiHelper( + adminUserRequestContext, + BucketUtil.getAdminUserBucket(), + ); + await use(adminItemApiHelper); + }, + adminApiInjector: async ({ adminItemApiHelper }, use) => { + const adminApiInjector = new ApiInjector(adminItemApiHelper); + await use(adminApiInjector); + }, + adminPage: async ({ browser }, use) => { + const context = await browser.newContext({ + storageState: overlayStateFilePath(+config.workers!), + }); + const adminPage = await context.newPage(); + await use(adminPage); + await context.close(); + }, + adminLocalStorageManager: async ({ adminPage }, use) => { + const adminLocalStorageManager = new LocalStorageManager(adminPage); + await use(adminLocalStorageManager); + }, + adminBrowserStorageInjector: async ({ adminLocalStorageManager }, use) => { + const adminBrowserStorageInjector = new BrowserStorageInjector( + adminLocalStorageManager, + ); + await use(adminBrowserStorageInjector); + }, + adminDataInjector: async ( + { adminApiInjector, adminBrowserStorageInjector }, + use, + ) => { + const adminDataInjector = isApiStorageType + ? adminApiInjector + : adminBrowserStorageInjector; + await use(adminDataInjector); + }, + overlayActions: async ({ overlayHomePage }, use) => { + const overlayActions = overlayHomePage.getActions(); + await use(overlayActions); + }, + overlayConfiguration: async ({ overlayHomePage }, use) => { + const overlayConfiguration = overlayHomePage.getConfiguration(); + await use(overlayConfiguration); + }, + overlayDialog: async ({ page }, use) => { + const overlayDialog = new Dialog(page); + await use(overlayDialog); + }, }); export default dialOverlayTest; diff --git a/apps/chat-e2e/src/core/localStorageManager.ts b/apps/chat-e2e/src/core/localStorageManager.ts index a8b10cf99..9e9828831 100644 --- a/apps/chat-e2e/src/core/localStorageManager.ts +++ b/apps/chat-e2e/src/core/localStorageManager.ts @@ -170,19 +170,23 @@ export class LocalStorageManager { } async getSelectedConversationIds(originHost?: string) { - let selectedConversationIds; - const storage = await this.page.context().storageState(); - let origin; - if (originHost) { - origin = storage.origins.find((o) => o.origin === originHost); - } else { - origin = storage.origins[0]; - } - if (origin) { - selectedConversationIds = origin.localStorage.find( - (s) => s.name === 'selectedConversationIds', - )?.value; - } + const selectedConversationIds = await this.getKey( + 'selectedConversationIds', + originHost, + ); return selectedConversationIds ? JSON.parse(selectedConversationIds) : ''; } + + async getRecentModelsIds(originHost?: string) { + const recentModelsIds = await this.getKey('recentModelsIds', originHost); + return recentModelsIds ? JSON.parse(recentModelsIds) : ''; + } + + private async getKey(key: string, originHost?: string) { + const storage = await this.page.context().storageState(); + const origin = originHost + ? storage.origins.find((o) => o.origin === originHost) + : storage.origins[0]; + return origin?.localStorage.find((s) => s.name === key)?.value; + } } diff --git a/apps/chat-e2e/src/testData/api/itemApiHelper.ts b/apps/chat-e2e/src/testData/api/itemApiHelper.ts index 3d8de8e7b..d13c3f2f0 100644 --- a/apps/chat-e2e/src/testData/api/itemApiHelper.ts +++ b/apps/chat-e2e/src/testData/api/itemApiHelper.ts @@ -1,5 +1,5 @@ import { Conversation } from '@/chat/types/chat'; -import { BackendDataEntity, BackendDataNodeType } from '@/chat/types/common'; +import { BackendChatEntity, BackendDataNodeType } from '@/chat/types/common'; import { Prompt } from '@/chat/types/prompt'; import { API } from '@/src/testData'; import { BaseApiHelper } from '@/src/testData/api/baseApiHelper'; @@ -39,10 +39,20 @@ export class ItemApiHelper extends BaseApiHelper { statusCode, `Received response code: ${statusCode} with body: ${await response.text()}`, ).toBe(200); - return (await response.json()) as BackendDataEntity[]; + return (await response.json()) as BackendChatEntity[]; } - public async deleteBackendItem(...items: BackendDataEntity[]) { + public async getItem(id: string) { + const response = await this.request.get(this.getHost(`/api/${id}`)); + const statusCode = response.status(); + expect( + statusCode, + `Received response code: ${statusCode} with body: ${await response.text()}`, + ).toBe(200); + return (await response.json()) as Conversation; + } + + public async deleteBackendItem(...items: BackendChatEntity[]) { for (const item of items) { const path = `/api/${item.url}`; const response = await this.request.delete(this.getHost(path)); diff --git a/apps/chat-e2e/src/testData/api/publicationApiHelper.ts b/apps/chat-e2e/src/testData/api/publicationApiHelper.ts index 9145514a4..89e2a054f 100644 --- a/apps/chat-e2e/src/testData/api/publicationApiHelper.ts +++ b/apps/chat-e2e/src/testData/api/publicationApiHelper.ts @@ -1,9 +1,11 @@ +import { Conversation } from '@/chat/types/chat'; import { Publication, PublicationInfo, PublicationRequestModel, PublicationStatus, PublicationsListModel, + PublishedList, } from '@/chat/types/publication'; import { API, ExpectedConstants } from '@/src/testData'; import { BaseApiHelper } from '@/src/testData/api/baseApiHelper'; @@ -31,6 +33,23 @@ export class PublicationApiHelper extends BaseApiHelper { return (await response.json()) as PublicationsListModel; } + public async listPublishedConversations() { + const response = await this.request.get( + this.getHost(API.publishedConversations), + { + params: { + recursive: true, + }, + }, + ); + const statusCode = response.status(); + expect( + statusCode, + `Received response code: ${statusCode} with body: ${await response.text()}`, + ).toBe(200); + return (await response.json()) as PublishedList; + } + public async getPublicationRequestDetails(publicationUrl: string) { const response = await this.request.post( this.getHost(API.publicationRequestDetails), @@ -46,6 +65,18 @@ export class PublicationApiHelper extends BaseApiHelper { return (await response.json()) as Publication; } + public async getPublishedConversation(conversationUrl: string) { + const response = await this.request.get( + this.getHost(`/api/${conversationUrl}`), + ); + const statusCode = response.status(); + expect( + statusCode, + `Received response code: ${statusCode} with body: ${await response.text()}`, + ).toBe(200); + return (await response.json()) as Conversation; + } + public async approveRequest( publicationRequest: Publication | PublicationInfo, ) { diff --git a/apps/chat-e2e/src/testData/expectedConstants.ts b/apps/chat-e2e/src/testData/expectedConstants.ts index d53449569..c2f4fc78c 100644 --- a/apps/chat-e2e/src/testData/expectedConstants.ts +++ b/apps/chat-e2e/src/testData/expectedConstants.ts @@ -320,6 +320,7 @@ export const API = { publicationRulesList: '/api/publication/rulesList', multipleListingHost: () => `${API.listingHost}/multiple?recursive=true`, pendingPublicationsListing: '/api/publication/listing', + publishedConversations: '/api/publication/conversations/public', }; export const Import = { @@ -433,3 +434,8 @@ export enum AttachFilesFolders { appdata = 'appdata', images = 'images', } + +export enum PseudoModel { + replay = 'replay', + playback = 'playback', +} diff --git a/apps/chat-e2e/src/tests/overlay/chatSettingsFeature.test.ts b/apps/chat-e2e/src/tests/overlay/chatSettingsFeature.test.ts index 9c1e1d8a5..54e734373 100644 --- a/apps/chat-e2e/src/tests/overlay/chatSettingsFeature.test.ts +++ b/apps/chat-e2e/src/tests/overlay/chatSettingsFeature.test.ts @@ -201,7 +201,8 @@ dialOverlayTest( dialOverlayTest( `[Overlay] When no any feature is enabled in the code.\n` + - '[Overlay] Display configure settings for empty chat - Feature.EmptyChatSettings. p1', + '[Overlay] Display configure settings for empty chat - Feature.EmptyChatSettings. p1.\n' + + `[Overlay] Send 'Hello' to Chat manually`, async ({ overlayHomePage, overlayChat, @@ -212,7 +213,7 @@ dialOverlayTest( overlaySendMessage, setTestIds, }) => { - setTestIds('EPMRTC-3780', 'EPMRTC-3765'); + setTestIds('EPMRTC-3780', 'EPMRTC-3765', 'EPMRTC-4846'); await dialTest.step( 'Open sandbox and verify model information, send request field and "Change agent" link are available', @@ -255,7 +256,12 @@ dialOverlayTest( MockedChatApiResponseBodies.simpleTextBody, { isOverlay: true }, ); - await overlayChat.sendRequestWithButton('test'); + const requestContent = 'test'; + const request = await overlayChat.sendRequestWithButton(requestContent); + overlayBaseAssertion.assertValue( + request.messages[0].content, + requestContent, + ); await overlayChatMessagesAssertion.assertMessageDeleteIconState( 1, 'visible', diff --git a/apps/chat-e2e/src/tests/overlay/events.test.ts b/apps/chat-e2e/src/tests/overlay/events.test.ts new file mode 100644 index 000000000..7e44ed17f --- /dev/null +++ b/apps/chat-e2e/src/tests/overlay/events.test.ts @@ -0,0 +1,443 @@ +import { Conversation } from '@/chat/types/chat'; +import { Publication } from '@/chat/types/publication'; +import dialOverlayTest from '@/src/core/dialOverlayFixtures'; +import { + API, + ExpectedConstants, + FolderConversation, + MockedChatApiResponseBodies, + OverlaySandboxUrls, + PseudoModel, + Theme, +} from '@/src/testData'; +import { GeneratorUtil, ModelsUtil } from '@/src/utils'; +import { + CreateConversationResponse, + GetConversationsResponse, + GetMessagesResponse, + OverlayConversation, + PublishActions, + SelectConversationResponse, +} from '@epam/ai-dial-shared'; +import { expect } from '@playwright/test'; + +const publicationsToUnpublish: Publication[] = []; + +dialOverlayTest( + `[Overlay. Events in sandbox] Send 'Hello' to Chat.\n` + + '[Overlay. Events in sandbox] Set system prompt.\n' + + '[Overlay. Events in sandbox] Get messages.\n' + + '[Overlay. Events in sandbox] Create conversation. Specific for Overlay: new conversation is created each time.\n' + + `[Overlay. Events in sandbox] Overlay configuration. Click on "Set light theme and new model" when new conversation is on the screen`, + async ({ + overlayHomePage, + overlayHeader, + overlayIconApiHelper, + overlayBaseAssertion, + overlayAgentInfoAssertion, + overlayAssertion, + overlayActions, + overlayConfiguration, + overlayAgentInfo, + overlayChat, + overlayDialog, + overlayItemApiHelper, + localStorageManager, + setTestIds, + }) => { + setTestIds( + 'EPMRTC-4845', + 'EPMRTC-395', + 'EPMRTC-4850', + 'EPMRTC-4506', + 'EPMRTC-4853', + ); + const firstRequestContent = 'Hello'; + const secondRequestContent = 'test'; + const systemPrompt = `End each word with string "!?!?!"`; + let secondRequest: Conversation; + const configuredModelId = 'stability.stable-diffusion-xl'; + + await overlayHomePage.mockChatTextResponse( + MockedChatApiResponseBodies.simpleTextBody, + { isOverlay: true }, + ); + + await dialOverlayTest.step( + `Click on "Send 'Hello' to Chat" and verify request with correct message is sent`, + async () => { + await overlayHomePage.navigateToUrl( + OverlaySandboxUrls.enabledOnlyHeaderSandboxUrl, + ); + await overlayHomePage.waitForPageLoaded(); + const request = await overlayActions.clickSendMessage(); + overlayBaseAssertion.assertValue( + request.messages[0].content, + firstRequestContent, + ); + }, + ); + + await dialOverlayTest.step( + `Click on "Set system prompt', send one more message and verify system prompt is set in the request`, + async () => { + await overlayActions.setSysPromptButton.click(); + secondRequest = (await overlayChat.sendRequestWithButton( + secondRequestContent, + )) as Conversation; + const systemMessage = secondRequest.messages.find( + (m) => m.role === 'system', + ); + expect.soft(systemMessage).toBeDefined(); + overlayBaseAssertion.assertValue(systemMessage?.content, systemPrompt); + }, + ); + + await dialOverlayTest.step( + `Click on "Get messages" and verify dialog with conversation messages is displayed`, + async () => { + await overlayActions.getMessagesButton.click(); + await overlayBaseAssertion.assertElementState(overlayDialog, 'visible'); + const actualMessages = + await overlayDialog.content.getElementInnerContent(); + const expectedItem = await overlayItemApiHelper.getItem( + secondRequest.id, + ); + const expectedMessages: GetMessagesResponse = { + messages: expectedItem.messages, + }; + expect + .soft(JSON.parse(actualMessages) as GetMessagesResponse) + .toStrictEqual(expectedMessages); + await overlayDialog.closeButton.click(); + }, + ); + + await dialOverlayTest.step( + `Click on "Create conversation" button two times and verify dialog with conversation is displayed, conversation index is incremented`, + async () => { + for (let i = 1; i <= 2; i++) { + const newConversationData = + await overlayActions.clickCreateConversation(); + expect + .soft( + newConversationData.request.id.endsWith( + ExpectedConstants.newConversationWithIndexTitle(i), + ), + ) + .toBeTruthy(); + await overlayBaseAssertion.assertElementState( + overlayDialog, + 'visible', + ); + const actualMessages = + await overlayDialog.content.getElementInnerContent(); + const expectedConversation: CreateConversationResponse = { + conversation: { + model: newConversationData.request.model, + name: newConversationData.request.name, + isPlayback: newConversationData.request.isPlayback ?? false, + isReplay: newConversationData.request.isReplay ?? false, + id: newConversationData.request.id, + lastActivityDate: newConversationData.response.updatedAt, + folderId: newConversationData.request.folderId, + bucket: newConversationData.response.bucket, + }, + }; + expect + .soft(JSON.parse(actualMessages) as CreateConversationResponse) + .toStrictEqual(expectedConversation); + await overlayDialog.closeButton.click(); + } + }, + ); + + await dialOverlayTest.step( + `Click on "Set light theme and new model" button and verify theme is changed to light, model is added to the recent models`, + async () => { + await overlayConfiguration.setConfigurationButton.click(); + await overlayAssertion.assertOverlayTheme(overlayHomePage, Theme.light); + await overlayBaseAssertion.assertElementText( + overlayAgentInfo.agentName, + ModelsUtil.getDefaultModel()!.name, + ); + const recentModels = await localStorageManager.getRecentModelsIds( + process.env.NEXT_PUBLIC_OVERLAY_HOST, + ); + overlayBaseAssertion.assertValue(recentModels[0], configuredModelId); + }, + ); + + await dialOverlayTest.step( + `Click on "Create new conversation" button and verify new model is applied`, + async () => { + const expectedModel = ModelsUtil.getModel(configuredModelId)!; + await overlayHeader.createNewConversation(); + await overlayAgentInfoAssertion.assertElementText( + overlayAgentInfo.agentName, + expectedModel.name, + ); + await overlayAgentInfoAssertion.assertShortDescription(expectedModel); + await overlayAgentInfoAssertion.assertAgentIcon( + overlayIconApiHelper.getEntityIcon(expectedModel), + ); + }, + ); + }, +); + +dialOverlayTest( + '[Overlay. Events in sandbox] Select conversation and its json appears if to click on Select conversation by ID.\n' + + '[Overlay. Events in sandbox] Get conversations', + async ({ + overlayHomePage, + overlayActions, + overlayDialog, + conversationData, + overlayChatHeader, + overlayBaseAssertion, + localStorageManager, + overlayDataInjector, + overlayShareApiHelper, + overlayItemApiHelper, + setTestIds, + adminShareApiHelper, + adminPublicationApiHelper, + adminDataInjector, + publishRequestBuilder, + }) => { + setTestIds('EPMRTC-396', 'EPMRTC-4852'); + let todayConversation: Conversation; + let folderConversation: FolderConversation; + let publishedConversation: Conversation; + let sharedConversation: Conversation; + const expectedConversationsArray: OverlayConversation[] = []; + + await dialOverlayTest.step( + `Prepare conversations in Today, Pinned, Organization and Shared sections`, + async () => { + todayConversation = conversationData.prepareDefaultConversation(); + conversationData.resetData(); + folderConversation = + conversationData.prepareDefaultConversationInFolder(); + conversationData.resetData(); + publishedConversation = conversationData.prepareDefaultConversation(); + conversationData.resetData(); + sharedConversation = conversationData.prepareDefaultConversation(); + await overlayDataInjector.createConversations([ + todayConversation, + ...folderConversation.conversations, + ]); + await adminDataInjector.createConversations([ + publishedConversation, + sharedConversation, + ]); + //publish conversation by admin + const publishRequest = publishRequestBuilder + .withName(GeneratorUtil.randomPublicationRequestName()) + .withConversationResource(publishedConversation, PublishActions.ADD) + .build(); + const publication = + await adminPublicationApiHelper.createPublishRequest(publishRequest); + await adminPublicationApiHelper.approveRequest(publication); + publicationsToUnpublish.push(publication); + //share conversation by admin + const shareByLinkResponse = await adminShareApiHelper.shareEntityByLink( + [sharedConversation], + ); + await overlayShareApiHelper.acceptInvite(shareByLinkResponse); + }, + ); + + await dialOverlayTest.step( + `Click "Get conversations" button and verify dialog with conversations is displayed`, + async () => { + await overlayHomePage.navigateToUrl( + OverlaySandboxUrls.enabledHeaderSandboxUrl, + ); + await overlayHomePage.waitForPageLoaded(); + await overlayActions.getConversationsButton.click(); + await overlayBaseAssertion.assertElementState(overlayDialog, 'visible'); + const actualConversations = + await overlayDialog.content.getElementInnerContent(); + const actualConversationsModels = JSON.parse( + actualConversations, + ) as GetConversationsResponse; + + const expectedConversationsModel: GetConversationsResponse = { + conversations: expectedConversationsArray, + }; + + //build expected conversations published with user + //TODO: enable when fixed https://github.com/epam/ai-dial-chat/issues/2929 + // const actualPublishedConversationsList = + // await overlayPublicationApiHelper.listPublishedConversations(); + // for (const actualPublishedConversation of actualPublishedConversationsList.items!) { + // const conversation = + // await overlayPublicationApiHelper.getPublishedConversation( + // actualPublishedConversation.url, + // ); + // const isPlayback = conversation.playback?.isPlayback; + // const isReplay = conversation.replay?.isReplay; + // const parentPath = actualPublishedConversation.parentPath; + // expectedConversationsArray.push({ + // model: isPlayback + // ? { id: PseudoModel.playback } + // : isReplay + // ? { id: PseudoModel.replay } + // : conversation.model, + // name: conversation.name, + // isPlayback: isPlayback ?? false, + // isReplay: isReplay ?? false, + // publicationInfo: { + // version: actualPublishedConversation.name.substring( + // actualPublishedConversation.name.lastIndexOf( + // ItemUtil.conversationIdSeparator, + // ) + ItemUtil.conversationIdSeparator.length, + // ), + // }, + // id: conversation.id, + // folderId: conversation.folderId, + // publishedWithMe: !parentPath, + // lastActivityDate: actualPublishedConversation.updatedAt, + // bucket: actualPublishedConversation.bucket, + // ...(parentPath && { parentPath }), + // }); + // } + + //build expected conversations created by user + const actualConversationsList = await overlayItemApiHelper.listItems( + API.conversationsHost(), + ); + for (const actualConversation of actualConversationsList) { + const conversation = await overlayItemApiHelper.getItem( + actualConversation.url, + ); + const parentPath = actualConversation.parentPath; + const isPlayback = conversation.playback?.isPlayback; + const isReplay = conversation.replay?.isReplay; + expectedConversationsArray.push({ + model: isPlayback + ? { id: PseudoModel.playback } + : isReplay + ? { id: PseudoModel.replay } + : conversation.model, + name: conversation.name, + isPlayback: isPlayback ?? false, + isReplay: isReplay ?? false, + id: conversation.id, + lastActivityDate: actualConversation.updatedAt, + folderId: conversation.folderId, + bucket: actualConversation.bucket, + ...(parentPath && { parentPath }), + }); + } + + //build expected conversations shared with user + const actualSharedConversationsList = + await overlayShareApiHelper.listSharedWithMeConversations(); + for (const actualSharedConversation of actualSharedConversationsList.resources) { + const conversation = await overlayItemApiHelper.getItem( + actualSharedConversation.url, + ); + const isPlayback = conversation.playback?.isPlayback; + const isReplay = conversation.replay?.isReplay; + expectedConversationsArray.push({ + model: isPlayback + ? { id: PseudoModel.playback } + : isReplay + ? { id: PseudoModel.replay } + : conversation.model, + name: conversation.name, + isPlayback: isPlayback ?? false, + isReplay: isReplay ?? false, + id: conversation.id, + folderId: conversation.folderId, + sharedWithMe: true, + bucket: actualSharedConversation.bucket, + }); + } + + //compare conversations from bucket storage + //TODO: enable when fixed https://github.com/epam/ai-dial-chat/issues/2929 + // expect(actualConversationsModels.conversations.length).toBe( + // expectedConversationsModel.conversations.length + 1, + // ); + expect(actualConversationsModels.conversations).toEqual( + expect.arrayContaining(expectedConversationsModel.conversations), + ); + + //check newly created 'New conversation 1' is displayed + const selectedConversationIds = + await localStorageManager.getSelectedConversationIds( + process.env.NEXT_PUBLIC_OVERLAY_HOST, + ); + actualConversationsModels.conversations.find( + (c) => c.id === selectedConversationIds[0], + ); + expect( + actualConversationsModels.conversations.find( + (c) => c.id === selectedConversationIds[0], + ), + ).toBeDefined(); + await overlayDialog.closeButton.click(); + }, + ); + + await dialOverlayTest.step( + `Set id into "Select conversation by ID" field and verify conversation is selected, dialog with conversation is displayed`, + async () => { + await overlayActions.conversationIdField.fillInInput( + todayConversation.id, + ); + const conversationResponse = + await overlayActions.clickSelectConversationById(); + overlayBaseAssertion.assertValue( + conversationResponse.id, + todayConversation.id, + ); + + await overlayBaseAssertion.assertElementState(overlayDialog, 'visible'); + const actualConversation = + await overlayDialog.content.getElementInnerContent(); + const actualConversationModel = JSON.parse( + actualConversation, + ) as SelectConversationResponse; + + const expectedConversation = expectedConversationsArray.find( + (c) => c.id === todayConversation.id, + ); + const expectedSelectedConversation = { ...expectedConversation }; + delete expectedSelectedConversation.bucket; + + expect + .soft(actualConversationModel) + .toStrictEqual(expectedSelectedConversation); + await overlayDialog.closeButton.click(); + + await overlayBaseAssertion.assertElementText( + overlayChatHeader.chatTitle, + todayConversation.name, + ); + const selectedConversationIds = + await localStorageManager.getSelectedConversationIds( + process.env.NEXT_PUBLIC_OVERLAY_HOST, + ); + overlayBaseAssertion.assertValue( + selectedConversationIds[0], + todayConversation.id, + ); + }, + ); + }, +); + +dialOverlayTest.afterAll( + async ({ overlayPublicationApiHelper, adminPublicationApiHelper }) => { + for (const publication of publicationsToUnpublish) { + const unpublishResponse = + await overlayPublicationApiHelper.createUnpublishRequest(publication); + await adminPublicationApiHelper.approveRequest(unpublishResponse); + } + }, +); diff --git a/apps/chat-e2e/src/tests/overlay/modelIdFeature.test.ts b/apps/chat-e2e/src/tests/overlay/modelIdFeature.test.ts index 6c19fea89..b617e9215 100644 --- a/apps/chat-e2e/src/tests/overlay/modelIdFeature.test.ts +++ b/apps/chat-e2e/src/tests/overlay/modelIdFeature.test.ts @@ -61,8 +61,6 @@ dialOverlayTest( const expectedModel = ModelsUtil.getModel(expectedModelId)!; const expectedModelIcon = overlayIconApiHelper.getEntityIcon(expectedModel); - const expectedShortDescription = - expectedModel.description?.split(/\s*\n\s*\n\s*/g)[0]; await overlayHomePage.mockChatTextResponse( MockedChatApiResponseBodies.simpleTextBody, { isOverlay: true }, @@ -82,9 +80,7 @@ dialOverlayTest( overlayAgentInfo.agentName, expectedModel.name, ); - await overlayAgentInfoAssertion.assertDescription( - expectedShortDescription, - ); + await overlayAgentInfoAssertion.assertShortDescription(expectedModel); await overlayAgentInfoAssertion.assertAgentIcon(expectedModelIcon); }, ); diff --git a/apps/chat-e2e/src/ui/domData/tags.ts b/apps/chat-e2e/src/ui/domData/tags.ts index ea149df52..572ff9dcf 100644 --- a/apps/chat-e2e/src/ui/domData/tags.ts +++ b/apps/chat-e2e/src/ui/domData/tags.ts @@ -14,4 +14,6 @@ export enum Tags { td = 'td', html = 'html', label = 'label', + dialog = 'dialog', + p = 'p', } diff --git a/apps/chat-e2e/src/ui/pages/overlay/overlayBasePage.ts b/apps/chat-e2e/src/ui/pages/overlay/overlayBasePage.ts index b0ad408ed..5094a2dd0 100644 --- a/apps/chat-e2e/src/ui/pages/overlay/overlayBasePage.ts +++ b/apps/chat-e2e/src/ui/pages/overlay/overlayBasePage.ts @@ -2,6 +2,8 @@ import { Attributes, Tags } from '@/src/ui/domData'; import { BasePage } from '@/src/ui/pages'; import { OverlaySelectors, layoutContainer } from '@/src/ui/selectors'; import { BaseElement, BaseLayoutContainer } from '@/src/ui/webElements'; +import { Actions } from '@/src/ui/webElements/overlay/actions'; +import { Configuration } from '@/src/ui/webElements/overlay/configuration'; import { Page } from '@playwright/test'; export class OverlayBasePage extends BasePage { @@ -36,6 +38,23 @@ export class OverlayBasePage extends BasePage { OverlaySelectors.overlayManagerContainer, ); + public actions!: Actions; + public configuration!: Configuration; + + getActions(): Actions { + if (!this.actions) { + this.actions = new Actions(this.page); + } + return this.actions; + } + + getConfiguration(): Configuration { + if (!this.configuration) { + this.configuration = new Configuration(this.page); + } + return this.configuration; + } + public async getTheme() { return new BaseElement( this.page, diff --git a/apps/chat-e2e/src/ui/selectors/overlaySelectors.ts b/apps/chat-e2e/src/ui/selectors/overlaySelectors.ts index 8b589e620..337fec692 100644 --- a/apps/chat-e2e/src/ui/selectors/overlaySelectors.ts +++ b/apps/chat-e2e/src/ui/selectors/overlaySelectors.ts @@ -5,3 +5,17 @@ export const OverlaySelectors = { overlayManagerFullScreenButton: '#full-screen-button', overlayManagerContainer: '#frame-container', }; + +export const EventSelectors = { + chatActionsContainer: '#chat-actions', + sendMessageButton: '[data-qa="send-message"]', + setSysPromptButton: '[data-qa="set-sys-prompt"]', + getMessagesButton: '[data-qa="get-messages"]', + getConversationsButton: '[data-qa="get-conversations"]', + createConversationButton: '[data-qa="create-conversation"]', + createConversationInFolderButton: '[data-qa="create-conversation-in-folder"]', + selectConversationByIdButton: '[data-qa="select-conversation-by-id"]', + conversationIdField: '[data-qa="conversation-id"]', + configurationContainer: '#configuration', + setConfigurationButton: '[data-qa="set-configuration"]', +}; diff --git a/apps/chat-e2e/src/ui/webElements/overlay/actions.ts b/apps/chat-e2e/src/ui/webElements/overlay/actions.ts new file mode 100644 index 000000000..3c115a8ef --- /dev/null +++ b/apps/chat-e2e/src/ui/webElements/overlay/actions.ts @@ -0,0 +1,71 @@ +import { Conversation } from '@/chat/types/chat'; +import { BackendChatEntity } from '@/chat/types/common'; +import { API } from '@/src/testData'; +import { EventSelectors } from '@/src/ui/selectors'; +import { BaseElement } from '@/src/ui/webElements'; +import { Page } from '@playwright/test'; +import * as process from 'node:process'; + +export class Actions extends BaseElement { + constructor(page: Page) { + super(page, EventSelectors.chatActionsContainer); + } + + public sendMessageButton = this.getChildElementBySelector( + EventSelectors.sendMessageButton, + ); + public setSysPromptButton = this.getChildElementBySelector( + EventSelectors.setSysPromptButton, + ); + public getMessagesButton = this.getChildElementBySelector( + EventSelectors.getMessagesButton, + ); + public getConversationsButton = this.getChildElementBySelector( + EventSelectors.getConversationsButton, + ); + public createConversationButton = this.getChildElementBySelector( + EventSelectors.createConversationButton, + ); + public createConversationInFolderButton = this.getChildElementBySelector( + EventSelectors.createConversationInFolderButton, + ); + public selectConversationByIdButton = this.getChildElementBySelector( + EventSelectors.selectConversationByIdButton, + ); + public conversationIdField = this.getChildElementBySelector( + EventSelectors.conversationIdField, + ); + + public async clickSendMessage() { + const requestPromise = this.page.waitForRequest( + process.env.NEXT_PUBLIC_OVERLAY_HOST + API.chatHost, + ); + await this.sendMessageButton.click(); + const request = await requestPromise; + return request.postDataJSON(); + } + + public async clickCreateConversation() { + const respPromise = this.page.waitForResponse( + (response) => + response.request().method() === 'POST' && response.status() === 200, + ); + await this.createConversationButton.click(); + const response = await respPromise; + const responseBody = (await response.json()) as BackendChatEntity; + return { + request: response.request().postDataJSON() as Conversation, + response: responseBody, + }; + } + + public async clickSelectConversationById() { + const respPromise = this.page.waitForResponse( + (response) => + response.request().method() === 'GET' && response.status() === 200, + ); + await this.selectConversationByIdButton.click(); + const response = await respPromise; + return (await response.json()) as Conversation; + } +} diff --git a/apps/chat-e2e/src/ui/webElements/overlay/configuration.ts b/apps/chat-e2e/src/ui/webElements/overlay/configuration.ts new file mode 100644 index 000000000..1000361d7 --- /dev/null +++ b/apps/chat-e2e/src/ui/webElements/overlay/configuration.ts @@ -0,0 +1,13 @@ +import { EventSelectors } from '@/src/ui/selectors'; +import { BaseElement } from '@/src/ui/webElements'; +import { Page } from '@playwright/test'; + +export class Configuration extends BaseElement { + constructor(page: Page) { + super(page, EventSelectors.configurationContainer); + } + + public setConfigurationButton = this.getChildElementBySelector( + EventSelectors.setConfigurationButton, + ); +} diff --git a/apps/chat-e2e/src/ui/webElements/overlay/dialog.ts b/apps/chat-e2e/src/ui/webElements/overlay/dialog.ts new file mode 100644 index 000000000..0cd3e1313 --- /dev/null +++ b/apps/chat-e2e/src/ui/webElements/overlay/dialog.ts @@ -0,0 +1,12 @@ +import { Tags } from '@/src/ui/domData'; +import { BaseElement } from '@/src/ui/webElements'; +import { Page } from '@playwright/test'; + +export class Dialog extends BaseElement { + constructor(page: Page) { + super(page, Tags.dialog); + } + + public closeButton = this.getChildElementBySelector(Tags.button); + public content = this.getChildElementBySelector(Tags.p); +} diff --git a/apps/overlay-sandbox/app/cases/components/chatOverlayWrapper.tsx b/apps/overlay-sandbox/app/cases/components/chatOverlayWrapper.tsx index f46fddb2a..18d2a054a 100644 --- a/apps/overlay-sandbox/app/cases/components/chatOverlayWrapper.tsx +++ b/apps/overlay-sandbox/app/cases/components/chatOverlayWrapper.tsx @@ -97,7 +97,7 @@ export const ChatOverlayWrapper: React.FC = ({ >
-
+
Chat actions
@@ -106,6 +106,7 @@ export const ChatOverlayWrapper: React.FC = ({ onClick={() => { overlay.current?.sendMessage('Hello'); }} + data-qa="send-message" > Send 'Hello' to Chat @@ -117,6 +118,7 @@ export const ChatOverlayWrapper: React.FC = ({ 'End each word with string "!?!?!"', ); }} + data-qa="set-sys-prompt" > Set system prompt: End each word with string "!?!?!" @@ -128,6 +130,7 @@ export const ChatOverlayWrapper: React.FC = ({ handleDisplayInformation(JSON.stringify(messages, null, 2)); }} + data-qa="get-messages" > Get messages @@ -141,6 +144,7 @@ export const ChatOverlayWrapper: React.FC = ({ JSON.stringify(conversations, null, 2), ); }} + data-qa="get-conversations" > Get conversations @@ -153,6 +157,7 @@ export const ChatOverlayWrapper: React.FC = ({ handleDisplayInformation(JSON.stringify(conversation, null, 2)); }} + data-qa="create-conversation" > Create conversation @@ -167,6 +172,7 @@ export const ChatOverlayWrapper: React.FC = ({ handleDisplayInformation(JSON.stringify(conversation, null, 2)); }} + data-qa="create-conversation-in-folder" > Create conversation in inner folder @@ -175,6 +181,7 @@ export const ChatOverlayWrapper: React.FC = ({ @@ -183,11 +190,12 @@ export const ChatOverlayWrapper: React.FC = ({ placeholder="Type conversation ID" value={conversationIdInputValue} onChange={(e) => setConversationIdInputValue(e.target.value)} + data-qa="conversation-id" />
-
+
Overlay configuration
@@ -199,13 +207,14 @@ export const ChatOverlayWrapper: React.FC = ({ hostDomain: window.location.origin, }; - newOptions.theme = 'dark'; + newOptions.theme = 'light'; newOptions.modelId = 'stability.stable-diffusion-xl'; overlay.current?.setOverlayOptions(newOptions); }} + data-qa="set-configuration" > - Set dark theme and new model + Set light theme and new model