Files
upstream-mirror 30810eecd7
Some checks failed
Create issues from TODOs / todos (push) Failing after 3s
Close inactive issues / close-issues (push) Has been cancelled
Deploy GitHub Pages / deploy-preview (push) Has been cancelled
CI / ⚙️ Variables Setup (push) Has been cancelled
CI / ⚙️ Test Guard (push) Has been cancelled
CI / 🚀 Notify external services - draft (push) Has been cancelled
CI / 📦 Build Packages (push) Has been cancelled
CI / 📦 Meteor Build (coverage) (push) Has been cancelled
CI / 📦 Meteor Build (production) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], fips) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Publish Docker Images (ghcr.io) (push) Has been cancelled
CI / 📦 Track Image Sizes (push) Has been cancelled
CI / 🔎 Code Check - Action Lint (push) Has been cancelled
CI / 🔎 Code Check (push) Has been cancelled
CI / 🔨 Test Storybook (push) Has been cancelled
CI / 🔨 Test Unit (push) Has been cancelled
CI / 🔨 Test API (CE) (push) Has been cancelled
CI / 🔨 Test API Livechat (CE) (push) Has been cancelled
CI / 🔨 Test UI (CE) (push) Has been cancelled
CI / 🔨 Test API (EE) (push) Has been cancelled
CI / 🔨 Test API Livechat (EE) (push) Has been cancelled
CI / 🔨 Test API Apps (deno-runtime - EE) (push) Has been cancelled
CI / 🔨 Test UI (EE) (push) Has been cancelled
CI / 🔨 Test API (FIPS) (push) Has been cancelled
CI / 🔨 Test API Livechat (FIPS) (push) Has been cancelled
CI / 🔨 Test UI (FIPS) (push) Has been cancelled
CI / 🔨 Test Federation Matrix (push) Has been cancelled
CI / 📊 Report Coverage (push) Has been cancelled
CI / ✅ Tests Done (push) Has been cancelled
CI / 🚀 Publish build assets (push) Has been cancelled
CI / 🚀 Publish Docker Images (DockerHub) (push) Has been cancelled
CI / 🚀 Notify external services (push) Has been cancelled
CI / Update Version Durability (push) Has been cancelled
Release / Release (push) Has been cancelled
Code scanning - action / CodeQL-Build (push) Failing after 2s
Auto-close duplicate issues / auto-close-duplicates (push) Failing after 1s
snapshot of https://github.com/RocketChat/Rocket.Chat.git (develop, 2026-09-16; blobs > 131072B omitted)
2026-09-16 01:21:02 +05:00

1474 lines
59 KiB
TypeScript

import { faker } from '@faker-js/faker';
import type { Credentials } from '@rocket.chat/api-client';
import type { ILivechatDepartment, ILivechatVisitor, IOmnichannelRoom, IUser } from '@rocket.chat/core-typings';
import { Random } from '@rocket.chat/random';
import { expect } from 'chai';
import { before, after, describe, it } from 'mocha';
import moment from 'moment';
import type { Response } from 'supertest';
import { getCredentials, api, request, credentials } from '../../../data/api-data';
import { addOrRemoveAgentFromDepartment, createDepartmentWithAnOnlineAgent, deleteDepartment } from '../../../data/livechat/department';
import {
closeOmnichannelRoom,
deleteVisitor,
makeAgentAvailable,
placeRoomOnHold,
sendAgentMessage,
sendMessage,
startANewLivechatRoomAndTakeIt,
} from '../../../data/livechat/rooms';
import { createAnOnlineAgent, createBotAgent } from '../../../data/livechat/users';
import { sleep } from '../../../data/livechat/utils';
import { removePermissionFromAllRoles, restorePermissionToRoles, updateEESetting, updateSetting } from '../../../data/permissions.helper';
import { deleteUser } from '../../../data/users.helper';
import { IS_EE } from '../../../e2e/config/constants';
describe('LIVECHAT - dashboards', function () {
// This test is expected to take more time since we're simulating real time conversations to verify analytics
this.timeout(60000);
before((done) => getCredentials(done));
before(async () => {
await updateSetting('Livechat_enabled', true);
await updateEESetting('Livechat_Require_Contact_Verification', 'never');
await updateSetting('Omnichannel_enable_department_removal', true);
});
let department: ILivechatDepartment;
let roomList: IOmnichannelRoom[];
let visitorList: ILivechatVisitor[];
const agents: {
credentials: Credentials;
user: IUser & { username: string };
}[] = [];
let avgClosedRoomChatDuration = 0;
const inactivityTimeout = 3;
const TOTAL_MESSAGES = {
min: 5,
max: 10,
};
const DELAY_BETWEEN_MESSAGES = {
min: 1000,
max: (inactivityTimeout - 1) * 1000,
};
const TOTAL_ROOMS = 7;
const simulateRealtimeConversation = async (chatInfo: Awaited<ReturnType<typeof startANewLivechatRoomAndTakeIt>>[]) => {
const promises = chatInfo.map(async (info) => {
const { room, visitor } = info;
// send a few messages
const numberOfMessages = Random.between(TOTAL_MESSAGES.min, TOTAL_MESSAGES.max);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
for await (const _ of Array(numberOfMessages - 1).keys()) {
// flip a coin to decide who will send the message
const willSendFromAgent = Random.between(0, 1) === 1;
if (willSendFromAgent) {
await sendAgentMessage(room._id);
} else {
await sendMessage(room._id, faker.lorem.sentence(), visitor.token);
}
const delay = Random.between(DELAY_BETWEEN_MESSAGES.min, DELAY_BETWEEN_MESSAGES.max);
await sleep(delay);
}
// Last message is always from visitor so that the chat doesn't get abandoned due to
// "Livechat_visitor_inactivity_timeout" setting
await sendMessage(room._id, faker.lorem.sentence(), visitor.token);
});
await Promise.all(promises);
};
before(async () => {
if (!IS_EE) {
return;
}
await updateSetting('Livechat_visitor_inactivity_timeout', inactivityTimeout);
await updateSetting('Livechat_enable_business_hours', false);
// create dummy test data for further tests
const { department: createdDept, agent: agent1 } = await createDepartmentWithAnOnlineAgent();
department = createdDept;
const agent2 = await createAnOnlineAgent();
await addOrRemoveAgentFromDepartment(department._id, { agentId: agent2.user._id, username: agent2.user.username }, true);
agents.push(agent1);
agents.push(agent2);
const roomCreationStart = moment();
// start a few chats
const promises = Array.from(Array(TOTAL_ROOMS).keys()).map((i) => {
// 2 rooms by agent 1
if (i < 2) {
return startANewLivechatRoomAndTakeIt({ departmentId: department._id, agent: agent1.credentials });
}
return startANewLivechatRoomAndTakeIt({ departmentId: department._id, agent: agent2.credentials });
});
const chatInfo = await Promise.all(promises);
roomList = chatInfo.map((info) => info.room);
visitorList = chatInfo.map((info) => info.visitor);
// simulate messages being exchanged between agents and visitors
await simulateRealtimeConversation(chatInfo);
// put a chat on hold
await sendAgentMessage(chatInfo[1].room._id);
await placeRoomOnHold(chatInfo[1].room._id);
// close a chat
await closeOmnichannelRoom(chatInfo[4].room._id);
const room5ChatDuration = moment().diff(roomCreationStart, 'seconds');
// close an abandoned chat
await sendAgentMessage(chatInfo[5].room._id);
await sleep(inactivityTimeout * 1000); // wait for the chat to be considered abandoned
await closeOmnichannelRoom(chatInfo[5].room._id);
const room6ChatDuration = moment().diff(roomCreationStart, 'seconds');
avgClosedRoomChatDuration = (room5ChatDuration + room6ChatDuration) / 2;
});
after(async () => {
if (!IS_EE) {
return;
}
await Promise.allSettled(roomList.map((room) => closeOmnichannelRoom(room._id)));
await Promise.allSettled(visitorList.map((visitor) => deleteVisitor(visitor.token)));
await deleteDepartment(department._id);
await Promise.allSettled(agents.map((agent) => deleteUser(agent.user)));
await updateSetting('Omnichannel_enable_department_removal', false);
});
describe('livechat/analytics/dashboards/conversation-totalizers', () => {
const expectedMetrics = [
'Total_conversations',
'Open_conversations',
'On_Hold_conversations',
'Total_messages',
'Busiest_time',
'Total_abandoned_chats',
'Total_visitors',
];
it('should return an "unauthorized error" when the user does not have the necessary permission', async () => {
await removePermissionFromAllRoles('view-livechat-manager');
await request
.get(api('livechat/analytics/dashboards/conversation-totalizers'))
.query({
start: '2019-10-25T15:08:17.248Z',
end: '2019-12-08T15:08:17.248Z',
})
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(403);
});
it('should return an array of conversation totalizers', async () => {
await restorePermissionToRoles('view-livechat-manager');
await request
.get(api('livechat/analytics/dashboards/conversation-totalizers'))
.query({
start: '2019-10-25T15:08:17.248Z',
end: '2019-12-08T15:08:17.248Z',
})
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
expect(res.body.totalizers).to.be.an('array');
(res.body.totalizers as { title: string; value: string }[]).forEach(
(prop) => expect(expectedMetrics.includes(prop.title)).to.be.true,
);
});
});
(IS_EE ? it : it.skip)('should return data with correct values', async () => {
const start = moment().subtract(1, 'days').toISOString();
const end = moment().toISOString();
const result = await request
.get(api('livechat/analytics/dashboards/conversation-totalizers'))
.query({ start, end, departmentId: department._id })
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(200);
expect(result.body).to.have.property('success', true);
expect(result.body).to.have.property('totalizers');
expect(result.body.totalizers).to.be.an('array');
expect(result.body.totalizers).to.have.lengthOf(5);
const expectedResult = [
{ title: 'Total_conversations', value: 7 },
{ title: 'Open_conversations', value: 4 },
{ title: 'On_Hold_conversations', value: 1 },
// { title: 'Total_messages', value: 60 },
{ title: 'Total_visitors', value: 7 },
];
expectedResult.forEach((expected) => {
const resultItem = result.body.totalizers.find((item: any) => item.title === expected.title);
expect(resultItem).to.not.be.undefined;
expect(resultItem).to.have.property('value', expected.value);
});
const minMessages = TOTAL_MESSAGES.min * TOTAL_ROOMS;
const totalMessages = result.body.totalizers.find((item: any) => item.title === 'Total_messages');
expect(totalMessages).to.not.be.undefined;
const totalMessagesValue = parseInt(totalMessages.value);
expect(totalMessagesValue).to.be.greaterThanOrEqual(minMessages);
});
});
describe('livechat/analytics/dashboards/productivity-totalizers', () => {
const expectedMetrics = ['Avg_response_time', 'Avg_first_response_time', 'Avg_reaction_time', 'Avg_of_waiting_time'];
it('should return an "unauthorized error" when the user does not have the necessary permission', async () => {
await removePermissionFromAllRoles('view-livechat-manager');
await request
.get(api('livechat/analytics/dashboards/productivity-totalizers'))
.query({
start: '2019-10-25T15:08:17.248Z',
end: '2019-12-08T15:08:17.248Z',
})
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(403);
});
it('should return an array of productivity totalizers', async () => {
await restorePermissionToRoles('view-livechat-manager');
await request
.get(api('livechat/analytics/dashboards/productivity-totalizers'))
.query({
start: '2019-10-25T15:08:17.248Z',
end: '2019-12-08T15:08:17.248Z',
})
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
expect(res.body.totalizers).to.be.an('array');
(res.body.totalizers as { title: string; value: string }[]).forEach(
(prop) => expect(expectedMetrics.includes(prop.title)).to.be.true,
);
});
});
(IS_EE ? it : it.skip)('should return data with correct values', async () => {
const start = moment().subtract(1, 'days').toISOString();
const end = moment().toISOString();
const result = await request
.get(api('livechat/analytics/dashboards/productivity-totalizers'))
.query({ start, end, departmentId: department._id })
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(200);
expect(result.body).to.have.property('success', true);
// const expected = [
// // There's a bug in the code for calculation of these 3 values.
// // Due to which it always return 0
// { title: 'Avg_response_time', value: '00:00:00' },
// { title: 'Avg_first_response_time', value: '00:00:00' },
// { title: 'Avg_reaction_time', value: '00:00:00' },
// { title: 'Avg_of_waiting_time', value: '00:00:03' }, // approx 3, 5 delta
// ];
const avgWaitingTime = result.body.totalizers.find((item: any) => item.title === 'Avg_of_waiting_time');
expect(avgWaitingTime).to.not.be.undefined;
/* const avgWaitingTimeValue = moment.duration(avgWaitingTime.value).asSeconds();
expect(avgWaitingTimeValue).to.be.closeTo(DELAY_BETWEEN_MESSAGES.max / 1000, 5); */
});
});
describe('livechat/analytics/dashboards/chats-totalizers', () => {
const expectedMetrics = ['Total_abandoned_chats', 'Avg_of_abandoned_chats', 'Avg_of_chat_duration_time'];
it('should return an "unauthorized error" when the user does not have the necessary permission', async () => {
await removePermissionFromAllRoles('view-livechat-manager');
await request
.get(api('livechat/analytics/dashboards/chats-totalizers'))
.query({
start: '2019-10-25T15:08:17.248Z',
end: '2019-12-08T15:08:17.248Z',
})
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(403);
});
it('should return an array of chats totalizers', async () => {
await restorePermissionToRoles('view-livechat-manager');
await request
.get(api('livechat/analytics/dashboards/chats-totalizers'))
.query({
start: '2019-10-25T15:08:17.248Z',
end: '2019-12-08T15:08:17.248Z',
})
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
expect(res.body.totalizers).to.be.an('array');
(res.body.totalizers as { title: string; value: string }[]).forEach(
(prop) => expect(expectedMetrics.includes(prop.title)).to.be.true,
);
});
});
(IS_EE ? it : it.skip)('should return data with correct values', async () => {
const start = moment().subtract(1, 'days').toISOString();
const end = moment().toISOString();
const result = await request
.get(api('livechat/analytics/dashboards/chats-totalizers'))
.query({ start, end, departmentId: department._id })
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(200);
const expected = [
{ title: 'Total_abandoned_chats', value: 1 },
{ title: 'Avg_of_abandoned_chats', value: '14%' },
// { title: 'Avg_of_chat_duration_time', value: '00:00:01' },
];
expect(result.body).to.have.property('success', true);
expect(result.body).to.have.property('totalizers');
expect(result.body.totalizers).to.be.an('array');
expected.forEach((expected) => {
const resultItem = result.body.totalizers.find((item: any) => item.title === expected.title);
expect(resultItem).to.not.be.undefined;
expect(resultItem).to.have.property('value', expected.value);
});
const resultAverageChatDuration = result.body.totalizers.find((item: any) => item.title === 'Avg_of_chat_duration_time');
expect(resultAverageChatDuration).to.not.be.undefined;
const resultAverageChatDurationValue = moment.duration(resultAverageChatDuration.value).asSeconds();
expect(resultAverageChatDurationValue).to.be.closeTo(avgClosedRoomChatDuration, 5); // Keep a margin of 3 seconds
});
});
describe('livechat/analytics/dashboards/agents-productivity-totalizers', () => {
const expectedMetrics = ['Busiest_time', 'Avg_of_available_service_time', 'Avg_of_service_time'];
it('should return an "unauthorized error" when the user does not have the necessary permission', async () => {
await removePermissionFromAllRoles('view-livechat-manager');
await request
.get(api('livechat/analytics/dashboards/agents-productivity-totalizers'))
.query({
start: '2019-10-25T15:08:17.248Z',
end: '2019-12-08T15:08:17.248Z',
})
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(403);
});
it('should return an array of agents productivity totalizers', async () => {
await restorePermissionToRoles('view-livechat-manager');
await request
.get(api('livechat/analytics/dashboards/agents-productivity-totalizers'))
.query({
start: '2019-10-25T15:08:17.248Z',
end: '2019-12-08T15:08:17.248Z',
})
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
expect(res.body.totalizers).to.be.an('array');
(res.body.totalizers as { title: string; value: string }[]).forEach(
(prop) => expect(expectedMetrics.includes(prop.title)).to.be.true,
);
});
});
(IS_EE ? it : it.skip)('should return data with correct values', async () => {
const start = moment().subtract(1, 'days').toISOString();
const end = moment().toISOString();
const result = await request
.get(api('livechat/analytics/dashboards/agents-productivity-totalizers'))
.query({ start, end, departmentId: department._id })
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(200);
// [
// { title: 'Busiest_time', value: '- -' },
// { title: 'Avg_of_available_service_time', value: '00:00:00' },
// { title: 'Avg_of_service_time', value: '00:00:16' } approx 17, 6 delta
// ],
expect(result.body).to.have.property('success', true);
expect(result.body).to.have.property('totalizers');
expect(result.body.totalizers).to.be.an('array');
const avgServiceTime = result.body.totalizers.find((item: any) => item.title === 'Avg_of_service_time');
expect(avgServiceTime).to.not.be.undefined;
const avgServiceTimeValue = moment.duration(avgServiceTime.value).asSeconds();
const minChatDuration = (DELAY_BETWEEN_MESSAGES.min * TOTAL_MESSAGES.min) / 1000;
const maxChatDuration = (DELAY_BETWEEN_MESSAGES.max * TOTAL_MESSAGES.max) / 1000;
expect(avgServiceTimeValue).to.be.closeTo((minChatDuration + maxChatDuration) / 2, 10);
});
});
describe('livechat/analytics/dashboards/charts/chats', () => {
it('should return an "unauthorized error" when the user does not have the necessary permission', async () => {
await removePermissionFromAllRoles('view-livechat-manager');
await request
.get(api('livechat/analytics/dashboards/charts/chats'))
.query({
start: '2019-10-25T15:08:17.248Z',
end: '2019-12-08T15:08:17.248Z',
})
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(403);
});
it('should return an array of productivity totalizers', async () => {
await restorePermissionToRoles('view-livechat-manager');
await request
.get(api('livechat/analytics/dashboards/charts/chats'))
.query({
start: '2019-10-25T15:08:17.248Z',
end: '2019-12-08T15:08:17.248Z',
})
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('open');
expect(res.body).to.have.property('closed');
expect(res.body).to.have.property('queued');
});
});
(IS_EE ? it : it.skip)('should return data with correct values', async () => {
const start = moment().subtract(1, 'days').toISOString();
const end = moment().toISOString();
const result = await request
.get(api('livechat/analytics/dashboards/charts/chats'))
.query({ start, end, departmentId: department._id })
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(200);
const expected = {
open: 4,
closed: 2,
queued: 0,
onhold: 1,
};
expect(result.body).to.have.property('success', true);
Object.entries(expected).forEach(([key, value]) => {
expect(result.body).to.have.property(key, value);
});
});
});
describe('livechat/analytics/dashboards/charts/chats-per-agent', () => {
it('should return an "unauthorized error" when the user does not have the necessary permission', async () => {
await removePermissionFromAllRoles('view-livechat-manager');
await request
.get(api('livechat/analytics/dashboards/charts/chats-per-agent'))
.query({
start: '2019-10-25T15:08:17.248Z',
end: '2019-12-08T15:08:17.248Z',
})
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(403);
});
it('should return an object with open and closed chats by agent', async () => {
await restorePermissionToRoles('view-livechat-manager');
await request
.get(api('livechat/analytics/dashboards/charts/chats-per-agent'))
.query({
start: '2019-10-25T15:08:17.248Z',
end: '2019-12-08T15:08:17.248Z',
})
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
});
});
(IS_EE ? it : it.skip)('should return data with correct values', async () => {
const start = moment().subtract(1, 'days').toISOString();
const end = moment().toISOString();
const result = await request
.get(api('livechat/analytics/dashboards/charts/chats-per-agent'))
.query({ start, end, departmentId: department._id })
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(200);
const expected = {
agent0: { open: 1, closed: 0, onhold: 1 },
agent1: { open: 3, closed: 2 },
};
expect(result.body).to.have.property('success', true);
const agent0 = result.body[agents[0].user.username as string];
const agent1 = result.body[agents[1].user.username as string];
Object.entries(expected.agent0).forEach(([key, value]) => {
expect(agent0).to.have.property(key, value);
});
Object.entries(expected.agent1).forEach(([key, value]) => {
expect(agent1).to.have.property(key, value);
});
});
});
describe('livechat/analytics/dashboards/charts/agents-status', () => {
it('should return an "unauthorized error" when the user does not have the necessary permission', async () => {
await removePermissionFromAllRoles('view-livechat-manager');
await request
.get(api('livechat/analytics/dashboards/charts/agents-status'))
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(403);
});
it('should return an object with agents status metrics', async () => {
await restorePermissionToRoles('view-livechat-manager');
await request
.get(api('livechat/analytics/dashboards/charts/agents-status'))
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('offline');
expect(res.body).to.have.property('away');
expect(res.body).to.have.property('busy');
expect(res.body).to.have.property('available');
});
});
(IS_EE ? it : it.skip)('should return data with correct values', async () => {
const start = moment().subtract(1, 'days').toISOString();
const end = moment().toISOString();
await Promise.all(agents.map((agent) => makeAgentAvailable(agent.credentials)));
const result = await request
.get(api('livechat/analytics/dashboards/charts/agents-status'))
.query({ start, end, departmentId: department._id })
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(200);
// TODO: We can improve tests further by creating some agents with different status
const expected = {
offline: 0,
away: 0,
busy: 0,
available: 2,
};
expect(result.body).to.have.property('success', true);
Object.entries(expected).forEach(([key, value]) => {
expect(result.body).to.have.property(key, value);
});
});
});
describe('livechat/analytics/dashboards/charts/chats-per-department', () => {
it('should return an "unauthorized error" when the user does not have the necessary permission', async () => {
await removePermissionFromAllRoles('view-livechat-manager');
await request
.get(api('livechat/analytics/dashboards/charts/chats-per-department'))
.query({
start: '2019-10-25T15:08:17.248Z',
end: '2019-12-08T15:08:17.248Z',
})
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(403);
});
it('should return an object with open and closed chats by department', async () => {
await restorePermissionToRoles('view-livechat-manager');
await request
.get(api('livechat/analytics/dashboards/charts/chats-per-department'))
.query({
start: '2019-10-25T15:08:17.248Z',
end: '2019-12-08T15:08:17.248Z',
})
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
});
});
(IS_EE ? it : it.skip)('should return data with correct values', async () => {
const start = moment().subtract(1, 'days').toISOString();
const end = moment().toISOString();
const result = await request
.get(api('livechat/analytics/dashboards/charts/chats-per-department'))
.query({ start, end, departmentId: department._id })
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(200);
const expected = {
department0: { open: 5, closed: 2 },
};
expect(result.body).to.have.property('success', true);
const department0 = result.body[department.name];
Object.entries(expected.department0).forEach(([key, value]) => {
expect(department0).to.have.property(key, value);
});
});
});
describe('livechat/analytics/dashboards/charts/timings', () => {
it('should return an "unauthorized error" when the user does not have the necessary permission', async () => {
await removePermissionFromAllRoles('view-livechat-manager');
await request
.get(api('livechat/analytics/dashboards/charts/timings'))
.query({
start: '2019-10-25T15:08:17.248Z',
end: '2019-12-08T15:08:17.248Z',
})
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(403);
});
it('should return an object with open and closed chats by department', async () => {
await restorePermissionToRoles('view-livechat-manager');
await request
.get(api('livechat/analytics/dashboards/charts/timings'))
.query({
start: '2019-10-25T15:08:17.248Z',
end: '2019-12-08T15:08:17.248Z',
})
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('response');
expect(res.body).to.have.property('reaction');
expect(res.body).to.have.property('chatDuration');
expect(res.body.response).to.have.property('avg');
expect(res.body.response).to.have.property('longest');
expect(res.body.reaction).to.have.property('avg');
expect(res.body.reaction).to.have.property('longest');
expect(res.body.chatDuration).to.have.property('avg');
expect(res.body.chatDuration).to.have.property('longest');
});
});
(IS_EE ? it : it.skip)('should return data with correct values', async () => {
const start = moment().subtract(1, 'days').toISOString();
const end = moment().toISOString();
const result = await request
.get(api('livechat/analytics/dashboards/charts/timings'))
.query({ start, end, departmentId: department._id })
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(200);
expect(result.body).to.have.property('success', true);
// const expected = {
// response: { avg: 0, longest: 0.207 }, // avg between delayBetweenMessage.min and delayBetweenMessage.max
// reaction: { avg: 0, longest: 0.221 }, // avg between delayBetweenMessage.min and delayBetweenMessage.max
// chatDuration: { avg: 0, longest: 0.18 }, // avg should be about avgClosedRoomChatDuration, and longest should be greater than avgClosedRoomChatDuration and within delta of 20
// success: true,
// };
const maxChatDuration = (DELAY_BETWEEN_MESSAGES.max * TOTAL_MESSAGES.max) / 1000;
const responseValues = result.body.response;
expect(responseValues).to.have.property('avg');
expect(responseValues).to.have.property('longest');
expect(responseValues.avg).to.be.closeTo((DELAY_BETWEEN_MESSAGES.min + DELAY_BETWEEN_MESSAGES.max) / 2000, 5);
expect(responseValues.longest).to.be.lessThan(maxChatDuration);
const reactionValues = result.body.reaction;
expect(reactionValues).to.have.property('avg');
expect(reactionValues).to.have.property('longest');
expect(reactionValues.avg).to.be.closeTo((DELAY_BETWEEN_MESSAGES.min + DELAY_BETWEEN_MESSAGES.max) / 2000, 5);
expect(reactionValues.longest).to.be.lessThan(maxChatDuration);
const chatDurationValues = result.body.chatDuration;
expect(chatDurationValues).to.have.property('avg');
expect(chatDurationValues).to.have.property('longest');
expect(chatDurationValues.avg).to.be.closeTo(avgClosedRoomChatDuration, 5);
expect(chatDurationValues.longest).to.be.greaterThan(avgClosedRoomChatDuration);
expect(chatDurationValues.longest).to.be.lessThan(avgClosedRoomChatDuration + 20);
});
});
describe('livechat/analytics/dashboards/charts-data', () => {
it('should return the correct data structure for the charts', async () => {
const yesterday = moment().subtract(1, 'days').format('YYYY-MM-DD');
const today = moment().startOf('day').format('YYYY-MM-DD');
const result = await request
.get(api('livechat/analytics/dashboards/charts-data'))
.query({ chartName: 'Total_conversations', start: yesterday, end: today })
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(200);
expect(result.body).to.have.property('success', true);
expect(result.body).to.have.property('chartLabel', 'Total_conversations');
expect(result.body).to.have.property('dataLabels').to.be.an('array');
expect(result.body).to.have.property('dataPoints').to.be.an('array');
});
// it('should return the correct data structure but empty for unavailable data');
});
describe('livechat/analytics/agent-overview', () => {
it('should return an "unauthorized error" when the user does not have the necessary permission', async () => {
await removePermissionFromAllRoles('view-livechat-manager');
await request
.get(api('livechat/analytics/agent-overview'))
.query({ from: '2020-01-01', to: '2020-01-02', name: 'Total_conversations' })
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(403);
});
it('should return an "invalid-chart-name error" when the chart name is empty', async () => {
await restorePermissionToRoles('view-livechat-manager');
await request
.get(api('livechat/analytics/agent-overview'))
.query({ from: '2020-01-01', to: '2020-01-02', name: '' })
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(400);
});
it('should return empty when chart name is invalid', async () => {
await request
.get(api('livechat/analytics/agent-overview'))
.query({ from: '2020-01-01', to: '2020-01-02', name: 'invalid-chart-name' })
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
expect(Object.keys(res.body)).to.have.lengthOf(1);
});
});
it('should return an array of agent overview data', async () => {
const result = await request
.get(api('livechat/analytics/agent-overview'))
.query({ from: '2020-01-01', to: '2020-01-02', name: 'Total_conversations' })
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(200);
expect(result.body).to.have.property('success', true);
expect(result.body).to.have.property('head');
expect(result.body).to.have.property('data');
expect(result.body.head).to.be.an('array');
expect(result.body.data).to.be.an('array');
});
(IS_EE ? it : it.skip)('should return agent overview data with correct values', async () => {
const yesterday = moment().subtract(1, 'days').format('YYYY-MM-DD');
const today = moment().startOf('day').format('YYYY-MM-DD');
const result = await request
.get(api('livechat/analytics/agent-overview'))
.query({ from: yesterday, to: today, name: 'Total_conversations', departmentId: department._id })
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(200);
expect(result.body).to.have.property('success', true);
expect(result.body).to.have.property('head');
expect(result.body).to.have.property('data');
expect(result.body.data).to.be.an('array');
expect(result.body.data).to.have.lengthOf(2);
const user1Data = result.body.data.find((data: any) => data.name === agents[0].user.username);
const user2Data = result.body.data.find((data: any) => data.name === agents[1].user.username);
expect(user1Data).to.not.be.undefined;
expect(user2Data).to.not.be.undefined;
expect(user1Data).to.have.property('value', '28.57%');
expect(user2Data).to.have.property('value', '71.43%');
});
(IS_EE ? it : it.skip)('should only return results in the provided date interval when searching for total conversations', async () => {
const yesterday = moment().subtract(1, 'days').format('YYYY-MM-DD');
const result = await request
.get(api('livechat/analytics/agent-overview'))
.query({ from: yesterday, to: yesterday, name: 'Total_conversations', departmentId: department._id })
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(200);
expect(result.body).to.have.property('success', true);
expect(result.body).to.have.property('head');
expect(result.body.head).to.be.an('array').with.lengthOf(2);
expect(result.body.head[0]).to.have.property('name', 'Agent');
expect(result.body.head[1]).to.have.property('name', '%_of_conversations');
expect(result.body).to.have.property('data');
expect(result.body.data).to.be.an('array').that.is.empty;
});
(IS_EE ? it : it.skip)(
'should only return results in the provided date interval when searching for average chat durations',
async () => {
const yesterday = moment().subtract(1, 'days').format('YYYY-MM-DD');
const result = await request
.get(api('livechat/analytics/agent-overview'))
.query({ from: yesterday, to: yesterday, name: 'Avg_chat_duration', departmentId: department._id })
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(200);
expect(result.body).to.have.property('success', true);
expect(result.body).to.have.property('head');
expect(result.body.head).to.be.an('array').with.lengthOf(2);
expect(result.body.head[0]).to.have.property('name', 'Agent');
expect(result.body.head[1]).to.have.property('name', 'Avg_chat_duration');
expect(result.body).to.have.property('data');
expect(result.body.data).to.be.an('array').that.is.empty;
},
);
(IS_EE ? it : it.skip)('should only return results in the provided date interval when searching for total messages', async () => {
const yesterday = moment().subtract(1, 'days').format('YYYY-MM-DD');
const result = await request
.get(api('livechat/analytics/agent-overview'))
.query({ from: yesterday, to: yesterday, name: 'Total_messages', departmentId: department._id })
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(200);
expect(result.body).to.have.property('success', true);
expect(result.body).to.have.property('head');
expect(result.body.head).to.be.an('array').with.lengthOf(2);
expect(result.body.head[0]).to.have.property('name', 'Agent');
expect(result.body.head[1]).to.have.property('name', 'Total_messages');
expect(result.body).to.have.property('data');
expect(result.body.data).to.be.an('array').that.is.empty;
});
(IS_EE ? it : it.skip)(
'should only return results in the provided date interval when searching for average first response times',
async () => {
const yesterday = moment().subtract(1, 'days').format('YYYY-MM-DD');
const result = await request
.get(api('livechat/analytics/agent-overview'))
.query({ from: yesterday, to: yesterday, name: 'Avg_first_response_time', departmentId: department._id })
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(200);
expect(result.body).to.have.property('success', true);
expect(result.body).to.have.property('head');
expect(result.body.head).to.be.an('array').with.lengthOf(2);
expect(result.body.head[0]).to.have.property('name', 'Agent');
expect(result.body.head[1]).to.have.property('name', 'Avg_first_response_time');
expect(result.body).to.have.property('data');
expect(result.body.data).to.be.an('array').that.is.empty;
},
);
(IS_EE ? it : it.skip)(
'should only return results in the provided date interval when searching for best first response times',
async () => {
const yesterday = moment().subtract(1, 'days').format('YYYY-MM-DD');
const result = await request
.get(api('livechat/analytics/agent-overview'))
.query({ from: yesterday, to: yesterday, name: 'Best_first_response_time', departmentId: department._id })
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(200);
expect(result.body).to.have.property('success', true);
expect(result.body).to.have.property('head');
expect(result.body.head).to.be.an('array').with.lengthOf(2);
expect(result.body.head[0]).to.have.property('name', 'Agent');
expect(result.body.head[1]).to.have.property('name', 'Best_first_response_time');
expect(result.body).to.have.property('data');
expect(result.body.data).to.be.an('array').that.is.empty;
},
);
(IS_EE ? it : it.skip)(
'should only return results in the provided date interval when searching for average response times',
async () => {
const yesterday = moment().subtract(1, 'days').format('YYYY-MM-DD');
const result = await request
.get(api('livechat/analytics/agent-overview'))
.query({ from: yesterday, to: yesterday, name: 'Avg_response_time', departmentId: department._id })
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(200);
expect(result.body).to.have.property('success', true);
expect(result.body).to.have.property('head');
expect(result.body.head).to.be.an('array').with.lengthOf(2);
expect(result.body.head[0]).to.have.property('name', 'Agent');
expect(result.body.head[1]).to.have.property('name', 'Avg_response_time');
expect(result.body).to.have.property('data');
expect(result.body.data).to.be.an('array').that.is.empty;
},
);
(IS_EE ? it : it.skip)(
'should only return results in the provided date interval when searching for average reaction times',
async () => {
const yesterday = moment().subtract(1, 'days').format('YYYY-MM-DD');
const result = await request
.get(api('livechat/analytics/agent-overview'))
.query({ from: yesterday, to: yesterday, name: 'Avg_reaction_time', departmentId: department._id })
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(200);
expect(result.body).to.have.property('success', true);
expect(result.body).to.have.property('head');
expect(result.body.head).to.be.an('array').with.lengthOf(2);
expect(result.body.head[0]).to.have.property('name', 'Agent');
expect(result.body.head[1]).to.have.property('name', 'Avg_reaction_time');
expect(result.body).to.have.property('data');
expect(result.body.data).to.be.an('array').that.is.empty;
},
);
});
describe('[livechat/analytics/agent-overview] - Average first response time', () => {
let agent: { credentials: Credentials; user: IUser & { username: string } };
let forwardAgent: { credentials: Credentials; user: IUser & { username: string } };
let botAgent: { credentials: Credentials; user: IUser };
let originalFirstResponseTimeInSeconds: number;
let roomId: string;
const firstDelayInSeconds = 4;
const secondDelayInSeconds = 8;
const roomsToClose: IOmnichannelRoom[] = [];
const visitorsToDelete: ILivechatVisitor[] = [];
before(async () => {
agent = await createAnOnlineAgent();
forwardAgent = await createAnOnlineAgent();
botAgent = await createBotAgent();
await updateSetting('Omnichannel_Metrics_Ignore_Automatic_Messages', true);
});
after(async () => {
await Promise.allSettled(roomsToClose.map((room) => closeOmnichannelRoom(room._id)));
await Promise.allSettled(visitorsToDelete.map((visitor) => deleteVisitor(visitor.token)));
await Promise.all([
deleteUser(agent.user),
deleteUser(forwardAgent.user),
deleteUser(botAgent.user),
updateSetting('Omnichannel_Metrics_Ignore_Automatic_Messages', false),
]);
});
it('should return no average response time for an agent if no response has been sent in the period', async () => {
const { room, visitor } = await startANewLivechatRoomAndTakeIt({ agent: agent.credentials });
roomsToClose.push(room);
visitorsToDelete.push(visitor);
const today = moment().startOf('day').format('YYYY-MM-DD');
const result = await request
.get(api('livechat/analytics/agent-overview'))
.query({ from: today, to: today, name: 'Avg_first_response_time' })
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(200);
expect(result.body).to.have.property('success', true);
expect(result.body).to.have.property('head');
expect(result.body).to.have.property('data');
expect(result.body.data).to.be.an('array');
expect(result.body.data).to.not.deep.include({ name: agent.user.username });
});
it("should not consider system messages in agents' first response time metric", async () => {
const { room, visitor } = await startANewLivechatRoomAndTakeIt({ agent: agent.credentials });
roomsToClose.push(room);
visitorsToDelete.push(visitor);
roomId = room._id;
await sleep(firstDelayInSeconds * 1000);
await sendAgentMessage(roomId, 'first response from agent', agent.credentials);
const today = moment().startOf('day').format('YYYY-MM-DD');
const result = await request
.get(api('livechat/analytics/agent-overview'))
.query({ from: today, to: today, name: 'Avg_first_response_time' })
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(200);
expect(result.body).to.have.property('success', true);
expect(result.body).to.have.property('head');
expect(result.body).to.have.property('data');
expect(result.body.data).to.be.an('array');
const agentData = result.body.data.find(
(agentOverviewData: { name: string; value: string }) => agentOverviewData.name === agent.user.username,
);
expect(agentData).to.not.be.undefined;
expect(agentData).to.have.property('name', agent.user.username);
expect(agentData).to.have.property('value');
originalFirstResponseTimeInSeconds = moment.duration(agentData.value).asSeconds();
expect(originalFirstResponseTimeInSeconds).to.be.greaterThanOrEqual(firstDelayInSeconds);
});
it("should not consider bot messages in agent's first response time metric if setting is enabled", async () => {
const { room, visitor } = await startANewLivechatRoomAndTakeIt({ agent: botAgent.credentials });
roomsToClose.push(room);
visitorsToDelete.push(visitor);
roomId = room._id;
await sleep(firstDelayInSeconds * 1000);
await sendAgentMessage(roomId, 'first response from agent', botAgent.credentials);
const today = moment().startOf('day').format('YYYY-MM-DD');
const result = await request
.get(api('livechat/analytics/agent-overview'))
.query({ from: today, to: today, name: 'Avg_first_response_time' })
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(200);
expect(result.body).to.have.property('success', true);
expect(result.body).to.have.property('head');
expect(result.body).to.have.property('data');
expect(result.body.data).to.be.an('array');
const agentData = result.body.data.find(
(agentOverviewData: { name: string; value: string }) => agentOverviewData.name === botAgent.user.username,
);
expect(agentData).to.be.undefined;
});
it('should correctly associate the first response time to the first agent who responded the room', async () => {
const { room, visitor } = await startANewLivechatRoomAndTakeIt({ agent: forwardAgent.credentials });
roomsToClose.push(room);
visitorsToDelete.push(visitor);
roomId = room._id;
await sendAgentMessage(roomId, 'first response from agent', forwardAgent.credentials);
await request
.post(api('livechat/room.forward'))
.set(credentials)
.send({
roomId,
userId: agent.user._id,
comment: 'test comment',
})
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
});
await sendAgentMessage(roomId, 'first response from forwarded agent', agent.credentials);
const today = moment().startOf('day').format('YYYY-MM-DD');
const result = await request
.get(api('livechat/analytics/agent-overview'))
.query({ from: today, to: today, name: 'Avg_first_response_time' })
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(200);
expect(result.body).to.have.property('success', true);
expect(result.body).to.have.property('head');
expect(result.body).to.have.property('data');
expect(result.body.data).to.be.an('array');
// The agent to whom the room has been forwarded shouldn't have their average first response time changed
const agentData = result.body.data.find(
(agentOverviewData: { name: string; value: string }) => agentOverviewData.name === agent.user.username,
);
expect(agentData).to.not.be.undefined;
expect(agentData).to.have.property('name', agent.user.username);
expect(agentData).to.have.property('value');
const averageFirstResponseTimeInSeconds = moment.duration(agentData.value).asSeconds();
expect(originalFirstResponseTimeInSeconds).to.be.equal(averageFirstResponseTimeInSeconds);
// A room's first response time should be attached to the agent who first responded to it even if it has been forwarded
const forwardAgentData = result.body.data.find(
(agentOverviewData: { name: string; value: string }) => agentOverviewData.name === forwardAgent.user.username,
);
expect(forwardAgentData).to.not.be.undefined;
expect(forwardAgentData).to.have.property('name', forwardAgent.user.username);
expect(forwardAgentData).to.have.property('value');
const forwardAgentAverageFirstResponseTimeInSeconds = moment.duration(forwardAgentData.value).asSeconds();
expect(originalFirstResponseTimeInSeconds).to.be.greaterThan(forwardAgentAverageFirstResponseTimeInSeconds);
});
it('should correctly calculate the average time of first responses for an agent', async () => {
const { room, visitor } = await startANewLivechatRoomAndTakeIt({ agent: agent.credentials });
roomsToClose.push(room);
visitorsToDelete.push(visitor);
roomId = room._id;
await sleep(secondDelayInSeconds * 1000);
await sendAgentMessage(roomId, 'first response from agent', agent.credentials);
const today = moment().startOf('day').format('YYYY-MM-DD');
const result = await request
.get(api('livechat/analytics/agent-overview'))
.query({ from: today, to: today, name: 'Avg_first_response_time' })
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(200);
expect(result.body).to.have.property('success', true);
expect(result.body).to.have.property('head');
expect(result.body).to.have.property('data');
expect(result.body.data).to.be.an('array').that.is.not.empty;
const agentData = result.body.data.find(
(agentOverviewData: { name: string; value: string }) => agentOverviewData.name === agent.user.username,
);
expect(agentData).to.not.be.undefined;
expect(agentData).to.have.property('name', agent.user.username);
expect(agentData).to.have.property('value');
const averageFirstResponseTimeInSeconds = moment.duration(agentData.value).asSeconds();
expect(averageFirstResponseTimeInSeconds).to.be.greaterThan(originalFirstResponseTimeInSeconds);
expect(averageFirstResponseTimeInSeconds).to.be.greaterThanOrEqual((firstDelayInSeconds + secondDelayInSeconds) / 2);
expect(averageFirstResponseTimeInSeconds).to.be.lessThan(secondDelayInSeconds);
});
});
describe('[livechat/analytics/agent-overview] - Best first response time', () => {
let agent: { credentials: Credentials; user: IUser & { username: string } };
let forwardAgent: { credentials: Credentials; user: IUser & { username: string } };
let originalBestFirstResponseTimeInSeconds: number;
let roomId: string;
const roomsToClose: IOmnichannelRoom[] = [];
const visitorsToDelete: ILivechatVisitor[] = [];
before(async () => {
agent = await createAnOnlineAgent();
forwardAgent = await createAnOnlineAgent();
});
after(async () => {
await Promise.allSettled(roomsToClose.map((room) => closeOmnichannelRoom(room._id)));
await Promise.allSettled(visitorsToDelete.map((visitor) => deleteVisitor(visitor.token)));
await Promise.all([deleteUser(agent.user), deleteUser(forwardAgent.user)]);
});
it('should return no best response time for an agent if no response has been sent in the period', async () => {
const { room, visitor } = await startANewLivechatRoomAndTakeIt({ agent: agent.credentials });
roomsToClose.push(room);
visitorsToDelete.push(visitor);
const today = moment().startOf('day').format('YYYY-MM-DD');
const result = await request
.get(api('livechat/analytics/agent-overview'))
.query({ from: today, to: today, name: 'Best_first_response_time' })
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(200);
expect(result.body).to.have.property('success', true);
expect(result.body).to.have.property('head');
expect(result.body).to.have.property('data');
expect(result.body.data).to.be.an('array');
expect(result.body.data).to.not.deep.include({ name: agent.user.username });
});
it("should not consider system messages in agents' best response time metric", async () => {
const { room, visitor } = await startANewLivechatRoomAndTakeIt({ agent: agent.credentials });
roomsToClose.push(room);
visitorsToDelete.push(visitor);
roomId = room._id;
const delayInSeconds = 4;
await sleep(delayInSeconds * 1000);
await sendAgentMessage(roomId, 'first response from agent', agent.credentials);
const today = moment().startOf('day').format('YYYY-MM-DD');
const result = await request
.get(api('livechat/analytics/agent-overview'))
.query({ from: today, to: today, name: 'Best_first_response_time' })
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(200);
expect(result.body).to.have.property('success', true);
expect(result.body).to.have.property('head');
expect(result.body).to.have.property('data');
expect(result.body.data).to.be.an('array').that.is.not.empty;
const agentData = result.body.data.find(
(agentOverviewData: { name: string; value: string }) => agentOverviewData.name === agent.user.username,
);
expect(agentData).to.not.be.undefined;
expect(agentData).to.have.property('name', agent.user.username);
expect(agentData).to.have.property('value');
originalBestFirstResponseTimeInSeconds = moment.duration(agentData.value).asSeconds();
expect(originalBestFirstResponseTimeInSeconds).to.be.greaterThanOrEqual(delayInSeconds);
});
it('should correctly calculate the best first response time for an agent and there are multiple first responses in the period', async () => {
const { room, visitor } = await startANewLivechatRoomAndTakeIt({ agent: agent.credentials });
roomsToClose.push(room);
visitorsToDelete.push(visitor);
roomId = room._id;
const delayInSeconds = 6;
await sleep(delayInSeconds * 1000);
await sendAgentMessage(roomId, 'first response from agent', agent.credentials);
const today = moment().startOf('day').format('YYYY-MM-DD');
const result = await request
.get(api('livechat/analytics/agent-overview'))
.query({ from: today, to: today, name: 'Best_first_response_time' })
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(200);
expect(result.body).to.have.property('success', true);
expect(result.body).to.have.property('head');
expect(result.body).to.have.property('data');
expect(result.body.data).to.be.an('array');
const agentData = result.body.data.find(
(agentOverviewData: { name: string; value: string }) => agentOverviewData.name === agent.user.username,
);
expect(agentData).to.not.be.undefined;
expect(agentData).to.have.property('name', agent.user.username);
expect(agentData).to.have.property('value');
const bestFirstResponseTimeInSeconds = moment.duration(agentData.value).asSeconds();
expect(bestFirstResponseTimeInSeconds).to.be.equal(originalBestFirstResponseTimeInSeconds);
});
it('should correctly associate best first response time to the first agent who responded the room', async () => {
const { room, visitor } = await startANewLivechatRoomAndTakeIt({ agent: forwardAgent.credentials });
roomsToClose.push(room);
visitorsToDelete.push(visitor);
roomId = room._id;
await sendAgentMessage(roomId, 'first response from agent', forwardAgent.credentials);
await request
.post(api('livechat/room.forward'))
.set(credentials)
.send({
roomId,
userId: agent.user._id,
comment: 'test comment',
})
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
});
await sendAgentMessage(roomId, 'first response from forwarded agent', agent.credentials);
const today = moment().startOf('day').format('YYYY-MM-DD');
const result = await request
.get(api('livechat/analytics/agent-overview'))
.query({ from: today, to: today, name: 'Best_first_response_time' })
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(200);
expect(result.body).to.have.property('success', true);
expect(result.body).to.have.property('head');
expect(result.body).to.have.property('data');
expect(result.body.data).to.be.an('array');
// The agent to whom the room has been forwarded shouldn't have their best first response time changed
const agentData = result.body.data.find(
(agentOverviewData: { name: string; value: string }) => agentOverviewData.name === agent.user.username,
);
expect(agentData).to.not.be.undefined;
expect(agentData).to.have.property('name', agent.user.username);
expect(agentData).to.have.property('value');
const bestFirstResponseTimeInSeconds = moment.duration(agentData.value).asSeconds();
expect(bestFirstResponseTimeInSeconds).to.be.equal(originalBestFirstResponseTimeInSeconds);
// A room's first response time should be attached to the agent who first responded to it even if it has been forwarded
const forwardAgentData = result.body.data.find(
(agentOverviewData: { name: string; value: string }) => agentOverviewData.name === forwardAgent.user.username,
);
expect(forwardAgentData).to.not.be.undefined;
expect(forwardAgentData).to.have.property('name', forwardAgent.user.username);
expect(forwardAgentData).to.have.property('value');
const forwardAgentBestFirstResponseTimeInSeconds = moment.duration(forwardAgentData.value).asSeconds();
expect(forwardAgentBestFirstResponseTimeInSeconds).to.be.lessThan(originalBestFirstResponseTimeInSeconds);
});
});
describe('livechat/analytics/overview', () => {
it('should return an "unauthorized error" when the user does not have the necessary permission', async () => {
await removePermissionFromAllRoles('view-livechat-manager');
await request
.get(api('livechat/analytics/overview'))
.query({ from: '2020-01-01', to: '2020-01-02', name: 'Conversations' })
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(403);
});
it('should return an "invalid-chart-name error" when the chart name is empty', async () => {
await restorePermissionToRoles('view-livechat-manager');
await request
.get(api('livechat/analytics/overview'))
.query({ from: '2020-01-01', to: '2020-01-02', name: '' })
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(400);
});
it('should return empty when chart name is invalid', async () => {
await request
.get(api('livechat/analytics/overview'))
.query({ from: '2020-01-01', to: '2020-01-02', name: 'invalid-chart-name' })
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
expect(Object.keys(res.body)).to.have.lengthOf(1);
});
});
it('should return an array of analytics overview data', async () => {
const result = await request
.get(api('livechat/analytics/overview'))
.query({ from: '2020-01-01', to: '2020-01-02', name: 'Conversations' })
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(200);
expect(result.body).to.be.an('array');
expect(result.body).to.have.lengthOf(7);
expect(result.body[0]).to.have.property('title', 'Total_conversations');
expect(result.body[0]).to.have.property('value', 0);
});
(IS_EE ? it : it.skip)('should return analytics overview data with correct values', async () => {
const yesterday = moment().subtract(1, 'days').format('YYYY-MM-DD');
const today = moment().startOf('day').format('YYYY-MM-DD');
const result = await request
.get(api('livechat/analytics/overview'))
.query({ from: yesterday, to: today, name: 'Conversations', departmentId: department._id })
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(200);
expect(result.body).to.be.an('array');
const expectedResult = [
{ title: 'Total_conversations', value: 7 },
{ title: 'Open_conversations', value: 4 },
{ title: 'On_Hold_conversations', value: 1 },
// { title: 'Total_messages', value: 6 },
// { title: 'Busiest_day', value: moment().format('dddd') },
{ title: 'Conversations_per_day', value: '3.50' },
// { title: 'Busiest_time', value: '' },
];
expectedResult.forEach((expected) => {
const resultItem = result.body.find((item: any) => item.title === expected.title);
expect(resultItem).to.not.be.undefined;
expect(resultItem).to.have.property('value', expected.value);
});
const minMessages = TOTAL_MESSAGES.min * TOTAL_ROOMS;
const totalMessages = result.body.find((item: any) => item.title === 'Total_messages');
expect(totalMessages).to.not.be.undefined;
const totalMessagesValue = parseInt(totalMessages.value);
expect(totalMessagesValue).to.be.greaterThanOrEqual(minMessages);
});
(IS_EE ? it : it.skip)(
'should only consider conversations in the provided time range when returning analytics conversations overview data',
async () => {
const yesterday = moment().subtract(1, 'days').format('YYYY-MM-DD');
const result = await request
.get(api('livechat/analytics/overview'))
.query({ from: yesterday, to: yesterday, name: 'Conversations', departmentId: department._id })
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(200);
expect(result.body).to.be.an('array');
const expectedResult = [
{ title: 'Total_conversations', value: 0 },
{ title: 'Open_conversations', value: 0 },
{ title: 'On_Hold_conversations', value: 0 },
{ title: 'Conversations_per_day', value: '0.00' },
];
expectedResult.forEach((expected) => {
const resultItem = result.body.find((item: any) => item.title === expected.title);
expect(resultItem).to.not.be.undefined;
expect(resultItem).to.have.property('value', expected.value);
});
},
);
(IS_EE ? it : it.skip)(
'should only consider conversations in the provided time range when returning analytics productivity overview data',
async () => {
const yesterday = moment().subtract(1, 'days').format('YYYY-MM-DD');
const result = await request
.get(api('livechat/analytics/overview'))
.query({ from: yesterday, to: yesterday, name: 'Productivity', departmentId: department._id })
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(200);
expect(result.body).to.be.an('array');
const expectedResult = [
{ title: 'Avg_response_time', value: '00:00:00' },
{ title: 'Avg_first_response_time', value: '00:00:00' },
{ title: 'Avg_reaction_time', value: '00:00:00' },
];
expectedResult.forEach((expected) => {
const resultItem = result.body.find((item: any) => item.title === expected.title);
expect(resultItem).to.not.be.undefined;
expect(resultItem).to.have.property('value', expected.value);
});
},
);
});
});