Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
133 changes: 133 additions & 0 deletions e2e-tests/aseloWebchat.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
/**
* 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<void>;
openChat: () => Promise<void>;
selectHelpline: (helpline: string) => Promise<void>;
chat: (statements: ChatStatement[]) => AsyncIterable<ChatStatement>;
close: () => Promise<void>;
};

export async function open(browser: Browser | BrowserContext): Promise<AseloWebChatPage> {
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.count()) > 0) {
await selectors.termsAndConditionsCheckbox.scrollIntoViewIfNeeded();
await selectors.termsAndConditionsCheckbox.check({ force: true });
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<ChatStatement> {
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();
},
};
}
6 changes: 6 additions & 0 deletions e2e-tests/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}/`,
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

webchatUrl explicitly uses the direct S3 URL to avoid CloudFront caching issues (see the comment above), but aseloWebchatUrl defaults to the assets-...tl.techmatters.org host. For consistency and to reduce the risk of E2E tests hitting stale cached assets, consider switching this default to the equivalent https://s3.amazonaws.com/assets-${localOverrideEnv}.tl.techmatters.org/aselo-webchat-react-app/${helplineShortCode}/ (still overridable via ASELO_WEBCHAT_URL).

Suggested change
default: `https://assets-${localOverrideEnv}.tl.techmatters.org/aselo-webchat-react-app/${helplineShortCode}/`,
default: `https://s3.amazonaws.com/assets-${localOverrideEnv}.tl.techmatters.org/aselo-webchat-react-app/${helplineShortCode}/`,

Copilot uses AI. Check for mistakes.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using the cloudfront URL is a better test as it checks cloudfront and the cache evection on deployment is correctly configured

},

// 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',
Expand Down
137 changes: 137 additions & 0 deletions e2e-tests/tests/aseloWebchat.spec.ts
Original file line number Diff line number Diff line change
@@ -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<ChatStatement> = 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 its 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([
<ContactFormTab>{
id: 'childInformation',
label: 'Child',
fill: form.fillStandardTab,
items: {
firstName: 'E2E',
lastName: 'TEST',
phone1: '1234512345',
province: 'Northern',
district: 'District A',
},
},
<ContactFormTab<Categories>>{
id: 'categories',
label: 'Categories',
fill: form.fillCategoriesTab,
items: {
Accessibility: ['Education'],
},
},
<ContactFormTab>{
id: 'caseInformation',
label: 'Summary',
fill: form.fillStandardTab,
items: {
callSummary: 'E2E TEST CALL',
},
},
]);

console.log('Saving form');
await form.save();
});
});
Loading