Files
odoo_source/addons/bus/static/tests/bus_tests.js
T
8f2c15a719 [FIX] bus: poll different channels in different tabs
Commit b743fda doesn't seem to work properly.
Steps to reproduce:
- Open a spreadsheet A in one tab
- Open a spreadsheet B in another tab
=> the master tab should poll both spreadsheet channels but it
does not.

The condition here sheems wrong
https://github.com/odoo/odoo/blob/bf2ce0aa0c7d45c90d7ff104d5af5f99b29d59ae/addons/bus/static/src/js/crosstab_bus.js#L299
`peerChannelsBefore` is not really "before the channel was added".
It's "before outdated channels are cleared".
It already contains the new channel added to local storage by the slave tab!
Hence it wrongly returns `false` which means the poll request is not
restarted.

Additional fix required since e2aeb5f
-------------------------------------

Commit e2aeb5f broke a little more the feature.
It is assumed the ids in `channels` are matching the ids in `lastPresenceByTab`
(previously named `peers`). It was no longer true since `lastPresenceByTab`
is now managed by multi_tab service (with it's own id).
With those non-matching ids, channels from other tabs were always considered
outdated and therefore cleaned up.

Small comment on the change in the mock server
----------------------------------------------

Cross tab bus tests were not working properly.
The error message string "XmlHttpRequestError abort" was stringified
(stringifying a string), leading to "\"XmlHttpRequestError abort\"". Since the
implementation depends on the exact error message, tests didn't propely
reflect reality.
This is technically not needed anymore since bus tests are no longer using
the legacy mock server but it can't hurt to fix it. (the new mock server
doesn't have this issue)

closes odoo/odoo#97036

X-original-commit: f25f36a
Signed-off-by: David Monjoie (dmo) <dmo@odoo.com>
Co-authored-by: Goaman <nby@odoo.com>
Co-authored-by: tsm-odoo <tsm@odoo.com>
2022-08-03 13:45:02 +02:00

349 lines
12 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');
var { CrossTab } = require('@bus/crosstab_bus');
var testUtils = require('web.test_utils');
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 { 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 longpolling channel', async function (assert) {
assert.expect(6);
var pollPromise = testUtils.makeTestPromise();
const env = await makeTestEnv({
mockRPC(route, args) {
if (route === '/longpolling/poll') {
assert.step(route + ' - ' + args.channels.join(','));
pollPromise = testUtils.makeTestPromise();
pollPromise.abort = (function () {
this.reject({message: "XmlHttpRequestError abort"}, $.Event());
}).bind(pollPromise);
return pollPromise;
}
},
});
env.services['bus_service'].addEventListener('notification', ({ detail: notifications }) => {
assert.step('notification - ' + notifications.toString());
});
env.services['bus_service'].addChannel('lambda');
pollPromise.resolve([{
message: 'beta',
}]);
await testUtils.nextTick();
pollPromise.resolve([{
message: 'epsilon',
}]);
await testUtils.nextTick();
assert.verifySteps([
'/longpolling/poll - lambda',
'notification - beta',
'/longpolling/poll - lambda',
'notification - epsilon',
'/longpolling/poll - lambda',
]);
});
QUnit.test('longpolling restarts when connection is lost', async function (assert) {
assert.expect(4);
const oldSetTimeout = window.setTimeout;
patchWithCleanup(
window,
{
setTimeout: callback => oldSetTimeout(callback, 0)
},
{ pure: true },
)
let busService;
let rpcCount = 0;
const env = await makeTestEnv({
mockRPC(route) {
if (route === '/longpolling/poll') {
rpcCount++;
assert.step(`polling ${rpcCount}`);
if (rpcCount == 1) {
return new ConnectionLostError();
}
assert.equal(rpcCount, 2, "Should not be called after stopPolling");
busService.stopPolling();
return new ConnectionLostError();
}
},
});
busService = env.services['bus_service'];
busService.startPolling();
// Give longpolling bus a tick to try to restart polling
await nextTick();
assert.verifySteps([
"polling 1",
"polling 2",
]);
});
QUnit.test('provide notification ID of 0 by default', async function (assert) {
// This test is important in order to ensure that we provide the correct
// sentinel value 0 when we are not aware of the last notification ID
// that we have received. We cannot provide an ID of -1, otherwise it
// may likely be handled incorrectly (before this test was written,
// it was providing -1 to the server, which in return sent every stored
// notifications related to this user).
assert.expect(3);
// Simulate no ID of last notification in the local storage
patchWithCleanup(browser, {
getItem(key) {
if (key === 'last_ts') {
return 0;
}
return this._super(...arguments);
},
});
var pollPromise = testUtils.makeTestPromise();
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();
return pollPromise;
}
}
});
// trigger longpolling poll RPC
env.services['bus_service'].addChannel('lambda');
assert.verifySteps(['/longpolling/poll']);
});
QUnit.test('cross tab bus share message from a channel', async function (assert) {
assert.expect(5);
// master
var pollPromiseMaster = testUtils.makeTestPromise();
const masterEnv = await makeTestEnv({
mockRPC(route, args) {
if (route === '/longpolling/poll') {
assert.step('master' + ' - ' + route + ' - ' + args.channels.join(','));
pollPromiseMaster = testUtils.makeTestPromise();
return pollPromiseMaster;
}
}
});
masterEnv.services['bus_service'].addEventListener('notification', ({ detail: notifications }) => {
assert.step('master - notification - ' + notifications.toString());
});
masterEnv.services['bus_service'].addChannel('lambda');
// slave
const slaveEnv = await makeTestEnv({
mockRPC(route, args) {
if (route === '/longpolling/poll') {
throw new Error("Can not use the longpolling of the slave client");
}
}
});
slaveEnv.services['bus_service'].addEventListener('notification', ({ detail: notifications }) => {
assert.step('slave - notification - ' + notifications.toString());
});
slaveEnv.services['bus_service'].addChannel('lambda');
pollPromiseMaster.resolve([{
id: 1,
channel: 'lambda',
message: 'beta',
}]);
await testUtils.nextTick();
assert.verifySteps([
'master - /longpolling/poll - lambda',
'master - notification - beta',
'slave - notification - beta',
'master - /longpolling/poll - lambda',
]);
});
QUnit.test('multi tab service elects new master on master unload', async function (assert) {
assert.expect(8);
// master
var pollPromiseMaster = testUtils.makeTestPromise();
const masterEnv = await makeTestEnv({
mockRPC(route, args) {
if (route === '/longpolling/poll') {
assert.step('master - ' + route + ' - ' + args.channels.join(','));
pollPromiseMaster = testUtils.makeTestPromise();
return pollPromiseMaster;
}
}
});
masterEnv.services['bus_service'].addEventListener('notification', ({ detail: notifications }) => {
assert.step('master - notification - ' + notifications.toString());
});
masterEnv.services['bus_service'].addChannel('lambda');
// slave
var pollPromiseSlave = testUtils.makeTestPromise();
// prevent slave tab from receiving unload event.
patchWithCleanup(browser, {
addEventListener(eventName, callback) {
if (eventName === 'unload') {
return;
}
this._super(eventName, callback);
},
});
const slaveEnv = await makeTestEnv({
mockRPC(route, args) {
if (route === '/longpolling/poll') {
assert.step('slave - ' + route + ' - ' + args.channels.join(','));
pollPromiseSlave = testUtils.makeTestPromise();
return pollPromiseSlave;
}
}
});
slaveEnv.services['bus_service'].addEventListener('notification', ({ detail: notifications }) => {
assert.step('slave - notification - ' + notifications.toString());
});
slaveEnv.services['bus_service'].addChannel('lambda');
pollPromiseMaster.resolve([{
id: 1,
channel: 'lambda',
message: 'beta',
}]);
await testUtils.nextTick();
// simulate unloading master
window.dispatchEvent(new Event('unload'));
pollPromiseSlave.resolve([{
id: 2,
channel: 'lambda',
message: 'gamma',
}]);
await testUtils.nextTick();
assert.verifySteps([
'master - /longpolling/poll - lambda',
'master - notification - beta',
'slave - notification - beta',
'master - /longpolling/poll - lambda',
'slave - /longpolling/poll - lambda',
'slave - notification - gamma',
'slave - /longpolling/poll - lambda',
]);
});
QUnit.test('two tabs calling addChannel simultaneously', async function (assert) {
assert.expect(5);
patchWithCleanup(CrossTab.prototype, {
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);
},
});
let pollPromise;
const firstTabEnv = await makeTestEnv({
mockRPC(route) {
if (route === '/longpolling/poll') {
pollPromise = testUtils.makeTestPromise();
return pollPromise;
}
}
});
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",
"Tab 2: addChannel alpha",
"Tab 1: addChannel beta",
"Tab 2: addChannel beta",
]);
});
QUnit.test('two tabs adding a different channel', async function (assert) {
const firstTabEnv = await makeTestEnv({
mockRPC: function (route, args) {
if (route === '/longpolling/poll') {
assert.step(args.channels.join());
return testUtils.makeTestPromise();
}
}
});
const secondTabEnv = await makeTestEnv({
mockRPC: function (route, args) {
if (route === '/longpolling/poll') {
throw new Error("slave tab should not use the polling route");
}
}
});
firstTabEnv.services['bus_service'].addChannel("alpha");
await nextTick();
assert.verifySteps(["alpha"]);
secondTabEnv.services['bus_service'].addChannel("beta");
await nextTick();
assert.verifySteps(["alpha,beta"]);
});
});
});