Odoo18-Base/addons/mail/static/tests/discuss/call/call.test.js

479 lines
19 KiB
JavaScript
Raw Permalink Normal View History

2025-01-06 10:57:38 +07:00
import {
SIZES,
assertSteps,
click,
contains,
defineMailModels,
mockGetMedia,
onRpcBefore,
openDiscuss,
patchUiSize,
start,
startServer,
step,
triggerEvents,
} from "@mail/../tests/mail_test_helpers";
import { mailDataHelpers } from "@mail/../tests/mock_server/mail_mock_server";
import { describe, expect, test } from "@odoo/hoot";
import { hover, queryFirst } from "@odoo/hoot-dom";
import { mockUserAgent } from "@odoo/hoot-mock";
import {
Command,
mockService,
patchWithCleanup,
serverState,
} from "@web/../tests/web_test_helpers";
import { browser } from "@web/core/browser/browser";
import { isMobileOS } from "@web/core/browser/feature_detection";
describe.current.tags("desktop");
defineMailModels();
test("basic rendering", async () => {
mockGetMedia();
const pyEnv = await startServer();
const channelId = pyEnv["discuss.channel"].create({ name: "General" });
await start();
await openDiscuss(channelId);
await click("[title='Start a Call']");
await contains(".o-discuss-Call");
await contains(".o-discuss-CallParticipantCard[aria-label='Mitchell Admin']");
await contains(".o-discuss-CallActionList");
await contains(".o-discuss-CallMenu-buttonContent");
await contains(".o-discuss-CallActionList button", { count: 6 });
await contains("button[aria-label='Unmute'], button[aria-label='Mute']"); // FIXME depends on current browser permission
await contains(".o-discuss-CallActionList button[aria-label='Deafen']");
await contains(".o-discuss-CallActionList button[aria-label='Turn camera on']");
await contains(".o-discuss-CallActionList button[aria-label='Share Screen']");
await contains("[title='More']");
await contains(".o-discuss-CallActionList button[aria-label='Disconnect']");
await click("[title='More']");
await contains("[title='Raise Hand']");
await contains("[title='Enter Full Screen']");
// screen sharing not available in mobile OS
mockUserAgent("Chrome/0.0.0 Android (OdooMobile; Linux; Android 13; Odoo TestSuite)");
expect(isMobileOS()).toBe(true);
await contains("[title='Share Screen']", { count: 0 });
});
test("keep the `more` popover active when hovering it", async () => {
mockGetMedia();
const pyEnv = await startServer();
const channelId = pyEnv["discuss.channel"].create({ name: "General" });
await start();
await openDiscuss(channelId);
await click("[title='Start a Call']");
await contains(".o-discuss-Call");
await contains(".o-discuss-CallActionList");
await click("[title='More']");
const enterFullScreenSelector = ".o-dropdown-item[title='Enter Full Screen']";
await contains(enterFullScreenSelector);
await hover(queryFirst(enterFullScreenSelector));
await contains(enterFullScreenSelector);
});
test("no call with odoobot", async () => {
const pyEnv = await startServer();
const channelId = pyEnv["discuss.channel"].create({
channel_member_ids: [
Command.create({ partner_id: serverState.partnerId }),
Command.create({ partner_id: serverState.odoobotId }),
],
channel_type: "chat",
});
await start();
await openDiscuss(channelId);
await contains(".o-mail-Discuss-header");
await contains("[title='Start a Call']", { count: 0 });
});
test("should not display call UI when no more members (self disconnect)", async () => {
mockGetMedia();
const pyEnv = await startServer();
const channelId = pyEnv["discuss.channel"].create({ name: "General" });
await start();
await openDiscuss(channelId);
await click("[title='Start a Call']");
await contains(".o-discuss-Call");
await click(".o-discuss-CallActionList button[aria-label='Disconnect']");
await contains(".o-discuss-Call", { count: 0 });
});
test("show call UI in chat window when in call", async () => {
mockGetMedia();
const pyEnv = await startServer();
pyEnv["discuss.channel"].create({ name: "General" });
await start();
await click(".o_menu_systray i[aria-label='Messages']");
await click(".o-mail-NotificationItem", { text: "General" });
await contains(".o-mail-ChatWindow");
await contains(".o-discuss-Call", { count: 0 });
await click(".o-mail-ChatWindow-command[title='Start a Call']");
await contains(".o-discuss-Call");
await contains(".o-mail-ChatWindow-command[title='Start a Call']", { count: 0 });
});
test("should disconnect when closing page while in call", async () => {
mockGetMedia();
const pyEnv = await startServer();
const channelId = pyEnv["discuss.channel"].create({ name: "General" });
await start();
await openDiscuss(channelId);
patchWithCleanup(browser, {
navigator: {
...browser.navigator,
sendBeacon: async (route, data) => {
if (data instanceof Blob && route === "/mail/rtc/channel/leave_call") {
const blobText = await data.text();
const blobData = JSON.parse(blobText);
step(`sendBeacon_leave_call:${blobData.params.channel_id}`);
}
},
},
});
await click("[title='Start a Call']");
await contains(".o-discuss-Call");
// simulate page close
window.dispatchEvent(new Event("pagehide"), { bubble: true });
await assertSteps([`sendBeacon_leave_call:${channelId}`]);
});
test("should display invitations", async () => {
const pyEnv = await startServer();
const channelId = pyEnv["discuss.channel"].create({ name: "General" });
const partnerId = pyEnv["res.partner"].create({ name: "InvitationSender" });
const memberId = pyEnv["discuss.channel.member"].create({
channel_id: channelId,
partner_id: partnerId,
});
const sessionId = pyEnv["discuss.channel.rtc.session"].create({
channel_member_id: memberId,
channel_id: channelId,
});
onRpcBefore("/mail/data", (args) => {
if (args.init_messaging) {
step(`/mail/data - ${JSON.stringify(args)}`);
}
});
mockService("mail.sound_effects", {
play(name) {
step(`play - ${name}`);
},
stop(name) {
step(`stop - ${name}`);
},
});
await start();
await assertSteps([
`/mail/data - ${JSON.stringify({
init_messaging: {},
failures: true,
systray_get_activities: true,
context: { lang: "en", tz: "taht", uid: serverState.userId, allowed_company_ids: [1] },
})}`,
]);
const [partner] = pyEnv["res.partner"].read(serverState.partnerId);
// send after init_messaging because bus subscription is done after init_messaging
pyEnv["bus.bus"]._sendone(
partner,
"mail.record/insert",
new mailDataHelpers.Store(pyEnv["discuss.channel.rtc.session"].browse(sessionId), {
channelMember: { id: memberId },
})
.add(pyEnv["discuss.channel.member"].browse(memberId), {
persona: { id: partnerId, type: "partner" },
thread: { id: channelId, model: "discuss.channel" },
})
.add(pyEnv["discuss.channel"].browse(channelId), {
rtcInvitingSession: { id: sessionId },
})
.get_result()
);
await contains(".o-discuss-CallInvitation");
await assertSteps(["play - incoming-call"]);
// Simulate stop receiving call invitation
pyEnv["bus.bus"]._sendone(
partner,
"mail.record/insert",
new mailDataHelpers.Store(pyEnv["discuss.channel"].browse(channelId), {
rtcInvitingSession: false,
}).get_result()
);
await contains(".o-discuss-CallInvitation", { count: 0 });
await assertSteps(["stop - incoming-call"]);
});
test("can share screen", async () => {
mockGetMedia();
const pyEnv = await startServer();
const channelId = pyEnv["discuss.channel"].create({ name: "General" });
await start();
await openDiscuss(channelId);
await click("[title='Start a Call']");
await click("[title='More']");
await click("[title='Share Screen']");
await contains("video");
await triggerEvents(".o-discuss-Call-mainCards", ["mousemove"]); // show overlay
await click("[title='More']");
await click("[title='Stop Sharing Screen']");
await contains("video", { count: 0 });
});
test("can share user camera", async () => {
mockGetMedia();
const pyEnv = await startServer();
const channelId = pyEnv["discuss.channel"].create({ name: "General" });
await start();
await openDiscuss(channelId);
await click("[title='Start a Call']");
await click("[title='Turn camera on']");
await contains("video");
await click("[title='Stop camera']");
await contains("video", { count: 0 });
});
test("Card should remain in focus after Share Screen is toggled", async () => {
mockGetMedia();
const pyEnv = await startServer();
const channelId = pyEnv["discuss.channel"].create({ name: "General" });
await start();
await openDiscuss(channelId);
await click("[title='Start a Call']");
await contains(".o-discuss-Call.o-minimized"); // minimized = no card focused
await click(".o-discuss-CallParticipantCard-avatar");
await contains(
".o-discuss-Call:not(.o-minimized) .o-discuss-CallParticipantCard:not(.o-inset) img" // inset = aside, not inset = center
);
await click("[title='Share Screen']");
await contains(".o-discuss-CallParticipantCard.o-inset img");
await contains(
".o-discuss-Call:not(.o-minimized) .o-discuss-CallParticipantCard:not(.o-inset) video[type='screen']"
);
await click("[title='Stop Sharing Screen']");
await contains(".o-discuss-CallParticipantCard.o-inset", { count: 0 });
await contains(
".o-discuss-Call:not(.o-minimized) .o-discuss-CallParticipantCard:not(.o-inset) img"
);
});
test("Camera video stream stays in focus when on/off", async () => {
mockGetMedia();
const pyEnv = await startServer();
const channelId = pyEnv["discuss.channel"].create({ name: "General" });
await start();
await openDiscuss(channelId);
await click("[title='Start a Call']");
await click(".o-discuss-CallParticipantCard-avatar");
await click("[title='Turn camera on']");
await click("[title='Stop camera']");
await click("[title='Turn camera on']");
await contains("video[type='camera']:not(.o-inset)");
// test screen sharing then camera on to check camera aside
await click("[title='Stop camera']");
await click("[title='Share Screen']");
await click("[title='Turn camera on']");
await contains("video[type='screen']:not(.o-inset)");
await contains("video[type='camera'].o-inset");
});
test("Create a direct message channel when clicking on start a meeting", async () => {
mockGetMedia();
await start();
await openDiscuss();
await click("button", { text: "Start a meeting" });
await contains(".o-mail-DiscussSidebarChannel", { text: "Mitchell Admin" });
await contains(".o-discuss-Call");
await contains(".o-discuss-ChannelInvitation");
});
test("Can share user camera and screen together", async () => {
mockGetMedia();
const pyEnv = await startServer();
const channelId = pyEnv["discuss.channel"].create({ name: "General" });
await start();
await openDiscuss(channelId);
await click("[title='Start a Call']");
await click("[title='More']");
await click("[title='Share Screen']");
await click("[title='Turn camera on']");
await contains("video", { count: 2 });
});
test("Click on inset card should replace the inset and active stream together", async () => {
mockGetMedia();
const pyEnv = await startServer();
const channelId = pyEnv["discuss.channel"].create({ name: "General" });
await start();
await openDiscuss(channelId);
await click("[title='Start a Call']");
await click("[title='More']");
await click("[title='Share Screen']");
await click("[title='Turn camera on']");
await contains("video[type='screen']:not(.o-inset)");
await click("video[type='camera'].o-inset");
await contains("video[type='screen'].o-inset");
await contains("video[type='camera']:not(.o-inset)");
});
test("join/leave sounds are only played on main tab", async () => {
mockGetMedia();
const pyEnv = await startServer();
const channelId = pyEnv["discuss.channel"].create({ name: "General" });
const env1 = await start({ asTab: true });
const env2 = await start({ asTab: true });
patchWithCleanup(env1.services["mail.sound_effects"], {
play(name) {
step(`tab1 - play - ${name}`);
},
});
patchWithCleanup(env2.services["mail.sound_effects"], {
play(name) {
step(`tab2 - play - ${name}`);
},
});
await openDiscuss(channelId, { target: env1 });
await openDiscuss(channelId, { target: env2 });
await click("[title='Start a Call']", { target: env1 });
await contains(".o-discuss-Call", { target: env1 });
await contains(".o-discuss-Call", { target: env2 });
await assertSteps(["tab1 - play - channel-join"]);
await click("[title='Disconnect']:not([disabled])", { target: env1 });
await contains(".o-discuss-Call", { target: env1, count: 0 });
await contains(".o-discuss-Call", { target: env2, count: 0 });
await assertSteps(["tab1 - play - channel-leave"]);
});
test("'Start a meeting' in mobile", async () => {
mockGetMedia();
patchUiSize({ size: SIZES.SM });
const pyEnv = await startServer();
const partnerId = pyEnv["res.partner"].create({ name: "Partner 2" });
pyEnv["res.users"].create({ partner_id: partnerId });
pyEnv["discuss.channel"].create({ name: "Slytherin" });
await start();
await openDiscuss();
await contains("button.active", { text: "Inbox" });
await click("button", { text: "Chat" });
await click("button", { text: "Start a meeting" });
await click(".o-discuss-ChannelInvitation-selectable", { text: "Partner 2" });
await click("button:not([disabled])", { text: "Invite to Group Chat" });
await contains(".o-discuss-Call");
await click("[title='Open Actions Menu']");
await click(".o-dropdown-item", { text: "Members" });
await contains(".o-discuss-ChannelMember", { text: "Partner 2" });
});
test("Systray icon shows latest action", async () => {
mockGetMedia();
const pyEnv = await startServer();
const channelId = pyEnv["discuss.channel"].create({ name: "General" });
await start();
await openDiscuss(channelId);
await click("[title='Start a Call']");
await contains(".o-discuss-CallMenu-buttonContent .fa-microphone");
await click("[title='Mute']");
await contains(".o-discuss-CallMenu-buttonContent .fa-microphone-slash");
await click("[title='Deafen']");
await contains(".o-discuss-CallMenu-buttonContent .fa-deaf");
await click("[title='Turn camera on']");
await contains(".o-discuss-CallMenu-buttonContent .fa-video-camera");
await click("[title='More']");
await click("[title='Share Screen']");
await contains(".o-discuss-CallMenu-buttonContent .fa-desktop");
await triggerEvents(".o-discuss-Call-mainCards", ["mousemove"]); // show overlay
await click("[title='More']");
await click("[title='Raise Hand']");
await contains(".o-discuss-CallMenu-buttonContent .fa-hand-paper-o");
});
test("Systray icon keeps track of earlier actions", async () => {
mockGetMedia();
const pyEnv = await startServer();
const channelId = pyEnv["discuss.channel"].create({ name: "General" });
await start();
await openDiscuss(channelId);
await click("[title='Start a Call']");
await contains(".o-discuss-CallMenu-buttonContent .fa-microphone");
await click("[title='More']");
await click("[title='Share Screen']");
// stack: ["share-screen"]
await contains(".o-discuss-CallMenu-buttonContent .fa-desktop");
await triggerEvents(".o-discuss-Call-mainCards", ["mousemove"]); // show overlay
await click("[title='Turn camera on']");
// stack: ["video", "share-screen"]
await contains(".o-discuss-CallMenu-buttonContent .fa-video-camera");
await click("[title='Mute']");
// stack: ["mute", "video", "share-screen"]
await contains(".o-discuss-CallMenu-buttonContent .fa-microphone-slash");
await click("[title='Unmute']");
// stack: ["video", "share-screen"]
await contains(".o-discuss-CallMenu-buttonContent .fa-video-camera");
await click("[title='Stop camera']");
// stack: ["share-screen"]
await contains(".o-discuss-CallMenu-buttonContent .fa-desktop");
});
test("show call participants in discuss sidebar", async () => {
mockGetMedia();
const pyEnv = await startServer();
const channelId = pyEnv["discuss.channel"].create({ name: "General" });
await start();
await openDiscuss(channelId);
await click("[title='Start a Call']");
await contains(".o-mail-DiscussSidebar", {
contains: [
".o-mail-DiscussSidebarChannel:contains('General') ~ .o-mail-DiscussSidebarCallParticipants:contains(Mitchell Admin)",
],
});
});
test("Sort call participants in side bar by name", async () => {
mockGetMedia();
const pyEnv = await startServer();
const channelId = pyEnv["discuss.channel"].create({ name: "General" });
pyEnv["discuss.channel.rtc.session"].create([
{
channel_member_id: pyEnv["discuss.channel.member"].create({
channel_id: channelId,
partner_id: pyEnv["res.partner"].create({ name: "CCC" }),
}),
channel_id: channelId,
},
{
channel_member_id: pyEnv["discuss.channel.member"].create({
channel_id: channelId,
partner_id: pyEnv["res.partner"].create({ name: "AAA" }),
}),
channel_id: channelId,
},
{
channel_member_id: pyEnv["discuss.channel.member"].create({
channel_id: channelId,
partner_id: pyEnv["res.partner"].create({ name: "BBB" }),
}),
channel_id: channelId,
},
]);
await start();
await openDiscuss(channelId);
await contains(".o-mail-DiscussSidebarCallParticipants", {
contains: [
".o-mail-DiscussSidebarCallParticipants-participant:nth-child(1):contains('AAA')",
],
});
await contains(" .o-mail-DiscussSidebarCallParticipants", {
contains: [
".o-mail-DiscussSidebarCallParticipants-participant:nth-child(2):contains('BBB')",
],
});
await contains(" .o-mail-DiscussSidebarCallParticipants", {
contains: [
".o-mail-DiscussSidebarCallParticipants-participant:nth-child(3):contains('CCC')",
],
});
});