bus/static/tests/bus_tests.js

394 lines
16 KiB
JavaScript
Raw Normal View History

2024-05-03 15:03:07 +03:00
/** @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"]);
});