mirror of
https://github.com/bringout/oca-ocb-core.git
synced 2026-04-22 15:12:01 +02:00
19.0 vanilla
This commit is contained in:
parent
d1963a3c3a
commit
2d3ee4855a
7430 changed files with 2687981 additions and 2965473 deletions
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,81 @@
|
|||
import {
|
||||
click,
|
||||
contains,
|
||||
start,
|
||||
startServer,
|
||||
openDiscuss,
|
||||
mockGetMedia,
|
||||
onlineTest,
|
||||
defineMailModels,
|
||||
} from "@mail/../tests/mail_test_helpers";
|
||||
import { onRpc } from "@web/../tests/web_test_helpers";
|
||||
import { PeerToPeer, UPDATE_EVENT } from "@mail/discuss/call/common/peer_to_peer";
|
||||
|
||||
defineMailModels();
|
||||
|
||||
function connectionReady(p2p) {
|
||||
return new Promise((resolve) => {
|
||||
p2p.addEventListener("update", ({ detail }) => {
|
||||
if (
|
||||
detail.name === UPDATE_EVENT.CONNECTION_CHANGE &&
|
||||
detail.payload.state === "connected"
|
||||
) {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function mockPeerToPeerCallEnvironment({ channelId, remoteSessionId }) {
|
||||
const env = await start();
|
||||
const rtc = env.services["discuss.rtc"];
|
||||
const localUserP2P = env.services["discuss.p2p"];
|
||||
const remoteUserP2P = new PeerToPeer({
|
||||
notificationRoute: "/mail/rtc/session/notify_call_members",
|
||||
});
|
||||
remoteUserP2P.connect(remoteSessionId, channelId);
|
||||
|
||||
onRpc("/mail/rtc/session/notify_call_members", async (req) => {
|
||||
const {
|
||||
params: { peer_notifications },
|
||||
} = await req.json();
|
||||
for (const [sender, , message] of peer_notifications) {
|
||||
/**
|
||||
* This is a simplification, if more than 2 users we should check notification.target to know which user
|
||||
* should get the notification.
|
||||
*/
|
||||
if (sender === rtc.selfSession.id) {
|
||||
await remoteUserP2P.handleNotification(sender, message);
|
||||
} else {
|
||||
await localUserP2P.handleNotification(sender, message);
|
||||
}
|
||||
}
|
||||
});
|
||||
const localUserConnected = connectionReady(localUserP2P);
|
||||
const remoteUserConnected = connectionReady(remoteUserP2P);
|
||||
return { localUserConnected, remoteUserConnected };
|
||||
}
|
||||
|
||||
onlineTest("Can join a call in p2p", async (assert) => {
|
||||
mockGetMedia();
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "General" });
|
||||
const remoteSessionId = pyEnv["discuss.channel.rtc.session"].create({
|
||||
channel_member_id: pyEnv["discuss.channel.member"].create({
|
||||
channel_id: channelId,
|
||||
partner_id: pyEnv["res.partner"].create({ name: "Remote" }),
|
||||
}),
|
||||
channel_id: channelId,
|
||||
});
|
||||
const { localUserConnected, remoteUserConnected } = await mockPeerToPeerCallEnvironment({
|
||||
channelId,
|
||||
remoteSessionId,
|
||||
});
|
||||
|
||||
await openDiscuss(channelId);
|
||||
await click("[title='Join Call']");
|
||||
await contains(".o-discuss-Call");
|
||||
await contains(".o-discuss-CallParticipantCard[title='Remote']");
|
||||
await Promise.all([localUserConnected, remoteUserConnected]);
|
||||
await contains("span[data-connection-state='connected']");
|
||||
});
|
||||
|
|
@ -0,0 +1,103 @@
|
|||
import {
|
||||
click,
|
||||
contains,
|
||||
defineMailModels,
|
||||
mockGetMedia,
|
||||
mockPermissionsPrompt,
|
||||
openDiscuss,
|
||||
start,
|
||||
startServer,
|
||||
} from "@mail/../tests/mail_test_helpers";
|
||||
|
||||
import { describe, test } from "@odoo/hoot";
|
||||
|
||||
describe.current.tags("desktop");
|
||||
defineMailModels();
|
||||
|
||||
test("Starting a video call asks for permissions", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "General" });
|
||||
mockGetMedia();
|
||||
mockPermissionsPrompt();
|
||||
const env = await start();
|
||||
const rtc = env.services["discuss.rtc"];
|
||||
await openDiscuss(channelId);
|
||||
await click("[title='Start Video Call']");
|
||||
await contains(".modal[role='dialog']", { count: 1 });
|
||||
rtc.cameraPermission = "granted";
|
||||
await click(".modal-footer button", { text: "Use Camera" });
|
||||
await contains(".o-discuss-CallActionList button[title='Stop camera']");
|
||||
});
|
||||
|
||||
test("Turning on the microphone asks for permissions", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "General" });
|
||||
mockGetMedia();
|
||||
mockPermissionsPrompt();
|
||||
const env = await start();
|
||||
const rtc = env.services["discuss.rtc"];
|
||||
await openDiscuss(channelId);
|
||||
await click("[title='Start Call']");
|
||||
await contains(".o-discuss-CallActionList button[title='Turn camera on']");
|
||||
await click(".o-discuss-CallActionList button[title='Unmute']");
|
||||
await contains(".modal[role='dialog']", { count: 1 });
|
||||
rtc.microphonePermission = "granted";
|
||||
await click(".modal-footer button", { text: "Use Microphone" });
|
||||
await contains(".o-discuss-CallActionList button[title='Mute']");
|
||||
await contains(".o-discuss-CallActionList button[title='Turn camera on']");
|
||||
});
|
||||
|
||||
test("Turning on the camera asks for permissions", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "General" });
|
||||
mockGetMedia();
|
||||
mockPermissionsPrompt();
|
||||
const env = await start();
|
||||
const rtc = env.services["discuss.rtc"];
|
||||
await openDiscuss(channelId);
|
||||
await click("[title='Start Call']");
|
||||
await click(".o-discuss-CallActionList button[title='Turn camera on']");
|
||||
await contains(".modal[role='dialog']", { count: 1 });
|
||||
rtc.cameraPermission = "granted";
|
||||
await click(".modal-footer button", { text: "Use Camera" });
|
||||
await contains(".o-discuss-CallActionList button[title='Stop camera']");
|
||||
});
|
||||
|
||||
test("Turn on both microphone and camera from permission dialog", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "General" });
|
||||
mockGetMedia();
|
||||
mockPermissionsPrompt();
|
||||
const env = await start();
|
||||
const rtc = env.services["discuss.rtc"];
|
||||
await openDiscuss(channelId);
|
||||
await click("[title='Start Call']");
|
||||
await contains(".o-discuss-CallActionList button[title='Turn camera on']");
|
||||
await click(".o-discuss-CallActionList button[title='Turn camera on']");
|
||||
await contains(".modal[role='dialog']", { count: 1 });
|
||||
rtc.microphonePermission = "granted";
|
||||
rtc.cameraPermission = "granted";
|
||||
await click(".modal-footer button", { text: "Use microphone and camera" });
|
||||
await contains(".o-discuss-CallActionList button[title='Stop camera']");
|
||||
await contains(".o-discuss-CallActionList button[title='Mute']");
|
||||
});
|
||||
|
||||
test("Combined mic+camera button only shown when both permissions not granted", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "General" });
|
||||
mockGetMedia();
|
||||
mockPermissionsPrompt();
|
||||
const env = await start();
|
||||
const rtc = env.services["discuss.rtc"];
|
||||
await openDiscuss(channelId);
|
||||
await click("[title='Start Call']");
|
||||
await click(".o-discuss-CallActionList button[title='Turn camera on']");
|
||||
await contains(".modal-footer button", { count: 2 });
|
||||
await contains(".modal-footer button", { text: "Use microphone and camera" });
|
||||
await contains(".modal-footer button", { text: "Use Camera" });
|
||||
rtc.cameraPermission = "granted";
|
||||
await click(".modal-footer button", { text: "Use Camera" });
|
||||
await click(".o-discuss-CallActionList button[title='Unmute']");
|
||||
await contains(".modal-footer button");
|
||||
await contains(".modal-footer button", { text: "Use Microphone" });
|
||||
});
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
import {
|
||||
click,
|
||||
contains,
|
||||
defineMailModels,
|
||||
openDiscuss,
|
||||
start,
|
||||
startServer,
|
||||
} from "@mail/../tests/mail_test_helpers";
|
||||
import { describe, test } from "@odoo/hoot";
|
||||
|
||||
describe.current.tags("desktop");
|
||||
defineMailModels();
|
||||
|
||||
test("Call has Picture-in-picture feature", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "General" });
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await click("[title='Start Call']");
|
||||
await contains(".o-discuss-Call");
|
||||
await contains(".o-discuss-Call-layoutActions button[title='Picture in Picture']");
|
||||
});
|
||||
|
|
@ -0,0 +1,131 @@
|
|||
import {
|
||||
click,
|
||||
contains,
|
||||
defineMailModels,
|
||||
editInput,
|
||||
openDiscuss,
|
||||
patchUiSize,
|
||||
SIZES,
|
||||
start,
|
||||
startServer,
|
||||
} from "@mail/../tests/mail_test_helpers";
|
||||
import { describe, test, expect } from "@odoo/hoot";
|
||||
import { advanceTime } from "@odoo/hoot-mock";
|
||||
import { asyncStep, patchWithCleanup, waitForSteps } from "@web/../tests/web_test_helpers";
|
||||
|
||||
import { browser } from "@web/core/browser/browser";
|
||||
|
||||
describe.current.tags("desktop");
|
||||
defineMailModels();
|
||||
|
||||
test("Renders the call settings", async () => {
|
||||
patchWithCleanup(browser.navigator.mediaDevices, {
|
||||
enumerateDevices: () =>
|
||||
Promise.resolve([
|
||||
{
|
||||
deviceId: "mockAudioDeviceId",
|
||||
kind: "audioinput",
|
||||
label: "mockAudioDeviceLabel",
|
||||
},
|
||||
{
|
||||
deviceId: "mockVideoDeviceId",
|
||||
kind: "videoinput",
|
||||
label: "mockVideoDeviceLabel",
|
||||
},
|
||||
]),
|
||||
});
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "test" });
|
||||
patchUiSize({ size: SIZES.SM });
|
||||
const env = await start();
|
||||
const rtc = env.services["discuss.rtc"];
|
||||
await openDiscuss(channelId);
|
||||
// dropdown requires an extra delay before click (because handler is registered in useEffect)
|
||||
await contains("[title='Open Actions Menu']");
|
||||
await click("[title='Open Actions Menu']");
|
||||
await click(".o-dropdown-item", { text: "Call Settings" });
|
||||
await contains(".o-discuss-CallSettings");
|
||||
await contains("label[aria-label='Camera']");
|
||||
await contains("label[aria-label='Microphone']");
|
||||
await contains("label[aria-label='Audio Output']");
|
||||
await contains("option", { textContent: "Permission Needed", count: 3 });
|
||||
rtc.microphonePermission = "granted";
|
||||
await contains("option[value=mockAudioDeviceId]");
|
||||
rtc.cameraPermission = "granted";
|
||||
await contains("option[value=mockVideoDeviceId]");
|
||||
await contains("button", { text: "Voice Detection" });
|
||||
await contains("button", { text: "Push to Talk" });
|
||||
await contains("span", { text: "Voice detection sensitivity" });
|
||||
await contains("button", { text: "Test" });
|
||||
await contains("label", { text: "Show video participants only" });
|
||||
await contains("label", { text: "Blur video background" });
|
||||
});
|
||||
|
||||
test("activate push to talk", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "test" });
|
||||
patchUiSize({ size: SIZES.SM });
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
// dropdown requires an extra delay before click (because handler is registered in useEffect)
|
||||
await contains("[title='Open Actions Menu']");
|
||||
await click("[title='Open Actions Menu']");
|
||||
await click(".o-dropdown-item", { text: "Call Settings" });
|
||||
await click("button", { text: "Push to Talk" });
|
||||
await contains("i[aria-label='Register new key']");
|
||||
await contains("label", { text: "Delay after releasing push-to-talk" });
|
||||
await contains("label", { text: "Voice detection sensitivity", count: 0 });
|
||||
});
|
||||
|
||||
test("activate blur", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "test" });
|
||||
patchUiSize({ size: SIZES.SM });
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
// dropdown requires an extra delay before click (because handler is registered in useEffect)
|
||||
await contains("[title='Open Actions Menu']");
|
||||
await click("[title='Open Actions Menu']");
|
||||
await click(".o-dropdown-item", { text: "Call Settings" });
|
||||
await click("input[title='Blur video background']");
|
||||
await contains("label", { text: "Blur video background" });
|
||||
await contains("label", { text: "Edge blur intensity" });
|
||||
});
|
||||
|
||||
test("local storage for call settings", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "test" });
|
||||
localStorage.setItem("mail_user_setting_background_blur_amount", "3");
|
||||
localStorage.setItem("mail_user_setting_edge_blur_amount", "5");
|
||||
localStorage.setItem("mail_user_setting_show_only_video", "true");
|
||||
localStorage.setItem("mail_user_setting_use_blur", "true");
|
||||
patchWithCleanup(localStorage, {
|
||||
setItem(key, value) {
|
||||
if (key.startsWith("mail_user_setting")) {
|
||||
asyncStep(`${key}: ${value}`);
|
||||
}
|
||||
return super.setItem(key, value);
|
||||
},
|
||||
});
|
||||
patchUiSize({ size: SIZES.SM });
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
// testing load from local storage
|
||||
// dropdown requires an extra delay before click (because handler is registered in useEffect)
|
||||
await contains("[title='Open Actions Menu']");
|
||||
await click("[title='Open Actions Menu']");
|
||||
await click(".o-dropdown-item", { text: "Call Settings" });
|
||||
await contains("input[title='Show video participants only']:checked");
|
||||
await contains("input[title='Blur video background']:checked");
|
||||
await contains("label[title='Background blur intensity']", { text: "15%" });
|
||||
await contains("label[title='Edge blur intensity']", { text: "25%" });
|
||||
|
||||
// testing save to local storage
|
||||
await click("input[title='Show video participants only']");
|
||||
await waitForSteps(["mail_user_setting_show_only_video: false"]);
|
||||
await click("input[title='Blur video background']");
|
||||
expect(localStorage.getItem("mail_user_setting_use_blur")).toBe(null);
|
||||
await editInput(document.body, ".o-Discuss-CallSettings-thresholdInput", 0.3);
|
||||
await advanceTime(2000); // threshold setting debounce timer
|
||||
await waitForSteps(["mail_user_setting_voice_threshold: 0.3"]);
|
||||
});
|
||||
|
|
@ -0,0 +1,211 @@
|
|||
import { describe, expect } from "@odoo/hoot";
|
||||
import { advanceTime } from "@odoo/hoot-mock";
|
||||
import { browser } from "@web/core/browser/browser";
|
||||
import { asyncStep, onRpc, mountWebClient, waitForSteps } from "@web/../tests/web_test_helpers";
|
||||
import { defineMailModels, mockGetMedia, onlineTest } from "@mail/../tests/mail_test_helpers";
|
||||
import { PeerToPeer, STREAM_TYPE, UPDATE_EVENT } from "@mail/discuss/call/common/peer_to_peer";
|
||||
|
||||
describe.current.tags("desktop");
|
||||
defineMailModels();
|
||||
|
||||
class Network {
|
||||
_peerToPeerInstances = new Map();
|
||||
_notificationRoute;
|
||||
constructor(route) {
|
||||
this._notificationRoute = route || "/any/mock/notification";
|
||||
onRpc(this._notificationRoute, async (req) => {
|
||||
const {
|
||||
params: { peer_notifications },
|
||||
} = await req.json();
|
||||
for (const notification of peer_notifications) {
|
||||
const [sender_session_id, target_session_ids, content] = notification;
|
||||
for (const id of target_session_ids) {
|
||||
const p2p = this._peerToPeerInstances.get(id);
|
||||
p2p.handleNotification(sender_session_id, content);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
/**
|
||||
* @param id
|
||||
* @return {{id, p2p: PeerToPeer}}
|
||||
*/
|
||||
register(id) {
|
||||
const p2p = new PeerToPeer({ notificationRoute: this._notificationRoute });
|
||||
this._peerToPeerInstances.set(id, p2p);
|
||||
return { id, p2p };
|
||||
}
|
||||
close() {
|
||||
for (const p2p of this._peerToPeerInstances.values()) {
|
||||
p2p.disconnect();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onlineTest("basic peer to peer connection", async () => {
|
||||
await mountWebClient();
|
||||
const channelId = 1;
|
||||
const network = new Network();
|
||||
const user1 = network.register(1);
|
||||
const user2 = network.register(2);
|
||||
user2.p2p.addEventListener("update", ({ detail: { name, payload } }) => {
|
||||
if (name === UPDATE_EVENT.CONNECTION_CHANGE && payload.state === "connected") {
|
||||
asyncStep(payload.state);
|
||||
}
|
||||
});
|
||||
|
||||
user2.p2p.connect(user2.id, channelId);
|
||||
user1.p2p.connect(user1.id, channelId);
|
||||
await user1.p2p.addPeer(user2.id);
|
||||
await waitForSteps(["connected"]);
|
||||
network.close();
|
||||
});
|
||||
|
||||
onlineTest("mesh peer to peer connections", async () => {
|
||||
await mountWebClient();
|
||||
const channelId = 2;
|
||||
const network = new Network();
|
||||
const userCount = 10;
|
||||
const users = Array.from({ length: userCount }, (_, i) => network.register(i));
|
||||
const promises = [];
|
||||
for (const user of users) {
|
||||
user.p2p.connect(user.id, channelId);
|
||||
for (let i = 0; i < user.id; i++) {
|
||||
promises.push(user.p2p.addPeer(i));
|
||||
}
|
||||
}
|
||||
await Promise.all(promises);
|
||||
|
||||
let connectionsCount = 0;
|
||||
for (const user of users) {
|
||||
connectionsCount += user.p2p.peers.size;
|
||||
}
|
||||
expect(connectionsCount).toBe(userCount * (userCount - 1));
|
||||
connectionsCount = 0;
|
||||
network.close();
|
||||
for (const user of users) {
|
||||
connectionsCount += user.p2p.peers.size;
|
||||
}
|
||||
expect(connectionsCount).toBe(0);
|
||||
});
|
||||
|
||||
onlineTest("connection recovery", async () => {
|
||||
await mountWebClient();
|
||||
const channelId = 1;
|
||||
const network = new Network();
|
||||
const user1 = network.register(1);
|
||||
const user2 = network.register(2);
|
||||
user2.remoteStates = new Map();
|
||||
user2.p2p.addEventListener("update", ({ detail: { name, payload } }) => {
|
||||
if (name === UPDATE_EVENT.CONNECTION_CHANGE && payload.state === "connected") {
|
||||
asyncStep(payload.state);
|
||||
}
|
||||
});
|
||||
|
||||
user1.p2p.connect(user1.id, channelId);
|
||||
user1.p2p.addPeer(user2.id);
|
||||
// only connecting user2 after user1 has called addPeer so that user2 ignores notifications
|
||||
// from user1, which simulates a connection drop that should be recovered.
|
||||
user2.p2p.connect(user2.id, channelId);
|
||||
const openPromise = new Promise((resolve) => {
|
||||
user1.p2p.peers.get(2).dataChannel.onopen = resolve;
|
||||
});
|
||||
advanceTime(5_000); // recovery timeout
|
||||
await openPromise;
|
||||
await waitForSteps(["connected"]);
|
||||
network.close();
|
||||
});
|
||||
|
||||
onlineTest("can broadcast a stream and control download", async () => {
|
||||
mockGetMedia();
|
||||
await mountWebClient();
|
||||
const channelId = 3;
|
||||
const network = new Network();
|
||||
const user1 = network.register(1);
|
||||
const user2 = network.register(2);
|
||||
user2.remoteMedia = new Map();
|
||||
const trackPromise = new Promise((resolve) => {
|
||||
user2.p2p.addEventListener("update", ({ detail: { name, payload } }) => {
|
||||
if (name === UPDATE_EVENT.TRACK) {
|
||||
user2.remoteMedia.set(payload.sessionId, {
|
||||
[payload.type]: {
|
||||
track: payload.track,
|
||||
active: payload.active,
|
||||
},
|
||||
});
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
user2.p2p.connect(user2.id, channelId);
|
||||
user1.p2p.connect(user1.id, channelId);
|
||||
await user1.p2p.addPeer(user2.id);
|
||||
const videoStream = await browser.navigator.mediaDevices.getUserMedia({
|
||||
video: true,
|
||||
});
|
||||
const videoTrack = videoStream.getVideoTracks()[0];
|
||||
await user1.p2p.updateUpload(STREAM_TYPE.CAMERA, videoTrack);
|
||||
await trackPromise;
|
||||
const user2RemoteMedia = user2.remoteMedia.get(user1.id);
|
||||
const user2CameraTransceiver = user2.p2p.peers.get(user1.id).getTransceiver(STREAM_TYPE.CAMERA);
|
||||
expect(user2CameraTransceiver.direction).toBe("recvonly");
|
||||
expect(user2RemoteMedia[STREAM_TYPE.CAMERA].track.kind).toBe("video");
|
||||
expect(user2RemoteMedia[STREAM_TYPE.CAMERA].active).toBe(true);
|
||||
user2.p2p.updateDownload(user1.id, { camera: false });
|
||||
expect(user2CameraTransceiver.direction).toBe("inactive");
|
||||
network.close();
|
||||
});
|
||||
|
||||
onlineTest("can broadcast arbitrary messages (dataChannel)", async () => {
|
||||
await mountWebClient();
|
||||
const channelId = 4;
|
||||
const network = new Network();
|
||||
const user1 = network.register(1);
|
||||
const user2 = network.register(2);
|
||||
user2.p2p.connect(user2.id, channelId);
|
||||
user1.p2p.connect(user1.id, channelId);
|
||||
await user1.p2p.addPeer(user2.id);
|
||||
user1.inbox = [];
|
||||
const pongPromise = new Promise((resolve) => {
|
||||
user1.p2p.addEventListener("update", ({ detail: { name, payload } }) => {
|
||||
if (name === UPDATE_EVENT.BROADCAST) {
|
||||
user1.inbox.push(payload);
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
user2.inbox = [];
|
||||
user2.p2p.addEventListener("update", ({ detail: { name, payload } }) => {
|
||||
if (name === UPDATE_EVENT.BROADCAST && payload.message === "ping") {
|
||||
user2.inbox.push(payload);
|
||||
user2.p2p.broadcast("pong");
|
||||
}
|
||||
});
|
||||
user1.p2p.broadcast("ping");
|
||||
await pongPromise;
|
||||
expect(user2.inbox[0].senderId).toBe(user1.id);
|
||||
expect(user2.inbox[0].message).toBe("ping");
|
||||
expect(user1.inbox[0].senderId).toBe(user2.id);
|
||||
expect(user1.inbox[0].message).toBe("pong");
|
||||
network.close();
|
||||
});
|
||||
|
||||
onlineTest("can reject arbitrary offers", async () => {
|
||||
await mountWebClient();
|
||||
const channelId = 1;
|
||||
const network = new Network();
|
||||
const user1 = network.register(1);
|
||||
const user2 = network.register(2);
|
||||
user2.p2p.connect(user2.id, channelId);
|
||||
user1.p2p.connect(user1.id, channelId);
|
||||
user2.p2p._emitLog = (id, message) => {
|
||||
if (message === "offer rejected") {
|
||||
asyncStep("offer rejected");
|
||||
}
|
||||
};
|
||||
user2.p2p.acceptOffer = (id, sequence) => id !== user1.id || sequence > 20;
|
||||
user1.p2p.addPeer(user2.id, { sequence: 19 });
|
||||
await waitForSteps(["offer rejected"]);
|
||||
network.close();
|
||||
});
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
import {
|
||||
click,
|
||||
contains,
|
||||
defineMailModels,
|
||||
mockGetMedia,
|
||||
openDiscuss,
|
||||
patchUiSize,
|
||||
SIZES,
|
||||
start,
|
||||
startServer,
|
||||
} from "@mail/../tests/mail_test_helpers";
|
||||
import { pttExtensionServiceInternal } from "@mail/discuss/call/common/ptt_extension_service";
|
||||
import { describe, test } from "@odoo/hoot";
|
||||
import { patchWithCleanup } from "@web/../tests/web_test_helpers";
|
||||
|
||||
describe.current.tags("desktop");
|
||||
defineMailModels();
|
||||
|
||||
test("display banner when ptt extension is not enabled", async () => {
|
||||
mockGetMedia();
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "General" });
|
||||
patchWithCleanup(pttExtensionServiceInternal, {
|
||||
onAnswerIsEnabled(pttService) {
|
||||
pttService.isEnabled = false;
|
||||
},
|
||||
});
|
||||
patchUiSize({ size: SIZES.SM });
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
// dropdown requires an extra delay before click (because handler is registered in useEffect)
|
||||
await contains("[title='Open Actions Menu']");
|
||||
await click("[title='Open Actions Menu']");
|
||||
await click(".o-dropdown-item", { text: "Call Settings" });
|
||||
await click("button", { text: "Push to Talk" });
|
||||
await click("[title*='Close Chat Window']");
|
||||
await click("button[title='New Meeting']");
|
||||
await click("button[title='Close panel']"); // invitation panel automatically open
|
||||
await contains(".o-discuss-PttAdBanner");
|
||||
// dropdown requires an extra delay before click (because handler is registered in useEffect)
|
||||
await contains("[title='Open Actions Menu']");
|
||||
await click("[title='Open Actions Menu']");
|
||||
await click(".o-dropdown-item", { text: "Call Settings" });
|
||||
await click("button", { text: "Voice Detection" });
|
||||
await click("[title*='Close Chat Window']");
|
||||
await contains(".o-discuss-PttAdBanner", { count: 0 });
|
||||
});
|
||||
|
|
@ -0,0 +1,123 @@
|
|||
import {
|
||||
click,
|
||||
contains,
|
||||
defineMailModels,
|
||||
insertText,
|
||||
mockGetMedia,
|
||||
openDiscuss,
|
||||
patchUiSize,
|
||||
SIZES,
|
||||
start,
|
||||
startServer,
|
||||
} from "@mail/../tests/mail_test_helpers";
|
||||
import { pttExtensionServiceInternal } from "@mail/discuss/call/common/ptt_extension_service";
|
||||
import { PTT_RELEASE_DURATION } from "@mail/discuss/call/common/rtc_service";
|
||||
import { advanceTime, freezeTime, keyDown, mockTouch, mockUserAgent, test } from "@odoo/hoot";
|
||||
import { patchWithCleanup, serverState } from "@web/../tests/web_test_helpers";
|
||||
import { browser } from "@web/core/browser/browser";
|
||||
|
||||
defineMailModels();
|
||||
|
||||
test.tags("desktop");
|
||||
test("no auto-call on joining chat", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const partnerId = pyEnv["res.partner"].create({ name: "Mario" });
|
||||
pyEnv["res.users"].create({ partner_id: partnerId });
|
||||
await start();
|
||||
await openDiscuss();
|
||||
await click("input[placeholder='Search conversations']");
|
||||
await contains(".o_command_name", { count: 5 });
|
||||
await insertText("input[placeholder='Search a conversation']", "mario");
|
||||
await contains(".o_command_name", { count: 3 });
|
||||
await click(".o_command_name", { text: "Mario" });
|
||||
await contains(".o-mail-DiscussSidebar-item", { text: "Mario" });
|
||||
await contains(".o-mail-Message", { count: 0 });
|
||||
await contains(".o-discuss-Call", { count: 0 });
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("no auto-call on joining group chat", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const [partnerId_1, partnerId_2] = pyEnv["res.partner"].create([
|
||||
{ name: "Mario" },
|
||||
{ name: "Luigi" },
|
||||
]);
|
||||
pyEnv["res.users"].create([{ partner_id: partnerId_1 }, { partner_id: partnerId_2 }]);
|
||||
await start();
|
||||
await openDiscuss();
|
||||
await click("input[placeholder='Search conversations']");
|
||||
await click("a", { text: "Create Chat" });
|
||||
await click("li", { text: "Mario" });
|
||||
await click("li", { text: "Luigi" });
|
||||
await click("button", { text: "Create Group Chat" });
|
||||
await contains(".o-mail-DiscussSidebar-item:contains('Mario, and Luigi')");
|
||||
await contains(".o-mail-Message", { count: 0 });
|
||||
await contains(".o-discuss-Call", { count: 0 });
|
||||
});
|
||||
|
||||
test.tags("mobile");
|
||||
test("show Push-to-Talk button on mobile", async () => {
|
||||
mockGetMedia();
|
||||
mockTouch(true);
|
||||
mockUserAgent("android");
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "General" });
|
||||
patchWithCleanup(pttExtensionServiceInternal, {
|
||||
onAnswerIsEnabled(pttService) {
|
||||
pttService.isEnabled = false;
|
||||
},
|
||||
});
|
||||
patchUiSize({ size: SIZES.SM });
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await click(".o-mail-ChatWindow-moreActions", { text: "General" });
|
||||
await click(".o-dropdown-item:text('Start Call')");
|
||||
// dropdown requires an extra delay before click (because handler is registered in useEffect)
|
||||
await contains("[title='Open Actions Menu']");
|
||||
await click("[title='Open Actions Menu']");
|
||||
await click(".o-dropdown-item", { text: "Call Settings" });
|
||||
await click("button", { text: "Push to Talk" });
|
||||
// dropdown requires an extra delay before click (because handler is registered in useEffect)
|
||||
await contains("[title='Open Actions Menu']");
|
||||
await click("[title='Open Actions Menu']");
|
||||
await click(".o-dropdown-item", { text: "Call Settings" });
|
||||
await contains("button", { text: "Push to talk" });
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("Can push-to-talk", async () => {
|
||||
mockGetMedia();
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "General" });
|
||||
pyEnv["res.users.settings"].create({
|
||||
use_push_to_talk: true,
|
||||
user_id: serverState.userId,
|
||||
push_to_talk_key: "...f",
|
||||
});
|
||||
patchWithCleanup(pttExtensionServiceInternal, {
|
||||
onAnswerIsEnabled(pttService) {
|
||||
pttService.isEnabled = false;
|
||||
},
|
||||
});
|
||||
freezeTime();
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await advanceTime(1000);
|
||||
await click("[title='Start Call']");
|
||||
await advanceTime(1000);
|
||||
await contains(".o-discuss-Call");
|
||||
await click(".o-discuss-Call");
|
||||
await advanceTime(1000);
|
||||
await keyDown("f");
|
||||
await advanceTime(PTT_RELEASE_DURATION);
|
||||
await contains(".o-discuss-CallParticipantCard .o-isTalking");
|
||||
// switching tab while PTT key still pressed then released on other tab should eventually release PTT
|
||||
browser.dispatchEvent(new Event("blur"));
|
||||
await advanceTime(PTT_RELEASE_DURATION + 1000);
|
||||
await contains(".o-discuss-CallParticipantCard:not(:has(.o-isTalking))");
|
||||
await click(".o-discuss-Call");
|
||||
await advanceTime(1000);
|
||||
await keyDown("f");
|
||||
await advanceTime(PTT_RELEASE_DURATION);
|
||||
await contains(".o-discuss-CallParticipantCard .o-isTalking");
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue