Odoo18-Base/addons/bus/static/tests/bus_test_helpers.js
2025-01-06 10:57:38 +07:00

267 lines
8.6 KiB
JavaScript

import { busService } from "@bus/services/bus_service";
import { after, expect, registerDebugInfo } from "@odoo/hoot";
import { Deferred } from "@odoo/hoot-mock";
import {
MockServer,
defineModels,
patchWithCleanup,
webModels,
} from "@web/../tests/web_test_helpers";
import { BusBus } from "./mock_server/mock_models/bus_bus";
import { IrWebSocket } from "./mock_server/mock_models/ir_websocket";
import { registry } from "@web/core/registry";
import { patch } from "@web/core/utils/patch";
import { WEBSOCKET_CLOSE_CODES } from "@bus/workers/websocket_worker";
patch(busService, {
_onMessage(id, type, payload) {
registerDebugInfo("bus:", id, type, payload);
},
});
//-----------------------------------------------------------------------------
// Exports
//-----------------------------------------------------------------------------
export function defineBusModels() {
return defineModels({ ...webModels, ...busModels });
}
export const busModels = { BusBus, IrWebSocket };
//-----------------------------------------------------------------------------
// Setup
//-----------------------------------------------------------------------------
const viewsRegistry = registry.category("bus.view.archs");
viewsRegistry.category("activity").add(
"default",
/* xml */ `
<activity><templates /></activity>
`
);
viewsRegistry.category("form").add("default", /* xml */ `<form />`);
viewsRegistry.category("kanban").add("default", /* xml */ `<kanban><templates /></kanban>`);
viewsRegistry.category("list").add("default", /* xml */ `<list />`);
viewsRegistry.category("search").add("default", /* xml */ `<search />`);
viewsRegistry.category("form").add(
"res.partner",
/* xml */ `
<form>
<sheet>
<field name="name" />
</sheet>
<chatter/>
</form>`
);
// should be enough to decide whether or not notifications/channel
// subscriptions... are received.
const TIMEOUT = 500;
/**
* @param {string} eventName
* @param {Function} cb
*/
export function onWebsocketEvent(eventName, cb) {
const callbacks = registry
.category("mock_server_websocket_callbacks")
.get(eventName, new Set());
callbacks.add(cb);
registry.category("mock_server_websocket_callbacks").add(eventName, callbacks, { force: true });
}
/**
* @param {string} eventName
* @param {Function} cb
*/
export function offWebsocketEvent(eventName, cb) {
registry.category("mock_server_websocket_callbacks").get(eventName, new Set()).delete(cb);
}
/**
* Returns a deferred that resolves when a websocket subscription is
* done.
*
* @returns {import("@web/core/utils/concurrency").Deferred}
*/
export function waitUntilSubscribe() {
const def = new Deferred();
const timeout = setTimeout(() => handleResult(false), TIMEOUT);
function handleResult(success) {
clearTimeout(timeout);
offWebsocketEvent("subscribe", onSubscribe);
const message = success
? "Websocket subscription received."
: "Websocket subscription not received.";
expect(success).toBe(true, { message });
if (success) {
def.resolve();
} else {
def.reject(new Error(message));
}
}
const onSubscribe = () => handleResult(true);
onWebsocketEvent("subscribe", onSubscribe);
return def;
}
/**
* Returns a deferred that resolves when the given channel addition/deletion
* occurs. Resolve immediately if the operation was already done.
*
* @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 { env } = MockServer.current;
const def = new Deferred();
let done = false;
function check({ crashOnFail = false } = {}) {
const userChannels = new Set(env["bus.bus"].channelsByUser[env.uid]);
const success = channels.every((c) =>
operation === "add" ? userChannels.has(c) : !userChannels.has(c)
);
if (!success && !crashOnFail) {
return;
}
clearTimeout(failTimeout);
offWebsocketEvent("subscribe", check);
const message = success
? `Channel(s) [${channels.join(", ")}] ${operation === "add" ? "added" : "deleted"}.`
: `Waited ${TIMEOUT}ms for [${channels.join(", ")}] to be ${
operation === "add" ? "added" : "deleted"
}`;
expect(success).toBe(true, { message });
if (success) {
def.resolve();
} else {
def.reject(new Error(message));
}
done = true;
}
const failTimeout = setTimeout(() => check({ crashOnFail: true }), TIMEOUT);
after(() => {
if (!done) {
check({ crashOnFail: true });
}
});
onWebsocketEvent("subscribe", check);
check();
return def;
}
/**
* @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 = new Deferred();
const failTimeout = setTimeout(() => {
expect(!received).toBe(true, {
message: `Notification of type "${type}" with payload ${payload} not received.`,
});
env.services["bus_service"].unsubscribe(type, callback);
notificationDeferred.resolve();
}, TIMEOUT);
const callback = (notifPayload) => {
if (payload === undefined || JSON.stringify(notifPayload) === JSON.stringify(payload)) {
expect(received).toBe(true, {
message: `Notification of type "${type}" with payload ${JSON.stringify(
notifPayload
)} receveived.`,
});
notificationDeferred.resolve();
clearTimeout(failTimeout);
env.services["bus_service"].unsubscribe(type, callback);
}
};
env.services["bus_service"].subscribe(type, 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))
);
}
/**
* Lock the bus service start process until the returned function is called.
* This is useful in tests where an environment is mounted and the bus service
* is started immediately. However, some tests need to wait in order to setup
* their listeners.
*
* @returns {Function} A function that can be used to unlock the bus service
* start process.
*/
export function lockBusServiceStart() {
const unlockDeferred = new Deferred();
patchWithCleanup(busService, {
start() {
const API = super.start(...arguments);
patch(API, {
async start() {
await unlockDeferred;
return super.start(...arguments);
},
});
return API;
},
});
return () => unlockDeferred.resolve();
}
/**
* Lock the websocket connection until the returned function is called. Usefull
* to simulate server being unavailable.
*
* @returns {Function} A function that can be used to unlock the websocket
* connection.
*/
export function lockWebsocketConnect() {
let locked = true;
const ogSocket = window.WebSocket;
patchWithCleanup(window, {
WebSocket: function () {
const ws = locked ? new EventTarget() : new ogSocket(...arguments);
if (locked) {
queueMicrotask(() => {
ws.dispatchEvent(new Event("error"));
ws.dispatchEvent(
new CloseEvent("close", { code: WEBSOCKET_CLOSE_CODES.ABNORMAL_CLOSURE })
);
});
}
return ws;
},
});
return () => (locked = false);
}