Files
odoo_source/addons/mail/static/tests/discuss_tests.js
T
Christophe Matthieu 6448420c5d [IMP] bus: re-factoring of bus.bus (Longpolling and CrossTab)
The purpose of this change is to make the code clearer and testable.

In this change, the 'tab_manager' static object was merged with the bus
cross tab.

Cleaning was done to clearly define private and public functions as well
as handlers. The methods are documented and the constants are now defined
on the class. The bus use the service behavior with 'trigger_up'.

'bus.CrossTab' who extend 'bus.Longpolling' are instantiated by the
bus service.

The class is always instantiated with a parent, or root in the case of the website
(im_livechat), to use the ajax and localstorage services. So the behavior, perhaps
logger or redefined by the parents.
2018-08-09 02:31:59 +02:00

697 lines
24 KiB
JavaScript

odoo.define('mail.discuss_test', function (require) {
"use strict";
var Discuss = require('mail.Discuss');
var mailTestUtils = require('mail.testUtils');
var concurrency = require('web.concurrency');
var SearchView = require('web.SearchView');
var testUtils = require('web.test_utils');
var createDiscuss = mailTestUtils.createDiscuss;
QUnit.module('mail', {}, function () {
QUnit.module('Discuss', {
beforeEach: function () {
// patch _.debounce and _.throttle to be fast and synchronous
this.underscoreDebounce = _.debounce;
this.underscoreThrottle = _.throttle;
_.debounce = _.identity;
_.throttle = _.identity;
this.data = {
'mail.message': {
fields: {
body: {
string: "Contents",
type: 'html',
},
author_id: {
string: "Author",
relation: 'res.partner',
},
channel_ids: {
string: "Channels",
type: 'many2many',
relation: 'mail.channel',
},
starred: {
string: "Starred",
type: 'boolean',
},
needaction: {
string: "Need Action",
type: 'boolean',
},
starred_partner_ids: {
string: "partner ids",
type: 'integer',
},
model: {
string: "Related Document model",
type: 'char',
},
res_id: {
string: "Related Document ID",
type: 'integer',
},
},
},
};
this.services = mailTestUtils.getMailServices();
},
afterEach: function () {
// unpatch _.debounce and _.throttle
_.debounce = this.underscoreDebounce;
_.throttle = this.underscoreThrottle;
}
});
QUnit.test('basic rendering', function (assert) {
assert.expect(5);
var done = assert.async();
createDiscuss({
id: 1,
context: {},
params: {},
data: this.data,
services: this.services,
})
.then(function (discuss) {
// test basic rendering
var $sidebar = discuss.$('.o_mail_discuss_sidebar');
assert.strictEqual($sidebar.length, 1,
"should have rendered a sidebar");
assert.strictEqual(discuss.$('.o_mail_discuss_content').length, 1,
"should have rendered the content");
assert.strictEqual(discuss.$('.o_mail_no_content').length, 1,
"should display no content message");
var $inbox = $sidebar.find('.o_mail_discuss_item[data-thread-id=mailbox_inbox]');
assert.strictEqual($inbox.length, 1,
"should have the channel item 'mailbox_inbox' in the sidebar");
var $starred = $sidebar.find('.o_mail_discuss_item[data-thread-id=mailbox_starred]');
assert.strictEqual($starred.length, 1,
"should have the channel item 'mailbox_starred' in the sidebar");
discuss.destroy();
done();
});
});
QUnit.test('unescape channel name in the sidebar', function (assert) {
// When the user creates a channel, the channel's name is escaped, this in
// order to prevent XSS attacks. However, the user should see visually the
// unescaped name of the channel. For instance, when the user creates a
// channel named "R&D", he should see "R&D" and not "R&D".
assert.expect(2);
var done = assert.async();
this.data.initMessaging = {
channel_slots: {
channel_channel: [{
id: 1,
channel_type: "channel",
name: "R&D",
}],
},
};
createDiscuss({
id: 1,
context: {},
params: {},
data: this.data,
services: this.services,
})
.then(function (discuss) {
var $sidebar = discuss.$('.o_mail_discuss_sidebar');
var $channel = $sidebar.find('.o_mail_discuss_item[data-thread-id=1]');
assert.strictEqual($channel.length, 1,
"should have the channel item for channel 1 in the sidebar");
assert.strictEqual($channel.find('.o_thread_name').text().replace(/\s/g, ''),
"#R&D",
"should have unescaped channel name in the sidebar");
discuss.destroy();
done();
});
});
QUnit.test('@ mention in channel', function (assert) {
assert.expect(34);
var done = assert.async();
var fetchListenersDef = $.Deferred();
var receiveMessageDef = $.Deferred();
this.data.initMessaging = {
channel_slots: {
channel_channel: [{
id: 1,
channel_type: "channel",
name: "general",
}],
},
};
var objectDiscuss;
createDiscuss({
id: 1,
context: {},
params: {},
data: this.data,
services: this.services,
mockRPC: function (route, args) {
if (args.method === 'channel_fetch_listeners') {
fetchListenersDef.resolve();
return $.when([
{id: 1, name: 'Admin'},
{id: 2, name: 'TestUser'},
{id: 3, name: 'DemoUser'}
]);
}
if (args.method === 'message_post') {
var data = {
author_id: ["42", "Me"],
body: args.kwargs.body,
channel_ids: [1],
};
var notification = [[false, 'mail.channel'], data];
objectDiscuss.call('bus_service', 'trigger', 'notification', [notification]);
receiveMessageDef.resolve();
return $.when(42);
}
return this._super.apply(this, arguments);
},
})
.then(function (discuss) {
objectDiscuss = discuss;
var $general = discuss.$('.o_mail_discuss_sidebar')
.find('.o_mail_discuss_item[data-thread-id=1]');
assert.strictEqual($general.length, 1,
"should have the channel item with id 1");
assert.strictEqual($general.attr('title'), 'general',
"should have the title 'general'");
// click on general
$general.click();
var $input = discuss.$('textarea.o_composer_text_field').first();
assert.ok($input.length, "should display a composer input");
// Simulate '@' typed by user with mocked Window.getSelection
// Note: focus is needed in order to trigger rpc 'channel_fetch_listeners'
$input.focus();
$input.val("@");
$input.trigger('keyup');
fetchListenersDef
.then(concurrency.delay.bind(concurrency, 0))
.then(function () {
assert.strictEqual(discuss.$('.dropup.o_composer_mention_dropdown.show').length, 1,
"dropup menu for partner mentions should be open");
var $mentionPropositions = discuss.$('.o_mention_proposition');
assert.strictEqual($mentionPropositions.length, 3,
"should display 3 partner mention propositions");
var $mention1 = $mentionPropositions.first();
var $mention2 = $mentionPropositions.first().next();
var $mention3 = $mentionPropositions.first().next().next();
// correct 1st mention proposition
assert.ok($mention1.hasClass('active'),
"first partner mention should be active");
assert.strictEqual($mention1.data('id'), 1,
"first partner mention should link to the correct partner id");
assert.strictEqual($mention1.find('.o_mention_name').text(), "Admin",
"first partner mention should display the correct partner name");
// correct 2nd mention proposition
assert.notOk($mention2.hasClass('active'),
"second partner mention should not be active");
assert.strictEqual($mention2.data('id'), 2,
"second partner mention should link to the correct partner id");
assert.strictEqual($mention2.find('.o_mention_name').text(), "TestUser",
"second partner mention should display the correct partner name");
// correct 3rd mention proposition
assert.notOk($mention3.hasClass('active'),
"third partner mention should not be active");
assert.strictEqual($mention3.data('id'), 3,
"third partner mention should link to the correct partner id");
assert.strictEqual($mention3.find('.o_mention_name').text(), "DemoUser",
"third partner mention should display the correct partner name");
// check DOWN event
$input.trigger($.Event('keyup', {which: $.ui.keyCode.DOWN}));
assert.notOk($mention1.hasClass('active'),
"first partner mention should not be active");
assert.ok($mention2.hasClass('active'),
"second partner mention should be active");
assert.notOk($mention3.hasClass('active'),
"third partner mention should not be active");
// check UP event
$input.trigger($.Event('keyup', {which: $.ui.keyCode.UP}));
assert.ok($mention1.hasClass('active'),
"first partner mention should be active");
assert.notOk($mention2.hasClass('active'),
"second partner mention should not be active");
assert.notOk($mention3.hasClass('active'),
"third partner mention should not be active");
// check TAB event (full cycle, hence 3 TABs)
$input.trigger($.Event('keyup', {which: $.ui.keyCode.TAB}));
assert.notOk($mention1.hasClass('active'),
"first partner mention should not be active");
assert.ok($mention2.hasClass('active'),
"second partner mention should be active");
assert.notOk($mention3.hasClass('active'),
"third partner mention should not be active");
$input.trigger($.Event('keyup', {which: $.ui.keyCode.TAB}));
assert.notOk($mention1.hasClass('active'),
"first partner mention should not be active");
assert.notOk($mention2.hasClass('active'),
"second partner mention should not be active");
assert.ok($mention3.hasClass('active'),
"third partner mention should be active");
$input.trigger($.Event('keyup', {which: $.ui.keyCode.TAB}));
assert.ok($mention1.hasClass('active'),
"first partner mention should be active");
assert.notOk($mention2.hasClass('active'),
"second partner mention should not be active");
assert.notOk($mention3.hasClass('active'),
"third partner mention should not be active");
// equivalent to $mentionPropositions.find('active').click();
$input.trigger($.Event('keyup', {which: $.ui.keyCode.ENTER}));
assert.strictEqual(discuss.$('.o_mention_proposition').length, 0,
"should not have any partner mention proposition after ENTER");
assert.strictEqual($input.val().trim() , "@Admin",
"should have the correct mention link in the composer input");
// send message
$input.trigger($.Event('keydown', {which: $.ui.keyCode.ENTER}));
receiveMessageDef
.then(concurrency.delay.bind(concurrency, 0))
.then(function () {
assert.strictEqual(discuss.$('.o_thread_message_content').length, 1,
"should display one message with some content");
assert.strictEqual(discuss.$('.o_thread_message_content a').length, 1,
"should contain a link in the message content");
assert.strictEqual(discuss.$('.o_thread_message_content a').text(),
"@Admin", "should have correct mention link in the message content");
// Restore window.getSelection
discuss.destroy();
done();
});
});
});
});
QUnit.test('no crash focusout emoji button', function (assert) {
assert.expect(3);
var done = assert.async();
this.data.initMessaging = {
channel_slots: {
channel_channel: [{
id: 1,
channel_type: "channel",
name: "general",
}],
},
};
createDiscuss({
id: 1,
context: {},
params: {},
data: this.data,
services: this.services,
})
.then(function (discuss) {
var $general = discuss.$('.o_mail_discuss_sidebar')
.find('.o_mail_discuss_item[data-thread-id=1]');
assert.strictEqual($general.length, 1,
"should have the channel item with id 1");
assert.strictEqual($general.attr('title'), 'general',
"should have the title 'general'");
// click on general
$general.click();
discuss.$('.o_composer_button_emoji').focus();
try {
discuss.$('.o_composer_button_emoji').focusout();
assert.ok(true, "should not crash on focusout of emoji button");
} finally {
discuss.destroy();
done();
}
});
});
QUnit.test('older messages are loaded on scroll', function (assert) {
assert.expect(3);
var done = assert.async();
var fetchCount = 0;
var loadMoreDef = $.Deferred();
var messageData = [];
for (var i = 0; i < 35; i++) {
messageData.push({
author_id: ['1', 'Me'],
body: '<p>test ' + i + '</p>',
channel_ids: [1],
id: i,
});
}
this.data.initMessaging = {
channel_slots: {
channel_channel: [{
id: 1,
channel_type: "channel",
name: "general",
static: true,
}],
},
};
this.data['mail.message'].records = messageData;
createDiscuss({
context: {},
data: this.data,
params: {},
services: this.services,
mockRPC: function (route, args) {
if (args.method === 'message_fetch') {
fetchCount++;
// 1st fetch: inbox initial fetch
// 2nd fetch: general initial fetch
// 3rd fetch: general load more
if (fetchCount === 3) {
loadMoreDef.resolve();
}
}
return this._super.apply(this, arguments);
},
}).then(function (discuss) {
var $general = discuss.$('.o_mail_discuss_item[data-thread-id=1]');
assert.strictEqual($general.length, 1,
"should have a channel item with id 1");
// switch to 'general'
$general.click();
assert.strictEqual(discuss.$('.o_thread_message').length, 30,
"should display the 30 messages");
// simulate a scroll to top to load more messages
discuss.$('.o_mail_thread').scrollTop(0);
loadMoreDef
.then(concurrency.delay.bind(concurrency, 0))
.then(function () {
assert.strictEqual(discuss.$('.o_thread_message').length, 35,
"all messages should now be loaded");
discuss.destroy();
done();
});
});
});
QUnit.test('"Unstar all" button should reset the starred counter', function (assert) {
assert.expect(2);
var done = assert.async();
var messageData = [];
_.each(_.range(1, 41), function (num) {
messageData.push({
id: num,
body: "<p>test" + num + "</p>",
author_id: ["1", "Me"],
channel_ids: [1],
starred: true,
starred_partner_ids: [1],
}
);
});
this.data.initMessaging = {
channel_slots: {
channel_channel: [{
id: 1,
channel_type: "channel",
name: "general",
}],
},
starred_counter: messageData.length,
};
this.data['mail.message'].records = messageData;
var objectDiscuss;
createDiscuss({
id: 1,
context: {},
params: {},
data: this.data,
services: this.services,
mockRPC: function (route, args) {
if (args.method === 'unstar_all') {
var data = {
message_ids: _.range(1, 41),
starred: false,
type: 'toggle_star',
};
var notification = [[false, 'res.partner'], data];
objectDiscuss.call('bus_service', 'trigger', 'notification', [notification]);
return $.when(42);
}
return this._super.apply(this, arguments);
},
session: {partner_id: 1},
})
.then(function (discuss) {
objectDiscuss = discuss;
var $starred = discuss.$('.o_mail_discuss_sidebar').find('.o_mail_mailbox_title_starred');
var $starredCounter = $('.o_mail_mailbox_title_starred > .o_mail_sidebar_needaction');
// Go to Starred channel
$starred.click();
// Test Initial Value
assert.strictEqual($starredCounter.text().trim(), "40", "40 messages should be starred");
// Unstar all and wait 'update_starred'
$('.o_control_panel .o_mail_discuss_button_unstar_all').click();
$starredCounter = $('.o_mail_mailbox_title_starred > .o_mail_sidebar_needaction');
assert.strictEqual($starredCounter.text().trim(), "0",
"All messages should be unstarred");
discuss.destroy();
done();
});
});
QUnit.test('do not crash when destroyed before start is completed', function (assert) {
assert.expect(3);
var discuss;
testUtils.patch(Discuss, {
init: function () {
discuss = this;
this._super.apply(this, arguments);
},
});
createDiscuss({
id: 1,
context: {},
params: {},
data: this.data,
services: this.services,
mockRPC: function (route, args) {
if (args.method) {
assert.step(args.method);
}
var result = this._super.apply(this, arguments);
if (args.method === 'message_fetch') {
discuss.destroy();
}
return result;
},
});
assert.verifySteps([
"load_views",
"message_fetch"
]);
testUtils.unpatch(Discuss);
});
QUnit.test('do not crash when destroyed between start en end of _renderSearchView', function (assert) {
assert.expect(2);
var discuss;
testUtils.patch(Discuss, {
init: function () {
discuss = this;
this._super.apply(this, arguments);
},
});
var def = $.Deferred();
testUtils.patch(SearchView, {
willStart: function () {
var result = this._super.apply(this, arguments);
return def.then($.when(result));
},
});
createDiscuss({
id: 1,
context: {},
params: {},
data: this.data,
services: this.services,
mockRPC: function (route, args) {
if (args.method) {
assert.step(args.method);
}
return this._super.apply(this, arguments);
},
});
discuss.destroy();
def.resolve();
assert.verifySteps([
"load_views",
]);
testUtils.unpatch(Discuss);
testUtils.unpatch(SearchView);
});
QUnit.test('confirm dialog when administrator leave (not chat) channel', function (assert) {
assert.expect(2);
var done = assert.async();
this.data.initMessaging = {
channel_slots: {
channel_channel: [{
id: 1,
channel_type: "channel",
name: "MyChannel",
create_uid: 3,
}],
},
};
createDiscuss({
id: 1,
context: {},
params: {},
data: this.data,
services: this.services,
session: {
uid: 3,
},
})
.then(function (discuss) {
// Unsubscribe on MyChannel as administrator
discuss.$('.o_mail_partner_unpin').click();
assert.strictEqual($('.modal-dialog').length, 1,
"should display a dialog");
assert.strictEqual($('.modal-body').text(),
"You are the administrator of this channel. Are you sure you want to unsubscribe?",
"Warn user that he will be unsubscribed from channel as admin.");
discuss.destroy();
done();
});
});
QUnit.test('convert emoji sources to unicodes on message_post', function (assert) {
assert.expect(2);
var done = assert.async();
var receiveMessageDef = $.Deferred();
this.data.initMessaging = {
channel_slots: {
channel_channel: [{
id: 1,
channel_type: "channel",
name: "general",
}],
},
};
var objectDiscuss;
createDiscuss({
id: 1,
context: {},
params: {},
data: this.data,
services: this.services,
mockRPC: function (route, args) {
if (args.method === 'message_post') {
assert.strictEqual(args.kwargs.body, "😊 😂",
"message_post data should have all emojis in their unicode representation");
var data = {
author_id: ["42", "Me"],
body: args.kwargs.body,
channel_ids: [1],
};
var notification = [[false, 'mail.channel'], data];
objectDiscuss.call('bus_service', 'trigger', 'notification', [notification]);
receiveMessageDef.resolve();
return $.when(42);
}
return this._super.apply(this, arguments);
},
})
.then(function (discuss) {
objectDiscuss= discuss;
var $general = discuss.$('.o_mail_discuss_sidebar')
.find('.o_mail_discuss_item[data-thread-id=1]');
// click on general
$general.click();
var $input = discuss.$('textarea.o_composer_text_field').first();
$input.focus();
$input.val(":) x'D");
$input.trigger($.Event('keydown', {which: $.ui.keyCode.ENTER}));
receiveMessageDef
.then(concurrency.delay.bind(concurrency, 0))
.then(function () {
assert.strictEqual(discuss.$('.o_thread_message_content').text().replace(/\s/g, ""),
"😊😂",
"New posted message should contain all emojis in their unicode representation");
discuss.destroy();
done();
});
});
});
});
});