[IMP] bus: prepare bus tests for websocket introduction

The bus tests rely to heavily on the underlying technology (polling).
In order to keep the test intact in the PR introducing the websockets
in Odoo, let's convert the test to be less technical.

task-2053917

closes odoo/odoo#97975

Related: odoo/enterprise#30360
Signed-off-by: Sébastien Theys (seb) <seb@odoo.com>
This commit is contained in:
tsm-odoo
2022-08-12 23:37:39 +02:00
parent 9d86827840
commit 15eda2d7cf
3 changed files with 130 additions and 167 deletions
+105 -160
View File
@@ -2,13 +2,13 @@ odoo.define('web.bus_tests', function (require) {
"use strict";
var { busService } = require('@bus/services/bus_service');
const { startServer } = require('@bus/../tests/helpers/mock_python_environment');
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');
@@ -30,48 +30,28 @@ QUnit.module('Bus', {
},
}, function () {
QUnit.test('notifications received from the longpolling channel', async function (assert) {
assert.expect(6);
assert.expect(3);
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;
}
},
});
const pyEnv = await startServer();
const env = await makeTestEnv({ activateMockServer: true });
env.services['bus_service'].addEventListener('notification', ({ detail: notifications }) => {
assert.step('notification - ' + notifications.toString());
assert.step('notification - ' + notifications.map(notif => notif.payload).toString());
});
env.services['bus_service'].addChannel('lambda');
pollPromise.resolve([{
message: 'beta',
}]);
pyEnv['bus.bus']._sendone('lambda', 'notifType', 'beta');
await testUtils.nextTick();
pollPromise.resolve([{
message: 'epsilon',
}]);
pyEnv['bus.bus']._sendone('lambda', 'notifType', '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);
QUnit.test('notifications still received after disconnect/reconnect', async function (assert) {
assert.expect(3);
const oldSetTimeout = window.setTimeout;
patchWithCleanup(
@@ -82,30 +62,21 @@ QUnit.module('Bus', {
{ 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();
}
},
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());
});
busService = env.services['bus_service'];
busService.startPolling();
env.services['bus_service'].addChannel('lambda');
pyEnv['bus.bus']._sendone('lambda', 'notifType', 'beta');
pyEnv.simulateConnectionLostAndRecovered();
pyEnv['bus.bus']._sendone('lambda', 'notifType', 'gamma');
// Give longpolling bus a tick to try to restart polling
await nextTick();
assert.verifySteps([
"polling 1",
"polling 2",
"notification - beta",
"notification - gamma",
]);
});
@@ -147,26 +118,16 @@ QUnit.module('Bus', {
assert.verifySteps(['/longpolling/poll']);
});
QUnit.test('cross tab bus share message from a channel', async function (assert) {
assert.expect(5);
QUnit.test('tabs share message from a channel', async function (assert) {
assert.expect(3);
// 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;
}
}
const pyEnv = await startServer();
// main
const mainEnv = await makeTestEnv({ activateMockServer: true });
mainEnv.services['bus_service'].addEventListener('notification', ({ detail: notifications }) => {
assert.step('main - notification - ' + notifications.map(notif => notif.payload).toString());
});
masterEnv.services['bus_service'].addEventListener('notification', ({ detail: notifications }) => {
assert.step('master - notification - ' + notifications.toString());
});
masterEnv.services['bus_service'].addChannel('lambda');
mainEnv.services['bus_service'].addChannel('lambda');
// slave
const slaveEnv = await makeTestEnv({
@@ -177,50 +138,31 @@ QUnit.module('Bus', {
}
});
slaveEnv.services['bus_service'].addEventListener('notification', ({ detail: notifications }) => {
assert.step('slave - notification - ' + notifications.toString());
assert.step('slave - notification - ' + notifications.map(notif => notif.payload).toString());
});
slaveEnv.services['bus_service'].addChannel('lambda');
pollPromiseMaster.resolve([{
id: 1,
channel: 'lambda',
message: 'beta',
}]);
pyEnv['bus.bus']._sendone('lambda', 'notifType', 'beta');
await testUtils.nextTick();
assert.verifySteps([
'master - /longpolling/poll - lambda',
'master - notification - beta',
'main - 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);
QUnit.test('secondEnv start polling after main unload', async function (assert) {
assert.expect(3);
// master
var pollPromiseMaster = testUtils.makeTestPromise();
const masterEnv = await makeTestEnv({
mockRPC(route, args) {
// main tab.
const mainEnv = await makeTestEnv({
mockRPC(route) {
if (route === '/longpolling/poll') {
assert.step('master - ' + route + ' - ' + args.channels.join(','));
pollPromiseMaster = testUtils.makeTestPromise();
return pollPromiseMaster;
assert.step('main - poll');
}
}
});
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.
// prevent second tab from receiving unload event.
patchWithCleanup(browser, {
addEventListener(eventName, callback) {
if (eventName === 'unload') {
@@ -229,45 +171,63 @@ QUnit.module('Bus', {
this._super(eventName, callback);
},
});
const slaveEnv = await makeTestEnv({
mockRPC(route, args) {
// second tab
await makeTestEnv({
mockRPC(route) {
if (route === '/longpolling/poll') {
assert.step('slave - ' + route + ' - ' + args.channels.join(','));
pollPromiseSlave = testUtils.makeTestPromise();
return pollPromiseSlave;
assert.step('second - poll');
}
}
});
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
mainEnv.services['bus_service'].startPolling();
// simulate main unload.
window.dispatchEvent(new Event('unload'));
pollPromiseSlave.resolve([{
id: 2,
channel: 'lambda',
message: 'gamma',
}]);
assert.verifySteps([
'main - poll',
'second - poll',
]);
});
QUnit.test('second tab still receives notifications after main unload', async function (assert) {
assert.expect(4);
const pyEnv = await startServer();
// main
const mainEnv = await makeTestEnv({ activateMockServer: true });
mainEnv.services['bus_service'].addEventListener('notification', ({ detail: notifications }) => {
assert.step('main - notification - ' + notifications.map(notif => notif.payload).toString());
});
mainEnv.services['bus_service'].addChannel('lambda');
// second env
// prevent second tab from receiving unload event.
patchWithCleanup(browser, {
addEventListener(eventName, callback) {
if (eventName === 'unload') {
return;
}
this._super(eventName, callback);
},
});
const secondEnv = await makeTestEnv({ activateMockServer: true });
secondEnv.services['bus_service'].addEventListener('notification', ({ detail: notifications }) => {
assert.step('slave - notification - ' + notifications.map(notif => notif.payload).toString());
});
secondEnv.services['bus_service'].addChannel('lambda');
pyEnv['bus.bus']._sendone('lambda', 'notifType', 'beta');
await testUtils.nextTick();
// simulate unloading main
window.dispatchEvent(new Event('unload'));
pyEnv['bus.bus']._sendone('lambda', 'notifType', 'gamma');
await testUtils.nextTick();
assert.verifySteps([
'master - /longpolling/poll - lambda',
'master - notification - beta',
'main - notification - beta',
'slave - notification - beta',
'master - /longpolling/poll - lambda',
'slave - /longpolling/poll - lambda',
'slave - notification - gamma',
'slave - /longpolling/poll - lambda',
]);
});
@@ -285,24 +245,8 @@ QUnit.module('Bus', {
},
});
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;
}
}
});
const firstTabEnv = await makeTestEnv({ activateMockServer: true });
const secondTabEnv = await makeTestEnv({ activateMockServer: true });
firstTabEnv.services['bus_service'].__tabId__ = 1;
secondTabEnv.services['bus_service'].__tabId__ = 2;
firstTabEnv.services['bus_service'].addChannel('alpha');
@@ -319,29 +263,30 @@ QUnit.module('Bus', {
});
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");
}
}
});
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");
await nextTick();
assert.verifySteps(["alpha"]);
secondTabEnv.services['bus_service'].addChannel("beta");
await nextTick();
assert.verifySteps(["alpha,beta"]);
pyEnv['bus.bus']._sendmany([
['alpha', 'notifType', 'alpha'],
['beta', 'notifType', 'beta']
]);
await nextTick();
assert.verifySteps([
'first - notification - alpha,beta',
'second - notification - alpha,beta',
]);
});
});
@@ -74,9 +74,7 @@ let pyEnv;
* Creates an environment that can be used to setup test data as well as
* creating data after test start.
*
* @param {Object} serverData serverData to pass to the mockServer,
* models and records are ignored: models should be fetched and records
* should be created with the environment returned by this method.
* @param {Object} serverData serverData to pass to the mockServer.
* @param {Object} [serverData.action] actions to be passed to the mock
* server.
* @param {Object} [serverData.views] views to be passed to the mock
@@ -106,16 +104,19 @@ let pyEnv;
}
pyEnv = new Proxy(
{
get currentPartner() {
return this.mockServer.currentPartner;
},
getData() {
return this.mockServer.models;
},
getViews() {
return views;
},
...TEST_USER_IDS,
get currentPartner() {
return this.mockServer.currentPartner;
simulateConnectionLostAndRecovered() {
this.mockServer._simulateConnectionLostAndRecovered();
},
...TEST_USER_IDS,
},
{
get(target, name) {
+18 -1
View File
@@ -3,6 +3,7 @@
import { TEST_USER_IDS } from "@bus/../tests/helpers/test_constants";
import { patch } from "@web/core/utils/patch";
import { ConnectionLostError } from "@web/core/network/rpc_service";
import { MockServer } from "@web/../tests/helpers/mock_server";
import { makeDeferred } from "@web/../tests/helpers/utils";
@@ -24,7 +25,10 @@ patch(MockServer.prototype, 'bus', {
async _performRPC(route, args) {
if (route === '/longpolling/poll') {
const longpollingPromise = makeDeferred();
if (this.notificationsToBeResolved.length) {
if (this.hasLostConnection) {
longpollingPromise.reject(new ConnectionLostError());
this.hasLostConnection = false;
} else if (this.notificationsToBeResolved.length) {
longpollingPromise.resolve(this.notificationsToBeResolved);
this.notificationsToBeResolved = [];
} else {
@@ -62,4 +66,17 @@ patch(MockServer.prototype, 'bus', {
this.notificationsToBeResolved.push(...values);
}
},
/**
* Simulate the lost of the connection by rejecting the pending
* promise with ConnectionLostError. If there is no pending promise,
* reject the next one.
*/
_simulateConnectionLostAndRecovered() {
if (this.pendingLongpollingPromise) {
this.pendingLongpollingPromise.reject(new ConnectionLostError());
} else {
this.hasLostConnection = true;
}
},
});