Skip to content
Snippets Groups Projects
Unverified Commit a2222386 authored by Martin Schoeler's avatar Martin Schoeler Committed by GitHub
Browse files

regression: Livechat resubscription (#31985)

parent e350be57
No related branches found
No related tags found
No related merge requests found
......@@ -5,7 +5,12 @@ import { Users } from '../fixtures/userStates';
import { HomeOmnichannel, OmnichannelLiveChat } from '../page-objects';
import { test, expect } from '../utils/test';
const newUser = {
const firstUser = {
name: `${faker.person.firstName()} ${faker.string.uuid()}}`,
email: faker.internet.email(),
};
const secondUser = {
name: `${faker.person.firstName()} ${faker.string.uuid()}}`,
email: faker.internet.email(),
};
......@@ -43,7 +48,7 @@ test.describe.serial('OC - Livechat', () => {
await test.step('expect message to be sent by livechat', async () => {
await poLiveChat.page.reload();
await poLiveChat.openLiveChat();
await poLiveChat.sendMessage(newUser, false);
await poLiveChat.sendMessage(firstUser, false);
await poLiveChat.onlineAgentMessage.type('this_a_test_message_from_user');
await poLiveChat.btnSendMessageToOnlineAgent.click();
......@@ -52,14 +57,14 @@ test.describe.serial('OC - Livechat', () => {
});
await test.step('expect message to be received by agent', async () => {
await poHomeOmnichannel.sidenav.openChat(newUser.name);
await poHomeOmnichannel.sidenav.openChat(firstUser.name);
await expect(poHomeOmnichannel.content.lastUserMessage).toBeVisible();
await expect(poHomeOmnichannel.content.lastUserMessage).toContainText('this_a_test_message_from_user');
});
});
test('OC - Livechat - Send message to livechat costumer', async () => {
await poHomeOmnichannel.sidenav.openChat(newUser.name);
await poHomeOmnichannel.sidenav.openChat(firstUser.name);
await test.step('expect message to be sent by agent', async () => {
await poHomeOmnichannel.content.sendMessage('this_a_test_message_from_agent');
......@@ -83,7 +88,7 @@ test.describe.serial('OC - Livechat', () => {
});
test('OC - Livechat - Close livechat conversation', async () => {
await poHomeOmnichannel.sidenav.openChat(newUser.name);
await poHomeOmnichannel.sidenav.openChat(firstUser.name);
await test.step('expect livechat conversation to be closed by agent', async () => {
await poHomeOmnichannel.content.btnCloseChat.click();
......@@ -93,3 +98,51 @@ test.describe.serial('OC - Livechat', () => {
});
});
});
test.describe.serial('OC - Livechat - Resub after close room', () => {
let poLiveChat: OmnichannelLiveChat;
let poHomeOmnichannel: HomeOmnichannel;
test.beforeAll(async ({ api }) => {
const statusCode = (await api.post('/livechat/users/agent', { username: 'user1' })).status();
await expect(statusCode).toBe(200);
});
test.beforeAll(async ({ browser, api }) => {
await api.post('/settings/Livechat_clear_local_storage_when_chat_ended', { value: true });
const { page: omniPage } = await createAuxContext(browser, Users.user1, '/', true);
poHomeOmnichannel = new HomeOmnichannel(omniPage);
const { page: livechatPage } = await createAuxContext(browser, Users.user1, '/livechat', false);
poLiveChat = new OmnichannelLiveChat(livechatPage, api);
await poLiveChat.sendMessageAndCloseChat(firstUser);
});
test.afterAll(async ({ api }) => {
await api.post('/settings/Livechat_clear_local_storage_when_chat_ended', { value: false });
await api.delete('/livechat/users/agent/user1');
await poLiveChat.page?.close();
});
test('OC - Livechat - Resub after close room', async () => {
await test.step('expect livechat conversation to be opened again', async () => {
await poLiveChat.startNewChat();
await poLiveChat.sendMessage(secondUser, false);
await poLiveChat.onlineAgentMessage.type('this_a_test_message_from_user');
await poLiveChat.btnSendMessageToOnlineAgent.click();
await expect(poLiveChat.page.locator('div >> text="this_a_test_message_from_user"')).toBeVisible();
});
await test.step('expect message to be received by agent', async () => {
await poHomeOmnichannel.sidenav.openChat(secondUser.name);
await expect(poHomeOmnichannel.content.lastUserMessage).toBeVisible();
await expect(poHomeOmnichannel.content.lastUserMessage).toContainText('this_a_test_message_from_user');
});
await test.step('expect message to be sent by agent', async () => {
await poHomeOmnichannel.content.sendMessage('this_a_test_message_from_agent');
await expect(poLiveChat.page.locator('div >> text="this_a_test_message_from_agent"')).toBeVisible();
});
});
});
......@@ -7,10 +7,18 @@ export class OmnichannelLiveChat {
this.page = page;
}
btnOpenLiveChat(label: string): Locator {
btnOpenOnlineLiveChat(label: string): Locator {
return this.page.locator(`role=button[name="${label}"]`);
}
btnOpenLiveChat(): Locator {
return this.page.locator(`[data-qa-id="chat-button"]`);
}
get btnNewChat(): Locator {
return this.page.locator(`role=button[name="New Chat"]`);
}
get btnOptions(): Locator {
return this.page.locator(`button >> text="Options"`);
}
......@@ -43,7 +51,17 @@ export class OmnichannelLiveChat {
async openLiveChat(): Promise<void> {
const { value: siteName } = await (await this.api.get('/settings/Site_Name')).json();
await this.btnOpenLiveChat(siteName).click();
await this.btnOpenOnlineLiveChat(siteName).click();
}
// TODO: replace openLivechat with this method and create a new method for openOnlineLivechat
// as openLivechat only opens a chat that is in the 'online' state
async openAnyLiveChat(): Promise<void> {
await this.btnOpenLiveChat().click();
}
async startNewChat(): Promise<void> {
await this.btnNewChat.click();
}
unreadMessagesBadge(count: number): Locator {
......@@ -94,4 +112,12 @@ export class OmnichannelLiveChat {
await this.btnSendMessage(buttonLabel).click();
await this.page.waitForSelector('[data-qa="livechat-composer"]');
}
public async sendMessageAndCloseChat(liveChatUser: { name: string; email: string }): Promise<void> {
await this.openLiveChat();
await this.sendMessage(liveChatUser, false);
await this.onlineAgentMessage.type('this_a_test_message_from_user');
await this.btnSendMessageToOnlineAgent.click();
await this.closeChat();
}
}
......@@ -12,121 +12,126 @@ import type {
} from '@rocket.chat/ui-contexts';
import { ServerContext } from '@rocket.chat/ui-contexts';
import { compile } from 'path-to-regexp';
import { useMemo } from 'preact/hooks';
import React from 'react';
import { host } from '../components/App';
import store from '../store';
import { useStore } from '../store';
import { useSDK } from './SDKProvider';
const ServerProvider = ({ children }: { children: React.ReactNode }) => {
const sdk = useSDK();
const { token } = store.state;
const absoluteUrl = (path: string): string => {
return `${host}${path}`;
};
const callMethod = <MethodName extends ServerMethodName>(
methodName: MethodName,
...args: ServerMethodParameters<MethodName>
): Promise<ServerMethodReturn<MethodName>> => sdk.client.callAsync(methodName, ...args);
const callEndpoint = <TMethod extends Method, TPathPattern extends PathPattern>({
method,
pathPattern,
keys,
params,
}: {
method: TMethod;
pathPattern: TPathPattern;
keys: UrlParams<TPathPattern>;
params: OperationParams<TMethod, TPathPattern>;
}): Promise<Serialized<OperationResult<TMethod, TPathPattern>>> => {
const compiledPath = compile(pathPattern, { encode: encodeURIComponent })(keys) as any;
switch (method) {
case 'GET':
return sdk.rest.get(compiledPath, params as any) as any;
case 'POST':
return sdk.rest.post(compiledPath, params as any) as any;
case 'PUT':
return sdk.rest.put(compiledPath, params as never) as never;
case 'DELETE':
return sdk.rest.delete(compiledPath, params as any) as any;
default:
throw new Error('Invalid HTTP method');
}
};
const uploadToEndpoint = (endpoint: PathFor<'POST'>, formData: any): Promise<UploadResult> => sdk.rest.post(endpoint as any, formData);
const getStream = <N extends StreamNames, K extends StreamKeys<N>>(
streamName: N,
_options?: {
retransmit?: boolean | undefined;
retransmitToSelf?: boolean | undefined;
},
): ((eventName: K, callback: (...args: StreamerCallbackArgs<N, K>) => void) => () => void) => {
return (eventName, callback): (() => void) => {
return sdk.stream(streamName, [eventName, { visitorToken: token, token }], callback as (...args: any[]) => void).stop;
};
};
const ee = new Emitter<Record<string, void>>();
const { token } = useStore();
const events = new Map<string, () => void>();
const contextValue = useMemo(() => {
const absoluteUrl = (path: string): string => {
return `${host}${path}`;
};
const getSingleStream = <N extends StreamNames, K extends StreamKeys<N>>(
streamName: N,
_options?: {
retransmit?: boolean | undefined;
retransmitToSelf?: boolean | undefined;
},
): ((eventName: K, callback: (...args: StreamerCallbackArgs<N, K>) => void) => () => void) => {
const stream = getStream(streamName);
return (eventName, callback): (() => void) => {
ee.on(`${streamName}/${eventName}`, callback);
const callMethod = <MethodName extends ServerMethodName>(
methodName: MethodName,
...args: ServerMethodParameters<MethodName>
): Promise<ServerMethodReturn<MethodName>> => sdk.client.callAsync(methodName, ...args);
const callEndpoint = <TMethod extends Method, TPathPattern extends PathPattern>({
method,
pathPattern,
keys,
params,
}: {
method: TMethod;
pathPattern: TPathPattern;
keys: UrlParams<TPathPattern>;
params: OperationParams<TMethod, TPathPattern>;
}): Promise<Serialized<OperationResult<TMethod, TPathPattern>>> => {
const compiledPath = compile(pathPattern, { encode: encodeURIComponent })(keys) as any;
switch (method) {
case 'GET':
return sdk.rest.get(compiledPath, params as any) as any;
case 'POST':
return sdk.rest.post(compiledPath, params as any) as any;
case 'PUT':
return sdk.rest.put(compiledPath, params as never) as never;
case 'DELETE':
return sdk.rest.delete(compiledPath, params as any) as any;
default:
throw new Error('Invalid HTTP method');
}
};
const handler = (...args: any[]): void => {
ee.emit(`${streamName}/${eventName}`, ...args);
const uploadToEndpoint = (endpoint: PathFor<'POST'>, formData: any): Promise<UploadResult> => sdk.rest.post(endpoint as any, formData);
const getStream = <N extends StreamNames, K extends StreamKeys<N>>(
streamName: N,
_options?: {
retransmit?: boolean | undefined;
retransmitToSelf?: boolean | undefined;
},
): ((eventName: K, callback: (...args: StreamerCallbackArgs<N, K>) => void) => () => void) => {
return (eventName, callback): (() => void) => {
return sdk.stream(streamName, [eventName, { visitorToken: token, token }], callback as (...args: any[]) => void).stop;
};
};
const stop = (): void => {
// If someone is still listening, don't unsubscribe
ee.off(`${streamName}/${eventName}`, callback);
if (ee.has(`${streamName}/${eventName}`)) {
return;
}
const unsubscribe = events.get(`${streamName}/${eventName}`);
if (unsubscribe) {
unsubscribe();
events.delete(`${streamName}/${eventName}`);
const ee = new Emitter<Record<string, void>>();
const events = new Map<string, () => void>();
const getSingleStream = <N extends StreamNames, K extends StreamKeys<N>>(
streamName: N,
_options?: {
retransmit?: boolean | undefined;
retransmitToSelf?: boolean | undefined;
},
): ((eventName: K, callback: (...args: StreamerCallbackArgs<N, K>) => void) => () => void) => {
const stream = getStream(streamName);
return (eventName, callback): (() => void) => {
ee.on(`${streamName}/${eventName}`, callback);
const handler = (...args: any[]): void => {
ee.emit(`${streamName}/${eventName}`, ...args);
};
const stop = (): void => {
// If someone is still listening, don't unsubscribe
ee.off(`${streamName}/${eventName}`, callback);
if (ee.has(`${streamName}/${eventName}`)) {
return;
}
const unsubscribe = events.get(`${streamName}/${eventName}`);
if (unsubscribe) {
unsubscribe();
events.delete(`${streamName}/${eventName}`);
}
};
if (!events.has(`${streamName}/${eventName}`)) {
events.set(`${streamName}/${eventName}`, stream(eventName, handler));
}
return stop;
};
};
if (!events.has(`${streamName}/${eventName}`)) {
events.set(`${streamName}/${eventName}`, stream(eventName, handler));
}
return stop;
const contextValue = {
// info,
absoluteUrl,
callMethod,
callEndpoint,
uploadToEndpoint,
getStream,
getSingleStream,
};
};
const contextValue = {
// info,
absoluteUrl,
callMethod,
callEndpoint,
uploadToEndpoint,
getStream,
getSingleStream,
};
return contextValue;
}, [sdk, token]);
return <ServerContext.Provider children={children} value={contextValue} />;
};
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment