From bbf45632cddf90fb5c346ff6d4408253d02bdf05 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 18 Mar 2026 11:49:48 +0000 Subject: [PATCH 1/5] Initial plan From 528c27b32a6f82fb325452956a9a221c561c04ee Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 18 Mar 2026 12:06:00 +0000 Subject: [PATCH 2/5] feat: add aselo webchat E2E test and POM support code Co-authored-by: stephenhand <1694716+stephenhand@users.noreply.github.com> --- e2e-tests/aseloWebchat.ts | 117 +++++++++++++++++++++++ e2e-tests/config.ts | 6 ++ e2e-tests/tests/aseloWebchat.spec.ts | 137 +++++++++++++++++++++++++++ 3 files changed, 260 insertions(+) create mode 100644 e2e-tests/aseloWebchat.ts create mode 100644 e2e-tests/tests/aseloWebchat.spec.ts diff --git a/e2e-tests/aseloWebchat.ts b/e2e-tests/aseloWebchat.ts new file mode 100644 index 0000000000..313d59ed81 --- /dev/null +++ b/e2e-tests/aseloWebchat.ts @@ -0,0 +1,117 @@ +/** + * 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"]'), + nameInput: page.locator('input#name'), + 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.nameInput.isVisible()) { + await selectors.nameInput.fill('test name'); + await selectors.nameInput.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 caller statement in the list, it will yield execution back to the calling code, so it can action the caller 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..c1c08b9a2c --- /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 was 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(); + }); +}); From 86095f93c1a7981ad768660c5299899477366139 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 19 Mar 2026 07:17:53 +0000 Subject: [PATCH 3/5] fix: populate all mandatory pre-engagement form fields in aseloWebchat POM Co-authored-by: stephenhand <1694716+stephenhand@users.noreply.github.com> --- e2e-tests/aseloWebchat.ts | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/e2e-tests/aseloWebchat.ts b/e2e-tests/aseloWebchat.ts index 313d59ed81..686db1b27b 100644 --- a/e2e-tests/aseloWebchat.ts +++ b/e2e-tests/aseloWebchat.ts @@ -39,7 +39,10 @@ export async function open(browser: Browser | BrowserContext): Promise { await selectors.preEngagementForm.waitFor(); - if (await selectors.nameInput.isVisible()) { - await selectors.nameInput.fill('test name'); - await selectors.nameInput.blur(); + 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(); From ea98202aa74f543b5636fe051659e88c5e5d73d4 Mon Sep 17 00:00:00 2001 From: Stephen Hand Date: Thu, 19 Mar 2026 09:36:17 +0000 Subject: [PATCH 4/5] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- e2e-tests/aseloWebchat.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/e2e-tests/aseloWebchat.ts b/e2e-tests/aseloWebchat.ts index 686db1b27b..fe5d9f40a5 100644 --- a/e2e-tests/aseloWebchat.ts +++ b/e2e-tests/aseloWebchat.ts @@ -98,7 +98,7 @@ export async function open(browser: Browser | BrowserContext): Promise Date: Thu, 19 Mar 2026 13:49:15 +0000 Subject: [PATCH 5/5] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- e2e-tests/tests/aseloWebchat.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/e2e-tests/tests/aseloWebchat.spec.ts b/e2e-tests/tests/aseloWebchat.spec.ts index c1c08b9a2c..2fcfc1262f 100644 --- a/e2e-tests/tests/aseloWebchat.spec.ts +++ b/e2e-tests/tests/aseloWebchat.spec.ts @@ -76,7 +76,7 @@ test.describe.serial('Aselo web chat caller', () => { // 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 was of injecting actions to be taken partway through the chat should be implemented. + // 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) {