/* @odoo-module */ import { patchWebsocketWorkerWithCleanup } from "@bus/../tests/helpers/mock_websocket"; import { registry } from "@web/core/registry"; import { patch } from "@web/core/utils/patch"; import { registerCleanup } from "@web/../tests/helpers/cleanup"; 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"); /** * Returns a deferred that resolves when a websocket subscription is * done. If channels are provided, the deferred will only resolve when * we subscribe to all of them. * * @param {...string} [requiredChannels] * @returns {import("@web/core/utils/concurrency").Deferred} */ export function waitUntilSubscribe(...requiredChannels) { const subscribeDeferred = makeDeferred(); const failTimeout = setTimeout(() => { const errMsg = `Subscription to ${JSON.stringify(requiredChannels)} not received.`; subscribeDeferred.reject(new Error(errMsg)); QUnit.assert.ok(false, errMsg); }, TIMEOUT); const lastCallback = callbackRegistry.get("subscribe", () => {}); callbackRegistry.add( "subscribe", (data) => { const { channels } = data; lastCallback(data); const allChannelsSubscribed = requiredChannels.every((channel) => channels.includes(channel) ); if (allChannelsSubscribed) { subscribeDeferred.resolve(); QUnit.assert.ok( true, `Subscription to ${JSON.stringify(requiredChannels)} received.` ); clearTimeout(failTimeout); } }, { force: true } ); 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 missingChannels = new Set(channels); const deferred = makeDeferred(); function check({ crashOnFail = false } = {}) { const success = missingChannels.size === 0; if (!success && !crashOnFail) { return; } unpatch(); clearTimeout(failTimeout); const msg = success ? `Channel(s) [${channels.join(", ")}] ${operation === "add" ? "added" : "deleted"}.` : `Waited ${TIMEOUT}ms for [${channels.join(", ")}] to be ${ operation === "add" ? "added" : "deleted" }`; QUnit.assert.ok(success, msg); if (success) { deferred.resolve(); } else { deferred.reject(new Error(msg)); } } const failTimeout = setTimeout(() => check({ crashOnFail: true }), TIMEOUT); registerCleanup(() => { if (missingChannels.length > 0) { check({ crashOnFail: true }); } }); const worker = patchWebsocketWorkerWithCleanup(); const workerMethod = operation === "add" ? "_addChannel" : "_deleteChannel"; const unpatch = patch(worker, { async [workerMethod](client, channel) { await super[workerMethod](client, channel); missingChannels.delete(channel); check(); }, }); return deferred; } /** * @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 ( payload === undefined || JSON.stringify(notification.payload) === JSON.stringify(payload) ) { QUnit.assert.ok( received, `Notification of type "${type}" with payload ${JSON.stringify( notification.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 from the bus service. * * @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 waitForBusEvent(env, eventType, { received = true } = {}) { const eventReceivedDeferred = makeDeferred(); const failTimeout = setTimeout(() => { env.services["bus_service"].removeEventListener(eventType, callback); QUnit.assert.ok( !received, received ? `Waited ${TIMEOUT}ms for ${eventType} event.` : `Event of type "${eventType}" not received.` ); 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; } /** * Returns a deferred that resolves when an event matching the given type is * received by the websocket worker. * * @param {import("@web/env").OdooEnv} env * @param {import("@bus/workers/websocket_worker").WorkerAction} targetAction * @param {object} [options={}] * @param {boolean} [options.received=true] */ export function waitForWorkerEvent(targetAction) { const eventReiceivedDeferred = makeDeferred(); const failTimeout = setTimeout(() => { unpatch(); QUnit.assert.ok(false, `Waited ${TIMEOUT}ms for ${targetAction} to be received.`); eventReiceivedDeferred.resolve(); }, TIMEOUT); const worker = patchWebsocketWorkerWithCleanup(); const unpatch = patch(worker, { _onClientMessage(_, { action }) { super._onClientMessage(...arguments); if (targetAction === action) { unpatch(); QUnit.assert.ok(true, `Action "${action}" received.`); eventReiceivedDeferred.resolve(); clearTimeout(failTimeout); } }, }); registerCleanup(unpatch); return eventReiceivedDeferred; }