From 7bab1cfde14c8030caa5eb34eaa88aab72f092a3 Mon Sep 17 00:00:00 2001 From: tsm-odoo Date: Tue, 10 Oct 2023 14:14:08 +0200 Subject: [PATCH] [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) --- addons/bus/static/tests/bus_tests.js | 480 ++++++------------ addons/bus/static/tests/helpers/test_utils.js | 2 +- .../tests/helpers/websocket_event_deferred.js | 140 ++++- 3 files changed, 279 insertions(+), 343 deletions(-) diff --git a/addons/bus/static/tests/bus_tests.js b/addons/bus/static/tests/bus_tests.js index 13d5acafb5c..ab236f75109 100644 --- a/addons/bus/static/tests/bus_tests.js +++ b/addons/bus/static/tests/bus_tests.js @@ -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(); diff --git a/addons/bus/static/tests/helpers/test_utils.js b/addons/bus/static/tests/helpers/test_utils.js index 68acc80184f..45cc6e7ccbc 100644 --- a/addons/bus/static/tests/helpers/test_utils.js +++ b/addons/bus/static/tests/helpers/test_utils.js @@ -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"; diff --git a/addons/bus/static/tests/helpers/websocket_event_deferred.js b/addons/bus/static/tests/helpers/websocket_event_deferred.js index e013239d2ba..21b331375c8 100644 --- a/addons/bus/static/tests/helpers/websocket_event_deferred.js +++ b/addons/bus/static/tests/helpers/websocket_event_deferred.js @@ -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; +}