Files
odoo_source/addons/bus/static/tests/bus_tests.js
T
tsm-odoo 252f7f5f82 [IMP] bus: lazy start of the websocket
Before [1], the bus was started lazily: either as a consequence of
the addition of a channel to listen to or by manually calling the
`startPolling` method.

Before this commit, the websocket would have been started as soon as
the bus service starts which degrades performances.

This PR fixes the issue by re-introducing the same mechanism as before
that is by starting the websocket either by calling manually the `start`
method of the bus service or automatically when adding a channel.

[1]: odoo#75510

closes odoo/odoo#107878

X-original-commit: 5d7deacf54f37f0938b92a3c45c9f1d1325d1a9f
Signed-off-by: Alexandre Kühn (aku) <aku@odoo.com>
Signed-off-by: Stockbauer Matthieu (tsm) <tsm@odoo.com>
2022-12-14 14:19:58 +01:00

467 lines
18 KiB
JavaScript

odoo.define('web.bus_tests', function (require) {
"use strict";
var { busService } = require('@bus/services/bus_service');
const { presenceService } = require('@bus/services/presence_service');
const { multiTabService } = require('@bus/multi_tab_service');
const { WEBSOCKET_CLOSE_CODES } = require("@bus/workers/websocket_worker");
const { startServer } = require('@bus/../tests/helpers/mock_python_environment');
const { patchWebsocketWorkerWithCleanup } = require("@bus/../tests/helpers/mock_websocket");
const { browser } = require("@web/core/browser/browser");
const { registry } = require("@web/core/registry");
const { session } = require('@web/session');
const { makeDeferred, nextTick, patchWithCleanup } = require("@web/../tests/helpers/utils");
const { makeTestEnv } = require('@web/../tests/helpers/mock_env');
QUnit.module('Bus', {
beforeEach: function () {
const customMultiTabService = {
...multiTabService,
start() {
const originalMultiTabService = multiTabService.start(...arguments);
originalMultiTabService.TAB_HEARTBEAT_PERIOD = 10;
originalMultiTabService.MAIN_TAB_HEARTBEAT_PERIOD = 1;
return originalMultiTabService;
},
};
registry.category('services').add('bus_service', busService);
registry.category('services').add('presence', presenceService);
registry.category('services').add('multi_tab', customMultiTabService);
},
}, function () {
QUnit.test('notifications received from the channel', async function (assert) {
assert.expect(3);
const pyEnv = await startServer();
const env = await makeTestEnv({ activateMockServer: true });
env.services['bus_service'].addEventListener('notification', ({ detail: notifications }) => {
assert.step('notification - ' + notifications.map(notif => notif.payload).toString());
});
env.services['bus_service'].addChannel('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',
]);
});
QUnit.test('notifications not received after stoping the service', async function (assert) {
assert.expect(4);
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('1 - notification - ' + notifications.map(notif => notif.payload).toString());
});
firstTabEnv.services['bus_service'].addChannel('lambda');
secondTabEnv.services['bus_service'].addEventListener('notification', ({ detail: notifications }) => {
assert.step('2 - notification - ' + notifications.map(notif => notif.payload).toString());
});
// both tabs should receive the notification
pyEnv['bus.bus']._sendone('lambda', 'notifType', 'beta');
await nextTick();
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',
]);
});
QUnit.test('notifications still received after disconnect/reconnect', async function (assert) {
assert.expect(3);
const oldSetTimeout = window.setTimeout;
patchWithCleanup(
window,
{
setTimeout: callback => oldSetTimeout(callback, 0)
},
{ pure: true },
)
const pyEnv = await startServer();
const env = await makeTestEnv({ activateMockServer: true });
env.services["bus_service"].start();
await nextTick();
env.services['bus_service'].addEventListener('notification', ({ detail: notifications }) => {
assert.step('notification - ' + notifications.map(notif => notif.payload).toString());
});
env.services['bus_service'].addChannel('lambda');
pyEnv['bus.bus']._sendone('lambda', '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",
]);
});
QUnit.test('tabs share message from a channel', async function (assert) {
assert.expect(1);
const pyEnv = await startServer();
const steps = new Set();
// main
const mainEnv = await makeTestEnv({ activateMockServer: true });
mainEnv.services['bus_service'].addEventListener('notification', ({ detail: notifications }) => {
steps.add('main - notification - ' + notifications.map(notif => notif.payload).toString());
});
mainEnv.services['bus_service'].addChannel('lambda');
// slave
const slaveEnv = await makeTestEnv();
slaveEnv.services['bus_service'].addEventListener('notification', ({ detail: notifications }) => {
steps.add('slave - notification - ' + notifications.map(notif => notif.payload).toString());
});
slaveEnv.services['bus_service'].addChannel('lambda');
pyEnv['bus.bus']._sendone('lambda', 'notifType', 'beta');
await nextTick();
assert.deepEqual(
[...steps],
["slave - notification - beta", "main - notification - beta"]
);
});
QUnit.test('second tab still receives notifications after main pagehide', async function (assert) {
assert.expect(1);
const pyEnv = await startServer();
const steps = new Set();
// main
const mainEnv = await makeTestEnv({ activateMockServer: true });
mainEnv.services['bus_service'].addEventListener('notification', ({ detail: notifications }) => {
steps.add('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) {
if (eventName === 'pagehide') {
return;
}
this._super(eventName, callback);
},
});
const secondEnv = await makeTestEnv({ activateMockServer: true });
secondEnv.services['bus_service'].addEventListener('notification', ({ detail: notifications }) => {
steps.add('slave - notification - ' + notifications.map(notif => notif.payload).toString());
});
secondEnv.services['bus_service'].addChannel('lambda');
pyEnv['bus.bus']._sendone('lambda', 'notifType', 'beta');
await nextTick();
// simulate unloading main
window.dispatchEvent(new Event('pagehide'));
await nextTick();
pyEnv['bus.bus']._sendone('lambda', 'notifType', 'gamma');
await nextTick();
assert.deepEqual(
[...steps],
[
'slave - notification - beta',
'main - notification - beta',
'slave - notification - gamma',
]);
});
QUnit.test('two tabs calling addChannel simultaneously', async function (assert) {
assert.expect(5);
const channelPatch = {
addChannel(channel) {
assert.step('Tab ' + this.__tabId__ + ': addChannel ' + channel);
this._super.apply(this, arguments);
},
deleteChannel(channel) {
assert.step('Tab ' + this.__tabId__ + ': deleteChannel ' + channel);
this._super.apply(this, 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');
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) {
assert.expect(3);
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 nextTick();
pyEnv['bus.bus']._sendmany([
['alpha', 'notifType', 'alpha'],
['beta', 'notifType', 'beta']
]);
await nextTick();
assert.verifySteps([
'first - notification - alpha,beta',
'second - notification - alpha,beta',
]);
});
QUnit.test('channel management from multiple tabs', async function (assert) {
assert.expect(3);
patchWebsocketWorkerWithCleanup({
_sendToServer({ event_name, data }) {
assert.step(`${event_name} - [${data.channels.toString()}]`);
},
});
const firstTabEnv = await makeTestEnv();
const secTabEnv = await makeTestEnv();
firstTabEnv.services['bus_service'].addChannel('channel1');
await nextTick();
// this should not trigger a subscription since the channel1 was
// aleady known.
secTabEnv.services['bus_service'].addChannel('channel1');
await nextTick();
// 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();
// this should trigger a subscription since the channel2 was not
// known.
secTabEnv.services['bus_service'].addChannel('channel2');
await nextTick();
assert.verifySteps([
'subscribe - [channel1]',
'subscribe - [channel1,channel2]',
]);
});
QUnit.test('channels subscription after disconnection', async function (assert) {
// Patch setTimeout in order for the worker to reconnect immediatly.
patchWithCleanup(window, {
setTimeout: fn => fn(),
});
const firstSubscribeDeferred = makeDeferred();
const worker = patchWebsocketWorkerWithCleanup({
_sendToServer({ event_name, data }) {
assert.step(`${event_name} - [${data.channels.toString()}]`);
if (event_name === 'subscribe') {
firstSubscribeDeferred.resolve();
}
},
});
const env = await makeTestEnv();
env.services["bus_service"].start();
// wait for the websocket to connect and the first subscription
// to occur.
await firstSubscribeDeferred;
worker.websocket.close(WEBSOCKET_CLOSE_CODES.KEEP_ALIVE_TIMEOUT);
// wait for the websocket to re-connect.
await nextTick();
assert.verifySteps([
'subscribe - []',
'subscribe - []',
]);
});
QUnit.test('Last notification id is passed to the worker on service start', async function (assert) {
const pyEnv = await startServer();
let updateLastNotificationDeferred = makeDeferred();
patchWebsocketWorkerWithCleanup({
_onClientMessage(_, { action, data }) {
if (action === 'initialize_connection') {
assert.step(`${action} - ${data['lastNotificationId']}`);
updateLastNotificationDeferred.resolve();
}
return this._super(...arguments);
},
});
await makeTestEnv();
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();
await makeTestEnv();
await updateLastNotificationDeferred;
// Second bus service sends the last known notification id.
assert.verifySteps([`initialize_connection - 1`]);
});
QUnit.test('Last notification id reset after db change', async function (assert) {
const pyEnv = await startServer();
let updateLastNotificationDeferred = makeDeferred();
patchWebsocketWorkerWithCleanup({
_onClientMessage(_, { action, data }) {
if (action === 'initialize_connection') {
assert.step(`${action} - ${data['lastNotificationId']}`);
updateLastNotificationDeferred.resolve();
}
return this._super(...arguments);
},
});
await makeTestEnv();
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();
// dbuuid change should reset last notification id.
patchWithCleanup(session, { dbuuid: 'ABCDE-FGHIJ-KLMNO' });
updateLastNotificationDeferred = makeDeferred();
await makeTestEnv();
await updateLastNotificationDeferred;
assert.verifySteps([`initialize_connection - 0`]);
});
QUnit.test('Websocket disconnects upon user log out', async function (assert) {
// first tab connects to the worker with user logged.
patchWithCleanup(session, {
user_id: 1,
});
const connectionInitializedDeferred = makeDeferred();
let connectionOpenedDeferred = makeDeferred();
patchWebsocketWorkerWithCleanup({
_initializeConnection(client, data) {
this._super(client, data);
connectionInitializedDeferred.resolve();
},
});
const firstTabEnv = await makeTestEnv();
firstTabEnv.services["bus_service"].start();
firstTabEnv.services['bus_service'].addEventListener('connect', () => {
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,
});
await makeTestEnv();
await nextTick();
assert.verifySteps([
'connect',
'disconnect',
]);
});
QUnit.test('Websocket reconnects upon user log in', async function (assert) {
// 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) {
this._super(client, data);
connectionInitializedDeferred.resolve();
},
});
const firstTabEnv = await makeTestEnv();
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;
// second tab connects to the worker after connection: user_id
// is now set.
patchWithCleanup(session, {
user_id: 1,
});
const env = await makeTestEnv();
env.services["bus_service"].start();
await websocketConnectedDeferred;
assert.verifySteps([
'connect',
'disconnect',
'connect',
]);
});
});
});