diff --git a/addons/im_support/__init__.py b/addons/im_support/__init__.py
index a0fdc10fe11..aa4d0fd63a9 100644
--- a/addons/im_support/__init__.py
+++ b/addons/im_support/__init__.py
@@ -1,2 +1,4 @@
# -*- coding: utf-8 -*-
+
+from . import controllers
from . import models
diff --git a/addons/im_support/controllers/__init__.py b/addons/im_support/controllers/__init__.py
new file mode 100644
index 00000000000..5d4b25db9c0
--- /dev/null
+++ b/addons/im_support/controllers/__init__.py
@@ -0,0 +1,4 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from . import main
diff --git a/addons/im_support/controllers/main.py b/addons/im_support/controllers/main.py
new file mode 100644
index 00000000000..c95e642cdef
--- /dev/null
+++ b/addons/im_support/controllers/main.py
@@ -0,0 +1,12 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import http
+from odoo.http import request
+
+
+class ImSupport(http.Controller):
+
+ @http.route('/im_support/tests', type='http', auth="user")
+ def test_suite(self, mod=None, **kwargs):
+ return request.render('im_support.support_qunit_suite')
diff --git a/addons/im_support/static/tests/helpers/test_utils.js b/addons/im_support/static/tests/helpers/test_utils.js
new file mode 100644
index 00000000000..5f90097a8f1
--- /dev/null
+++ b/addons/im_support/static/tests/helpers/test_utils.js
@@ -0,0 +1,81 @@
+odoo.define('im_support.test_utils', function (require) {
+"use strict";
+
+var supportSession = require('im_support.SupportSession');
+
+var testUtils = require('web.test_utils');
+
+
+/**
+ * Extended version of addMockEnvironment that mocks RPCs done to the Support
+ * server (CORS), and enables a longpolling to the Support server, optionally.
+ *
+ * @param {Widget} widget
+ * @param {Object} params
+ * @param {function} [params.mockSupportRPC]
+ * @param {boolean} [params.enableSupportPoll=false]
+ */
+function addMockSupportEnvironment(widget, params) {
+ // mock CORS RPCs
+ var originalRPC = supportSession.rpc;
+ var defaultMockSupportRPC = function (route, args) {
+ if (route === '/odoo_im_support/get_support_channel') {
+ return $.when({
+ available: true,
+ channel_type: 'livechat',
+ public: 'private',
+ uuid: args.channel_uuid,
+ });
+ }
+ if (route === '/odoo_im_support/fetch_messages') { // fetching history
+ return $.when([]);
+ }
+ if (route === '/odoo_im_support/chat_post') {
+ return $.when();
+ }
+ if (route === '/longpolling/support_poll') {
+ return $.Deferred();
+ }
+ };
+ supportSession.rpc = function (route, args) {
+ var result;
+ if (params.mockSupportRPC) {
+ var _super = this._super;
+ this._super = defaultMockSupportRPC.bind(this, route, args);
+ result = params.mockSupportRPC.apply(this, arguments);
+ this._super = _super;
+ } else {
+ result = defaultMockSupportRPC.apply(this, arguments);
+ }
+ return result;
+ };
+ testUtils.addMockEnvironment(widget, params);
+ var widgetDestroy = widget.destroy;
+ widget.destroy = function () {
+ supportSession.rpc = originalRPC;
+ widgetDestroy.call(this);
+ };
+
+ // intercepts the getItem call to localStorage service for key
+ // 'im_support.poll_timeout', to simulate a pending longpolling connection to
+ // the Support server, such that the initSupport initiates a poll connection
+ if (params.enableSupportPoll) {
+ testUtils.intercept(widget, 'call_service', function (ev) {
+ if (ev.data.service === 'local_storage') {
+ if (ev.data.method === 'getItem' && ev.data.args[0] === 'im_support.poll_timeout') {
+ ev.data.callback(Date.now() + (60 * 1000));
+ // hack to prevent the default mock to handle the request and erase the above value
+ ev.data.callback = function () {};
+ }
+ }
+ }, true);
+ // manually call initSupport (which is supposed to be called at webclient startup)
+ widget.call('chat_manager', 'initSupport');
+ }
+}
+
+return {
+ addMockSupportEnvironment: addMockSupportEnvironment,
+};
+
+});
diff --git a/addons/im_support/static/tests/systray_no_support_tests.js b/addons/im_support/static/tests/systray_no_support_tests.js
new file mode 100644
index 00000000000..f2c9333ca7e
--- /dev/null
+++ b/addons/im_support/static/tests/systray_no_support_tests.js
@@ -0,0 +1,45 @@
+odoo.define('im_support.systray_no_support_tests', function (require) {
+"use strict";
+
+/**
+ * The purpose of these tests is to ensure that im_support doesn't have an impact
+ * on the webclient when the support is not available.
+ */
+
+var ChatManager = require('mail.ChatManager');
+var systray = require('mail.systray');
+var mailTestUtils = require('mail.testUtils');
+
+var testUtils = require('web.test_utils');
+
+var createBusService = mailTestUtils.createBusService;
+
+QUnit.module('im_support', {}, function () {
+
+QUnit.module('systray', {
+ beforeEach: function () {
+ this.services = [ChatManager, createBusService()];
+ },
+});
+
+QUnit.test('messaging menu does not display the Support channel if not available', function (assert) {
+ // the Support channel should only be displayed if a support_token and a support_origin are
+ // specified in the session, which is not the case for this test
+ assert.expect(1);
+
+ var messagingMenu = new systray.MessagingMenu();
+ testUtils.addMockEnvironment(messagingMenu, {
+ services: this.services,
+ });
+ messagingMenu.appendTo($('#qunit-fixture'));
+
+ messagingMenu.$('.dropdown-toggle').click();
+ assert.strictEqual(messagingMenu.$('.o_mail_channel_preview[data-channel_id=SupportChannel]').length,
+ 0, "should not display the Support channel");
+
+ messagingMenu.destroy();
+});
+
+});
+
+});
diff --git a/addons/im_support/static/tests/systray_tests.js b/addons/im_support/static/tests/systray_tests.js
new file mode 100644
index 00000000000..8a6b02de118
--- /dev/null
+++ b/addons/im_support/static/tests/systray_tests.js
@@ -0,0 +1,320 @@
+odoo.define('im_support.systray_tests', function (require) {
+"use strict";
+
+var supportBus = require('im_support.SupportBus');
+var imSupportTestUtils = require('im_support.test_utils');
+
+var ChatManager = require('mail.ChatManager');
+var ChatWindowManager = require('mail.ChatWindowManager');
+var systray = require('mail.systray');
+var mailTestUtils = require('mail.testUtils');
+
+var testUtils = require('web.test_utils');
+
+var addMockSupportEnvironment = imSupportTestUtils.addMockSupportEnvironment;
+var createBusService = mailTestUtils.createBusService;
+
+QUnit.module('im_support', {}, function () {
+
+QUnit.module('systray', {
+ beforeEach: function () {
+ this.services = [ChatManager, ChatWindowManager, createBusService()];
+ this.supportParams = {
+ db_uuid: 'some_uuid',
+ support_token: 'ABCDEFGHIJ',
+ support_origin: 'something.com',
+ };
+ },
+});
+
+QUnit.test('messaging menu displays the Support channel', function (assert) {
+ assert.expect(1);
+
+ var messagingMenu = new systray.MessagingMenu();
+ addMockSupportEnvironment(messagingMenu, {
+ services: this.services,
+ session: this.supportParams,
+ });
+ messagingMenu.appendTo($('#qunit-fixture'));
+
+ messagingMenu.$('.dropdown-toggle').click();
+ assert.strictEqual(messagingMenu.$('.o_mail_navbar_dropdown_bottom .o_mail_channel_preview[data-channel_id=SupportChannel]').length,
+ 1, "should display the Support channel");
+
+ messagingMenu.destroy();
+});
+
+QUnit.test('clicking on Support channel: channel not available', function (assert) {
+ assert.expect(9);
+
+ var messagingMenu = new systray.MessagingMenu();
+ addMockSupportEnvironment(messagingMenu, {
+ mockRPC: function (route, args) {
+ assert.step(args.method || route);
+ return this._super.apply(this, arguments);
+ },
+ mockSupportRPC: function (route) {
+ assert.step('cors: ' + route);
+ if (route === '/odoo_im_support/get_support_channel') {
+ return this._super.apply(this, arguments).then(function (supportChannel) {
+ supportChannel.available = false;
+ return supportChannel;
+ });
+ }
+ return this._super.apply(this, arguments);
+ },
+ services: this.services,
+ session: this.supportParams,
+ });
+ messagingMenu.appendTo($('#qunit-fixture'));
+
+ messagingMenu.$('.dropdown-toggle').click();
+ assert.strictEqual(messagingMenu.$('.o_mail_channel_preview[data-channel_id=SupportChannel]').length,
+ 1, "should display the Support channel");
+
+ messagingMenu.$('.o_mail_channel_preview[data-channel_id=SupportChannel]').click();
+ assert.strictEqual($('.o_chat_window').length, 1,
+ "should have open a chat window");
+ assert.strictEqual($('.o_chat_window .o_chat_title').text().trim(), 'Support (offline)',
+ "should display the offline status in the header");
+ assert.strictEqual($('.o_chat_window .o_composer_input').length, 0,
+ "should have no composer");
+
+ assert.verifySteps([
+ '/mail/init_messaging',
+ 'message_fetch',
+ 'cors: /odoo_im_support/get_support_channel',
+ 'cors: /odoo_im_support/fetch_messages',
+ ]);
+
+ messagingMenu.destroy();
+});
+
+QUnit.test('clicking on Support channel: channel available', function (assert) {
+ assert.expect(9);
+
+ var messagingMenu = new systray.MessagingMenu();
+ addMockSupportEnvironment(messagingMenu, {
+ mockRPC: function (route, args) {
+ assert.step(args.method || route);
+ return this._super.apply(this, arguments);
+ },
+ mockSupportRPC: function (route) {
+ if (route.split('/')[1] === 'odoo_im_support') {
+ assert.step('cors: ' + route);
+ }
+ return this._super.apply(this, arguments);
+ },
+ services: this.services,
+ session: this.supportParams,
+ });
+ messagingMenu.appendTo($('#qunit-fixture'));
+
+ messagingMenu.$('.dropdown-toggle').click();
+ assert.strictEqual(messagingMenu.$('.o_mail_channel_preview[data-channel_id=SupportChannel]').length,
+ 1, "should display the Support channel");
+
+ messagingMenu.$('.o_mail_channel_preview[data-channel_id=SupportChannel]').click();
+ assert.strictEqual($('.o_chat_window').length, 1,
+ "should have open a chat window");
+ assert.strictEqual($('.o_chat_window .o_chat_title').text().trim(), 'Support',
+ "should display the correct channel title");
+ assert.strictEqual($('.o_chat_window .o_composer_input').length, 1,
+ "should have a composer");
+
+ assert.verifySteps([
+ '/mail/init_messaging',
+ 'message_fetch',
+ 'cors: /odoo_im_support/get_support_channel',
+ 'cors: /odoo_im_support/fetch_messages',
+ ]);
+
+ messagingMenu.destroy();
+});
+
+QUnit.test('post messages in Support channel', function (assert) {
+ assert.expect(8);
+
+ var messagingMenu = new systray.MessagingMenu();
+ addMockSupportEnvironment(messagingMenu, {
+ mockRPC: function (route, args) {
+ assert.step(args.method || route);
+ return this._super.apply(this, arguments);
+ },
+ mockSupportRPC: function (route) {
+ if (route.split('/')[1] === 'odoo_im_support') {
+ assert.step('cors: ' + route);
+ }
+ return this._super.apply(this, arguments);
+ },
+ services: this.services,
+ session: this.supportParams,
+ });
+ messagingMenu.appendTo($('#qunit-fixture'));
+
+ messagingMenu.$('.dropdown-toggle').click();
+ assert.strictEqual(messagingMenu.$('.o_mail_channel_preview[data-channel_id=SupportChannel]').length,
+ 1, "should display the Support channel");
+
+ messagingMenu.$('.o_mail_channel_preview[data-channel_id=SupportChannel]').click();
+ assert.strictEqual($('.o_chat_window .o_composer_input').length, 1,
+ "should have a composer");
+
+ $('.o_chat_window .o_composer_input .o_input')
+ .val('some message')
+ .trigger($.Event('keydown', {which: $.ui.keyCode.ENTER}));
+
+ assert.verifySteps([
+ '/mail/init_messaging',
+ 'message_fetch',
+ 'cors: /odoo_im_support/get_support_channel',
+ 'cors: /odoo_im_support/fetch_messages',
+ 'cors: /odoo_im_support/chat_post',
+ ]);
+
+ messagingMenu.destroy();
+});
+
+QUnit.test('fold Support channel', function (assert) {
+ assert.expect(11);
+
+ var messagingMenu = new systray.MessagingMenu();
+ addMockSupportEnvironment(messagingMenu, {
+ mockRPC: function (route, args) {
+ assert.step(args.method || route);
+ return this._super.apply(this, arguments);
+ },
+ mockSupportRPC: function (route) {
+ if (route.split('/')[1] === 'odoo_im_support') {
+ assert.step('cors: ' + route);
+ }
+ return this._super.apply(this, arguments);
+ },
+ services: this.services,
+ session: this.supportParams,
+ });
+ testUtils.intercept(messagingMenu, 'call_service', function (ev) {
+ if (ev.data.service === 'local_storage') {
+ assert.step('LocalStorage: ' + ev.data.method + ' ' + ev.data.args);
+ }
+ }, true);
+ messagingMenu.appendTo($('#qunit-fixture'));
+
+ messagingMenu.$('.dropdown-toggle').click();
+ assert.strictEqual(messagingMenu.$('.o_mail_channel_preview[data-channel_id=SupportChannel]').length,
+ 1, "should display the Support channel");
+
+ messagingMenu.$('.o_mail_channel_preview[data-channel_id=SupportChannel]').click();
+ assert.strictEqual($('.o_chat_window').length, 1,
+ "should have open a chat window");
+
+ // fold, re-open and close channel
+ $('.o_chat_window .o_chat_title').click();
+ $('.o_chat_window .o_chat_title').click();
+ $('.o_chat_window .o_chat_window_close').click();
+
+ assert.verifySteps([
+ '/mail/init_messaging',
+ 'message_fetch',
+ 'cors: /odoo_im_support/get_support_channel',
+ 'LocalStorage: setItem im_support.channel_state,open',
+ 'cors: /odoo_im_support/fetch_messages',
+ 'LocalStorage: setItem im_support.channel_state,folded',
+ 'LocalStorage: setItem im_support.channel_state,open',
+ 'LocalStorage: setItem im_support.channel_state,closed',
+ ]);
+
+ messagingMenu.destroy();
+});
+
+QUnit.test('restore Support channel if necessary', function (assert) {
+ assert.expect(5);
+
+ var messagingMenu = new systray.MessagingMenu();
+ addMockSupportEnvironment(messagingMenu, {
+ enableSupportPoll: true,
+ mockRPC: function (route, args) {
+ assert.step(args.method || route);
+ return this._super.apply(this, arguments);
+ },
+ mockSupportRPC: function (route) {
+ if (route.split('/')[1] === 'odoo_im_support') {
+ assert.step('cors: ' + route);
+ }
+ return this._super.apply(this, arguments);
+ },
+ services: this.services,
+ session: this.supportParams,
+ });
+ messagingMenu.appendTo($('#qunit-fixture'));
+
+ assert.strictEqual($('.o_chat_window').length, 1,
+ "should have open a chat window");
+
+ assert.verifySteps([
+ '/mail/init_messaging',
+ 'cors: /odoo_im_support/get_support_channel',
+ 'cors: /odoo_im_support/fetch_messages',
+ ]);
+
+ messagingMenu.destroy();
+});
+
+QUnit.test('receive messages in the Support channel', function (assert) {
+ assert.expect(10);
+
+ var supportChannelID;
+
+ var messagingMenu = new systray.MessagingMenu();
+ addMockSupportEnvironment(messagingMenu, {
+ enableSupportPoll: true,
+ mockRPC: function (route, args) {
+ assert.step(args.method || route);
+ return this._super.apply(this, arguments);
+ },
+ mockSupportRPC: function (route) {
+ if (route.split('/')[1] === 'odoo_im_support') {
+ assert.step('cors: ' + route);
+ }
+ return this._super.apply(this, arguments);
+ },
+ services: this.services,
+ session: this.supportParams,
+ });
+ messagingMenu.appendTo($('#qunit-fixture'));
+
+ assert.strictEqual($('.o_chat_window').length, 1,
+ "should have open a chat window");
+ assert.strictEqual($('.o_chat_window .o_thread_message').length, 0,
+ "there should be no message in the thread");
+
+ // simulate an incoming message on the supportBus
+ var data = {
+ author_id: [42, 'An operator'],
+ body: 'A message',
+ channel_ids: [supportChannelID],
+ };
+ var notification = [[false, 'mail.channel'], data];
+ supportBus.trigger('notification', [notification]);
+
+ assert.strictEqual($('.o_chat_window .o_thread_message').length, 1,
+ "there should be a new message in the thread");
+ assert.strictEqual($('.o_chat_window .o_thread_message .o_thread_author ').text().trim(),
+ 'An operator', "should correctly display the author");
+ assert.strictEqual($('.o_chat_window .o_thread_message .o_thread_message_avatar').attr('src'),
+ '/mail/static/src/img/odoo_o.png', "should have correct avatar");
+ assert.strictEqual($('.o_chat_window .o_thread_message .o_thread_message_content ').text().trim(),
+ 'A message', "message is correct");
+
+ assert.verifySteps([
+ '/mail/init_messaging',
+ 'cors: /odoo_im_support/get_support_channel',
+ 'cors: /odoo_im_support/fetch_messages',
+ ]);
+
+ messagingMenu.destroy();
+});
+
+});
+
+});
diff --git a/addons/im_support/tests/__init__.py b/addons/im_support/tests/__init__.py
new file mode 100644
index 00000000000..b9df3527588
--- /dev/null
+++ b/addons/im_support/tests/__init__.py
@@ -0,0 +1,4 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from . import test_js
diff --git a/addons/im_support/tests/test_js.py b/addons/im_support/tests/test_js.py
new file mode 100644
index 00000000000..0b52ea4a596
--- /dev/null
+++ b/addons/im_support/tests/test_js.py
@@ -0,0 +1,11 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+import odoo.tests
+
+
+@odoo.tests.tagged('post_install', '-at_install')
+class IMSupportSuite(odoo.tests.HttpCase):
+
+ def test_im_support_js(self):
+ self.phantom_js('/im_support/tests?mod=web&failfast', "", "", login='admin', timeout=180)
diff --git a/addons/im_support/views/assets.xml b/addons/im_support/views/assets.xml
index 7322c6e7611..7512577ebbc 100644
--- a/addons/im_support/views/assets.xml
+++ b/addons/im_support/views/assets.xml
@@ -12,5 +12,34 @@
+
+
+
+
+
+
+
+
+
+
+ IM Support Tests
+
+
+
+
+
+
+
+
+
+
+
+
+