[IMP] bus: make bus tests more robust

This commit makes bus tests more robust by implementing
functions that wait for some events instead of relying
on `nextTick` that might or not be sufficient.

fixes runbot-23097

closes odoo/odoo#138221

Signed-off-by: Matthieu Stockbauer (tsm) <tsm@odoo.com>
This commit is contained in:
tsm-odoo
2023-10-12 09:49:46 +00:00
parent 621668e53b
commit 7bab1cfde1
3 changed files with 279 additions and 343 deletions
+142 -338
View File
@@ -3,7 +3,12 @@
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 { waitUntilSubscribe } from "@bus/../tests/helpers/websocket_event_deferred";
import {
waitForEvent,
waitForChannels,
waitNotifications,
waitUntilSubscribe,
} from "@bus/../tests/helpers/websocket_event_deferred";
import { busParametersService } from "@bus/bus_parameters_service";
import { WEBSOCKET_CLOSE_CODES } from "@bus/workers/websocket_worker";
@@ -14,147 +19,73 @@ import { session } from "@web/session";
QUnit.module("Bus");
QUnit.test("notifications received from the channel", async function (assert) {
QUnit.test("notifications received from the channel", async () => {
addBusServicesToRegistry();
const pyEnv = await startServer();
const env = await makeTestEnv({ activateMockServer: true });
await env.services["bus_service"].start();
env.services["bus_service"].addEventListener("notification", ({ detail: notifications }) => {
assert.step("notification - " + notifications.map((notif) => notif.payload).toString());
});
env.services["bus_service"].addChannel("lambda");
await waitUntilSubscribe("lambda");
await waitForChannels(["lambda"]);
pyEnv["bus.bus"]._sendone("lambda", "notifType", "beta");
await nextTick();
pyEnv["bus.bus"]._sendone("lambda", "notifType", "epsilon");
await nextTick();
assert.verifySteps(["notification - beta", "notification - epsilon"]);
await waitNotifications([env, "notifType", "beta"], [env, "notifType", "epsilon"]);
});
QUnit.test("notifications not received after stoping the service", async function (assert) {
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 });
await firstTabEnv.services["bus_service"].start();
await secondTabEnv.services["bus_service"].start();
firstTabEnv.services["bus_service"].addEventListener(
"notification",
({ detail: notifications }) => {
assert.step(
"1 - notification - " + notifications.map((notif) => notif.payload).toString()
);
}
);
firstTabEnv.services["bus_service"].start();
secondTabEnv.services["bus_service"].start();
firstTabEnv.services["bus_service"].addChannel("lambda");
secondTabEnv.services["bus_service"].addEventListener(
"notification",
({ detail: notifications }) => {
assert.step(
"2 - notification - " + notifications.map((notif) => notif.payload).toString()
);
}
);
await waitUntilSubscribe("lambda");
await waitForChannels(["lambda"]);
// both tabs should receive the notification
pyEnv["bus.bus"]._sendone("lambda", "notifType", "beta");
await nextTick();
await waitNotifications(
[firstTabEnv, "notifType", "beta"],
[secondTabEnv, "notifType", "beta"]
);
secondTabEnv.services["bus_service"].stop();
await nextTick();
// only first tab should receive the notification since the
// second tab has called the stop method.
pyEnv["bus.bus"]._sendone("lambda", "notifType", "epsilon");
await nextTick();
assert.verifySteps([
"1 - notification - beta",
"2 - notification - beta",
"1 - notification - epsilon",
]);
await waitNotifications(
[firstTabEnv, "notifType", "epsilon"],
[secondTabEnv, "notifType", "epsilon", { received: false }]
);
});
QUnit.test("notifications still received after disconnect/reconnect", async function (assert) {
QUnit.test("notifications still received after disconnect/reconnect", async () => {
addBusServicesToRegistry();
const pyEnv = await startServer();
const env = await makeTestEnv({ activateMockServer: true });
await env.services["bus_service"].start();
env.services["bus_service"].addEventListener("notification", ({ detail: notifications }) => {
assert.step("notification - " + notifications.map((notif) => notif.payload).toString());
});
env.services["bus_service"].addChannel("lambda");
await waitUntilSubscribe("lambda");
await waitForChannels(["lambda"]);
pyEnv["bus.bus"]._sendone("lambda", "notifType", "beta");
await waitNotifications([env, "notifType", "beta"]);
pyEnv.simulateConnectionLost(WEBSOCKET_CLOSE_CODES.ABNORMAL_CLOSURE);
// Give websocket worker a tick to try to restart
await nextTick();
pyEnv["bus.bus"]._sendone("lambda", "notifType", "gamma");
// Give bus service a tick to receive the notification from
// postMessage.
await nextTick();
assert.verifySteps(["notification - beta", "notification - gamma"]);
await waitNotifications([env, "notifType", "gamma"]);
});
QUnit.test("tabs share message from a channel", async function (assert) {
QUnit.test("tabs share message from a channel", async () => {
addBusServicesToRegistry();
const pyEnv = await startServer();
const steps = [];
// main
const mainEnv = await makeTestEnv({ activateMockServer: true });
mainEnv.services["bus_service"].addEventListener(
"notification",
({ detail: notifications }) => {
steps.push(
"main - notification - " + notifications.map((notif) => notif.payload).toString()
);
}
);
mainEnv.services["bus_service"].addChannel("lambda");
// slave
const slaveEnv = await makeTestEnv();
slaveEnv.services["bus_service"].start();
slaveEnv.services["bus_service"].addEventListener(
"notification",
({ detail: notifications }) => {
steps.push(
"slave - notification - " + notifications.map((notif) => notif.payload).toString()
);
}
);
await slaveEnv.services["bus_service"].addChannel("lambda");
await waitUntilSubscribe("lambda");
slaveEnv.services["bus_service"].addChannel("lambda");
await waitForChannels(["lambda"]);
pyEnv["bus.bus"]._sendone("lambda", "notifType", "beta");
// Wait one tick for the worker `postMessage` to reach the bus_service.
await nextTick();
// Wait another tick for the `bus.trigger` to reach the listeners.
await nextTick();
assert.deepEqual(steps.sort(), ["main - notification - beta", "slave - notification - beta"]);
await waitNotifications([mainEnv, "notifType", "beta"], [slaveEnv, "notifType", "beta"]);
});
QUnit.test("second tab still receives notifications after main pagehide", async function (assert) {
QUnit.test("second tab still receives notifications after main pagehide", async () => {
addBusServicesToRegistry();
const pyEnv = await startServer();
const steps = [];
// main
const mainEnv = await makeTestEnv({ activateMockServer: true });
mainEnv.services["bus_service"].start();
mainEnv.services["bus_service"].addEventListener(
"notification",
({ detail: notifications }) => {
steps.push(
"main - notification - " + notifications.map((notif) => notif.payload).toString()
);
}
);
mainEnv.services["bus_service"].addChannel("lambda");
// second env
// prevent second tab from receiving pagehide event.
patchWithCleanup(browser, {
addEventListener(eventName, callback) {
@@ -165,100 +96,41 @@ QUnit.test("second tab still receives notifications after main pagehide", async
},
});
const secondEnv = await makeTestEnv({ activateMockServer: true });
secondEnv.services["bus_service"].addEventListener(
"notification",
({ detail: notifications }) => {
steps.push(
"slave - notification - " + notifications.map((notif) => notif.payload).toString()
);
}
);
secondEnv.services["bus_service"].addChannel("lambda");
await waitUntilSubscribe("lambda");
await waitForChannels(["lambda"]);
pyEnv["bus.bus"]._sendone("lambda", "notifType", "beta");
await nextTick();
await waitNotifications([mainEnv, "notifType", "beta"], [secondEnv, "notifType", "beta"]);
// simulate unloading main
window.dispatchEvent(new Event("pagehide"));
await nextTick();
pyEnv["bus.bus"]._sendone("lambda", "notifType", "gamma");
await nextTick();
assert.deepEqual(steps.sort(), [
"main - notification - beta",
"slave - notification - beta",
"slave - notification - gamma",
]);
await waitNotifications(
[mainEnv, "notifType", "gamma", { received: false }],
[secondEnv, "notifType", "gamma"]
);
});
QUnit.test("two tabs calling addChannel simultaneously", async function (assert) {
addBusServicesToRegistry();
const channelPatch = () => ({
addChannel(channel) {
assert.step("Tab " + this.__tabId__ + ": addChannel " + channel);
super.addChannel(...arguments);
},
deleteChannel(channel) {
assert.step("Tab " + this.__tabId__ + ": deleteChannel " + channel);
super.deleteChannel(...arguments);
},
});
const firstTabEnv = await makeTestEnv({ activateMockServer: true });
const secondTabEnv = await makeTestEnv({ activateMockServer: true });
firstTabEnv.services["bus_service"].__tabId__ = 1;
secondTabEnv.services["bus_service"].__tabId__ = 2;
patchWithCleanup(firstTabEnv.services["bus_service"], channelPatch());
patchWithCleanup(secondTabEnv.services["bus_service"], channelPatch());
firstTabEnv.services["bus_service"].addChannel("alpha");
secondTabEnv.services["bus_service"].addChannel("alpha");
firstTabEnv.services["bus_service"].addChannel("beta");
secondTabEnv.services["bus_service"].addChannel("beta");
await waitUntilSubscribe("alpha", "beta");
assert.verifySteps([
"Tab 1: addChannel alpha",
"Tab 2: addChannel alpha",
"Tab 1: addChannel beta",
"Tab 2: addChannel beta",
]);
});
QUnit.test("two tabs adding a different channel", async function (assert) {
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"].addEventListener(
"notification",
({ detail: notifications }) => {
assert.step(
"first - notification - " + notifications.map((notif) => notif.payload).toString()
);
}
);
secondTabEnv.services["bus_service"].addEventListener(
"notification",
({ detail: notifications }) => {
assert.step(
"second - notification - " + notifications.map((notif) => notif.payload).toString()
);
}
);
firstTabEnv.services["bus_service"].addChannel("alpha");
secondTabEnv.services["bus_service"].addChannel("beta");
await waitUntilSubscribe("alpha", "beta");
await waitForChannels(["alpha", "beta"]);
pyEnv["bus.bus"]._sendmany([
["alpha", "notifType", "alpha"],
["beta", "notifType", "beta"],
]);
await nextTick();
assert.verifySteps(["first - notification - alpha,beta", "second - notification - alpha,beta"]);
await waitNotifications(
[firstTabEnv, "notifType", "alpha"],
[secondTabEnv, "notifType", "alpha"],
[firstTabEnv, "notifType", "beta"],
[secondTabEnv, "notifType", "beta"]
);
});
QUnit.test("channel management from multiple tabs", async function (assert) {
QUnit.test("channel management from multiple tabs", async (assert) => {
addBusServicesToRegistry();
patchWebsocketWorkerWithCleanup({
_sendToServer({ event_name, data }) {
@@ -266,29 +138,27 @@ QUnit.test("channel management from multiple tabs", async function (assert) {
super._sendToServer(...arguments);
},
});
const firstTabEnv = await makeTestEnv();
const secTabEnv = await makeTestEnv();
firstTabEnv.services["bus_service"].addChannel("channel1");
await nextTick();
await waitForChannels(["channel1"]);
// this should not trigger a subscription since the channel1 was
// aleady known.
secTabEnv.services["bus_service"].addChannel("channel1");
await nextTick();
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 nextTick();
await waitForChannels(["channel1"], { operation: "delete" });
// this should trigger a subscription since the channel2 was not
// known.
secTabEnv.services["bus_service"].addChannel("channel2");
await nextTick();
await waitForChannels(["channel2"]);
assert.verifySteps(["subscribe - [channel1]", "subscribe - [channel1,channel2]"]);
});
QUnit.test("channels subscription after disconnection", async function (assert) {
QUnit.test("channels subscription after disconnection", async (assert) => {
addBusServicesToRegistry();
const worker = patchWebsocketWorkerWithCleanup();
const env = await makeTestEnv({ activateMockServer: true });
@@ -302,127 +172,74 @@ QUnit.test("channels subscription after disconnection", async function (assert)
);
});
QUnit.test(
"Last notification id is passed to the worker on service start",
async function (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"],
]);
// let the bus service store the last notification id.
await nextTick();
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 function (assert) {
QUnit.test("Last notification id is passed to the worker on service start", async (assert) => {
addBusServicesToRegistry();
// first tab connects to the worker with user logged.
patchWithCleanup(session, {
user_id: 1,
});
const connectionInitializedDeferred = makeDeferred();
let connectionOpenedDeferred = makeDeferred();
const pyEnv = await startServer();
let updateLastNotificationDeferred = makeDeferred();
patchWebsocketWorkerWithCleanup({
_initializeConnection(client, data) {
super._initializeConnection(client, data);
connectionInitializedDeferred.resolve();
_onClientMessage(_, { action, data }) {
if (action === "initialize_connection") {
assert.step(`${action} - ${data["lastNotificationId"]}`);
updateLastNotificationDeferred.resolve();
}
return super._onClientMessage(...arguments);
},
});
const firstTabEnv = await makeTestEnv();
await firstTabEnv.services["bus_service"].start();
firstTabEnv.services["bus_service"].addEventListener("connect", () => {
if (session.user_id) {
assert.step("connect");
}
connectionOpenedDeferred.resolve();
connectionOpenedDeferred = makeDeferred();
});
firstTabEnv.services["bus_service"].addEventListener("disconnect", () => {
assert.step("disconnect");
});
await connectionInitializedDeferred;
await connectionOpenedDeferred;
// second tab connects to the worker after disconnection: user_id
// is now false.
patchWithCleanup(session, {
user_id: false,
});
const env1 = await makeTestEnv();
env1.services["bus_service"].start();
env1.services["bus_service"].addChannel("lambda");
await waitForChannels(["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 waitForEvent(env1, "notification");
updateLastNotificationDeferred = makeDeferred();
const env2 = await makeTestEnv();
await env2.services["bus_service"].start();
assert.verifySteps(["connect", "disconnect"]);
await updateLastNotificationDeferred;
// Second bus service sends the last known notification id.
assert.verifySteps([`initialize_connection - 2`]);
});
QUnit.test("Websocket reconnects upon user log in", async function (assert) {
QUnit.test("Websocket disconnects upon user log out", async () => {
addBusServicesToRegistry();
// first tab connects to the worker with user logged.
patchWithCleanup(session, { user_id: 1 });
const firstTabEnv = await makeTestEnv();
firstTabEnv.services["bus_service"].start();
await waitForEvent(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 waitForEvent(firstTabEnv, "disconnect");
});
QUnit.test("Websocket reconnects upon user log in", async () => {
addBusServicesToRegistry();
// first tab connects to the worker with no user logged.
patchWithCleanup(session, {
user_id: false,
});
const connectionInitializedDeferred = makeDeferred();
let websocketConnectedDeferred = makeDeferred();
patchWebsocketWorkerWithCleanup({
_initializeConnection(client, data) {
super._initializeConnection(client, data);
connectionInitializedDeferred.resolve();
},
});
patchWithCleanup(session, { user_id: false });
const firstTabEnv = await makeTestEnv();
await firstTabEnv.services["bus_service"].start();
firstTabEnv.services["bus_service"].addEventListener("connect", () => {
assert.step("connect");
websocketConnectedDeferred.resolve();
websocketConnectedDeferred = makeDeferred();
});
firstTabEnv.services["bus_service"].addEventListener("disconnect", () => {
assert.step("disconnect");
});
await connectionInitializedDeferred;
await websocketConnectedDeferred;
firstTabEnv.services["bus_service"].start();
await waitForEvent(firstTabEnv, "connect");
// second tab connects to the worker after connection: user_id
// is now set.
patchWithCleanup(session, {
user_id: 1,
});
const env = await makeTestEnv();
await env.services["bus_service"].start();
await websocketConnectedDeferred;
assert.verifySteps(["connect", "disconnect", "connect"]);
patchWithCleanup(session, { user_id: 1 });
const secondTabEnv = await makeTestEnv();
secondTabEnv.services["bus_service"].start();
await Promise.all([
waitForEvent(firstTabEnv, "disconnect"),
waitForEvent(firstTabEnv, "connect"),
]);
});
QUnit.test("WebSocket connects with URL corresponding to given serverURL", async function (assert) {
QUnit.test("WebSocket connects with URL corresponding to given serverURL", async (assert) => {
addBusServicesToRegistry();
patchWebsocketWorkerWithCleanup();
const serverURL = "http://random-website.com";
@@ -448,40 +265,29 @@ QUnit.test("WebSocket connects with URL corresponding to given serverURL", async
assert.verifySteps([`${serverURL.replace("http", "ws")}/websocket`]);
});
QUnit.test("Disconnect on offline, re-connect on online", async function (assert) {
QUnit.test("Disconnect on offline, re-connect on online", async () => {
addBusServicesToRegistry();
patchWebsocketWorkerWithCleanup();
let websocketConnectedDeferred = makeDeferred();
const env = await makeTestEnv();
env.services["bus_service"].addEventListener("connect", () => {
assert.step("connect");
websocketConnectedDeferred.resolve();
websocketConnectedDeferred = makeDeferred();
});
env.services["bus_service"].addEventListener("disconnect", () => assert.step("disconnect"));
await env.services["bus_service"].start();
await websocketConnectedDeferred;
await waitForEvent(env, "connect");
window.dispatchEvent(new Event("offline"));
await nextTick();
await waitForEvent(env, "disconnect");
window.dispatchEvent(new Event("online"));
await websocketConnectedDeferred;
assert.verifySteps(["connect", "disconnect", "connect"]);
await waitForEvent(env, "connect");
});
QUnit.test("No disconnect on change offline/online when bus inactive", async function (assert) {
QUnit.test("No disconnect on change offline/online when bus inactive", async () => {
addBusServicesToRegistry();
patchWebsocketWorkerWithCleanup();
const env = await makeTestEnv();
env.services["bus_service"].addEventListener("connect", () => assert.step("connect"));
env.services["bus_service"].addEventListener("disconnect", () => assert.step("disconnect"));
window.dispatchEvent(new Event("offline"));
await nextTick();
await waitForEvent(env, "disconnect", { received: false });
window.dispatchEvent(new Event("online"));
await nextTick();
assert.verifySteps([]);
await waitForEvent(env, "connect", { received: false });
});
QUnit.test("Can reconnect after late close event", async function (assert) {
QUnit.test("Can reconnect after late close event", async (assert) => {
addBusServicesToRegistry();
let subscribeSent = 0;
const closeDeferred = makeDeferred();
@@ -548,39 +354,37 @@ QUnit.test("Can reconnect after late close event", async function (assert) {
]);
});
QUnit.test(
"Fallback on simple worker when shared worker failed to initialize",
async function (assert) {
addBusServicesToRegistry();
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();
await env.services["bus_service"].start();
assert.verifySteps([
"shared-worker creation",
'Error while loading "bus_service" SharedWorker, fallback on Worker.',
"worker creation",
]);
}
);
QUnit.test("Fallback on simple worker when shared worker failed to initialize", async (assert) => {
addBusServicesToRegistry();
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 waitForEvent(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();
@@ -2,8 +2,8 @@
import { assetsWatchdogService } from "@bus/services/assets_watchdog_service";
import { busParametersService } from "@bus/bus_parameters_service";
import { multiTabService } from "@bus/multi_tab_service";
import { busService } from "@bus/services/bus_service";
import { multiTabService } from "@bus/multi_tab_service";
import { presenceService } from "@bus/services/presence_service";
import { registry } from "@web/core/registry";
@@ -1,9 +1,13 @@
/* @odoo-module */
import { registry } from "@web/core/registry";
import { patchWebsocketWorkerWithCleanup } from "@bus/../tests/helpers/mock_websocket";
import { registry } from "@web/core/registry";
import { makeDeferred } from "@web/../tests/helpers/utils";
// should be enough to decide whether or not notifications/channel
// subscriptions... are received.
const TIMEOUT = 500;
const callbackRegistry = registry.category("mock_server_websocket_callbacks");
/**
@@ -17,9 +21,11 @@ const callbackRegistry = registry.category("mock_server_websocket_callbacks");
export function waitUntilSubscribe(...requiredChannels) {
const subscribeDeferred = makeDeferred();
const failTimeout = setTimeout(() => {
subscribeDeferred.reject(new Error(`Waited 1s for ${requiredChannels} subscription`));
console.error(`Waited 1s for ${requiredChannels} subscription`);
}, 1000);
subscribeDeferred.reject(
new Error(`Waited ${TIMEOUT}ms for ${requiredChannels} subscription.`)
);
console.error(`Waited ${TIMEOUT}ms for ${requiredChannels} subscription.`);
}, TIMEOUT);
const lastCallback = callbackRegistry.get("subscribe", () => {});
callbackRegistry.add(
"subscribe",
@@ -38,3 +44,129 @@ export function waitUntilSubscribe(...requiredChannels) {
);
return subscribeDeferred;
}
/**
* Returns a deferred that resolves when the given channel(s) addition/deletion
* is notified to the websocket worker.
*
* @param {string[]} channels
* @param {object} [options={}]
* @param {"add"|"delete"} [options.operation="add"]
*
* @returns {import("@web/core/utils/concurrency").Deferred} */
export function waitForChannels(channels, { operation = "add" } = {}) {
const successDeferred = makeDeferred();
const failTimeout = setTimeout(() => {
const failMessage = `Waited ${TIMEOUT}ms for ${channels.join(", ")} to be ${
operation === "add" ? "added" : "deleted"
}`;
QUnit.assert.ok(false, failMessage);
successDeferred.resolve();
}, TIMEOUT);
const channelsSeen = new Set();
patchWebsocketWorkerWithCleanup({
async [operation === "add" ? "_addChannel" : "_deleteChannel"](client, channel) {
await super[operation === "add" ? "_addChannel" : "_deleteChannel"](client, channel);
if (channels.includes(channel)) {
channelsSeen.add(channel);
}
if (channelsSeen.size === channels.length) {
QUnit.assert.ok(
true,
`Channel(s) ${channels.join(", ")} ${
operation === "add" ? "added" : "deleted"
}.`
);
successDeferred.resolve();
clearTimeout(failTimeout);
}
},
});
return successDeferred;
}
/**
* @typedef {Object} ExpectedNotificationOptions
* @property {boolean} [received=true]
* @typedef {[env: import("@web/env").OdooEnv, notificationType: string, notificationPayload: any, options: ExpectedNotificationOptions]} ExpectedNotification
*/
/**
* Wait for a notification to be received/not received. Returns
* a deferred that resolves when the assertion is done.
*
* @param {ExpectedNotification} notification
* @returns {import("@web/core/utils/concurrency").Deferred}
*/
function _waitNotification(notification) {
const [env, type, payload, { received = true } = {}] = notification;
const notificationDeferred = makeDeferred();
const failTimeout = setTimeout(() => {
QUnit.assert.ok(
!received,
`Notification of type "${type}" with payload ${payload} not received.`
);
env.services["bus_service"].removeEventListener("notification", callback);
notificationDeferred.resolve();
}, TIMEOUT);
const callback = ({ detail: notifications }) => {
for (const notification of notifications) {
if (notification.type !== type) {
continue;
}
if (JSON.stringify(notification.payload) === JSON.stringify(payload)) {
QUnit.assert.ok(
received,
`Notification of type "${type}" with payload ${JSON.stringify(
payload
)} receveived.`
);
notificationDeferred.resolve();
clearTimeout(failTimeout);
env.services["bus_service"].removeEventListener("notification", callback);
}
}
};
env.services["bus_service"].addEventListener("notification", callback);
return notificationDeferred;
}
/**
* Wait for the expected notifications to be received/not received. Returns
* a deferred that resolves when the assertion is done.
*
* @param {ExpectedNotification[]} expectedNotifications
* @returns {import("@web/core/utils/concurrency").Deferred}
*/
export function waitNotifications(...expectedNotifications) {
return Promise.all(
expectedNotifications.map((expectedNotification) => _waitNotification(expectedNotification))
);
}
/**
* Returns a deferred that resolves when an event matching the given type is
* received.
*
* @typedef {"connect"|"disconnect"|"reconnect"|"reconnecting"|"notification"} EventType
* @param {import("@web/env").OdooEnv} env
* @param {EventType} eventType
* @param {object} [options={}]
* @param {boolean} [options.received=true]
*/
export function waitForEvent(env, eventType, { received = true } = {}) {
const eventReceivedDeferred = makeDeferred();
const failTimeout = setTimeout(() => {
env.services["bus_service"].removeEventListener(eventType, callback);
QUnit.assert.ok(!received, `Waited ${TIMEOUT}ms for ${eventType} event.`);
eventReceivedDeferred.resolve();
}, TIMEOUT);
const callback = () => {
env.services["bus_service"].removeEventListener(eventType, callback);
QUnit.assert.ok(received, `Event of type "${eventType}" received.`);
eventReceivedDeferred.resolve();
clearTimeout(failTimeout);
};
env.services["bus_service"].addEventListener(eventType, callback);
return eventReceivedDeferred;
}