394 lines
16 KiB
JavaScript
394 lines
16 KiB
JavaScript
|
/** @odoo-module **/
|
||
|
|
||
|
import { addBusServicesToRegistry } from "@bus/../tests/helpers/test_utils";
|
||
|
import { startServer } from "@bus/../tests/helpers/mock_python_environment";
|
||
|
import { patchWebsocketWorkerWithCleanup } from "@bus/../tests/helpers/mock_websocket";
|
||
|
import {
|
||
|
waitForBusEvent,
|
||
|
waitForChannels,
|
||
|
waitNotifications,
|
||
|
waitUntilSubscribe,
|
||
|
waitForWorkerEvent,
|
||
|
} from "@bus/../tests/helpers/websocket_event_deferred";
|
||
|
import { busParametersService } from "@bus/bus_parameters_service";
|
||
|
import { WEBSOCKET_CLOSE_CODES } from "@bus/workers/websocket_worker";
|
||
|
|
||
|
import { makeTestEnv } from "@web/../tests/helpers/mock_env";
|
||
|
import { makeDeferred, patchWithCleanup } from "@web/../tests/helpers/utils";
|
||
|
import { browser } from "@web/core/browser/browser";
|
||
|
import { session } from "@web/session";
|
||
|
|
||
|
QUnit.module("Bus");
|
||
|
|
||
|
QUnit.test("notifications received from the channel", async () => {
|
||
|
addBusServicesToRegistry();
|
||
|
const pyEnv = await startServer();
|
||
|
const env = await makeTestEnv({ activateMockServer: true });
|
||
|
env.services["bus_service"].addChannel("lambda");
|
||
|
await waitUntilSubscribe("lambda");
|
||
|
pyEnv["bus.bus"]._sendone("lambda", "notifType", "beta");
|
||
|
pyEnv["bus.bus"]._sendone("lambda", "notifType", "epsilon");
|
||
|
await waitNotifications([env, "notifType", "beta"], [env, "notifType", "epsilon"]);
|
||
|
});
|
||
|
|
||
|
QUnit.test("notifications not received after stoping the service", async () => {
|
||
|
addBusServicesToRegistry();
|
||
|
const pyEnv = await startServer();
|
||
|
const firstTabEnv = await makeTestEnv({ activateMockServer: true });
|
||
|
const secondTabEnv = await makeTestEnv({ activateMockServer: true });
|
||
|
firstTabEnv.services["bus_service"].start();
|
||
|
secondTabEnv.services["bus_service"].start();
|
||
|
firstTabEnv.services["bus_service"].addChannel("lambda");
|
||
|
await waitUntilSubscribe("lambda");
|
||
|
// both tabs should receive the notification
|
||
|
pyEnv["bus.bus"]._sendone("lambda", "notifType", "beta");
|
||
|
await waitNotifications(
|
||
|
[firstTabEnv, "notifType", "beta"],
|
||
|
[secondTabEnv, "notifType", "beta"]
|
||
|
);
|
||
|
secondTabEnv.services["bus_service"].stop();
|
||
|
await waitForWorkerEvent("leave");
|
||
|
pyEnv["bus.bus"]._sendone("lambda", "notifType", "epsilon");
|
||
|
await waitNotifications(
|
||
|
[firstTabEnv, "notifType", "epsilon"],
|
||
|
[secondTabEnv, "notifType", "epsilon", { received: false }]
|
||
|
);
|
||
|
});
|
||
|
|
||
|
QUnit.test("notifications still received after disconnect/reconnect", async () => {
|
||
|
addBusServicesToRegistry();
|
||
|
const pyEnv = await startServer();
|
||
|
const env = await makeTestEnv({ activateMockServer: true });
|
||
|
env.services["bus_service"].addChannel("lambda");
|
||
|
await Promise.all([waitForBusEvent(env, "connect"), waitForChannels(["lambda"])]);
|
||
|
pyEnv["bus.bus"]._sendone("lambda", "notifType", "beta");
|
||
|
await waitNotifications([env, "notifType", "beta"]);
|
||
|
pyEnv.simulateConnectionLost(WEBSOCKET_CLOSE_CODES.ABNORMAL_CLOSURE);
|
||
|
await waitForBusEvent(env, "reconnect");
|
||
|
pyEnv["bus.bus"]._sendone("lambda", "notifType", "gamma");
|
||
|
await waitNotifications([env, "notifType", "gamma"]);
|
||
|
});
|
||
|
|
||
|
QUnit.test("tabs share message from a channel", async () => {
|
||
|
addBusServicesToRegistry();
|
||
|
const pyEnv = await startServer();
|
||
|
const mainEnv = await makeTestEnv({ activateMockServer: true });
|
||
|
mainEnv.services["bus_service"].addChannel("lambda");
|
||
|
const slaveEnv = await makeTestEnv();
|
||
|
slaveEnv.services["bus_service"].addChannel("lambda");
|
||
|
await waitUntilSubscribe("lambda");
|
||
|
pyEnv["bus.bus"]._sendone("lambda", "notifType", "beta");
|
||
|
await waitNotifications([mainEnv, "notifType", "beta"], [slaveEnv, "notifType", "beta"]);
|
||
|
});
|
||
|
|
||
|
QUnit.test("second tab still receives notifications after main pagehide", async () => {
|
||
|
addBusServicesToRegistry();
|
||
|
const pyEnv = await startServer();
|
||
|
const mainEnv = await makeTestEnv({ activateMockServer: true });
|
||
|
mainEnv.services["bus_service"].addChannel("lambda");
|
||
|
// prevent second tab from receiving pagehide event.
|
||
|
patchWithCleanup(browser, {
|
||
|
addEventListener(eventName, callback) {
|
||
|
if (eventName === "pagehide") {
|
||
|
return;
|
||
|
}
|
||
|
super.addEventListener(eventName, callback);
|
||
|
},
|
||
|
});
|
||
|
const secondEnv = await makeTestEnv({ activateMockServer: true });
|
||
|
secondEnv.services["bus_service"].addChannel("lambda");
|
||
|
await waitUntilSubscribe("lambda");
|
||
|
pyEnv["bus.bus"]._sendone("lambda", "notifType", "beta");
|
||
|
await waitNotifications([mainEnv, "notifType", "beta"], [secondEnv, "notifType", "beta"]);
|
||
|
// simulate unloading main
|
||
|
window.dispatchEvent(new Event("pagehide"));
|
||
|
await waitForWorkerEvent("leave");
|
||
|
pyEnv["bus.bus"]._sendone("lambda", "notifType", "gamma");
|
||
|
await waitNotifications(
|
||
|
[mainEnv, "notifType", "gamma", { received: false }],
|
||
|
[secondEnv, "notifType", "gamma"]
|
||
|
);
|
||
|
});
|
||
|
|
||
|
QUnit.test("two tabs adding a different channel", async () => {
|
||
|
addBusServicesToRegistry();
|
||
|
const pyEnv = await startServer();
|
||
|
const firstTabEnv = await makeTestEnv({ activateMockServer: true });
|
||
|
const secondTabEnv = await makeTestEnv({ activateMockServer: true });
|
||
|
firstTabEnv.services["bus_service"].addChannel("alpha");
|
||
|
secondTabEnv.services["bus_service"].addChannel("beta");
|
||
|
await waitUntilSubscribe("alpha", "beta");
|
||
|
pyEnv["bus.bus"]._sendmany([
|
||
|
["alpha", "notifType", "alpha"],
|
||
|
["beta", "notifType", "beta"],
|
||
|
]);
|
||
|
await waitNotifications(
|
||
|
[firstTabEnv, "notifType", "alpha"],
|
||
|
[secondTabEnv, "notifType", "alpha"],
|
||
|
[firstTabEnv, "notifType", "beta"],
|
||
|
[secondTabEnv, "notifType", "beta"]
|
||
|
);
|
||
|
});
|
||
|
|
||
|
QUnit.test("channel management from multiple tabs", async (assert) => {
|
||
|
await startServer();
|
||
|
addBusServicesToRegistry();
|
||
|
patchWebsocketWorkerWithCleanup({
|
||
|
_sendToServer({ event_name, data }) {
|
||
|
assert.step(`${event_name} - [${data.channels.toString()}]`);
|
||
|
super._sendToServer(...arguments);
|
||
|
},
|
||
|
});
|
||
|
const firstTabEnv = await makeTestEnv({ activateMockServer: true });
|
||
|
const secTabEnv = await makeTestEnv({ activateMockServer: true });
|
||
|
firstTabEnv.services["bus_service"].addChannel("channel1");
|
||
|
await waitForChannels(["channel1"]);
|
||
|
// this should not trigger a subscription since the channel1 was
|
||
|
// aleady known.
|
||
|
secTabEnv.services["bus_service"].addChannel("channel1");
|
||
|
await waitForChannels(["channel1"]);
|
||
|
// removing channel1 from first tab should not trigger
|
||
|
// re-subscription since the second tab still listens to this
|
||
|
// channel.
|
||
|
firstTabEnv.services["bus_service"].deleteChannel("channel1");
|
||
|
await waitForChannels(["channel1"], { operation: "delete" });
|
||
|
// this should trigger a subscription since the channel2 was not
|
||
|
// known.
|
||
|
secTabEnv.services["bus_service"].addChannel("channel2");
|
||
|
await waitUntilSubscribe("channel2");
|
||
|
assert.verifySteps(["subscribe - [channel1]", "subscribe - [channel1,channel2]"]);
|
||
|
});
|
||
|
|
||
|
QUnit.test("channels subscription after disconnection", async (assert) => {
|
||
|
await startServer();
|
||
|
addBusServicesToRegistry();
|
||
|
const worker = patchWebsocketWorkerWithCleanup();
|
||
|
const env = await makeTestEnv({ activateMockServer: true });
|
||
|
env.services["bus_service"].start();
|
||
|
await waitUntilSubscribe();
|
||
|
worker.websocket.close(WEBSOCKET_CLOSE_CODES.KEEP_ALIVE_TIMEOUT);
|
||
|
await Promise.all([waitForBusEvent(env, "reconnect"), waitUntilSubscribe()]);
|
||
|
assert.ok(
|
||
|
true,
|
||
|
"No error means waitUntilSubscribe resolves twice thus two subscriptions were triggered as expected"
|
||
|
);
|
||
|
});
|
||
|
|
||
|
QUnit.test("Last notification id is passed to the worker on service start", async (assert) => {
|
||
|
addBusServicesToRegistry();
|
||
|
const pyEnv = await startServer();
|
||
|
let updateLastNotificationDeferred = makeDeferred();
|
||
|
patchWebsocketWorkerWithCleanup({
|
||
|
_onClientMessage(_, { action, data }) {
|
||
|
if (action === "initialize_connection") {
|
||
|
assert.step(`${action} - ${data["lastNotificationId"]}`);
|
||
|
updateLastNotificationDeferred.resolve();
|
||
|
}
|
||
|
return super._onClientMessage(...arguments);
|
||
|
},
|
||
|
});
|
||
|
const env1 = await makeTestEnv();
|
||
|
env1.services["bus_service"].start();
|
||
|
env1.services["bus_service"].addChannel("lambda");
|
||
|
await waitUntilSubscribe("lambda");
|
||
|
await updateLastNotificationDeferred;
|
||
|
// First bus service has never received notifications thus the
|
||
|
// default is 0.
|
||
|
assert.verifySteps(["initialize_connection - 0"]);
|
||
|
pyEnv["bus.bus"]._sendmany([
|
||
|
["lambda", "notifType", "beta"],
|
||
|
["lambda", "notifType", "beta"],
|
||
|
]);
|
||
|
await waitForBusEvent(env1, "notification");
|
||
|
updateLastNotificationDeferred = makeDeferred();
|
||
|
const env2 = await makeTestEnv();
|
||
|
await env2.services["bus_service"].start();
|
||
|
await updateLastNotificationDeferred;
|
||
|
// Second bus service sends the last known notification id.
|
||
|
assert.verifySteps([`initialize_connection - 2`]);
|
||
|
});
|
||
|
|
||
|
QUnit.test("Websocket disconnects upon user log out", async () => {
|
||
|
addBusServicesToRegistry();
|
||
|
patchWebsocketWorkerWithCleanup();
|
||
|
// first tab connects to the worker with user logged.
|
||
|
patchWithCleanup(session, { user_id: 1 });
|
||
|
const firstTabEnv = await makeTestEnv();
|
||
|
firstTabEnv.services["bus_service"].start();
|
||
|
await waitForBusEvent(firstTabEnv, "connect");
|
||
|
// second tab connects to the worker after disconnection: user_id
|
||
|
// is now false.
|
||
|
patchWithCleanup(session, { user_id: false });
|
||
|
const env2 = await makeTestEnv();
|
||
|
env2.services["bus_service"].start();
|
||
|
await waitForBusEvent(firstTabEnv, "disconnect");
|
||
|
});
|
||
|
|
||
|
QUnit.test("Websocket reconnects upon user log in", async () => {
|
||
|
addBusServicesToRegistry();
|
||
|
patchWebsocketWorkerWithCleanup();
|
||
|
// first tab connects to the worker with no user logged.
|
||
|
patchWithCleanup(session, { user_id: false });
|
||
|
const firstTabEnv = await makeTestEnv();
|
||
|
firstTabEnv.services["bus_service"].start();
|
||
|
await waitForBusEvent(firstTabEnv, "connect");
|
||
|
// second tab connects to the worker after connection: user_id
|
||
|
// is now set.
|
||
|
patchWithCleanup(session, { user_id: 1 });
|
||
|
const secondTabEnv = await makeTestEnv();
|
||
|
secondTabEnv.services["bus_service"].start();
|
||
|
await Promise.all([
|
||
|
waitForBusEvent(firstTabEnv, "disconnect"),
|
||
|
waitForBusEvent(firstTabEnv, "connect"),
|
||
|
]);
|
||
|
});
|
||
|
|
||
|
QUnit.test("WebSocket connects with URL corresponding to given serverURL", async (assert) => {
|
||
|
addBusServicesToRegistry();
|
||
|
patchWebsocketWorkerWithCleanup();
|
||
|
const serverURL = "http://random-website.com";
|
||
|
patchWithCleanup(busParametersService, {
|
||
|
start() {
|
||
|
return {
|
||
|
...super.start(...arguments),
|
||
|
serverURL,
|
||
|
};
|
||
|
},
|
||
|
});
|
||
|
const websocketCreatedDeferred = makeDeferred();
|
||
|
patchWithCleanup(window, {
|
||
|
WebSocket: function (url) {
|
||
|
assert.step(url);
|
||
|
websocketCreatedDeferred.resolve();
|
||
|
return new EventTarget();
|
||
|
},
|
||
|
});
|
||
|
const env = await makeTestEnv();
|
||
|
env.services["bus_service"].start();
|
||
|
await websocketCreatedDeferred;
|
||
|
assert.verifySteps([`${serverURL.replace("http", "ws")}/websocket`]);
|
||
|
});
|
||
|
|
||
|
QUnit.test("Disconnect on offline, re-connect on online", async () => {
|
||
|
addBusServicesToRegistry();
|
||
|
patchWebsocketWorkerWithCleanup();
|
||
|
const env = await makeTestEnv();
|
||
|
await env.services["bus_service"].start();
|
||
|
await waitForBusEvent(env, "connect");
|
||
|
window.dispatchEvent(new Event("offline"));
|
||
|
await waitForBusEvent(env, "disconnect");
|
||
|
window.dispatchEvent(new Event("online"));
|
||
|
await waitForBusEvent(env, "connect");
|
||
|
});
|
||
|
|
||
|
QUnit.test("No disconnect on change offline/online when bus inactive", async () => {
|
||
|
addBusServicesToRegistry();
|
||
|
patchWebsocketWorkerWithCleanup();
|
||
|
const env = await makeTestEnv();
|
||
|
window.dispatchEvent(new Event("offline"));
|
||
|
await waitForBusEvent(env, "disconnect", { received: false });
|
||
|
window.dispatchEvent(new Event("online"));
|
||
|
await waitForBusEvent(env, "connect", { received: false });
|
||
|
});
|
||
|
|
||
|
QUnit.test("Can reconnect after late close event", async (assert) => {
|
||
|
addBusServicesToRegistry();
|
||
|
let subscribeSent = 0;
|
||
|
const closeDeferred = makeDeferred();
|
||
|
const worker = patchWebsocketWorkerWithCleanup({
|
||
|
_sendToServer({ event_name }) {
|
||
|
if (event_name === "subscribe") {
|
||
|
subscribeSent++;
|
||
|
}
|
||
|
},
|
||
|
});
|
||
|
const pyEnv = await startServer();
|
||
|
const env = await makeTestEnv();
|
||
|
env.services["bus_service"].start();
|
||
|
await waitForBusEvent(env, "connect");
|
||
|
patchWithCleanup(worker.websocket, {
|
||
|
close(code = WEBSOCKET_CLOSE_CODES.CLEAN, reason) {
|
||
|
this.readyState = 2;
|
||
|
if (code === WEBSOCKET_CLOSE_CODES.CLEAN) {
|
||
|
closeDeferred.then(() => {
|
||
|
// Simulate that the connection could not be closed cleanly.
|
||
|
super.close(WEBSOCKET_CLOSE_CODES.ABNORMAL_CLOSURE, reason);
|
||
|
});
|
||
|
} else {
|
||
|
super.close(code, reason);
|
||
|
}
|
||
|
},
|
||
|
});
|
||
|
// Connection will be closed when passing offline. But the close event
|
||
|
// will be delayed to come after the next open event. The connection
|
||
|
// will thus be in the closing state in the meantime.
|
||
|
window.dispatchEvent(new Event("offline"));
|
||
|
// Worker reconnects upon the reception of the online event.
|
||
|
window.dispatchEvent(new Event("online"));
|
||
|
await waitForBusEvent(env, "disconnect");
|
||
|
await waitForBusEvent(env, "connect");
|
||
|
// Trigger the close event, it shouldn't have any effect since it is
|
||
|
// related to an old connection that is no longer in use.
|
||
|
closeDeferred.resolve();
|
||
|
await waitForBusEvent(env, "disconnect", { received: false });
|
||
|
// Server closes the connection, the worker should reconnect.
|
||
|
pyEnv.simulateConnectionLost(WEBSOCKET_CLOSE_CODES.KEEP_ALIVE_TIMEOUT);
|
||
|
await waitForBusEvent(env, "reconnecting");
|
||
|
await waitForBusEvent(env, "reconnect");
|
||
|
// 3 connections were opened, so 3 subscriptions are expected.
|
||
|
assert.strictEqual(subscribeSent, 3);
|
||
|
});
|
||
|
|
||
|
QUnit.test("Fallback on simple worker when shared worker failed to initialize", async (assert) => {
|
||
|
addBusServicesToRegistry();
|
||
|
patchWebsocketWorkerWithCleanup();
|
||
|
const originalSharedWorker = browser.SharedWorker;
|
||
|
const originalWorker = browser.Worker;
|
||
|
patchWithCleanup(browser, {
|
||
|
SharedWorker: function (url, options) {
|
||
|
assert.step("shared-worker creation");
|
||
|
const sw = new originalSharedWorker(url, options);
|
||
|
// Simulate error during shared worker creation.
|
||
|
setTimeout(() => sw.dispatchEvent(new Event("error")));
|
||
|
return sw;
|
||
|
},
|
||
|
Worker: function (url, options) {
|
||
|
assert.step("worker creation");
|
||
|
return new originalWorker(url, options);
|
||
|
},
|
||
|
});
|
||
|
patchWithCleanup(window.console, {
|
||
|
warn(message) {
|
||
|
assert.step(message);
|
||
|
},
|
||
|
});
|
||
|
const env = await makeTestEnv();
|
||
|
env.services["bus_service"].start();
|
||
|
await waitForBusEvent(env, "connect");
|
||
|
assert.verifySteps([
|
||
|
"shared-worker creation",
|
||
|
'Error while loading "bus_service" SharedWorker, fallback on Worker.',
|
||
|
"worker creation",
|
||
|
]);
|
||
|
});
|
||
|
|
||
|
QUnit.test("subscribe to single notification", async (assert) => {
|
||
|
addBusServicesToRegistry();
|
||
|
const pyEnv = await startServer();
|
||
|
const env = await makeTestEnv({ activateMockServer: true });
|
||
|
env.services["bus_service"].start();
|
||
|
const messageReceivedDeferred = makeDeferred();
|
||
|
env.services["bus_service"].subscribe("message", (payload) => {
|
||
|
assert.deepEqual({ body: "hello", id: 1 }, payload);
|
||
|
assert.step("message");
|
||
|
messageReceivedDeferred.resolve();
|
||
|
});
|
||
|
await waitUntilSubscribe();
|
||
|
pyEnv["bus.bus"]._sendone(pyEnv.currentPartner, "message", {
|
||
|
body: "hello",
|
||
|
id: 1,
|
||
|
});
|
||
|
await messageReceivedDeferred;
|
||
|
assert.verifySteps(["message"]);
|
||
|
});
|