Odoo18-Base/addons/mail/static/tests/discuss/call/peer_to_peer.test.js
2025-01-06 10:57:38 +07:00

196 lines
6.9 KiB
JavaScript

import { describe, expect, test } from "@odoo/hoot";
import { advanceTime } from "@odoo/hoot-mock";
import { browser } from "@web/core/browser/browser";
import { onRpc, mountWebClient } from "@web/../tests/web_test_helpers";
import { defineMailModels, mockGetMedia } 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();
}
}
}
test("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.remoteStates = new Map();
user2.p2p.addEventListener("update", ({ detail: { name, payload } }) => {
if (name === UPDATE_EVENT.CONNECTION_CHANGE) {
user2.remoteStates.set(payload.id, payload.state);
}
});
user2.p2p.connect(user2.id, channelId);
user1.p2p.connect(user1.id, channelId);
await user1.p2p.addPeer(user2.id);
expect(user2.remoteStates.get(user1.id)).toBe("connected");
network.close();
});
test("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);
});
test("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) {
user2.remoteStates.set(payload.id, 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);
expect(user2.remoteStates.get(user1.id)).toBe(undefined);
const openPromise = new Promise((resolve) => {
user1.p2p.peers.get(2).dataChannel.onopen = resolve;
});
advanceTime(5_000); // recovery timeout
await openPromise;
expect(user2.remoteStates.get(user1.id)).toBe("connected");
network.close();
});
test("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();
});
test("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);
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) {
user2.inbox.push(payload);
user2.p2p.broadcast("pong");
}
});
user2.p2p.connect(user2.id, channelId);
user1.p2p.connect(user1.id, channelId);
await user1.p2p.addPeer(user2.id);
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();
});