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
132 changes: 132 additions & 0 deletions e2e-tests/aseloWebchat.ts
Original file line number Diff line number Diff line change
@@ -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<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.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<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}/`,
},

// 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 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([
<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