[IMP] mail, *: adapt bus service to wowl env.

*: bus, calendar, iap_mail, im_livechat, project, web.

In order to ease the PR introducing the websockets in Odoo, the bus service
has to be updated to be a wowl service. This PR takes care of it.

task-2053917

closes odoo/odoo#95824

Related: odoo/enterprise#29361
Signed-off-by: Sébastien Theys (seb) <seb@odoo.com>
This commit is contained in:
Matthieu Stockbauer
2022-07-14 11:42:42 +02:00
committed by tsm-odoo
parent 8467f7b9ff
commit 3ae11c87e8
32 changed files with 432 additions and 583 deletions
+1 -5
View File
@@ -13,11 +13,7 @@
'bus/static/src/**/*',
],
'web.assets_frontend': [
'bus/static/src/js/longpolling_bus.js',
'bus/static/src/js/crosstab_bus.js',
'bus/static/src/js/services/bus_service.js',
'bus/static/src/js/services/legacy/*.js',
'bus/static/src/js/*.js',
'bus/static/src/**/*.js',
],
'web.qunit_suite_tests': [
'bus/static/tests/*.js',
@@ -1,9 +1,9 @@
odoo.define('bus.CrossTab', function (require) {
"use strict";
/** @odoo-module **/
var Longpolling = require('bus.Longpolling');
import { browser } from '@web/core/browser/browser';
import session from 'web.session';
var session = require('web.session');
import { Longpolling } from '@bus/longpolling_bus';
/**
* CrossTab
@@ -26,23 +26,20 @@ var session = require('web.session');
* - become_master : when this tab became the master
* - no_longer_master : when this tab is not longer the master (the user swith tab)
*/
var CrossTabBus = Longpolling.extend({
// constants
TAB_HEARTBEAT_PERIOD: 10000, // 10 seconds
MASTER_TAB_HEARTBEAT_PERIOD: 1500, // 1.5 seconds
HEARTBEAT_OUT_OF_DATE_PERIOD: 5000, // 5 seconds
HEARTBEAT_KILL_OLD_PERIOD: 15000, // 15 seconds
LOCAL_STORAGE_PREFIX: 'bus',
export class CrossTab extends Longpolling {
constructor(env, services) {
super(env, services);
// constants
this.TAB_HEARTBEAT_PERIOD = 10000; // 10 seconds
this.MASTER_TAB_HEARTBEAT_PERIOD = 1500; // 1.5 seconds
this.HEARTBEAT_OUT_OF_DATE_PERIOD = 5000; // 5 seconds
this.HEARTBEAT_KILL_OLD_PERIOD = 15000; // 15 seconds
this.LOCAL_STORAGE_PREFIX = 'bus';
// properties
_isMasterTab: false,
_isRegistered: false,
// properties
this._isMasterTab = false;
this._isRegistered = false;
/**
* @override
*/
init: function () {
this._super.apply(this, arguments);
var now = new Date().getTime();
// used to prefix localStorage keys
this._sanitizedOrigin = session.origin.replace(/:\/{0,2}/g, '_');
@@ -53,55 +50,57 @@ var CrossTabBus = Longpolling.extend({
this._callLocalStorage('removeItem', 'last');
}
this._lastNotificationID = this._callLocalStorage('getItem', 'last', 0);
this.call('local_storage', 'onStorage', this, this._onStorage);
},
destroy: function () {
this._super();
clearTimeout(this._heartbeatTimeout);
},
browser.addEventListener('storage', this._onStorage.bind(this));
}
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
/**
* Share the bus channels with the others tab by the local storage
*
* @override
*/
addChannel: function (channel) {
addChannel(channel) {
this._currentTabChannels.add(channel);
this._super.apply(this, arguments);
super.addChannel(channel);
this._updateChannels();
},
}
/**
* Share the bus channels with the others tab by the local storage
*
* @override
*/
deleteChannel: function (channel) {
deleteChannel(channel) {
this._currentTabChannels.delete(channel);
this._super.apply(this, arguments);
super.deleteChannel(channel);
this._updateChannels();
},
}
/**
* @return {string}
*/
getTabId: function () {
getTabId() {
return this._id;
},
}
/**
* Tells whether this bus is related to the master tab.
*
* @returns {boolean}
*/
isMasterTab: function () {
isMasterTab() {
return this._isMasterTab;
},
}
/**
* Use the local storage to share the long polling from the master tab.
*
* @override
*/
startPolling: function () {
startPolling() {
if (this._isActive === null) {
this._heartbeat = this._heartbeat.bind(this);
}
@@ -130,23 +129,26 @@ var CrossTabBus = Longpolling.extend({
}
if (this._isMasterTab) {
this._super.apply(this, arguments);
super.startPolling();
}
},
}
/**
* Share the option with the local storage
*
* @override
*/
updateOption: function () {
this._super.apply(this, arguments);
updateOption(key, value) {
super.updateOption(key, value);
this._callLocalStorage('setItem', 'options', this._options);
},
}
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* Call local_storage service
* Call browser localStorage.
*
* @private
* @param {string} method (getItem, setItem, removeItem, on)
@@ -154,9 +156,17 @@ var CrossTabBus = Longpolling.extend({
* @param {any} param
* @returns service information
*/
_callLocalStorage: function (method, key, param) {
return this.call('local_storage', method, this._generateKey(key), param);
},
_callLocalStorage(method, key, param) {
if (method === 'setItem') {
param = JSON.stringify(param);
}
const result = browser.localStorage[method](this._generateKey(key), param);
if (method === 'getItem') {
return result ? JSON.parse(result) : param;
}
}
/**
* Generates localStorage keys prefixed by bus. (LOCAL_STORAGE_PREFIX = the name
* of this addon), and the sanitized origin, to prevent keys from
@@ -167,16 +177,18 @@ var CrossTabBus = Longpolling.extend({
* @param {string} key
* @returns key prefixed with the origin
*/
_generateKey: function (key) {
_generateKey(key) {
return this.LOCAL_STORAGE_PREFIX + '.' + this._sanitizedOrigin + '.' + key;
},
}
/**
* @override
* @returns {integer} number of milliseconds since 1 January 1970 00:00:00
*/
_getLastPresence: function () {
return this._callLocalStorage('getItem', 'lastPresence') || this._super();
},
_getLastPresence() {
return this._callLocalStorage('getItem', 'lastPresence') || super._getLastPresence();
}
/**
* Check all the time (according to the constants) if the tab is the master tab and
* check if it is active. Use the local storage for this checks.
@@ -184,7 +196,7 @@ var CrossTabBus = Longpolling.extend({
* @private
* @see _startElection method
*/
_heartbeat: function () {
_heartbeat() {
var now = new Date().getTime();
var heartbeatValue = parseInt(this._callLocalStorage('getItem', 'heartbeat', 0));
var peers = this._callLocalStorage('getItem', 'peers', {});
@@ -229,22 +241,23 @@ var CrossTabBus = Longpolling.extend({
if (this._lastPresenceTime + hbPeriod > now) {
this._callLocalStorage('setItem', 'lastPresence', this._lastPresenceTime);
}
this._heartbeatTimeout = browser.setTimeout(this._heartbeat.bind(this), hbPeriod);
}
this._heartbeatTimeout = setTimeout(this._heartbeat.bind(this), hbPeriod);
},
/**
* @private
*/
_registerWindowUnload: function () {
$(window).on('unload.' + this._id, this._onUnload.bind(this));
},
_registerWindowUnload() {
browser.addEventListener('unload', this._onUnload.bind(this));
}
/**
* Check with the local storage if the current tab is the master tab.
* If this tab became the master, trigger 'become_master' event
*
* @private
*/
_startElection: function () {
_startElection() {
if (this._isMasterTab) {
return;
}
@@ -275,14 +288,15 @@ var CrossTabBus = Longpolling.extend({
delete peers[newMaster];
this._callLocalStorage('setItem', 'peers', peers);
}
},
}
/**
* Update localstorage channels of with the channels of this tab.
*
* @private
* @return {boolean} true if the aggregated channels has changed.
*/
_updateChannels: function () {
_updateChannels() {
const currentPeerIds = new Set(Object.keys(this._callLocalStorage('getItem', 'peers') || {}));
const peerChannels = this._callLocalStorage('getItem', 'channels') || {};
const peerChannelsBefore = JSON.stringify(peerChannels);
@@ -314,31 +328,35 @@ var CrossTabBus = Longpolling.extend({
}
this._channels = Array.from(allChannels);
return true;
},
}
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
/**
* @override
*/
_onFocusChange: function (params) {
this._super.apply(this, arguments);
_onFocusChange(params) {
super._onFocusChange(params);
this._callLocalStorage('setItem', 'focus', params.focus);
},
}
/**
* If it's the master tab, the notifications ares broadcasted to other tabs by the
* local storage.
*
* @override
*/
_onPoll: function (notifications) {
var notifs = this._super(notifications);
_onPoll(notifications) {
var notifs = super._onPoll(notifications);
if (this._isMasterTab && notifs.length) {
this._callLocalStorage('setItem', 'last', this._lastNotificationID);
this._callLocalStorage('setItem', 'last_ts', new Date().getTime());
this._callLocalStorage('setItem', 'notification', notifs);
}
},
}
/**
* Handler when the local storage is updated
*
@@ -347,7 +365,10 @@ var CrossTabBus = Longpolling.extend({
* @param {string} event.key
* @param {string} event.newValue
*/
_onStorage: function (e) {
_onStorage(e) {
if (!e.key || !e.key.includes(this.LOCAL_STORAGE_PREFIX)) {
return;
}
var value = JSON.parse(e.newValue);
var key = e.key;
@@ -381,13 +402,14 @@ var CrossTabBus = Longpolling.extend({
this._isOdooFocused = value;
this.trigger('window_focus', this._isOdooFocused);
}
},
}
/**
* Handler when unload the window
*
* @private
*/
_onUnload: function () {
_onUnload() {
// unload peer
var peers = this._callLocalStorage('getItem', 'peers') || {};
delete peers[this._id];
@@ -399,10 +421,5 @@ var CrossTabBus = Longpolling.extend({
if (this._isMasterTab) {
this._callLocalStorage('removeItem', 'master');
}
},
});
return CrossTabBus;
});
}
}
@@ -1,13 +0,0 @@
/** @odoo-module **/
import { registry } from '@web/core/registry';
const { Component } = owl;
export const legacyBusService = {
start() {
return Component.env.services.bus_service;
},
};
registry.category('services').add('legacy_bus_service', legacyBusService);
@@ -1,9 +1,8 @@
odoo.define('bus.Longpolling', function (require) {
"use strict";
/** @odoo-module **/
var Bus = require('web.Bus');
var ServicesMixin = require('web.ServicesMixin');
import { browser } from '@web/core/browser/browser';
const { EventBus } = owl;
/**
* Event Longpolling bus used to bind events on the server long polling return
@@ -11,27 +10,23 @@ var ServicesMixin = require('web.ServicesMixin');
* trigger:
* - window_focus : when the window focus change (true for focused, false for blur)
* - notification : when a notification is receive from the long polling
*
* @class Longpolling
*/
var LongpollingBus = Bus.extend(ServicesMixin, {
// constants
PARTNERS_PRESENCE_CHECK_PERIOD: 30000, // don't check presence more than once every 30s
ERROR_RETRY_DELAY: 10000, // 10 seconds
POLL_ROUTE: '/longpolling/poll',
export class Longpolling extends EventBus {
constructor(env) {
super();
this.env = env;
// properties
_isActive: null,
_lastNotificationID: 0,
_isOdooFocused: true,
_pollRetryTimeout: null,
// CONSTANTS
this.PARTNERS_PRESENCE_CHECK_PERIOD = 30000; // don't check presence more than once every 30s
this.ERROR_RETRY_DELAY = 10000; // 10 seconds
this.POLL_ROUTE = '/longpolling/poll';
/**
* @override
*/
init: function (parent, params) {
this._super.apply(this, arguments);
// PROPERTIES
this._isActive = null;
this._id = _.uniqueId('bus');
this._lastNotificationID = 0;
this._isOdooFocused = true;
this._pollRetryTimeout = null;
// the _id is modified by crosstab_bus, so we can't use it to unbind the events in the destroy.
this._longPollingBusId = this._id;
@@ -40,30 +35,19 @@ var LongpollingBus = Bus.extend(ServicesMixin, {
// bus presence
this._lastPresenceTime = new Date().getTime();
$(window).on("focus." + this._longPollingBusId, this._onFocusChange.bind(this, {focus: true}));
$(window).on("blur." + this._longPollingBusId, this._onFocusChange.bind(this, {focus: false}));
$(window).on("unload." + this._longPollingBusId, this._onFocusChange.bind(this, {focus: false}));
$(window).on("click." + this._longPollingBusId, this._onPresence.bind(this));
$(window).on("keydown." + this._longPollingBusId, this._onPresence.bind(this));
$(window).on("keyup." + this._longPollingBusId, this._onPresence.bind(this));
},
/**
* @override
*/
destroy: function () {
this.stopPolling();
$(window).off("focus." + this._longPollingBusId);
$(window).off("blur." + this._longPollingBusId);
$(window).off("unload." + this._longPollingBusId);
$(window).off("click." + this._longPollingBusId);
$(window).off("keydown." + this._longPollingBusId);
$(window).off("keyup." + this._longPollingBusId);
this._super();
},
browser.addEventListener("focus", this._onFocusChange.bind(this, { focus: true }));
browser.addEventListener("blur", this._onFocusChange.bind(this, { focus: false }));
browser.addEventListener("unload", this._onFocusChange.bind(this, { focus: false }));
browser.addEventListener("click", this._onPresence.bind(this));
browser.addEventListener("keydown", this._onPresence.bind(this));
browser.addEventListener("keyup", this._onPresence.bind(this));
}
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
/**
* Register a new channel to listen on the longpoll (ignore if already
* listening on this channel).
@@ -72,12 +56,13 @@ var LongpollingBus = Bus.extend(ServicesMixin, {
*
* @param {string} channel
*/
addChannel: function (channel) {
addChannel(channel) {
if (this._channels.indexOf(channel) === -1) {
this._channels.push(channel);
this._restartPolling();
}
},
}
/**
* Unregister a channel from listening on the longpoll.
*
@@ -87,7 +72,7 @@ var LongpollingBus = Bus.extend(ServicesMixin, {
*
* @param {string} channel
*/
deleteChannel: function (channel) {
deleteChannel(channel) {
var index = this._channels.indexOf(channel);
if (index !== -1) {
this._channels.splice(index, 1);
@@ -95,20 +80,22 @@ var LongpollingBus = Bus.extend(ServicesMixin, {
this._pollRpc.abort();
}
}
},
}
/**
* Tell whether odoo is focused or not
*
* @returns {boolean}
*/
isOdooFocused: function () {
isOdooFocused() {
return this._isOdooFocused;
},
}
/**
* Start a long polling, i.e. it continually opens a long poll
* connection as long as it is not stopped (@see `stopPolling`)
*/
startPolling: function () {
startPolling() {
if (this._isActive === null) {
this._poll = this._poll.bind(this);
}
@@ -116,21 +103,23 @@ var LongpollingBus = Bus.extend(ServicesMixin, {
this._isActive = true;
this._poll();
}
},
}
/**
* Stops any started long polling
*
* Aborts a pending longpoll so that we immediately remove ourselves
* from listening on notifications on this channel.
*/
stopPolling: function () {
stopPolling() {
this._isActive = false;
this._channels = [];
clearTimeout(this._pollRetryTimeout);
if (this._pollRpc) {
this._pollRpc.abort();
}
},
}
/**
* Add or update an option on the longpoll bus.
* Stored options are sent to the server whenever a poll is started.
@@ -138,21 +127,25 @@ var LongpollingBus = Bus.extend(ServicesMixin, {
* @param {string} key
* @param {any} value
*/
updateOption: function (key, value) {
updateOption(key, value) {
this._options[key] = value;
},
}
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* returns the last recorded presence
*
* @private
* @returns {integer} number of milliseconds since 1 January 1970 00:00:00
*/
_getLastPresence: function () {
_getLastPresence() {
return this._lastPresenceTime;
},
}
/**
* Continually start a poll:
*
@@ -167,7 +160,7 @@ var LongpollingBus = Bus.extend(ServicesMixin, {
*
* @private
*/
_poll: function () {
_poll() {
var self = this;
if (!this._isActive) {
return;
@@ -183,10 +176,8 @@ var LongpollingBus = Bus.extend(ServicesMixin, {
self._pollRpc = false;
self._onPoll(result);
self._poll();
}).guardedCatch(function (result) {
}).catch(function (result) {
self._pollRpc = false;
// no error popup if request is interrupted or fails for any reason
result.event.preventDefault();
if (result.message === "XmlHttpRequestError abort") {
self._poll();
} else {
@@ -194,19 +185,20 @@ var LongpollingBus = Bus.extend(ServicesMixin, {
self._pollRetryTimeout = setTimeout(self._poll, self.ERROR_RETRY_DELAY + (Math.floor((Math.random()*20)+1)*1000));
}
});
},
}
/**
* @private
* @param data: object with poll parameters
*/
_makePoll: function(data) {
return this._rpc({route: this.POLL_ROUTE, params: data}, {shadow : true, timeout: 60000});
},
_makePoll(params) {
return this.env.services.rpc(this.POLL_ROUTE, params, { silent: true, timeout: 60000 });
}
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
/**
* Handler when the focus of the window change.
* Trigger the 'window_focus' event.
@@ -215,13 +207,14 @@ var LongpollingBus = Bus.extend(ServicesMixin, {
* @param {Object} params
* @param {Boolean} params.focus
*/
_onFocusChange: function (params) {
_onFocusChange(params) {
this._isOdooFocused = params.focus;
if (params.focus) {
this._lastPresenceTime = new Date().getTime();
this.trigger('window_focus', this._isOdooFocused);
}
},
}
/**
* Handler when the long polling receive the new notifications
* Update the last notification id received.
@@ -231,7 +224,7 @@ var LongpollingBus = Bus.extend(ServicesMixin, {
* @param {Object[]} notifications, Input notifications have an id, channel, message
* @returns {Array[]} Output arrays have notification's channel and message
*/
_onPoll: function (notifications) {
_onPoll(notifications) {
var self = this;
var notifs = _.map(notifications, function (notif) {
if (notif.id > self._lastNotificationID) {
@@ -241,16 +234,18 @@ var LongpollingBus = Bus.extend(ServicesMixin, {
});
this.trigger("notification", notifs);
return notifs;
},
}
/**
* Handler when they are an activity on the window (click, keydown, keyup)
* Update the last presence date.
*
* @private
*/
_onPresence: function () {
_onPresence() {
this._lastPresenceTime = new Date().getTime();
},
}
/**
* Restart polling.
*
@@ -262,9 +257,5 @@ var LongpollingBus = Bus.extend(ServicesMixin, {
} else {
this.startPolling();
}
},
});
return LongpollingBus;
});
}
}
@@ -4,19 +4,16 @@ import { browser } from "@web/core/browser/browser";
import { registry } from "@web/core/registry";
import { session } from "@web/session";
const { Component } = owl;
export const assetsWatchdogService = {
dependencies: ["notification"],
dependencies: ["bus_service", "notification"],
start(env, { notification }) {
start(env, { bus_service, notification }) {
let isNotificationDisplayed = false;
let bundleNotifTimerID = null;
env.bus.on("WEB_CLIENT_READY", null, async () => {
const legacyEnv = Component.env;
legacyEnv.services.bus_service.onNotification(this, onNotification);
legacyEnv.services.bus_service.startPolling();
bus_service.onNotification(this, onNotification);
bus_service.startPolling();
});
/**
@@ -1,55 +1,17 @@
odoo.define('bus.BusService', function (require) {
"use strict";
/** @odoo-module **/
var CrossTab = require('bus.CrossTab');
var core = require('web.core');
var ServicesMixin = require('web.ServicesMixin');
const session = require('web.session');
import { CrossTab } from '@bus/crosstab_bus';
var BusService = CrossTab.extend(ServicesMixin, {
dependencies : ['local_storage'],
import { registry } from '@web/core/registry';
import session from 'web.session';
// properties
_audio: null,
export class BusService extends CrossTab {
constructor(env, services) {
super(env, services);
/**
* As the BusService doesn't extend AbstractService, we have to replicate
* here what is done in AbstractService
*
* @param {Object} env
*/
init: function (env) {
this.env = env;
this._super();
},
/**
* Replicate the behavior of AbstractService:
*
* Directly calls the requested service, instead of triggering a
* 'call_service' event up, which wouldn't work as services have no parent.
*
* @param {OdooEvent} ev
*/
_trigger_up: function (ev) {
if (ev.name === 'call_service') {
const payload = ev.data;
let args = payload.args || [];
if (payload.service === 'ajax' && payload.method === 'rpc') {
// ajax service uses an extra 'target' argument for rpc
args = args.concat(ev.target);
}
const service = this.env.services[payload.service];
const result = service[payload.method].apply(service, args);
payload.callback(result);
}
},
/**
* This method is necessary in order for this Class to be used to instantiate services
*
* @abstract
*/
start: function () {},
// properties
this._audio = null;
}
//--------------------------------------------------------------------------
// Public
@@ -71,10 +33,10 @@ var BusService = CrossTab.extend(ServicesMixin, {
this._sendNativeNotification(options.title, options.message, callback);
} catch (error) {
// Notification without Serviceworker in Chrome Android doesn't works anymore
// So we fallback to displayNotification() in this case
// So we fallback to the notification service in this case
// https://bugs.chromium.org/p/chromium/issues/detail?id=481856
if (error.message.indexOf('ServiceWorkerRegistration') > -1) {
this.displayNotification(options);
this.env.services['notification'].add(options.message, options);
this._beep();
} else {
throw error;
@@ -82,21 +44,22 @@ var BusService = CrossTab.extend(ServicesMixin, {
}
}
} else {
this.displayNotification(options);
this.env.services['notification'].add(options.message, options);
if (this.isMasterTab()) {
this._beep();
}
}
},
}
/**
* Register listeners on notifications received on this bus service
*
* @param {Object} receiver
* @param {function} func
*/
onNotification: function () {
onNotification() {
this.on.apply(this, ["notification"].concat(Array.prototype.slice.call(arguments)));
},
}
//--------------------------------------------------------------------------
// Private
@@ -107,7 +70,7 @@ var BusService = CrossTab.extend(ServicesMixin, {
*
* @private
*/
_beep: function () {
_beep() {
if (typeof(Audio) !== "undefined") {
if (!this._audio) {
this._audio = new Audio();
@@ -116,7 +79,8 @@ var BusService = CrossTab.extend(ServicesMixin, {
}
Promise.resolve(this._audio.play()).catch(_.noop);
}
},
}
/**
* Show a browser notification
*
@@ -125,7 +89,7 @@ var BusService = CrossTab.extend(ServicesMixin, {
* @param {string} content
* @param {function} [callback] if given callback will be called when user clicks on notification
*/
_sendNativeNotification: function (title, content, callback) {
_sendNativeNotification(title, content, callback) {
var notification = new Notification(
// The native Notification API works with plain text and not HTML
// unescaping is safe because done only at the **last** step
@@ -145,12 +109,13 @@ var BusService = CrossTab.extend(ServicesMixin, {
callback();
}
};
}
}
export const busService = {
dependencies: ['notification'],
start(env, services) {
return new BusService(env, services);
},
});
core.serviceRegistry.add('bus_service', BusService);
return BusService;
});
};
registry.category('services').add('bus_service', busService);
@@ -0,0 +1,14 @@
/** @odoo-module **/
import { registry } from '@web/core/registry';
export function makeBusServiceToLegacyEnv(legacyEnv) {
return {
dependencies: ['bus_service'],
start(_, { bus_service }) {
legacyEnv.services['bus_service'] = bus_service;
},
};
}
registry.category('wowlToLegacyServiceMappers').add('bus_service_to_legacy_env', makeBusServiceToLegacyEnv);
@@ -1,33 +1,24 @@
/** @odoo-module */
import * as legacyRegistry from "web.Registry";
import * as BusService from "bus.BusService";
import * as RamStorage from "web.RamStorage";
import * as AbstractStorageService from "web.AbstractStorageService";
import { busService } from "@bus/services/bus_service";
import { createWebClient } from "@web/../tests/webclient/helpers";
import { assetsWatchdogService } from "@bus/js/services/assets_watchdog_service";
import { assetsWatchdogService } from "@bus/services/assets_watchdog_service";
import { click, getFixture, nextTick, patchWithCleanup } from "@web/../tests/helpers/utils";
import { browser } from "@web/core/browser/browser";
import { registry } from "@web/core/registry";
const LocalStorageService = AbstractStorageService.extend({
storage: new RamStorage(),
});
const serviceRegistry = registry.category("services");
QUnit.module("Bus Assets WatchDog", (hooks) => {
let legacyServicesRegistry;
let target;
hooks.beforeEach((assert) => {
legacyServicesRegistry = new legacyRegistry();
legacyServicesRegistry.add("bus_service", BusService);
legacyServicesRegistry.add("local_storage", LocalStorageService);
serviceRegistry.add("assetsWatchdog", assetsWatchdogService);
serviceRegistry.add("bus_service", busService);
patchWithCleanup(browser, {
setTimeout: (fn) => fn(),
clearTimeout: () => {},
setTimeout(fn) {
return this._super(fn, 0);
},
location: {
reload: () => assert.step("reloadPage"),
},
@@ -56,7 +47,6 @@ QUnit.module("Bus Assets WatchDog", (hooks) => {
};
await createWebClient({
legacyParams: { serviceRegistry: legacyServicesRegistry },
mockRPC,
});
+66 -194
View File
@@ -1,45 +1,35 @@
odoo.define('web.bus_tests', function (require) {
"use strict";
var BusService = require('bus.BusService');
var CrossTabBus = require('bus.CrossTab');
var AbstractStorageService = require('web.AbstractStorageService');
var RamStorage = require('web.RamStorage');
var { busService } = require('@bus/services/bus_service');
var { CrossTab } = require('@bus/crosstab_bus');
var testUtils = require('web.test_utils');
var Widget = require('web.Widget');
const LegacyRegistry = require("web.Registry");
const { browser } = require("@web/core/browser/browser");
const { ConnectionLostError } = require("@web/core/network/rpc_service");
const { registry } = require("@web/core/registry");
const { patchWithCleanup, nextTick } = require("@web/../tests/helpers/utils");
const { createWebClient } = require('@web/../tests/webclient/helpers');
const { Component } = owl;
var LocalStorageServiceMock;
BusService = BusService.extend({
TAB_HEARTBEAT_PERIOD: 10,
MASTER_TAB_HEARTBEAT_PERIOD: 1,
});
const { makeTestEnv } = require('@web/../tests/helpers/mock_env');
QUnit.module('Bus', {
beforeEach: function () {
LocalStorageServiceMock = AbstractStorageService.extend({storage: new RamStorage()});
const customBusService = {
...busService,
start() {
const originalBusService = busService.start(...arguments);
originalBusService.TAB_HEARTBEAT_PERIOD = 10;
originalBusService.MASTER_TAB_HEARTBEAT_PERIOD = 1;
return originalBusService;
},
};
registry.category('services').add('bus_service', customBusService);
},
}, function () {
QUnit.test('notifications received from the longpolling channel', async function (assert) {
assert.expect(6);
var pollPromise = testUtils.makeTestPromise();
var parent = new Widget();
await testUtils.mock.addMockEnvironment(parent, {
data: {},
services: {
bus_service: BusService,
local_storage: LocalStorageServiceMock,
},
mockRPC: function (route, args) {
const env = await makeTestEnv({
mockRPC(route, args) {
if (route === '/longpolling/poll') {
assert.step(route + ' - ' + args.channels.join(','));
@@ -49,17 +39,12 @@ QUnit.module('Bus', {
}).bind(pollPromise);
return pollPromise;
}
return this._super.apply(this, arguments);
}
},
});
var widget = new Widget(parent);
await widget.appendTo($('#qunit-fixture'));
widget.call('bus_service', 'onNotification', this, function (notifications) {
env.services['bus_service'].onNotification(null, notifications => {
assert.step('notification - ' + notifications.toString());
});
widget.call('bus_service', 'addChannel', 'lambda');
env.services['bus_service'].addChannel('lambda');
pollPromise.resolve([{
message: 'beta',
@@ -78,15 +63,10 @@ QUnit.module('Bus', {
'notification - epsilon',
'/longpolling/poll - lambda',
]);
parent.destroy();
});
QUnit.test('longpolling restarts when connection is lost', async function (assert) {
assert.expect(4);
const legacyRegistry = new LegacyRegistry();
legacyRegistry.add("bus_service", BusService);
legacyRegistry.add("local_storage", LocalStorageServiceMock);
const oldSetTimeout = window.setTimeout;
patchWithCleanup(
@@ -99,23 +79,21 @@ QUnit.module('Bus', {
let busService;
let rpcCount = 0;
// Using createWebclient to get the compatibility layer between the old services and the new
await createWebClient({
const env = await makeTestEnv({
mockRPC(route) {
if (route === '/longpolling/poll') {
rpcCount++;
assert.step(`polling ${rpcCount}`);
if (rpcCount == 1) {
return Promise.reject(new ConnectionLostError());
return new ConnectionLostError();
}
assert.equal(rpcCount, 2, "Should not be called after stopPolling");
busService.stopPolling();
return Promise.reject(new ConnectionLostError());
return new ConnectionLostError();
}
},
legacyParams: { serviceRegistry: legacyRegistry },
});
busService = Component.env.services.bus_service;
busService = env.services['bus_service'];
busService.startPolling();
// Give longpolling bus a tick to try to restart polling
await nextTick();
@@ -136,48 +114,32 @@ QUnit.module('Bus', {
assert.expect(3);
// Simulate no ID of last notification in the local storage
testUtils.mock.patch(LocalStorageServiceMock, {
getItem: function (key) {
patchWithCleanup(browser, {
getItem(key) {
if (key === 'last_ts') {
return 0;
}
return this._super.apply(this, arguments);
return this._super(...arguments);
},
});
var pollPromise = testUtils.makeTestPromise();
var parent = new Widget();
await testUtils.mock.addMockEnvironment(parent, {
data: {},
services: {
bus_service: BusService,
local_storage: LocalStorageServiceMock,
},
mockRPC: function (route, args) {
const env = await makeTestEnv({
mockRPC(route, args) {
if (route === '/longpolling/poll') {
assert.step(route);
assert.strictEqual(args.last, 0,
"provided last notification ID should be 0");
pollPromise = testUtils.makeTestPromise();
pollPromise.abort = (function () {
this.reject({message: "XmlHttpRequestError abort"}, $.Event());
}).bind(pollPromise);
return pollPromise;
}
return this._super.apply(this, arguments);
}
});
var widget = new Widget(parent);
await widget.appendTo($('#qunit-fixture'));
// trigger longpolling poll RPC
widget.call('bus_service', 'addChannel', 'lambda');
env.services['bus_service'].addChannel('lambda');
assert.verifySteps(['/longpolling/poll']);
testUtils.mock.unpatch(LocalStorageServiceMock);
parent.destroy();
});
QUnit.test('cross tab bus share message from a channel', async function (assert) {
@@ -186,60 +148,33 @@ QUnit.module('Bus', {
// master
var pollPromiseMaster = testUtils.makeTestPromise();
var parentMaster = new Widget();
await testUtils.mock.addMockEnvironment(parentMaster, {
data: {},
services: {
bus_service: BusService,
local_storage: LocalStorageServiceMock,
},
mockRPC: function (route, args) {
const masterEnv = await makeTestEnv({
mockRPC(route, args) {
if (route === '/longpolling/poll') {
assert.step('master' + ' - ' + route + ' - ' + args.channels.join(','));
pollPromiseMaster = testUtils.makeTestPromise();
pollPromiseMaster.abort = (function () {
this.reject({message: "XmlHttpRequestError abort"}, $.Event());
}).bind(pollPromiseMaster);
return pollPromiseMaster;
}
return this._super.apply(this, arguments);
}
});
var master = new Widget(parentMaster);
await master.appendTo($('#qunit-fixture'));
master.call('bus_service', 'onNotification', master, function (notifications) {
masterEnv.services['bus_service'].onNotification(null, notifications => {
assert.step('master - notification - ' + notifications.toString());
});
master.call('bus_service', 'addChannel', 'lambda');
masterEnv.services['bus_service'].addChannel('lambda');
// slave
await testUtils.nextTick();
var parentSlave = new Widget();
await testUtils.mock.addMockEnvironment(parentSlave, {
data: {},
services: {
bus_service: BusService,
local_storage: LocalStorageServiceMock,
},
mockRPC: function (route, args) {
const slaveEnv = await makeTestEnv({
mockRPC(route, args) {
if (route === '/longpolling/poll') {
throw new Error("Can not use the longpolling of the slave client");
}
return this._super.apply(this, arguments);
}
});
var slave = new Widget(parentSlave);
await slave.appendTo($('#qunit-fixture'));
slave.call('bus_service', 'onNotification', slave, function (notifications) {
slaveEnv.services['bus_service'].onNotification(null, notifications => {
assert.step('slave - notification - ' + notifications.toString());
});
slave.call('bus_service', 'addChannel', 'lambda');
slaveEnv.services['bus_service'].addChannel('lambda');
pollPromiseMaster.resolve([{
id: 1,
@@ -254,9 +189,6 @@ QUnit.module('Bus', {
'slave - notification - beta',
'master - /longpolling/poll - lambda',
]);
parentMaster.destroy();
parentSlave.destroy();
});
QUnit.test('cross tab bus elect new master on master unload', async function (assert) {
@@ -265,67 +197,38 @@ QUnit.module('Bus', {
// master
var pollPromiseMaster = testUtils.makeTestPromise();
var parentMaster = new Widget();
await testUtils.mock.addMockEnvironment(parentMaster, {
data: {},
services: {
bus_service: BusService,
local_storage: LocalStorageServiceMock,
},
mockRPC: function (route, args) {
const masterEnv = await makeTestEnv({
mockRPC(route, args) {
if (route === '/longpolling/poll') {
assert.step('master - ' + route + ' - ' + args.channels.join(','));
pollPromiseMaster = testUtils.makeTestPromise();
pollPromiseMaster.abort = (function () {
this.reject({message: "XmlHttpRequestError abort"}, $.Event());
}).bind(pollPromiseMaster);
return pollPromiseMaster;
}
return this._super.apply(this, arguments);
}
});
var master = new Widget(parentMaster);
await master.appendTo($('#qunit-fixture'));
master.call('bus_service', 'onNotification', master, function (notifications) {
masterEnv.services['bus_service'].onNotification(null, notifications => {
assert.step('master - notification - ' + notifications.toString());
});
master.call('bus_service', 'addChannel', 'lambda');
masterEnv.services['bus_service'].addChannel('lambda');
// slave
await testUtils.nextTick();
var parentSlave = new Widget();
var pollPromiseSlave = testUtils.makeTestPromise();
await testUtils.mock.addMockEnvironment(parentSlave, {
data: {},
services: {
bus_service: BusService,
local_storage: LocalStorageServiceMock,
},
mockRPC: function (route, args) {
const slaveEnv = await makeTestEnv({
mockRPC(route, args) {
if (route === '/longpolling/poll') {
assert.step('slave - ' + route + ' - ' + args.channels.join(','));
pollPromiseSlave = testUtils.makeTestPromise();
pollPromiseSlave.abort = (function () {
this.reject({message: "XmlHttpRequestError abort"}, $.Event());
}).bind(pollPromiseSlave);
return pollPromiseSlave;
}
return this._super.apply(this, arguments);
}
});
var slave = new Widget(parentSlave);
await slave.appendTo($('#qunit-fixture'));
slave.call('bus_service', 'onNotification', slave, function (notifications) {
slaveEnv.services['bus_service'].onNotification(null, notifications => {
assert.step('slave - notification - ' + notifications.toString());
});
slave.call('bus_service', 'addChannel', 'lambda');
slaveEnv.services['bus_service'].addChannel('lambda');
pollPromiseMaster.resolve([{
id: 1,
channel: 'lambda',
@@ -334,7 +237,7 @@ QUnit.module('Bus', {
await testUtils.nextTick();
// simulate unloading master
master.call('bus_service', '_onUnload');
masterEnv.services['bus_service']._onUnload();
pollPromiseSlave.resolve([{
id: 2,
@@ -352,73 +255,46 @@ QUnit.module('Bus', {
'slave - notification - gamma',
'slave - /longpolling/poll - lambda',
]);
parentMaster.destroy();
parentSlave.destroy();
});
QUnit.test('two tabs calling addChannel simultaneously', async function (assert) {
assert.expect(5);
let id = 1;
testUtils.mock.patch(CrossTabBus, {
init: function () {
this._super.apply(this, arguments);
this.__tabId__ = id++;
},
addChannel: function (channel) {
patchWithCleanup(CrossTab.prototype, {
addChannel(channel) {
assert.step('Tab ' + this.__tabId__ + ': addChannel ' + channel);
this._super.apply(this, arguments);
},
deleteChannel: function (channel) {
deleteChannel(channel) {
assert.step('Tab ' + this.__tabId__ + ': deleteChannel ' + channel);
this._super.apply(this, arguments);
},
});
let pollPromise;
const parentTab1 = new Widget();
await testUtils.mock.addMockEnvironment(parentTab1, {
data: {},
services: {
local_storage: LocalStorageServiceMock,
},
mockRPC: function (route) {
const firstTabEnv = await makeTestEnv({
mockRPC(route) {
if (route === '/longpolling/poll') {
pollPromise = testUtils.makeTestPromise();
pollPromise.abort = (function () {
this.reject({message: "XmlHttpRequestError abort"}, $.Event());
}).bind(pollPromise);
return pollPromise;
}
return this._super.apply(this, arguments);
}
});
const parentTab2 = new Widget();
await testUtils.mock.addMockEnvironment(parentTab2, {
data: {},
services: {
local_storage: LocalStorageServiceMock,
},
mockRPC: function (route) {
if (route === '/longpolling/poll') {
pollPromise = testUtils.makeTestPromise();
pollPromise.abort = (function () {
this.reject({message: "XmlHttpRequestError abort"}, $.Event());
}).bind(pollPromise);
return pollPromise;
}
return this._super.apply(this, arguments);
}
});
const tab1 = new CrossTabBus(parentTab1);
const tab2 = new CrossTabBus(parentTab2);
tab1.addChannel("alpha");
tab2.addChannel("alpha");
tab1.addChannel("beta");
tab2.addChannel("beta");
const secondTabEnv = await makeTestEnv({
mockRPC(route) {
if (route === '/longpolling/poll') {
pollPromise = testUtils.makeTestPromise();
return pollPromise;
}
}
});
firstTabEnv.services['bus_service'].__tabId__ = 1;
secondTabEnv.services['bus_service'].__tabId__ = 2;
firstTabEnv.services['bus_service'].addChannel('alpha');
secondTabEnv.services['bus_service'].addChannel('alpha');
firstTabEnv.services['bus_service'].addChannel('beta');
secondTabEnv.services['bus_service'].addChannel('beta');
assert.verifySteps([
"Tab 1: addChannel alpha",
@@ -426,10 +302,6 @@ QUnit.module('Bus', {
"Tab 1: addChannel beta",
"Tab 2: addChannel beta",
]);
testUtils.mock.unpatch(CrossTabBus);
parentTab1.destroy();
parentTab2.destroy();
});
});
@@ -4,26 +4,23 @@ import { browser } from "@web/core/browser/browser";
import { ConnectionLostError } from "@web/core/network/rpc_service";
import { registry } from "@web/core/registry";
const { Component } = owl;
export const calendarNotificationService = {
dependencies: ["action", "notification", "rpc"],
dependencies: ["action", "bus_service", "notification", "rpc"],
start(env, { action, notification, rpc }) {
start(env, { action, bus_service, notification, rpc }) {
let calendarNotifTimeouts = {};
let nextCalendarNotifTimeout = null;
const displayedNotifications = new Set();
env.bus.on("WEB_CLIENT_READY", null, async () => {
const legacyEnv = Component.env;
legacyEnv.services.bus_service.onNotification(this, (notifications) => {
bus_service.onNotification(this, (notifications) => {
for (const { payload, type } of notifications) {
if (type === "calendar.alarm") {
displayCalendarNotification(payload);
}
}
});
legacyEnv.services.bus_service.startPolling();
bus_service.startPolling();
});
/**
@@ -1,9 +1,6 @@
/** @odoo-module */
import * as legacyRegistry from "web.Registry";
import * as BusService from "bus.BusService";
import * as RamStorage from "web.RamStorage";
import * as AbstractStorageService from "web.AbstractStorageService";
import { busService } from "@bus/services/bus_service";
import { createWebClient } from "@web/../tests/webclient/helpers";
import { calendarNotificationService } from "@calendar/js/services/calendar_notification_service";
@@ -11,9 +8,6 @@ import { click, getFixture, nextTick, patchWithCleanup } from "@web/../tests/hel
import { browser } from "@web/core/browser/browser";
import { registry } from "@web/core/registry";
const LocalStorageService = AbstractStorageService.extend({
storage: new RamStorage(),
});
const serviceRegistry = registry.category("services");
QUnit.module("Calendar Notification", (hooks) => {
@@ -22,15 +16,12 @@ QUnit.module("Calendar Notification", (hooks) => {
hooks.beforeEach(() => {
target = getFixture();
legacyServicesRegistry = new legacyRegistry();
legacyServicesRegistry.add("bus_service", BusService);
legacyServicesRegistry.add("local_storage", LocalStorageService);
serviceRegistry.add("calendarNotification", calendarNotificationService);
serviceRegistry.add("bus_service", busService);
patchWithCleanup(browser, {
setTimeout: (fn) => fn(),
clearTimeout: () => {},
setTimeout(fn) {
this._super(fn, 0);
},
});
});
@@ -4,12 +4,11 @@ import { Markup } from 'web.utils';
import { registry } from "@web/core/registry";
export const iapNotificationService = {
dependencies: ["notification"],
dependencies: ["bus_service", "notification"],
start(env, { notification }) {
start(env, { bus_service, notification }) {
env.bus.on("WEB_CLIENT_READY", null, async () => {
const legacyEnv = owl.Component.env;
legacyEnv.services.bus_service.onNotification(this, (notifications) => {
bus_service.onNotification(this, (notifications) => {
for (const { payload, type } of notifications) {
if (type === 'iap_notification') {
if (payload.error_type == 'success') {
@@ -20,7 +19,7 @@ export const iapNotificationService = {
}
}
});
legacyEnv.services.bus_service.startPolling();
bus_service.startPolling();
});
/**
+6 -6
View File
@@ -182,9 +182,9 @@ Help your customers with this chat, and analyse their feedback.
'web/static/src/legacy/js/services/ajax_service.js',
'web/static/src/legacy/js/services/local_storage_service.js',
# Bus, Mail, Livechat
'bus/static/src/js/longpolling_bus.js',
'bus/static/src/js/crosstab_bus.js',
'bus/static/src/js/services/bus_service.js',
'bus/static/src/longpolling_bus.js',
'bus/static/src/crosstab_bus.js',
'bus/static/src/services/bus_service.js',
'mail/static/src/js/utils.js',
'im_livechat/static/src/legacy/public_livechat_history_tracking.js',
'im_livechat/static/src/legacy/public_livechat_chatbot.js',
@@ -207,9 +207,9 @@ Help your customers with this chat, and analyse their feedback.
('include', 'mail.assets_messaging'),
'mail/static/src/services/messaging_service.js',
# Framework JS
'bus/static/src/js/*.js',
'bus/static/src/js/services/bus_service.js',
'bus/static/src/js/services/legacy/legacy_bus_service.js',
'bus/static/src/*.js',
'bus/static/src/services/bus_service.js',
'bus/static/src/services/legacy/make_bus_service_to_legacy_env.js',
'web/static/lib/luxon/luxon.js',
'web/static/src/core/**/*',
# FIXME: debug menu currently depends on webclient, once it doesn't we don't need to remove the contents of the debug folder
+2 -1
View File
@@ -22,5 +22,6 @@ const messagingValuesService = {
const serviceRegistry = registry.category('services');
serviceRegistry.add('messaging', messagingService);
serviceRegistry.add('messagingValues', messagingValuesService);
serviceRegistry.add('messaging_service_to_legacy_env', makeMessagingToLegacyEnv(owl.Component.env));
serviceRegistry.add('public_livechat_service', publicLivechatService);
registry.category('wowlToLegacyServiceMappers').add('make_messaging_to_legacy_env', makeMessagingToLegacyEnv);
+3 -3
View File
@@ -95,9 +95,9 @@
'mail/static/src/services/*.js',
'mail/static/src/utils/*.js',
# Framework JS
'bus/static/src/js/*.js',
'bus/static/src/js/services/bus_service.js',
'bus/static/src/js/services/legacy/legacy_bus_service.js',
'bus/static/src/*.js',
'bus/static/src/services/bus_service.js',
'bus/static/src/services/legacy/make_bus_service_to_legacy_env.js',
'web/static/lib/luxon/luxon.js',
'web/static/src/core/**/*',
# FIXME: debug menu currently depends on webclient, once it doesn't we don't need to remove the contents of the debug folder
@@ -31,7 +31,7 @@ export class NotificationRequest extends Component {
_handleResponseNotificationPermission(value) {
this.messaging.refreshIsNotificationPermissionDefault();
if (value !== 'granted') {
this.env.services['legacy_bus_service'].sendNotification({
this.env.services['bus_service'].sendNotification({
message: this.env._t("Odoo will not have the permission to send native notifications on this device."),
title: this.env._t("Permission denied"),
});
+3 -3
View File
@@ -18,8 +18,8 @@ registerModel({
odoo.__DEBUG__.messaging = this;
},
_willDelete() {
if (this.env.services['legacy_bus_service']) {
this.env.services['legacy_bus_service'].off('window_focus', null, this._handleGlobalWindowFocus);
if (this.env.services['bus_service']) {
this.env.services['bus_service'].off('window_focus', null, this._handleGlobalWindowFocus);
}
delete odoo.__DEBUG__.messaging;
},
@@ -29,7 +29,7 @@ registerModel({
* Starts messaging and related records.
*/
async start() {
this.env.services['legacy_bus_service'].on('window_focus', null, this._handleGlobalWindowFocus);
this.env.services['bus_service'].on('window_focus', null, this._handleGlobalWindowFocus);
await this.initializer.start();
if (!this.exists()) {
return;
@@ -16,9 +16,9 @@ registerModel({
identifyingFields: ['messaging'],
lifecycleHooks: {
_willDelete() {
if (this.env.services['legacy_bus_service']) {
this.env.services['legacy_bus_service'].off('notification');
this.env.services['legacy_bus_service'].stopPolling();
if (this.env.services['bus_service']) {
this.env.services['bus_service'].off('notification');
this.env.services['bus_service'].stopPolling();
}
},
},
@@ -28,8 +28,8 @@ registerModel({
* the current users. This includes pinned channels for instance.
*/
start() {
this.env.services.legacy_bus_service.onNotification(null, notifs => this._handleNotifications(notifs));
this.env.services.legacy_bus_service.startPolling();
this.env.services['bus_service'].onNotification(null, notifs => this._handleNotifications(notifs));
this.env.services['bus_service'].startPolling();
},
/**
* @private
@@ -269,7 +269,7 @@ registerModel({
channel.correspondent === this.messaging.partnerRoot
);
if (!isChatWithOdooBot) {
const isOdooFocused = this.env.services['legacy_bus_service'].isOdooFocused();
const isOdooFocused = this.env.services['bus_service'].isOdooFocused();
// Notify if out of focus
if (!isOdooFocused && channel.isChatChannel) {
this._notifyNewChannelMessageWhileOutOfFocus({
@@ -620,7 +620,7 @@ registerModel({
// then open a chat for the current user with the new user.
const message = sprintf(this.env._t('%s connected'), username);
const title = this.env._t("This is their first connection. Wish them luck.");
this.env.services['legacy_bus_service'].sendNotification({ message, title, type: 'info' });
this.env.services['bus_service'].sendNotification({ message, title, type: 'info' });
const chat = await this.messaging.getChat({ partnerId });
if (!this.exists() || !chat || this.messaging.device.isSmall) {
return;
@@ -662,7 +662,7 @@ registerModel({
const notificationContent = escape(
htmlToTextContentInline(message.body).substr(0, PREVIEW_MSG_MAX_SIZE)
);
this.env.services['legacy_bus_service'].sendNotification({
this.env.services['bus_service'].sendNotification({
message: notificationContent,
title: notificationTitle,
type: 'info',
@@ -1,7 +1,7 @@
/** @odoo-module **/
import { data } from 'mail.discuss_public_channel_template';
import { makeMessagingToLegacyEnv } from '@mail/utils/make_messaging_to_legacy_env';
import { messagingToLegacyEnv } from '@mail/utils/make_messaging_to_legacy_env';
import { DialogManagerContainer } from '@mail/components/dialog_manager_container/dialog_manager_container';
import { DiscussPublicViewContainer } from '@mail/components/discuss_public_view_container/discuss_public_view_container';
@@ -46,11 +46,12 @@ Component.env = legacyEnv;
serviceRegistry.add('legacy_notification', makeLegacyNotificationService(Component.env));
serviceRegistry.add('legacy_crash_manager', makeLegacyCrashManagerService(Component.env));
serviceRegistry.add('legacy_dialog_mapping', makeLegacyDialogMappingService(Component.env));
serviceRegistry.add('messaging_service_to_legacy_env', makeMessagingToLegacyEnv(Component.env));
serviceRegistry.add('messaging', messagingService);
serviceRegistry.add('messagingValues', messagingValuesService);
registry.category('wowlToLegacyServiceMappers').add('make_messaging_to_legacy_env', messagingToLegacyEnv);
const mainComponentsRegistry = registry.category('main_components');
mainComponentsRegistry.add('DiscussPublicViewContainer', { Component: DiscussPublicViewContainer });
// needed by the attachment viewer
@@ -5,7 +5,7 @@ import { ModelManager } from '@mail/model/model_manager';
export const messagingService = {
dependencies: [
'effect',
'legacy_bus_service',
'bus_service',
'localization',
'messagingValues',
'orm',
@@ -566,7 +566,6 @@ async function start(param0 = {}) {
registerCleanup(async () => {
await webClient.env.services.messaging.modelManager.messagingInitializedPromise;
webClient.env.services.messaging.modelManager.destroy();
webClient.env.services.legacy_bus_service.destroy();
delete webClient.env.services.messaging;
delete owl.Component.env.services.messaging;
delete owl.Component.env[wowlServicesSymbol].messaging;
@@ -1,8 +1,7 @@
/** @odoo-module **/
import { legacyBusService } from '@bus/js/services/legacy/legacy_bus_service';
import BusService from 'bus.BusService';
import { busService } from '@bus/services/bus_service';
import { makeBusServiceToLegacyEnv } from '@bus/services/legacy/make_bus_service_to_legacy_env';
import { ChatWindowManagerContainer } from '@mail/components/chat_window_manager_container/chat_window_manager_container';
import { DialogManagerContainer } from '@mail/components/dialog_manager_container/dialog_manager_container';
@@ -16,10 +15,6 @@ import { registry } from '@web/core/registry';
import { patchWithCleanup } from "@web/../tests/helpers/utils";
import { createWebClient } from "@web/../tests/webclient/helpers";
import AbstractStorageService from 'web.AbstractStorageService';
import RamStorage from 'web.RamStorage';
import LegacyRegistry from 'web.Registry';
const WEBCLIENT_LOAD_ROUTES = [
'/web/webclient/load_menus',
'/web/dataset/call_kw/res.users/load_views',
@@ -46,7 +41,6 @@ const SERVICES_PARAMETER_NAMES = new Set([
*
* @param {Object} param0
* @param {Object} [param0.services]
* @param {Object} [param0.legacyServices]
* @param {number} [param0.loadingBaseDelayDuration=0]
* @param {Promise} [param0.messagingBeforeCreationDeferred=Promise.resolve()]
* Deferred that let tests block messaging creation and simulate resolution.
@@ -58,7 +52,6 @@ const SERVICES_PARAMETER_NAMES = new Set([
* to the webClient as a legacy parameter.
*/
function setupMessagingServiceRegistries({
legacyServices,
loadingBaseDelayDuration = 0,
messagingBeforeCreationDeferred = Promise.resolve(),
messagingBus,
@@ -66,20 +59,6 @@ function setupMessagingServiceRegistries({
testSetupDoneDeferred,
}) {
const serviceRegistry = registry.category('services');
const legacyServiceRegistry = new LegacyRegistry();
legacyServices = {
bus_service: BusService.extend({
_beep() {}, // Do nothing
_poll() {}, // Do nothing
_registerWindowUnload() {}, // Do nothing
isOdooFocused() {
return true;
},
updateOption() {},
}),
local_storage: AbstractStorageService.extend({ storage: new RamStorage() }),
...legacyServices,
};
patchWithCleanup(messagingService, {
async _startModelManager() {
@@ -101,31 +80,36 @@ function setupMessagingServiceRegistries({
}
};
const customBusService = {
start() {
const originalService = busService.start(...arguments);
Object.assign(originalService, {
_beep() {}, // Do nothing
_poll() {}, // Do nothing
_registerWindowUnload() {}, // Do nothing
isOdooFocused() {
return true;
},
updateOption() {},
});
return originalService;
},
};
services = {
bus_service: customBusService,
messaging: messagingService,
messagingValues,
systrayService,
...services,
};
// during tests, we need to wait for the legacyBusService to be added before adding the messaging
// service to the registry or it will result in an error during the webClient creation.
const addMessagingToRegistryFn = ev => {
const { key, operation } = ev.detail;
if (key === 'legacy_bus_service' && operation === 'add') {
serviceRegistry.add('messaging', messagingService);
serviceRegistry.add('systray_service', systrayService);
serviceRegistry.add('messaging_service_to_legacy_env', makeMessagingToLegacyEnv(owl.Component.env));
serviceRegistry.removeEventListener('UPDATE', addMessagingToRegistryFn);
}
};
serviceRegistry.addEventListener('UPDATE', addMessagingToRegistryFn);
Object.entries(services).forEach(([serviceName, service]) => {
serviceRegistry.add(serviceName, service);
});
Object.entries(legacyServices).forEach(([serviceName, service]) => {
legacyServiceRegistry.add(serviceName, service);
});
return legacyServiceRegistry;
registry.category('wowlToLegacyServiceMappers').add('bus_service_to_legacy_env', makeBusServiceToLegacyEnv);
registry.category('wowlToLegacyServiceMappers').add('messaging_service_to_legacy_env', makeMessagingToLegacyEnv);
}
/**
@@ -135,8 +119,6 @@ function setupMessagingServiceRegistries({
* @param {Object} param0
* @param {Object} [param0.serverData]
* @param {Object} [param0.services]
* @param {Object} [param0.legacyServices]
* @param {Object} [param0.legacyParams={}]
* @param {Object} [param0.loadingBaseDelayDuration]
* @param {Object} [param0.messagingBeforeCreationDeferred]
* @param {Promise} [param0.testSetupDoneDeferred] The Promise to whose revolution has to be
@@ -154,7 +136,7 @@ function setupMessagingServiceRegistries({
servicesParameters[parameterName] = value;
}
}
const legacyServiceRegistry = setupMessagingServiceRegistries(servicesParameters);
setupMessagingServiceRegistries(servicesParameters);
const webClientParameters = {};
for (const [parameterName, value] of param0Entries) {
@@ -162,15 +144,7 @@ function setupMessagingServiceRegistries({
webClientParameters[parameterName] = value;
}
}
webClientParameters.legacyParams = {
legacyServicesToDeployInWowlEnv: {
legacy_bus_service: legacyBusService,
},
serviceRegistry: legacyServiceRegistry,
...webClientParameters.legacyParams,
};
const webClient = await createWebClient(webClientParameters);
return webClient;
return createWebClient(webClientParameters);
}
export {
@@ -1,7 +1,5 @@
/** @odoo-module **/
import BusService from 'bus.BusService';
import {
afterNextRender,
isScrolledToBottom,
@@ -14,6 +12,7 @@ import { makeFakeNotificationService } from "@web/../tests/helpers/mock_services
import { destroy } from '@web/../tests/helpers/utils';
import { makeTestPromise, file } from 'web.test_utils';
import { busService } from '@bus/services/bus_service';
const { createFile, inputFiles } = file;
@@ -3561,15 +3560,23 @@ QUnit.test('receive new chat message: out of odoo focus (notification, channel)'
const pyEnv = await startServer();
const mailChannelId1 = pyEnv['mail.channel'].create({ channel_type: 'chat' });
const { env, openDiscuss } = await start({
legacyServices: {
bus_service: BusService.extend({
const customBusService = {
...busService,
start() {
const originalBusService = busService.start(...arguments);
Object.assign(originalBusService, {
_beep() {}, // Do nothing
_poll() {}, // Do nothing
_registerWindowUnload() {}, // Do nothing
isOdooFocused: () => false,
updateOption() {},
}),
});
return originalBusService;
},
};
const { env, openDiscuss } = await start({
services: {
bus_service: customBusService,
},
});
await openDiscuss();
@@ -3599,15 +3606,23 @@ QUnit.test('receive new chat message: out of odoo focus (notification, chat)', a
const pyEnv = await startServer();
const mailChannelId1 = pyEnv['mail.channel'].create({ channel_type: "chat" });
const { env, openDiscuss } = await start({
legacyServices: {
bus_service: BusService.extend({
const customBusService = {
...busService,
start() {
const originalBusService = busService.start(...arguments);
Object.assign(originalBusService, {
_beep() {}, // Do nothing
_poll() {}, // Do nothing
_registerWindowUnload() {}, // Do nothing
isOdooFocused: () => false,
updateOption() {},
}),
});
return originalBusService;
},
};
const { env, openDiscuss } = await start({
services: {
bus_service: customBusService,
},
});
await openDiscuss();
@@ -3641,15 +3656,23 @@ QUnit.test('receive new chat messages: out of odoo focus (tab title)', async fun
{ channel_type: 'chat', public: 'private' },
{ channel_type: 'chat', public: 'private' },
]);
const { env, openDiscuss } = await start({
legacyServices: {
bus_service: BusService.extend({
const customBusService = {
...busService,
start() {
const originalBusService = busService.start(...arguments);
Object.assign(originalBusService, {
_beep() {}, // Do nothing
_poll() {}, // Do nothing
_registerWindowUnload() {}, // Do nothing
isOdooFocused: () => false,
updateOption() {},
}),
});
return originalBusService;
},
};
const { env, openDiscuss } = await start({
services: {
bus_service: customBusService,
},
});
await openDiscuss();
+1 -1
View File
@@ -112,7 +112,7 @@
('include', 'web.assets_backend'),
# Remove Longpolling bus and packages needed this bus
('remove', 'bus/static/src/js/services/assets_watchdog_service.js'),
('remove', 'bus/static/src/services/assets_watchdog_service.js'),
('remove', 'mail/static/src/services/messaging_service.js'),
('remove', 'mail/static/src/components/dialog_manager/dialog_manager.js'),
@@ -73,6 +73,9 @@ export function makeRAMLocalStorage() {
return {
setItem(key, value) {
store[key] = value;
window.dispatchEvent(
new StorageEvent('storage', { key, newValue: value })
);
},
getItem(key) {
return store[key];
@@ -82,6 +85,9 @@ export function makeRAMLocalStorage() {
},
removeItem(key) {
delete store[key];
window.dispatchEvent(
new StorageEvent('storage', { key, newValue: null })
);
},
get length() {
return Object.keys(store).length;
+7 -9
View File
@@ -34,15 +34,13 @@ function _genericJsonRpc (fct_name, params, settings, fct) {
var result = xhr.then(function(result) {
core.bus.trigger('rpc:result', data, result);
if (result.error !== undefined) {
if (result.error.data.arguments[0] !== "bus.Bus not available in test mode") {
console.debug(
"Server application error\n",
"Error code:", result.error.code, "\n",
"Error message:", result.error.message, "\n",
"Error data message:\n", result.error.data.message, "\n",
"Error data debug:\n", result.error.data.debug
);
}
console.debug(
"Server application error\n",
"Error code:", result.error.code, "\n",
"Error message:", result.error.message, "\n",
"Error data message:\n", result.error.data.message, "\n",
"Error data debug:\n", result.error.data.debug
);
return Promise.reject({type: "server", error: result.error});
} else {
return result.result;
@@ -377,6 +377,10 @@ export async function createPublicRoot(RootWidget) {
serviceRegistry.add("legacy_notification", makeLegacyNotificationService(legacyEnv));
serviceRegistry.add("legacy_dialog_mapping", makeLegacyDialogMappingService(legacyEnv));
serviceRegistry.add("legacy_rainbowman_service", makeLegacyRainbowManService(legacyEnv));
const wowlToLegacyServiceMappers = registry.category('wowlToLegacyServiceMappers').getEntries();
for (const [legacyServiceName, wowlToLegacyServiceMapper] of wowlToLegacyServiceMappers) {
serviceRegistry.add(legacyServiceName, wowlToLegacyServiceMapper(legacyEnv));
}
await Promise.all([whenReady(), session.is_bound]);
// Patch browser.fetch and the rpc service to use the correct base url when
@@ -46,6 +46,10 @@ export const legacySetupProm = new Promise((resolve) => {
const legacyCommandService = makeLegacyCommandService(legacyEnv);
serviceRegistry.add("legacy_command", legacyCommandService);
serviceRegistry.add("legacy_dropdown", makeLegacyDropdownService(legacyEnv));
const wowlToLegacyServiceMappers = registry.category('wowlToLegacyServiceMappers').getEntries();
for (const [legacyServiceName, wowlToLegacyServiceMapper] of wowlToLegacyServiceMappers) {
serviceRegistry.add(legacyServiceName, wowlToLegacyServiceMapper(legacyEnv));
}
await Promise.all([whenReady(), session.is_bound]);
legacyEnv.templates = session.owlTemplates;
legacySetupResolver(legacyEnv);
@@ -53,6 +53,7 @@ function prepareRegistriesWithCleanup() {
cloneRegistryWithCleanup(registry.category("command_setup"));
cloneRegistryWithCleanup(registry.category("view_widgets"));
cloneRegistryWithCleanup(registry.category("fields"));
cloneRegistryWithCleanup(registry.category('wowlToLegacyServiceMappers'));
cloneRegistryWithCleanup(registry.category("main_components"));
cloneRegistryWithCleanup(registry.category("fields"));
@@ -63,6 +64,7 @@ function prepareRegistriesWithCleanup() {
clearRegistryWithCleanup(registry.category("error_dialogs"));
clearRegistryWithCleanup(registry.category("favoriteMenu"));
clearRegistryWithCleanup(registry.category("ir.actions.report handlers"));
clearRegistryWithCleanup(registry.category('wowlToLegacyServiceMappers'));
clearRegistryWithCleanup(registry.category("services"));
clearServicesMetadataWithCleanup();
@@ -12,6 +12,7 @@ import { registerCleanup } from "./cleanup";
import { patchWithCleanup } from "./utils";
import { companyService } from "@web/webclient/company_service";
import { uiService } from "@web/core/ui/ui_service";
import { ConnectionAbortedError } from "../../src/core/network/rpc_service";
const { Component, status } = owl;
@@ -60,7 +61,16 @@ export function makeFakeRPCService(mockRPC) {
return {
name: "rpc",
start() {
return buildMockRPC(mockRPC);
const rpcService = buildMockRPC(mockRPC);
return function () {
let rejectFn;
const rpcProm = new Promise((resolve, reject) => {
rejectFn = reject;
resolve(rpcService(...arguments));
});
rpcProm.abort = () => rejectFn(new ConnectionAbortedError("XmlHttpRequestError abort"));
return rpcProm;
};
},
specializeForComponent: rpcService.specializeForComponent,
};
+11
View File
@@ -117,6 +117,8 @@ function makeMockLocation(hasListeners = () => true) {
function patchBrowserWithCleanup() {
const originalAddEventListener = browser.addEventListener;
const originalRemoveEventListener = browser.removeEventListener;
const originalSetTimeout = browser.setTimeout;
const originalClearTimeout = browser.clearTimeout;
let hasHashChangeListeners = false;
const mockLocation = makeMockLocation(() => hasHashChangeListeners);
@@ -134,6 +136,15 @@ function patchBrowserWithCleanup() {
originalRemoveEventListener(...arguments);
});
},
// patch setTimeout to automatically remove timeouts bound (via
// browser.setTimeout) during a test (e.g. during the deployment of a service)
setTimeout() {
const timeout = originalSetTimeout(...arguments);
registerCleanup(() => {
originalClearTimeout(timeout);
});
return timeout;
},
navigator: {
userAgent: browser.navigator.userAgent.replace(/\([^)]*\)/, "(X11; Linux x86_64)"),
},
+4 -4
View File
@@ -194,10 +194,10 @@ export async function addLegacyMockEnvironment(env, legacyParams = {}) {
const legacyActionManagerService = makeLegacyActionManagerService(legacyEnv);
serviceRegistry.add("legacy_action_manager", legacyActionManagerService);
serviceRegistry.add("legacy_notification", makeLegacyNotificationService(legacyEnv));
// deploy the legacyServices given in parameter in WowlEnv
const { legacyServicesToDeployInWowlEnv } = legacyParams;
for (const legacyServiceName in legacyServicesToDeployInWowlEnv) {
serviceRegistry.add(legacyServiceName, legacyServicesToDeployInWowlEnv[legacyServiceName]);
// deploy wowl services into the legacy env.
const wowlToLegacyServiceMappers = registry.category('wowlToLegacyServiceMappers').getEntries();
for (const [legacyServiceName, wowlToLegacyServiceMapper] of wowlToLegacyServiceMappers) {
serviceRegistry.add(legacyServiceName, wowlToLegacyServiceMapper(legacyEnv));
}
// patch DebouncedField delay
const debouncedField = basicFields.DebouncedField;