[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:
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user