diff --git a/e2e-tests/aseloWebchat.ts b/e2e-tests/aseloWebchat.ts new file mode 100644 index 0000000000..fe5d9f40a5 --- /dev/null +++ b/e2e-tests/aseloWebchat.ts @@ -0,0 +1,132 @@ +/** + * Copyright (C) 2021-2023 Technology Matters + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ + +// eslint-disable-next-line import/no-extraneous-dependencies +import { Browser, BrowserContext, expect } from '@playwright/test'; +import { ChatStatement, ChatStatementOrigin } from './chatModel'; +import { getConfigValue } from './config'; + +const E2E_ASELO_CHAT_URL = getConfigValue('aseloWebchatUrl') as string; + +export type AseloWebChatPage = { + fillPreEngagementForm: () => Promise; + openChat: () => Promise; + selectHelpline: (helpline: string) => Promise; + chat: (statements: ChatStatement[]) => AsyncIterable; + close: () => Promise; +}; + +export async function open(browser: Browser | BrowserContext): Promise { + const page = await browser.newPage(); + const selectors = { + entryPointButton: page.locator('[data-testid="entry-point-button"]'), + rootContainer: page.locator('[data-test="root-container"]'), + + // Pre-engagement + preEngagementForm: page.locator('[data-test="pre-engagement-chat-form"]'), + helplineSelect: page.locator('select#helpline'), + startChatButton: page.locator('[data-test="pre-engagement-start-chat-button"]'), + firstNameInput: page.locator('input#firstName'), + contactIdentifierInput: page.locator('input#contactIdentifier'), + ageSelect: page.locator('select#age'), + genderSelect: page.locator('select#gender'), + termsAndConditionsCheckbox: page.locator('input#termsAndConditions'), + + // Chatting + chatInput: page.locator('[data-test="message-input-textarea"]'), + chatSendButton: page.locator('[data-test="message-send-button"]'), + messageBubbles: page.locator('[data-testid="message-bubble"]'), + messageWithText: (text: string) => + page.locator(`[data-testid="message-bubble"] p:text-is("${text}")`), + }; + + await page.goto(E2E_ASELO_CHAT_URL); + console.log('Waiting for entry point button to render.'); + await selectors.entryPointButton.waitFor(); + console.log('Found entry point button.'); + + return { + fillPreEngagementForm: async () => { + await selectors.preEngagementForm.waitFor(); + if (await selectors.firstNameInput.isVisible()) { + await selectors.firstNameInput.fill('Test'); + await selectors.firstNameInput.blur(); + } + if (await selectors.contactIdentifierInput.isVisible()) { + await selectors.contactIdentifierInput.fill('test@example.com'); + await selectors.contactIdentifierInput.blur(); + } + if (await selectors.ageSelect.isVisible()) { + await selectors.ageSelect.selectOption('10'); + await selectors.ageSelect.blur(); + } + if (await selectors.genderSelect.isVisible()) { + await selectors.genderSelect.selectOption('Girl'); + await selectors.genderSelect.blur(); + } + if (await selectors.termsAndConditionsCheckbox.isVisible()) { + await selectors.termsAndConditionsCheckbox.check(); + await selectors.termsAndConditionsCheckbox.blur(); + } + }, + + openChat: async () => { + await expect(selectors.rootContainer).toHaveCount(0, { timeout: 500 }); + await selectors.entryPointButton.click(); + await expect(selectors.rootContainer).toBeVisible(); + }, + + selectHelpline: async (helpline: string) => { + await selectors.preEngagementForm.waitFor(); + await selectors.helplineSelect.selectOption(helpline); + await selectors.helplineSelect.blur(); + }, + + /** + * This function runs the 'caller side' of an aselo webchat conversation. + * It will loop through a list of chat statements, typing and sending caller statements in the webchat client + * As soon as it hits a non-caller statement in the list (e.g. counselor-side), it will yield execution back to the calling code, so it can action those statement(s) + * + * A similar function exists in flexChat.ts to handle actioning the counselor side of the conversation. + * This means that they can both be looping through the same conversation, yielding control when they hit a statement the other chat function needs to handle + * @param statements - a unified list of all the chat statements in a conversation, for caller and counselor + */ + chat: async function* (statements: ChatStatement[]): AsyncIterable { + await selectors.startChatButton.click(); + await selectors.chatInput.waitFor(); + + for (const statementItem of statements) { + const { text, origin }: ChatStatement = statementItem; + switch (origin) { + case ChatStatementOrigin.CALLER: + await selectors.chatInput.fill(text); + await selectors.chatSendButton.click(); + break; + case ChatStatementOrigin.BOT: + await selectors.messageWithText(text).waitFor({ timeout: 60000, state: 'attached' }); + break; + default: + yield statementItem; + await selectors.messageWithText(text).waitFor({ timeout: 60000, state: 'attached' }); + } + } + }, + + close: async () => { + await page.close(); + }, + }; +} diff --git a/e2e-tests/config.ts b/e2e-tests/config.ts index 38547af18b..d5a58632b0 100644 --- a/e2e-tests/config.ts +++ b/e2e-tests/config.ts @@ -174,6 +174,12 @@ const configOptions: ConfigOptions = { default: `https://s3.amazonaws.com/assets-${localOverrideEnv}.tl.techmatters.org/webchat/${helplineShortCode}/e2e-chat.html`, }, + // The url of the aselo webchat react app is used to navigate to the new aselo webchat client + aseloWebchatUrl: { + envKey: 'ASELO_WEBCHAT_URL', + default: `https://assets-${localOverrideEnv}.tl.techmatters.org/aselo-webchat-react-app/${helplineShortCode}/`, + }, + // inLambda is used to determine if we are running in a lambda or not and set other config values accordingly inLambda: { envKey: 'TEST_IN_LAMBDA', diff --git a/e2e-tests/tests/aseloWebchat.spec.ts b/e2e-tests/tests/aseloWebchat.spec.ts new file mode 100644 index 0000000000..2fcfc1262f --- /dev/null +++ b/e2e-tests/tests/aseloWebchat.spec.ts @@ -0,0 +1,137 @@ +/** + * Copyright (C) 2021-2023 Technology Matters + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ + +import { BrowserContext, Page, request, test } from '@playwright/test'; +import * as aseloWebchat from '../aseloWebchat'; +import { AseloWebChatPage } from '../aseloWebchat'; +import { statusIndicator } from '../workerStatus'; +import { ChatStatement, ChatStatementOrigin } from '../chatModel'; +import { getWebchatScript } from '../chatScripts'; +import { flexChat } from '../flexChat'; +import { getConfigValue } from '../config'; +import { skipTestIfNotTargeted } from '../skipTest'; +import { tasks } from '../tasks'; +import { Categories, contactForm, ContactFormTab } from '../contactForm'; +import { deleteAllTasksInQueue } from '../twilio/tasks'; +import { notificationBar } from '../notificationBar'; +import { navigateToAgentDesktop } from '../agent-desktop'; +import { setupContextAndPage, closePage } from '../browser'; +import { clearOfflineTask } from '../hrm/clearOfflineTask'; +import { apiHrmRequest } from '../hrm/hrmRequest'; + +test.describe.serial('Aselo web chat caller', () => { + skipTestIfNotTargeted(); + + let chatPage: AseloWebChatPage, pluginPage: Page, context: BrowserContext; + test.beforeAll(async ({ browser }) => { + ({ context, page: pluginPage } = await setupContextAndPage(browser)); + + await clearOfflineTask( + apiHrmRequest(await request.newContext(), process.env.FLEX_TOKEN!), + process.env.LOGGED_IN_WORKER_SID!, + ); + + await navigateToAgentDesktop(pluginPage); + console.log('Plugin page visited.'); + chatPage = await aseloWebchat.open(context); + console.log('Aselo webchat browser session launched.'); + }); + + test.afterAll(async () => { + await statusIndicator(pluginPage)?.setStatus('OFFLINE'); + if (pluginPage) { + await notificationBar(pluginPage).dismissAllNotifications(); + } + await closePage(pluginPage); + await deleteAllTasksInQueue(); + }); + + test.afterEach(async () => { + await deleteAllTasksInQueue(); + }); + + test('Chat ', async () => { + test.setTimeout(180000); + await chatPage.openChat(); + await chatPage.fillPreEngagementForm(); + + const chatScript = getWebchatScript(); + + const webchatProgress = chatPage.chat(chatScript); + const flexChatProgress: AsyncIterator = flexChat(pluginPage).chat(chatScript); + + // Currently this loop handles the handing back and forth of control between the caller & counselor sides of the chat. + // Each time round the loop it allows the webchat to process statements until it yields control back to this loop + // And each time flexChatProgress.next(), the flex chat processes statements until it yields + // Should be moved out to it's own function in time, and a cleaner way of injecting actions to be taken partway through the chat should be implemented. + for await (const expectedCounselorStatement of webchatProgress) { + console.log('Statement for flex chat to process', expectedCounselorStatement); + if (expectedCounselorStatement) { + switch (expectedCounselorStatement.origin) { + case ChatStatementOrigin.COUNSELOR_AUTO: + await statusIndicator(pluginPage).setStatus('AVAILABLE'); + await tasks(pluginPage).acceptNextTask(); + await flexChatProgress.next(); + break; + default: + await flexChatProgress.next(); + break; + } + } + } + + if (getConfigValue('skipDataUpdate') as boolean) { + console.log('Skipping saving form'); + return; + } + + console.log('Starting filling form'); + const form = contactForm(pluginPage); + await form.fill([ + { + id: 'childInformation', + label: 'Child', + fill: form.fillStandardTab, + items: { + firstName: 'E2E', + lastName: 'TEST', + phone1: '1234512345', + province: 'Northern', + district: 'District A', + }, + }, + >{ + id: 'categories', + label: 'Categories', + fill: form.fillCategoriesTab, + items: { + Accessibility: ['Education'], + }, + }, + { + id: 'caseInformation', + label: 'Summary', + fill: form.fillStandardTab, + items: { + callSummary: 'E2E TEST CALL', + }, + }, + ]); + + console.log('Saving form'); + await form.save(); + }); +});